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

@@ -7,13 +7,13 @@ from Options import PlandoConnection, OptionError, PerGameCommonOptions, Range,
from settings import Group, Bool, FilePath
from worlds.AutoWorld import WebWorld, World
# from .bells import bell_location_groups, bell_location_name_to_id
from .bells import bell_location_groups, bell_location_name_to_id
from .breakables import breakable_location_name_to_id, breakable_location_groups, breakable_location_table
from .combat_logic import area_data, CombatState
from .er_data import portal_mapping, RegionInfo, tunic_er_regions
from .er_rules import set_er_location_rules
from .er_scripts import create_er_regions, verify_plando_directions
# from .fuses import fuse_location_name_to_id, fuse_location_groups
from .fuses import fuse_location_name_to_id, fuse_location_groups
from .grass import grass_location_table, grass_location_name_to_id, grass_location_name_groups, excluded_grass_locations
from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names,
combat_items)
@@ -75,6 +75,8 @@ class SeedGroup(TypedDict):
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
bell_shuffle: bool # off controls
fuse_shuffle: bool # off controls
class TunicWorld(World):
@@ -98,17 +100,17 @@ class TunicWorld(World):
location_name_groups.setdefault(group_name, set()).update(members)
for group_name, members in breakable_location_groups.items():
location_name_groups.setdefault(group_name, set()).update(members)
# for group_name, members in fuse_location_groups.items():
# location_name_groups.setdefault(group_name, set()).update(members)
# for group_name, members in bell_location_groups.items():
# location_name_groups.setdefault(group_name, set()).update(members)
for group_name, members in fuse_location_groups.items():
location_name_groups.setdefault(group_name, set()).update(members)
for group_name, members in bell_location_groups.items():
location_name_groups.setdefault(group_name, set()).update(members)
item_name_to_id = item_name_to_id
location_name_to_id = standard_location_name_to_id.copy()
location_name_to_id.update(grass_location_name_to_id)
location_name_to_id.update(breakable_location_name_to_id)
# location_name_to_id.update(fuse_location_name_to_id)
# location_name_to_id.update(bell_location_name_to_id)
location_name_to_id.update(fuse_location_name_to_id)
location_name_to_id.update(bell_location_name_to_id)
player_location_table: dict[str, int]
ability_unlocks: dict[str, int]
@@ -227,11 +229,11 @@ class TunicWorld(World):
self.player_location_table.update({name: num for name, num in breakable_location_name_to_id.items()
if not name.startswith("Purgatory")})
# if self.options.shuffle_fuses:
# self.player_location_table.update(fuse_location_name_to_id)
#
# if self.options.shuffle_bells:
# self.player_location_table.update(bell_location_name_to_id)
if self.options.shuffle_fuses:
self.player_location_table.update(fuse_location_name_to_id)
if self.options.shuffle_bells:
self.player_location_table.update(bell_location_name_to_id)
@classmethod
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
@@ -259,7 +261,9 @@ class TunicWorld(World):
laurels_at_10_fairies=tunic.options.laurels_location == LaurelsLocation.option_10_fairies,
entrance_layout=tunic.options.entrance_layout.value,
has_decoupled_enabled=bool(tunic.options.decoupled),
plando=tunic.options.plando_connections.value.copy())
plando=tunic.options.plando_connections.value.copy(),
bell_shuffle=bool(tunic.options.shuffle_bells),
fuse_shuffle=bool(tunic.options.shuffle_fuses))
continue
# I feel that syncing this one is worse than erroring out
if bool(tunic.options.decoupled) != cls.seed_groups[group]["has_decoupled_enabled"]:
@@ -277,6 +281,12 @@ 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
# off is more restrictive
if not tunic.options.shuffle_bells:
cls.seed_groups[group]["bell_shuffle"] = False
# off is more restrictive
if not tunic.options.shuffle_fuses:
cls.seed_groups[group]["fuse_shuffle"] = False
# 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:
@@ -428,6 +438,19 @@ class TunicWorld(World):
ladder_count += 1
remove_filler(ladder_count)
if self.options.shuffle_fuses:
for item_name, item_data in item_table.items():
if item_data.item_group == "Fuses":
if item_name == "Cathedral Elevator Fuse" and self.options.entrance_rando:
tunic_items.append(self.create_item(item_name, ItemClassification.useful))
continue
items_to_create[item_name] = 1
if self.options.shuffle_bells:
for item_name, item_data in item_table.items():
if item_data.item_group == "Bells":
items_to_create[item_name] = 1
if self.options.hexagon_quest:
# Replace pages and normal hexagons with filler
for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)):
@@ -480,7 +503,6 @@ class TunicWorld(World):
# pull out the filler so that we can place it manually during pre_fill
self.fill_items = []
if self.options.local_fill > 0 and self.multiworld.players > 1:
# skip items marked local or non-local, let fill deal with them in its own way
all_filler: list[TunicItem] = []
non_filler: list[TunicItem] = []
for tunic_item in tunic_items:
@@ -709,8 +731,8 @@ class TunicWorld(World):
"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,
# "shuffle_fuses": self.options.shuffle_fuses.value,
# "shuffle_bells": self.options.shuffle_bells.value,
"shuffle_fuses": self.options.shuffle_fuses.value,
"shuffle_bells": self.options.shuffle_bells.value,
"grass_randomizer": self.options.grass_randomizer.value,
"combat_logic": self.options.combat_logic.value,
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],

