TUNIC: Entrance rando Direction Pairs + Decoupled (#3761)

* Fix merge conflict

* Fix formatting, fix rule for heir access after merge

* Writing combat logic helpers

* More helpers!

* More logic!

* Rename has_stick to has_melee, some fixes per Medic's review

* Clamp max power from sword upgrades

* Wrote the rest of the helpers

* Remove unused import

* Apply item classifications

* Create the combat logic option

* Item classification varies based on option

* Add the shop sword logic stuff in

* Add the rules for the boss-only option

* Fix tiny issues

* Some early Overworld combat logic

* Fill out swamp combat logic

* Add note

* Bump up Boss Scav and Heir

* More revisions to combat logic

* Some changes, currently broken

* New system for power, kinda jank probably

* Revisions to new system, needs more balancing

* Cap attack upgrades

* Uncap mp power since it's directly related to damage output

* Voidlings

* Put together a table showing the vanilla-expected stats for each area

* Added some info on potion counts

* Made new helper functions

* Make has_required_stats

* Make has_combat_reqs

* Update er_rules for new combat reqs

* Fix all the broken things ever

* Remove outdated todo

* Make temp option for testing logic

* More flexible choices for combat items

* Hard require sword for bosses

* Temporarily default combat logic to on

* Finish writing overworld combat logic

* East Forest combat logic done

* Remove a few easy ones

* Finish beneath the well

* Dark Tomb combat logic

* West Garden combat logic

* make unit tests checkmark again

* Weird west garden dagger house edge case

* Try block for that weird west garden edge case

* Add quarry combat logic

* Update to filter out unreachable regions outside of ER

* Fortress Grave Path logic, and a couple fixes to the west garden logic

* Fortress east shortcut logic, and rewriting the try except blocks to use finally

* Refactor to use a new function cause wow there was a lot of repeated code

* Add combat logic to the other two sets of fortress fuses

* Add combat rules to beneath the vault

* Fix missing cathedral -> elevator connection

* Combat logic for cathedral to elevator

* Add cathedral main region, rename cathedral -> cathedral entry

* Setup cathedral combat logic

* Adjust locations' regions for ER

* Add laurels zip logic to the chest in the spike room in cathedral

* Add combat logic to frog's domain

* Move frog's domain locations to regions for combat logic

* Add new frog's domain regions for combat logic

* Update region name for frog's domain

* Fix typo

* Add more regions for lower zig

* Move around lower zig regions for combat logic

* Lower Zig combat logic

* Upper zig combat logic

* Fix typo

* Fix typos

* Fix missing world.

* Update combat logic description

* Add todo

* Add todo

* Don't make zig skip if er or fixed shop is off

* Make it so zig skip is only made with fewer shops and er

* Temporarily default combat logic on

* Update test to explicitly disable combat logic

* Update test_access.py

* Slight wording changes

* Fix bugs, refactor quarry regions so you can access chests in lower quarry with ice grapples

* Run through checks you can do with magic dagger

* Run through checks you can do with magic dagger

* Add rule for entering town portal of having equipment to deal with enemies

* Add rule for atoll near the 6 crabs surrounding a poor defenseless baby slorm

* Update the rule for the chest near the 6 crabs surrounding a slorm to also possibly require laurels

* Revamp combat logic function to work properly without melee

* Add laurels rules to combat logic chests

* Modify beneath the vault bridge rule to need a lantern if combat logic is on

* Put in money logic

* Dagger or combat for swamp big skeleton chest

* Remove the 100 moneys from logic

* Modify lower zig ls drop region destinations

* Remove completed todo

* Reword combat logic option description, remove test option

* Add combat logic to slot data

* Merge Silent's missing slot data bugfix PR #3628

* Remove test combat option

* Update combat logic description

* Fix secret gathering place issue

* Fix secret gathering place issue

* Fix lower zig ls rule

* Fix accidentally removed librarian rule

* Remove redundant rule

* Update gauntlet rule to hard-require a sword

* Add test for a problematic connection

* Adjust combat logic to deal with weird edge cases so it doesn't take stuff out of logic that was previously in logic

* Fix create_item classification

* Update some comments

* Update per exempt's suggestion

* Add combat logic to the well boss fight, reorder the combat logic stuff a little to better section them off

* Add EntranceLayout option

* Add back LogicRules as an invisible option, to not break old yamls

* Fix a bug with seed group, continue changing fixed shop to entrance layout

* Fix missed fixed shop -> entrance layout spot

* Fix bug in seed groups with fixed shop on and off

* Add entrance layout to the UT regen stuff

* Put direction. in, will add them later

* Remove unused elevation from portal class

* Got like half of them in

* Finish adding all of the directions

* Add combat rule for zig front to back

* Update per Medic's suggestion

* Update ladder storage without items option description

* Mess with state with collect and remove to save like 2 seconds (never again)

* Save even more time, still never going to do this again on anything else

* Add option check for collect and remove

* Add directions to shop portals

* Update direction in Portal with default

* Move Direction above Portal

* Add decoupled option, mess with plando connection stuff

* Merge, implement verify plando directions

* Condense the stuff in change and remove to less lines (thanks medic)

* Remove unused thing

* Swap to using logicmixin instead of prog_items (thanks Vi)

* Fix consistency in stat counters

* Add back something that was needed

* Fix mistake when adding back

* Making the fix better (thanks medic)

* Make it actually return false if it gets to the backup lists and fails them

* Fix stuff after merge

* Add outlet regions, create new regions as needed for them

* Put together part of decoupled and direction pairs

* make direction pairs work

* Make decoupled work

* Make fixed shop work again

* Fix a few minor bugs

* Fix a few minor bugs

* Fix plando

* god i love programming

* Reorder portal list

* Update portal sorter for variable shops

* Add missing parameter

* Some cleanup of prints and functions

* Fix typo

* it's aliiiiiive

* Make seed groups not sync decoupled

* Add test with full-shop plando

* Fix bug with vanilla portals

* Handle plando connections and direction pair errors

* Update plando checking for decoupled

* Fix typo

* Fix exception text to be shorter

* Add some more comments

* Add todo note

* Remove unused safety thing

* Remove extra plando connections definition in options

* Make seed groups in decoupled with overlapping but not fully overlapped plando connections interact nicely without messing with what the entrances look like in the spoiler log

* Fix weird edge case that is technically user error

* Add note to fixed shop

* Fix parsing shop names in UT

* Remove debug print

* Actually make UT work

* multiworld. to world.

* Fix typo from merge

* Make it so the shops show up in the entrance hints

* Fix bug in ladder storage rules

* Remove blank line

* # Conflicts:
#	worlds/tunic/__init__.py
#	worlds/tunic/er_data.py
#	worlds/tunic/er_rules.py
#	worlds/tunic/er_scripts.py
#	worlds/tunic/rules.py
#	worlds/tunic/test/test_access.py

* Fix issues after merge

* Update plando connections stuff in docs

* Fix library mistake

* has_stick -> has_melee

* has_stick -> has_melee

* Add a failsafe for direction pairing

* Fix playthrough crash bug

* Remove init from logicmixin

* Updates per code review (thanks hesto)

* has_stick to has_melee in newer update

* has_stick to has_melee in newer update

* # Conflicts:
#	worlds/tunic/__init__.py
#	worlds/tunic/combat_logic.py
#	worlds/tunic/er_data.py
#	worlds/tunic/er_rules.py
#	worlds/tunic/er_scripts.py

* Cleanup more stuff after merge

* Revert "Cleanup more stuff after merge"

This reverts commit a6ee9a93da8f2fcc4413de6df6927b246017889d.

* Revert "# Conflicts:"

This reverts commit c74ccd74a45b6ad6b9abe6e339d115a0c98baf30.

* Cleanup more stuff after merge

* Swap to .get for decoupled so it works with older games probably maybe

* Fix after merge

* Fix typo

* Fix UT support with fixed shop option

* Backport plando connections fix

* Fix issue with fixed shop + decoupled

* Make the error not duplicate the while loop condition

* Fix rule for quarry back to monastery

* Fix more stuff after merge

* Make it not output anything if you set plando connections but not ER

* Add obvious note to plando connections description

* Fix after merge

* add comment to commented out connection

---------

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
Scipio Wright
2025-05-06 12:33:21 -04:00
committed by GitHub
parent 7bbe62019a
commit a3aac3d737
9 changed files with 1150 additions and 516 deletions

View File

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