From dead6ebb0d2b017d7e7d1a0cd4d80b6c8b314764 Mon Sep 17 00:00:00 2001 From: CookieCat Date: Thu, 16 May 2024 22:29:59 -0400 Subject: [PATCH] change act shuffle starting acts + logic updates --- worlds/ahit/Options.py | 10 +-- worlds/ahit/Regions.py | 184 +++++++++++++++++++++++++++++++---------- worlds/ahit/Rules.py | 17 ++-- 3 files changed, 156 insertions(+), 55 deletions(-) diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 87b3768c00..f7c6ba953d 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -45,30 +45,30 @@ def adjust_options(world: "HatInTimeWorld"): if world.is_dlc1() and world.options.ShipShapeCustomTaskGoal <= 0: # automatically determine task count based on Tasksanity settings if world.options.Tasksanity: - world.options.ShipShapeCustomTaskGoal = world.options.TasksanityCheckCount * world.options.TasksanityTaskStep + world.options.ShipShapeCustomTaskGoal.value = world.options.TasksanityCheckCount * world.options.TasksanityTaskStep else: world.options.ShipShapeCustomTaskGoal.value = 18 # Don't allow Rush Hour goal if DLC2 content is disabled if world.options.EndGoal == EndGoal.option_rush_hour and not world.options.EnableDLC2: - world.options.EndGoal.value = 1 + world.options.EndGoal.value = EndGoal.option_finale # Don't allow Seal the Deal goal if Death Wish content is disabled if world.options.EndGoal == EndGoal.option_seal_the_deal and not world.is_dw(): - world.options.EndGoal.value = 1 + world.options.EndGoal.value = EndGoal.option_finale if world.options.DWEnableBonus: world.options.DWAutoCompleteBonuses.value = 0 if world.is_dw_only(): - world.options.EndGoal.value = 3 + world.options.EndGoal.value = EndGoal.option_seal_the_deal world.options.ActRandomizer.value = 0 world.options.ShuffleAlpineZiplines.value = 0 world.options.ShuffleSubconPaintings.value = 0 world.options.ShuffleStorybookPages.value = 0 world.options.ShuffleActContracts.value = 0 world.options.EnableDLC1.value = 0 - world.options.LogicDifficulty.value = -1 + world.options.LogicDifficulty.value = LogicDifficulty.option_normal world.options.DWTimePieceRequirement.value = 0 diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 25c9e86031..2cdeab2e81 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -1,4 +1,4 @@ -from BaseClasses import Region, Entrance, ItemClassification, Location +from BaseClasses import Region, Entrance, ItemClassification, Location, LocationProgressType from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard @@ -10,6 +10,9 @@ if TYPE_CHECKING: from . import HatInTimeWorld +MIN_FIRST_SPHERE_LOCATIONS = 30 + + # ChapterIndex: region chapter_regions = { ChapterIndex.SPACESHIP: "Spaceship", @@ -217,17 +220,32 @@ chapter_act_info = { "Time Rift - Rumbi Factory": "Metro_CaveRift_RumbiFactory" } -# Guarantee that the first level a player can access is a location dense area beatable with no items +# Some of these may vary depending on options. See is_valid_first_act() guaranteed_first_acts = [ "Welcome to Mafia Town", "Barrel Battle", "She Came from Outer Space", "Down with the Mafia!", - "Heating Up Mafia Town", # Removed in umbrella logic + "Heating Up Mafia Town", "The Golden Vault", - "Contractual Obligations", # Removed in painting logic - "Queen Vanessa's Manor", # Removed in umbrella/painting logic + "Dead Bird Studio", + "Murder on the Owl Express", + "Dead Bird Studio Basement", + + "Contractual Obligations", + "The Subcon Well", + "Queen Vanessa's Manor", + "Your Contract has Expired", + + "Rock the Boat", + + "Time Rift - Mafia of Cooks", + "Time Rift - Dead Bird Studio", + "Time Rift - Sleepy Subcon", + "Time Rift - Alpine Skyline" + "Time Rift - Tour", + "Time Rift - Rumbi Factory", ] purple_time_rifts = [ @@ -482,29 +500,61 @@ def randomize_act_entrances(world: "HatInTimeWorld"): f"\"{name1}: {name2}\" " f"is an invalid or disallowed act plando combination!") - first_act_mapped: bool = False + # Decide what should be on the first few levels before randomizing the rest + first_acts: List[Region] = [] + first_chapter_name = chapter_regions[ChapterIndex(world.options.StartingChapter)] + first_acts.append(get_act_by_number(world, first_chapter_name, 1)) + # Chapter 3 and 4 only have one level accessible at the start + if first_chapter_name == "Mafia Town" or first_chapter_name == "Battle of the Birds": + first_acts.append(get_act_by_number(world, first_chapter_name, 2)) + first_acts.append(get_act_by_number(world, first_chapter_name, 3)) + + valid_first_acts: List[Region] = [] + for candidate in candidate_list: + if is_valid_first_act(world, candidate): + valid_first_acts.append(candidate) + + total_locations = 0 + for level in first_acts: + if level not in region_list: # make sure it hasn't been plando'd + continue + + candidate = valid_first_acts[world.random.randint(0, len(valid_first_acts)-1)] + region_list.remove(level) + candidate_list.remove(candidate) + valid_first_acts.remove(candidate) + connect_acts(world, level, candidate, rift_dict) + + # Only allow one purple rift + if candidate.name in purple_time_rifts: + for act in reversed(valid_first_acts): + if act.name in purple_time_rifts: + valid_first_acts.remove(act) + + total_locations += get_region_location_count(world, candidate.name) + if "Time Rift" not in candidate.name: + chapter = act_chapters.get(candidate.name) + if chapter == "Mafia Town": + total_locations += get_region_location_count(world, "Mafia Town Area (HUMT)") + if candidate.name != "Heating Up Mafia Town": + total_locations += get_region_location_count(world, "Mafia Town Area") + elif chapter == "Subcon Forest": + total_locations += get_region_location_count(world, "Subcon Forest Area") + elif chapter == "The Arctic Cruise": + total_locations += get_region_location_count(world, "Cruise Ship") + + # If we have enough Sphere 1 locations, we can allow the rest to be randomized + if total_locations >= MIN_FIRST_SPHERE_LOCATIONS: + break + ignore_certain_rules: bool = False while len(region_list) > 0: - region: Region - if not first_act_mapped: - region = get_first_act(world) - else: - region = region_list[0] - + region = region_list[0] candidate: Region valid_candidates: List[Region] = [] # Look for candidates to map this act to for c in candidate_list: - # Map the first act before anything - if not first_act_mapped: - if not is_valid_first_act(world, c): - continue - - valid_candidates.append(c) - first_act_mapped = True - break # we can stop here, as we only need one - if is_valid_act_combo(world, region, c, ignore_certain_rules): valid_candidates.append(c) @@ -545,7 +595,7 @@ def sort_acts(act: Region) -> int: and "Time Rift" not in act.name: return -3 - if act.name == "Contractual Obligations": + if act.name == "Contractual Obligations" or act.name == "The Subcon Well": return -2 world = act.multiworld.worlds[act.player] @@ -558,17 +608,6 @@ def sort_acts(act: Region) -> int: return 0 -def get_first_act(world: "HatInTimeWorld") -> Region: - first_chapter = get_first_chapter_region(world) - act: Optional[Region] = None - for e in first_chapter.exits: - if "Act 1" in e.name or "Free Roam" in e.name: - act = e.connected_region - break - - return act - - def connect_acts(world: "HatInTimeWorld", entrance_act: Region, exit_act: Region, rift_dict: Dict[str, Region]): # Vanilla if exit_act.name == entrance_act.name: @@ -642,32 +681,66 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool: if act.name not in guaranteed_first_acts: return False - # Not completable without Umbrella - if world.options.UmbrellaLogic and (act.name == "Heating Up Mafia Town" or act.name == "Queen Vanessa's Manor"): + # If there's only a single level in the starting chapter, only allow Mafia Town or Subcon Forest levels + start_chapter = world.options.StartingChapter + if start_chapter is ChapterIndex.ALPINE or start_chapter is ChapterIndex.SUBCON: + if "Time Rift" in act.name: + return False + + if act_chapters[act.name] != "Mafia Town" and act_chapters[act.name] != "Subcon Forest": + return False + + if act.name in purple_time_rifts and not world.options.ShuffleStorybookPages: return False - # Subcon sphere 1 is too small without painting unlocks, and no acts are completable either - if world.options.ShuffleSubconPaintings and "Subcon Forest" in act_entrances[act.name]: + diff = get_difficulty(world) + # Not completable without Umbrella? + if world.options.UmbrellaLogic: + # Needs to be at least moderate to cross the big dweller wall + if act.name == "Queen Vanessa's Manor" and diff < Difficulty.MODERATE: + return False + elif act.name == "Your Contract has Expired" and diff < Difficulty.EXPERT: # Snatcher Hover + return False + elif act.name == "Heating Up Mafia Town": # Straight up impossible + return False + + if act.name == "Dead Bird Studio": + # No umbrella logic = moderate, umbrella logic = expert. + if diff < Difficulty.MODERATE or world.options.UmbrellaLogic and diff < Difficulty.EXPERT: + return False + elif act.name == "Dead Bird Studio Basement" and (diff < Difficulty.EXPERT or world.options.FinaleShuffle): return False + elif act.name == "Rock the Boat" and (diff < Difficulty.MODERATE or world.options.FinaleShuffle): + return False + elif act.name == "The Subcon Well" and diff < Difficulty.MODERATE: + return False + elif act.name == "Contractual Obligations" and world.options.ShuffleSubconPaintings: + return False + + if world.options.ShuffleSubconPaintings and "Time Rift" not in act.name and act_chapters[act.name] == "Subcon Forest": + # This requires a cherry hover to enter Subcon + if act.name == "Your Contract has Expired": + if diff < Difficulty.EXPERT or world.options.NoPaintingSkips: + return False + else: + # Only allow Subcon levels if paintings can be skipped + if diff < Difficulty.MODERATE or world.options.NoPaintingSkips: + return False return True def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region): - count: int = len(rift_access_regions[time_rift.name]) - i: int = 1 - while i <= count: + i = 1 + while i <= len(rift_access_regions[time_rift.name]): name = f"{time_rift.name} Portal - Entrance {i}" entrance: Entrance try: entrance = world.multiworld.get_entrance(name, world.player) + reconnect_regions(entrance, entrance.parent_region, exit_region) except KeyError: - if len(time_rift.entrances) > 0: - entrance = time_rift.entrances[i-1] - else: - entrance = time_rift.connect(exit_region, name) + time_rift.connect(exit_region, name) - reconnect_regions(entrance, entrance.parent_region, exit_region) i += 1 @@ -862,6 +935,27 @@ def get_shuffled_region(world: "HatInTimeWorld", region: str) -> str: return name +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) + for loc in region.locations: + if loc.address is not None and (not included_only or loc.progress_type is not LocationProgressType.EXCLUDED): + count += 1 + + return count + + +def get_act_by_number(world: "HatInTimeWorld", chapter_name: str, num: int) -> Region: + chapter = world.multiworld.get_region(chapter_name, world.player) + act: Optional[Region] = None + for e in chapter.exits: + if f"Act {num}" in e.name or num == 1 and "Free Roam" in e.name: + act = e.connected_region + break + + return act + + def create_thug_shops(world: "HatInTimeWorld"): min_items: int = world.options.NyakuzaThugMinShopItems.value max_items: int = world.options.NyakuzaThugMaxShopItems.value diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index d97e6d73b3..71f74b17d7 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -399,6 +399,10 @@ def set_moderate_rules(world: "HatInTimeWorld"): set_rule(world.multiworld.get_location("Subcon Forest - Manor Rooftop", world.player), lambda state: has_paintings(state, world, 1)) + # Moderate: Village Time Rift with nothing IF umbrella logic is off + if not world.options.UmbrellaLogic: + set_rule(world.multiworld.get_location("Act Completion (Time Rift - Village)", world.player), lambda state: True) + # Moderate: get to Birdhouse/Yellow Band Hills without Brewing Hat set_rule(world.multiworld.get_entrance("-> The Birdhouse", world.player), lambda state: can_use_hookshot(state, world)) @@ -478,6 +482,8 @@ def set_hard_rules(world: "HatInTimeWorld"): # No Dweller Mask required set_rule(world.multiworld.get_location("Subcon Forest - Dweller Floating Rocks", world.player), lambda state: has_paintings(state, world, 3)) + set_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), + lambda state: has_paintings(state, world, 3)) # Cherry bridge over boss arena gap (painting still expected) set_rule(world.multiworld.get_location("Subcon Forest - Boss Arena Chest", world.player), @@ -494,9 +500,6 @@ def set_hard_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT) and has_paintings(state, world, 2), "or") - add_rule(world.multiworld.get_location("Subcon Forest - Dweller Platforming Tree B", world.player), - lambda state: has_paintings(state, world, 3) and can_use_hat(state, world, HatType.SPRINT), "or") - add_rule(world.multiworld.get_location("Act Completion (Time Rift - Curly Tail Trail)", world.player), lambda state: can_use_hat(state, world, HatType.SPRINT), "or") @@ -581,8 +584,6 @@ def set_expert_rules(world: "HatInTimeWorld"): # 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 - Dweller Platforming Tree B", world.player), - lambda state: has_paintings(state, world, 3, True)) set_rule(world.multiworld.get_location("Subcon Forest - Magnet Badge Bush", world.player), lambda state: has_paintings(state, world, 3, True)) @@ -601,6 +602,12 @@ def set_expert_rules(world: "HatInTimeWorld"): and state.has("Metro Ticket - Blue", world.player) and state.has("Metro Ticket - Pink", world.player)) + # Expert: Yellow/Green Manhole with nothing using a Boop Clip + set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Manhole)", world.player), + lambda state: True) + set_rule(world.multiworld.get_location("Act Completion (Green Clean Manhole)", world.player), + lambda state: True) + def set_mafia_town_rules(world: "HatInTimeWorld"): add_rule(world.multiworld.get_location("Mafia Town - Behind HQ Chest", world.player),