mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-27 14:59:56 -07:00
TUNIC: Entrance rando Direction Pairs + Decoupled (#3761)
* Fix merge conflict * Fix formatting, fix rule for heir access after merge * Writing combat logic helpers * More helpers! * More logic! * Rename has_stick to has_melee, some fixes per Medic's review * Clamp max power from sword upgrades * Wrote the rest of the helpers * Remove unused import * Apply item classifications * Create the combat logic option * Item classification varies based on option * Add the shop sword logic stuff in * Add the rules for the boss-only option * Fix tiny issues * Some early Overworld combat logic * Fill out swamp combat logic * Add note * Bump up Boss Scav and Heir * More revisions to combat logic * Some changes, currently broken * New system for power, kinda jank probably * Revisions to new system, needs more balancing * Cap attack upgrades * Uncap mp power since it's directly related to damage output * Voidlings * Put together a table showing the vanilla-expected stats for each area * Added some info on potion counts * Made new helper functions * Make has_required_stats * Make has_combat_reqs * Update er_rules for new combat reqs * Fix all the broken things ever * Remove outdated todo * Make temp option for testing logic * More flexible choices for combat items * Hard require sword for bosses * Temporarily default combat logic to on * Finish writing overworld combat logic * East Forest combat logic done * Remove a few easy ones * Finish beneath the well * Dark Tomb combat logic * West Garden combat logic * make unit tests checkmark again * Weird west garden dagger house edge case * Try block for that weird west garden edge case * Add quarry combat logic * Update to filter out unreachable regions outside of ER * Fortress Grave Path logic, and a couple fixes to the west garden logic * Fortress east shortcut logic, and rewriting the try except blocks to use finally * Refactor to use a new function cause wow there was a lot of repeated code * Add combat logic to the other two sets of fortress fuses * Add combat rules to beneath the vault * Fix missing cathedral -> elevator connection * Combat logic for cathedral to elevator * Add cathedral main region, rename cathedral -> cathedral entry * Setup cathedral combat logic * Adjust locations' regions for ER * Add laurels zip logic to the chest in the spike room in cathedral * Add combat logic to frog's domain * Move frog's domain locations to regions for combat logic * Add new frog's domain regions for combat logic * Update region name for frog's domain * Fix typo * Add more regions for lower zig * Move around lower zig regions for combat logic * Lower Zig combat logic * Upper zig combat logic * Fix typo * Fix typos * Fix missing world. * Update combat logic description * Add todo * Add todo * Don't make zig skip if er or fixed shop is off * Make it so zig skip is only made with fewer shops and er * Temporarily default combat logic on * Update test to explicitly disable combat logic * Update test_access.py * Slight wording changes * Fix bugs, refactor quarry regions so you can access chests in lower quarry with ice grapples * Run through checks you can do with magic dagger * Run through checks you can do with magic dagger * Add rule for entering town portal of having equipment to deal with enemies * Add rule for atoll near the 6 crabs surrounding a poor defenseless baby slorm * Update the rule for the chest near the 6 crabs surrounding a slorm to also possibly require laurels * Revamp combat logic function to work properly without melee * Add laurels rules to combat logic chests * Modify beneath the vault bridge rule to need a lantern if combat logic is on * Put in money logic * Dagger or combat for swamp big skeleton chest * Remove the 100 moneys from logic * Modify lower zig ls drop region destinations * Remove completed todo * Reword combat logic option description, remove test option * Add combat logic to slot data * Merge Silent's missing slot data bugfix PR #3628 * Remove test combat option * Update combat logic description * Fix secret gathering place issue * Fix secret gathering place issue * Fix lower zig ls rule * Fix accidentally removed librarian rule * Remove redundant rule * Update gauntlet rule to hard-require a sword * Add test for a problematic connection * Adjust combat logic to deal with weird edge cases so it doesn't take stuff out of logic that was previously in logic * Fix create_item classification * Update some comments * Update per exempt's suggestion * Add combat logic to the well boss fight, reorder the combat logic stuff a little to better section them off * Add EntranceLayout option * Add back LogicRules as an invisible option, to not break old yamls * Fix a bug with seed group, continue changing fixed shop to entrance layout * Fix missed fixed shop -> entrance layout spot * Fix bug in seed groups with fixed shop on and off * Add entrance layout to the UT regen stuff * Put direction. in, will add them later * Remove unused elevation from portal class * Got like half of them in * Finish adding all of the directions * Add combat rule for zig front to back * Update per Medic's suggestion * Update ladder storage without items option description * Mess with state with collect and remove to save like 2 seconds (never again) * Save even more time, still never going to do this again on anything else * Add option check for collect and remove * Add directions to shop portals * Update direction in Portal with default * Move Direction above Portal * Add decoupled option, mess with plando connection stuff * Merge, implement verify plando directions * Condense the stuff in change and remove to less lines (thanks medic) * Remove unused thing * Swap to using logicmixin instead of prog_items (thanks Vi) * Fix consistency in stat counters * Add back something that was needed * Fix mistake when adding back * Making the fix better (thanks medic) * Make it actually return false if it gets to the backup lists and fails them * Fix stuff after merge * Add outlet regions, create new regions as needed for them * Put together part of decoupled and direction pairs * make direction pairs work * Make decoupled work * Make fixed shop work again * Fix a few minor bugs * Fix a few minor bugs * Fix plando * god i love programming * Reorder portal list * Update portal sorter for variable shops * Add missing parameter * Some cleanup of prints and functions * Fix typo * it's aliiiiiive * Make seed groups not sync decoupled * Add test with full-shop plando * Fix bug with vanilla portals * Handle plando connections and direction pair errors * Update plando checking for decoupled * Fix typo * Fix exception text to be shorter * Add some more comments * Add todo note * Remove unused safety thing * Remove extra plando connections definition in options * Make seed groups in decoupled with overlapping but not fully overlapped plando connections interact nicely without messing with what the entrances look like in the spoiler log * Fix weird edge case that is technically user error * Add note to fixed shop * Fix parsing shop names in UT * Remove debug print * Actually make UT work * multiworld. to world. * Fix typo from merge * Make it so the shops show up in the entrance hints * Fix bug in ladder storage rules * Remove blank line * # Conflicts: # worlds/tunic/__init__.py # worlds/tunic/er_data.py # worlds/tunic/er_rules.py # worlds/tunic/er_scripts.py # worlds/tunic/rules.py # worlds/tunic/test/test_access.py * Fix issues after merge * Update plando connections stuff in docs * Fix library mistake * has_stick -> has_melee * has_stick -> has_melee * Add a failsafe for direction pairing * Fix playthrough crash bug * Remove init from logicmixin * Updates per code review (thanks hesto) * has_stick to has_melee in newer update * has_stick to has_melee in newer update * # Conflicts: # worlds/tunic/__init__.py # worlds/tunic/combat_logic.py # worlds/tunic/er_data.py # worlds/tunic/er_rules.py # worlds/tunic/er_scripts.py * Cleanup more stuff after merge * Revert "Cleanup more stuff after merge" This reverts commita6ee9a93da. * Revert "# Conflicts:" This reverts commitc74ccd74a4. * Cleanup more stuff after merge * Swap to .get for decoupled so it works with older games probably maybe * Fix after merge * Fix typo * Fix UT support with fixed shop option * Backport plando connections fix * Fix issue with fixed shop + decoupled * Make the error not duplicate the while loop condition * Fix rule for quarry back to monastery * Fix more stuff after merge * Make it not output anything if you set plando connections but not ER * Add obvious note to plando connections description * Fix after merge * add comment to commented out connection --------- Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
+465
-169
@@ -1,11 +1,12 @@
|
||||
from typing import Dict, List, Set, Tuple, TYPE_CHECKING
|
||||
from BaseClasses import Region, ItemClassification, Item, Location
|
||||
from .locations import all_locations
|
||||
from .er_data import Portal, portal_mapping, traversal_requirements, DeadEnd, RegionInfo
|
||||
from .er_data import (Portal, portal_mapping, traversal_requirements, DeadEnd, Direction, RegionInfo,
|
||||
get_portal_outlet_region)
|
||||
from .er_rules import set_er_region_rules
|
||||
from .breakables import create_breakable_exclusive_regions, set_breakable_location_rules
|
||||
from Options import PlandoConnection
|
||||
from .options import EntranceRando
|
||||
from .options import EntranceRando, EntranceLayout
|
||||
from random import Random
|
||||
from copy import deepcopy
|
||||
|
||||
@@ -23,17 +24,18 @@ class TunicERLocation(Location):
|
||||
|
||||
def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||
regions: Dict[str, Region] = {}
|
||||
world.used_shop_numbers = set()
|
||||
|
||||
for region_name, region_data in world.er_regions.items():
|
||||
if world.options.entrance_rando and region_name == "Zig Skip Exit":
|
||||
# need to check if there's a seed group for this first
|
||||
if world.options.entrance_rando.value not in EntranceRando.options.values():
|
||||
if not world.seed_groups[world.options.entrance_rando.value]["fixed_shop"]:
|
||||
if world.seed_groups[world.options.entrance_rando.value]["entrance_layout"] != EntranceLayout.option_fixed_shop:
|
||||
continue
|
||||
elif not world.options.fixed_shop:
|
||||
elif world.options.entrance_layout != EntranceLayout.option_fixed_shop:
|
||||
continue
|
||||
if not world.options.entrance_rando and region_name in ("Zig Skip Exit", "Purgatory"):
|
||||
continue
|
||||
|
||||
region = Region(region_name, world.player, world.multiworld)
|
||||
regions[region_name] = region
|
||||
world.multiworld.regions.append(region)
|
||||
@@ -46,13 +48,18 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||
portal_pairs = pair_portals(world, regions)
|
||||
|
||||
# output the entrances to the spoiler log here for convenience
|
||||
sorted_portal_pairs = sort_portals(portal_pairs)
|
||||
for portal1, portal2 in sorted_portal_pairs.items():
|
||||
world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player)
|
||||
sorted_portal_pairs = sort_portals(portal_pairs, world)
|
||||
if not world.options.decoupled:
|
||||
for portal1, portal2 in sorted_portal_pairs.items():
|
||||
world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player)
|
||||
else:
|
||||
for portal1, portal2 in sorted_portal_pairs.items():
|
||||
world.multiworld.spoiler.set_entrance(portal1, portal2, "entrance", world.player)
|
||||
|
||||
else:
|
||||
portal_pairs = vanilla_portals(world, regions)
|
||||
|
||||
create_randomized_entrances(portal_pairs, regions)
|
||||
create_randomized_entrances(world, portal_pairs, regions)
|
||||
|
||||
set_er_region_rules(world, regions, portal_pairs)
|
||||
|
||||
@@ -75,6 +82,7 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||
return portal_pairs
|
||||
|
||||
|
||||
# keys are event names, values are event regions
|
||||
tunic_events: Dict[str, str] = {
|
||||
"Eastern Bell": "Forest Belltower Upper",
|
||||
"Western Bell": "Overworld Belltower at Bell",
|
||||
@@ -111,17 +119,31 @@ def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None:
|
||||
region.locations.append(location)
|
||||
|
||||
|
||||
# keeping track of which shop numbers have been used already to avoid duplicates
|
||||
# due to plando, shops can be added out of order, so a set is the best way to make this work smoothly
|
||||
def get_shop_num(world: "TunicWorld") -> int:
|
||||
portal_num = -1
|
||||
for i in range(500):
|
||||
if i + 1 not in world.used_shop_numbers:
|
||||
portal_num = i + 1
|
||||
world.used_shop_numbers.add(portal_num)
|
||||
break
|
||||
if portal_num == -1:
|
||||
raise Exception(f"TUNIC: {world.player_name} has plando'd too many shops.")
|
||||
return portal_num
|
||||
|
||||
|
||||
# all shops are the same shop. however, you cannot get to all shops from the same shop entrance.
|
||||
# so, we need a bunch of shop regions that connect to the actual shop, but the actual shop cannot connect back
|
||||
def create_shop_region(world: "TunicWorld", regions: Dict[str, Region]) -> None:
|
||||
new_shop_name = f"Shop {world.shop_num}"
|
||||
def create_shop_region(world: "TunicWorld", regions: Dict[str, Region], portal_num) -> None:
|
||||
new_shop_name = f"Shop {portal_num}"
|
||||
world.er_regions[new_shop_name] = RegionInfo("Shop", dead_end=DeadEnd.all_cats)
|
||||
new_shop_region = Region(new_shop_name, world.player, world.multiworld)
|
||||
new_shop_region.connect(regions["Shop"])
|
||||
regions[new_shop_name] = new_shop_region
|
||||
world.shop_num += 1
|
||||
|
||||
|
||||
# for non-ER that uses the ER rules, we create a vanilla set of portal pairs
|
||||
def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]:
|
||||
portal_pairs: Dict[Portal, Portal] = {}
|
||||
# we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here
|
||||
@@ -135,9 +157,10 @@ def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Por
|
||||
portal2_sdt = portal1.destination_scene()
|
||||
|
||||
if portal2_sdt.startswith("Shop,"):
|
||||
portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}",
|
||||
destination="Previous Region", tag="_")
|
||||
create_shop_region(world, regions)
|
||||
portal_num = get_shop_num(world)
|
||||
portal2 = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}",
|
||||
destination=str(portal_num), tag="_", direction=Direction.none)
|
||||
create_shop_region(world, regions, portal_num)
|
||||
|
||||
for portal in portal_map:
|
||||
if portal.scene_destination() == portal2_sdt:
|
||||
@@ -152,7 +175,13 @@ def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Por
|
||||
return portal_pairs
|
||||
|
||||
|
||||
# pairing off portals, starting with dead ends
|
||||
# the really long function that gives us our portal pairs
|
||||
# before we start pairing, we separate the portals into dead ends and non-dead ends (two_plus)
|
||||
# then, we do a few other important tasks to accommodate options and seed gropus
|
||||
# first phase: pick a two_plus in a reachable region and non-reachable region and pair them
|
||||
# repeat this phase until all regions are reachable
|
||||
# second phase: randomly pair dead ends to random two_plus
|
||||
# third phase: randomly pair the remaining two_plus to each other
|
||||
def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]:
|
||||
portal_pairs: Dict[Portal, Portal] = {}
|
||||
dead_ends: List[Portal] = []
|
||||
@@ -162,8 +191,9 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
laurels_zips = world.options.laurels_zips.value
|
||||
ice_grappling = world.options.ice_grappling.value
|
||||
ladder_storage = world.options.ladder_storage.value
|
||||
fixed_shop = world.options.fixed_shop
|
||||
entrance_layout = world.options.entrance_layout
|
||||
laurels_location = world.options.laurels_location
|
||||
decoupled = world.options.decoupled
|
||||
traversal_reqs = deepcopy(traversal_requirements)
|
||||
has_laurels = True
|
||||
waterfall_plando = False
|
||||
@@ -174,7 +204,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
laurels_zips = seed_group["laurels_zips"]
|
||||
ice_grappling = seed_group["ice_grappling"]
|
||||
ladder_storage = seed_group["ladder_storage"]
|
||||
fixed_shop = seed_group["fixed_shop"]
|
||||
entrance_layout = seed_group["entrance_layout"]
|
||||
laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
|
||||
|
||||
logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage)
|
||||
@@ -183,15 +213,18 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
if laurels_location == "10_fairies" and not world.using_ut:
|
||||
has_laurels = False
|
||||
|
||||
shop_count = 6
|
||||
if fixed_shop:
|
||||
shop_count = 0
|
||||
else:
|
||||
# if fixed shop is off, remove this portal
|
||||
for portal in portal_map:
|
||||
if portal.region == "Zig Skip Exit":
|
||||
portal_map.remove(portal)
|
||||
break
|
||||
# for the direction pairs option with decoupled off
|
||||
# tracks how many portals are in each direction in each list
|
||||
two_plus_direction_tracker: Dict[int, int] = {direction: 0 for direction in range(8)}
|
||||
dead_end_direction_tracker: Dict[int, int] = {direction: 0 for direction in range(8)}
|
||||
|
||||
# for ensuring we have enough entrances in directions left that we don't leave dead ends without any
|
||||
def too_few_portals_for_direction_pairs(direction: int, offset: int) -> bool:
|
||||
if two_plus_direction_tracker[direction] <= (dead_end_direction_tracker[direction_pairs[direction]] + offset):
|
||||
return False
|
||||
if two_plus_direction_tracker[direction_pairs[direction]] <= dead_end_direction_tracker[direction] + offset:
|
||||
return False
|
||||
return True
|
||||
|
||||
# If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit
|
||||
if world.using_ut:
|
||||
@@ -202,25 +235,59 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
dead_end_status = world.er_regions[portal.region].dead_end
|
||||
if dead_end_status == DeadEnd.free:
|
||||
two_plus.append(portal)
|
||||
two_plus_direction_tracker[portal.direction] += 1
|
||||
elif dead_end_status == DeadEnd.all_cats:
|
||||
dead_ends.append(portal)
|
||||
dead_end_direction_tracker[portal.direction] += 1
|
||||
elif dead_end_status == DeadEnd.restricted:
|
||||
if ice_grappling:
|
||||
two_plus.append(portal)
|
||||
two_plus_direction_tracker[portal.direction] += 1
|
||||
else:
|
||||
dead_ends.append(portal)
|
||||
dead_end_direction_tracker[portal.direction] += 1
|
||||
# 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)
|
||||
two_plus_direction_tracker[portal.direction] += 1
|
||||
else:
|
||||
dead_ends.append(portal)
|
||||
dead_end_direction_tracker[portal.direction] += 1
|
||||
if portal.region == "Zig Skip Exit" and entrance_layout == EntranceLayout.option_fixed_shop:
|
||||
# direction isn't meaningful here since zig skip cannot be in direction pairs mode
|
||||
two_plus.append(portal)
|
||||
|
||||
# now we generate the shops and add them to the dead ends list
|
||||
shop_count = 6
|
||||
if entrance_layout == EntranceLayout.option_fixed_shop:
|
||||
shop_count = 0
|
||||
else:
|
||||
# if fixed shop is off, remove this portal
|
||||
for portal in portal_map:
|
||||
if portal.region == "Zig Skip Exit":
|
||||
if fixed_shop:
|
||||
two_plus.append(portal)
|
||||
else:
|
||||
dead_ends.append(portal)
|
||||
portal_map.remove(portal)
|
||||
break
|
||||
# need 8 shops with direction pairs or there won't be a valid set of pairs
|
||||
if entrance_layout == EntranceLayout.option_direction_pairs:
|
||||
shop_count = 8
|
||||
|
||||
# for universal tracker, we want to skip shop gen since it's essentially full plando
|
||||
if world.using_ut:
|
||||
shop_count = 0
|
||||
|
||||
for _ in range(shop_count):
|
||||
# 6 of the shops have south exits, 2 of them have west exits
|
||||
portal_num = get_shop_num(world)
|
||||
shop_dir = Direction.south
|
||||
if portal_num > 6:
|
||||
shop_dir = Direction.west
|
||||
shop_portal = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}",
|
||||
destination=str(portal_num), tag="_", direction=shop_dir)
|
||||
create_shop_region(world, regions, portal_num)
|
||||
dead_ends.append(shop_portal)
|
||||
dead_end_direction_tracker[shop_portal.direction] += 1
|
||||
|
||||
connected_regions: Set[str] = set()
|
||||
# make better start region stuff when/if implementing random start
|
||||
@@ -249,29 +316,68 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
portal_name2 = portal.name
|
||||
# connected_regions.update(add_dependent_regions(portal.region, logic_rules))
|
||||
# shops have special handling
|
||||
if not portal_name2 and portal2 == "Shop, Previous Region_":
|
||||
portal_name2 = "Shop Portal"
|
||||
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
|
||||
if not portal_name1 and portal1.startswith("Shop"):
|
||||
# it should show up as "Shop, 1_" for shop 1
|
||||
portal_name1 = "Shop Portal " + str(portal1).split(", ")[1].split("_")[0]
|
||||
if not portal_name2 and portal2.startswith("Shop"):
|
||||
portal_name2 = "Shop Portal " + str(portal2).split(", ")[1].split("_")[0]
|
||||
if world.options.decoupled:
|
||||
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "entrance"))
|
||||
else:
|
||||
plando_connections.append(PlandoConnection(portal_name1, portal_name2, "both"))
|
||||
|
||||
# put together the list of non-deadend regions
|
||||
non_dead_end_regions = set()
|
||||
for region_name, region_info in world.er_regions.items():
|
||||
if not region_info.dead_end:
|
||||
# these are not real regions, they are just here to be descriptive
|
||||
if region_info.is_fake_region or region_name == "Shop":
|
||||
continue
|
||||
# dead ends aren't real in decoupled
|
||||
if decoupled:
|
||||
non_dead_end_regions.add(region_name)
|
||||
elif not region_info.dead_end:
|
||||
non_dead_end_regions.add(region_name)
|
||||
# if ice grappling to places is in logic, both places stop being dead ends
|
||||
elif region_info.dead_end == DeadEnd.restricted and ice_grappling:
|
||||
non_dead_end_regions.add(region_name)
|
||||
# secret gathering place and zig skip get weird, special handling
|
||||
# secret gathering place is treated as a non-dead end if 10 fairies is on to assure non-laurels access to it
|
||||
elif region_info.dead_end == DeadEnd.special:
|
||||
if (region_name == "Secret Gathering Place" and laurels_location == "10_fairies") \
|
||||
or (region_name == "Zig Skip Exit" and fixed_shop):
|
||||
if region_name == "Secret Gathering Place" and laurels_location == "10_fairies":
|
||||
non_dead_end_regions.add(region_name)
|
||||
|
||||
if decoupled:
|
||||
# add the dead ends to the two plus list, since dead ends aren't real in decoupled
|
||||
two_plus.extend(dead_ends)
|
||||
dead_ends.clear()
|
||||
# if decoupled is on, we make a second two_plus list, where the first is entrances and the second is exits
|
||||
two_plus2 = two_plus.copy()
|
||||
else:
|
||||
# if decoupled is off, the two lists are the same list, since entrances and exits are intertwined
|
||||
two_plus2 = two_plus
|
||||
|
||||
if plando_connections:
|
||||
for connection in plando_connections:
|
||||
if decoupled:
|
||||
modified_plando_connections = plando_connections.copy()
|
||||
for index, cxn in enumerate(modified_plando_connections):
|
||||
# it's much easier if we split both-direction portals into two one-ways in decoupled
|
||||
if cxn.direction == "both":
|
||||
replacement1 = PlandoConnection(cxn.entrance, cxn.exit, "entrance")
|
||||
replacement2 = PlandoConnection(cxn.exit, cxn.entrance, "entrance")
|
||||
modified_plando_connections.remove(cxn)
|
||||
modified_plando_connections.insert(index, replacement1)
|
||||
modified_plando_connections.append(replacement2)
|
||||
else:
|
||||
modified_plando_connections = plando_connections
|
||||
|
||||
connected_shop_portal1s: Set[int] = set()
|
||||
connected_shop_portal2s: Set[int] = set()
|
||||
for connection in modified_plando_connections:
|
||||
p_entrance = connection.entrance
|
||||
p_exit = connection.exit
|
||||
# if you plando secret gathering place, need to know that during portal pairing
|
||||
if "Secret Gathering Place Exit" in [p_entrance, p_exit]:
|
||||
if p_exit == "Secret Gathering Place Exit":
|
||||
waterfall_plando = True
|
||||
if p_entrance == "Secret Gathering Place Exit" and not decoupled:
|
||||
waterfall_plando = True
|
||||
portal1_dead_end = True
|
||||
portal2_dead_end = True
|
||||
@@ -279,118 +385,186 @@ 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:
|
||||
two_plus.remove(portal1)
|
||||
else:
|
||||
# if not both, they're both dead ends
|
||||
if not portal2:
|
||||
if not portal2 and not decoupled:
|
||||
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.")
|
||||
f"plando connections -- {connection.entrance} to {connection.exit}")
|
||||
|
||||
for portal in dead_ends:
|
||||
if p_entrance == portal.name:
|
||||
portal1 = portal
|
||||
dead_ends.remove(portal1)
|
||||
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:
|
||||
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]:
|
||||
pdir = Direction.east
|
||||
else:
|
||||
pdir = Direction.none
|
||||
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)
|
||||
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, it might be a shop
|
||||
if p_exit == "Shop Portal":
|
||||
portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}",
|
||||
destination="Previous Region", tag="_")
|
||||
create_shop_region(world, regions)
|
||||
shop_count -= 1
|
||||
# need to maintain an even number of portals total
|
||||
if shop_count < 0:
|
||||
shop_count += 2
|
||||
# and if it's neither shop nor dead end, it just isn't correct
|
||||
# if it's not a dead end, maybe it's a plando'd shop portal that doesn't normally exist
|
||||
else:
|
||||
if not portal2:
|
||||
raise Exception(f"Could not find entrance named {p_exit} for "
|
||||
f"plando connections in {player_name}'s YAML.\n"
|
||||
f"If you are using Universal Tracker, the most likely reason for this error "
|
||||
f"is that the host generated with a newer version of the APWorld.\n"
|
||||
f"Please check the TUNIC Randomizer Github and place the newest APWorld in your "
|
||||
f"custom_worlds folder, and remove the one in lib/worlds if there is one there.")
|
||||
dead_ends.remove(portal2)
|
||||
if p_exit.startswith("Shop Portal "):
|
||||
portal_num = int(p_exit.split("Shop Portal ")[-1])
|
||||
if portal_num <= 6:
|
||||
pdir = Direction.south
|
||||
elif portal_num in [7, 8]:
|
||||
pdir = Direction.east
|
||||
else:
|
||||
pdir = Direction.none
|
||||
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)
|
||||
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)] = []
|
||||
# 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 portal1_dead_end:
|
||||
dead_end_direction_tracker[portal1.direction] -= 1
|
||||
else:
|
||||
two_plus_direction_tracker[portal1.direction] -= 1
|
||||
if portal2_dead_end:
|
||||
dead_end_direction_tracker[portal2.direction] -= 1
|
||||
else:
|
||||
two_plus_direction_tracker[portal2.direction] -= 1
|
||||
|
||||
# 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 fixed_shop and not world.using_ut:
|
||||
portal1 = None
|
||||
# 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)
|
||||
shop_portal = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}",
|
||||
destination=str(portal_num), tag="_", direction=Direction.none)
|
||||
create_shop_region(world, regions, portal_num)
|
||||
dead_ends.append(shop_portal)
|
||||
|
||||
if entrance_layout == EntranceLayout.option_fixed_shop and not world.using_ut:
|
||||
windmill = None
|
||||
for portal in two_plus:
|
||||
if portal.scene_destination() == "Overworld Redux, Windmill_":
|
||||
portal1 = portal
|
||||
windmill = portal
|
||||
break
|
||||
if not portal1:
|
||||
raise Exception(f"Failed to do Fixed Shop option. "
|
||||
f"Did {player_name} plando connection the Windmill Shop entrance?")
|
||||
if not windmill:
|
||||
raise Exception(f"Failed to do Fixed Shop option for Entrance Layout. "
|
||||
f"Did {player_name} plando the Windmill Shop entrance?")
|
||||
|
||||
portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}",
|
||||
destination="Previous Region", tag="_")
|
||||
create_shop_region(world, regions)
|
||||
portal_num = get_shop_num(world)
|
||||
shop = Portal(name=f"Shop Portal {portal_num}", region=f"Shop {portal_num}",
|
||||
destination=str(portal_num), tag="_", direction=Direction.south)
|
||||
create_shop_region(world, regions, portal_num)
|
||||
|
||||
portal_pairs[portal1] = portal2
|
||||
two_plus.remove(portal1)
|
||||
portal_pairs[windmill] = shop
|
||||
two_plus.remove(windmill)
|
||||
if decoupled:
|
||||
two_plus.append(shop)
|
||||
non_dead_end_regions.add(shop.region)
|
||||
connected_regions.add(shop.region)
|
||||
|
||||
random_object: Random = world.random
|
||||
# use the seed given in the options to shuffle the portals
|
||||
if isinstance(world.options.entrance_rando.value, str):
|
||||
random_object = Random(world.options.entrance_rando.value)
|
||||
else:
|
||||
random_object: Random = world.random
|
||||
|
||||
# we want to start by making sure every region is accessible
|
||||
random_object.shuffle(two_plus)
|
||||
check_success = 0
|
||||
|
||||
# this is a backup in case we run into that rare direction pairing failure
|
||||
# so that we don't have to redo the plando bit basically
|
||||
backup_connected_regions = connected_regions.copy()
|
||||
backup_portal_pairs = portal_pairs.copy()
|
||||
backup_two_plus = two_plus.copy()
|
||||
backup_two_plus_direction_tracker = two_plus_direction_tracker.copy()
|
||||
rare_failure_count = 0
|
||||
|
||||
portal1 = None
|
||||
portal2 = None
|
||||
previous_conn_num = 0
|
||||
@@ -403,96 +577,182 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
# should, hopefully, only ever occur if someone plandos connections poorly
|
||||
if previous_conn_num == len(connected_regions):
|
||||
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 errors. "
|
||||
"Unconnected regions:", non_dead_end_regions - connected_regions)
|
||||
f"Unconnected regions: {non_dead_end_regions - connected_regions}.\n"
|
||||
f"Unconnected portals: {[portal.name for portal in two_plus]}")
|
||||
if (fail_count > 100 and not decoupled
|
||||
and (world.options.entrance_layout == EntranceLayout.option_direction_pairs or waterfall_plando)):
|
||||
# in direction pairs, we may run into a case where we run out of pairable directions
|
||||
# since we need to ensure the dead ends will have something to connect to
|
||||
# or if fairy cave is plando'd, it may run into an issue where it is trying to get access to 2 separate
|
||||
# areas at once to give access to laurels
|
||||
# so, this is basically just resetting entrance pairing
|
||||
# this should be very rare, so this fail-safe shouldn't be covering up for an actual solution
|
||||
# this should never happen in decoupled, since it's entirely too flexible for that
|
||||
portal_pairs = backup_portal_pairs.copy()
|
||||
two_plus = two_plus2 = backup_two_plus.copy()
|
||||
two_plus_direction_tracker = backup_two_plus_direction_tracker.copy()
|
||||
random_object.shuffle(two_plus)
|
||||
connected_regions = backup_connected_regions.copy()
|
||||
rare_failure_count += 1
|
||||
fail_count = 0
|
||||
|
||||
if rare_failure_count > 100:
|
||||
raise Exception(f"Failed to pair regions due to rare pairing issues for {player_name}. "
|
||||
f"Unconnected regions: {non_dead_end_regions - connected_regions}.\n"
|
||||
f"Unconnected portals: {[portal.name for portal in two_plus]}")
|
||||
else:
|
||||
fail_count = 0
|
||||
previous_conn_num = len(connected_regions)
|
||||
|
||||
# find a portal in a connected region
|
||||
if check_success == 0:
|
||||
for portal in two_plus:
|
||||
if portal.region in connected_regions:
|
||||
portal1 = portal
|
||||
two_plus.remove(portal)
|
||||
check_success = 1
|
||||
break
|
||||
for portal in two_plus:
|
||||
if portal.region in connected_regions:
|
||||
# if there's more dead ends of a direction than two plus of the opposite direction,
|
||||
# then we'll run out of viable connections for those dead ends later
|
||||
# decoupled does not have this issue since dead ends aren't real in decoupled
|
||||
if not decoupled and entrance_layout == EntranceLayout.option_direction_pairs:
|
||||
if not too_few_portals_for_direction_pairs(portal.direction, 0):
|
||||
continue
|
||||
|
||||
# then we find a portal in an inaccessible region
|
||||
if check_success == 1:
|
||||
for portal in two_plus:
|
||||
if portal.region not in connected_regions:
|
||||
# if secret gathering place happens to get paired really late, you can end up running out
|
||||
if not has_laurels and len(two_plus) < 80:
|
||||
# if you plando'd secret gathering place with laurels at 10 fairies, you're the reason for this
|
||||
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_tricks):
|
||||
continue
|
||||
# if not waterfall_plando, then we just want to pair secret gathering place now
|
||||
elif portal.region != "Secret Gathering Place":
|
||||
portal1 = portal
|
||||
two_plus.remove(portal)
|
||||
break
|
||||
if not portal1:
|
||||
raise Exception("TUNIC: Failed to pair portals at first part of first phase.")
|
||||
|
||||
# then we find a portal in an unconnected region
|
||||
for portal in two_plus2:
|
||||
if portal.region not in connected_regions:
|
||||
# if secret gathering place happens to get paired really late, you can end up running out
|
||||
if not has_laurels and len(two_plus2) < 80:
|
||||
# if you plando'd secret gathering place with laurels at 10 fairies, you're the reason for this
|
||||
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_tricks):
|
||||
continue
|
||||
portal2 = portal
|
||||
connected_regions.add(portal.region)
|
||||
two_plus.remove(portal)
|
||||
check_success = 2
|
||||
break
|
||||
# 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
|
||||
# this cuts down on using the failsafe significantly
|
||||
if not decoupled and entrance_layout == EntranceLayout.option_direction_pairs:
|
||||
should_continue = False
|
||||
# these portals are weird since they're one-ways essentially
|
||||
# we need to make sure they are connected in this first phase
|
||||
south_problems = ["Ziggurat Upper to Ziggurat Entry Hallway",
|
||||
"Ziggurat Tower to Ziggurat Upper", "Forest Belltower to Guard Captain Room"]
|
||||
if (portal.direction == Direction.south and portal.name not in south_problems
|
||||
and not too_few_portals_for_direction_pairs(portal.direction, 3)):
|
||||
for test_portal in two_plus:
|
||||
if test_portal.name in south_problems:
|
||||
should_continue = True
|
||||
# at risk of connecting frog's domain entry ladder to librarian exit
|
||||
if (portal.direction == Direction.ladder_down
|
||||
or portal.direction == Direction.ladder_up and portal.name != "Frog's Domain Ladder Exit"
|
||||
and not too_few_portals_for_direction_pairs(portal.direction, 1)):
|
||||
for test_portal in two_plus:
|
||||
if test_portal.name == "Frog's Domain Ladder Exit":
|
||||
should_continue = True
|
||||
if should_continue:
|
||||
continue
|
||||
|
||||
portal2 = portal
|
||||
connected_regions.add(get_portal_outlet_region(portal, world))
|
||||
two_plus2.remove(portal)
|
||||
break
|
||||
|
||||
if not portal2:
|
||||
if entrance_layout == EntranceLayout.option_direction_pairs or waterfall_plando:
|
||||
# portal1 doesn't have a valid direction pair yet, throw it back and start over
|
||||
two_plus.append(portal1)
|
||||
continue
|
||||
else:
|
||||
raise Exception(f"TUNIC: Failed to pair portals at second part of first phase for {world.player_name}.")
|
||||
|
||||
# once we have both portals, connect them and add the new region(s) to connected_regions
|
||||
if check_success == 2:
|
||||
if "Secret Gathering Place" in connected_regions:
|
||||
has_laurels = True
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
|
||||
portal_pairs[portal1] = portal2
|
||||
check_success = 0
|
||||
random_object.shuffle(two_plus)
|
||||
if not has_laurels and "Secret Gathering Place" in connected_regions:
|
||||
has_laurels = True
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
|
||||
|
||||
# for universal tracker, we want to skip shop gen
|
||||
if world.using_ut:
|
||||
shop_count = 0
|
||||
|
||||
for i in range(shop_count):
|
||||
portal1 = two_plus.pop()
|
||||
if portal1 is None:
|
||||
raise Exception("TUNIC: Too many shops in the pool, or something else went wrong.")
|
||||
portal2 = Portal(name=f"Shop Portal {world.shop_num}", region=f"Shop {world.shop_num}",
|
||||
destination="Previous Region", tag="_")
|
||||
create_shop_region(world, regions)
|
||||
|
||||
portal_pairs[portal1] = portal2
|
||||
two_plus_direction_tracker[portal1.direction] -= 1
|
||||
two_plus_direction_tracker[portal2.direction] -= 1
|
||||
portal1 = None
|
||||
portal2 = None
|
||||
random_object.shuffle(two_plus)
|
||||
if two_plus != two_plus2:
|
||||
random_object.shuffle(two_plus2)
|
||||
|
||||
# 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
|
||||
# there are no dead ends in decoupled
|
||||
while len(dead_ends) > 0:
|
||||
if world.using_ut:
|
||||
break
|
||||
portal1 = two_plus.pop()
|
||||
portal2 = dead_ends.pop()
|
||||
portal_pairs[portal1] = portal2
|
||||
portal2 = dead_ends[0]
|
||||
for portal in two_plus:
|
||||
if entrance_layout == EntranceLayout.option_direction_pairs and not verify_direction_pair(portal, portal2):
|
||||
continue
|
||||
if entrance_layout == EntranceLayout.option_fixed_shop and portal.region == "Zig Skip Exit":
|
||||
continue
|
||||
portal1 = portal
|
||||
portal_pairs[portal1] = portal2
|
||||
two_plus.remove(portal1)
|
||||
dead_ends.remove(portal2)
|
||||
break
|
||||
else:
|
||||
raise Exception(f"Failed to pair {portal2.name} with anything in two_plus for player {world.player_name}.")
|
||||
|
||||
# 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:
|
||||
final_pair_number = 0
|
||||
while len(two_plus) > 0:
|
||||
if world.using_ut:
|
||||
break
|
||||
portal1 = two_plus.pop()
|
||||
portal2 = two_plus.pop()
|
||||
final_pair_number += 1
|
||||
if final_pair_number > 10000:
|
||||
raise Exception(f"Failed to pair portals while pairing the final entrances off to each other. "
|
||||
f"Remaining portals in two_plus: {[portal.name for portal in two_plus]}. "
|
||||
f"Remaining portals in two_plus2: {[portal.name for portal in two_plus2]}.")
|
||||
portal1 = two_plus[0]
|
||||
two_plus.remove(portal1)
|
||||
portal2 = None
|
||||
if entrance_layout != EntranceLayout.option_direction_pairs:
|
||||
portal2 = two_plus2.pop()
|
||||
else:
|
||||
for portal in two_plus2:
|
||||
if verify_direction_pair(portal1, portal):
|
||||
portal2 = portal
|
||||
two_plus2.remove(portal2)
|
||||
break
|
||||
if portal2 is None:
|
||||
raise Exception("Something went wrong with the remaining two plus portals. Contact the TUNIC rando devs.")
|
||||
portal_pairs[portal1] = portal2
|
||||
|
||||
if len(two_plus) == 1:
|
||||
raise Exception("two plus had an odd number of portals, investigate this. last portal is " + two_plus[0].name)
|
||||
if len(two_plus2) > 0:
|
||||
raise Exception(f"TUNIC: Something went horribly wrong in ER for {world.player_name}. "
|
||||
f"Please contact the TUNIC rando devs.")
|
||||
|
||||
return portal_pairs
|
||||
|
||||
|
||||
# loop through our list of paired portals and make two-way connections
|
||||
def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None:
|
||||
def create_randomized_entrances(world: "TunicWorld", portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
region1 = regions[portal1.region]
|
||||
region2 = regions[portal2.region]
|
||||
region1.connect(connecting_region=region2, name=portal1.name)
|
||||
region2.connect(connecting_region=region1, name=portal2.name)
|
||||
# connect to the outlet region if there is one, if not connect to the actual region
|
||||
regions[portal1.region].connect(
|
||||
connecting_region=regions[get_portal_outlet_region(portal2, world)],
|
||||
name=portal1.name)
|
||||
if not world.options.decoupled or not world.options.entrance_rando:
|
||||
regions[portal2.region].connect(
|
||||
connecting_region=regions[get_portal_outlet_region(portal1, world)],
|
||||
name=portal2.name)
|
||||
|
||||
|
||||
def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]],
|
||||
@@ -541,22 +801,58 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s
|
||||
return connected_regions
|
||||
|
||||
|
||||
# which directions are opposites
|
||||
direction_pairs: Dict[int, int] = {
|
||||
Direction.north: Direction.south,
|
||||
Direction.south: Direction.north,
|
||||
Direction.east: Direction.west,
|
||||
Direction.west: Direction.east,
|
||||
Direction.ladder_up: Direction.ladder_down,
|
||||
Direction.ladder_down: Direction.ladder_up,
|
||||
Direction.floor: Direction.floor,
|
||||
}
|
||||
|
||||
|
||||
# verify that two portals are in compatible directions
|
||||
def verify_direction_pair(portal1: Portal, portal2: Portal) -> bool:
|
||||
return portal1.direction == direction_pairs[portal2.direction]
|
||||
|
||||
|
||||
# verify that two plando'd portals are in compatible directions
|
||||
def verify_plando_directions(connection: PlandoConnection) -> bool:
|
||||
entrance_portal = None
|
||||
exit_portal = None
|
||||
for portal in portal_mapping:
|
||||
if connection.entrance == portal.name:
|
||||
entrance_portal = portal
|
||||
if connection.exit == portal.name:
|
||||
exit_portal = portal
|
||||
if entrance_portal and exit_portal:
|
||||
break
|
||||
# neither of these are shops, so verify the pair
|
||||
if entrance_portal and exit_portal:
|
||||
return verify_direction_pair(entrance_portal, exit_portal)
|
||||
# this is two shop portals, they can never pair directions
|
||||
elif not entrance_portal and not exit_portal:
|
||||
return False
|
||||
# if one of them is none, it's a shop, which has two possible directions
|
||||
elif not entrance_portal:
|
||||
return exit_portal.direction in [Direction.north, Direction.east]
|
||||
elif not exit_portal:
|
||||
return entrance_portal.direction in [Direction.north, Direction.east]
|
||||
else:
|
||||
# shouldn't be reachable, more of a just in case
|
||||
raise Exception("Something went very wrong with verify_plando_directions")
|
||||
|
||||
|
||||
# sort the portal dict by the name of the first portal, referring to the portal order in the master portal list
|
||||
def sort_portals(portal_pairs: Dict[Portal, Portal]) -> Dict[str, str]:
|
||||
def sort_portals(portal_pairs: Dict[Portal, Portal], world: "TunicWorld") -> Dict[str, str]:
|
||||
sorted_pairs: Dict[str, str] = {}
|
||||
reference_list: List[str] = [portal.name for portal in portal_mapping]
|
||||
reference_list.append("Shop Portal")
|
||||
|
||||
# note: this is not necessary yet since the shop portals aren't numbered yet -- they will be when decoupled happens
|
||||
# due to plando, there can be a variable number of shops
|
||||
# I could either do it like this, or just go up to like 200, this seemed better
|
||||
# shop_count = 0
|
||||
# for portal1, portal2 in portal_pairs.items():
|
||||
# if portal1.name.startswith("Shop"):
|
||||
# shop_count += 1
|
||||
# if portal2.name.startswith("Shop"):
|
||||
# shop_count += 1
|
||||
# reference_list.extend([f"Shop Portal {i + 1}" for i in range(shop_count)])
|
||||
largest_shop_number = max(world.used_shop_numbers)
|
||||
reference_list.extend([f"Shop Portal {i + 1}" for i in range(largest_shop_number)])
|
||||
|
||||
for name in reference_list:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
|
||||
Reference in New Issue
Block a user