39
worlds/tunic/bells.py Normal file
View File

@@ -0,0 +1,39 @@
from typing import NamedTuple, TYPE_CHECKING
from worlds.generic.Rules import set_rule
from .constants import base_id
from .logic_helpers import has_melee
if TYPE_CHECKING:
from . import TunicWorld
class TunicLocationData(NamedTuple):
region: str
er_region: str
bell_location_table: dict[str, TunicLocationData] = {
"Forest Belltower - Ring the East Bell": TunicLocationData("Forest Belltower", "Forest Belltower Upper"),
"Overworld - [West] Ring the West Bell": TunicLocationData("Overworld", "Overworld Belltower at Bell"),
}
bell_location_base_id = base_id + 11000
bell_location_name_to_id: dict[str, int] = {name: bell_location_base_id + index
for index, name in enumerate(bell_location_table)}
bell_location_groups: dict[str, set[str]] = {}
for location_name, location_data in bell_location_table.items():
bell_location_groups.setdefault(location_data.region, set()).add(location_name)
bell_location_groups.setdefault("Bells", set()).add(location_name)
def set_bell_location_rules(world: "TunicWorld") -> None:
player = world.player
set_rule(world.get_location("Forest Belltower - Ring the East Bell"),
lambda state: has_melee(state, player) or state.has("Magic Wand", player))
set_rule(world.get_location("Overworld - [West] Ring the West Bell"),
lambda state: has_melee(state, player) or state.has("Magic Wand", player))

View File

