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 commit a6ee9a93da8f2fcc4413de6df6927b246017889d.

* Revert "# Conflicts:"

This reverts commit c74ccd74a45b6ad6b9abe6e339d115a0c98baf30.

* 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:
Scipio Wright
2025-05-06 12:33:21 -04:00
committed by GitHub
parent 7bbe62019a
commit a3aac3d737
9 changed files with 1150 additions and 516 deletions

View File

@@ -7,12 +7,12 @@ from .locations import location_table, location_name_groups, standard_location_n
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
from .er_rules import set_er_location_rules
from .regions import tunic_regions
from .er_scripts import create_er_regions
from .er_scripts import create_er_regions, verify_plando_directions
from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations
from .er_data import portal_mapping, RegionInfo, tunic_er_regions
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage, check_options,
get_hexagons_in_pool, HexagonQuestAbilityUnlockType)
get_hexagons_in_pool, HexagonQuestAbilityUnlockType, EntranceLayout)
from .breakables import breakable_location_name_to_id, breakable_location_groups, breakable_location_table
from .combat_logic import area_data, CombatState
from worlds.AutoWorld import WebWorld, World
@@ -61,8 +61,9 @@ class SeedGroup(TypedDict):
ice_grappling: int # ice_grappling value
ladder_storage: int # ls value
laurels_at_10_fairies: bool # laurels location value
fixed_shop: bool # fixed shop value
plando: TunicPlandoConnections # consolidated plando connections for the seed group
entrance_layout: int # entrance layout value
has_decoupled_enabled: bool # for checking that players don't have conflicting options
plando: List[PlandoConnection] # consolidated plando connections for the seed group
class TunicWorld(World):
@@ -95,7 +96,7 @@ class TunicWorld(World):
tunic_portal_pairs: Dict[str, str]
er_portal_hints: Dict[int, str]
seed_groups: Dict[str, SeedGroup] = {}
shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected
used_shop_numbers: Set[int]
er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work
# for the local_fill option
@@ -122,24 +123,35 @@ class TunicWorld(World):
check_options(self)
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
self.options.laurels_zips.value = LaurelsZips.option_true
self.options.ice_grappling.value = IceGrappling.option_medium
if self.options.logic_rules.value == LogicRules.option_unrestricted:
self.options.ladder_storage.value = LadderStorage.option_medium
self.er_regions = tunic_er_regions.copy()
if self.options.plando_connections and not self.options.entrance_rando:
self.options.plando_connections.value = ()
if self.options.plando_connections:
def replace_connection(old_cxn: PlandoConnection, new_cxn: PlandoConnection, index: int) -> None:
self.options.plando_connections.value.remove(old_cxn)
self.options.plando_connections.value.insert(index, new_cxn)
for index, cxn in enumerate(self.options.plando_connections):
# making shops second to simplify other things later
if cxn.entrance.startswith("Shop"):
replacement = PlandoConnection(cxn.exit, "Shop Portal", "both")
self.options.plando_connections.value.remove(cxn)
self.options.plando_connections.value.insert(index, replacement)
elif cxn.exit.startswith("Shop"):
replacement = PlandoConnection(cxn.entrance, "Shop Portal", "both")
self.options.plando_connections.value.remove(cxn)
self.options.plando_connections.value.insert(index, replacement)
replacement = None
if self.options.decoupled:
# flip any that are pointing to exit to point to entrance so that I don't have to deal with it
if cxn.direction == "exit":
replacement = PlandoConnection(cxn.exit, cxn.entrance, "entrance", cxn.percentage)
# if decoupled is on and you plando'd an entrance to itself but left the direction as both
if cxn.direction == "both" and cxn.entrance == cxn.exit:
replacement = PlandoConnection(cxn.entrance, cxn.exit, "entrance")
# if decoupled is off, just convert these to both
elif cxn.direction != "both":
replacement = PlandoConnection(cxn.entrance, cxn.exit, "both", cxn.percentage)
if replacement:
replace_connection(cxn, replacement, index)
if (self.options.entrance_layout == EntranceLayout.option_direction_pairs
and not verify_plando_directions(cxn)):
raise OptionError(f"TUNIC: Player {self.player_name} has invalid plando connections. "
f"They have Direction Pairs enabled and the connection "
f"{cxn.entrance} --> {cxn.exit} does not abide by this option.")
# Universal tracker stuff, shouldn't do anything in standard gen
if hasattr(self.multiworld, "re_gen_passthrough"):
@@ -160,16 +172,16 @@ class TunicWorld(World):
self.options.hexagon_quest_ability_type.value = self.passthrough.get("hexagon_quest_ability_type", 0)
self.options.entrance_rando.value = self.passthrough["entrance_rando"]
self.options.shuffle_ladders.value = self.passthrough["shuffle_ladders"]
self.options.entrance_layout.value = EntranceLayout.option_standard
if ("ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].keys()
or "ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].values()):
self.options.entrance_layout.value = EntranceLayout.option_fixed_shop
self.options.decoupled = self.passthrough.get("decoupled", 0)
self.options.laurels_location.value = LaurelsLocation.option_anywhere
self.options.grass_randomizer.value = self.passthrough.get("grass_randomizer", 0)
self.options.breakable_shuffle.value = self.passthrough.get("breakable_shuffle", 0)
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
self.options.combat_logic.value = self.passthrough["combat_logic"]
self.options.fixed_shop.value = self.options.fixed_shop.option_false
if ("ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].keys()
or "ziggurat2020_3, ziggurat2020_1_zig2_skip" in self.passthrough["Entrance Rando"].values()):
self.options.fixed_shop.value = self.options.fixed_shop.option_true
self.options.combat_logic.value = self.passthrough.get("combat_logic", 0)
else:
self.using_ut = False
else:
@@ -227,10 +239,14 @@ class TunicWorld(World):
ice_grappling=tunic.options.ice_grappling.value,
ladder_storage=tunic.options.ladder_storage.value,
laurels_at_10_fairies=tunic.options.laurels_location == LaurelsLocation.option_10_fairies,
fixed_shop=bool(tunic.options.fixed_shop),
plando=tunic.options.plando_connections)
entrance_layout=tunic.options.entrance_layout.value,
has_decoupled_enabled=bool(tunic.options.decoupled),
plando=tunic.options.plando_connections.value.copy())
continue
# I feel that syncing this one is worse than erroring out
if bool(tunic.options.decoupled) != cls.seed_groups[group]["has_decoupled_enabled"]:
raise OptionError(f"TUNIC: All players in the seed group {group} must "
f"have Decoupled either enabled or disabled.")
# off is more restrictive
if not tunic.options.laurels_zips:
cls.seed_groups[group]["laurels_zips"] = False
@@ -243,34 +259,52 @@ class TunicWorld(World):
# laurels at 10 fairies changes logic for secret gathering place placement
if tunic.options.laurels_location == 3:
cls.seed_groups[group]["laurels_at_10_fairies"] = True
# more restrictive, overrides the option for others in the same group, which is better than failing imo
if tunic.options.fixed_shop:
cls.seed_groups[group]["fixed_shop"] = True
# fixed shop and direction pairs override standard, but conflict with each other
if tunic.options.entrance_layout:
if cls.seed_groups[group]["entrance_layout"] == EntranceLayout.option_standard:
cls.seed_groups[group]["entrance_layout"] = tunic.options.entrance_layout.value
elif cls.seed_groups[group]["entrance_layout"] != tunic.options.entrance_layout.value:
raise OptionError(f"TUNIC: Conflict between seed group {group}'s Entrance Layout options. "
f"Seed group cannot have both Fixed Shop and Direction Pairs enabled.")
if tunic.options.plando_connections:
# loop through the connections in the player's yaml
for cxn in tunic.options.plando_connections:
for index, player_cxn in enumerate(tunic.options.plando_connections):
new_cxn = True
for group_cxn in cls.seed_groups[group]["plando"]:
# if neither entrance nor exit match anything in the group, add to group
if ((cxn.entrance == group_cxn.entrance and cxn.exit == group_cxn.exit)
or (cxn.exit == group_cxn.entrance and cxn.entrance == group_cxn.exit)):
new_cxn = False
break
# verify that it abides by direction pairs if enabled
if (cls.seed_groups[group]["entrance_layout"] == EntranceLayout.option_direction_pairs
and not verify_plando_directions(player_cxn)):
player_dir = "<->" if player_cxn.direction == "both" else "-->"
raise Exception(f"TUNIC: Conflict between Entrance Layout option and Plando Connection: "
f"{player_cxn.entrance} {player_dir} {player_cxn.exit}")
# check if this pair is the same as a pair in the group already
if ((player_cxn.entrance == group_cxn.entrance and player_cxn.exit == group_cxn.exit)
or (player_cxn.entrance == group_cxn.exit and player_cxn.exit == group_cxn.entrance
and "both" in [player_cxn.direction, group_cxn.direction])):
new_cxn = False
# if the group's was one-way and the player's was two-way, we replace the group's now
if player_cxn.direction == "both" and group_cxn.direction == "entrance":
cls.seed_groups[group]["plando"].remove(group_cxn)
cls.seed_groups[group]["plando"].insert(index, player_cxn)
break
is_mismatched = (
cxn.entrance == group_cxn.entrance and cxn.exit != group_cxn.exit
or cxn.entrance == group_cxn.exit and cxn.exit != group_cxn.entrance
or cxn.exit == group_cxn.entrance and cxn.entrance != group_cxn.exit
or cxn.exit == group_cxn.exit and cxn.entrance != group_cxn.entrance
player_cxn.entrance == group_cxn.entrance and player_cxn.exit != group_cxn.exit
or player_cxn.exit == group_cxn.exit and player_cxn.entrance != group_cxn.entrance
)
if not tunic.options.decoupled:
is_mismatched = is_mismatched or (
player_cxn.entrance == group_cxn.exit and player_cxn.exit != group_cxn.entrance
or player_cxn.exit == group_cxn.entrance and player_cxn.entrance != group_cxn.exit
)
if is_mismatched:
raise Exception(f"TUNIC: Conflict between seed group {group}'s plando "
f"connection {group_cxn.entrance} <-> {group_cxn.exit} and "
f"{tunic.player_name}'s plando connection {cxn.entrance} <-> {cxn.exit}")
group_dir = "<->" if group_cxn.direction == "both" else "-->"
player_dir = "<->" if player_cxn.direction == "both" else "-->"
raise OptionError(f"TUNIC: Conflict between seed group {group}'s plando "
f"connection {group_cxn.entrance} {group_dir} {group_cxn.exit} and "
f"{tunic.player_name}'s plando connection "
f"{player_cxn.entrance} {player_dir} {player_cxn.exit}")
if new_cxn:
cls.seed_groups[group]["plando"].value.append(cxn)
cls.seed_groups[group]["plando"].append(player_cxn)
def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
item_data = item_table[name]
@@ -571,7 +605,7 @@ class TunicWorld(World):
all_state = self.multiworld.get_all_state(True)
all_state.update_reachable_regions(self.player)
paths = all_state.path
portal_names = [portal.name for portal in portal_mapping]
portal_names = {portal.name for portal in portal_mapping}.union({f"Shop Portal {i + 1}" for i in range(500)})
for location in self.multiworld.get_locations(self.player):
# skipping event locations
if not location.address:
@@ -630,6 +664,7 @@ class TunicWorld(World):
"lanternless": self.options.lanternless.value,
"maskless": self.options.maskless.value,
"entrance_rando": int(bool(self.options.entrance_rando.value)),
"decoupled": self.options.decoupled.value if self.options.entrance_rando else 0,
"shuffle_ladders": self.options.shuffle_ladders.value,
"grass_randomizer": self.options.grass_randomizer.value,
"combat_logic": self.options.combat_logic.value,

