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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user