mirror of
https://github.com/MarioSpore/Grinch-AP.git
synced 2025-10-21 20:21:32 -06:00
TUNIC: Entrance rando Direction Pairs + Decoupled (#3761)
* Fix merge conflict * Fix formatting, fix rule for heir access after merge * Writing combat logic helpers * More helpers! * More logic! * Rename has_stick to has_melee, some fixes per Medic's review * Clamp max power from sword upgrades * Wrote the rest of the helpers * Remove unused import * Apply item classifications * Create the combat logic option * Item classification varies based on option * Add the shop sword logic stuff in * Add the rules for the boss-only option * Fix tiny issues * Some early Overworld combat logic * Fill out swamp combat logic * Add note * Bump up Boss Scav and Heir * More revisions to combat logic * Some changes, currently broken * New system for power, kinda jank probably * Revisions to new system, needs more balancing * Cap attack upgrades * Uncap mp power since it's directly related to damage output * Voidlings * Put together a table showing the vanilla-expected stats for each area * Added some info on potion counts * Made new helper functions * Make has_required_stats * Make has_combat_reqs * Update er_rules for new combat reqs * Fix all the broken things ever * Remove outdated todo * Make temp option for testing logic * More flexible choices for combat items * Hard require sword for bosses * Temporarily default combat logic to on * Finish writing overworld combat logic * East Forest combat logic done * Remove a few easy ones * Finish beneath the well * Dark Tomb combat logic * West Garden combat logic * make unit tests checkmark again * Weird west garden dagger house edge case * Try block for that weird west garden edge case * Add quarry combat logic * Update to filter out unreachable regions outside of ER * Fortress Grave Path logic, and a couple fixes to the west garden logic * Fortress east shortcut logic, and rewriting the try except blocks to use finally * Refactor to use a new function cause wow there was a lot of repeated code * Add combat logic to the other two sets of fortress fuses * Add combat rules to beneath the vault * Fix missing cathedral -> elevator connection * Combat logic for cathedral to elevator * Add cathedral main region, rename cathedral -> cathedral entry * Setup cathedral combat logic * Adjust locations' regions for ER * Add laurels zip logic to the chest in the spike room in cathedral * Add combat logic to frog's domain * Move frog's domain locations to regions for combat logic * Add new frog's domain regions for combat logic * Update region name for frog's domain * Fix typo * Add more regions for lower zig * Move around lower zig regions for combat logic * Lower Zig combat logic * Upper zig combat logic * Fix typo * Fix typos * Fix missing world. * Update combat logic description * Add todo * Add todo * Don't make zig skip if er or fixed shop is off * Make it so zig skip is only made with fewer shops and er * Temporarily default combat logic on * Update test to explicitly disable combat logic * Update test_access.py * Slight wording changes * Fix bugs, refactor quarry regions so you can access chests in lower quarry with ice grapples * Run through checks you can do with magic dagger * Run through checks you can do with magic dagger * Add rule for entering town portal of having equipment to deal with enemies * Add rule for atoll near the 6 crabs surrounding a poor defenseless baby slorm * Update the rule for the chest near the 6 crabs surrounding a slorm to also possibly require laurels * Revamp combat logic function to work properly without melee * Add laurels rules to combat logic chests * Modify beneath the vault bridge rule to need a lantern if combat logic is on * Put in money logic * Dagger or combat for swamp big skeleton chest * Remove the 100 moneys from logic * Modify lower zig ls drop region destinations * Remove completed todo * Reword combat logic option description, remove test option * Add combat logic to slot data * Merge Silent's missing slot data bugfix PR #3628 * Remove test combat option * Update combat logic description * Fix secret gathering place issue * Fix secret gathering place issue * Fix lower zig ls rule * Fix accidentally removed librarian rule * Remove redundant rule * Update gauntlet rule to hard-require a sword * Add test for a problematic connection * Adjust combat logic to deal with weird edge cases so it doesn't take stuff out of logic that was previously in logic * Fix create_item classification * Update some comments * Update per exempt's suggestion * Add combat logic to the well boss fight, reorder the combat logic stuff a little to better section them off * Add EntranceLayout option * Add back LogicRules as an invisible option, to not break old yamls * Fix a bug with seed group, continue changing fixed shop to entrance layout * Fix missed fixed shop -> entrance layout spot * Fix bug in seed groups with fixed shop on and off * Add entrance layout to the UT regen stuff * Put direction. in, will add them later * Remove unused elevation from portal class * Got like half of them in * Finish adding all of the directions * Add combat rule for zig front to back * Update per Medic's suggestion * Update ladder storage without items option description * Mess with state with collect and remove to save like 2 seconds (never again) * Save even more time, still never going to do this again on anything else * Add option check for collect and remove * Add directions to shop portals * Update direction in Portal with default * Move Direction above Portal * Add decoupled option, mess with plando connection stuff * Merge, implement verify plando directions * Condense the stuff in change and remove to less lines (thanks medic) * Remove unused thing * Swap to using logicmixin instead of prog_items (thanks Vi) * Fix consistency in stat counters * Add back something that was needed * Fix mistake when adding back * Making the fix better (thanks medic) * Make it actually return false if it gets to the backup lists and fails them * Fix stuff after merge * Add outlet regions, create new regions as needed for them * Put together part of decoupled and direction pairs * make direction pairs work * Make decoupled work * Make fixed shop work again * Fix a few minor bugs * Fix a few minor bugs * Fix plando * god i love programming * Reorder portal list * Update portal sorter for variable shops * Add missing parameter * Some cleanup of prints and functions * Fix typo * it's aliiiiiive * Make seed groups not sync decoupled * Add test with full-shop plando * Fix bug with vanilla portals * Handle plando connections and direction pair errors * Update plando checking for decoupled * Fix typo * Fix exception text to be shorter * Add some more comments * Add todo note * Remove unused safety thing * Remove extra plando connections definition in options * Make seed groups in decoupled with overlapping but not fully overlapped plando connections interact nicely without messing with what the entrances look like in the spoiler log * Fix weird edge case that is technically user error * Add note to fixed shop * Fix parsing shop names in UT * Remove debug print * Actually make UT work * multiworld. to world. * Fix typo from merge * Make it so the shops show up in the entrance hints * Fix bug in ladder storage rules * Remove blank line * # Conflicts: # worlds/tunic/__init__.py # worlds/tunic/er_data.py # worlds/tunic/er_rules.py # worlds/tunic/er_scripts.py # worlds/tunic/rules.py # worlds/tunic/test/test_access.py * Fix issues after merge * Update plando connections stuff in docs * Fix library mistake * has_stick -> has_melee * has_stick -> has_melee * Add a failsafe for direction pairing * Fix playthrough crash bug * Remove init from logicmixin * Updates per code review (thanks hesto) * has_stick to has_melee in newer update * has_stick to has_melee in newer update * # Conflicts: # worlds/tunic/__init__.py # worlds/tunic/combat_logic.py # worlds/tunic/er_data.py # worlds/tunic/er_rules.py # worlds/tunic/er_scripts.py * Cleanup more stuff after merge * Revert "Cleanup more stuff after merge" This reverts 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:
@@ -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,
|
||||
|
@@ -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"]),
|
||||
|
@@ -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.
|
||||
|
||||
|
@@ -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":
|
||||
[],
|
||||
|
@@ -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)
|
||||
|
@@ -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():
|
||||
|
@@ -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]] = {
|
||||
|
@@ -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))
|
||||
|
||||
|
||||
|
@@ -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"},
|
||||
]}
|
||||
|
Reference in New Issue
Block a user