TUNIC: Fuse and Bell Shuffle (#5420)

* 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

* Make early bushes only contain grass

* Fix library mistake

* Backport changes to grass rando (#20)

* Backport changes to grass rando

* add_rule instead of set_rule for the special cases, add special cases for back of swamp laurels area cause I should've made a new region for the swamp upper entrance

* Remove item name group for grass

* Update grass rando option descriptions

- Also ignore grass fill for single player games

* Ignore grass fill option for solo rando

* Update er_rules.py

* Fix pre fill issue

* Remove duplicate option

* Add excluded grass locations back

* Hide grass fill option from simple ui options page

* Check for start with sword before setting grass rules

* Update worlds/tunic/options.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* 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

* Exclude grass from get_filler_item_name

- non-grass rando games were accidentally seeing grass items get shuffled in as filler, which is funny but probably shouldn't happen

* Update worlds/tunic/__init__.py

Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* Apply suggestions from code review

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
Co-authored-by: Scipio Wright <scipiowright@gmail.com>

* change the rest of grass_fill to local_fill

* Filter out grass from filler_items

* remove -> discard

* Update worlds/tunic/__init__.py

Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>

* Starting out

* Rules for breakable regions

* # 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

* change has_stick to has_melee

* Update grass list with combat logic regions

* More fixes from combat logic merge

* Fix some dumb stuff (#21)

* Reorganize pre fill for grass

* make the rest of it work, it's pr ready, boom

* Make it work in not pot shuffle

* Merge grass rando

* multiworld -> world get_location, use has_any

* Swap out region for West Garden Before Terry grass

* Adjust west garden rules to add west combat region

* Adjust grass regions for south checkpoint grass

* Adjust grass regions for after terry grass

* Adjust grass regions for west combat grass

* Adjust grass regions for dagger house grass

* Adjust grass regions for south checkpoint grass, adjust regions and rules for some related locations

* Finish the remainder of the west garden grass, reformat ruined atoll a little

* More hex quest updates

- Implement page ability shuffle for hex quest
- Fix keys behind bosses if hex goal is less than 3
- Added check to fix conflicting hex quest options
- Add option to slot data

* Change option comparison

* Change option checking and fix some stuff

- also keep prayer first on low hex counts

* Update option defaulting

* Update option checking

* Fix option assignment again

* Merge in hex hunt

* Merge in changes

* Clean up imports

* Add ability type to UT stuff

* merge it all

* Make local fill work across pot and grass (to be adjusted later)

* Make separate pools for the grass and non-grass fills

* Fix id overlap

* Update option description

* Fix default

* Reorder localfill option desc

* Load the purgatory ones in

* Adjustments after merge

* Fully remove logicrules

* Fix UT support with fixed shop option

* Add breakable shuffle to the ut stuff

* Make it load in a specific number of locations

* Add Silent's spoiler log ability thing

* Fix for groups

* Fix for groups

* Fix typo

* Fix hex quest UT support

* Use .get

* UT fixes, classification fixes

* Rename some locations

* Adjust guard house names

* Adjust guard house names

* Rework create_item

* Fix for plando connections

* Rename, add new breakables

* Rename more stuff

* Time to rename them again

* Fix issue with fixed shop + decoupled

* Put in an exception to catch that error in the future

* Update create_item to match main

* Update spoiler log lines for hex abilities

* Burn the signs down

* Bring over the combat logic fix

* Merge in combat logic fix

* Silly static method thing

* Move a few areas to before well instead of east forest

* Add an all_random hidden option for dev stuff

* Port over changes from main

* Fix west courtyard pot regions

* Remove debug prints

* Fix fortress courtyard and beneath the fortress loc groups again

* Add exception handling to deal with duplicate apworlds

* Fix typo

* More missing loc group conversions

* Initial fuse shuffle stuff

* Fix gun missing from combat_items, add new for combat logic cache, very slight refactor of check_combat_reqs to let it do the changeover in a less complicated fashion, fix area being a boss area rather than non-boss area for a check

* Add fuse shuffle logic

* reorder atoll statue rule

* Update traversal reqs

* Remove fuse shuffle from temple door

* Combine rules and option checking

* Add bell shuffle; fix fuse location groups

* Fix portal rules not requiring prayer

* Merge the grass laurels exit grass PR

* Merge in fortress bridge PR

* Do a little clean up

* Fix a regression

* Update after merge

* Some more stuff

* More Silent changes

* Update more info section in game info page

* Fix rules for atoll and swamp fuses

* Precollect cathedral fuse in ER

* actually just make the fuse useful instead of progression

* Add it to the swamp and cath rules too

* Fix cath fuse name

* Minor fixes and edits

* Some UT stuff

* Fix a couple more groups

* Move a bunch of UT stuff to its own file

* Fix up a couple UT things

* Couple minor ER fixes

* Formatting change

* UT poptracker stuff enabled since it's optional in one of the releases

* Add author string to world class

* Adjust local fill option name

* Update ut_stuff to match the PR

* Add exception handling for UT with old apworld

* Fix missing tracker_world

* Remove extra entrance from cath main -> elevator

Entry <-> Elev exists,
Entry <-> Main exists
So no connection is needed between Main and Elev

* Fix so that decoupled doesn't incorrectly use get_portal_info and get_paired_portal

* Fix so that decoupled doesn't incorrectly use get_portal_info and get_paired_portal

* Update for breakables poptracker

* Backup and warnings instead

* Update typing

* Delete old regions and rules, move stuff to logic_helpers and constants

* Delete now much less useful tests

* Fix breakables map tracking

* Add more comments to init

* Add todo to grass.py

* Fix up tests

* Fully remove fixed_shop

* Finish hard deprecating FixedShop

* Fix zig skip showing up in decoupled fixed shop

* Make local_fill show up on the website

* Merge with main

* Fixes after merge

* More fixes after merge

* oh right that's why it was there, circular imports

* Swap {} to ()

* Add fuse and bell shuffle to seed groups since they're logically significant for entrance pairing

---------

Co-authored-by: silent-destroyer <osilentdestroyer@gmail.com>
Co-authored-by: Silent <110704408+silent-destroyer@users.noreply.github.com>
Co-authored-by: Exempt-Medic <60412657+Exempt-Medic@users.noreply.github.com>
This commit is contained in:
Scipio Wright
2025-09-30 15:39:41 -04:00
committed by GitHub
parent e6fb7d9c6a
commit 6a63de2f0f
12 changed files with 325 additions and 104 deletions

View File

@@ -3,11 +3,11 @@ from typing import FrozenSet, TYPE_CHECKING
from BaseClasses import Region
from worlds.generic.Rules import set_rule, add_rule, forbid_item
# from .bells import set_bell_location_rules
from .bells import set_bell_location_rules
from .combat_logic import has_combat_reqs
from .constants import *
from .er_data import Portal, get_portal_outlet_region
# from .fuses import set_fuse_location_rules, has_fuses
from .fuses import set_fuse_location_rules
from .grass import set_grass_location_rules
from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls
from .logic_helpers import (has_ability, has_ladder, has_melee, has_sword, has_lantern, has_mask, has_fuses,
@@ -17,9 +17,6 @@ from .options import IceGrappling, LadderStorage, CombatLogic
if TYPE_CHECKING:
from . import TunicWorld
fuses_option = False # replace with options.shuffle_fuses when fuse shuffle is in
bells_option = False # replace with options.shuffle_bells when bell shuffle is in
def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_pairs: dict[Portal, Portal]) -> None:
player = world.player
@@ -334,8 +331,8 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
# nmg: ice grapple through temple door
regions["Overworld"].connect(
connecting_region=regions["Overworld Temple Door"],
rule=lambda state: (state.has_all(("Ring Eastern Bell", "Ring Western Bell"), player) and not bells_option)
or (state.has_all(("East Bell", "West Bell"), player) and bells_option)
rule=lambda state: (state.has_all(("Ring Eastern Bell", "Ring Western Bell"), player) and not options.shuffle_bells)
or (state.has_all(("East Bell", "West Bell"), player) and options.shuffle_bells)
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
regions["Overworld Temple Door"].connect(
@@ -671,9 +668,9 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
and (has_sword(state, player) or state.has_any((gun, fire_wand), player)))
# shoot fuse and have the shot hit you mid-LS
or (can_ladder_storage(state, world) and state.has(fire_wand, player)
and options.ladder_storage >= LadderStorage.option_hard))) and not fuses_option)
and options.ladder_storage >= LadderStorage.option_hard))) and not options.shuffle_fuses)
or (state.has_all((atoll_northwest_fuse, atoll_northeast_fuse, atoll_southwest_fuse, atoll_southeast_fuse), player)
and fuses_option))
and options.shuffle_fuses))
)
regions["Ruined Atoll Statue"].connect(
@@ -804,12 +801,12 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
connecting_region=regions["Fortress Exterior from Overworld"],
rule=lambda state: state.has(laurels, player)
or (has_ability(prayer, state, world) and state.has(fortress_exterior_fuse_1, player)
and fuses_option))
and options.shuffle_fuses))
regions["Fortress Exterior from Overworld"].connect(
connecting_region=regions["Fortress Exterior near cave"],
rule=lambda state: state.has(laurels, player)
or (has_ability(prayer, state, world) and state.has(fortress_exterior_fuse_1, player)
if fuses_option else has_ability(prayer, state, world)))
if options.shuffle_fuses else has_ability(prayer, state, world)))
# shoot far fire pot, enemy gets aggro'd
regions["Fortress Exterior near cave"].connect(
@@ -880,7 +877,7 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)
or (has_fuses("Activate Eastern Vault West Fuses", state, world)
and has_fuses("Activate Eastern Vault East Fuse", state, world)
and fuses_option))
and options.shuffle_fuses))
fort_grave_entry_to_combat = regions["Fortress Grave Path Entry"].connect(
connecting_region=regions["Fortress Grave Path Combat"])
@@ -1027,18 +1024,18 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
connecting_region=regions["Rooted Ziggurat Lower Miniboss Platform"])
zig_low_miniboss_to_mid = regions["Rooted Ziggurat Lower Miniboss Platform"].connect(
connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"],
rule=lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option
rule=lambda state: state.has(ziggurat_miniboss_fuse, player) if options.shuffle_fuses
else (has_sword(state, player) and has_ability(prayer, state, world)))
# can ice grapple to the voidlings to get to the double admin fight, still need to pray at the fuse
zig_low_miniboss_to_back = regions["Rooted Ziggurat Lower Miniboss Platform"].connect(
connecting_region=regions["Rooted Ziggurat Lower Back"],
rule=lambda state: state.has(laurels, player) or (state.has(ziggurat_miniboss_fuse, player) and fuses_option)
or (has_sword(state, player) and has_ability(prayer, state, world) and not fuses_option))
rule=lambda state: state.has(laurels, player) or (state.has(ziggurat_miniboss_fuse, player) and options.shuffle_fuses)
or (has_sword(state, player) and has_ability(prayer, state, world) and not options.shuffle_fuses))
regions["Rooted Ziggurat Lower Back"].connect(
connecting_region=regions["Rooted Ziggurat Lower Miniboss Platform"],
rule=lambda state: state.has(laurels, player)
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)
or (state.has(ziggurat_miniboss_fuse, player) and fuses_option))
or (state.has(ziggurat_miniboss_fuse, player) and options.shuffle_fuses))
regions["Rooted Ziggurat Lower Back"].connect(
connecting_region=regions["Rooted Ziggurat Portal Room Entrance"],
@@ -1086,8 +1083,8 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
and state.can_reach_region("Overworld Beach", player)))))
and (not options.combat_logic
or has_combat_reqs("Swamp", state, player))
and not fuses_option)
or (state.has_all((swamp_fuse_1, swamp_fuse_2, swamp_fuse_3), player) and fuses_option)
and not options.shuffle_fuses)
or (state.has_all((swamp_fuse_1, swamp_fuse_2, swamp_fuse_3), player) and options.shuffle_fuses)
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders:
@@ -1096,7 +1093,7 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
regions["Swamp to Cathedral Main Entrance Region"].connect(
connecting_region=regions["Swamp Mid"],
rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)
or (state.has_all((swamp_fuse_1, swamp_fuse_2, swamp_fuse_3), player) and fuses_option))
or (state.has_all((swamp_fuse_1, swamp_fuse_2, swamp_fuse_3), player) and options.shuffle_fuses))
# grapple push the enemy by the door down, then grapple to it. Really jank
regions["Swamp Mid"].connect(
@@ -1142,7 +1139,7 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
cath_entry_to_elev = regions["Cathedral Entry"].connect(
connecting_region=regions["Cathedral to Gauntlet"],
rule=lambda state: ((state.has(cathedral_elevator_fuse, player) if fuses_option else has_ability(prayer, state, world))
rule=lambda state: ((state.has(cathedral_elevator_fuse, player) if options.shuffle_fuses else has_ability(prayer, state, world))
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
or options.entrance_rando) # elevator is always there in ER
regions["Cathedral to Gauntlet"].connect(
@@ -1444,17 +1441,17 @@ def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_
lambda state: has_combat_reqs("Rooted Ziggurat", state, player))
set_rule(zig_low_miniboss_to_back,
lambda state: state.has(laurels, player)
or (state.has(ziggurat_miniboss_fuse, player) if fuses_option
or (state.has(ziggurat_miniboss_fuse, player) if options.shuffle_fuses
else (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player))))
set_rule(zig_low_miniboss_to_mid,
lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option
lambda state: state.has(ziggurat_miniboss_fuse, player) if options.shuffle_fuses
else (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player)))
# only activating the fuse requires combat logic
set_rule(cath_entry_to_elev,
lambda state: options.entrance_rando
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
or (state.has(cathedral_elevator_fuse, player) if fuses_option
or (state.has(cathedral_elevator_fuse, player) if options.shuffle_fuses
else (has_ability(prayer, state, world) and has_combat_reqs("Swamp", state, player))))
set_rule(cath_entry_to_main,
@@ -1535,11 +1532,11 @@ def set_er_location_rules(world: "TunicWorld") -> None:
if options.grass_randomizer:
set_grass_location_rules(world)
# if options.shuffle_fuses:
# set_fuse_location_rules(world)
#
# if options.shuffle_bells:
# set_bell_location_rules(world)
if options.shuffle_fuses:
set_fuse_location_rules(world)
if options.shuffle_bells:
set_bell_location_rules(world)
forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player)
@@ -1702,7 +1699,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
and (state.has(laurels, player)
or options.entrance_rando)))
set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"),
lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option
lambda state: state.has(ziggurat_miniboss_fuse, player) if options.shuffle_fuses
else has_sword(state, player) and has_ability(prayer, state, world))
# Bosses
@@ -1745,12 +1742,12 @@ def set_er_location_rules(world: "TunicWorld") -> None:
lambda state: state.has(laurels, player))
# Events
if not bells_option:
if not options.shuffle_bells:
set_rule(world.get_location("Eastern Bell"),
lambda state: (has_melee(state, player) or state.has(fire_wand, player)))
set_rule(world.get_location("Western Bell"),
lambda state: (has_melee(state, player) or state.has(fire_wand, player)))
if not fuses_option:
if not options.shuffle_fuses:
set_rule(world.get_location("Furnace Fuse"),
lambda state: has_ability(prayer, state, world))
set_rule(world.get_location("South and West Fortress Exterior Fuses"),
@@ -1877,7 +1874,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# could just do the last two, but this outputs better in the spoiler log
# dagger is maybe viable here, but it's sketchy -- activate ladder switch, save to reset enemies, climb up
if not fuses_option:
if not options.shuffle_fuses:
combat_logic_to_loc("Upper and Central Fortress Exterior Fuses", "Eastern Vault Fortress")
combat_logic_to_loc("Beneath the Vault Fuse", "Beneath the Vault")
combat_logic_to_loc("Eastern Vault West Fuses", "Eastern Vault Fortress")
@@ -1896,10 +1893,10 @@ def set_er_location_rules(world: "TunicWorld") -> None:
and (state.has(laurels, player) or world.options.entrance_rando))
or has_combat_reqs("Rooted Ziggurat", state, player))
set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"),
lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option
lambda state: state.has(ziggurat_miniboss_fuse, player) if options.shuffle_fuses
else (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player)))
if fuses_option:
if options.shuffle_fuses:
set_rule(world.get_location("Rooted Ziggurat Lower - [Miniboss] Activate Fuse"),
lambda state: has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player))
combat_logic_to_loc("Beneath the Fortress - Activate Fuse", "Beneath the Vault")