diff --git a/worlds/ahit/DeathWishLocations.py b/worlds/ahit/DeathWishLocations.py index f3e546f6c9..470e8087c9 100644 --- a/worlds/ahit/DeathWishLocations.py +++ b/worlds/ahit/DeathWishLocations.py @@ -160,9 +160,7 @@ def create_dw_regions(world: World): 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.options.DWTimePieceRequirement.value)) + add_rule(entrance, lambda state: state.has("Time Piece", world.player, world.options.DWTimePieceRequirement.value)) if world.options.DWShuffle.value > 0: dw_list: List[str] = [] @@ -173,8 +171,7 @@ def create_dw_regions(world: World): dw_list.append(name) world.random.shuffle(dw_list) - count = world.random.randint(world.options.DWShuffleCountMin.value, - world.options.DWShuffleCountMax.value) + count = world.random.randint(world.options.DWShuffleCountMin.value, world.options.DWShuffleCountMax.value) dw_shuffle: List[str] = [] total = min(len(dw_list), count) @@ -197,6 +194,7 @@ def create_dw_regions(world: World): if i == 0: connect_regions(dw_map, dw, f"-> {name}", world.player) else: + # noinspection PyUnboundLocalVariable connect_regions(prev_dw, dw, f"{prev_dw.name} -> {name}", world.player) loc_id = death_wishes[name] diff --git a/worlds/ahit/DeathWishRules.py b/worlds/ahit/DeathWishRules.py index 648549cee0..da8d639a92 100644 --- a/worlds/ahit/DeathWishRules.py +++ b/worlds/ahit/DeathWishRules.py @@ -388,7 +388,6 @@ def create_enemy_events(world: World): if area == "Bluefin Tunnel" and not world.is_dlc2(): continue - if world.options.DWShuffle.value > 0 and area in death_wishes.keys() \ and area not in world.get_dw_shuffle(): continue diff --git a/worlds/ahit/Locations.py b/worlds/ahit/Locations.py index 2bc1c27080..6e6e041bc2 100644 --- a/worlds/ahit/Locations.py +++ b/worlds/ahit/Locations.py @@ -3,6 +3,7 @@ from .Types import HatDLC, HatType, LocData, Difficulty from typing import Dict from .Options import TasksanityCheckCount +TASKSANITY_START_ID = 2000300204 def get_total_locations(world: World) -> int: total: int = 0 @@ -96,7 +97,7 @@ def is_location_valid(world: World, location: str) -> bool: 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() + id_start: int = TASKSANITY_START_ID for i in range(TasksanityCheckCount.range_end): names.setdefault(f"Tasksanity Check {i+1}", id_start+i) @@ -107,10 +108,6 @@ def get_location_names() -> Dict[str, int]: return names -def get_tasksanity_start_id() -> int: - return 2000300204 - - ahit_locations = { "Spaceship - Rumbi Abuse": LocData(2000301000, "Spaceship", hit_requirement=1), @@ -284,7 +281,7 @@ ahit_locations = { # Alpine Skyline "Alpine Skyline - Goat Village: Below Hookpoint": LocData(2000334856, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Goat Village: Hidden Branch": LocData(2000334855, "Alpine Skyline Area (TIHS)"), - "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area"), + "Alpine Skyline - Goat Refinery": LocData(2000333635, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Bird Pass Fork": LocData(2000335911, "Alpine Skyline Area (TIHS)"), "Alpine Skyline - Yellow Band Hills": LocData(2000335756, "Alpine Skyline Area (TIHS)", @@ -848,6 +845,7 @@ zero_jumps = { dlc_flags=HatDLC.dlc2_dw), } +# noinspection PyDictDuplicateKeys snatcher_coins = { "Snatcher Coin - Top of HQ": LocData(0, "Down with the Mafia!", dlc_flags=HatDLC.death_wish), "Snatcher Coin - Top of HQ": LocData(0, "Cheating the Race", dlc_flags=HatDLC.death_wish), @@ -900,6 +898,8 @@ event_locs = { "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"), "Birdhouse Cleared": LocData(0, "The Birdhouse", act_event=True), "Lava Cake Cleared": LocData(0, "The Lava Cake", act_event=True), diff --git a/worlds/ahit/Options.py b/worlds/ahit/Options.py index 4e8738b5ce..626b4671c3 100644 --- a/worlds/ahit/Options.py +++ b/worlds/ahit/Options.py @@ -459,6 +459,15 @@ class NyakuzaThugMaxShopItems(Range): default = 4 +class NoTicketSkips(Choice): + """Prevent metro gate skips from being in logic on higher difficulties. + Rush Hour option will only consider the ticket skips for Rush Hour in logic.""" + display_name = "No Ticket Skips" + option_false = 0 + option_true = 1 + option_rush_hour = 2 + + class BaseballBat(Toggle): """Replace the Umbrella with the baseball bat from Nyakuza Metro. DLC2 content does not have to be shuffled for this option but Nyakuza Metro still needs to be installed.""" diff --git a/worlds/ahit/Regions.py b/worlds/ahit/Regions.py index 70f7ede3b7..6c0266bf44 100644 --- a/worlds/ahit/Regions.py +++ b/worlds/ahit/Regions.py @@ -2,7 +2,7 @@ from worlds.AutoWorld import World from BaseClasses import Region, Entrance, ItemClassification, Location from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \ - shop_locations, get_tasksanity_start_id, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard + shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard import typing from .Rules import set_rift_rules, get_difficulty @@ -438,8 +438,8 @@ def create_rift_connections(world: World, region: Region): 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.options.TasksanityCheckCount.value): + id_start: int = TASKSANITY_START_ID + for i in range(world.multiworld.TasksanityCheckCount[world.player].value): location = HatInTimeLocation(world.player, f"Tasksanity Check {i+1}", id_start+i, ship_shape) ship_shape.locations.append(location) @@ -500,12 +500,12 @@ def randomize_act_entrances(world: World): region_list.append(region) for region in region_list.copy(): - if "Time Rift" in region.name: + if region.name in chapter_finales: region_list.remove(region) region_list.append(region) for region in region_list.copy(): - if region.name in chapter_finales: + if "Time Rift" in region.name: region_list.remove(region) region_list.append(region) @@ -630,8 +630,8 @@ def randomize_act_entrances(world: World): candidate = c break + # noinspection PyUnboundLocalVariable shuffled_list.append(candidate) - # print(region, candidate) # Vanilla if candidate.name == region.name: @@ -825,11 +825,8 @@ def get_shuffled_region(self, region: str) -> str: def create_thug_shops(world: World): - min_items: int = min(world.options.NyakuzaThugMinShopItems.value, - world.options.NyakuzaThugMaxShopItems.value) - - max_items: int = max(world.options.NyakuzaThugMaxShopItems.value, - world.options.NyakuzaThugMinShopItems.value) + min_items: int = min(world.options.NyakuzaThugMinShopItems.value, world.options.NyakuzaThugMaxShopItems.value) + max_items: int = max(world.options.NyakuzaThugMaxShopItems.value, world.options.NyakuzaThugMinShopItems.value) count: int = -1 step: int = 0 old_name: str = "" @@ -876,6 +873,7 @@ def create_events(world: World) -> int: if not is_location_valid(world, name): continue + item_name: str = name if world.is_dw(): if name in snatcher_coins.keys(): name = f"{name} ({data.region})" @@ -886,15 +884,15 @@ def create_events(world: World) -> int: if get_difficulty(world) < Difficulty.EXPERT and name in zero_jumps_expert: continue - event: Location = create_event(name, world.multiworld.get_region(data.region, world.player), world) + event: Location = create_event(name, item_name, world.multiworld.get_region(data.region, world.player), world) event.show_in_spoiler = False count += 1 return count -def create_event(name: str, region: Region, world: World) -> Location: +def create_event(name: str, item_name: str, region: Region, world: World) -> Location: event = HatInTimeLocation(world.player, name, None, region) region.locations.append(event) - event.place_locked_item(HatInTimeItem(name, ItemClassification.progression, None, world.player)) + event.place_locked_item(HatInTimeItem(item_name, ItemClassification.progression, None, world.player)) return event diff --git a/worlds/ahit/Rules.py b/worlds/ahit/Rules.py index f971526482..44a815d00d 100644 --- a/worlds/ahit/Rules.py +++ b/worlds/ahit/Rules.py @@ -187,7 +187,6 @@ def set_rules(world: World): lowest_cost: int = world.options.LowestChapterCost.value highest_cost: int = world.options.HighestChapterCost.value - cost_increment: int = world.options.ChapterCostIncrement.value min_difference: int = world.options.ChapterCostMinDifference.value last_cost: int = 0 @@ -317,9 +316,25 @@ def set_rules(world: World): for loc in world.multiworld.get_region("Alpine Skyline Area (TIHS)", world.player).locations: if "Goat Village" in loc.name: continue + # This needs some special handling + if loc.name == "Alpine Skyline - Goat Refinery": + add_rule(loc, lambda state: state.has("AFR Access", world.player) + and can_use_hookshot(state, world) + and can_hit(state, world, True)) + + difficulty: Difficulty = Difficulty(world.multiworld.LogicDifficulty[world.player].value) + if difficulty >= Difficulty.MODERATE: + add_rule(loc, lambda state: state.has("TIHS Access", world.player) + and can_use_hat(state, world, HatType.SPRINT), "or") + elif difficulty >= Difficulty.HARD: + add_rule(loc, lambda state: state.has("TIHS Access", world.player, "or")) + + continue add_rule(loc, lambda state: can_use_hookshot(state, world)) + dummy_entrances: typing.List[Entrance] = [] + for (key, acts) in act_connections.items(): if "Arctic Cruise" in key and not world.is_dlc1(): continue @@ -328,7 +343,7 @@ def set_rules(world: World): entrance: Entrance = world.multiworld.get_entrance(key, world.player) region: Region = entrance.connected_region access_rules: typing.List[typing.Callable[[CollectionState], bool]] = [] - entrance.parent_region.exits.remove(entrance) + dummy_entrances.append(entrance) # Entrances to this act that we have to set access_rules on entrances: typing.List[Entrance] = [] @@ -354,6 +369,9 @@ def set_rules(world: World): for rules in access_rules: add_rule(e, rules) + for e in dummy_entrances: + set_rule(e, lambda state: False) + set_event_rules(world) if world.options.EndGoal.value == 1: @@ -448,13 +466,12 @@ def set_moderate_rules(world: World): # There is a glitched fall damage volume near the Yellow Overpass time piece that warps the player to Pink Paw. # Yellow Overpass time piece can also be reached without Hookshot quite easily. if world.is_dlc2(): - set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) + # No Hookshot set_rule(world.multiworld.get_location("Act Completion (Yellow Overpass Station)", world.player), lambda state: True) + # No Dweller, Hookshot, or Time Stop for these set_rule(world.multiworld.get_location("Pink Paw Station - Cat Vacuum", world.player), lambda state: True) - - # The player can quite literally walk past the fan from the side without Time Stop. set_rule(world.multiworld.get_location("Pink Paw Station - Behind Fan", world.player), lambda state: True) # Moderate: clear Rush Hour without Hookshot @@ -465,8 +482,10 @@ def set_moderate_rules(world: World): and can_use_hat(state, world, HatType.ICE) and can_use_hat(state, world, HatType.BREWING)) - # Moderate: Bluefin Tunnel without tickets - set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) + # Moderate: Bluefin Tunnel + Pink Paw Station without tickets + if world.multiworld.NoTicketSkips[world.player].value == 0: + set_rule(world.multiworld.get_entrance("-> Pink Paw Station", world.player), lambda state: True) + set_rule(world.multiworld.get_entrance("-> Bluefin Tunnel", world.player), lambda state: True) def set_hard_rules(world: World): @@ -483,6 +502,13 @@ def set_hard_rules(world: World): 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)) + set_rule(world.multiworld.get_location("Subcon Forest - Noose Treehouse", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), + lambda state: has_paintings(state, world, 2, True)) + set_rule(world.multiworld.get_location("Subcon Forest - Tall Tree Hookshot Swing", world.player), + lambda state: has_paintings(state, world, 3, True)) + # SDJ add_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), lambda state: can_sdj(state, world) and has_paintings(state, world, 2), "or") @@ -508,8 +534,15 @@ def set_hard_rules(world: World): lambda state: can_use_hat(state, world, HatType.ICE)) # Hard: clear Rush Hour with Brewing Hat only - set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), - lambda state: can_use_hat(state, world, HatType.BREWING)) + if world.multiworld.NoTicketSkips[world.player].value != 1: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING)) + else: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: can_use_hat(state, world, HatType.BREWING) + and state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) def set_expert_rules(world: World): @@ -517,8 +550,10 @@ def set_expert_rules(world: World): set_rule(world.multiworld.get_entrance("Telescope -> Time's End", world.player), lambda state: state.has("Time Piece", world.player, world.get_chapter_cost(ChapterIndex.FINALE))) - # Expert: Mafia Town - Above Boats with nothing + # Expert: Mafia Town - Above Boats, Top of Lighthouse, and Hot Air Balloon with nothing set_rule(world.multiworld.get_location("Mafia Town - Above Boats", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Mafia Town - Top of Lighthouse", world.player), lambda state: True) + set_rule(world.multiworld.get_location("Mafia Town - Hot Air Balloon", world.player), lambda state: True) # Expert: Clear Dead Bird Studio with nothing for loc in world.multiworld.get_region("Dead Bird Studio - Post Elevator Area", world.player).locations: @@ -561,13 +596,9 @@ def set_expert_rules(world: World): # 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 - Noose Treehouse", world.player), - lambda state: has_paintings(state, world, 2, True)) - set_rule(world.multiworld.get_location("Subcon Forest - Long Tree Climb Chest", world.player), - lambda state: has_paintings(state, world, 2, 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 - Tall Tree Hookshot Swing", world.player), + 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 @@ -579,7 +610,13 @@ def set_expert_rules(world: World): if world.is_dlc2(): # Expert: clear Rush Hour with nothing - set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) + if world.multiworld.NoTicketSkips[world.player].value == 0: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), lambda state: True) + else: + set_rule(world.multiworld.get_location("Act Completion (Rush Hour)", world.player), + lambda state: state.has("Metro Ticket - Yellow", world.player) + and state.has("Metro Ticket - Blue", world.player) + and state.has("Metro Ticket - Pink", world.player)) def set_mafia_town_rules(world: World): diff --git a/worlds/ahit/__init__.py b/worlds/ahit/__init__.py index 8dd16d4a90..14eba116a6 100644 --- a/worlds/ahit/__init__.py +++ b/worlds/ahit/__init__.py @@ -1,7 +1,8 @@ from BaseClasses import Item, ItemClassification, Tutorial from .Items import 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 .Locations import location_table, contract_locations, is_location_valid, get_location_names, TASKSANITY_START_ID, \ + get_total_locations from .Rules import set_rules from .Options import AHITOptions, slot_data_options, adjust_options from .Types import HatType, ChapterIndex, HatInTimeItem @@ -87,7 +88,6 @@ class HatInTimeWorld(World): self.topology_present = self.options.ActRandomizer.value create_regions(self) - if self.options.EnableDeathWish.value > 0: create_dw_regions(self) @@ -174,7 +174,8 @@ class HatInTimeWorld(World): "Chapter7Cost": chapter_timepiece_costs[self.player][ChapterIndex.METRO], "BadgeSellerItemCount": badge_seller_count[self.player], "SeedNumber": str(self.multiworld.seed), # For shop prices - "SeedName": self.multiworld.seed_name} + "SeedName": self.multiworld.seed_name, + "TotalLocations": get_total_locations(self)} if self.options.HatItems.value == 0: slot_data.setdefault("SprintYarnCost", hat_yarn_costs[self.player][HatType.SPRINT]) @@ -253,7 +254,7 @@ class HatInTimeWorld(World): if self.is_dlc1() and self.options.Tasksanity.value > 0: ship_shape_region = get_shuffled_region(self, "Ship Shape") - id_start: int = get_tasksanity_start_id() + id_start: int = TASKSANITY_START_ID for i in range(self.options.TasksanityCheckCount.value): new_hint_data[id_start+i] = ship_shape_region diff --git a/worlds/ahit/test/TestActs.py b/worlds/ahit/test/TestActs.py index 7c2b9783e6..28e1a430d4 100644 --- a/worlds/ahit/test/TestActs.py +++ b/worlds/ahit/test/TestActs.py @@ -1,11 +1,12 @@ from worlds.ahit.Regions import act_chapters +from worlds.ahit.Rules import act_connections from worlds.ahit.test.TestBase import HatInTimeTestBase class TestActs(HatInTimeTestBase): def run_default_tests(self) -> bool: return False - + def testAllStateCanReachEverything(self): pass @@ -24,6 +25,9 @@ class TestActs(HatInTimeTestBase): for name in act_chapters.keys(): region = self.multiworld.get_region(name, 1) for entrance in region.entrances: + if entrance.name in act_connections.keys(): + continue + self.assertTrue(self.can_reach_entrance(entrance.name), f"Can't reach {name} from {entrance}\n" f"{entrance.parent_region.entrances[0]} -> {entrance.parent_region} "