View File

@@ -22,6 +22,7 @@ class AreaStats(NamedTuple):
# the vanilla upgrades/equipment you would have
area_data: Dict[str, AreaStats] = {
# The upgrade page is right by the Well entrance. Upper Overworld by the chest in the top right might need something
"Overworld": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Stick"]),
"East Forest": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Sword"]),
"Before Well": AreaStats(1, 1, 1, 1, 1, 1, 3, ["Sword", "Shield"]),

View File

@@ -83,7 +83,7 @@ Notes:
- The Entrance Randomizer option must be enabled for it to work.
- The `direction` field is not supported. Connections are always coupled.
- For a list of entrance names, check `er_data.py` in the TUNIC world folder or generate a game with the Entrance Randomizer option enabled and check the spoiler log.
- There is no limit to the number of Shops you can plando.
- You can plando up to 500 additional shops in Decoupled. You should not do this.
See the [Archipelago Plando Guide](../../../tutorial/Archipelago/plando/en) for more information on Plando and Connection Plando.

View File

@@ -1,15 +1,28 @@
from typing import Dict, NamedTuple, List, TYPE_CHECKING, Optional
from typing import Dict, NamedTuple, List, Optional, TYPE_CHECKING
from enum import IntEnum
if TYPE_CHECKING:
from . import TunicWorld
# the direction you go to enter a portal
class Direction(IntEnum):
none = 0 # for when the direction isn't relevant
north = 1
south = 2
east = 3
west = 4
floor = 5
ladder_up = 6
ladder_down = 7
class Portal(NamedTuple):
name: str # human-readable name
region: str # AP region
destination: str # vanilla destination scene
tag: str # vanilla tag
direction: int # the direction you go to enter a portal
def scene(self) -> str: # the actual scene name in Tunic
if self.region.startswith("Shop"):
@@ -25,497 +38,497 @@ class Portal(NamedTuple):
portal_mapping: List[Portal] = [
Portal(name="Stick House Entrance", region="Overworld",
destination="Sword Cave", tag="_"),
destination="Sword Cave", tag="_", direction=Direction.north),
Portal(name="Windmill Entrance", region="Overworld",
destination="Windmill", tag="_"),
destination="Windmill", tag="_", direction=Direction.north),
Portal(name="Well Ladder Entrance", region="Overworld Well Ladder",
destination="Sewer", tag="_entrance"),
destination="Sewer", tag="_entrance", direction=Direction.ladder_down),
Portal(name="Entrance to Well from Well Rail", region="Overworld Well to Furnace Rail",
destination="Sewer", tag="_west_aqueduct"),
destination="Sewer", tag="_west_aqueduct", direction=Direction.north),
Portal(name="Old House Door Entrance", region="Overworld Old House Door",
destination="Overworld Interiors", tag="_house"),
destination="Overworld Interiors", tag="_house", direction=Direction.east),
Portal(name="Old House Waterfall Entrance", region="Overworld",
destination="Overworld Interiors", tag="_under_checkpoint"),
destination="Overworld Interiors", tag="_under_checkpoint", direction=Direction.east),
Portal(name="Entrance to Furnace from Well Rail", region="Overworld Well to Furnace Rail",
destination="Furnace", tag="_gyro_upper_north"),
destination="Furnace", tag="_gyro_upper_north", direction=Direction.south),
Portal(name="Entrance to Furnace under Windmill", region="Overworld",
destination="Furnace", tag="_gyro_upper_east"),
destination="Furnace", tag="_gyro_upper_east", direction=Direction.west),
Portal(name="Entrance to Furnace near West Garden", region="Overworld to West Garden from Furnace",
destination="Furnace", tag="_gyro_west"),
destination="Furnace", tag="_gyro_west", direction=Direction.east),
Portal(name="Entrance to Furnace from Beach", region="Overworld Tunnel Turret",
destination="Furnace", tag="_gyro_lower"),
destination="Furnace", tag="_gyro_lower", direction=Direction.north),
Portal(name="Caustic Light Cave Entrance", region="Overworld Swamp Lower Entry",
destination="Overworld Cave", tag="_"),
destination="Overworld Cave", tag="_", direction=Direction.north),
Portal(name="Swamp Upper Entrance", region="Overworld Swamp Upper Entry",
destination="Swamp Redux 2", tag="_wall"),
destination="Swamp Redux 2", tag="_wall", direction=Direction.south),
Portal(name="Swamp Lower Entrance", region="Overworld Swamp Lower Entry",
destination="Swamp Redux 2", tag="_conduit"),
destination="Swamp Redux 2", tag="_conduit", direction=Direction.south),
Portal(name="Ruined Passage Not-Door Entrance", region="After Ruined Passage",
destination="Ruins Passage", tag="_east"),
destination="Ruins Passage", tag="_east", direction=Direction.north),
Portal(name="Ruined Passage Door Entrance", region="Overworld Ruined Passage Door",
destination="Ruins Passage", tag="_west"),
destination="Ruins Passage", tag="_west", direction=Direction.east),
Portal(name="Atoll Upper Entrance", region="Overworld to Atoll Upper",
destination="Atoll Redux", tag="_upper"),
destination="Atoll Redux", tag="_upper", direction=Direction.south),
Portal(name="Atoll Lower Entrance", region="Overworld Beach",
destination="Atoll Redux", tag="_lower"),
destination="Atoll Redux", tag="_lower", direction=Direction.south),
Portal(name="Special Shop Entrance", region="Overworld Special Shop Entry",
destination="ShopSpecial", tag="_"),
destination="ShopSpecial", tag="_", direction=Direction.east),
Portal(name="Maze Cave Entrance", region="Overworld Beach",
destination="Maze Room", tag="_"),
destination="Maze Room", tag="_", direction=Direction.north),
Portal(name="West Garden Entrance near Belltower", region="Overworld to West Garden Upper",
destination="Archipelagos Redux", tag="_upper"),
destination="Archipelagos Redux", tag="_upper", direction=Direction.west),
Portal(name="West Garden Entrance from Furnace", region="Overworld to West Garden from Furnace",
destination="Archipelagos Redux", tag="_lower"),
destination="Archipelagos Redux", tag="_lower", direction=Direction.west),
Portal(name="West Garden Laurels Entrance", region="Overworld West Garden Laurels Entry",
destination="Archipelagos Redux", tag="_lowest"),
destination="Archipelagos Redux", tag="_lowest", direction=Direction.west),
Portal(name="Temple Door Entrance", region="Overworld Temple Door",
destination="Temple", tag="_main"),
destination="Temple", tag="_main", direction=Direction.north),
Portal(name="Temple Rafters Entrance", region="Overworld after Temple Rafters",
destination="Temple", tag="_rafters"),
destination="Temple", tag="_rafters", direction=Direction.east),
Portal(name="Ruined Shop Entrance", region="Overworld",
destination="Ruined Shop", tag="_"),
destination="Ruined Shop", tag="_", direction=Direction.east),
Portal(name="Patrol Cave Entrance", region="Overworld at Patrol Cave",
destination="PatrolCave", tag="_"),
destination="PatrolCave", tag="_", direction=Direction.north),
Portal(name="Hourglass Cave Entrance", region="Overworld Beach",
destination="Town Basement", tag="_beach"),
destination="Town Basement", tag="_beach", direction=Direction.north),
Portal(name="Changing Room Entrance", region="Overworld",
destination="Changing Room", tag="_"),
destination="Changing Room", tag="_", direction=Direction.south),
Portal(name="Cube Cave Entrance", region="Cube Cave Entrance Region",
destination="CubeRoom", tag="_"),
destination="CubeRoom", tag="_", direction=Direction.north),
Portal(name="Stairs from Overworld to Mountain", region="Upper Overworld",
destination="Mountain", tag="_"),
destination="Mountain", tag="_", direction=Direction.north),
Portal(name="Overworld to Fortress", region="East Overworld",
destination="Fortress Courtyard", tag="_"),
destination="Fortress Courtyard", tag="_", direction=Direction.east),
Portal(name="Fountain HC Door Entrance", region="Overworld Fountain Cross Door",
destination="Town_FiligreeRoom", tag="_"),
destination="Town_FiligreeRoom", tag="_", direction=Direction.north),
Portal(name="Southeast HC Door Entrance", region="Overworld Southeast Cross Door",
destination="EastFiligreeCache", tag="_"),
destination="EastFiligreeCache", tag="_", direction=Direction.north),
Portal(name="Overworld to Quarry Connector", region="Overworld Quarry Entry",
destination="Darkwoods Tunnel", tag="_"),
destination="Darkwoods Tunnel", tag="_", direction=Direction.north),
Portal(name="Dark Tomb Main Entrance", region="Overworld",
destination="Crypt Redux", tag="_"),
destination="Crypt Redux", tag="_", direction=Direction.north),
Portal(name="Overworld to Forest Belltower", region="East Overworld",
destination="Forest Belltower", tag="_"),
destination="Forest Belltower", tag="_", direction=Direction.east),
Portal(name="Town to Far Shore", region="Overworld Town Portal",
destination="Transit", tag="_teleporter_town"),
destination="Transit", tag="_teleporter_town", direction=Direction.floor),
Portal(name="Spawn to Far Shore", region="Overworld Spawn Portal",
destination="Transit", tag="_teleporter_starting island"),
destination="Transit", tag="_teleporter_starting island", direction=Direction.floor),
Portal(name="Secret Gathering Place Entrance", region="Overworld",
destination="Waterfall", tag="_"),
destination="Waterfall", tag="_", direction=Direction.north),
Portal(name="Secret Gathering Place Exit", region="Secret Gathering Place",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Windmill Exit", region="Windmill",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Windmill Shop", region="Windmill",
destination="Shop", tag="_"),
destination="Shop", tag="_", direction=Direction.north),
Portal(name="Old House Door Exit", region="Old House Front",
destination="Overworld Redux", tag="_house"),
destination="Overworld Redux", tag="_house", direction=Direction.west),
Portal(name="Old House to Glyph Tower", region="Old House Front",
destination="g_elements", tag="_"),
destination="g_elements", tag="_", direction=Direction.south), # portal drops you on north side
Portal(name="Old House Waterfall Exit", region="Old House Back",
destination="Overworld Redux", tag="_under_checkpoint"),
destination="Overworld Redux", tag="_under_checkpoint", direction=Direction.west),
Portal(name="Glyph Tower Exit", region="Relic Tower",
destination="Overworld Interiors", tag="_"),
destination="Overworld Interiors", tag="_", direction=Direction.north),
Portal(name="Changing Room Exit", region="Changing Room",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.north),
Portal(name="Fountain HC Room Exit", region="Fountain Cross Room",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Cube Cave Exit", region="Cube Cave",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Guard Patrol Cave Exit", region="Patrol Cave",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Ruined Shop Exit", region="Ruined Shop",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.west),
Portal(name="Furnace Exit towards Well", region="Furnace Fuse",
destination="Overworld Redux", tag="_gyro_upper_north"),
destination="Overworld Redux", tag="_gyro_upper_north", direction=Direction.north),
Portal(name="Furnace Exit to Dark Tomb", region="Furnace Walking Path",
destination="Crypt Redux", tag="_"),
destination="Crypt Redux", tag="_", direction=Direction.east),
Portal(name="Furnace Exit towards West Garden", region="Furnace Walking Path",
destination="Overworld Redux", tag="_gyro_west"),
destination="Overworld Redux", tag="_gyro_west", direction=Direction.west),
Portal(name="Furnace Exit to Beach", region="Furnace Ladder Area",
destination="Overworld Redux", tag="_gyro_lower"),
destination="Overworld Redux", tag="_gyro_lower", direction=Direction.south),
Portal(name="Furnace Exit under Windmill", region="Furnace Ladder Area",
destination="Overworld Redux", tag="_gyro_upper_east"),
destination="Overworld Redux", tag="_gyro_upper_east", direction=Direction.east),
Portal(name="Stick House Exit", region="Stick House",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Ruined Passage Not-Door Exit", region="Ruined Passage",
destination="Overworld Redux", tag="_east"),
destination="Overworld Redux", tag="_east", direction=Direction.south),
Portal(name="Ruined Passage Door Exit", region="Ruined Passage",
destination="Overworld Redux", tag="_west"),
destination="Overworld Redux", tag="_west", direction=Direction.west),
Portal(name="Southeast HC Room Exit", region="Southeast Cross Room",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Caustic Light Cave Exit", region="Caustic Light Cave",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Maze Cave Exit", region="Maze Cave",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Hourglass Cave Exit", region="Hourglass Cave",
destination="Overworld Redux", tag="_beach"),
destination="Overworld Redux", tag="_beach", direction=Direction.south),
Portal(name="Special Shop Exit", region="Special Shop",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.west),
Portal(name="Temple Rafters Exit", region="Sealed Temple Rafters",
destination="Overworld Redux", tag="_rafters"),
destination="Overworld Redux", tag="_rafters", direction=Direction.west),
Portal(name="Temple Door Exit", region="Sealed Temple",
destination="Overworld Redux", tag="_main"),
destination="Overworld Redux", tag="_main", direction=Direction.south),
Portal(name="Forest Belltower to Fortress", region="Forest Belltower Main behind bushes",
destination="Fortress Courtyard", tag="_"),
destination="Fortress Courtyard", tag="_", direction=Direction.north),
Portal(name="Forest Belltower to Forest", region="Forest Belltower Lower",
destination="East Forest Redux", tag="_"),
destination="East Forest Redux", tag="_", direction=Direction.south),
Portal(name="Forest Belltower to Overworld", region="Forest Belltower Main",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.west),
Portal(name="Forest Belltower to Guard Captain Room", region="Forest Belltower Upper",
destination="Forest Boss Room", tag="_"),
destination="Forest Boss Room", tag="_", direction=Direction.south),
Portal(name="Forest to Belltower", region="East Forest",
destination="Forest Belltower", tag="_"),
destination="Forest Belltower", tag="_", direction=Direction.north),
Portal(name="Forest Guard House 1 Lower Entrance", region="East Forest",
destination="East Forest Redux Laddercave", tag="_lower"),
destination="East Forest Redux Laddercave", tag="_lower", direction=Direction.north),
Portal(name="Forest Guard House 1 Gate Entrance", region="East Forest",
destination="East Forest Redux Laddercave", tag="_gate"),
destination="East Forest Redux Laddercave", tag="_gate", direction=Direction.north),
Portal(name="Forest Dance Fox Outside Doorway", region="East Forest Dance Fox Spot",
destination="East Forest Redux Laddercave", tag="_upper"),
destination="East Forest Redux Laddercave", tag="_upper", direction=Direction.east),
Portal(name="Forest to Far Shore", region="East Forest Portal",
destination="Transit", tag="_teleporter_forest teleporter"),
destination="Transit", tag="_teleporter_forest teleporter", direction=Direction.floor),
Portal(name="Forest Guard House 2 Lower Entrance", region="Lower Forest",
destination="East Forest Redux Interior", tag="_lower"),
destination="East Forest Redux Interior", tag="_lower", direction=Direction.north),
Portal(name="Forest Guard House 2 Upper Entrance", region="East Forest",
destination="East Forest Redux Interior", tag="_upper"),
destination="East Forest Redux Interior", tag="_upper", direction=Direction.east),
Portal(name="Forest Grave Path Lower Entrance", region="East Forest",
destination="Sword Access", tag="_lower"),
destination="Sword Access", tag="_lower", direction=Direction.east),
Portal(name="Forest Grave Path Upper Entrance", region="East Forest",
destination="Sword Access", tag="_upper"),
destination="Sword Access", tag="_upper", direction=Direction.east),
Portal(name="Forest Grave Path Upper Exit", region="Forest Grave Path Upper",
destination="East Forest Redux", tag="_upper"),
destination="East Forest Redux", tag="_upper", direction=Direction.west),
Portal(name="Forest Grave Path Lower Exit", region="Forest Grave Path Main",
destination="East Forest Redux", tag="_lower"),
destination="East Forest Redux", tag="_lower", direction=Direction.west),
Portal(name="East Forest Hero's Grave", region="Forest Hero's Grave",
destination="RelicVoid", tag="_teleporter_relic plinth"),
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Guard House 1 Dance Fox Exit", region="Guard House 1 West",
destination="East Forest Redux", tag="_upper"),
destination="East Forest Redux", tag="_upper", direction=Direction.west),
Portal(name="Guard House 1 Lower Exit", region="Guard House 1 West",
destination="East Forest Redux", tag="_lower"),
destination="East Forest Redux", tag="_lower", direction=Direction.south),
Portal(name="Guard House 1 Upper Forest Exit", region="Guard House 1 East",
destination="East Forest Redux", tag="_gate"),
destination="East Forest Redux", tag="_gate", direction=Direction.south),
Portal(name="Guard House 1 to Guard Captain Room", region="Guard House 1 East",
destination="Forest Boss Room", tag="_"),
destination="Forest Boss Room", tag="_", direction=Direction.north),
Portal(name="Guard House 2 Lower Exit", region="Guard House 2 Lower",
destination="East Forest Redux", tag="_lower"),
destination="East Forest Redux", tag="_lower", direction=Direction.south),
Portal(name="Guard House 2 Upper Exit", region="Guard House 2 Upper before bushes",
destination="East Forest Redux", tag="_upper"),
destination="East Forest Redux", tag="_upper", direction=Direction.west),
Portal(name="Guard Captain Room Non-Gate Exit", region="Forest Boss Room",
destination="East Forest Redux Laddercave", tag="_"),
destination="East Forest Redux Laddercave", tag="_", direction=Direction.south),
Portal(name="Guard Captain Room Gate Exit", region="Forest Boss Room",
destination="Forest Belltower", tag="_"),
destination="Forest Belltower", tag="_", direction=Direction.north),
Portal(name="Well Ladder Exit", region="Beneath the Well Ladder Exit",
destination="Overworld Redux", tag="_entrance"),
destination="Overworld Redux", tag="_entrance", direction=Direction.ladder_up),
Portal(name="Well to Well Boss", region="Beneath the Well Back",
destination="Sewer_Boss", tag="_"),
destination="Sewer_Boss", tag="_", direction=Direction.east),
Portal(name="Well Exit towards Furnace", region="Beneath the Well Back",
destination="Overworld Redux", tag="_west_aqueduct"),
destination="Overworld Redux", tag="_west_aqueduct", direction=Direction.south),
Portal(name="Well Boss to Well", region="Well Boss",
destination="Sewer", tag="_"),
destination="Sewer", tag="_", direction=Direction.west),
Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint",
destination="Crypt Redux", tag="_"),
destination="Crypt Redux", tag="_", direction=Direction.ladder_up),
Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit",
destination="Furnace", tag="_"),
destination="Furnace", tag="_", direction=Direction.west),
Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point",
destination="Sewer_Boss", tag="_"),
destination="Sewer_Boss", tag="_", direction=Direction.ladder_down),
Portal(name="West Garden Exit near Hero's Grave", region="West Garden before Terry",
destination="Overworld Redux", tag="_lower"),
destination="Overworld Redux", tag="_lower", direction=Direction.east),
Portal(name="West Garden to Magic Dagger House", region="West Garden at Dagger House",
destination="archipelagos_house", tag="_"),
destination="archipelagos_house", tag="_", direction=Direction.east),
Portal(name="West Garden Exit after Boss", region="West Garden after Boss",
destination="Overworld Redux", tag="_upper"),
destination="Overworld Redux", tag="_upper", direction=Direction.east),
Portal(name="West Garden Shop", region="West Garden before Terry",
destination="Shop", tag="_"),
destination="Shop", tag="_", direction=Direction.east),
Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region",
destination="Overworld Redux", tag="_lowest"),
destination="Overworld Redux", tag="_lowest", direction=Direction.east),
Portal(name="West Garden Hero's Grave", region="West Garden Hero's Grave Region",
destination="RelicVoid", tag="_teleporter_relic plinth"),
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="West Garden to Far Shore", region="West Garden Portal",
destination="Transit", tag="_teleporter_archipelagos_teleporter"),
destination="Transit", tag="_teleporter_archipelagos_teleporter", direction=Direction.floor),
Portal(name="Magic Dagger House Exit", region="Magic Dagger House",
destination="Archipelagos Redux", tag="_"),
destination="Archipelagos Redux", tag="_", direction=Direction.west),
Portal(name="Fortress Courtyard to Fortress Grave Path Lower", region="Fortress Courtyard",
destination="Fortress Reliquary", tag="_Lower"),
destination="Fortress Reliquary", tag="_Lower", direction=Direction.east),
Portal(name="Fortress Courtyard to Fortress Grave Path Upper", region="Fortress Courtyard Upper",
destination="Fortress Reliquary", tag="_Upper"),
destination="Fortress Reliquary", tag="_Upper", direction=Direction.east),
Portal(name="Fortress Courtyard to Fortress Interior", region="Fortress Courtyard",
destination="Fortress Main", tag="_Big Door"),
destination="Fortress Main", tag="_Big Door", direction=Direction.north),
Portal(name="Fortress Courtyard to East Fortress", region="Fortress Courtyard Upper",
destination="Fortress East", tag="_"),
destination="Fortress East", tag="_", direction=Direction.north),
Portal(name="Fortress Courtyard to Beneath the Vault", region="Beneath the Vault Entry",
destination="Fortress Basement", tag="_"),
destination="Fortress Basement", tag="_", direction=Direction.ladder_down),
Portal(name="Fortress Courtyard to Forest Belltower", region="Fortress Exterior from East Forest",
destination="Forest Belltower", tag="_"),
destination="Forest Belltower", tag="_", direction=Direction.south),
Portal(name="Fortress Courtyard to Overworld", region="Fortress Exterior from Overworld",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.west),
Portal(name="Fortress Courtyard Shop", region="Fortress Exterior near cave",
destination="Shop", tag="_"),
destination="Shop", tag="_", direction=Direction.north),
Portal(name="Beneath the Vault to Fortress Interior", region="Beneath the Vault Back",
destination="Fortress Main", tag="_"),
destination="Fortress Main", tag="_", direction=Direction.east),
Portal(name="Beneath the Vault to Fortress Courtyard", region="Beneath the Vault Ladder Exit",
destination="Fortress Courtyard", tag="_"),
destination="Fortress Courtyard", tag="_", direction=Direction.ladder_up),
Portal(name="Fortress Interior Main Exit", region="Eastern Vault Fortress",
destination="Fortress Courtyard", tag="_Big Door"),
destination="Fortress Courtyard", tag="_Big Door", direction=Direction.south),
Portal(name="Fortress Interior to Beneath the Earth", region="Eastern Vault Fortress",
destination="Fortress Basement", tag="_"),
destination="Fortress Basement", tag="_", direction=Direction.west),
Portal(name="Fortress Interior to Siege Engine Arena", region="Eastern Vault Fortress Gold Door",
destination="Fortress Arena", tag="_"),
destination="Fortress Arena", tag="_", direction=Direction.north),
Portal(name="Fortress Interior Shop", region="Eastern Vault Fortress",
destination="Shop", tag="_"),
destination="Shop", tag="_", direction=Direction.north),
Portal(name="Fortress Interior to East Fortress Upper", region="Eastern Vault Fortress",
destination="Fortress East", tag="_upper"),
destination="Fortress East", tag="_upper", direction=Direction.east),
Portal(name="Fortress Interior to East Fortress Lower", region="Eastern Vault Fortress",
destination="Fortress East", tag="_lower"),
destination="Fortress East", tag="_lower", direction=Direction.east),
Portal(name="East Fortress to Interior Lower", region="Fortress East Shortcut Lower",
destination="Fortress Main", tag="_lower"),
destination="Fortress Main", tag="_lower", direction=Direction.west),
Portal(name="East Fortress to Courtyard", region="Fortress East Shortcut Upper",
destination="Fortress Courtyard", tag="_"),
destination="Fortress Courtyard", tag="_", direction=Direction.south),
Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper",
destination="Fortress Main", tag="_upper"),
destination="Fortress Main", tag="_upper", direction=Direction.west),
Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path Entry",
destination="Fortress Courtyard", tag="_Lower"),
destination="Fortress Courtyard", tag="_Lower", direction=Direction.west),
Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region",
destination="RelicVoid", tag="_teleporter_relic plinth"),
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Fortress Grave Path Upper Exit", region="Fortress Grave Path Upper",
destination="Fortress Courtyard", tag="_Upper"),
destination="Fortress Courtyard", tag="_Upper", direction=Direction.west),
Portal(name="Fortress Grave Path Dusty Entrance", region="Fortress Grave Path Dusty Entrance Region",
destination="Dusty", tag="_"),
destination="Dusty", tag="_", direction=Direction.north),
Portal(name="Dusty Exit", region="Fortress Leaf Piles",
destination="Fortress Reliquary", tag="_"),
destination="Fortress Reliquary", tag="_", direction=Direction.south),
Portal(name="Siege Engine Arena to Fortress", region="Fortress Arena",
destination="Fortress Main", tag="_"),
destination="Fortress Main", tag="_", direction=Direction.south),
Portal(name="Fortress to Far Shore", region="Fortress Arena Portal",
destination="Transit", tag="_teleporter_spidertank"),
destination="Transit", tag="_teleporter_spidertank", direction=Direction.floor),
Portal(name="Atoll Upper Exit", region="Ruined Atoll",
destination="Overworld Redux", tag="_upper"),
destination="Overworld Redux", tag="_upper", direction=Direction.north),
Portal(name="Atoll Lower Exit", region="Ruined Atoll Lower Entry Area",
destination="Overworld Redux", tag="_lower"),
destination="Overworld Redux", tag="_lower", direction=Direction.north),
Portal(name="Atoll Shop", region="Ruined Atoll",
destination="Shop", tag="_"),
destination="Shop", tag="_", direction=Direction.north),
Portal(name="Atoll to Far Shore", region="Ruined Atoll Portal",
destination="Transit", tag="_teleporter_atoll"),
destination="Transit", tag="_teleporter_atoll", direction=Direction.floor),
Portal(name="Atoll Statue Teleporter", region="Ruined Atoll Statue",
destination="Library Exterior", tag="_"),
destination="Library Exterior", tag="_", direction=Direction.floor),
Portal(name="Frog Stairs Eye Entrance", region="Ruined Atoll Frog Eye",
destination="Frog Stairs", tag="_eye"),
destination="Frog Stairs", tag="_eye", direction=Direction.south), # camera rotates, it's fine
Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth",
destination="Frog Stairs", tag="_mouth"),
destination="Frog Stairs", tag="_mouth", direction=Direction.east),
Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit",
destination="Atoll Redux", tag="_eye"),
destination="Atoll Redux", tag="_eye", direction=Direction.north),
Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper",
destination="Atoll Redux", tag="_mouth"),
destination="Atoll Redux", tag="_mouth", direction=Direction.west),
Portal(name="Frog Stairs to Frog's Domain's Entrance", region="Frog Stairs to Frog's Domain",
destination="frog cave main", tag="_Entrance"),
destination="frog cave main", tag="_Entrance", direction=Direction.ladder_down),
Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower",
destination="frog cave main", tag="_Exit"),
destination="frog cave main", tag="_Exit", direction=Direction.east),
Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry",
destination="Frog Stairs", tag="_Entrance"),
destination="Frog Stairs", tag="_Entrance", direction=Direction.ladder_up),
Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back",
destination="Frog Stairs", tag="_Exit"),
destination="Frog Stairs", tag="_Exit", direction=Direction.west),
Portal(name="Library Exterior Tree", region="Library Exterior Tree Region",
destination="Atoll Redux", tag="_"),
destination="Atoll Redux", tag="_", direction=Direction.floor),
Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region",
destination="Library Hall", tag="_"),
destination="Library Hall", tag="_", direction=Direction.west), # camera rotates
Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf",
destination="Library Exterior", tag="_"),
destination="Library Exterior", tag="_", direction=Direction.east),
Portal(name="Library Hero's Grave", region="Library Hero's Grave Region",
destination="RelicVoid", tag="_teleporter_relic plinth"),
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda",
destination="Library Rotunda", tag="_"),
destination="Library Rotunda", tag="_", direction=Direction.ladder_up),
Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall",
destination="Library Hall", tag="_"),
destination="Library Hall", tag="_", direction=Direction.ladder_down),
Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab",
destination="Library Lab", tag="_"),
destination="Library Lab", tag="_", direction=Direction.ladder_up),
Portal(name="Library Lab to Rotunda", region="Library Lab Lower",
destination="Library Rotunda", tag="_"),
destination="Library Rotunda", tag="_", direction=Direction.ladder_down),
Portal(name="Library to Far Shore", region="Library Portal",
destination="Transit", tag="_teleporter_library teleporter"),
destination="Transit", tag="_teleporter_library teleporter", direction=Direction.floor),
Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian",
destination="Library Arena", tag="_"),
destination="Library Arena", tag="_", direction=Direction.ladder_up),
Portal(name="Librarian Arena Exit", region="Library Arena",
destination="Library Lab", tag="_"),
destination="Library Lab", tag="_", direction=Direction.ladder_down),
Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs",
destination="Mountaintop", tag="_"),
destination="Mountaintop", tag="_", direction=Direction.north),
Portal(name="Mountain to Quarry", region="Lower Mountain",
destination="Quarry Redux", tag="_"),
destination="Quarry Redux", tag="_", direction=Direction.south), # connecting is north
Portal(name="Mountain to Overworld", region="Lower Mountain",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Top of the Mountain Exit", region="Top of the Mountain",
destination="Mountain", tag="_"),
destination="Mountain", tag="_", direction=Direction.south),
Portal(name="Quarry Connector to Overworld", region="Quarry Connector",
destination="Overworld Redux", tag="_"),
destination="Overworld Redux", tag="_", direction=Direction.south),
Portal(name="Quarry Connector to Quarry", region="Quarry Connector",
destination="Quarry Redux", tag="_"),
destination="Quarry Redux", tag="_", direction=Direction.north), # rotates, it's fine
Portal(name="Quarry to Overworld Exit", region="Quarry Entry",
destination="Darkwoods Tunnel", tag="_"),
destination="Darkwoods Tunnel", tag="_", direction=Direction.south), # rotates, it's fine
Portal(name="Quarry Shop", region="Quarry Entry",
destination="Shop", tag="_"),
destination="Shop", tag="_", direction=Direction.north),
Portal(name="Quarry to Monastery Front", region="Quarry Monastery Entry",
destination="Monastery", tag="_front"),
destination="Monastery", tag="_front", direction=Direction.north),
Portal(name="Quarry to Monastery Back", region="Monastery Rope",
destination="Monastery", tag="_back"),
destination="Monastery", tag="_back", direction=Direction.east),
Portal(name="Quarry to Mountain", region="Quarry Back",
destination="Mountain", tag="_"),
destination="Mountain", tag="_", direction=Direction.north),
Portal(name="Quarry to Ziggurat", region="Lower Quarry Zig Door",
destination="ziggurat2020_0", tag="_"),
destination="ziggurat2020_0", tag="_", direction=Direction.north),
Portal(name="Quarry to Far Shore", region="Quarry Portal",
destination="Transit", tag="_teleporter_quarry teleporter"),
destination="Transit", tag="_teleporter_quarry teleporter", direction=Direction.floor),
Portal(name="Monastery Rear Exit", region="Monastery Back",
destination="Quarry Redux", tag="_back"),
destination="Quarry Redux", tag="_back", direction=Direction.west),
Portal(name="Monastery Front Exit", region="Monastery Front",
destination="Quarry Redux", tag="_front"),
destination="Quarry Redux", tag="_front", direction=Direction.south),
Portal(name="Monastery Hero's Grave", region="Monastery Hero's Grave Region",
destination="RelicVoid", tag="_teleporter_relic plinth"),
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Ziggurat Entry Hallway to Ziggurat Upper", region="Rooted Ziggurat Entry",
destination="ziggurat2020_1", tag="_"),
destination="ziggurat2020_1", tag="_", direction=Direction.north),
Portal(name="Ziggurat Entry Hallway to Quarry", region="Rooted Ziggurat Entry",
destination="Quarry Redux", tag="_"),
destination="Quarry Redux", tag="_", direction=Direction.south),
Portal(name="Ziggurat Upper to Ziggurat Entry Hallway", region="Rooted Ziggurat Upper Entry",
destination="ziggurat2020_0", tag="_"),
destination="ziggurat2020_0", tag="_", direction=Direction.south),
Portal(name="Ziggurat Upper to Ziggurat Tower", region="Rooted Ziggurat Upper Back",
destination="ziggurat2020_2", tag="_"),
destination="ziggurat2020_2", tag="_", direction=Direction.north), # connecting is south
Portal(name="Ziggurat Tower to Ziggurat Upper", region="Rooted Ziggurat Middle Top",
destination="ziggurat2020_1", tag="_"),
destination="ziggurat2020_1", tag="_", direction=Direction.south),
Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom",
destination="ziggurat2020_3", tag="_"),
destination="ziggurat2020_3", tag="_", direction=Direction.south),
Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Entry",
destination="ziggurat2020_2", tag="_"),
destination="ziggurat2020_2", tag="_", direction=Direction.north),
Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance",
destination="ziggurat2020_FTRoom", tag="_"),
destination="ziggurat2020_FTRoom", tag="_", direction=Direction.north),
# only if fixed shop is on, removed otherwise
Portal(name="Ziggurat Lower Falling Entrance", region="Zig Skip Exit",
destination="ziggurat2020_1", tag="_zig2_skip"),
Portal(name="Ziggurat Lower Falling Entrance", region="Zig Skip Exit", # not a real region
destination="ziggurat2020_1", tag="_zig2_skip", direction=Direction.none),
Portal(name="Ziggurat Portal Room Exit", region="Rooted Ziggurat Portal Room Exit",
destination="ziggurat2020_3", tag="_"),
destination="ziggurat2020_3", tag="_", direction=Direction.south),
Portal(name="Ziggurat to Far Shore", region="Rooted Ziggurat Portal",
destination="Transit", tag="_teleporter_ziggurat teleporter"),
destination="Transit", tag="_teleporter_ziggurat teleporter", direction=Direction.floor),
Portal(name="Swamp Lower Exit", region="Swamp Front",
destination="Overworld Redux", tag="_conduit"),
destination="Overworld Redux", tag="_conduit", direction=Direction.north),
Portal(name="Swamp to Cathedral Main Entrance", region="Swamp to Cathedral Main Entrance Region",
destination="Cathedral Redux", tag="_main"),
destination="Cathedral Redux", tag="_main", direction=Direction.north),
Portal(name="Swamp to Cathedral Secret Legend Room Entrance", region="Swamp to Cathedral Treasure Room",
destination="Cathedral Redux", tag="_secret"),
destination="Cathedral Redux", tag="_secret", direction=Direction.south), # feels a little weird
Portal(name="Swamp to Gauntlet", region="Back of Swamp",
destination="Cathedral Arena", tag="_"),
destination="Cathedral Arena", tag="_", direction=Direction.north),
Portal(name="Swamp Shop", region="Swamp Front",
destination="Shop", tag="_"),
destination="Shop", tag="_", direction=Direction.north),
Portal(name="Swamp Upper Exit", region="Back of Swamp Laurels Area",
destination="Overworld Redux", tag="_wall"),
destination="Overworld Redux", tag="_wall", direction=Direction.north),
Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave Region",
destination="RelicVoid", tag="_teleporter_relic plinth"),
destination="RelicVoid", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Cathedral Main Exit", region="Cathedral Entry",
destination="Swamp Redux 2", tag="_main"),
destination="Swamp Redux 2", tag="_main", direction=Direction.south),
Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet",
destination="Cathedral Arena", tag="_"),
destination="Cathedral Arena", tag="_", direction=Direction.ladder_down), # elevators are ladders, right?
Portal(name="Cathedral Secret Legend Room Exit", region="Cathedral Secret Legend Room",
destination="Swamp Redux 2", tag="_secret"),
destination="Swamp Redux 2", tag="_secret", direction=Direction.north),
Portal(name="Gauntlet to Swamp", region="Cathedral Gauntlet Exit",
destination="Swamp Redux 2", tag="_"),
destination="Swamp Redux 2", tag="_", direction=Direction.south),
Portal(name="Gauntlet Elevator", region="Cathedral Gauntlet Checkpoint",
destination="Cathedral Redux", tag="_"),
destination="Cathedral Redux", tag="_", direction=Direction.ladder_up),
Portal(name="Gauntlet Shop", region="Cathedral Gauntlet Checkpoint",
destination="Shop", tag="_"),
destination="Shop", tag="_", direction=Direction.east),
Portal(name="Hero's Grave to Fortress", region="Hero Relic - Fortress",
destination="Fortress Reliquary", tag="_teleporter_relic plinth"),
destination="Fortress Reliquary", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Hero's Grave to Monastery", region="Hero Relic - Quarry",
destination="Monastery", tag="_teleporter_relic plinth"),
destination="Monastery", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Hero's Grave to West Garden", region="Hero Relic - West Garden",
destination="Archipelagos Redux", tag="_teleporter_relic plinth"),
destination="Archipelagos Redux", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Hero's Grave to East Forest", region="Hero Relic - East Forest",
destination="Sword Access", tag="_teleporter_relic plinth"),
destination="Sword Access", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Hero's Grave to Library", region="Hero Relic - Library",
destination="Library Hall", tag="_teleporter_relic plinth"),
destination="Library Hall", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Hero's Grave to Swamp", region="Hero Relic - Swamp",
destination="Swamp Redux 2", tag="_teleporter_relic plinth"),
destination="Swamp Redux 2", tag="_teleporter_relic plinth", direction=Direction.floor),
Portal(name="Far Shore to West Garden", region="Far Shore to West Garden Region",
destination="Archipelagos Redux", tag="_teleporter_archipelagos_teleporter"),
destination="Archipelagos Redux", tag="_teleporter_archipelagos_teleporter", direction=Direction.floor),
Portal(name="Far Shore to Library", region="Far Shore to Library Region",
destination="Library Lab", tag="_teleporter_library teleporter"),
destination="Library Lab", tag="_teleporter_library teleporter", direction=Direction.floor),
Portal(name="Far Shore to Quarry", region="Far Shore to Quarry Region",
destination="Quarry Redux", tag="_teleporter_quarry teleporter"),
destination="Quarry Redux", tag="_teleporter_quarry teleporter", direction=Direction.floor),
Portal(name="Far Shore to East Forest", region="Far Shore to East Forest Region",
destination="East Forest Redux", tag="_teleporter_forest teleporter"),
destination="East Forest Redux", tag="_teleporter_forest teleporter", direction=Direction.floor),
Portal(name="Far Shore to Fortress", region="Far Shore to Fortress Region",
destination="Fortress Arena", tag="_teleporter_spidertank"),
destination="Fortress Arena", tag="_teleporter_spidertank", direction=Direction.floor),
Portal(name="Far Shore to Atoll", region="Far Shore",
destination="Atoll Redux", tag="_teleporter_atoll"),
destination="Atoll Redux", tag="_teleporter_atoll", direction=Direction.floor),
Portal(name="Far Shore to Ziggurat", region="Far Shore",
destination="ziggurat2020_FTRoom", tag="_teleporter_ziggurat teleporter"),
destination="ziggurat2020_FTRoom", tag="_teleporter_ziggurat teleporter", direction=Direction.floor),
Portal(name="Far Shore to Heir", region="Far Shore",
destination="Spirit Arena", tag="_teleporter_spirit arena"),
destination="Spirit Arena", tag="_teleporter_spirit arena", direction=Direction.floor),
Portal(name="Far Shore to Town", region="Far Shore",
destination="Overworld Redux", tag="_teleporter_town"),
destination="Overworld Redux", tag="_teleporter_town", direction=Direction.floor),
Portal(name="Far Shore to Spawn", region="Far Shore to Spawn Region",
destination="Overworld Redux", tag="_teleporter_starting island"),
destination="Overworld Redux", tag="_teleporter_starting island", direction=Direction.floor),
Portal(name="Heir Arena Exit", region="Spirit Arena",
destination="Transit", tag="_teleporter_spirit arena"),
destination="Transit", tag="_teleporter_spirit arena", direction=Direction.floor),
Portal(name="Purgatory Bottom Exit", region="Purgatory",
destination="Purgatory", tag="_bottom"),
destination="Purgatory", tag="_bottom", direction=Direction.south),
Portal(name="Purgatory Top Exit", region="Purgatory",
destination="Purgatory", tag="_top"),
destination="Purgatory", tag="_top", direction=Direction.north),
]
@@ -523,6 +536,7 @@ class RegionInfo(NamedTuple):
game_scene: str # the name of the scene in the actual game
dead_end: int = 0 # if a region has only one exit
outlet_region: Optional[str] = None
is_fake_region: bool = False
# gets the outlet region name if it exists, the region if it doesn't
@@ -540,9 +554,9 @@ class DeadEnd(IntEnum):
# key is the AP region name. "Fake" in region info just means the mod won't receive that info at all
tunic_er_regions: Dict[str, RegionInfo] = {
"Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats),
"Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats, is_fake_region=True),
"Overworld": RegionInfo("Overworld Redux"), # main overworld, the central area
"Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats), # main overworld holy cross checks
"Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats, is_fake_region=True), # main overworld holy cross checks
"Overworld Belltower": RegionInfo("Overworld Redux"), # the area with the belltower and chest
"Overworld Belltower at Bell": RegionInfo("Overworld Redux"), # being able to ring the belltower, basically
"Overworld Swamp Upper Entry": RegionInfo("Overworld Redux"), # upper swamp entry spot
@@ -722,7 +736,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the front for combat logic
"Rooted Ziggurat Lower Mid Checkpoint": RegionInfo("ziggurat2020_3"), # the mid-checkpoint before double admin
"Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side
"Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Entry"), # for use with fixed shop on
"Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Entry", is_fake_region=True), # for use with fixed shop on
"Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3", outlet_region="Rooted Ziggurat Lower Back"), # the door itself on the zig 3 side
"Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom", outlet_region="Rooted Ziggurat Portal Room"),
"Rooted Ziggurat Portal Room": RegionInfo("ziggurat2020_FTRoom"),
@@ -758,7 +772,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Purgatory": RegionInfo("Purgatory"),
"Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats),
"Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats),
"Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats),
"Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats, is_fake_region=True),
}
@@ -1301,7 +1315,6 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[],
},
# cannot get from frogs back to front
"Library Exterior Ladder Region": {
"Library Exterior by Tree":
[],
@@ -1634,10 +1647,6 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Rooted Ziggurat Portal Room Entrance":
[],
},
"Zig Skip Exit": {
"Rooted Ziggurat Lower Front":
[],
},
"Rooted Ziggurat Portal Room Entrance": {
"Rooted Ziggurat Lower Back":
[],

View File

@@ -381,9 +381,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Overworld"].connect(
connecting_region=regions["Overworld Tunnel Turret"],
rule=lambda state: state.has(laurels, player))
regions["Overworld Tunnel Turret"].connect(
connecting_region=regions["Overworld"],
rule=lambda state: state.has_any({grapple, laurels}, player))
# always have access to Overworld, so connecting back isn't needed
# regions["Overworld Tunnel Turret"].connect(
# connecting_region=regions["Overworld"],
# rule=lambda state: state.has_any({grapple, laurels}, player))
cube_entrance = regions["Overworld"].connect(
connecting_region=regions["Cube Cave Entrance Region"],
@@ -1053,11 +1055,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Rooted Ziggurat Portal Room Entrance"].connect(
connecting_region=regions["Rooted Ziggurat Lower Back"])
# zig skip region only gets made if entrance rando and fewer shops are on
if options.entrance_rando and options.fixed_shop:
regions["Zig Skip Exit"].connect(
connecting_region=regions["Rooted Ziggurat Lower Front"])
regions["Rooted Ziggurat Portal"].connect(
connecting_region=regions["Rooted Ziggurat Portal Room"])
regions["Rooted Ziggurat Portal Room"].connect(
@@ -1226,14 +1223,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
and has_sword(state, player))))
if options.ladder_storage:
def get_portal_info(portal_sd: str) -> Tuple[str, str]:
for portal1, portal2 in portal_pairs.items():
if portal1.scene_destination() == portal_sd:
return portal1.name, get_portal_outlet_region(portal2, world)
if portal2.scene_destination() == portal_sd:
return portal2.name, get_portal_outlet_region(portal1, world)
raise Exception("no matches found in get_paired_region")
# connect ls elevation regions to their destinations
def ls_connect(origin_name: str, portal_sdt: str) -> None:
p_name, paired_region_name = get_portal_info(portal_sdt)

