diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 4de7c24e99..5e32240b30 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -65,6 +65,10 @@ def is_location_valid(world: World, location: str) -> bool: if location in shop_locations and location not in world.shop_locs: 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: + return False + return True diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 4b9d290fa7..97cd838762 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -1,6 +1,6 @@ import typing from worlds.AutoWorld import World -from Options import Option, Range, Toggle, DeathLink, Choice +from Options import Option, Range, Toggle, DeathLink, Choice, OptionDict from .Items import get_total_time_pieces @@ -72,26 +72,22 @@ class ActRandomizer(Choice): default = 1 +class ActPlando(OptionDict): + """Plando acts onto other acts. For example, \"Train Rush\": \"Alpine Free Roam\"""" + 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 VanillaAlpine(Choice): - """If enabled, force Alpine (and optionally its finale) onto their vanilla locations in act shuffle.""" - display_name = "Vanilla Alpine Skyline" - option_false = 0 - option_true = 1 - option_finale = 2 +class FinaleShuffle(Toggle): + """If enabled, chapter finales will only be shuffled amongst each other in act shuffle.""" default = 0 -class NoFreeRoamFinale(Toggle): - """If enabled, prevent Free Roam acts from being shuffled onto chapter finales.""" - default = 1 - - class LogicDifficulty(Choice): """Choose the difficulty setting for logic.""" display_name = "Logic Difficulty" @@ -221,6 +217,22 @@ class TasksanityCheckCount(Range): default = 18 +class ExcludeTour(Toggle): + """Removes the Tour time rift from the game. This option is recommended if you don't want to deal with + important levels being shuffled onto the Tour time rift, or important items being shuffled onto Tour pages + when your goal is Time's End.""" + display_name = "Exclude Tour Time Rift" + default = 0 + + +class ShipShapeCustomTaskGoal(Range): + """Change the amount of tasks required to complete Ship Shape. This will not affect Cruisin' for a Bruisin'.""" + display_name = "Ship Shape Custom Task Goal" + range_start = 5 + range_end = 30 + default = 18 + + class EnableDLC2(Toggle): """Shuffle content from Nyakuza Metro (Chapter 7) into the game. DO NOT ENABLE THIS OPTION IF YOU DO NOT HAVE NYAKUZA METRO DLC INSTALLED!!!""" @@ -238,7 +250,7 @@ class MetroMinPonCost(Range): class MetroMaxPonCost(Range): """The most expensive an item can be in any Nyakuza Metro shop. Includes ticket booths.""" - display_name = "Metro Shops Minimum Pon Cost" + display_name = "Metro Shops Maximum Pon Cost" range_start = 10 range_end = 800 default = 200 @@ -267,14 +279,6 @@ class BaseballBat(Toggle): default = 0 -class VanillaMetro(Choice): - """Force Nyakuza Metro (and optionally its finale) onto their vanilla locations in act shuffle.""" - display_name = "Vanilla Metro" - option_false = 0 - option_true = 1 - option_finale = 2 - - class ChapterCostIncrement(Range): """Lower values mean chapter costs increase slower. Higher values make the cost differences more steep.""" display_name = "Chapter Cost Increment" @@ -357,7 +361,7 @@ class DWExcludeAnnoyingContracts(Toggle): class DWExcludeAnnoyingBonuses(Toggle): """NOT IMPLEMENTED If Death Wish full completions are shuffled in, exclude particularly tedious Death Wish full completions - from the pool""" + from the pool. DANGER! DISABLE AT YOUR OWN RISK! THIS OPTION WHEN DISABLED CAN CREATE VERY DIFFICULT SEEDS!!!""" display_name = "Exclude Annoying Death Wish Full Completions" default = 1 @@ -459,9 +463,9 @@ ahit_options: typing.Dict[str, type(Option)] = { "EndGoal": EndGoal, "ActRandomizer": ActRandomizer, + "ActPlando": ActPlando, "ShuffleAlpineZiplines": ShuffleAlpineZiplines, - "VanillaAlpine": VanillaAlpine, - "NoFreeRoamFinale": NoFreeRoamFinale, + "FinaleShuffle": FinaleShuffle, "LogicDifficulty": LogicDifficulty, "KnowledgeChecks": KnowledgeChecks, "YarnBalancePercent": YarnBalancePercent, @@ -480,11 +484,12 @@ ahit_options: typing.Dict[str, type(Option)] = { "Tasksanity": Tasksanity, "TasksanityTaskStep": TasksanityTaskStep, "TasksanityCheckCount": TasksanityCheckCount, + "ExcludeTour": ExcludeTour, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, "EnableDeathWish": EnableDeathWish, "EnableDLC2": EnableDLC2, "BaseballBat": BaseballBat, - "VanillaMetro": VanillaMetro, "MetroMinPonCost": MetroMinPonCost, "MetroMaxPonCost": MetroMaxPonCost, "NyakuzaThugMinShopItems": NyakuzaThugMinShopItems, @@ -534,6 +539,8 @@ slot_data_options: typing.Dict[str, type(Option)] = { "Tasksanity": Tasksanity, "TasksanityTaskStep": TasksanityTaskStep, "TasksanityCheckCount": TasksanityCheckCount, + "ShipShapeCustomTaskGoal": ShipShapeCustomTaskGoal, + "ExcludeTour": ExcludeTour, "EnableDeathWish": EnableDeathWish, diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 1473d2f605..be13fa1f22 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -252,6 +252,16 @@ blacklisted_acts = { "Battle of the Birds - Finale A": "Award Ceremony", } +# 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"], + "Time Rift - The Moon": ["Alpine Free Roam", "Nyakuza Free Roam"], + "Time Rift - Dead Bird Studio": ["Alpine Free Roam", "Nyakuza Free Roam"], + "Time Rift - Rumbi Factory": ["Alpine Free Roam"], +} + def create_regions(world: World): w = world @@ -371,7 +381,9 @@ def create_regions(world: World): connect_regions(ac_act3, cruise_ship, "Cruise Ship Entrance RTB", p) create_rift_connections(w, create_region(w, "Time Rift - Balcony")) create_rift_connections(w, create_region(w, "Time Rift - Deep Sea")) - create_rift_connections(w, create_region(w, "Time Rift - Tour")) + + if mw.ExcludeTour[world.player].value == 0: + create_rift_connections(w, create_region(w, "Time Rift - Tour")) if mw.Tasksanity[p].value > 0: create_tasksanity_locations(w) @@ -419,9 +431,52 @@ def create_tasksanity_locations(world: World): ship_shape.locations.append(location) +def is_valid_plando(world: World, region: str) -> bool: + if region in blacklisted_acts.values(): + return False + + if region not in world.multiworld.ActPlando[world.player].keys(): + return False + + act = world.multiworld.ActPlando[world.player].get(region) + if act in blacklisted_acts.values(): + return False + + # Don't allow plando-ing things onto the first act that aren't completable with nothing + is_first_act: bool = act_chapters[region] == get_first_chapter_region(world).name \ + and region in act_entrances.keys() and ("Act 1" in act_entrances[region] or "Free Roam" in act_entrances[region]) + + if is_first_act: + if act_chapters[act] == "Subcon Forest" and world.multiworld.ShuffleSubconPaintings[world.player].value > 0: + return False + + if world.multiworld.UmbrellaLogic[world.player].value > 0 \ + and (act == "Heating Up Mafia Town" or act == "Queen Vanessa's Manor"): + return False + + if act not in guaranteed_first_acts: + return False + + # Don't allow straight up impossible mappings + if region == "The Illness has Spread" and act == "Alpine Free Roam": + return False + + if region == "Rush Hour" and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - Rumbi Factory" and act == "Nyakuza Free Roam": + return False + + if region == "Time Rift - The Owl Express" and act == "Murder on the Owl Express": + return False + + return any(a.name == world.multiworld.ActPlando[world.player].get(region) for a in + world.multiworld.get_regions(world.player)) + + def randomize_act_entrances(world: World): region_list: typing.List[Region] = get_act_regions(world) - world.multiworld.random.shuffle(region_list) + world.random.shuffle(region_list) separate_rifts: bool = bool(world.multiworld.ActRandomizer[world.player].value == 1) @@ -436,12 +491,21 @@ def randomize_act_entrances(world: World): region_list.remove(region) region_list.append(region) - # We want to do these first as well, since they can be blocked from being shuffled onto freeroam for region in region_list.copy(): - if region.name in chapter_finales or region.name == "Cheating the Race": + if region.name in chapter_finales: region_list.remove(region) region_list.append(region) + for region in region_list.copy(): + if region.name in world.multiworld.ActPlando[world.player].keys(): + if is_valid_plando(world, region.name): + region_list.remove(region) + region_list.append(region) + else: + print("Disallowing act plando for", + world.multiworld.player_name[world.player], + "-", region.name, ":", world.multiworld.ActPlando[world.player].get(region.name)) + # Reverse the list, so we can do what we want to do first region_list.reverse() @@ -465,6 +529,9 @@ def randomize_act_entrances(world: World): and "Free Roam" not in act_entrances[region.name]: continue + if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + has_guaranteed = True + i = 0 # Already mapped to something else @@ -481,6 +548,9 @@ def randomize_act_entrances(world: World): if candidate.name not in guaranteed_first_acts: continue + if candidate.name in world.multiworld.ActPlando[world.player].values(): + continue + # Not completable without Umbrella if world.multiworld.UmbrellaLogic[world.player].value > 0 \ and (candidate.name == "Heating Up Mafia Town" or candidate.name == "Queen Vanessa's Manor"): @@ -495,6 +565,12 @@ def randomize_act_entrances(world: World): has_guaranteed = True break + if region.name in world.multiworld.ActPlando[world.player].keys() and is_valid_plando(world, region.name): + candidate_list.clear() + candidate_list.append( + world.multiworld.get_region(world.multiworld.ActPlando[world.player].get(region.name), world.player)) + break + # Already mapped onto something else if candidate in shuffled_list: continue @@ -513,18 +589,11 @@ def randomize_act_entrances(world: World): or region.name not in purple_time_rifts and candidate.name in purple_time_rifts: continue - # Don't map Alpine to its own finale - if region.name == "The Illness has Spread" and candidate.name == "Alpine Free Roam": + if region.name in blacklisted_combos.keys() and candidate.name in blacklisted_combos[region.name]: continue - # Ditto for Metro - if region.name == "Rush Hour" and candidate.name == "Nyakuza Free Roam": - continue - - # CTR entrance and Tour aren't a finale, but have a fuck ton of unlock requirements - if world.multiworld.NoFreeRoamFinale[world.player].value > 0 and "Free Roam" in candidate.name: - if region.name in chapter_finales or region.name == "Cheating the Race" \ - or world.multiworld.EndGoal[world.player].value == 1 and region.name == "Time Rift - Tour": + if world.multiworld.FinaleShuffle[world.player].value > 0 and region.name in chapter_finales: + if candidate.name not in chapter_finales: continue if region.name in rift_access_regions and candidate.name in rift_access_regions[region.name]: @@ -532,8 +601,18 @@ def randomize_act_entrances(world: World): candidate_list.append(candidate) - candidate: Region = candidate_list[world.multiworld.random.randint(0, len(candidate_list)-1)] + candidate: Region + if len(candidate_list) > 0: + candidate = candidate_list[world.multiworld.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: + if c not in shuffled_list: + candidate = c + break + shuffled_list.append(candidate) + # print(region, candidate) # Vanilla if candidate.name == region.name: @@ -588,21 +667,17 @@ def get_act_regions(world: World) -> typing.List[Region]: def is_act_blacklisted(world: World, name: str) -> bool: + plando: bool = name in world.multiworld.ActPlando[world.player].keys() \ + or name in world.multiworld.ActPlando[world.player].values() + if name == "The Finale": - return world.multiworld.EndGoal[world.player].value == 1 - - if name == "Alpine Free Roam": - return world.multiworld.VanillaAlpine[world.player].value > 0 - - if name == "The Illness has Spread": - return world.multiworld.VanillaAlpine[world.player].value == 2 - - if name == "Nyakuza Free Roam": - return world.multiworld.VanillaMetro[world.player].value > 0 + return not plando and world.multiworld.EndGoal[world.player].value == 1 if name == "Rush Hour": - return world.multiworld.EndGoal[world.player].value == 2 \ - or world.multiworld.VanillaMetro[world.player].value == 2 + return not plando and world.multiworld.EndGoal[world.player].value == 2 + + if name == "Time Rift - Tour": + return world.multiworld.ExcludeTour[world.player].value > 0 return name in blacklisted_acts.values() diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index eb08c2a0c3..7fc5bf93e7 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -291,7 +291,7 @@ def set_rules(world: World): if data.hit_requirement > 0: if data.hit_requirement == 1: add_rule(location, lambda state: can_hit(state, world)) - else: # Can bypass with Dweller Mask (dweller bells) + 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)) if get_difficulty(world) >= 1: