TUNIC: The Big Refactor (#5195)

* 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

* Pull out fuse and bell shuffle

* Pull out fuse and bell shuffle

* Update worlds/tunic/options.py

Co-authored-by: qwint <qwint.42@gmail.com>

* Update worlds/tunic/logic_helpers.py

Co-authored-by: qwint <qwint.42@gmail.com>

* {} -> () in state functions

* {} -> () in state functions

* Change {} -> () in state functions, use constant for gun

* Remove floating constants in er_data

* Finish hard deprecating FixedShop

* Finish hard deprecating FixedShop

* Fix zig skip showing up in decoupled fixed shop

---------

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>
Co-authored-by: qwint <qwint.42@gmail.com>
This commit is contained in:
Scipio Wright
2025-09-04 18:44:32 -04:00
committed by GitHub
parent 42ace29db4
commit b0b3e3668f
17 changed files with 749 additions and 918 deletions

View File

@@ -1,25 +1,28 @@
from dataclasses import fields
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union, Set, TextIO
from logging import warning
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
from typing import Any, TypedDict, ClassVar, TextIO
from BaseClasses import Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
from Options import PlandoConnection, OptionError, PerGameCommonOptions, Range, Removed
from settings import Group, Bool, FilePath
from worlds.AutoWorld import WebWorld, World
# 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 .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)
from .locations import location_table, location_name_groups, standard_location_name_to_id, hexagon_locations
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, 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 .logic_helpers import randomize_ability_unlocks, gold_hexagon
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage, check_options,
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
LaurelsLocation, LaurelsZips, IceGrappling, LadderStorage, EntranceLayout,
check_options, LocalFill, get_hexagons_in_pool, HexagonQuestAbilityUnlockType)
from . import ut_stuff
from worlds.AutoWorld import WebWorld, World
from Options import PlandoConnection, OptionError, PerGameCommonOptions, Removed, Range
from settings import Group, Bool, FilePath
class TunicSettings(Group):
@@ -28,15 +31,15 @@ class TunicSettings(Group):
class LimitGrassRando(Bool):
"""Limits the impact of Grass Randomizer on the multiworld by disallowing local_fill percentages below 95."""
class UTPoptrackerPath(FilePath):
"""Path to the user's TUNIC Poptracker Pack."""
description = "TUNIC Poptracker Pack zip file"
required = False
disable_local_spoiler: Union[DisableLocalSpoiler, bool] = False
limit_grass_rando: Union[LimitGrassRando, bool] = True
ut_poptracker_path: Union[UTPoptrackerPath, str] = UTPoptrackerPath()
disable_local_spoiler: DisableLocalSpoiler | bool = False
limit_grass_rando: LimitGrassRando | bool = True
ut_poptracker_path: UTPoptrackerPath | str = UTPoptrackerPath()
class TunicWeb(WebWorld):
@@ -68,10 +71,10 @@ class SeedGroup(TypedDict):
laurels_zips: bool # laurels_zips value
ice_grappling: int # ice_grappling value
ladder_storage: int # ls value
laurels_at_10_fairies: bool # laurels location value
laurels_at_10_fairies: bool # whether laurels location is set to 10 fairies
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
plando: list[PlandoConnection] # consolidated plando connections for the seed group
class TunicWorld(World):
@@ -82,54 +85,66 @@ class TunicWorld(World):
"""
game = "TUNIC"
web = TunicWeb()
author: str = "SilentSR & ScipioWright"
options: TunicOptions
options_dataclass = TunicOptions
settings: ClassVar[TunicSettings]
item_name_groups = item_name_groups
# grass, breakables, fuses, and bells are separated out into their own files
# this makes for easier organization, at the cost of stuff like what's directly below here
location_name_groups = location_name_groups
for group_name, members in grass_location_name_groups.items():
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)
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)
player_location_table: Dict[str, int]
ability_unlocks: Dict[str, int]
slot_data_items: List[TunicItem]
tunic_portal_pairs: Dict[str, str]
er_portal_hints: Dict[int, str]
seed_groups: Dict[str, SeedGroup] = {}
used_shop_numbers: Set[int]
er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work
player_location_table: dict[str, int]
ability_unlocks: dict[str, int]
slot_data_items: list[TunicItem]
tunic_portal_pairs: dict[str, str]
er_portal_hints: dict[int, str]
seed_groups: dict[str, SeedGroup] = {}
used_shop_numbers: set[int]
er_regions: dict[str, RegionInfo] # absolutely needed so outlet regions work
# for the local_fill option
fill_items: List[TunicItem]
fill_locations: List[Location]
fill_items: list[TunicItem]
fill_locations: list[Location]
backup_locations: list[Location]
amount_to_local_fill: int
# so we only loop the multiworld locations once
# if these are locations instead of their info, it gives a memory leak error
item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {}
player_item_link_locations: Dict[str, List[Location]]
item_link_locations: dict[int, dict[str, list[tuple[int, str]]]] = {}
player_item_link_locations: dict[str, list[Location]]
using_ut: bool # so we can check if we're using UT only once
passthrough: Dict[str, Any]
passthrough: dict[str, Any]
ut_can_gen_without_yaml = True # class var that tells it to ignore the player yaml
tracker_world: ClassVar = ut_stuff.tracker_world
def generate_early(self) -> None:
# if you have multiple APWorlds, we want it to fail here instead of at the end of gen
try:
int(self.settings.disable_local_spoiler)
except AttributeError:
raise Exception("You have a TUNIC APWorld in your lib/worlds folder and custom_worlds folder.\n"
"This would cause an error at the end of generation.\n"
"Please remove one of them, most likely the one in lib/worlds.")
# hidden option for me to do multi-slot test gens with random options more easily
if self.options.all_random:
for option_name in (attr.name for attr in fields(TunicOptions)
if attr not in fields(PerGameCommonOptions)):
@@ -145,10 +160,11 @@ class TunicWorld(World):
option.value = self.random.choice(list(option.name_lookup))
check_options(self)
self.er_regions = tunic_er_regions.copy()
# empty plando connections if ER is off
if self.options.plando_connections and not self.options.entrance_rando:
self.options.plando_connections.value = ()
# modify direction and order of plando connections for more consistency later on
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)
@@ -180,6 +196,7 @@ class TunicWorld(World):
self.player_location_table = standard_location_name_to_id.copy()
# setup our defaults for the local_fill option
if self.options.local_fill == -1:
if self.options.grass_randomizer:
if self.options.breakable_shuffle:
@@ -206,9 +223,15 @@ 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)
@classmethod
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
tunic_worlds: tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
for tunic in tunic_worlds:
# setting up state combat logic stuff, see has_combat_reqs for its use
# and this is magic so pycharm doesn't like it, unfortunately
@@ -314,10 +337,10 @@ class TunicWorld(World):
return TunicItem(name, itemclass, self.item_name_to_id[name], self.player)
def create_items(self) -> None:
tunic_items: List[TunicItem] = []
tunic_items: list[TunicItem] = []
self.slot_data_items = []
items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()}
items_to_create: dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()}
# Calculate number of hexagons in item pool
if self.options.hexagon_quest:
@@ -377,7 +400,7 @@ class TunicWorld(World):
items_to_create[rgb_hexagon] = 0
# Filler items in the item pool
available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and
available_filler: list[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and
item_table[filler].classification == ItemClassification.filler]
# Remove filler to make room for other items
@@ -457,8 +480,8 @@ class TunicWorld(World):
# discard grass from non_local if it's meant to be limited
if self.settings.limit_grass_rando:
self.options.non_local_items.value.discard("Grass")
all_filler: List[TunicItem] = []
non_filler: List[TunicItem] = []
all_filler: list[TunicItem] = []
non_filler: list[TunicItem] = []
for tunic_item in tunic_items:
if (tunic_item.excludable
and tunic_item.name not in self.options.local_items
@@ -477,7 +500,7 @@ class TunicWorld(World):
if self.options.local_fill > 0 and self.multiworld.players > 1:
# we need to reserve a couple locations so that we don't fill up every sphere 1 location
sphere_one_locs = self.multiworld.get_reachable_locations(CollectionState(self.multiworld), self.player)
reserved_locations: Set[Location] = set(self.random.sample(sphere_one_locs, 2))
reserved_locations: set[Location] = set(self.random.sample(sphere_one_locs, 2))
viable_locations = [loc for loc in self.multiworld.get_unfilled_locations(self.player)
if loc not in reserved_locations
and loc.name not in self.options.priority_locations.value]
@@ -487,34 +510,91 @@ class TunicWorld(World):
f"This is likely due to excess plando or priority locations.")
self.random.shuffle(viable_locations)
self.fill_locations = viable_locations[:self.amount_to_local_fill]
self.backup_locations = viable_locations[self.amount_to_local_fill:]
@classmethod
def stage_pre_fill(cls, multiworld: MultiWorld) -> None:
tunic_fill_worlds: List[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC")
tunic_fill_worlds: list[TunicWorld] = [world for world in multiworld.get_game_worlds("TUNIC")
if world.options.local_fill.value > 0]
if tunic_fill_worlds and multiworld.players > 1:
grass_fill: List[TunicItem] = []
non_grass_fill: List[TunicItem] = []
grass_fill_locations: List[Location] = []
non_grass_fill_locations: List[Location] = []
grass_fill: list[TunicItem] = []
non_grass_fill: list[TunicItem] = []
grass_fill_locations: list[Location] = []
non_grass_fill_locations: list[Location] = []
backup_grass_locations: list[Location] = []
backup_non_grass_locations: list[Location] = []
for world in tunic_fill_worlds:
if world.options.grass_randomizer:
grass_fill.extend(world.fill_items)
grass_fill_locations.extend(world.fill_locations)
backup_grass_locations.extend(world.backup_locations)
else:
non_grass_fill.extend(world.fill_items)
non_grass_fill_locations.extend(world.fill_locations)
backup_non_grass_locations.extend(world.backup_locations)
multiworld.random.shuffle(grass_fill)
multiworld.random.shuffle(non_grass_fill)
multiworld.random.shuffle(grass_fill_locations)
multiworld.random.shuffle(non_grass_fill_locations)
multiworld.random.shuffle(backup_grass_locations)
multiworld.random.shuffle(backup_non_grass_locations)
# these are slots that filled in TUNIC locations during pre_fill
out_of_spec_worlds = set()
for filler_item in grass_fill:
grass_fill_locations.pop().place_locked_item(filler_item)
loc_to_fill = grass_fill_locations.pop()
try:
loc_to_fill.place_locked_item(filler_item)
except Exception:
out_of_spec_worlds.add(multiworld.worlds[loc_to_fill.item.player].game)
for loc in backup_grass_locations:
if not loc.item:
loc.place_locked_item(filler_item)
break
else:
out_of_spec_worlds.add(multiworld.worlds[loc_to_fill.item.player].game)
else:
raise Exception("TUNIC: Could not fulfill local_filler option. This issue is caused by another "
"world filling TUNIC locations during pre_fill.\n"
"Archipelago does not allow us to place items into the item pool after "
"create_items, so we cannot recover from this issue.\n"
f"This is likely caused by the following world(s): {out_of_spec_worlds}.\n"
f"Please let the world dev(s) for the listed world(s) know that there is an "
f"issue there.\n"
"As a workaround, you can try setting the local_filler option lower for "
"TUNIC slots with Breakable Shuffle or Grass Rando enabled. You may be able to "
"try generating again, as it may not happen every generation.")
for filler_item in non_grass_fill:
non_grass_fill_locations.pop().place_locked_item(filler_item)
loc_to_fill = non_grass_fill_locations.pop()
try:
loc_to_fill.place_locked_item(filler_item)
except Exception:
out_of_spec_worlds.add(multiworld.worlds[loc_to_fill.item.player].game)
for loc in backup_non_grass_locations:
if not loc.item:
loc.place_locked_item(filler_item)
break
else:
out_of_spec_worlds.add(multiworld.worlds[loc_to_fill.item.player].game)
else:
raise Exception("TUNIC: Could not fulfill local_filler option. This issue is caused by another "
"world filling TUNIC locations during pre_fill.\n"
"Archipelago does not allow us to place items into the item pool after "
"create_items, so we cannot recover from this issue.\n"
f"This is likely caused by the following world(s): {out_of_spec_worlds}.\n"
f"Please let the world dev(s) for the listed world(s) know that there is an "
f"issue there.\n"
"As a workaround, you can try setting the local_filler option lower for "
"TUNIC slots with Breakable Shuffle or Grass Rando enabled. You may be able to "
"try generating again, as it may not happen every generation.")
if out_of_spec_worlds:
warning("TUNIC: At least one other world has filled TUNIC locations during pre_fill. This may "
"cause issues for games that rely on placing items in their own world during pre_fill.\n"
f"This is likely being caused by the following world(s): {out_of_spec_worlds}.\n"
"Please let the world dev(s) for the listed world(s) know that there is an issue there.")
def create_regions(self) -> None:
self.tunic_portal_pairs = {}
@@ -522,48 +602,19 @@ class TunicWorld(World):
self.ability_unlocks = randomize_ability_unlocks(self)
# stuff for universal tracker support, can be ignored for standard gen
if self.using_ut:
if self.using_ut and self.options.hexagon_quest_ability_type == "hexagons":
self.ability_unlocks["Pages 24-25 (Prayer)"] = self.passthrough["Hexagon Quest Prayer"]
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = self.passthrough["Hexagon Quest Holy Cross"]
self.ability_unlocks["Pages 52-53 (Icebolt)"] = self.passthrough["Hexagon Quest Icebolt"]
# Most non-standard options use ER regions
if (self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic
or self.options.grass_randomizer or self.options.breakable_shuffle):
portal_pairs = create_er_regions(self)
if self.options.entrance_rando:
# these get interpreted by the game to tell it which entrances to connect
for portal1, portal2 in portal_pairs.items():
self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination()
else:
# uses the original rules, easier to navigate and reference
for region_name in tunic_regions:
region = Region(region_name, self.player, self.multiworld)
self.multiworld.regions.append(region)
for region_name, exits in tunic_regions.items():
region = self.get_region(region_name)
region.add_exits(exits)
for location_name, location_id in self.player_location_table.items():
region = self.get_region(location_table[location_name].region)
location = TunicLocation(self.player, location_name, location_id, region)
region.locations.append(location)
victory_region = self.get_region("Spirit Arena")
victory_location = TunicLocation(self.player, "The Heir", None, victory_region)
victory_location.place_locked_item(TunicItem("Victory", ItemClassification.progression, None, self.player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
victory_region.locations.append(victory_location)
portal_pairs = create_er_regions(self)
if self.options.entrance_rando:
# these get interpreted by the game to tell it which entrances to connect
for portal1, portal2 in portal_pairs.items():
self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination()
def set_rules(self) -> None:
# same reason as in create_regions
if (self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic
or self.options.grass_randomizer or self.options.breakable_shuffle):
set_er_location_rules(self)
else:
set_region_rules(self)
set_location_rules(self)
set_er_location_rules(self)
def get_filler_item_name(self) -> str:
return self.random.choice(filler_items)
@@ -582,14 +633,14 @@ class TunicWorld(World):
return change
def write_spoiler_header(self, spoiler_handle: TextIO):
if self.options.hexagon_quest and self.options.ability_shuffling\
and self.options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
if (self.options.hexagon_quest and self.options.ability_shuffling
and self.options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons):
spoiler_handle.write("\nAbility Unlocks (Hexagon Quest):\n")
for ability in self.ability_unlocks:
# Remove parentheses for better readability
spoiler_handle.write(f'{ability[ability.find("(")+1:ability.find(")")]}: {self.ability_unlocks[ability]} Gold Questagons\n')
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
def extend_hint_information(self, hint_data: dict[int, dict[int, str]]) -> None:
if self.options.entrance_rando:
hint_data.update({self.player: {}})
# all state seems to have efficient paths
@@ -626,7 +677,7 @@ class TunicWorld(World):
if hint_text:
hint_data[self.player][location.address] = hint_text
def get_real_location(self, location: Location) -> Tuple[str, int]:
def get_real_location(self, location: Location) -> tuple[str, int]:
# if it's not in a group, it's not in an item link
if location.player not in self.multiworld.groups or not location.item:
return location.name, location.player
@@ -638,8 +689,8 @@ class TunicWorld(World):
f"Using a potentially incorrect location name instead.")
return location.name, location.player
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {
def fill_slot_data(self) -> dict[str, Any]:
slot_data: dict[str, Any] = {
"seed": self.random.randint(0, 2147483647),
"start_with_sword": self.options.start_with_sword.value,
"keys_behind_bosses": self.options.keys_behind_bosses.value,
@@ -657,6 +708,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,
"grass_randomizer": self.options.grass_randomizer.value,
"combat_logic": self.options.combat_logic.value,
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
@@ -674,7 +727,7 @@ class TunicWorld(World):
# checking if groups so that this doesn't run if the player isn't in a group
if groups:
if not self.item_link_locations:
tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC")
tunic_worlds: tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC")
# figure out our groups and the items in them
for tunic in tunic_worlds:
for group in self.multiworld.get_player_groups(tunic.player):
@@ -710,7 +763,7 @@ class TunicWorld(World):
# for the universal tracker, doesn't get called in standard gen
# docs: https://github.com/FarisTheAncient/Archipelago/blob/tracker/worlds/tracker/docs/re-gen-passthrough.md
@staticmethod
def interpret_slot_data(slot_data: Dict[str, Any]) -> Dict[str, Any]:
def interpret_slot_data(slot_data: dict[str, Any]) -> dict[str, Any]:
# returning slot_data so it regens, giving it back in multiworld.re_gen_passthrough
# we are using re_gen_passthrough over modifying the world here due to complexities with ER
return slot_data