TUNIC: ER Refactor for better plando connections, fewer shops improvement (#3075)
* Fixed shop changes * Update option description * Apply suggestions from Vi's review (thank you) * Fix for plando connections on a full scene * Plando connections should work better now for complicated paths * Even more good plando connections yes * Starting to move the info over * Fixing up formatting a bit * Remove unneeded item info * Put in updated_reachable_regions, to replace add_dependent_regions * Updated to match ladder shuffle * More stuff I guess * It functions! * It mostly works with plando now, some slight issues still * Fixed minor logic bug * Fixed world leakage * Change exception message * Make exception message better for troubleshooting failed connections * Merged with main * technically a logic fix but it would never matter cause no start shuffle * Add a couple more alias item groups cause yeah * Rename beneath the vault front -> beneath the vault main * Flip lantern access rule to the region * Add missing connection to traversal reqs * Move start_inventory_from_pool to the top so that it's next to start_inventory * Reword the fixed shop description slightly * Refactor per ixrec's comments * Greatly reduced an overcomplicated block because Vi is cool and smart and also cool * Rewrite traversal reqs thing per Vi's comments
This commit is contained in:
parent
12cde88f95
commit
754fc11c1b
File diff suppressed because it is too large
Load Diff
|
@ -268,7 +268,8 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
|
||||||
connecting_region=regions["Overworld Well Ladder"],
|
connecting_region=regions["Overworld Well Ladder"],
|
||||||
rule=lambda state: has_ladder("Ladders in Well", state, player, options))
|
rule=lambda state: has_ladder("Ladders in Well", state, player, options))
|
||||||
regions["Overworld Well Ladder"].connect(
|
regions["Overworld Well Ladder"].connect(
|
||||||
connecting_region=regions["Overworld"])
|
connecting_region=regions["Overworld"],
|
||||||
|
rule=lambda state: has_ladder("Ladders in Well", state, player, options))
|
||||||
|
|
||||||
# nmg: can ice grapple through the door
|
# nmg: can ice grapple through the door
|
||||||
regions["Overworld"].connect(
|
regions["Overworld"].connect(
|
||||||
|
@ -706,17 +707,18 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
|
||||||
connecting_region=regions["Fortress Exterior from Overworld"])
|
connecting_region=regions["Fortress Exterior from Overworld"])
|
||||||
|
|
||||||
regions["Beneath the Vault Ladder Exit"].connect(
|
regions["Beneath the Vault Ladder Exit"].connect(
|
||||||
connecting_region=regions["Beneath the Vault Front"],
|
connecting_region=regions["Beneath the Vault Main"],
|
||||||
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options))
|
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options)
|
||||||
regions["Beneath the Vault Front"].connect(
|
and has_lantern(state, player, options))
|
||||||
|
regions["Beneath the Vault Main"].connect(
|
||||||
connecting_region=regions["Beneath the Vault Ladder Exit"],
|
connecting_region=regions["Beneath the Vault Ladder Exit"],
|
||||||
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options))
|
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, player, options))
|
||||||
|
|
||||||
regions["Beneath the Vault Front"].connect(
|
regions["Beneath the Vault Main"].connect(
|
||||||
connecting_region=regions["Beneath the Vault Back"],
|
connecting_region=regions["Beneath the Vault Back"])
|
||||||
rule=lambda state: has_lantern(state, player, options))
|
|
||||||
regions["Beneath the Vault Back"].connect(
|
regions["Beneath the Vault Back"].connect(
|
||||||
connecting_region=regions["Beneath the Vault Front"])
|
connecting_region=regions["Beneath the Vault Main"],
|
||||||
|
rule=lambda state: has_lantern(state, player, options))
|
||||||
|
|
||||||
regions["Fortress East Shortcut Upper"].connect(
|
regions["Fortress East Shortcut Upper"].connect(
|
||||||
connecting_region=regions["Fortress East Shortcut Lower"])
|
connecting_region=regions["Fortress East Shortcut Lower"])
|
||||||
|
@ -870,6 +872,9 @@ def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], re
|
||||||
regions["Rooted Ziggurat Portal Room Entrance"].connect(
|
regions["Rooted Ziggurat Portal Room Entrance"].connect(
|
||||||
connecting_region=regions["Rooted Ziggurat Lower Back"])
|
connecting_region=regions["Rooted Ziggurat Lower Back"])
|
||||||
|
|
||||||
|
regions["Zig Skip Exit"].connect(
|
||||||
|
connecting_region=regions["Rooted Ziggurat Lower Front"])
|
||||||
|
|
||||||
regions["Rooted Ziggurat Portal"].connect(
|
regions["Rooted Ziggurat Portal"].connect(
|
||||||
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
|
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
|
||||||
rule=lambda state: state.has("Activate Ziggurat Fuse", player))
|
rule=lambda state: state.has("Activate Ziggurat Fuse", player))
|
||||||
|
@ -1453,8 +1458,6 @@ def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int])
|
||||||
# Beneath the Vault
|
# Beneath the Vault
|
||||||
set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player),
|
set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player),
|
||||||
lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player))
|
lambda state: state.has_group("Melee Weapons", player, 1) or state.has_any({laurels, fire_wand}, player))
|
||||||
set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player),
|
|
||||||
lambda state: has_lantern(state, player, options))
|
|
||||||
|
|
||||||
# Quarry
|
# Quarry
|
||||||
set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player),
|
set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player),
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
from typing import Dict, List, Set, TYPE_CHECKING
|
from typing import Dict, List, Set, TYPE_CHECKING
|
||||||
from BaseClasses import Region, ItemClassification, Item, Location
|
from BaseClasses import Region, ItemClassification, Item, Location
|
||||||
from .locations import location_table
|
from .locations import location_table
|
||||||
from .er_data import Portal, tunic_er_regions, portal_mapping, \
|
from .er_data import Portal, tunic_er_regions, portal_mapping, traversal_requirements, DeadEnd
|
||||||
dependent_regions_restricted, dependent_regions_nmg, dependent_regions_ur
|
|
||||||
from .er_rules import set_er_region_rules
|
from .er_rules import set_er_region_rules
|
||||||
from .options import EntranceRando
|
from .options import EntranceRando
|
||||||
from worlds.generic import PlandoConnection
|
from worlds.generic import PlandoConnection
|
||||||
from random import Random
|
from random import Random
|
||||||
|
from copy import deepcopy
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from . import TunicWorld
|
from . import TunicWorld
|
||||||
|
@ -95,7 +95,8 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None:
|
||||||
|
|
||||||
def vanilla_portals() -> Dict[Portal, Portal]:
|
def vanilla_portals() -> Dict[Portal, Portal]:
|
||||||
portal_pairs: Dict[Portal, Portal] = {}
|
portal_pairs: Dict[Portal, Portal] = {}
|
||||||
portal_map = portal_mapping.copy()
|
# we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here
|
||||||
|
portal_map = [portal for portal in portal_mapping if portal.name != "Ziggurat Lower Falling Entrance"]
|
||||||
|
|
||||||
while portal_map:
|
while portal_map:
|
||||||
portal1 = portal_map[0]
|
portal1 = portal_map[0]
|
||||||
|
@ -130,9 +131,13 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
dead_ends: List[Portal] = []
|
dead_ends: List[Portal] = []
|
||||||
two_plus: List[Portal] = []
|
two_plus: List[Portal] = []
|
||||||
player_name = world.multiworld.get_player_name(world.player)
|
player_name = world.multiworld.get_player_name(world.player)
|
||||||
|
portal_map = portal_mapping.copy()
|
||||||
logic_rules = world.options.logic_rules.value
|
logic_rules = world.options.logic_rules.value
|
||||||
fixed_shop = world.options.fixed_shop
|
fixed_shop = world.options.fixed_shop
|
||||||
laurels_location = world.options.laurels_location
|
laurels_location = world.options.laurels_location
|
||||||
|
traversal_reqs = deepcopy(traversal_requirements)
|
||||||
|
has_laurels = True
|
||||||
|
waterfall_plando = False
|
||||||
|
|
||||||
# if it's not one of the EntranceRando options, it's a custom seed
|
# if it's not one of the EntranceRando options, it's a custom seed
|
||||||
if world.options.entrance_rando.value not in EntranceRando.options:
|
if world.options.entrance_rando.value not in EntranceRando.options:
|
||||||
|
@ -140,38 +145,53 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
logic_rules = seed_group["logic_rules"]
|
logic_rules = seed_group["logic_rules"]
|
||||||
fixed_shop = seed_group["fixed_shop"]
|
fixed_shop = seed_group["fixed_shop"]
|
||||||
laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
|
laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
|
||||||
|
|
||||||
|
# marking that you don't immediately have laurels
|
||||||
|
if laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
|
||||||
|
has_laurels = False
|
||||||
|
|
||||||
shop_scenes: Set[str] = set()
|
shop_scenes: Set[str] = set()
|
||||||
shop_count = 6
|
shop_count = 6
|
||||||
if fixed_shop:
|
if fixed_shop:
|
||||||
shop_count = 1
|
shop_count = 0
|
||||||
shop_scenes.add("Overworld Redux")
|
shop_scenes.add("Overworld Redux")
|
||||||
|
|
||||||
if not logic_rules:
|
|
||||||
dependent_regions = dependent_regions_restricted
|
|
||||||
elif logic_rules == 1:
|
|
||||||
dependent_regions = dependent_regions_nmg
|
|
||||||
else:
|
else:
|
||||||
dependent_regions = dependent_regions_ur
|
# if fixed shop is off, remove this portal
|
||||||
|
for portal in portal_map:
|
||||||
|
if portal.region == "Zig Skip Exit":
|
||||||
|
portal_map.remove(portal)
|
||||||
|
break
|
||||||
|
|
||||||
# create separate lists for dead ends and non-dead ends
|
# create separate lists for dead ends and non-dead ends
|
||||||
if logic_rules:
|
for portal in portal_map:
|
||||||
for portal in portal_mapping:
|
dead_end_status = tunic_er_regions[portal.region].dead_end
|
||||||
if tunic_er_regions[portal.region].dead_end == 1:
|
if dead_end_status == DeadEnd.free:
|
||||||
dead_ends.append(portal)
|
two_plus.append(portal)
|
||||||
else:
|
elif dead_end_status == DeadEnd.all_cats:
|
||||||
|
dead_ends.append(portal)
|
||||||
|
elif dead_end_status == DeadEnd.restricted:
|
||||||
|
if logic_rules:
|
||||||
two_plus.append(portal)
|
two_plus.append(portal)
|
||||||
else:
|
|
||||||
for portal in portal_mapping:
|
|
||||||
if tunic_er_regions[portal.region].dead_end:
|
|
||||||
dead_ends.append(portal)
|
|
||||||
else:
|
else:
|
||||||
two_plus.append(portal)
|
dead_ends.append(portal)
|
||||||
|
# these two get special handling
|
||||||
|
elif dead_end_status == DeadEnd.special:
|
||||||
|
if portal.region == "Secret Gathering Place":
|
||||||
|
if laurels_location == "10_fairies":
|
||||||
|
two_plus.append(portal)
|
||||||
|
else:
|
||||||
|
dead_ends.append(portal)
|
||||||
|
if portal.region == "Zig Skip Exit":
|
||||||
|
if fixed_shop:
|
||||||
|
two_plus.append(portal)
|
||||||
|
else:
|
||||||
|
dead_ends.append(portal)
|
||||||
|
|
||||||
connected_regions: Set[str] = set()
|
connected_regions: Set[str] = set()
|
||||||
# make better start region stuff when/if implementing random start
|
# make better start region stuff when/if implementing random start
|
||||||
start_region = "Overworld"
|
start_region = "Overworld"
|
||||||
connected_regions.update(add_dependent_regions(start_region, logic_rules))
|
connected_regions.add(start_region)
|
||||||
|
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
|
||||||
|
|
||||||
if world.options.entrance_rando.value in EntranceRando.options:
|
if world.options.entrance_rando.value in EntranceRando.options:
|
||||||
plando_connections = world.multiworld.plando_connections[world.player]
|
plando_connections = world.multiworld.plando_connections[world.player]
|
||||||
|
@ -205,11 +225,17 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
non_dead_end_regions.add(region_name)
|
non_dead_end_regions.add(region_name)
|
||||||
elif region_info.dead_end == 2 and logic_rules:
|
elif region_info.dead_end == 2 and logic_rules:
|
||||||
non_dead_end_regions.add(region_name)
|
non_dead_end_regions.add(region_name)
|
||||||
|
elif region_info.dead_end == 3:
|
||||||
|
if (region_name == "Secret Gathering Place" and laurels_location == "10_fairies") \
|
||||||
|
or (region_name == "Zig Skip Exit" and fixed_shop):
|
||||||
|
non_dead_end_regions.add(region_name)
|
||||||
|
|
||||||
if plando_connections:
|
if plando_connections:
|
||||||
for connection in plando_connections:
|
for connection in plando_connections:
|
||||||
p_entrance = connection.entrance
|
p_entrance = connection.entrance
|
||||||
p_exit = connection.exit
|
p_exit = connection.exit
|
||||||
|
portal1_dead_end = True
|
||||||
|
portal2_dead_end = True
|
||||||
|
|
||||||
portal1 = None
|
portal1 = None
|
||||||
portal2 = None
|
portal2 = None
|
||||||
|
@ -218,8 +244,10 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
for portal in two_plus:
|
for portal in two_plus:
|
||||||
if p_entrance == portal.name:
|
if p_entrance == portal.name:
|
||||||
portal1 = portal
|
portal1 = portal
|
||||||
|
portal1_dead_end = False
|
||||||
if p_exit == portal.name:
|
if p_exit == portal.name:
|
||||||
portal2 = portal
|
portal2 = portal
|
||||||
|
portal2_dead_end = False
|
||||||
|
|
||||||
# search dead_ends individually since we can't really remove items from two_plus during the loop
|
# search dead_ends individually since we can't really remove items from two_plus during the loop
|
||||||
if portal1:
|
if portal1:
|
||||||
|
@ -233,7 +261,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
else:
|
else:
|
||||||
raise Exception(f"{player_name} paired a dead end to a dead end in their "
|
raise Exception(f"{player_name} paired a dead end to a dead end in their "
|
||||||
"plando connections.")
|
"plando connections.")
|
||||||
|
|
||||||
for portal in dead_ends:
|
for portal in dead_ends:
|
||||||
if p_entrance == portal.name:
|
if p_entrance == portal.name:
|
||||||
portal1 = portal
|
portal1 = portal
|
||||||
|
@ -246,7 +274,6 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
if portal2:
|
if portal2:
|
||||||
two_plus.remove(portal2)
|
two_plus.remove(portal2)
|
||||||
else:
|
else:
|
||||||
# check if portal2 is a dead end
|
|
||||||
for portal in dead_ends:
|
for portal in dead_ends:
|
||||||
if p_exit == portal.name:
|
if p_exit == portal.name:
|
||||||
portal2 = portal
|
portal2 = portal
|
||||||
|
@ -256,6 +283,7 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
portal2 = Portal(name="Shop Portal", region="Shop",
|
portal2 = Portal(name="Shop Portal", region="Shop",
|
||||||
destination="Previous Region", tag="_")
|
destination="Previous Region", tag="_")
|
||||||
shop_count -= 1
|
shop_count -= 1
|
||||||
|
# need to maintain an even number of portals total
|
||||||
if shop_count < 0:
|
if shop_count < 0:
|
||||||
shop_count += 2
|
shop_count += 2
|
||||||
for p in portal_mapping:
|
for p in portal_mapping:
|
||||||
|
@ -269,48 +297,36 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
f"plando connections in {player_name}'s YAML.")
|
f"plando connections in {player_name}'s YAML.")
|
||||||
dead_ends.remove(portal2)
|
dead_ends.remove(portal2)
|
||||||
|
|
||||||
|
# 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 portal1.region == "Zig Skip Exit" or portal2.region == "Zig Skip Exit":
|
||||||
|
if portal1_dead_end or portal2_dead_end or \
|
||||||
|
portal1.region == "Secret Gathering Place" or portal2.region == "Secret Gathering Place":
|
||||||
|
if world.options.entrance_rando.value not in EntranceRando.options:
|
||||||
|
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" or portal2.region == "Secret Gathering Place":
|
||||||
|
# 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:
|
||||||
|
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.")
|
||||||
|
waterfall_plando = True
|
||||||
portal_pairs[portal1] = 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
|
# if we have plando connections, our connected regions may change somewhat
|
||||||
while True:
|
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
|
||||||
test1 = len(connected_regions)
|
|
||||||
for region in connected_regions.copy():
|
|
||||||
connected_regions.update(add_dependent_regions(region, logic_rules))
|
|
||||||
test2 = len(connected_regions)
|
|
||||||
if test1 == test2:
|
|
||||||
break
|
|
||||||
|
|
||||||
# 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 laurels_location == "10_fairies" and not hasattr(world.multiworld, "re_gen_passthrough"):
|
|
||||||
portal1 = None
|
|
||||||
portal2 = None
|
|
||||||
for portal in two_plus:
|
|
||||||
if portal.scene_destination() == "Overworld Redux, Waterfall_":
|
|
||||||
portal1 = portal
|
|
||||||
break
|
|
||||||
for portal in dead_ends:
|
|
||||||
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 fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
|
if fixed_shop and not hasattr(world.multiworld, "re_gen_passthrough"):
|
||||||
portal1 = None
|
portal1 = None
|
||||||
|
@ -339,47 +355,54 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
previous_conn_num = 0
|
previous_conn_num = 0
|
||||||
fail_count = 0
|
fail_count = 0
|
||||||
while len(connected_regions) < len(non_dead_end_regions):
|
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
|
# if this is universal tracker, just break immediately and move on
|
||||||
# should, hopefully, only ever occur if someone plandos connections poorly
|
|
||||||
if hasattr(world.multiworld, "re_gen_passthrough"):
|
if hasattr(world.multiworld, "re_gen_passthrough"):
|
||||||
break
|
break
|
||||||
|
# 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 previous_conn_num == len(connected_regions):
|
if previous_conn_num == len(connected_regions):
|
||||||
fail_count += 1
|
fail_count += 1
|
||||||
if fail_count >= 500:
|
if fail_count >= 500:
|
||||||
raise Exception(f"Failed to pair regions. Check plando connections for {player_name} for loops.")
|
raise Exception(f"Failed to pair regions. Check plando connections for {player_name} for errors. "
|
||||||
|
"Unconnected regions:", non_dead_end_regions - connected_regions)
|
||||||
else:
|
else:
|
||||||
fail_count = 0
|
fail_count = 0
|
||||||
previous_conn_num = len(connected_regions)
|
previous_conn_num = len(connected_regions)
|
||||||
|
|
||||||
# find a portal in an inaccessible region
|
# find a portal in a connected region
|
||||||
if check_success == 0:
|
if check_success == 0:
|
||||||
for portal in two_plus:
|
for portal in two_plus:
|
||||||
if portal.region in connected_regions:
|
if portal.region in connected_regions:
|
||||||
# if there's risk of self-locking, start over
|
|
||||||
if gate_before_switch(portal, two_plus):
|
|
||||||
random_object.shuffle(two_plus)
|
|
||||||
break
|
|
||||||
portal1 = portal
|
portal1 = portal
|
||||||
two_plus.remove(portal)
|
two_plus.remove(portal)
|
||||||
check_success = 1
|
check_success = 1
|
||||||
break
|
break
|
||||||
|
|
||||||
# then we find a portal in a connected region
|
# then we find a portal in an inaccessible region
|
||||||
if check_success == 1:
|
if check_success == 1:
|
||||||
for portal in two_plus:
|
for portal in two_plus:
|
||||||
if portal.region not in connected_regions:
|
if portal.region not in connected_regions:
|
||||||
# if there's risk of self-locking, shuffle and try again
|
# if secret gathering place happens to get paired really late, you can end up running out
|
||||||
if gate_before_switch(portal, two_plus):
|
if not has_laurels and len(two_plus) < 80:
|
||||||
random_object.shuffle(two_plus)
|
# if you plando'd secret gathering place with laurels at 10 fairies, you're the reason for this
|
||||||
break
|
if waterfall_plando:
|
||||||
|
cr = connected_regions.copy()
|
||||||
|
cr.add(portal.region)
|
||||||
|
if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_rules):
|
||||||
|
continue
|
||||||
|
elif portal.region != "Secret Gathering Place":
|
||||||
|
continue
|
||||||
portal2 = portal
|
portal2 = portal
|
||||||
|
connected_regions.add(portal.region)
|
||||||
two_plus.remove(portal)
|
two_plus.remove(portal)
|
||||||
check_success = 2
|
check_success = 2
|
||||||
break
|
break
|
||||||
|
|
||||||
# once we have both portals, connect them and add the new region(s) to connected_regions
|
# once we have both portals, connect them and add the new region(s) to connected_regions
|
||||||
if check_success == 2:
|
if check_success == 2:
|
||||||
connected_regions.update(add_dependent_regions(portal2.region, logic_rules))
|
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_rules)
|
||||||
|
if "Secret Gathering Place" in connected_regions:
|
||||||
|
has_laurels = True
|
||||||
portal_pairs[portal1] = portal2
|
portal_pairs[portal1] = portal2
|
||||||
check_success = 0
|
check_success = 0
|
||||||
random_object.shuffle(two_plus)
|
random_object.shuffle(two_plus)
|
||||||
|
@ -411,7 +434,6 @@ def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||||
portal1 = two_plus.pop()
|
portal1 = two_plus.pop()
|
||||||
portal2 = dead_ends.pop()
|
portal2 = dead_ends.pop()
|
||||||
portal_pairs[portal1] = portal2
|
portal_pairs[portal1] = portal2
|
||||||
|
|
||||||
# then randomly connect the remaining portals to each other
|
# then randomly connect the remaining portals to each other
|
||||||
# every region is accessible, so gate_before_switch is not necessary
|
# every region is accessible, so gate_before_switch is not necessary
|
||||||
while len(two_plus) > 1:
|
while len(two_plus) > 1:
|
||||||
|
@ -438,126 +460,42 @@ def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dic
|
||||||
region2.connect(connecting_region=region1, name=portal2.name)
|
region2.connect(connecting_region=region1, name=portal2.name)
|
||||||
|
|
||||||
|
|
||||||
# loop through the static connections, return regions you can reach from this region
|
def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]],
|
||||||
# todo: refactor to take region_name and dependent_regions
|
has_laurels: bool, logic: int) -> Set[str]:
|
||||||
def add_dependent_regions(region_name: str, logic_rules: int) -> Set[str]:
|
# starting count, so we can run it again if this changes
|
||||||
region_set = set()
|
region_count = len(connected_regions)
|
||||||
if not logic_rules:
|
for origin, destinations in traversal_reqs.items():
|
||||||
regions_to_add = dependent_regions_restricted
|
if origin not in connected_regions:
|
||||||
elif logic_rules == 1:
|
continue
|
||||||
regions_to_add = dependent_regions_nmg
|
# check if we can traverse to any of the destinations
|
||||||
else:
|
for destination, req_lists in destinations.items():
|
||||||
regions_to_add = dependent_regions_ur
|
if destination in connected_regions:
|
||||||
for origin_regions, destination_regions in regions_to_add.items():
|
continue
|
||||||
if region_name in origin_regions:
|
met_traversal_reqs = False
|
||||||
# if you matched something in the first set, you get the regions in its paired set
|
if len(req_lists) == 0:
|
||||||
region_set.update(destination_regions)
|
met_traversal_reqs = True
|
||||||
return region_set
|
# loop through each set of possible requirements, with a fancy for else loop
|
||||||
# if you didn't match anything in the first sets, just gives you the region
|
for reqs in req_lists:
|
||||||
region_set = {region_name}
|
for req in reqs:
|
||||||
return region_set
|
if req == "Hyperdash":
|
||||||
|
if not has_laurels:
|
||||||
|
break
|
||||||
|
elif req == "NMG":
|
||||||
|
if not logic:
|
||||||
|
break
|
||||||
|
elif req == "UR":
|
||||||
|
if logic < 2:
|
||||||
|
break
|
||||||
|
elif req not in connected_regions:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
met_traversal_reqs = True
|
||||||
|
break
|
||||||
|
if met_traversal_reqs:
|
||||||
|
connected_regions.add(destination)
|
||||||
|
|
||||||
|
# if the length of connected_regions changed, we got new regions, so we want to check those new origins
|
||||||
|
if region_count != len(connected_regions):
|
||||||
|
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic)
|
||||||
|
|
||||||
# we're checking if an event-locked portal is being placed before the regions where its key(s) is/are
|
return connected_regions
|
||||||
# doing this ensures the keys will not be locked behind the event-locked portal
|
|
||||||
def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool:
|
|
||||||
# the western belltower cannot be locked since you can access it with laurels
|
|
||||||
# so we only need to make sure the forest belltower isn't locked
|
|
||||||
if check_portal.scene_destination() == "Overworld Redux, Temple_main":
|
|
||||||
i = 0
|
|
||||||
for portal in two_plus:
|
|
||||||
if portal.region == "Forest Belltower Upper":
|
|
||||||
i += 1
|
|
||||||
break
|
|
||||||
if i == 1:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# fortress big gold door needs 2 scenes and one of the two upper portals of the courtyard
|
|
||||||
elif check_portal.scene_destination() == "Fortress Main, Fortress Arena_":
|
|
||||||
i = j = k = 0
|
|
||||||
for portal in two_plus:
|
|
||||||
if portal.region == "Fortress Courtyard Upper":
|
|
||||||
i += 1
|
|
||||||
if portal.scene() == "Fortress Basement":
|
|
||||||
j += 1
|
|
||||||
if portal.region == "Eastern Vault Fortress":
|
|
||||||
k += 1
|
|
||||||
if i == 2 or j == 2 or k == 5:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# fortress teleporter needs only the left fuses
|
|
||||||
elif check_portal.scene_destination() in {"Fortress Arena, Transit_teleporter_spidertank",
|
|
||||||
"Transit, Fortress Arena_teleporter_spidertank"}:
|
|
||||||
i = j = k = 0
|
|
||||||
for portal in two_plus:
|
|
||||||
if portal.scene() == "Fortress Courtyard":
|
|
||||||
i += 1
|
|
||||||
if portal.scene() == "Fortress Basement":
|
|
||||||
j += 1
|
|
||||||
if portal.region == "Eastern Vault Fortress":
|
|
||||||
k += 1
|
|
||||||
if i == 8 or j == 2 or k == 5:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Cathedral door needs Overworld and the front of Swamp
|
|
||||||
# Overworld is currently guaranteed, so no need to check it
|
|
||||||
elif check_portal.scene_destination() == "Swamp Redux 2, Cathedral Redux_main":
|
|
||||||
i = 0
|
|
||||||
for portal in two_plus:
|
|
||||||
if portal.region in {"Swamp Front", "Swamp to Cathedral Treasure Room",
|
|
||||||
"Swamp to Cathedral Main Entrance Region"}:
|
|
||||||
i += 1
|
|
||||||
if i == 4:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Zig portal room exit needs Zig 3 to be accessible to hit the fuse
|
|
||||||
elif check_portal.scene_destination() == "ziggurat2020_FTRoom, ziggurat2020_3_":
|
|
||||||
i = 0
|
|
||||||
for portal in two_plus:
|
|
||||||
if portal.scene() == "ziggurat2020_3":
|
|
||||||
i += 1
|
|
||||||
if i == 2:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Quarry teleporter needs you to hit the Darkwoods fuse
|
|
||||||
# Since it's physically in Quarry, we don't need to check for it
|
|
||||||
elif check_portal.scene_destination() in {"Quarry Redux, Transit_teleporter_quarry teleporter",
|
|
||||||
"Quarry Redux, ziggurat2020_0_"}:
|
|
||||||
i = 0
|
|
||||||
for portal in two_plus:
|
|
||||||
if portal.scene() == "Darkwoods Tunnel":
|
|
||||||
i += 1
|
|
||||||
if i == 2:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Same as above, but Quarry isn't guaranteed here
|
|
||||||
elif check_portal.scene_destination() == "Transit, Quarry Redux_teleporter_quarry teleporter":
|
|
||||||
i = j = 0
|
|
||||||
for portal in two_plus:
|
|
||||||
if portal.scene() == "Darkwoods Tunnel":
|
|
||||||
i += 1
|
|
||||||
if portal.scene() == "Quarry Redux":
|
|
||||||
j += 1
|
|
||||||
if i == 2 or j == 7:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Need Library fuse to use this teleporter
|
|
||||||
elif check_portal.scene_destination() == "Transit, Library Lab_teleporter_library teleporter":
|
|
||||||
i = 0
|
|
||||||
for portal in two_plus:
|
|
||||||
if portal.scene() == "Library Lab":
|
|
||||||
i += 1
|
|
||||||
if i == 3:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# Need West Garden fuse to use this teleporter
|
|
||||||
elif check_portal.scene_destination() == "Transit, Archipelagos Redux_teleporter_archipelagos_teleporter":
|
|
||||||
i = 0
|
|
||||||
for portal in two_plus:
|
|
||||||
if portal.scene() == "Archipelagos Redux":
|
|
||||||
i += 1
|
|
||||||
if i == 6:
|
|
||||||
return True
|
|
||||||
|
|
||||||
# false means you're good to place the portal
|
|
||||||
return False
|
|
||||||
|
|
|
@ -237,6 +237,8 @@ extra_groups: Dict[str, Set[str]] = {
|
||||||
"Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't
|
"Ladder to Atoll": {"Ladder to Ruined Atoll"}, # fuzzy matching made it hint Ladders in Well, now it won't
|
||||||
"Ladders to Bell": {"Ladders to West Bell"},
|
"Ladders to Bell": {"Ladders to West Bell"},
|
||||||
"Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell
|
"Ladders to Well": {"Ladders in Well"}, # fuzzy matching decided ladders in well was ladders to west bell
|
||||||
|
"Ladders in Atoll": {"Ladders in South Atoll"},
|
||||||
|
"Ladders in Ruined Atoll": {"Ladders in South Atoll"},
|
||||||
}
|
}
|
||||||
|
|
||||||
item_name_groups.update(extra_groups)
|
item_name_groups.update(extra_groups)
|
||||||
|
|
|
@ -86,7 +86,7 @@ location_table: Dict[str, TunicLocationData] = {
|
||||||
"Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"),
|
"Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"),
|
||||||
"Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
"Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||||
"Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
"Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||||
"Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Front"),
|
"Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Main"),
|
||||||
"Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
"Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||||
"Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
"Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||||
"Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
"Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||||
|
|
|
@ -118,7 +118,8 @@ class EntranceRando(TextChoice):
|
||||||
|
|
||||||
|
|
||||||
class FixedShop(Toggle):
|
class FixedShop(Toggle):
|
||||||
"""Forces the Windmill entrance to lead to a shop, and places only one other shop in the pool.
|
"""Forces the Windmill entrance to lead to a shop, and removes the remaining shops from the pool.
|
||||||
|
Adds another entrance in Rooted Ziggurat Lower to keep an even number of entrances.
|
||||||
Has no effect if Entrance Rando is not enabled."""
|
Has no effect if Entrance Rando is not enabled."""
|
||||||
internal_name = "fixed_shop"
|
internal_name = "fixed_shop"
|
||||||
display_name = "Fewer Shops in Entrance Rando"
|
display_name = "Fewer Shops in Entrance Rando"
|
||||||
|
@ -126,8 +127,7 @@ class FixedShop(Toggle):
|
||||||
|
|
||||||
class LaurelsLocation(Choice):
|
class LaurelsLocation(Choice):
|
||||||
"""Force the Hero's Laurels to be placed at a location in your world.
|
"""Force the Hero's Laurels to be placed at a location in your world.
|
||||||
For if you want to avoid or specify early or late Laurels.
|
For if you want to avoid or specify early or late Laurels."""
|
||||||
If you use the 10 Fairies option in Entrance Rando, Secret Gathering Place will be at its vanilla entrance."""
|
|
||||||
internal_name = "laurels_location"
|
internal_name = "laurels_location"
|
||||||
display_name = "Laurels Location"
|
display_name = "Laurels Location"
|
||||||
option_anywhere = 0
|
option_anywhere = 0
|
||||||
|
@ -147,6 +147,7 @@ class ShuffleLadders(Toggle):
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class TunicOptions(PerGameCommonOptions):
|
class TunicOptions(PerGameCommonOptions):
|
||||||
|
start_inventory_from_pool: StartInventoryPool
|
||||||
sword_progression: SwordProgression
|
sword_progression: SwordProgression
|
||||||
start_with_sword: StartWithSword
|
start_with_sword: StartWithSword
|
||||||
keys_behind_bosses: KeysBehindBosses
|
keys_behind_bosses: KeysBehindBosses
|
||||||
|
@ -162,4 +163,3 @@ class TunicOptions(PerGameCommonOptions):
|
||||||
lanternless: Lanternless
|
lanternless: Lanternless
|
||||||
maskless: Maskless
|
maskless: Maskless
|
||||||
laurels_location: LaurelsLocation
|
laurels_location: LaurelsLocation
|
||||||
start_inventory_from_pool: StartInventoryPool
|
|
||||||
|
|
Loading…
Reference in New Issue