from copy import deepcopy from typing import List, TYPE_CHECKING from BaseClasses import CollectionState, PlandoOptions from .options import ShufflePortals from ..generic import PlandoConnection if TYPE_CHECKING: from . import MessengerWorld PORTALS = [ "Autumn Hills", "Riviere Turquoise", "Howling Grotto", "Sunken Shrine", "Searing Crags", "Glacial Peak", ] SHOP_POINTS = { "Autumn Hills": [ "Climbing Claws", "Hope Path", "Dimension Climb", "Leaf Golem", ], "Forlorn Temple": [ "Outside", "Entrance", "Climb", "Rocket Sunset", "Descent", "Saw Gauntlet", "Demon King", ], "Catacombs": [ "Triple Spike Crushers", "Ruxxtin", ], "Bamboo Creek": [ "Spike Crushers", "Abandoned", "Time Loop", ], "Howling Grotto": [ "Wingsuit", "Crushing Pits", "Emerald Golem", ], "Quillshroom Marsh": [ "Spikey Window", "Sand Trap", "Queen of Quills", ], "Searing Crags": [ "Rope Dart", "Falling Rocks", "Searing Mega Shard", "Before Final Climb", "Colossuses", "Key of Strength", ], "Glacial Peak": [ "Ice Climbers'", "Glacial Mega Shard", "Tower Entrance", ], "Tower of Time": [ "Final Chance", "Arcane Golem", ], "Cloud Ruins": [ "Cloud Entrance", "Pillar Glide", "Crushers' Descent", "Seeing Spikes", "Final Flight", "Manfred's", ], "Underworld": [ "Left", "Fireball Wave", "Long Climb", # "Barm'athaziel", # not currently valid "Key of Chaos", ], "Riviere Turquoise": [ "Waterfall", "Launch of Faith", "Log Flume", "Log Climb", "Restock", "Butterfly Matriarch", ], "Elemental Skylands": [ "Air Intro", "Air Generator", "Earth Intro", "Earth Generator", "Fire Intro", "Fire Generator", "Water Intro", "Water Generator", ], "Sunken Shrine": [ "Above Portal", "Lifeguard", "Sun Path", "Tabi Gauntlet", "Moon Path", ] } CHECKPOINTS = { "Autumn Hills": [ "Hope Latch", "Key of Hope", "Lakeside", "Double Swing", "Spike Ball Swing", ], "Forlorn Temple": [ "Sunny Day", "Rocket Maze", ], "Catacombs": [ "Death Trap", "Crusher Gauntlet", "Dirty Pond", ], "Bamboo Creek": [ "Spike Ball Pits", "Spike Doors", ], "Howling Grotto": [ "Lost Woods", "Breezy Crushers", ], "Quillshroom Marsh": [ "Seashell", "Quicksand", "Spike Wave", ], "Searing Crags": [ "Triple Ball Spinner", "Raining Rocks", ], "Glacial Peak": [ "Projectile Spike Pit", "Air Swag", "Free Climbing", ], "Tower of Time": [ "First", "Second", "Third", "Fourth", "Fifth", "Sixth", ], "Cloud Ruins": [ "Spike Float", "Ghost Pit", "Toothbrush Alley", "Saw Pit", ], "Underworld": [ "Hot Dip", "Hot Tub", "Lava Run", ], "Riviere Turquoise": [ "Flower Flight", ], "Elemental Skylands": [ "Air Seal", ], "Sunken Shrine": [ "Lightfoot Tabi", "Sun Crest", "Waterfall Paradise", "Moon Crest", ] } REGION_ORDER = [ "Autumn Hills", "Forlorn Temple", "Catacombs", "Bamboo Creek", "Howling Grotto", "Quillshroom Marsh", "Searing Crags", "Glacial Peak", "Tower of Time", "Cloud Ruins", "Underworld", "Riviere Turquoise", "Elemental Skylands", "Sunken Shrine", ] def shuffle_portals(world: "MessengerWorld") -> None: """shuffles the output of the portals from the main hub""" def create_mapping(in_portal: str, warp: str) -> str: """assigns the chosen output to the input""" parent = out_to_parent[warp] exit_string = f"{parent.strip(' ')} - " if "Portal" in warp: exit_string += "Portal" world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}00")) elif warp in SHOP_POINTS[parent]: exit_string += f"{warp} Shop" world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}1{SHOP_POINTS[parent].index(warp)}")) else: exit_string += f"{warp} Checkpoint" world.portal_mapping.append(int(f"{REGION_ORDER.index(parent)}2{CHECKPOINTS[parent].index(warp)}")) world.spoiler_portal_mapping[in_portal] = exit_string connect_portal(world, in_portal, exit_string) return parent def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None: """checks the provided plando connections for portals and connects them""" for connection in plando_connections: if connection.entrance not in PORTALS: continue # let it crash here if input is invalid create_mapping(connection.entrance, connection.exit) world.plando_portals.append(connection.entrance) shuffle_type = world.options.shuffle_portals shop_points = deepcopy(SHOP_POINTS) for portal in PORTALS: shop_points[portal].append(f"{portal} Portal") if shuffle_type > ShufflePortals.option_shops: for area, points in CHECKPOINTS.items(): shop_points[area] += points out_to_parent = {checkpoint: parent for parent, checkpoints in shop_points.items() for checkpoint in checkpoints} available_portals = [val for zone in shop_points.values() for val in zone] world.random.shuffle(available_portals) plando = world.multiworld.plando_connections[world.player] if plando and world.multiworld.plando_options & PlandoOptions.connections: handle_planned_portals(plando) for portal in PORTALS: if portal in world.plando_portals: continue warp_point = available_portals.pop() parent = create_mapping(portal, warp_point) if shuffle_type < ShufflePortals.option_anywhere: available_portals = [port for port in available_portals if port not in shop_points[parent]] world.random.shuffle(available_portals) def connect_portal(world: "MessengerWorld", portal: str, out_region: str) -> None: entrance = world.multiworld.get_entrance(f"ToTHQ {portal} Portal", world.player) entrance.connect(world.multiworld.get_region(out_region, world.player)) def disconnect_portals(world: "MessengerWorld") -> None: for portal in [port for port in PORTALS if port not in world.plando_portals]: entrance = world.multiworld.get_entrance(f"ToTHQ {portal} Portal", world.player) entrance.connected_region.entrances.remove(entrance) entrance.connected_region = None if portal in world.spoiler_portal_mapping: del world.spoiler_portal_mapping[portal] if len(world.portal_mapping) > len(world.spoiler_portal_mapping): world.portal_mapping = world.portal_mapping[:len(world.spoiler_portal_mapping)] def validate_portals(world: "MessengerWorld") -> bool: # if world.options.shuffle_transitions: # return True new_state = CollectionState(world.multiworld) new_state.update_reachable_regions(world.player) reachable_locs = 0 for loc in world.multiworld.get_locations(world.player): reachable_locs += loc.can_reach(new_state) if reachable_locs > 5: return True return False def add_closed_portal_reqs(world: "MessengerWorld") -> None: closed_portals = [entrance for entrance in PORTALS if f"{entrance} Portal" not in world.starting_portals] for portal in closed_portals: tower_exit = world.multiworld.get_entrance(f"ToTHQ {portal} Portal", world.player) tower_exit.access_rule = lambda state: state.has(portal, world.player)