From 0d7b03deacdcae0bfc2446da58318beba969debf Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Thu, 8 Aug 2024 20:34:30 -0400 Subject: [PATCH] it's aliiiiiive --- worlds/tunic/__init__.py | 6 +-- worlds/tunic/er_data.py | 22 ++++++++- worlds/tunic/er_scripts.py | 93 ++++++++++++++++++++++++-------------- worlds/tunic/options.py | 10 ++-- 4 files changed, 90 insertions(+), 41 deletions(-) diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 29320c6f88..0f72548cf5 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -174,9 +174,9 @@ class TunicWorld(World): # loop through the connections in the player's yaml for cxn in tunic.options.plando_connections: new_cxn = True - # if they used the entrance direction, just swap it around - if cxn.direction == "entrance" and cls.seed_groups[group]["decoupled"]: - player_cxn = PlandoConnection(entrance=cxn.exit, exit=cxn.entrance, direction="exit", percentage=cxn.percentage) + # if they used the entrance direction, just swap it around so we don't have to deal with it + if cxn.direction == "exit" and cls.seed_groups[group]["decoupled"]: + player_cxn = PlandoConnection(entrance=cxn.exit, exit=cxn.entrance, direction="entrance", percentage=cxn.percentage) else: player_cxn = cxn for group_cxn in cls.seed_groups[group]["plando"]: diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index e3933448b1..8c63f2250a 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -1,6 +1,9 @@ -from typing import Dict, NamedTuple, List, Optional +from typing import Dict, NamedTuple, List, Optional, TYPE_CHECKING from enum import IntEnum +if TYPE_CHECKING: + from . import TunicWorld + # the direction you go to enter a portal class Direction(IntEnum): @@ -536,6 +539,23 @@ class RegionInfo(NamedTuple): is_fake_region: bool = False +# gets the outlet region name if it exists, the region if it doesn't +def get_portal_outlet_region(portal: Portal, world: "TunicWorld") -> str: + return world.er_regions[portal.region].outlet_region or portal.region + + +def is_dead_end(portal: Portal, restricted: bool, world: "TunicWorld") -> bool: + dead_end_status = world.er_regions[portal.region].dead_end + if not dead_end_status: + return False + # for the purposes of this function, special can be treated as a dead end + elif dead_end_status in [DeadEnd.all_cats, DeadEnd.special]: + return True + # last possibility is DeadEnd.restricted + else: + return restricted + + class DeadEnd(IntEnum): free = 0 # not a dead end all_cats = 1 # dead end in every logic category diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 2758a33165..5a823cae10 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -1,7 +1,8 @@ from typing import Dict, List, Set, Tuple, TYPE_CHECKING from BaseClasses import Region, ItemClassification, Item, Location from .locations import location_table -from .er_data import Portal, portal_mapping, traversal_requirements, DeadEnd, Direction, RegionInfo +from .er_data import (Portal, portal_mapping, traversal_requirements, DeadEnd, Direction, RegionInfo, + get_portal_outlet_region, is_dead_end) from .er_rules import set_er_region_rules from Options import PlandoConnection from .options import EntranceRando, EntranceLayout @@ -359,14 +360,17 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal portal1 = None portal2 = None - # search two_plus for both at once + # search the two_plus lists (or list) for the portals for portal in two_plus: if p_entrance == portal.name: portal1 = portal portal1_dead_end = False + break + for portal in two_plus2: if p_exit == portal.name: portal2 = portal portal2_dead_end = False + break # search dead_ends individually since we can't really remove items from two_plus during the loop if portal1: @@ -379,7 +383,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal "end to a dead end in their plando connections.") else: raise Exception(f"{player_name} paired a dead end to a dead end in their " - "plando connections.") + f"plando connections -- {connection.entrance} to {connection.exit}") for portal in dead_ends: if p_entrance == portal.name: @@ -389,6 +393,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal else: if p_entrance.startswith("Shop Portal "): portal_num = int(p_entrance.split("Shop Portal ")[-1]) + # shops 1-6 are south, 7 and 8 are east, and after that it just breaks direction pairs if portal_num <= 6: pdir = Direction.south elif portal_num in [7, 8]: @@ -398,23 +403,25 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal portal1 = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}", destination=str(portal_num), tag="_", direction=pdir) connected_shop_portal1s.add(portal_num) - create_shop_region(world, regions, portal_num) - world.used_shop_numbers.add(portal_num) + if portal_num not in world.used_shop_numbers: + create_shop_region(world, regions, portal_num) + world.used_shop_numbers.add(portal_num) if decoupled and portal_num not in connected_shop_portal2s: two_plus2.append(portal1) + non_dead_end_regions.add(portal1.region) else: raise Exception(f"Could not find entrance named {p_entrance} for " f"plando connections in {player_name}'s YAML.") if portal2: - two_plus.remove(portal2) + two_plus2.remove(portal2) else: for portal in dead_ends: if p_exit == portal.name: portal2 = portal dead_ends.remove(portal2) break - # if it's not a dead end, then it doesn't exist -- I don't think this code is actually reachable + # if it's not a dead end, maybe it's a plando'd shop portal that doesn't normally exist else: if not portal2: if p_exit.startswith("Shop Portal "): @@ -428,46 +435,57 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal portal2 = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}", destination=str(portal_num), tag="_", direction=pdir) connected_shop_portal2s.add(portal_num) - create_shop_region(world, regions, portal_num) - world.used_shop_numbers.add(portal_num) + if portal_num not in world.used_shop_numbers: + create_shop_region(world, regions, portal_num) + world.used_shop_numbers.add(portal_num) if decoupled and portal_num not in connected_shop_portal1s: two_plus.append(portal2) + non_dead_end_regions.add(portal2.region) else: raise Exception(f"Could not find entrance named {p_exit} for " f"plando connections in {player_name}'s YAML.") - # update the traversal chart to say you can get from portal1's region to portal2's and vice versa - if not portal1_dead_end and not portal2_dead_end: - traversal_reqs.setdefault(portal1.region, dict())[portal2.region] = [] - traversal_reqs.setdefault(portal2.region, dict())[portal1.region] = [] + # if we're doing decoupled, we don't need to do complex checks + if decoupled: + # we turn any plando that uses "exit" to use "entrance" instead + traversal_reqs.setdefault(portal1.region, dict())[get_portal_outlet_region(portal2, world)] = [] + if connection.direction == "both": + traversal_reqs.setdefault(portal2.region, dict())[get_portal_outlet_region(portal1, world)] = [] + # outside decoupled, we want to use what we were doing before decoupled got added + else: + # update the traversal chart to say you can get from portal1's region to portal2's and vice versa + if not portal1_dead_end and not portal2_dead_end: + traversal_reqs.setdefault(portal1.region, dict())[get_portal_outlet_region(portal2, world)] = [] + traversal_reqs.setdefault(portal2.region, dict())[get_portal_outlet_region(portal1, world)] = [] - if (portal1.region == "Zig Skip Exit" and (portal2_dead_end or portal2.region == "Secret Gathering Place") - or portal2.region == "Zig Skip Exit" and (portal1_dead_end or portal1.region == "Secret Gathering Place")): - if world.options.entrance_rando.value not in EntranceRando.options.values(): - raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " - "end to a dead end in their plando connections.") - else: - raise Exception(f"{player_name} paired a dead end to a dead end in their " - "plando connections.") - - if (portal1.region == "Secret Gathering Place" and (portal2_dead_end or portal2.region == "Zig Skip Exit") - or portal2.region == "Secret Gathering Place" and (portal1_dead_end or portal1.region == "Zig Skip Exit")): - # need to make sure you didn't pair this to a dead end or zig skip - if portal1_dead_end or portal2_dead_end or \ - portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit": + if (portal1.region == "Zig Skip Exit" and (portal2_dead_end or portal2.region == "Secret Gathering Place") + or portal2.region == "Zig Skip Exit" and (portal1_dead_end or portal1.region == "Secret Gathering Place")): if world.options.entrance_rando.value not in EntranceRando.options.values(): raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " "end to a dead end in their plando connections.") else: raise Exception(f"{player_name} paired a dead end to a dead end in their " "plando connections.") + + if (portal1.region == "Secret Gathering Place" and (portal2_dead_end or portal2.region == "Zig Skip Exit") + or portal2.region == "Secret Gathering Place" and (portal1_dead_end or portal1.region == "Zig Skip Exit")): + # need to make sure you didn't pair this to a dead end or zig skip + if portal1_dead_end or portal2_dead_end or \ + portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit": + if world.options.entrance_rando.value not in EntranceRando.options.values(): + raise Exception(f"Tunic ER seed group {world.options.entrance_rando.value} paired a dead " + "end to a dead end in their plando connections.") + else: + raise Exception(f"{player_name} paired a dead end to a dead end in their " + "plando connections.") + # okay now that we're done with all of that nonsense, we can finally make the portal pair portal_pairs[portal1] = portal2 # if we have plando connections, our connected regions may change somewhat connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks) - # if there are an odd number of shops after plando, add another one - if len(world.used_shop_numbers) % 2 == 1 and not decoupled: + # if there are an odd number of shops after plando, add another one, except in decoupled where it doesn't matter + if not decoupled and len(world.used_shop_numbers) % 2 == 1: if entrance_layout == EntranceLayout.option_direction_pairs: raise Exception(f"TUNIC: {world.player_name} plando'd too many shops for the Direction Pairs option.") portal_num = get_shop_num(world) @@ -555,9 +573,12 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal # if not waterfall_plando, then we just want to pair secret gathering place now elif portal.region != "Secret Gathering Place": continue + # if they're not facing opposite directions, just continue if entrance_layout == EntranceLayout.option_direction_pairs and not verify_direction_pair(portal, portal1): continue + + # if you have direction pairs, we need to make sure we don't run out of spots for problem portals if not decoupled and entrance_layout == EntranceLayout.option_direction_pairs: should_continue = False # these portals are weird since they're one-ways essentially @@ -579,15 +600,21 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal if should_continue: continue + # if decoupled is on, we need to make sure you aren't connecting two dead ends together both ways + if decoupled: + if portal1 in portal_pairs.keys() and portal_pairs[portal1] == portal: + if (is_dead_end(portal1, not ice_grappling, world) + and is_dead_end(portal, not ice_grappling, world)): + continue + portal2 = portal - connected_regions.add(world.er_regions[portal.region].outlet_region or portal.region) + connected_regions.add(get_portal_outlet_region(portal, world)) two_plus2.remove(portal) break if not portal2: if entrance_layout == EntranceLayout.option_direction_pairs: # portal1 doesn't have a valid direction pair yet, throw it back and start over - connected_regions.remove(world.er_regions[portal.region].outlet_region or portal.region) two_plus.append(portal1) continue else: @@ -658,11 +685,11 @@ def create_randomized_entrances(world: "TunicWorld", portal_pairs: Dict[Portal, for portal1, portal2 in portal_pairs.items(): # connect to the outlet region if there is one, if not connect to the actual region regions[portal1.region].connect( - connecting_region=regions[world.er_regions[portal2.region].outlet_region or portal2.region], + connecting_region=regions[get_portal_outlet_region(portal2, world)], name=portal1.name) if not world.options.decoupled: regions[portal2.region].connect( - connecting_region=regions[world.er_regions[portal1.region].outlet_region or portal1.region], + connecting_region=regions[get_portal_outlet_region(portal1, world)], name=portal2.name) diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index e6d4ae7eea..e3193a393c 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -183,11 +183,13 @@ class ShuffleLadders(Toggle): class TunicPlandoConnections(PlandoConnections): """ Generic connection plando. Format is: - - entrance: "Entrance Name" - exit: "Exit Name" - direction: "Direction" + - entrance: Entrance Name + exit: Exit Name + direction: Direction percentage: 100 - Direction must be one of 'entrance', 'exit', or 'both', and defaults to 'both' if omitted. + Direction must be one of entrance, exit, or both, and defaults to both if omitted. + Direction entrance means the entrance leads to the exit. Direction exit means the exit leads to the entrance. + If you do not have Decoupled enabled, you do not need the direction line, as it will only use both. Percentage is an integer from 0 to 100 which determines whether that connection will be made. Defaults to 100 if omitted. If the Entrance Layout option is set to Standard or Fixed Shop, you can plando multiple shops. If the Entrance Layout option is set to Direction Pairs, your plando connections must be facing opposite directions.