View File

@@ -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)
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)
if portal.region == "Zig Skip Exit":
if fixed_shop:
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:
dead_ends.append(portal)
# if fixed shop is off, remove this portal
for portal in portal_map:
if portal.region == "Zig Skip Exit":
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"
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,68 +385,101 @@ 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:
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.")
dead_ends.remove(portal1)
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:
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.\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)
f"plando connections in {player_name}'s YAML.")
# 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())[portal2.region] = []
traversal_reqs.setdefault(portal2.region, dict())[portal1.region] = []
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")):
@@ -362,35 +501,70 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
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,28 +577,56 @@ 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:
# 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
portal1 = portal
two_plus.remove(portal)
check_success = 1
break
if not portal1:
raise Exception("TUNIC: Failed to pair portals at first part of first phase.")
# then we find a portal in an inaccessible region
if check_success == 1:
for portal in two_plus:
# 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_plus) < 80:
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()
@@ -434,65 +636,123 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
# if not waterfall_plando, then we just want to pair secret gathering place now
elif portal.region != "Secret Gathering Place":
continue
# if they're not facing opposite directions, just continue
if entrance_layout == EntranceLayout.option_direction_pairs and not verify_direction_pair(portal, portal1):
continue
# if you have direction pairs, we need to make sure we don't run out of spots for problem portals
# 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(portal.region)
two_plus.remove(portal)
check_success = 2
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:
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)
portal_pairs[portal1] = portal2
check_success = 0
two_plus_direction_tracker[portal1.direction] -= 1
two_plus_direction_tracker[portal2.direction] -= 1
portal1 = None
portal2 = None
random_object.shuffle(two_plus)
# 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
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()
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():

