TUNIC: Implement support for connection plando (#2864)

This commit is contained in:
Scipio Wright
2024-03-15 12:52:05 -04:00
committed by GitHub
parent 2a8784ef72
commit e6198585c8
7 changed files with 150 additions and 121 deletions

View File

@@ -5,6 +5,7 @@ from .er_data import Portal, tunic_er_regions, portal_mapping, hallway_helper, h
dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur
from .er_rules import set_er_region_rules
from worlds.generic import PlandoConnection
from random import Random
if TYPE_CHECKING:
from . import TunicWorld
@@ -185,9 +186,14 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
portal_pairs: Dict[Portal, Portal] = {}
dead_ends: List[Portal] = []
two_plus: List[Portal] = []
plando_connections: List[PlandoConnection] = []
fixed_shop = False
logic_rules = world.options.logic_rules.value
player_name = world.multiworld.get_player_name(world.player)
shop_scenes: Set[str] = set()
shop_count = 6
if world.options.fixed_shop.value:
shop_count = 1
shop_scenes.add("Overworld Redux")
if not logic_rules:
dependent_regions = dependent_regions_restricted
@@ -215,19 +221,17 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
start_region = "Overworld"
connected_regions.update(add_dependent_regions(start_region, logic_rules))
plando_connections = world.multiworld.plando_connections[world.player]
# universal tracker support stuff, don't need to care about region dependency
if hasattr(world.multiworld, "re_gen_passthrough"):
if "TUNIC" in world.multiworld.re_gen_passthrough:
plando_connections.clear()
# universal tracker stuff, won't do anything in normal gen
for portal1, portal2 in world.multiworld.re_gen_passthrough["TUNIC"]["Entrance Rando"].items():
portal_name1 = ""
portal_name2 = ""
# skip this if 10 fairies laurels location is on, it can be handled normally
if portal1 == "Overworld Redux, Waterfall_" and portal2 == "Waterfall, Overworld Redux_" \
and world.options.laurels_location == "10_fairies":
continue
for portal in portal_mapping:
if portal.scene_destination() == portal1:
portal_name1 = portal.name
@@ -240,9 +244,78 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
portal_name2 = "Shop Portal"
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
non_dead_end_regions = set()
for region_name, region_info in tunic_er_regions.items():
if not region_info.dead_end:
non_dead_end_regions.add(region_name)
elif region_info.dead_end == 2 and logic_rules:
non_dead_end_regions.add(region_name)
if plando_connections:
portal_pairs, dependent_regions, dead_ends, two_plus = \
create_plando_connections(plando_connections, dependent_regions, dead_ends, two_plus)
for connection in plando_connections:
p_entrance = connection.entrance
p_exit = connection.exit
if p_entrance.startswith("Shop"):
p_entrance = p_exit
p_exit = "Shop Portal"
portal1 = None
portal2 = None
# search two_plus for both at once
for portal in two_plus:
if p_entrance == portal.name:
portal1 = portal
if p_exit == portal.name:
portal2 = portal
# search dead_ends individually since we can't really remove items from two_plus during the loop
if not portal1:
for portal in dead_ends:
if p_entrance == portal.name:
portal1 = portal
break
if not portal1:
raise Exception(f"Could not find entrance named {p_entrance} for "
f"plando connections in {player_name}'s YAML.")
dead_ends.remove(portal1)
else:
two_plus.remove(portal1)
if not portal2:
for portal in dead_ends:
if p_exit == portal.name:
portal2 = portal
break
if p_exit in ["Shop Portal", "Shop"]:
portal2 = Portal(name="Shop Portal", region=f"Shop",
destination="Previous Region_")
shop_count -= 1
if shop_count < 0:
shop_count += 2
for p in portal_mapping:
if p.name == p_entrance:
shop_scenes.add(p.scene())
break
else:
if not portal2:
raise Exception(f"Could not find entrance named {p_exit} for "
f"plando connections in {player_name}'s YAML.")
dead_ends.remove(portal2)
else:
two_plus.remove(portal2)
portal_pairs[portal1] = portal2
# update dependent regions based on the plando'd connections, to ensure the portals connect well, logically
for origins, destinations in dependent_regions.items():
if portal1.region in origins:
if portal2.region in non_dead_end_regions:
destinations.append(portal2.region)
if portal2.region in origins:
if portal1.region in non_dead_end_regions:
destinations.append(portal1.region)
# if we have plando connections, our connected regions may change somewhat
while True:
@@ -255,7 +328,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
# need to plando fairy cave, or it could end up laurels locked
# fix this later to be random after adding some item logic to dependent regions
if world.options.laurels_location == "10_fairies":
if world.options.laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
portal1 = None
portal2 = None
for portal in two_plus:
@@ -266,41 +339,59 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
if portal.scene_destination() == "Waterfall, Overworld Redux_":
portal2 = portal
break
if not portal1:
raise Exception(f"Failed to do Laurels Location at 10 Fairies option. "
f"Did {player_name} plando connection the Secret Gathering Place Entrance?")
if not portal2:
raise Exception(f"Failed to do Laurels Location at 10 Fairies option. "
f"Did {player_name} plando connection the Secret Gathering Place Exit?")
portal_pairs[portal1] = portal2
two_plus.remove(portal1)
dead_ends.remove(portal2)
if world.options.fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
fixed_shop = True
portal1 = None
for portal in two_plus:
if portal.scene_destination() == "Overworld Redux, Windmill_":
portal1 = portal
break
portal2 = Portal(name="Shop Portal", region=f"Shop Entrance 2", destination="Previous Region_")
portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region_")
if not portal1:
raise Exception(f"Failed to do Fixed Shop option. "
f"Did {player_name} plando connection the Windmill Shop entrance?")
portal_pairs[portal1] = portal2
two_plus.remove(portal1)
random_object: Random = world.random
if world.options.entrance_rando.value != 1:
random_object = Random(world.options.entrance_rando.value)
# we want to start by making sure every region is accessible
non_dead_end_regions = set()
for region_name, region_info in tunic_er_regions.items():
if not region_info.dead_end:
non_dead_end_regions.add(region_name)
elif region_info.dead_end == 2 and logic_rules:
non_dead_end_regions.add(region_name)
world.random.shuffle(two_plus)
random_object.shuffle(two_plus)
check_success = 0
portal1 = None
portal2 = None
previous_conn_num = 0
fail_count = 0
while len(connected_regions) < len(non_dead_end_regions):
# if the connected regions length stays unchanged for too long, it's stuck in a loop
# should, hopefully, only ever occur if someone plandos connections poorly
if hasattr(world.multiworld, "re_gen_passthrough"):
break
if previous_conn_num == len(connected_regions):
fail_count += 1
if fail_count >= 500:
raise Exception(f"Failed to pair regions. Check plando connections for {player_name} for loops.")
else:
fail_count = 0
previous_conn_num = len(connected_regions)
# find a portal in an inaccessible region
if check_success == 0:
for portal in two_plus:
if portal.region in connected_regions:
# if there's risk of self-locking, start over
if gate_before_switch(portal, two_plus):
world.random.shuffle(two_plus)
random_object.shuffle(two_plus)
break
portal1 = portal
two_plus.remove(portal)
@@ -313,7 +404,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
if portal.region not in connected_regions:
# if there's risk of self-locking, shuffle and try again
if gate_before_switch(portal, two_plus):
world.random.shuffle(two_plus)
random_object.shuffle(two_plus)
break
portal2 = portal
two_plus.remove(portal)
@@ -325,16 +416,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
connected_regions.update(add_dependent_regions(portal2.region, logic_rules))
portal_pairs[portal1] = portal2
check_success = 0
world.random.shuffle(two_plus)
# add 6 shops, connect them to unique scenes
# this is due to a limitation in Tunic -- you wrong warp if there's multiple shops
shop_scenes: Set[str] = set()
shop_count = 6
if fixed_shop:
shop_count = 1
shop_scenes.add("Overworld Redux")
random_object.shuffle(two_plus)
# for universal tracker, we want to skip shop gen
if hasattr(world.multiworld, "re_gen_passthrough"):
@@ -350,13 +432,15 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
two_plus.remove(portal)
break
if portal1 is None:
raise Exception("Too many shops in the pool, or something else went wrong")
portal2 = Portal(name="Shop Portal", region=f"Shop Entrance {i + 1}", destination="Previous Region_")
raise Exception("Too many shops in the pool, or something else went wrong.")
portal2 = Portal(name="Shop Portal", region="Shop", destination="Previous Region_")
portal_pairs[portal1] = portal2
# connect dead ends to random non-dead ends
# none of the key events are in dead ends, so we don't need to do gate_before_switch
while len(dead_ends) > 0:
if hasattr(world.multiworld, "re_gen_passthrough"):
break
portal1 = two_plus.pop()
portal2 = dead_ends.pop()
portal_pairs[portal1] = portal2
@@ -364,6 +448,8 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
# then randomly connect the remaining portals to each other
# every region is accessible, so gate_before_switch is not necessary
while len(two_plus) > 1:
if hasattr(world.multiworld, "re_gen_passthrough"):
break
portal1 = two_plus.pop()
portal2 = two_plus.pop()
portal_pairs[portal1] = portal2
@@ -381,7 +467,7 @@ def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dic
region2 = regions[portal2.region]
region1.connect(region2, f"{portal1.name} -> {portal2.name}")
# prevent the logic from thinking you can get to any shop-connected region from the shop
if portal2.name != "Shop":
if not portal2.name.startswith("Shop"):
region2.connect(region1, f"{portal2.name} -> {portal1.name}")
@@ -507,65 +593,3 @@ def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool:
# false means you're good to place the portal
return False
# this is for making the connections themselves
def create_plando_connections(plando_connections: List[PlandoConnection],
dependent_regions: Dict[Tuple[str, ...], List[str]], dead_ends: List[Portal],
two_plus: List[Portal]) \
-> Tuple[Dict[Portal, Portal], Dict[Tuple[str, ...], List[str]], List[Portal], List[Portal]]:
portal_pairs: Dict[Portal, Portal] = {}
shop_num = 1
for connection in plando_connections:
p_entrance = connection.entrance
p_exit = connection.exit
portal1 = None
portal2 = None
# search two_plus for both at once
for portal in two_plus:
if p_entrance == portal.name:
portal1 = portal
if p_exit == portal.name:
portal2 = portal
# search dead_ends individually since we can't really remove items from two_plus during the loop
if not portal1:
for portal in dead_ends:
if p_entrance == portal.name:
portal1 = portal
break
dead_ends.remove(portal1)
else:
two_plus.remove(portal1)
if not portal2:
for portal in dead_ends:
if p_exit == portal.name:
portal2 = portal
break
if p_exit == "Shop Portal":
portal2 = Portal(name="Shop Portal", region=f"Shop Entrance {shop_num}", destination="Previous Region_")
shop_num += 1
else:
dead_ends.remove(portal2)
else:
two_plus.remove(portal2)
if not portal1:
raise Exception("could not find entrance named " + p_entrance + " for Tunic player's plando")
if not portal2:
raise Exception("could not find entrance named " + p_exit + " for Tunic player's plando")
portal_pairs[portal1] = portal2
# update dependent regions based on the plando'd connections, to make sure the portals connect well, logically
for origins, destinations in dependent_regions.items():
if portal1.region in origins:
destinations.append(portal2.region)
if portal2.region in origins:
destinations.append(portal1.region)
return portal_pairs, dependent_regions, dead_ends, two_plus