@@ -735,7 +735,7 @@ tunic_er_regions: dict[str, RegionInfo] = {
"Rooted Ziggurat Lower Entry": RegionInfo("ziggurat2020_3"), # the vanilla entry point side
"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 Miniboss Platform": RegionInfo("ziggurat2020_3"), # the double admin platform
"Rooted Ziggurat Lower Miniboss Platform": RegionInfo("ziggurat2020_3"), # the double admin platform
"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", 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

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")

View File

@@ -113,13 +113,13 @@ def place_event_items(world: "TunicWorld", regions: dict[str, Region]) -> None:
location.place_locked_item(
TunicERItem("Unseal the Heir", ItemClassification.progression, None, world.player))
elif event_name.endswith("Bell"):
# if world.options.shuffle_bells:
# continue
if world.options.shuffle_bells:
continue
location.place_locked_item(
TunicERItem("Ring " + event_name, ItemClassification.progression, None, world.player))
elif event_name.endswith("Fuse") or event_name.endswith("Fuses"):
# if world.options.shuffle_fuses:
# continue
if world.options.shuffle_fuses:
continue
location.place_locked_item(
TunicERItem("Activate " + event_name, ItemClassification.progression, None, world.player))
region.locations.append(location)
@@ -200,10 +200,8 @@ def pair_portals(world: "TunicWorld", regions: dict[str, Region]) -> dict[Portal
entrance_layout = world.options.entrance_layout
laurels_location = world.options.laurels_location
decoupled = world.options.decoupled
# shuffle_fuses = bool(world.options.shuffle_fuses.value)
# shuffle_bells = bool(world.options.shuffle_bells.value)
shuffle_fuses = False
shuffle_bells = False
shuffle_fuses = bool(world.options.shuffle_fuses.value)
shuffle_bells = bool(world.options.shuffle_bells.value)
traversal_reqs = deepcopy(traversal_requirements)
has_laurels = True
waterfall_plando = False
@@ -216,6 +214,8 @@ def pair_portals(world: "TunicWorld", regions: dict[str, Region]) -> dict[Portal
ladder_storage = seed_group["ladder_storage"]
entrance_layout = seed_group["entrance_layout"]
laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
shuffle_bells = seed_group["bell_shuffle"]
shuffle_fuses = seed_group["fuse_shuffle"]
logic_tricks: tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage)

View File

@@ -1,30 +1,123 @@
from .constants import *
from typing import NamedTuple, TYPE_CHECKING
# for fuse locations and reusing event names to simplify er_rules
fuse_activation_reqs: dict[str, list[str]] = {
swamp_fuse_2: [swamp_fuse_1],
swamp_fuse_3: [swamp_fuse_1, swamp_fuse_2],
fortress_exterior_fuse_2: [fortress_exterior_fuse_1],
beneath_the_vault_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2],
fortress_candles_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse],
fortress_door_left_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse,
fortress_candles_fuse],
fortress_courtyard_upper_fuse: [fortress_exterior_fuse_1],
fortress_courtyard_lower_fuse: [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse],
fortress_door_right_fuse: [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, fortress_courtyard_lower_fuse],
quarry_fuse_2: [quarry_fuse_1],
"Activate Furnace Fuse": [west_furnace_fuse],
"Activate South and West Fortress Exterior Fuses": [fortress_exterior_fuse_1, fortress_exterior_fuse_2],
"Activate Upper and Central Fortress Exterior Fuses": [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse,
fortress_courtyard_lower_fuse],
"Activate Beneath the Vault Fuse": [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse],
"Activate Eastern Vault West Fuses": [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse,
fortress_candles_fuse, fortress_door_left_fuse],
"Activate Eastern Vault East Fuse": [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse,
fortress_courtyard_lower_fuse, fortress_door_right_fuse],
"Activate Quarry Connector Fuse": [quarry_fuse_1],
"Activate Quarry Fuse": [quarry_fuse_1, quarry_fuse_2],
"Activate Ziggurat Fuse": [ziggurat_teleporter_fuse],
"Activate West Garden Fuse": [west_garden_fuse],
"Activate Library Fuse": [library_lab_fuse],
from BaseClasses import CollectionState
from worlds.generic.Rules import set_rule
from .constants import *
from .logic_helpers import has_ability, has_sword, fuse_activation_reqs
if TYPE_CHECKING:
from . import TunicWorld
class TunicLocationData(NamedTuple):
loc_group: str
er_region: str
fuse_location_table: dict[str, TunicLocationData] = {
"Overworld - [Southeast] Activate Fuse": TunicLocationData("Overworld", "Overworld"),
"Swamp - [Central] Activate Fuse": TunicLocationData("Swamp", "Swamp Mid"),
"Swamp - [Outside Cathedral] Activate Fuse": TunicLocationData("Swamp", "Swamp Mid"),
"Cathedral - Activate Fuse": TunicLocationData("Cathedral", "Cathedral Main"),
"West Furnace - Activate Fuse": TunicLocationData("West Furnace", "Furnace Fuse"),
"West Garden - [South Highlands] Activate Fuse": TunicLocationData("West Garden", "West Garden South Checkpoint"),
"Ruined Atoll - [Northwest] Activate Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [Northeast] Activate Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [Southeast] Activate Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll Ladder Tops"),
"Ruined Atoll - [Southwest] Activate Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Library Lab - Activate Fuse": TunicLocationData("Library Lab", "Library Lab"),
"Fortress Courtyard - [From Overworld] Activate Fuse": TunicLocationData("Fortress Courtyard", "Fortress Exterior from Overworld"),
"Fortress Courtyard - [Near Cave] Activate Fuse": TunicLocationData("Fortress Courtyard", "Fortress Exterior from Overworld"),
"Fortress Courtyard - [Upper] Activate Fuse": TunicLocationData("Fortress Courtyard", "Fortress Courtyard Upper"),
"Fortress Courtyard - [Central] Activate Fuse": TunicLocationData("Fortress Courtyard", "Fortress Courtyard"),
"Beneath the Fortress - Activate Fuse": TunicLocationData("Beneath the Fortress", "Beneath the Vault Back"),
"Eastern Vault Fortress - [Candle Room] Activate Fuse": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Eastern Vault Fortress - [Left of Door] Activate Fuse": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Eastern Vault Fortress - [Right of Door] Activate Fuse": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Quarry Entryway - Activate Fuse": TunicLocationData("Quarry Connector", "Quarry Connector"),
"Quarry - Activate Fuse": TunicLocationData("Quarry", "Quarry Entry"),
"Rooted Ziggurat Lower - [Miniboss] Activate Fuse": TunicLocationData("Rooted Ziggurat Lower", "Rooted Ziggurat Lower Miniboss Platform"),
"Rooted Ziggurat Lower - [Before Boss] Activate Fuse": TunicLocationData("Rooted Ziggurat Lower", "Rooted Ziggurat Lower Back"),
}
fuse_location_base_id = base_id + 10000
fuse_location_name_to_id: dict[str, int] = {name: fuse_location_base_id + index
for index, name in enumerate(fuse_location_table)}
fuse_location_groups: dict[str, set[str]] = {}
for location_name, location_data in fuse_location_table.items():
fuse_location_groups.setdefault(location_data.loc_group, set()).add(location_name)
fuse_location_groups.setdefault("Fuses", set()).add(location_name)
# to be deduplicated in the big refactor
def has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> bool:
return not world.options.shuffle_ladders or state.has(ladder, world.player)
def set_fuse_location_rules(world: "TunicWorld") -> None:
player = world.player
set_rule(world.get_location("Overworld - [Southeast] Activate Fuse"),
lambda state: state.has(laurels, player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Swamp - [Central] Activate Fuse"),
lambda state: state.has_all(fuse_activation_reqs[swamp_fuse_2], player)
and has_ability(prayer, state, world)
and has_sword(state, player))
set_rule(world.get_location("Swamp - [Outside Cathedral] Activate Fuse"),
lambda state: state.has_all(fuse_activation_reqs[swamp_fuse_3], player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Cathedral - Activate Fuse"),
lambda state: has_ability(prayer, state, world))
set_rule(world.get_location("West Furnace - Activate Fuse"),
lambda state: has_ability(prayer, state, world))
set_rule(world.get_location("West Garden - [South Highlands] Activate Fuse"),
lambda state: has_ability(prayer, state, world))
set_rule(world.get_location("Ruined Atoll - [Northwest] Activate Fuse"),
lambda state: state.has_any([grapple, laurels], player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Ruined Atoll - [Northeast] Activate Fuse"),
lambda state: has_ability(prayer, state, world))
set_rule(world.get_location("Ruined Atoll - [Southeast] Activate Fuse"),
lambda state: has_ability(prayer, state, world))
set_rule(world.get_location("Ruined Atoll - [Southwest] Activate Fuse"),
lambda state: has_ability(prayer, state, world))
set_rule(world.get_location("Library Lab - Activate Fuse"),
lambda state: has_ability(prayer, state, world)
and has_ladder("Ladders in Library", state, world))
set_rule(world.get_location("Fortress Courtyard - [From Overworld] Activate Fuse"),
lambda state: has_ability(prayer, state, world))
set_rule(world.get_location("Fortress Courtyard - [Near Cave] Activate Fuse"),
lambda state: state.has(fortress_exterior_fuse_1, player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Fortress Courtyard - [Upper] Activate Fuse"),
lambda state: state.has(fortress_exterior_fuse_1, player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Fortress Courtyard - [Central] Activate Fuse"),
lambda state: state.has_all(fuse_activation_reqs[fortress_courtyard_lower_fuse], player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Beneath the Fortress - Activate Fuse"),
lambda state: state.has_all(fuse_activation_reqs[beneath_the_vault_fuse], player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Eastern Vault Fortress - [Candle Room] Activate Fuse"),
lambda state: state.has_all(fuse_activation_reqs[fortress_candles_fuse], player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Eastern Vault Fortress - [Left of Door] Activate Fuse"),
lambda state: state.has_all(fuse_activation_reqs[fortress_door_left_fuse], player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Eastern Vault Fortress - [Right of Door] Activate Fuse"),
lambda state: state.has_all(fuse_activation_reqs[fortress_door_right_fuse], player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Quarry Entryway - Activate Fuse"),
lambda state: state.has(grapple, player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Quarry - Activate Fuse"),
lambda state: state.has_all(fuse_activation_reqs[quarry_fuse_2], player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Rooted Ziggurat Lower - [Miniboss] Activate Fuse"),
lambda state: has_sword(state, player)
and has_ability(prayer, state, world))
set_rule(world.get_location("Rooted Ziggurat Lower - [Before Boss] Activate Fuse"),
lambda state: has_ability(prayer, state, world))

View File

@@ -173,6 +173,31 @@ item_table: dict[str, TunicItemData] = {
"Ladders in Lower Quarry": TunicItemData(IC.progression, 0, 149, "Ladders"),
"Ladders in Swamp": TunicItemData(IC.progression, 0, 150, "Ladders"),
"Grass": TunicItemData(IC.filler, 0, 151),
"Swamp Fuse 1": TunicItemData(IC.progression, 0, 157, "Fuses"),
"Swamp Fuse 2": TunicItemData(IC.progression, 0, 158, "Fuses"),
"Swamp Fuse 3": TunicItemData(IC.progression, 0, 159, "Fuses"),
"Cathedral Elevator Fuse": TunicItemData(IC.progression, 0, 160, "Fuses"),
"Quarry Fuse 1": TunicItemData(IC.progression, 0, 161, "Fuses"),
"Quarry Fuse 2": TunicItemData(IC.progression, 0, 162, "Fuses"),
"Ziggurat Miniboss Fuse": TunicItemData(IC.progression, 0, 163, "Fuses"),
"Ziggurat Teleporter Fuse": TunicItemData(IC.progression, 0, 164, "Fuses"),
"Fortress Exterior Fuse 1": TunicItemData(IC.progression, 0, 165, "Fuses"),
"Fortress Exterior Fuse 2": TunicItemData(IC.progression, 0, 166, "Fuses"),
"Fortress Courtyard Upper Fuse": TunicItemData(IC.progression, 0, 167, "Fuses"),
"Fortress Courtyard Fuse": TunicItemData(IC.progression, 0, 168, "Fuses"),
"Beneath the Vault Fuse": TunicItemData(IC.progression, 0, 169, "Fuses"),
"Fortress Candles Fuse": TunicItemData(IC.progression, 0, 170, "Fuses"),
"Fortress Door Left Fuse": TunicItemData(IC.progression, 0, 171, "Fuses"),
"Fortress Door Right Fuse": TunicItemData(IC.progression, 0, 172, "Fuses"),
"West Furnace Fuse": TunicItemData(IC.progression, 0, 173, "Fuses"),
"West Garden Fuse": TunicItemData(IC.progression, 0, 174, "Fuses"),
"Atoll Northeast Fuse": TunicItemData(IC.progression, 0, 175, "Fuses"),
"Atoll Northwest Fuse": TunicItemData(IC.progression, 0, 176, "Fuses"),
"Atoll Southeast Fuse": TunicItemData(IC.progression, 0, 177, "Fuses"),
"Atoll Southwest Fuse": TunicItemData(IC.progression, 0, 178, "Fuses"),
"Library Lab Fuse": TunicItemData(IC.progression, 0, 179, "Fuses"),
"East Bell": TunicItemData(IC.progression, 0, 180, "Bells"),
"West Bell": TunicItemData(IC.progression, 0, 181, "Bells")
}
# items to be replaced by fool traps

View File

@@ -1,9 +1,9 @@
from typing import NamedTuple
# from .bells import bell_location_table
from .bells import bell_location_table
from .breakables import breakable_location_table
from .constants import base_id
# from .fuses import fuse_location_table
from .fuses import fuse_location_table
from .grass import grass_location_table
@@ -329,8 +329,8 @@ standard_location_name_to_id: dict[str, int] = {name: base_id + index for index,
all_locations = location_table.copy()
all_locations.update(grass_location_table)
all_locations.update(breakable_location_table)
# all_locations.update(fuse_location_table)
# all_locations.update(bell_location_table)
all_locations.update(fuse_location_table)
all_locations.update(bell_location_table)
location_name_groups: dict[str, set[str]] = {}
for loc_name, loc_data in location_table.items():

View File

@@ -3,7 +3,6 @@ from typing import TYPE_CHECKING
from BaseClasses import CollectionState
from .constants import *
from .fuses import fuse_activation_reqs
from .options import HexagonQuestAbilityUnlockType, IceGrappling
if TYPE_CHECKING:
@@ -89,10 +88,38 @@ def can_get_past_bushes(state: CollectionState, world: "TunicWorld") -> bool:
return has_sword(state, world.player) or state.has_any((fire_wand, laurels, gun), world.player)
def has_fuses(fuse_event: str, state: CollectionState, world: "TunicWorld") -> bool:
player = world.player
fuses_option = False # replace fuses_option with world.options.shuffle_fuses when fuse shuffle is in
if fuses_option:
return state.has_all(fuse_activation_reqs[fuse_event], player)
# for fuse locations and reusing event names to simplify er_rules
fuse_activation_reqs: dict[str, list[str]] = {
swamp_fuse_2: [swamp_fuse_1],
swamp_fuse_3: [swamp_fuse_1, swamp_fuse_2],
fortress_exterior_fuse_2: [fortress_exterior_fuse_1],
beneath_the_vault_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2],
fortress_candles_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse],
fortress_door_left_fuse: [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse,
fortress_candles_fuse],
fortress_courtyard_upper_fuse: [fortress_exterior_fuse_1],
fortress_courtyard_lower_fuse: [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse],
fortress_door_right_fuse: [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse, fortress_courtyard_lower_fuse],
quarry_fuse_2: [quarry_fuse_1],
"Activate Furnace Fuse": [west_furnace_fuse],
"Activate South and West Fortress Exterior Fuses": [fortress_exterior_fuse_1, fortress_exterior_fuse_2],
"Activate Upper and Central Fortress Exterior Fuses": [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse,
fortress_courtyard_lower_fuse],
"Activate Beneath the Vault Fuse": [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse],
"Activate Eastern Vault West Fuses": [fortress_exterior_fuse_1, fortress_exterior_fuse_2, beneath_the_vault_fuse,
fortress_candles_fuse, fortress_door_left_fuse],
"Activate Eastern Vault East Fuse": [fortress_exterior_fuse_1, fortress_courtyard_upper_fuse,
fortress_courtyard_lower_fuse, fortress_door_right_fuse],
"Activate Quarry Connector Fuse": [quarry_fuse_1],
"Activate Quarry Fuse": [quarry_fuse_1, quarry_fuse_2],
"Activate Ziggurat Fuse": [ziggurat_teleporter_fuse],
"Activate West Garden Fuse": [west_garden_fuse],
"Activate Library Fuse": [library_lab_fuse],
}
return state.has(fuse_event, player)
def has_fuses(fuse_event: str, state: CollectionState, world: "TunicWorld") -> bool:
if world.options.shuffle_fuses:
return state.has_all(fuse_activation_reqs[fuse_event], world.player)
return state.has(fuse_event, world.player)

View File

@@ -198,6 +198,24 @@ class ShuffleLadders(Toggle):
display_name = "Shuffle Ladders"
class ShuffleFuses(Toggle):
"""
Praying at a fuse will reward a check instead of turning on the power. The power from each fuse gets turned into an
item that must be found in order to restore power for that part of the path.
"""
internal_name = "shuffle_fuses"
display_name = "Shuffle Fuses"
class ShuffleBells(Toggle):
"""
The East and West bells are shuffled into the item pool and must be found in order to unlock the Sealed Temple.
Ringing the bells will instead now reward a check.
"""
internal_name = "shuffle_bells"
display_name = "Shuffle Bells"
class GrassRandomizer(Toggle):
"""
Turns over 6,000 blades of grass and bushes in the game into checks.
@@ -357,8 +375,8 @@ class TunicOptions(PerGameCommonOptions):
hexagon_quest_ability_type: HexagonQuestAbilityUnlockType
shuffle_ladders: ShuffleLadders
# shuffle_fuses: ShuffleFuses
# shuffle_bells: ShuffleBells
shuffle_fuses: ShuffleFuses
shuffle_bells: ShuffleBells
grass_randomizer: GrassRandomizer
breakable_shuffle: BreakableShuffle
local_fill: LocalFill

View File

@@ -2,7 +2,7 @@ from .. import options
from .bases import TunicTestBase
class TestAccess(TunicTestBase):
class TestWells(TunicTestBase):
options = {options.CombatLogic.internal_name: options.CombatLogic.option_off}
# test that the wells function properly. Since fairies is written the same way, that should succeed too

View File

@@ -25,8 +25,8 @@ def setup_options_from_slot_data(world: "TunicWorld") -> None:
world.options.hexagon_quest_ability_type.value = world.passthrough.get("hexagon_quest_ability_type", 0)
world.options.entrance_rando.value = world.passthrough["entrance_rando"]
world.options.shuffle_ladders.value = world.passthrough["shuffle_ladders"]
# world.options.shuffle_fuses.value = world.passthrough.get("shuffle_fuses", 0)
# world.options.shuffle_bells.value = world.passthrough.get("shuffle_bells", 0)
world.options.shuffle_fuses.value = world.passthrough.get("shuffle_fuses", 0)
world.options.shuffle_bells.value = world.passthrough.get("shuffle_bells", 0)
world.options.grass_randomizer.value = world.passthrough.get("grass_randomizer", 0)
world.options.breakable_shuffle.value = world.passthrough.get("breakable_shuffle", 0)
world.options.entrance_layout.value = EntranceLayout.option_standard