View File

@@ -5,7 +5,7 @@ from typing import Dict, Any, TYPE_CHECKING
from decimal import Decimal, ROUND_HALF_UP
from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections,
PerGameCommonOptions, OptionGroup, Visibility, NamedRange)
PerGameCommonOptions, OptionGroup, Removed, Visibility, NamedRange)
from .er_data import portal_mapping
if TYPE_CHECKING:
from . import TunicWorld
@@ -147,14 +147,42 @@ class EntranceRando(TextChoice):
class FixedShop(Toggle):
"""
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.
This option has been superseded by the Entrance Layout option.
If enabled, it will override the Entrance Layout option.
This is kept to keep older yamls working, and will be removed at a later date.
"""
visibility = Visibility.none
internal_name = "fixed_shop"
display_name = "Fewer Shops in Entrance Rando"
class EntranceLayout(Choice):
"""
Decide how the Entrance Randomizer chooses how to pair the entrances.
Standard: Entrances are randomly connected. There are 6 shops in the pool with this option.
Fixed Shop: Forces the Windmill entrance to lead to a shop, and removes the other shops from the pool.
Adds another entrance in Rooted Ziggurat Lower to keep an even number of entrances.
Direction Pairs: Entrances facing opposite directions are paired together. There are 8 shops in the pool with this option.
Note: For seed groups, if one player in a group chooses Fixed Shop and another chooses Direction Pairs, it will error out.
Either of these options will override Standard within a seed group.
"""
internal_name = "entrance_layout"
display_name = "Entrance Layout"
option_standard = 0
option_fixed_shop = 1
option_direction_pairs = 2
default = 0
class Decoupled(Toggle):
"""
Decouple the entrances, so that when you go from one entrance to another, the return trip won't necessarily bring you back to the same place.
Note: For seed groups, all players in the group must have this option enabled or disabled.
"""
internal_name = "decoupled"
display_name = "Decoupled Entrances"
class LaurelsLocation(Choice):
"""
Force the Hero's Laurels to be placed at a location in your world.
@@ -210,13 +238,22 @@ class LocalFill(NamedRange):
class TunicPlandoConnections(PlandoConnections):
"""
Generic connection plando. Format is:
- entrance: "Entrance Name"
exit: "Exit Name"
- entrance: Entrance Name
exit: Exit Name
direction: Direction
percentage: 100
Direction must be one of entrance, exit, or both, and defaults to both if omitted.
Direction entrance means the entrance leads to the exit. Direction exit means the exit leads to the entrance.
If you do not have Decoupled enabled, you do not need the direction line, as it will only use both.
Percentage is an integer from 0 to 100 which determines whether that connection will be made. Defaults to 100 if omitted.
If the Entrance Layout option is set to Standard or Fixed Shop, you can plando multiple shops.
If the Entrance Layout option is set to Direction Pairs, your plando connections must be facing opposite directions.
Shop Portal 1-6 are South portals, and Shop Portal 7-8 are West portals.
This option does nothing if Entrance Rando is disabled.
"""
entrances = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"}
exits = {*(portal.name for portal in portal_mapping), "Shop", "Shop Portal"}
shops = {f"Shop Portal {i + 1}" for i in range(500)}
entrances = {portal.name for portal in portal_mapping}.union(shops)
exits = {portal.name for portal in portal_mapping}.union(shops)
duplicate_exits = True
@@ -329,6 +366,7 @@ class TunicOptions(PerGameCommonOptions):
start_with_sword: StartWithSword
keys_behind_bosses: KeysBehindBosses
ability_shuffling: AbilityShuffling
fool_traps: FoolTraps
laurels_location: LaurelsLocation
@@ -343,7 +381,9 @@ class TunicOptions(PerGameCommonOptions):
local_fill: LocalFill
entrance_rando: EntranceRando
fixed_shop: FixedShop
entrance_layout: EntranceLayout
decoupled: Decoupled
plando_connections: TunicPlandoConnections
combat_logic: CombatLogic
lanternless: Lanternless
@@ -353,9 +393,8 @@ class TunicOptions(PerGameCommonOptions):
ladder_storage: LadderStorage
ladder_storage_without_items: LadderStorageWithoutItems
plando_connections: TunicPlandoConnections
logic_rules: LogicRules
fixed_shop: FixedShop # will be removed at a later date
logic_rules: Removed # fully removed in the direction pairs update
tunic_option_groups = [
@@ -372,8 +411,14 @@ tunic_option_groups = [
LaurelsZips,
IceGrappling,
LadderStorage,
LadderStorageWithoutItems
])
LadderStorageWithoutItems,
]),
OptionGroup("Entrance Randomizer", [
EntranceRando,
EntranceLayout,
Decoupled,
TunicPlandoConnections,
]),
]
tunic_option_presets: Dict[str, Dict[str, Any]] = {

View File

@@ -1,5 +1,4 @@
from typing import Dict, TYPE_CHECKING
from decimal import Decimal, ROUND_HALF_UP
from worlds.generic.Rules import set_rule, forbid_item, add_rule
from BaseClasses import CollectionState
@@ -157,7 +156,7 @@ def set_region_rules(world: "TunicWorld") -> None:
if options.ladder_storage >= LadderStorage.option_medium:
# ls at any ladder in a safe spot in quarry to get to the monastery rope entrance
world.get_region("Quarry Back").connect(world.get_region("Monastery"),
add_rule(world.get_entrance(entrance_name="Quarry Back -> Monastery"),
rule=lambda state: can_ladder_storage(state, world))

View File

@@ -78,7 +78,8 @@ class TestERSpecial(TunicTestBase):
options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes,
options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true,
options.HexagonQuest.internal_name: options.HexagonQuest.option_false,
options.FixedShop.internal_name: options.FixedShop.option_false,
options.CombatLogic.internal_name: options.CombatLogic.option_off,
options.EntranceLayout.internal_name: options.EntranceLayout.option_fixed_shop,
options.IceGrappling.internal_name: options.IceGrappling.option_easy,
"plando_connections": [
{
@@ -126,3 +127,262 @@ class TestLadderStorage(TunicTestBase):
self.assertFalse(self.can_reach_location("Fortress Courtyard - Page Near Cave"))
self.collect_by_name(["Pages 24-25 (Prayer)"])
self.assertTrue(self.can_reach_location("Fortress Courtyard - Page Near Cave"))
# check that it still functions if in decoupled and every single normal entrance leads to a shop
class TestERDecoupledPlando(TunicTestBase):
options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes,
options.Decoupled.internal_name: options.Decoupled.option_true,
"plando_connections": [
{"entrance": "Stick House Entrance", "exit": "Shop Portal 1", "direction": "entrance"},
{"entrance": "Windmill Entrance", "exit": "Shop Portal 2", "direction": "entrance"},
{"entrance": "Well Ladder Entrance", "exit": "Shop Portal 3", "direction": "entrance"},
{"entrance": "Entrance to Well from Well Rail", "exit": "Shop Portal 4", "direction": "entrance"},
{"entrance": "Old House Door Entrance", "exit": "Shop Portal 5", "direction": "entrance"},
{"entrance": "Old House Waterfall Entrance", "exit": "Shop Portal 6", "direction": "entrance"},
{"entrance": "Entrance to Furnace from Well Rail", "exit": "Shop Portal 7", "direction": "entrance"},
{"entrance": "Entrance to Furnace under Windmill", "exit": "Shop Portal 8", "direction": "entrance"},
{"entrance": "Entrance to Furnace near West Garden", "exit": "Shop Portal 9",
"direction": "entrance"},
{"entrance": "Entrance to Furnace from Beach", "exit": "Shop Portal 10", "direction": "entrance"},
{"entrance": "Caustic Light Cave Entrance", "exit": "Shop Portal 11", "direction": "entrance"},
{"entrance": "Swamp Upper Entrance", "exit": "Shop Portal 12", "direction": "entrance"},
{"entrance": "Swamp Lower Entrance", "exit": "Shop Portal 13", "direction": "entrance"},
{"entrance": "Ruined Passage Not-Door Entrance", "exit": "Shop Portal 14", "direction": "entrance"},
{"entrance": "Ruined Passage Door Entrance", "exit": "Shop Portal 15", "direction": "entrance"},
{"entrance": "Atoll Upper Entrance", "exit": "Shop Portal 16", "direction": "entrance"},
{"entrance": "Atoll Lower Entrance", "exit": "Shop Portal 17", "direction": "entrance"},
{"entrance": "Special Shop Entrance", "exit": "Shop Portal 18", "direction": "entrance"},
{"entrance": "Maze Cave Entrance", "exit": "Shop Portal 19", "direction": "entrance"},
{"entrance": "West Garden Entrance near Belltower", "exit": "Shop Portal 20",
"direction": "entrance"},
{"entrance": "West Garden Entrance from Furnace", "exit": "Shop Portal 21", "direction": "entrance"},
{"entrance": "West Garden Laurels Entrance", "exit": "Shop Portal 22", "direction": "entrance"},
{"entrance": "Temple Door Entrance", "exit": "Shop Portal 23", "direction": "entrance"},
{"entrance": "Temple Rafters Entrance", "exit": "Shop Portal 24", "direction": "entrance"},
{"entrance": "Ruined Shop Entrance", "exit": "Shop Portal 25", "direction": "entrance"},
{"entrance": "Patrol Cave Entrance", "exit": "Shop Portal 26", "direction": "entrance"},
{"entrance": "Hourglass Cave Entrance", "exit": "Shop Portal 27", "direction": "entrance"},
{"entrance": "Changing Room Entrance", "exit": "Shop Portal 28", "direction": "entrance"},
{"entrance": "Cube Cave Entrance", "exit": "Shop Portal 29", "direction": "entrance"},
{"entrance": "Stairs from Overworld to Mountain", "exit": "Shop Portal 30", "direction": "entrance"},
{"entrance": "Overworld to Fortress", "exit": "Shop Portal 31", "direction": "entrance"},
{"entrance": "Fountain HC Door Entrance", "exit": "Shop Portal 32", "direction": "entrance"},
{"entrance": "Southeast HC Door Entrance", "exit": "Shop Portal 33", "direction": "entrance"},
{"entrance": "Overworld to Quarry Connector", "exit": "Shop Portal 34", "direction": "entrance"},
{"entrance": "Dark Tomb Main Entrance", "exit": "Shop Portal 35", "direction": "entrance"},
{"entrance": "Overworld to Forest Belltower", "exit": "Shop Portal 36", "direction": "entrance"},
{"entrance": "Town to Far Shore", "exit": "Shop Portal 37", "direction": "entrance"},
{"entrance": "Spawn to Far Shore", "exit": "Shop Portal 38", "direction": "entrance"},
{"entrance": "Secret Gathering Place Entrance", "exit": "Shop Portal 39", "direction": "entrance"},
{"entrance": "Secret Gathering Place Exit", "exit": "Shop Portal 40", "direction": "entrance"},
{"entrance": "Windmill Exit", "exit": "Shop Portal 41", "direction": "entrance"},
{"entrance": "Windmill Shop", "exit": "Shop Portal 42", "direction": "entrance"},
{"entrance": "Old House Door Exit", "exit": "Shop Portal 43", "direction": "entrance"},
{"entrance": "Old House to Glyph Tower", "exit": "Shop Portal 44", "direction": "entrance"},
{"entrance": "Old House Waterfall Exit", "exit": "Shop Portal 45", "direction": "entrance"},
{"entrance": "Glyph Tower Exit", "exit": "Shop Portal 46", "direction": "entrance"},
{"entrance": "Changing Room Exit", "exit": "Shop Portal 47", "direction": "entrance"},
{"entrance": "Fountain HC Room Exit", "exit": "Shop Portal 48", "direction": "entrance"},
{"entrance": "Cube Cave Exit", "exit": "Shop Portal 49", "direction": "entrance"},
{"entrance": "Guard Patrol Cave Exit", "exit": "Shop Portal 50", "direction": "entrance"},
{"entrance": "Ruined Shop Exit", "exit": "Shop Portal 51", "direction": "entrance"},
{"entrance": "Furnace Exit towards Well", "exit": "Shop Portal 52", "direction": "entrance"},
{"entrance": "Furnace Exit to Dark Tomb", "exit": "Shop Portal 53", "direction": "entrance"},
{"entrance": "Furnace Exit towards West Garden", "exit": "Shop Portal 54", "direction": "entrance"},
{"entrance": "Furnace Exit to Beach", "exit": "Shop Portal 55", "direction": "entrance"},
{"entrance": "Furnace Exit under Windmill", "exit": "Shop Portal 56", "direction": "entrance"},
{"entrance": "Stick House Exit", "exit": "Shop Portal 57", "direction": "entrance"},
{"entrance": "Ruined Passage Not-Door Exit", "exit": "Shop Portal 58", "direction": "entrance"},
{"entrance": "Ruined Passage Door Exit", "exit": "Shop Portal 59", "direction": "entrance"},
{"entrance": "Southeast HC Room Exit", "exit": "Shop Portal 60", "direction": "entrance"},
{"entrance": "Caustic Light Cave Exit", "exit": "Shop Portal 61", "direction": "entrance"},
{"entrance": "Maze Cave Exit", "exit": "Shop Portal 62", "direction": "entrance"},
{"entrance": "Hourglass Cave Exit", "exit": "Shop Portal 63", "direction": "entrance"},
{"entrance": "Special Shop Exit", "exit": "Shop Portal 64", "direction": "entrance"},
{"entrance": "Temple Rafters Exit", "exit": "Shop Portal 65", "direction": "entrance"},
{"entrance": "Temple Door Exit", "exit": "Shop Portal 66", "direction": "entrance"},
{"entrance": "Forest Belltower to Fortress", "exit": "Shop Portal 67", "direction": "entrance"},
{"entrance": "Forest Belltower to Forest", "exit": "Shop Portal 68", "direction": "entrance"},
{"entrance": "Forest Belltower to Overworld", "exit": "Shop Portal 69", "direction": "entrance"},
{"entrance": "Forest Belltower to Guard Captain Room", "exit": "Shop Portal 70",
"direction": "entrance"},
{"entrance": "Forest to Belltower", "exit": "Shop Portal 71", "direction": "entrance"},
{"entrance": "Forest Guard House 1 Lower Entrance", "exit": "Shop Portal 72",
"direction": "entrance"},
{"entrance": "Forest Guard House 1 Gate Entrance", "exit": "Shop Portal 73",
"direction": "entrance"},
{"entrance": "Forest Dance Fox Outside Doorway", "exit": "Shop Portal 74", "direction": "entrance"},
{"entrance": "Forest to Far Shore", "exit": "Shop Portal 75", "direction": "entrance"},
{"entrance": "Forest Guard House 2 Lower Entrance", "exit": "Shop Portal 76",
"direction": "entrance"},
{"entrance": "Forest Guard House 2 Upper Entrance", "exit": "Shop Portal 77",
"direction": "entrance"},
{"entrance": "Forest Grave Path Lower Entrance", "exit": "Shop Portal 78", "direction": "entrance"},
{"entrance": "Forest Grave Path Upper Entrance", "exit": "Shop Portal 79", "direction": "entrance"},
{"entrance": "Forest Grave Path Upper Exit", "exit": "Shop Portal 80", "direction": "entrance"},
{"entrance": "Forest Grave Path Lower Exit", "exit": "Shop Portal 81", "direction": "entrance"},
{"entrance": "East Forest Hero's Grave", "exit": "Shop Portal 82", "direction": "entrance"},
{"entrance": "Guard House 1 Dance Fox Exit", "exit": "Shop Portal 83", "direction": "entrance"},
{"entrance": "Guard House 1 Lower Exit", "exit": "Shop Portal 84", "direction": "entrance"},
{"entrance": "Guard House 1 Upper Forest Exit", "exit": "Shop Portal 85", "direction": "entrance"},
{"entrance": "Guard House 1 to Guard Captain Room", "exit": "Shop Portal 86",
"direction": "entrance"},
{"entrance": "Guard House 2 Lower Exit", "exit": "Shop Portal 87", "direction": "entrance"},
{"entrance": "Guard House 2 Upper Exit", "exit": "Shop Portal 88", "direction": "entrance"},
{"entrance": "Guard Captain Room Non-Gate Exit", "exit": "Shop Portal 89", "direction": "entrance"},
{"entrance": "Guard Captain Room Gate Exit", "exit": "Shop Portal 90", "direction": "entrance"},
{"entrance": "Well Ladder Exit", "exit": "Shop Portal 91", "direction": "entrance"},
{"entrance": "Well to Well Boss", "exit": "Shop Portal 92", "direction": "entrance"},
{"entrance": "Well Exit towards Furnace", "exit": "Shop Portal 93", "direction": "entrance"},
{"entrance": "Well Boss to Well", "exit": "Shop Portal 94", "direction": "entrance"},
{"entrance": "Checkpoint to Dark Tomb", "exit": "Shop Portal 95", "direction": "entrance"},
{"entrance": "Dark Tomb to Overworld", "exit": "Shop Portal 96", "direction": "entrance"},
{"entrance": "Dark Tomb to Furnace", "exit": "Shop Portal 97", "direction": "entrance"},
{"entrance": "Dark Tomb to Checkpoint", "exit": "Shop Portal 98", "direction": "entrance"},
{"entrance": "West Garden Exit near Hero's Grave", "exit": "Shop Portal 99",
"direction": "entrance"},
{"entrance": "West Garden to Magic Dagger House", "exit": "Shop Portal 100",
"direction": "entrance"},
{"entrance": "West Garden Exit after Boss", "exit": "Shop Portal 101", "direction": "entrance"},
{"entrance": "West Garden Shop", "exit": "Shop Portal 102", "direction": "entrance"},
{"entrance": "West Garden Laurels Exit", "exit": "Shop Portal 103", "direction": "entrance"},
{"entrance": "West Garden Hero's Grave", "exit": "Shop Portal 104", "direction": "entrance"},
{"entrance": "West Garden to Far Shore", "exit": "Shop Portal 105", "direction": "entrance"},
{"entrance": "Magic Dagger House Exit", "exit": "Shop Portal 106", "direction": "entrance"},
{"entrance": "Fortress Courtyard to Fortress Grave Path Lower", "exit": "Shop Portal 107",
"direction": "entrance"},
{"entrance": "Fortress Courtyard to Fortress Grave Path Upper", "exit": "Shop Portal 108",
"direction": "entrance"},
{"entrance": "Fortress Courtyard to Fortress Interior", "exit": "Shop Portal 109",
"direction": "entrance"},
{"entrance": "Fortress Courtyard to East Fortress", "exit": "Shop Portal 110",
"direction": "entrance"},
{"entrance": "Fortress Courtyard to Beneath the Vault", "exit": "Shop Portal 111",
"direction": "entrance"},
{"entrance": "Fortress Courtyard to Forest Belltower", "exit": "Shop Portal 112",
"direction": "entrance"},
{"entrance": "Fortress Courtyard to Overworld", "exit": "Shop Portal 113", "direction": "entrance"},
{"entrance": "Fortress Courtyard Shop", "exit": "Shop Portal 114", "direction": "entrance"},
{"entrance": "Beneath the Vault to Fortress Interior", "exit": "Shop Portal 115",
"direction": "entrance"},
{"entrance": "Beneath the Vault to Fortress Courtyard", "exit": "Shop Portal 116",
"direction": "entrance"},
{"entrance": "Fortress Interior Main Exit", "exit": "Shop Portal 117", "direction": "entrance"},
{"entrance": "Fortress Interior to Beneath the Earth", "exit": "Shop Portal 118",
"direction": "entrance"},
{"entrance": "Fortress Interior to Siege Engine Arena", "exit": "Shop Portal 119",
"direction": "entrance"},
{"entrance": "Fortress Interior Shop", "exit": "Shop Portal 120", "direction": "entrance"},
{"entrance": "Fortress Interior to East Fortress Upper", "exit": "Shop Portal 121",
"direction": "entrance"},
{"entrance": "Fortress Interior to East Fortress Lower", "exit": "Shop Portal 122",
"direction": "entrance"},
{"entrance": "East Fortress to Interior Lower", "exit": "Shop Portal 123", "direction": "entrance"},
{"entrance": "East Fortress to Courtyard", "exit": "Shop Portal 124", "direction": "entrance"},
{"entrance": "East Fortress to Interior Upper", "exit": "Shop Portal 125", "direction": "entrance"},
{"entrance": "Fortress Grave Path Lower Exit", "exit": "Shop Portal 126", "direction": "entrance"},
{"entrance": "Fortress Hero's Grave", "exit": "Shop Portal 127", "direction": "entrance"},
{"entrance": "Fortress Grave Path Upper Exit", "exit": "Shop Portal 128", "direction": "entrance"},
{"entrance": "Fortress Grave Path Dusty Entrance", "exit": "Shop Portal 129",
"direction": "entrance"},
{"entrance": "Dusty Exit", "exit": "Shop Portal 130", "direction": "entrance"},
{"entrance": "Siege Engine Arena to Fortress", "exit": "Shop Portal 131", "direction": "entrance"},
{"entrance": "Fortress to Far Shore", "exit": "Shop Portal 132", "direction": "entrance"},
{"entrance": "Atoll Upper Exit", "exit": "Shop Portal 133", "direction": "entrance"},
{"entrance": "Atoll Lower Exit", "exit": "Shop Portal 134", "direction": "entrance"},
{"entrance": "Atoll Shop", "exit": "Shop Portal 135", "direction": "entrance"},
{"entrance": "Atoll to Far Shore", "exit": "Shop Portal 136", "direction": "entrance"},
{"entrance": "Atoll Statue Teleporter", "exit": "Shop Portal 137", "direction": "entrance"},
{"entrance": "Frog Stairs Eye Entrance", "exit": "Shop Portal 138", "direction": "entrance"},
{"entrance": "Frog Stairs Mouth Entrance", "exit": "Shop Portal 139", "direction": "entrance"},
{"entrance": "Frog Stairs Eye Exit", "exit": "Shop Portal 140", "direction": "entrance"},
{"entrance": "Frog Stairs Mouth Exit", "exit": "Shop Portal 141", "direction": "entrance"},
{"entrance": "Frog Stairs to Frog's Domain's Entrance", "exit": "Shop Portal 142",
"direction": "entrance"},
{"entrance": "Frog Stairs to Frog's Domain's Exit", "exit": "Shop Portal 143",
"direction": "entrance"},
{"entrance": "Frog's Domain Ladder Exit", "exit": "Shop Portal 144", "direction": "entrance"},
{"entrance": "Frog's Domain Orb Exit", "exit": "Shop Portal 145", "direction": "entrance"},
{"entrance": "Library Exterior Tree", "exit": "Shop Portal 146", "direction": "entrance"},
{"entrance": "Library Exterior Ladder", "exit": "Shop Portal 147", "direction": "entrance"},
{"entrance": "Library Hall Bookshelf Exit", "exit": "Shop Portal 148", "direction": "entrance"},
{"entrance": "Library Hero's Grave", "exit": "Shop Portal 149", "direction": "entrance"},
{"entrance": "Library Hall to Rotunda", "exit": "Shop Portal 150", "direction": "entrance"},
{"entrance": "Library Rotunda Lower Exit", "exit": "Shop Portal 151", "direction": "entrance"},
{"entrance": "Library Rotunda Upper Exit", "exit": "Shop Portal 152", "direction": "entrance"},
{"entrance": "Library Lab to Rotunda", "exit": "Shop Portal 153", "direction": "entrance"},
{"entrance": "Library to Far Shore", "exit": "Shop Portal 154", "direction": "entrance"},
{"entrance": "Library Lab to Librarian Arena", "exit": "Shop Portal 155", "direction": "entrance"},
{"entrance": "Librarian Arena Exit", "exit": "Shop Portal 156", "direction": "entrance"},
{"entrance": "Stairs to Top of the Mountain", "exit": "Shop Portal 157", "direction": "entrance"},
{"entrance": "Mountain to Quarry", "exit": "Shop Portal 158", "direction": "entrance"},
{"entrance": "Mountain to Overworld", "exit": "Shop Portal 159", "direction": "entrance"},
{"entrance": "Top of the Mountain Exit", "exit": "Shop Portal 160", "direction": "entrance"},
{"entrance": "Quarry Connector to Overworld", "exit": "Shop Portal 161", "direction": "entrance"},
{"entrance": "Quarry Connector to Quarry", "exit": "Shop Portal 162", "direction": "entrance"},
{"entrance": "Quarry to Overworld Exit", "exit": "Shop Portal 163", "direction": "entrance"},
{"entrance": "Quarry Shop", "exit": "Shop Portal 164", "direction": "entrance"},
{"entrance": "Quarry to Monastery Front", "exit": "Shop Portal 165", "direction": "entrance"},
{"entrance": "Quarry to Monastery Back", "exit": "Shop Portal 166", "direction": "entrance"},
{"entrance": "Quarry to Mountain", "exit": "Shop Portal 167", "direction": "entrance"},
{"entrance": "Quarry to Ziggurat", "exit": "Shop Portal 168", "direction": "entrance"},
{"entrance": "Quarry to Far Shore", "exit": "Shop Portal 169", "direction": "entrance"},
{"entrance": "Monastery Rear Exit", "exit": "Shop Portal 170", "direction": "entrance"},
{"entrance": "Monastery Front Exit", "exit": "Shop Portal 171", "direction": "entrance"},
{"entrance": "Monastery Hero's Grave", "exit": "Shop Portal 172", "direction": "entrance"},
{"entrance": "Ziggurat Entry Hallway to Ziggurat Upper", "exit": "Shop Portal 173",
"direction": "entrance"},
{"entrance": "Ziggurat Entry Hallway to Quarry", "exit": "Shop Portal 174", "direction": "entrance"},
{"entrance": "Ziggurat Upper to Ziggurat Entry Hallway", "exit": "Shop Portal 175",
"direction": "entrance"},
{"entrance": "Ziggurat Upper to Ziggurat Tower", "exit": "Shop Portal 176", "direction": "entrance"},
{"entrance": "Ziggurat Tower to Ziggurat Upper", "exit": "Shop Portal 177", "direction": "entrance"},
{"entrance": "Ziggurat Tower to Ziggurat Lower", "exit": "Shop Portal 178", "direction": "entrance"},
{"entrance": "Ziggurat Lower to Ziggurat Tower", "exit": "Shop Portal 179", "direction": "entrance"},
{"entrance": "Ziggurat Portal Room Entrance", "exit": "Shop Portal 180", "direction": "entrance"},
{"entrance": "Ziggurat Portal Room Exit", "exit": "Shop Portal 181", "direction": "entrance"},
{"entrance": "Ziggurat to Far Shore", "exit": "Shop Portal 182", "direction": "entrance"},
{"entrance": "Swamp Lower Exit", "exit": "Shop Portal 183", "direction": "entrance"},
{"entrance": "Swamp to Cathedral Main Entrance", "exit": "Shop Portal 184", "direction": "entrance"},
{"entrance": "Swamp to Cathedral Secret Legend Room Entrance", "exit": "Shop Portal 185",
"direction": "entrance"},
{"entrance": "Swamp to Gauntlet", "exit": "Shop Portal 186", "direction": "entrance"},
{"entrance": "Swamp Shop", "exit": "Shop Portal 187", "direction": "entrance"},
{"entrance": "Swamp Upper Exit", "exit": "Shop Portal 188", "direction": "entrance"},
{"entrance": "Swamp Hero's Grave", "exit": "Shop Portal 189", "direction": "entrance"},
{"entrance": "Cathedral Main Exit", "exit": "Shop Portal 190", "direction": "entrance"},
{"entrance": "Cathedral Elevator", "exit": "Shop Portal 191", "direction": "entrance"},
{"entrance": "Cathedral Secret Legend Room Exit", "exit": "Shop Portal 192",
"direction": "entrance"},
{"entrance": "Gauntlet to Swamp", "exit": "Shop Portal 193", "direction": "entrance"},
{"entrance": "Gauntlet Elevator", "exit": "Shop Portal 194", "direction": "entrance"},
{"entrance": "Gauntlet Shop", "exit": "Shop Portal 195", "direction": "entrance"},
{"entrance": "Hero's Grave to Fortress", "exit": "Shop Portal 196", "direction": "entrance"},
{"entrance": "Hero's Grave to Monastery", "exit": "Shop Portal 197", "direction": "entrance"},
{"entrance": "Hero's Grave to West Garden", "exit": "Shop Portal 198", "direction": "entrance"},
{"entrance": "Hero's Grave to East Forest", "exit": "Shop Portal 199", "direction": "entrance"},
{"entrance": "Hero's Grave to Library", "exit": "Shop Portal 200", "direction": "entrance"},
{"entrance": "Hero's Grave to Swamp", "exit": "Shop Portal 201", "direction": "entrance"},
{"entrance": "Far Shore to West Garden", "exit": "Shop Portal 202", "direction": "entrance"},
{"entrance": "Far Shore to Library", "exit": "Shop Portal 203", "direction": "entrance"},
{"entrance": "Far Shore to Quarry", "exit": "Shop Portal 204", "direction": "entrance"},
{"entrance": "Far Shore to East Forest", "exit": "Shop Portal 205", "direction": "entrance"},
{"entrance": "Far Shore to Fortress", "exit": "Shop Portal 206", "direction": "entrance"},
{"entrance": "Far Shore to Atoll", "exit": "Shop Portal 207", "direction": "entrance"},
{"entrance": "Far Shore to Ziggurat", "exit": "Shop Portal 208", "direction": "entrance"},
{"entrance": "Far Shore to Heir", "exit": "Shop Portal 209", "direction": "entrance"},
{"entrance": "Far Shore to Town", "exit": "Shop Portal 210", "direction": "entrance"},
{"entrance": "Far Shore to Spawn", "exit": "Shop Portal 211", "direction": "entrance"},
{"entrance": "Heir Arena Exit", "exit": "Shop Portal 212", "direction": "entrance"},
{"entrance": "Purgatory Bottom Exit", "exit": "Shop Portal 213", "direction": "entrance"},
{"entrance": "Purgatory Top Exit", "exit": "Shop Portal 214", "direction": "entrance"},
{"entrance": "Shop Portal 215", "exit": "Shop Portal 216", "direction": "entrance"},
{"entrance": "Shop Portal 217", "exit": "Shop Portal 218", "direction": "entrance"},
{"entrance": "Shop Portal 219", "exit": "Shop Portal 220", "direction": "entrance"},
{"entrance": "Shop Portal 221", "exit": "Shop Portal 222", "direction": "entrance"},
{"entrance": "Shop Portal 223", "exit": "Shop Portal 224", "direction": "entrance"},
{"entrance": "Shop Portal 225", "exit": "Shop Portal 226", "direction": "entrance"},
{"entrance": "Shop Portal 227", "exit": "Shop Portal 228", "direction": "entrance"},
{"entrance": "Shop Portal 229", "exit": "Shop Portal 230", "direction": "entrance"},
]}