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
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
from enum import IntEnum
|
||||
from typing import TYPE_CHECKING, NamedTuple
|
||||
|
||||
from enum import IntEnum
|
||||
from BaseClasses import CollectionState, Region
|
||||
from worlds.generic.Rules import set_rule
|
||||
from .rules import has_sword, has_melee
|
||||
|
||||
from .constants import base_id
|
||||
from .er_rules import can_shop
|
||||
from .logic_helpers import has_sword, has_melee
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
|
||||
|
||||
# just getting an id that is a decent chunk ahead of the grass ones
|
||||
breakable_base_id = 509342400 + 8000
|
||||
|
||||
|
||||
class BreakableType(IntEnum):
|
||||
pot = 1
|
||||
fire_pot = 2
|
||||
@@ -341,6 +341,7 @@ breakable_location_table: dict[str, TunicLocationData] = {
|
||||
}
|
||||
|
||||
|
||||
breakable_base_id = base_id + 8000
|
||||
breakable_location_name_to_id: dict[str, int] = {name: breakable_base_id + index
|
||||
for index, name in enumerate(breakable_location_table)}
|
||||
|
||||
@@ -358,6 +359,7 @@ loc_group_convert: dict[str, str] = {
|
||||
"Beneath the Well Main": "Beneath the Well",
|
||||
"Well Boss": "Dark Tomb Checkpoint",
|
||||
"Dark Tomb Main": "Dark Tomb",
|
||||
"Magic Dagger House": "West Garden House",
|
||||
"Fortress Courtyard Upper": "Fortress Courtyard",
|
||||
"Fortress Courtyard Upper pot": "Fortress Courtyard",
|
||||
"Fortress Courtyard west pots": "Fortress Courtyard",
|
||||
@@ -370,13 +372,16 @@ loc_group_convert: dict[str, str] = {
|
||||
"Fortress Grave Path westmost pot": "Fortress Grave Path",
|
||||
"Fortress Grave Path pots": "Fortress Grave Path",
|
||||
"Dusty": "Fortress Leaf Piles",
|
||||
"Frog Stairs Upper": "Frog Stairs",
|
||||
"Frog Stairs Upper": "Frog Stairway",
|
||||
"Frog's Domain Front": "Frog's Domain",
|
||||
"Frog's Domain Main": "Frog's Domain",
|
||||
"Quarry Monastery Entry": "Quarry",
|
||||
"Quarry Back": "Quarry",
|
||||
"Lower Quarry": "Quarry",
|
||||
"Lower Quarry upper pots": "Quarry",
|
||||
"Even Lower Quarry": "Quarry",
|
||||
"Monastery Back": "Monastery",
|
||||
"Cathedral Main": "Cathedral",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
from typing import Dict, List, NamedTuple, Tuple, Optional
|
||||
from enum import IntEnum
|
||||
from collections import defaultdict
|
||||
from enum import IntEnum
|
||||
from typing import NamedTuple
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from .rules import has_sword, has_melee
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
|
||||
from .logic_helpers import has_sword, has_melee
|
||||
|
||||
|
||||
# the vanilla stats you are expected to have to get through an area, based on where they are in vanilla
|
||||
class AreaStats(NamedTuple):
|
||||
@@ -16,12 +18,12 @@ class AreaStats(NamedTuple):
|
||||
sp_level: int
|
||||
mp_level: int
|
||||
potion_count: int
|
||||
equipment: List[str] = []
|
||||
equipment: list[str] = []
|
||||
is_boss: bool = False
|
||||
|
||||
|
||||
# the vanilla upgrades/equipment you would have
|
||||
area_data: Dict[str, AreaStats] = {
|
||||
area_data: dict[str, AreaStats] = {
|
||||
# The upgrade page is right by the Well entrance. Upper Overworld by the chest in the top right might need something
|
||||
"Overworld": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Stick"]),
|
||||
"East Forest": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Sword"]),
|
||||
@@ -52,9 +54,9 @@ area_data: Dict[str, AreaStats] = {
|
||||
|
||||
# these are used for caching which areas can currently be reached in state
|
||||
# Gauntlet does not have exclusively higher stat requirements, so it will be checked separately
|
||||
boss_areas: List[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"]
|
||||
boss_areas: list[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"]
|
||||
# Swamp does not have exclusively higher stat requirements, so it will be checked separately
|
||||
non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss and name != "Swamp"]
|
||||
non_boss_areas: list[str] = [name for name, data in area_data.items() if not data.is_boss and name != "Swamp"]
|
||||
|
||||
|
||||
class CombatState(IntEnum):
|
||||
@@ -114,7 +116,7 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int) -> bool
|
||||
return met_combat_reqs
|
||||
|
||||
|
||||
def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: Optional[AreaStats] = None) -> bool:
|
||||
def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: AreaStats | None = None) -> bool:
|
||||
data = alt_data or area_data[area_name]
|
||||
extra_att_needed = 0
|
||||
extra_def_needed = 0
|
||||
@@ -303,7 +305,7 @@ def has_required_stats(data: AreaStats, state: CollectionState, player: int) ->
|
||||
|
||||
|
||||
# returns a tuple of your max attack level, the number of attack offerings
|
||||
def get_att_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
||||
def get_att_level(state: CollectionState, player: int) -> tuple[int, int]:
|
||||
att_offerings = state.count("ATT Offering", player)
|
||||
att_upgrades = state.count("Hero Relic - ATT", player)
|
||||
sword_level = state.count("Sword Upgrade", player)
|
||||
@@ -315,44 +317,44 @@ def get_att_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
||||
|
||||
|
||||
# returns a tuple of your max defense level, the number of defense offerings
|
||||
def get_def_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
||||
def get_def_level(state: CollectionState, player: int) -> tuple[int, int]:
|
||||
def_offerings = state.count("DEF Offering", player)
|
||||
# defense falls off, can just cap it at 8 for simplicity
|
||||
return (min(8, 1 + def_offerings
|
||||
+ state.count_from_list({"Hero Relic - DEF", "Secret Legend", "Phonomath"}, player))
|
||||
+ state.count_from_list(("Hero Relic - DEF", "Secret Legend", "Phonomath"), player))
|
||||
+ (2 if state.has("Shield", player) else 0)
|
||||
+ (2 if state.has("Hero's Laurels", player) else 0),
|
||||
def_offerings)
|
||||
|
||||
|
||||
# returns a tuple of your max potion level, the number of potion offerings
|
||||
def get_potion_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
||||
def get_potion_level(state: CollectionState, player: int) -> tuple[int, int]:
|
||||
potion_offerings = min(2, state.count("Potion Offering", player))
|
||||
# your third potion upgrade (from offerings) costs 1,000 money, reasonable to assume you won't do that
|
||||
return (1 + potion_offerings
|
||||
+ state.count_from_list({"Hero Relic - POTION", "Just Some Pals", "Spring Falls", "Back To Work"}, player),
|
||||
+ state.count_from_list(("Hero Relic - POTION", "Just Some Pals", "Spring Falls", "Back To Work"), player),
|
||||
potion_offerings)
|
||||
|
||||
|
||||
# returns a tuple of your max hp level, the number of hp offerings
|
||||
def get_hp_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
||||
def get_hp_level(state: CollectionState, player: int) -> tuple[int, int]:
|
||||
hp_offerings = state.count("HP Offering", player)
|
||||
return 1 + hp_offerings + state.count("Hero Relic - HP", player), hp_offerings
|
||||
|
||||
|
||||
# returns a tuple of your max sp level, the number of sp offerings
|
||||
def get_sp_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
||||
def get_sp_level(state: CollectionState, player: int) -> tuple[int, int]:
|
||||
sp_offerings = state.count("SP Offering", player)
|
||||
return (1 + sp_offerings
|
||||
+ state.count_from_list({"Hero Relic - SP", "Mr Mayor", "Power Up",
|
||||
"Regal Weasel", "Forever Friend"}, player),
|
||||
+ state.count_from_list(("Hero Relic - SP", "Mr Mayor", "Power Up",
|
||||
"Regal Weasel", "Forever Friend"), player),
|
||||
sp_offerings)
|
||||
|
||||
|
||||
def get_mp_level(state: CollectionState, player: int) -> Tuple[int, int]:
|
||||
def get_mp_level(state: CollectionState, player: int) -> tuple[int, int]:
|
||||
mp_offerings = state.count("MP Offering", player)
|
||||
return (1 + mp_offerings
|
||||
+ state.count_from_list({"Hero Relic - MP", "Sacred Geometry", "Vintage", "Dusty"}, player),
|
||||
+ state.count_from_list(("Hero Relic - MP", "Sacred Geometry", "Vintage", "Dusty"), player),
|
||||
mp_offerings)
|
||||
|
||||
|
||||
@@ -426,9 +428,9 @@ def calc_def_sp_cost(def_upgrades: int, sp_upgrades: int) -> int:
|
||||
|
||||
|
||||
class TunicState(LogicMixin):
|
||||
tunic_need_to_reset_combat_from_collect: Dict[int, bool]
|
||||
tunic_need_to_reset_combat_from_remove: Dict[int, bool]
|
||||
tunic_area_combat_state: Dict[int, Dict[str, int]]
|
||||
tunic_need_to_reset_combat_from_collect: dict[int, bool]
|
||||
tunic_need_to_reset_combat_from_remove: dict[int, bool]
|
||||
tunic_area_combat_state: dict[int, dict[str, int]]
|
||||
|
||||
def init_mixin(self, _):
|
||||
# the per-player need to reset the combat state when collecting a combat item
|
||||
|
||||
51
worlds/tunic/constants.py
Normal file
51
worlds/tunic/constants.py
Normal file
@@ -0,0 +1,51 @@
|
||||
base_id = 509342400
|
||||
|
||||
laurels = "Hero's Laurels"
|
||||
grapple = "Magic Orb"
|
||||
ice_dagger = "Magic Dagger"
|
||||
fire_wand = "Magic Wand"
|
||||
gun = "Gun"
|
||||
lantern = "Lantern"
|
||||
fairies = "Fairy"
|
||||
coins = "Golden Coin"
|
||||
prayer = "Pages 24-25 (Prayer)"
|
||||
holy_cross = "Pages 42-43 (Holy Cross)"
|
||||
icebolt = "Pages 52-53 (Icebolt)"
|
||||
shield = "Shield"
|
||||
key = "Key"
|
||||
house_key = "Old House Key"
|
||||
vault_key = "Fortress Vault Key"
|
||||
mask = "Scavenger Mask"
|
||||
red_hexagon = "Red Questagon"
|
||||
green_hexagon = "Green Questagon"
|
||||
blue_hexagon = "Blue Questagon"
|
||||
gold_hexagon = "Gold Questagon"
|
||||
|
||||
swamp_fuse_1 = "Swamp Fuse 1"
|
||||
swamp_fuse_2 = "Swamp Fuse 2"
|
||||
swamp_fuse_3 = "Swamp Fuse 3"
|
||||
cathedral_elevator_fuse = "Cathedral Elevator Fuse"
|
||||
quarry_fuse_1 = "Quarry Fuse 1"
|
||||
quarry_fuse_2 = "Quarry Fuse 2"
|
||||
ziggurat_miniboss_fuse = "Ziggurat Miniboss Fuse"
|
||||
ziggurat_teleporter_fuse = "Ziggurat Teleporter Fuse"
|
||||
fortress_exterior_fuse_1 = "Fortress Exterior Fuse 1"
|
||||
fortress_exterior_fuse_2 = "Fortress Exterior Fuse 2"
|
||||
fortress_courtyard_upper_fuse = "Fortress Courtyard Upper Fuse"
|
||||
fortress_courtyard_lower_fuse = "Fortress Courtyard Fuse"
|
||||
beneath_the_vault_fuse = "Beneath the Vault Fuse" # event needs to be renamed probably
|
||||
fortress_candles_fuse = "Fortress Candles Fuse"
|
||||
fortress_door_left_fuse = "Fortress Door Left Fuse"
|
||||
fortress_door_right_fuse = "Fortress Door Right Fuse"
|
||||
west_furnace_fuse = "West Furnace Fuse"
|
||||
west_garden_fuse = "West Garden Fuse"
|
||||
atoll_northeast_fuse = "Atoll Northeast Fuse"
|
||||
atoll_northwest_fuse = "Atoll Northwest Fuse"
|
||||
atoll_southeast_fuse = "Atoll Southeast Fuse"
|
||||
atoll_southwest_fuse = "Atoll Southwest Fuse"
|
||||
library_lab_fuse = "Library Lab Fuse"
|
||||
|
||||
# "Quarry - [East] Bombable Wall" is excluded from this list since it has slightly different rules
|
||||
bomb_walls = ["East Forest - Bombable Wall", "Eastern Vault Fortress - [East Wing] Bombable Wall",
|
||||
"Overworld - [Central] Bombable Wall", "Overworld - [Southwest] Bombable Wall Near Fountain",
|
||||
"Quarry - [West] Upper Area Bombable Wall", "Ruined Atoll - [Northwest] Bombable Wall"]
|
||||
@@ -1,5 +1,5 @@
|
||||
from typing import Dict, NamedTuple, List, Optional, TYPE_CHECKING
|
||||
from enum import IntEnum
|
||||
from typing import NamedTuple, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
@@ -36,7 +36,7 @@ class Portal(NamedTuple):
|
||||
return self.destination + ", " + self.scene() + self.tag
|
||||
|
||||
|
||||
portal_mapping: List[Portal] = [
|
||||
portal_mapping: list[Portal] = [
|
||||
Portal(name="Stick House Entrance", region="Overworld",
|
||||
destination="Sword Cave", tag="_", direction=Direction.north),
|
||||
Portal(name="Windmill Entrance", region="Overworld",
|
||||
@@ -535,7 +535,7 @@ portal_mapping: List[Portal] = [
|
||||
class RegionInfo(NamedTuple):
|
||||
game_scene: str # the name of the scene in the actual game
|
||||
dead_end: int = 0 # if a region has only one exit
|
||||
outlet_region: Optional[str] = None
|
||||
outlet_region: str | None = None
|
||||
is_fake_region: bool = False
|
||||
|
||||
|
||||
@@ -553,7 +553,7 @@ class DeadEnd(IntEnum):
|
||||
|
||||
|
||||
# key is the AP region name. "Fake" in region info just means the mod won't receive that info at all
|
||||
tunic_er_regions: Dict[str, RegionInfo] = {
|
||||
tunic_er_regions: dict[str, RegionInfo] = {
|
||||
"Menu": RegionInfo("Fake", dead_end=DeadEnd.all_cats, is_fake_region=True),
|
||||
"Overworld": RegionInfo("Overworld Redux"), # main overworld, the central area
|
||||
"Overworld Holy Cross": RegionInfo("Fake", dead_end=DeadEnd.all_cats, is_fake_region=True), # main overworld holy cross checks
|
||||
@@ -735,6 +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 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
|
||||
@@ -775,7 +776,6 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
||||
"Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats, is_fake_region=True),
|
||||
}
|
||||
|
||||
|
||||
# this is essentially a pared down version of the region connections in rules.py, with some minor differences
|
||||
# the main purpose of this is to make it so that you can access every region
|
||||
# most items are excluded from the rules here, since we can assume Archipelago will properly place them
|
||||
@@ -786,7 +786,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
||||
# LS# refers to ladder storage difficulties
|
||||
# LS rules are used for region connections here regardless of whether you have being knocked out of the air in logic
|
||||
# this is because it just means you can reach the entrances in that region via ladder storage
|
||||
traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
traversal_requirements: dict[str, dict[str, list[list[str]]]] = {
|
||||
"Overworld": {
|
||||
"Overworld Beach":
|
||||
[],
|
||||
@@ -801,7 +801,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
"Overworld Swamp Lower Entry":
|
||||
[],
|
||||
"Overworld Special Shop Entry":
|
||||
[["Hyperdash"], ["LS1"]],
|
||||
[["LS1"]],
|
||||
"Overworld Well Entry Area":
|
||||
[],
|
||||
"Overworld Ruined Passage Door":
|
||||
@@ -823,7 +823,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
"Overworld Tunnel Turret":
|
||||
[["IG1"], ["LS1"], ["Hyperdash"]],
|
||||
"Overworld Temple Door":
|
||||
[["IG2"], ["LS3"], ["Forest Belltower Upper", "Overworld Belltower"]],
|
||||
[["Bell Shuffle"], ["IG2"], ["LS3"], ["Forest Belltower Upper", "Overworld Belltower"]],
|
||||
"Overworld Southeast Cross Door":
|
||||
[],
|
||||
"Overworld Fountain Cross Door":
|
||||
@@ -1229,7 +1229,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
},
|
||||
"West Garden by Portal": {
|
||||
"West Garden Portal":
|
||||
[["West Garden South Checkpoint"]],
|
||||
[["Fuse Shuffle"], ["West Garden South Checkpoint"]],
|
||||
"West Garden Portal Item":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
@@ -1468,7 +1468,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
|
||||
"Eastern Vault Fortress": {
|
||||
"Eastern Vault Fortress Gold Door":
|
||||
[["IG2"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]],
|
||||
[["IG2"], ["Fuse Shuffle"],
|
||||
["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]],
|
||||
},
|
||||
"Eastern Vault Fortress Gold Door": {
|
||||
"Eastern Vault Fortress":
|
||||
@@ -1514,7 +1515,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
|
||||
"Fortress Arena": {
|
||||
"Fortress Arena Portal":
|
||||
[["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]],
|
||||
[["Fuse Shuffle"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]],
|
||||
},
|
||||
"Fortress Arena Portal": {
|
||||
"Fortress Arena":
|
||||
@@ -1547,7 +1548,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
|
||||
"Quarry Entry": {
|
||||
"Quarry Portal":
|
||||
[["Quarry Connector"]],
|
||||
[["Fuse Shuffle"], ["Quarry Connector"]],
|
||||
"Quarry":
|
||||
[],
|
||||
"Monastery Rope":
|
||||
@@ -1593,7 +1594,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
"Even Lower Quarry":
|
||||
[],
|
||||
"Lower Quarry Zig Door":
|
||||
[["Quarry", "Quarry Connector"], ["IG3"]],
|
||||
[["Fuse Shuffle"], ["Quarry", "Quarry Connector"], ["IG3"]],
|
||||
},
|
||||
"Monastery Rope": {
|
||||
"Quarry Back":
|
||||
@@ -1636,13 +1637,19 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
[["Hyperdash"]],
|
||||
"Rooted Ziggurat Lower Front":
|
||||
[],
|
||||
"Rooted Ziggurat Lower Back":
|
||||
"Rooted Ziggurat Lower Miniboss Platform":
|
||||
[],
|
||||
},
|
||||
"Rooted Ziggurat Lower Miniboss Platform": {
|
||||
"Rooted Ziggurat Lower Mid Checkpoint":
|
||||
[],
|
||||
"Rooted Ziggurat Lower Back":
|
||||
[]
|
||||
},
|
||||
"Rooted Ziggurat Lower Back": {
|
||||
"Rooted Ziggurat Lower Entry":
|
||||
[["LS2"]],
|
||||
"Rooted Ziggurat Lower Mid Checkpoint":
|
||||
"Rooted Ziggurat Lower Miniboss Platform":
|
||||
[["Hyperdash"], ["IG1"]],
|
||||
"Rooted Ziggurat Portal Room Entrance":
|
||||
[],
|
||||
@@ -1658,7 +1665,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
},
|
||||
"Rooted Ziggurat Portal Room": {
|
||||
"Rooted Ziggurat Portal Room Exit":
|
||||
[["Rooted Ziggurat Lower Back"]],
|
||||
[["Fuse Shuffle"], ["Rooted Ziggurat Lower Back"]],
|
||||
"Rooted Ziggurat Portal":
|
||||
[],
|
||||
},
|
||||
@@ -1742,7 +1749,6 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
"Cathedral Main":
|
||||
[],
|
||||
},
|
||||
|
||||
"Cathedral Gauntlet Checkpoint": {
|
||||
"Cathedral Gauntlet":
|
||||
[],
|
||||
@@ -1762,13 +1768,13 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
"Far Shore to East Forest Region":
|
||||
[["Hyperdash"]],
|
||||
"Far Shore to Quarry Region":
|
||||
[["Quarry Connector", "Quarry"]],
|
||||
[["Fuse Shuffle"], ["Quarry Connector", "Quarry"]],
|
||||
"Far Shore to Library Region":
|
||||
[["Library Lab"]],
|
||||
[["Fuse Shuffle"], ["Library Lab"]],
|
||||
"Far Shore to West Garden Region":
|
||||
[["West Garden South Checkpoint"]],
|
||||
[["Fuse Shuffle"], ["West Garden South Checkpoint"]],
|
||||
"Far Shore to Fortress Region":
|
||||
[["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]],
|
||||
[["Fuse Shuffle"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]],
|
||||
},
|
||||
"Far Shore to Spawn Region": {
|
||||
"Far Shore":
|
||||
|
||||
@@ -1,58 +1,32 @@
|
||||
from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING
|
||||
from typing import FrozenSet, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Region
|
||||
from worlds.generic.Rules import set_rule, add_rule, forbid_item
|
||||
from BaseClasses import Region, CollectionState
|
||||
from .options import IceGrappling, LadderStorage, CombatLogic
|
||||
from .rules import (has_ability, has_sword, has_melee, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage,
|
||||
laurels_zip, bomb_walls)
|
||||
from .er_data import Portal, get_portal_outlet_region
|
||||
from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls
|
||||
|
||||
# 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 .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,
|
||||
can_shop, can_get_past_bushes, laurels_zip, has_ice_grapple_logic, can_ladder_storage)
|
||||
from .options import IceGrappling, LadderStorage, CombatLogic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
|
||||
laurels = "Hero's Laurels"
|
||||
grapple = "Magic Orb"
|
||||
ice_dagger = "Magic Dagger"
|
||||
fire_wand = "Magic Wand"
|
||||
gun = "Gun"
|
||||
lantern = "Lantern"
|
||||
fairies = "Fairy"
|
||||
coins = "Golden Coin"
|
||||
prayer = "Pages 24-25 (Prayer)"
|
||||
holy_cross = "Pages 42-43 (Holy Cross)"
|
||||
icebolt = "Pages 52-53 (Icebolt)"
|
||||
key = "Key"
|
||||
house_key = "Old House Key"
|
||||
vault_key = "Fortress Vault Key"
|
||||
mask = "Scavenger Mask"
|
||||
red_hexagon = "Red Questagon"
|
||||
green_hexagon = "Green Questagon"
|
||||
blue_hexagon = "Blue Questagon"
|
||||
gold_hexagon = "Gold Questagon"
|
||||
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 has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> bool:
|
||||
return not world.options.shuffle_ladders or state.has(ladder, world.player)
|
||||
|
||||
|
||||
def can_shop(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
return has_sword(state, world.player) and state.can_reach_region("Shop", world.player)
|
||||
|
||||
|
||||
# for the ones that are not early bushes where ER can screw you over a bit
|
||||
def can_get_past_bushes(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
# add in glass cannon + stick for grass rando
|
||||
return has_sword(state, world.player) or state.has_any((fire_wand, laurels, gun), world.player)
|
||||
|
||||
|
||||
def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_pairs: Dict[Portal, Portal]) -> None:
|
||||
def set_er_region_rules(world: "TunicWorld", regions: dict[str, Region], portal_pairs: dict[Portal, Portal]) -> None:
|
||||
player = world.player
|
||||
options = world.options
|
||||
|
||||
# input scene destination tag, returns portal's name and paired portal's outlet region or region
|
||||
def get_portal_info(portal_sd: str) -> Tuple[str, str]:
|
||||
def get_portal_info(portal_sd: str) -> tuple[str, str]:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
if portal1.scene_destination() == portal_sd:
|
||||
return portal1.name, get_portal_outlet_region(portal2, world)
|
||||
@@ -61,7 +35,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
raise Exception(f"No matches found in get_portal_info for {portal_sd}")
|
||||
|
||||
# input scene destination tag, returns paired portal's name and region
|
||||
def get_paired_portal(portal_sd: str) -> Tuple[str, str]:
|
||||
def get_paired_portal(portal_sd: str) -> tuple[str, str]:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
if portal1.scene_destination() == portal_sd:
|
||||
return portal2.name, portal2.region
|
||||
@@ -81,7 +55,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Beach"],
|
||||
rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
|
||||
or state.has_any({laurels, grapple}, player))
|
||||
or state.has_any((laurels, grapple), player))
|
||||
# regions["Overworld Beach"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
|
||||
@@ -114,14 +88,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["Overworld to Atoll Upper"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: state.has_any({laurels, grapple}, player))
|
||||
rule=lambda state: state.has_any((laurels, grapple), player))
|
||||
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Belltower"],
|
||||
rule=lambda state: state.has(laurels, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
regions["Overworld Belltower"].connect(
|
||||
connecting_region=regions["Overworld"])
|
||||
# regions["Overworld Belltower"].connect(
|
||||
# connecting_region=regions["Overworld"])
|
||||
|
||||
# ice grapple rudeling across rubble, drop bridge, ice grapple rudeling down
|
||||
regions["Overworld Belltower"].connect(
|
||||
@@ -146,17 +120,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
connecting_region=regions["Overworld Ruined Passage Door"],
|
||||
rule=lambda state: state.has(key, player, 2)
|
||||
or laurels_zip(state, world))
|
||||
regions["Overworld Ruined Passage Door"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: laurels_zip(state, world))
|
||||
# regions["Overworld Ruined Passage Door"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: laurels_zip(state, world))
|
||||
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["After Ruined Passage"],
|
||||
rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
regions["After Ruined Passage"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: has_ladder("Ladders near Weathervane", state, world))
|
||||
# regions["After Ruined Passage"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: has_ladder("Ladders near Weathervane", state, world))
|
||||
|
||||
# for the hard ice grapple, get to the chest after the bomb wall, grab a slime, and grapple push down
|
||||
# you can ice grapple through the bomb wall, so no need for shop logic checking
|
||||
@@ -165,10 +139,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
|
||||
or state.has(laurels, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
|
||||
regions["Above Ruined Passage"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
|
||||
or state.has(laurels, player))
|
||||
# regions["Above Ruined Passage"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
|
||||
# or state.has(laurels, player))
|
||||
|
||||
regions["After Ruined Passage"].connect(
|
||||
connecting_region=regions["Above Ruined Passage"],
|
||||
@@ -183,8 +157,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
regions["East Overworld"].connect(
|
||||
connecting_region=regions["Above Ruined Passage"],
|
||||
rule=lambda state: has_ladder("Ladders near Weathervane", state, world)
|
||||
or state.has(laurels, player))
|
||||
rule=lambda state: has_ladder("Ladders near Weathervane", state, world))
|
||||
|
||||
# nmg: ice grapple the slimes, works both ways consistently
|
||||
regions["East Overworld"].connect(
|
||||
@@ -198,9 +171,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
connecting_region=regions["East Overworld"],
|
||||
rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
regions["East Overworld"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world))
|
||||
# regions["East Overworld"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world))
|
||||
|
||||
regions["East Overworld"].connect(
|
||||
connecting_region=regions["Overworld at Patrol Cave"])
|
||||
@@ -220,9 +193,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
connecting_region=regions["Overworld above Patrol Cave"],
|
||||
rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world)
|
||||
or state.has(grapple, player))
|
||||
regions["Overworld above Patrol Cave"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world))
|
||||
# regions["Overworld above Patrol Cave"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: has_ladder("Ladders near Overworld Checkpoint", state, world))
|
||||
|
||||
regions["East Overworld"].connect(
|
||||
connecting_region=regions["Overworld above Patrol Cave"],
|
||||
@@ -243,10 +216,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Upper Overworld"].connect(
|
||||
connecting_region=regions["Overworld above Quarry Entrance"],
|
||||
rule=lambda state: state.has_any({grapple, laurels}, player))
|
||||
rule=lambda state: state.has_any((grapple, laurels), player))
|
||||
regions["Overworld above Quarry Entrance"].connect(
|
||||
connecting_region=regions["Upper Overworld"],
|
||||
rule=lambda state: state.has_any({grapple, laurels}, player))
|
||||
rule=lambda state: state.has_any((grapple, laurels), player))
|
||||
|
||||
# ice grapple push guard captain down the ledge
|
||||
regions["Upper Overworld"].connect(
|
||||
@@ -267,11 +240,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld after Envoy"],
|
||||
rule=lambda state: state.has_any({laurels, grapple, gun}, player)
|
||||
rule=lambda state: state.has_any((laurels, grapple, gun), player)
|
||||
or state.has("Sword Upgrade", player, 4))
|
||||
regions["Overworld after Envoy"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: state.has_any({laurels, grapple, gun}, player)
|
||||
rule=lambda state: state.has_any((laurels, grapple, gun), player)
|
||||
or state.has("Sword Upgrade", player, 4))
|
||||
|
||||
regions["Overworld after Envoy"].connect(
|
||||
@@ -285,24 +258,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Quarry Entry"],
|
||||
rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
regions["Overworld Quarry Entry"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world))
|
||||
# regions["Overworld Quarry Entry"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world))
|
||||
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Swamp Upper Entry"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["Overworld Swamp Upper Entry"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
# regions["Overworld Swamp Upper Entry"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: state.has(laurels, player))
|
||||
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Swamp Lower Entry"],
|
||||
rule=lambda state: has_ladder("Ladder to Swamp", state, world)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
|
||||
regions["Overworld Swamp Lower Entry"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: has_ladder("Ladder to Swamp", state, world))
|
||||
# regions["Overworld Swamp Lower Entry"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: has_ladder("Ladder to Swamp", state, world))
|
||||
|
||||
regions["East Overworld"].connect(
|
||||
connecting_region=regions["Overworld Special Shop Entry"],
|
||||
@@ -335,33 +308,34 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
connecting_region=regions["Overworld Southeast Cross Door"],
|
||||
rule=lambda state: has_ability(holy_cross, state, world)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
|
||||
regions["Overworld Southeast Cross Door"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: has_ability(holy_cross, state, world))
|
||||
# regions["Overworld Southeast Cross Door"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: has_ability(holy_cross, state, world))
|
||||
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Fountain Cross Door"],
|
||||
rule=lambda state: has_ability(holy_cross, state, world)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
regions["Overworld Fountain Cross Door"].connect(
|
||||
connecting_region=regions["Overworld"])
|
||||
# regions["Overworld Fountain Cross Door"].connect(
|
||||
# connecting_region=regions["Overworld"])
|
||||
|
||||
ow_to_town_portal = regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Town Portal"],
|
||||
rule=lambda state: has_ability(prayer, state, world))
|
||||
regions["Overworld Town Portal"].connect(
|
||||
connecting_region=regions["Overworld"])
|
||||
# regions["Overworld Town Portal"].connect(
|
||||
# connecting_region=regions["Overworld"])
|
||||
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Spawn Portal"],
|
||||
rule=lambda state: has_ability(prayer, state, world))
|
||||
regions["Overworld Spawn Portal"].connect(
|
||||
connecting_region=regions["Overworld"])
|
||||
# regions["Overworld Spawn Portal"].connect(
|
||||
# connecting_region=regions["Overworld"])
|
||||
|
||||
# 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)
|
||||
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)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
|
||||
regions["Overworld Temple Door"].connect(
|
||||
@@ -637,7 +611,8 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
connecting_region=regions["West Garden by Portal"])
|
||||
regions["West Garden by Portal"].connect(
|
||||
connecting_region=regions["West Garden Portal"],
|
||||
rule=lambda state: has_ability(prayer, state, world) and state.has("Activate West Garden Fuse", player))
|
||||
rule=lambda state: has_ability(prayer, state, world)
|
||||
and has_fuses("Activate West Garden Fuse", state, world))
|
||||
|
||||
regions["West Garden by Portal"].connect(
|
||||
connecting_region=regions["West Garden Portal Item"],
|
||||
@@ -691,12 +666,16 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
atoll_statue = regions["Ruined Atoll"].connect(
|
||||
connecting_region=regions["Ruined Atoll Statue"],
|
||||
rule=lambda state: has_ability(prayer, state, world)
|
||||
and ((has_ladder("Ladders in South Atoll", state, world)
|
||||
and state.has_any((laurels, grapple), player)
|
||||
and (has_sword(state, player) or state.has_any((fire_wand, gun), 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 (((((has_ladder("Ladders in South Atoll", state, world)
|
||||
and state.has_any((laurels, grapple), player)
|
||||
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)
|
||||
or (state.has_all((atoll_northwest_fuse, atoll_northeast_fuse, atoll_southwest_fuse, atoll_southeast_fuse), player)
|
||||
and fuses_option))
|
||||
)
|
||||
|
||||
regions["Ruined Atoll Statue"].connect(
|
||||
connecting_region=regions["Ruined Atoll"])
|
||||
|
||||
@@ -742,7 +721,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Library Exterior by Tree"].connect(
|
||||
connecting_region=regions["Library Exterior Ladder Region"],
|
||||
rule=lambda state: state.has_any({grapple, laurels}, player)
|
||||
rule=lambda state: state.has_any((grapple, laurels), player)
|
||||
and has_ladder("Ladders in Library", state, world))
|
||||
regions["Library Exterior Ladder Region"].connect(
|
||||
connecting_region=regions["Library Exterior by Tree"],
|
||||
@@ -785,7 +764,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Library Lab Lower"].connect(
|
||||
connecting_region=regions["Library Lab"],
|
||||
rule=lambda state: state.has_any({grapple, laurels}, player)
|
||||
rule=lambda state: state.has_any((grapple, laurels), player)
|
||||
and has_ladder("Ladders in Library", state, world))
|
||||
regions["Library Lab"].connect(
|
||||
connecting_region=regions["Library Lab Lower"],
|
||||
@@ -802,7 +781,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Library Lab on Portal Pad"].connect(
|
||||
connecting_region=regions["Library Portal"],
|
||||
rule=lambda state: has_ability(prayer, state, world))
|
||||
rule=lambda state: has_ability(prayer, state, world) and has_fuses("Activate Library Fuse", state, world))
|
||||
regions["Library Portal"].connect(
|
||||
connecting_region=regions["Library Lab on Portal Pad"])
|
||||
|
||||
@@ -823,10 +802,14 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Fortress Exterior near cave"].connect(
|
||||
connecting_region=regions["Fortress Exterior from Overworld"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
rule=lambda state: state.has(laurels, player)
|
||||
or (has_ability(prayer, state, world) and state.has(fortress_exterior_fuse_1, player)
|
||||
and fuses_option))
|
||||
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))
|
||||
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)))
|
||||
|
||||
# shoot far fire pot, enemy gets aggro'd
|
||||
regions["Fortress Exterior near cave"].connect(
|
||||
@@ -889,12 +872,15 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Eastern Vault Fortress"].connect(
|
||||
connecting_region=regions["Eastern Vault Fortress Gold Door"],
|
||||
rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses",
|
||||
"Activate Eastern Vault East Fuse"}, player)
|
||||
rule=lambda state: (has_fuses("Activate Eastern Vault West Fuses", state, world)
|
||||
and has_fuses("Activate Eastern Vault East Fuse", state, world))
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
regions["Eastern Vault Fortress Gold Door"].connect(
|
||||
connecting_region=regions["Eastern Vault Fortress"],
|
||||
rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world))
|
||||
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))
|
||||
|
||||
fort_grave_entry_to_combat = regions["Fortress Grave Path Entry"].connect(
|
||||
connecting_region=regions["Fortress Grave Path Combat"])
|
||||
@@ -925,7 +911,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Fortress Arena"].connect(
|
||||
connecting_region=regions["Fortress Arena Portal"],
|
||||
rule=lambda state: state.has("Activate Eastern Vault West Fuses", player))
|
||||
rule=lambda state: has_ability(prayer, state, world) and has_fuses("Activate Eastern Vault West Fuses", state, world))
|
||||
regions["Fortress Arena Portal"].connect(
|
||||
connecting_region=regions["Fortress Arena"])
|
||||
|
||||
@@ -939,7 +925,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Quarry Entry"].connect(
|
||||
connecting_region=regions["Quarry Portal"],
|
||||
rule=lambda state: state.has("Activate Quarry Fuse", player))
|
||||
rule=lambda state: has_ability(prayer, state, world) and has_fuses("Activate Quarry Fuse", state, world))
|
||||
regions["Quarry Portal"].connect(
|
||||
connecting_region=regions["Quarry Entry"])
|
||||
|
||||
@@ -990,7 +976,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Even Lower Quarry Isolated Chest"].connect(
|
||||
connecting_region=regions["Lower Quarry Zig Door"],
|
||||
rule=lambda state: state.has("Activate Quarry Fuse", player)
|
||||
rule=lambda state: has_fuses("Activate Quarry Fuse", state, world)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
|
||||
|
||||
# don't need the mask for this either, please don't complain about not needing a mask here, you know what you did
|
||||
@@ -1037,21 +1023,26 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
zig_low_mid_to_front = regions["Rooted Ziggurat Lower Mid Checkpoint"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Front"])
|
||||
|
||||
zig_low_mid_to_back = regions["Rooted Ziggurat Lower Mid Checkpoint"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Back"],
|
||||
rule=lambda state: state.has(laurels, player)
|
||||
or (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_back_to_mid = regions["Rooted Ziggurat Lower Back"].connect(
|
||||
regions["Rooted Ziggurat Lower Mid Checkpoint"].connect(
|
||||
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(laurels, player)
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
and has_ability(prayer, state, world)
|
||||
and has_sword(state, player))
|
||||
rule=lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option
|
||||
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))
|
||||
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))
|
||||
|
||||
regions["Rooted Ziggurat Lower Back"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Portal Room Entrance"],
|
||||
rule=lambda state: has_ability(prayer, state, world))
|
||||
rule=lambda state: has_fuses("Activate Ziggurat Fuse", state, world))
|
||||
regions["Rooted Ziggurat Portal Room Entrance"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Back"])
|
||||
|
||||
@@ -1059,11 +1050,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
connecting_region=regions["Rooted Ziggurat Portal Room"])
|
||||
regions["Rooted Ziggurat Portal Room"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Portal"],
|
||||
rule=lambda state: has_ability(prayer, state, world))
|
||||
rule=lambda state: has_fuses("Activate Ziggurat Fuse", state, world) and has_ability(prayer, state, world))
|
||||
|
||||
regions["Rooted Ziggurat Portal Room"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
|
||||
rule=lambda state: state.has("Activate Ziggurat Fuse", player))
|
||||
rule=lambda state: has_fuses("Activate Ziggurat Fuse", state, world))
|
||||
regions["Rooted Ziggurat Portal Room Exit"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Portal Room"])
|
||||
|
||||
@@ -1082,19 +1073,21 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
swamp_mid_to_cath = regions["Swamp Mid"].connect(
|
||||
connecting_region=regions["Swamp to Cathedral Main Entrance Region"],
|
||||
rule=lambda state: (has_ability(prayer, state, world)
|
||||
and (has_sword(state, player))
|
||||
and has_sword(state, player)
|
||||
and (state.has(laurels, player)
|
||||
# blam yourself in the face with a wand shot off the fuse
|
||||
or (can_ladder_storage(state, world) and state.has(fire_wand, player)
|
||||
and options.ladder_storage >= LadderStorage.option_hard
|
||||
and (not options.shuffle_ladders
|
||||
or state.has_any({"Ladders in Overworld Town",
|
||||
or state.has_any(("Ladders in Overworld Town",
|
||||
"Ladder to Swamp",
|
||||
"Ladders near Weathervane"}, player)
|
||||
"Ladders near Weathervane"), player)
|
||||
or (state.has("Ladder to Ruined Atoll", player)
|
||||
and state.can_reach_region("Overworld Beach", player)))))
|
||||
and (not options.combat_logic
|
||||
or has_combat_reqs("Swamp", state, player)))
|
||||
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)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
|
||||
if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders:
|
||||
@@ -1102,7 +1095,8 @@ 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))
|
||||
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))
|
||||
|
||||
# grapple push the enemy by the door down, then grapple to it. Really jank
|
||||
regions["Swamp Mid"].connect(
|
||||
@@ -1148,7 +1142,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: (has_ability(prayer, state, world)
|
||||
rule=lambda state: ((state.has(cathedral_elevator_fuse, player) if fuses_option 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(
|
||||
@@ -1159,11 +1153,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
regions["Cathedral Main"].connect(
|
||||
connecting_region=regions["Cathedral Entry"])
|
||||
|
||||
cath_elev_to_main = regions["Cathedral to Gauntlet"].connect(
|
||||
connecting_region=regions["Cathedral Main"])
|
||||
regions["Cathedral Main"].connect(
|
||||
connecting_region=regions["Cathedral to Gauntlet"])
|
||||
|
||||
regions["Cathedral Gauntlet Checkpoint"].connect(
|
||||
connecting_region=regions["Cathedral Gauntlet"])
|
||||
|
||||
@@ -1191,25 +1180,25 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
regions["Far Shore"].connect(
|
||||
connecting_region=regions["Far Shore to West Garden Region"],
|
||||
rule=lambda state: state.has("Activate West Garden Fuse", player))
|
||||
rule=lambda state: has_fuses("Activate West Garden Fuse", state, world))
|
||||
regions["Far Shore to West Garden Region"].connect(
|
||||
connecting_region=regions["Far Shore"])
|
||||
|
||||
regions["Far Shore"].connect(
|
||||
connecting_region=regions["Far Shore to Quarry Region"],
|
||||
rule=lambda state: state.has("Activate Quarry Fuse", player))
|
||||
rule=lambda state: has_fuses("Activate Quarry Fuse", state, world))
|
||||
regions["Far Shore to Quarry Region"].connect(
|
||||
connecting_region=regions["Far Shore"])
|
||||
|
||||
regions["Far Shore"].connect(
|
||||
connecting_region=regions["Far Shore to Fortress Region"],
|
||||
rule=lambda state: state.has("Activate Eastern Vault West Fuses", player))
|
||||
rule=lambda state: has_fuses("Activate Eastern Vault West Fuses", state, world))
|
||||
regions["Far Shore to Fortress Region"].connect(
|
||||
connecting_region=regions["Far Shore"])
|
||||
|
||||
regions["Far Shore"].connect(
|
||||
connecting_region=regions["Far Shore to Library Region"],
|
||||
rule=lambda state: state.has("Activate Library Fuse", player))
|
||||
rule=lambda state: has_fuses("Activate Library Fuse", state, world))
|
||||
regions["Far Shore to Library Region"].connect(
|
||||
connecting_region=regions["Far Shore"])
|
||||
|
||||
@@ -1239,7 +1228,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
non_ow_ls_list.extend(hard_ls)
|
||||
|
||||
# create the ls elevation regions
|
||||
ladder_regions: Dict[str, Region] = {}
|
||||
ladder_regions: dict[str, Region] = {}
|
||||
for name in ow_ladder_groups.keys():
|
||||
ladder_regions[name] = Region(name, player, world.multiworld)
|
||||
|
||||
@@ -1409,10 +1398,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
lambda state: has_combat_reqs("Dark Tomb", state, player))
|
||||
|
||||
set_rule(wg_before_to_after_terry,
|
||||
lambda state: state.has_any({laurels, ice_dagger}, player)
|
||||
lambda state: state.has_any((laurels, ice_dagger), player)
|
||||
or has_combat_reqs("West Garden", state, player))
|
||||
set_rule(wg_after_to_before_terry,
|
||||
lambda state: state.has_any({laurels, ice_dagger}, player)
|
||||
lambda state: state.has_any((laurels, ice_dagger), player)
|
||||
or has_combat_reqs("West Garden", state, player))
|
||||
|
||||
set_rule(wg_after_terry_to_west_combat,
|
||||
@@ -1453,25 +1442,23 @@ 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_mid_to_front,
|
||||
lambda state: has_combat_reqs("Rooted Ziggurat", state, player))
|
||||
set_rule(zig_low_mid_to_back,
|
||||
set_rule(zig_low_miniboss_to_back,
|
||||
lambda state: state.has(laurels, player)
|
||||
or (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player)))
|
||||
set_rule(zig_low_back_to_mid,
|
||||
lambda state: (state.has(laurels, player)
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
and has_ability(prayer, state, world)
|
||||
and has_combat_reqs("Rooted Ziggurat", state, player))
|
||||
or (state.has(ziggurat_miniboss_fuse, player) if fuses_option
|
||||
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
|
||||
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 (has_ability(prayer, state, world) and has_combat_reqs("Swamp", state, player)))
|
||||
or (state.has(cathedral_elevator_fuse, player) if fuses_option
|
||||
else (has_ability(prayer, state, world) and has_combat_reqs("Swamp", state, player))))
|
||||
|
||||
set_rule(cath_entry_to_main,
|
||||
lambda state: has_combat_reqs("Swamp", state, player))
|
||||
set_rule(cath_elev_to_main,
|
||||
lambda state: has_combat_reqs("Swamp", state, player))
|
||||
|
||||
# for spots where you can go into and come out of an entrance to reset enemy aggro
|
||||
if world.options.entrance_rando:
|
||||
@@ -1543,10 +1530,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
player = world.player
|
||||
options = world.options
|
||||
|
||||
if world.options.grass_randomizer:
|
||||
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)
|
||||
|
||||
forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player)
|
||||
|
||||
# Ability Shuffle Exclusive Rules
|
||||
@@ -1557,7 +1551,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
set_rule(world.get_location("East Forest - Golden Obelisk Holy Cross"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("Beneath the Well - [Powered Secret Room] Chest"),
|
||||
lambda state: state.has("Activate Furnace Fuse", player))
|
||||
lambda state: has_fuses("Activate Furnace Fuse", state, world))
|
||||
set_rule(world.get_location("West Garden - [North] Behind Holy Cross Door"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("Library Hall - Holy Cross Chest"),
|
||||
@@ -1583,9 +1577,9 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
|
||||
# Overworld
|
||||
set_rule(world.get_location("Overworld - [Southwest] Grapple Chest Over Walkway"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
lambda state: state.has_any((grapple, laurels), player))
|
||||
set_rule(world.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
lambda state: state.has_any((grapple, laurels), player))
|
||||
set_rule(world.get_location("Overworld - [Southwest] From West Garden"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("Overworld - [Southeast] Page on Pillar by Swamp"),
|
||||
@@ -1635,9 +1629,9 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
set_rule(world.get_location("East Forest - Lower Grapple Chest"),
|
||||
lambda state: state.has(grapple, player))
|
||||
set_rule(world.get_location("East Forest - Lower Dash Chest"),
|
||||
lambda state: state.has_all({grapple, laurels}, player))
|
||||
lambda state: state.has_all((grapple, laurels), player))
|
||||
set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), lambda state: (
|
||||
state.has_all({grapple, ice_dagger, fire_wand}, player) and has_ability(icebolt, state, world)))
|
||||
state.has_all((grapple, ice_dagger, fire_wand), player) and has_ability(icebolt, state, world)))
|
||||
|
||||
# Dark Tomb
|
||||
# added to make combat logic smoother
|
||||
@@ -1669,11 +1663,11 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
|
||||
# Frog's Domain
|
||||
set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
lambda state: state.has_any((grapple, laurels), player))
|
||||
set_rule(world.get_location("Frog's Domain - Grapple Above Hot Tub"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
lambda state: state.has_any((grapple, laurels), player))
|
||||
set_rule(world.get_location("Frog's Domain - Escape Chest"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
lambda state: state.has_any((grapple, laurels), player))
|
||||
|
||||
# Library Lab
|
||||
set_rule(world.get_location("Library Lab - Page 1"),
|
||||
@@ -1695,7 +1689,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
# Beneath the Vault
|
||||
set_rule(world.get_location("Beneath the Fortress - Bridge"),
|
||||
lambda state: has_lantern(state, world) and
|
||||
(has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player)))
|
||||
(has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player)))
|
||||
|
||||
# Quarry
|
||||
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),
|
||||
@@ -1706,9 +1700,10 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"),
|
||||
lambda state: has_sword(state, player) or (state.has(fire_wand, player)
|
||||
and (state.has(laurels, player)
|
||||
or world.options.entrance_rando)))
|
||||
or options.entrance_rando)))
|
||||
set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"),
|
||||
lambda state: has_sword(state, player) and has_ability(prayer, state, world))
|
||||
lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option
|
||||
else has_sword(state, player) and has_ability(prayer, state, world))
|
||||
|
||||
# Bosses
|
||||
set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"),
|
||||
@@ -1750,34 +1745,36 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
lambda state: state.has(laurels, player))
|
||||
|
||||
# Events
|
||||
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)))
|
||||
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"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Upper and Central Fortress Exterior Fuses"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Beneath the Vault Fuse"),
|
||||
lambda state: state.has("Activate South and West Fortress Exterior Fuses", player))
|
||||
set_rule(world.get_location("Eastern Vault West Fuses"),
|
||||
lambda state: state.has("Activate Beneath the Vault Fuse", player))
|
||||
set_rule(world.get_location("Eastern Vault East Fuse"),
|
||||
lambda state: state.has_all({"Activate Upper and Central Fortress Exterior Fuses",
|
||||
"Activate South and West Fortress Exterior Fuses"}, player))
|
||||
set_rule(world.get_location("Quarry Connector Fuse"),
|
||||
lambda state: has_ability(prayer, state, world) and state.has(grapple, player))
|
||||
set_rule(world.get_location("Quarry Fuse"),
|
||||
lambda state: state.has("Activate Quarry Connector Fuse", player))
|
||||
set_rule(world.get_location("Ziggurat Fuse"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("West Garden Fuse"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Library Fuse"),
|
||||
lambda state: has_ability(prayer, state, world) and has_ladder("Ladders in Library", state, world))
|
||||
if not world.options.hexagon_quest:
|
||||
if not bells_option:
|
||||
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:
|
||||
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"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Upper and Central Fortress Exterior Fuses"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Beneath the Vault Fuse"),
|
||||
lambda state: state.has("Activate South and West Fortress Exterior Fuses", player))
|
||||
set_rule(world.get_location("Eastern Vault West Fuses"),
|
||||
lambda state: state.has("Activate Beneath the Vault Fuse", player))
|
||||
set_rule(world.get_location("Eastern Vault East Fuse"),
|
||||
lambda state: state.has_all(("Activate Upper and Central Fortress Exterior Fuses",
|
||||
"Activate South and West Fortress Exterior Fuses"), player))
|
||||
set_rule(world.get_location("Quarry Connector Fuse"),
|
||||
lambda state: has_ability(prayer, state, world) and state.has(grapple, player))
|
||||
set_rule(world.get_location("Quarry Fuse"),
|
||||
lambda state: state.has("Activate Quarry Connector Fuse", player))
|
||||
set_rule(world.get_location("Ziggurat Fuse"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("West Garden Fuse"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Library Fuse"),
|
||||
lambda state: has_ability(prayer, state, world) and has_ladder("Ladders in Library", state, world))
|
||||
if not options.hexagon_quest:
|
||||
set_rule(world.get_location("Place Questagons"),
|
||||
lambda state: state.has_all((red_hexagon, blue_hexagon, green_hexagon), player))
|
||||
|
||||
@@ -1868,7 +1865,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
|
||||
# laurels past the enemies, then use the wand or gun to take care of the fairies that chased you
|
||||
add_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"),
|
||||
lambda state: state.has_any({fire_wand, "Gun"}, player))
|
||||
lambda state: state.has_any((fire_wand, gun), player))
|
||||
combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Faeries", "West Garden")
|
||||
combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Save Point", "West Garden")
|
||||
combat_logic_to_loc("West Garden - [West Highlands] Upper Left Walkway", "West Garden")
|
||||
@@ -1880,13 +1877,14 @@ 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
|
||||
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")
|
||||
if not fuses_option:
|
||||
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")
|
||||
|
||||
# if you come in from the left, you only need to fight small crabs
|
||||
add_rule(world.get_location("Ruined Atoll - [South] Near Birds"),
|
||||
lambda state: has_melee(state, player) or state.has_any({laurels, "Gun"}, player))
|
||||
lambda state: has_melee(state, player) or state.has_any((laurels, gun), player))
|
||||
|
||||
# can get this one without fighting if you have laurels
|
||||
add_rule(world.get_location("Frog's Domain - Above Vault"),
|
||||
@@ -1898,8 +1896,21 @@ 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: has_ability(prayer, state, world)
|
||||
and has_combat_reqs("Rooted Ziggurat", state, player))
|
||||
lambda state: state.has(ziggurat_miniboss_fuse, player) if fuses_option
|
||||
else (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player)))
|
||||
|
||||
if fuses_option:
|
||||
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")
|
||||
combat_logic_to_loc("Fortress Courtyard - [Upper] Activate Fuse", "Eastern Vault Fortress")
|
||||
combat_logic_to_loc("Fortress Courtyard - [Central] Activate Fuse", "Eastern Vault Fortress")
|
||||
combat_logic_to_loc("Eastern Vault Fortress - [Candle Room] Activate Fuse", "Eastern Vault Fortress")
|
||||
combat_logic_to_loc("Eastern Vault Fortress - [Left of Door] Activate Fuse", "Eastern Vault Fortress")
|
||||
combat_logic_to_loc("Eastern Vault Fortress - [Right of Door] Activate Fuse", "Eastern Vault Fortress")
|
||||
combat_logic_to_loc("Ruined Atoll - [Northwest] Activate Fuse", "Ruined Atoll")
|
||||
combat_logic_to_loc("Ruined Atoll - [Southwest] Activate Fuse", "Ruined Atoll")
|
||||
combat_logic_to_loc("Swamp - [Central] Activate Fuse", "Swamp")
|
||||
|
||||
# replace the sword rule with this one
|
||||
combat_logic_to_loc("Swamp - [South Graveyard] 4 Orange Skulls", "Swamp", set_instead=True)
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
from typing import Dict, List, Set, Tuple, TYPE_CHECKING
|
||||
from copy import deepcopy
|
||||
from random import Random
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import Region, ItemClassification, Item, Location
|
||||
from .locations import all_locations
|
||||
from Options import PlandoConnection
|
||||
|
||||
from .breakables import create_breakable_exclusive_regions, set_breakable_location_rules
|
||||
from .er_data import (Portal, portal_mapping, traversal_requirements, DeadEnd, Direction, RegionInfo,
|
||||
get_portal_outlet_region)
|
||||
from .er_rules import set_er_region_rules
|
||||
from .breakables import create_breakable_exclusive_regions, set_breakable_location_rules
|
||||
from Options import PlandoConnection
|
||||
from .locations import all_locations
|
||||
from .options import EntranceRando, EntranceLayout
|
||||
from random import Random
|
||||
from copy import deepcopy
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
@@ -22,8 +24,8 @@ class TunicERLocation(Location):
|
||||
game: str = "TUNIC"
|
||||
|
||||
|
||||
def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||
regions: Dict[str, Region] = {}
|
||||
def create_er_regions(world: "TunicWorld") -> dict[Portal, Portal]:
|
||||
regions: dict[str, Region] = {}
|
||||
world.used_shop_numbers = set()
|
||||
|
||||
for region_name, region_data in world.er_regions.items():
|
||||
@@ -83,7 +85,7 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||
|
||||
|
||||
# keys are event names, values are event regions
|
||||
tunic_events: Dict[str, str] = {
|
||||
tunic_events: dict[str, str] = {
|
||||
"Eastern Bell": "Forest Belltower Upper",
|
||||
"Western Bell": "Overworld Belltower at Bell",
|
||||
"Furnace Fuse": "Furnace Fuse",
|
||||
@@ -101,7 +103,7 @@ tunic_events: Dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None:
|
||||
def place_event_items(world: "TunicWorld", regions: dict[str, Region]) -> None:
|
||||
for event_name, region_name in tunic_events.items():
|
||||
region = regions[region_name]
|
||||
location = TunicERLocation(world.player, event_name, None, region)
|
||||
@@ -111,9 +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
|
||||
location.place_locked_item(
|
||||
TunicERItem("Ring " + event_name, ItemClassification.progression, None, world.player))
|
||||
else:
|
||||
elif event_name.endswith("Fuse") or event_name.endswith("Fuses"):
|
||||
# if world.options.shuffle_fuses:
|
||||
# continue
|
||||
location.place_locked_item(
|
||||
TunicERItem("Activate " + event_name, ItemClassification.progression, None, world.player))
|
||||
region.locations.append(location)
|
||||
@@ -135,7 +141,7 @@ def get_shop_num(world: "TunicWorld") -> int:
|
||||
|
||||
# all shops are the same shop. however, you cannot get to all shops from the same shop entrance.
|
||||
# so, we need a bunch of shop regions that connect to the actual shop, but the actual shop cannot connect back
|
||||
def create_shop_region(world: "TunicWorld", regions: Dict[str, Region], portal_num) -> None:
|
||||
def create_shop_region(world: "TunicWorld", regions: dict[str, Region], portal_num) -> None:
|
||||
new_shop_name = f"Shop {portal_num}"
|
||||
world.er_regions[new_shop_name] = RegionInfo("Shop", dead_end=DeadEnd.all_cats)
|
||||
new_shop_region = Region(new_shop_name, world.player, world.multiworld)
|
||||
@@ -144,8 +150,8 @@ def create_shop_region(world: "TunicWorld", regions: Dict[str, Region], portal_n
|
||||
|
||||
|
||||
# for non-ER that uses the ER rules, we create a vanilla set of portal pairs
|
||||
def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]:
|
||||
portal_pairs: Dict[Portal, Portal] = {}
|
||||
def vanilla_portals(world: "TunicWorld", regions: dict[str, Region]) -> dict[Portal, Portal]:
|
||||
portal_pairs: dict[Portal, Portal] = {}
|
||||
# we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here
|
||||
portal_map = [portal for portal in portal_mapping if portal.name not in
|
||||
["Ziggurat Lower Falling Entrance", "Purgatory Bottom Exit", "Purgatory Top Exit"]]
|
||||
@@ -182,10 +188,10 @@ def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Por
|
||||
# repeat this phase until all regions are reachable
|
||||
# second phase: randomly pair dead ends to random two_plus
|
||||
# third phase: randomly pair the remaining two_plus to each other
|
||||
def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]:
|
||||
portal_pairs: Dict[Portal, Portal] = {}
|
||||
dead_ends: List[Portal] = []
|
||||
two_plus: List[Portal] = []
|
||||
def pair_portals(world: "TunicWorld", regions: dict[str, Region]) -> dict[Portal, Portal]:
|
||||
portal_pairs: dict[Portal, Portal] = {}
|
||||
dead_ends: list[Portal] = []
|
||||
two_plus: list[Portal] = []
|
||||
player_name = world.player_name
|
||||
portal_map = portal_mapping.copy()
|
||||
laurels_zips = world.options.laurels_zips.value
|
||||
@@ -194,6 +200,10 @@ 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
|
||||
traversal_reqs = deepcopy(traversal_requirements)
|
||||
has_laurels = True
|
||||
waterfall_plando = False
|
||||
@@ -207,7 +217,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
entrance_layout = seed_group["entrance_layout"]
|
||||
laurels_location = "10_fairies" if seed_group["laurels_at_10_fairies"] is True else False
|
||||
|
||||
logic_tricks: Tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage)
|
||||
logic_tricks: tuple[bool, int, int] = (laurels_zips, ice_grappling, ladder_storage)
|
||||
|
||||
# marking that you don't immediately have laurels
|
||||
if laurels_location == "10_fairies" and not world.using_ut:
|
||||
@@ -215,8 +225,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
|
||||
# for the direction pairs option with decoupled off
|
||||
# tracks how many portals are in each direction in each list
|
||||
two_plus_direction_tracker: Dict[int, int] = {direction: 0 for direction in range(8)}
|
||||
dead_end_direction_tracker: Dict[int, int] = {direction: 0 for direction in range(8)}
|
||||
two_plus_direction_tracker: dict[int, int] = {direction: 0 for direction in range(8)}
|
||||
dead_end_direction_tracker: dict[int, int] = {direction: 0 for direction in range(8)}
|
||||
|
||||
# for ensuring we have enough entrances in directions left that we don't leave dead ends without any
|
||||
def too_few_portals_for_direction_pairs(direction: int, offset: int) -> bool:
|
||||
@@ -226,10 +236,6 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
return False
|
||||
return True
|
||||
|
||||
# If using Universal Tracker, restore portal_map. Could be cleaner, but it does not matter for UT even a little bit
|
||||
if world.using_ut:
|
||||
portal_map = portal_mapping.copy()
|
||||
|
||||
# create separate lists for dead ends and non-dead ends
|
||||
for portal in portal_map:
|
||||
dead_end_status = world.er_regions[portal.region].dead_end
|
||||
@@ -291,11 +297,12 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
dead_ends.append(shop_portal)
|
||||
dead_end_direction_tracker[shop_portal.direction] += 1
|
||||
|
||||
connected_regions: Set[str] = set()
|
||||
connected_regions: set[str] = set()
|
||||
# make better start region stuff when/if implementing random start
|
||||
start_region = "Overworld"
|
||||
connected_regions.add(start_region)
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks,
|
||||
shuffle_fuses, shuffle_bells)
|
||||
|
||||
if world.options.entrance_rando.value in EntranceRando.options.values():
|
||||
plando_connections = world.options.plando_connections.value
|
||||
@@ -371,8 +378,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
else:
|
||||
modified_plando_connections = plando_connections
|
||||
|
||||
connected_shop_portal1s: Set[int] = set()
|
||||
connected_shop_portal2s: Set[int] = set()
|
||||
connected_shop_portal1s: set[int] = set()
|
||||
connected_shop_portal2s: set[int] = set()
|
||||
for connection in modified_plando_connections:
|
||||
p_entrance = connection.entrance
|
||||
p_exit = connection.exit
|
||||
@@ -419,7 +426,15 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
break
|
||||
else:
|
||||
if p_entrance.startswith("Shop Portal "):
|
||||
portal_num = int(p_entrance.split("Shop Portal ")[-1])
|
||||
try:
|
||||
portal_num = int(p_entrance.split("Shop Portal ")[-1])
|
||||
except ValueError:
|
||||
if "Previous Region" in p_entrance:
|
||||
raise Exception("TUNIC: APWorld used for generation is incompatible with newer APWorld. "
|
||||
"Please use the APWorld from Archipelago 0.6.1 instead.")
|
||||
else:
|
||||
raise Exception("TUNIC: Unknown error occurred in UT entrance setup, please contact "
|
||||
"the TUNIC APWorld devs.")
|
||||
# shops 1-6 are south, 7 and 8 are east, and after that it just breaks direction pairs
|
||||
if portal_num <= 6:
|
||||
pdir = Direction.south
|
||||
@@ -452,7 +467,15 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
else:
|
||||
if not portal2:
|
||||
if p_exit.startswith("Shop Portal "):
|
||||
portal_num = int(p_exit.split("Shop Portal ")[-1])
|
||||
try:
|
||||
portal_num = int(p_exit.split("Shop Portal ")[-1])
|
||||
except ValueError:
|
||||
if "Previous Region" in p_exit:
|
||||
raise Exception("TUNIC: APWorld used for generation is incompatible with newer APWorld. "
|
||||
"Please use the APWorld from Archipelago 0.6.1 instead.")
|
||||
else:
|
||||
raise Exception("TUNIC: Unknown error occurred in UT entrance setup, please contact "
|
||||
"the TUNIC APWorld devs.")
|
||||
if portal_num <= 6:
|
||||
pdir = Direction.south
|
||||
elif portal_num in [7, 8]:
|
||||
@@ -510,13 +533,15 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
dead_end_direction_tracker[portal1.direction] -= 1
|
||||
else:
|
||||
two_plus_direction_tracker[portal1.direction] -= 1
|
||||
|
||||
if portal2_dead_end:
|
||||
dead_end_direction_tracker[portal2.direction] -= 1
|
||||
else:
|
||||
two_plus_direction_tracker[portal2.direction] -= 1
|
||||
|
||||
# if we have plando connections, our connected regions may change somewhat
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks,
|
||||
shuffle_fuses, shuffle_bells)
|
||||
|
||||
# if there are an odd number of shops after plando, add another one, except in decoupled where it doesn't matter
|
||||
if not decoupled and len(world.used_shop_numbers) % 2 == 1:
|
||||
@@ -599,7 +624,6 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
connected_regions = backup_connected_regions.copy()
|
||||
rare_failure_count += 1
|
||||
fail_count = 0
|
||||
|
||||
if rare_failure_count > 100:
|
||||
raise Exception(f"Failed to pair regions due to rare pairing issues for {player_name}. "
|
||||
f"Unconnected regions: {non_dead_end_regions - connected_regions}.\n"
|
||||
@@ -633,7 +657,9 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
if waterfall_plando:
|
||||
cr = connected_regions.copy()
|
||||
cr.add(portal.region)
|
||||
if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks):
|
||||
if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels,
|
||||
logic_tricks, shuffle_fuses,
|
||||
shuffle_bells):
|
||||
continue
|
||||
# if not waterfall_plando, then we just want to pair secret gathering place now
|
||||
elif portal.region != "Secret Gathering Place":
|
||||
@@ -682,8 +708,8 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
# once we have both portals, connect them and add the new region(s) to connected_regions
|
||||
if not has_laurels and "Secret Gathering Place" in connected_regions:
|
||||
has_laurels = True
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks)
|
||||
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic_tricks,
|
||||
shuffle_fuses, shuffle_bells)
|
||||
portal_pairs[portal1] = portal2
|
||||
two_plus_direction_tracker[portal1.direction] -= 1
|
||||
two_plus_direction_tracker[portal2.direction] -= 1
|
||||
@@ -745,7 +771,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
||||
|
||||
|
||||
# loop through our list of paired portals and make two-way connections
|
||||
def create_randomized_entrances(world: "TunicWorld", portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None:
|
||||
def create_randomized_entrances(world: "TunicWorld", portal_pairs: dict[Portal, Portal], regions: dict[str, Region]) -> None:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
# connect to the outlet region if there is one, if not connect to the actual region
|
||||
regions[portal1.region].connect(
|
||||
@@ -757,8 +783,9 @@ def create_randomized_entrances(world: "TunicWorld", portal_pairs: Dict[Portal,
|
||||
name=portal2.name)
|
||||
|
||||
|
||||
def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[str, Dict[str, List[List[str]]]],
|
||||
has_laurels: bool, logic: Tuple[bool, int, int]) -> Set[str]:
|
||||
def update_reachable_regions(connected_regions: set[str], traversal_reqs: dict[str, dict[str, list[list[str]]]],
|
||||
has_laurels: bool, logic: tuple[bool, int, int], shuffle_fuses: bool,
|
||||
shuffle_bells: bool) -> set[str]:
|
||||
zips, ice_grapples, ls = logic
|
||||
# starting count, so we can run it again if this changes
|
||||
region_count = len(connected_regions)
|
||||
@@ -790,6 +817,12 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s
|
||||
break
|
||||
elif req not in connected_regions:
|
||||
break
|
||||
elif req == "Fuse Shuffle":
|
||||
if not shuffle_fuses:
|
||||
break
|
||||
elif req == "Bell Shuffle":
|
||||
if not shuffle_bells:
|
||||
break
|
||||
else:
|
||||
met_traversal_reqs = True
|
||||
break
|
||||
@@ -798,13 +831,14 @@ def update_reachable_regions(connected_regions: Set[str], traversal_reqs: Dict[s
|
||||
|
||||
# if the length of connected_regions changed, we got new regions, so we want to check those new origins
|
||||
if region_count != len(connected_regions):
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic)
|
||||
connected_regions = update_reachable_regions(connected_regions, traversal_reqs, has_laurels, logic,
|
||||
shuffle_fuses, shuffle_bells)
|
||||
|
||||
return connected_regions
|
||||
|
||||
|
||||
# which directions are opposites
|
||||
direction_pairs: Dict[int, int] = {
|
||||
direction_pairs: dict[int, int] = {
|
||||
Direction.north: Direction.south,
|
||||
Direction.south: Direction.north,
|
||||
Direction.east: Direction.west,
|
||||
@@ -848,9 +882,9 @@ def verify_plando_directions(connection: PlandoConnection) -> bool:
|
||||
|
||||
|
||||
# sort the portal dict by the name of the first portal, referring to the portal order in the master portal list
|
||||
def sort_portals(portal_pairs: Dict[Portal, Portal], world: "TunicWorld") -> Dict[str, str]:
|
||||
sorted_pairs: Dict[str, str] = {}
|
||||
reference_list: List[str] = [portal.name for portal in portal_mapping]
|
||||
def sort_portals(portal_pairs: dict[Portal, Portal], world: "TunicWorld") -> dict[str, str]:
|
||||
sorted_pairs: dict[str, str] = {}
|
||||
reference_list: list[str] = [portal.name for portal in portal_mapping]
|
||||
|
||||
# due to plando, there can be a variable number of shops
|
||||
largest_shop_number = max(world.used_shop_numbers)
|
||||
|
||||
30
worlds/tunic/fuses.py
Normal file
30
worlds/tunic/fuses.py
Normal file
@@ -0,0 +1,30 @@
|
||||
from .constants import *
|
||||
|
||||
# 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],
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
from typing import Dict, NamedTuple, Optional, TYPE_CHECKING, Set
|
||||
from typing import NamedTuple, TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .rules import has_sword, has_melee
|
||||
|
||||
from .constants import base_id
|
||||
from .logic_helpers import has_sword, has_melee
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
|
||||
@@ -10,12 +13,12 @@ if TYPE_CHECKING:
|
||||
class TunicLocationData(NamedTuple):
|
||||
region: str
|
||||
er_region: str # entrance rando region
|
||||
location_group: Optional[str] = None
|
||||
location_group: str | None = None
|
||||
|
||||
|
||||
location_base_id = 509342400
|
||||
|
||||
grass_location_table: Dict[str, TunicLocationData] = {
|
||||
# todo: remove region, make all of these regions append grass to the name
|
||||
# and then set the rules on the region entrances instead of the locations directly
|
||||
grass_location_table: dict[str, TunicLocationData] = {
|
||||
"Overworld - Overworld Grass (576) (7.0, 4.0, -223.0)": TunicLocationData("Overworld", "Overworld"),
|
||||
"Overworld - Overworld Grass (572) (6.0, 4.0, -223.0)": TunicLocationData("Overworld", "Overworld"),
|
||||
"Overworld - Overworld Grass (574) (7.0, 4.0, -224.0)": TunicLocationData("Overworld", "Overworld"),
|
||||
@@ -7763,9 +7766,10 @@ excluded_grass_locations = {
|
||||
"Overworld - East Overworld Bush (64) (56.0, 44.0, -107.0)",
|
||||
}
|
||||
|
||||
grass_location_name_to_id: Dict[str, int] = {name: location_base_id + 302 + index for index, name in enumerate(grass_location_table)}
|
||||
grass_base_id = base_id + 302
|
||||
grass_location_name_to_id: dict[str, int] = {name: grass_base_id + index for index, name in enumerate(grass_location_table)}
|
||||
|
||||
grass_location_name_groups: Dict[str, Set[str]] = {}
|
||||
grass_location_name_groups: dict[str, set[str]] = {}
|
||||
for loc_name, loc_data in grass_location_table.items():
|
||||
area_name = loc_name.split(" - ", 1)[0]
|
||||
# adding it to the normal location group and a grass-only one
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
from itertools import groupby
|
||||
from typing import Dict, List, Set, NamedTuple, Optional
|
||||
from typing import NamedTuple
|
||||
|
||||
from BaseClasses import ItemClassification as IC
|
||||
|
||||
from .constants import base_id
|
||||
|
||||
|
||||
class TunicItemData(NamedTuple):
|
||||
classification: IC
|
||||
@@ -9,12 +12,10 @@ class TunicItemData(NamedTuple):
|
||||
item_id_offset: int
|
||||
item_group: str = ""
|
||||
# classification if combat logic is on
|
||||
combat_ic: Optional[IC] = None
|
||||
combat_ic: None | IC = None
|
||||
|
||||
|
||||
item_base_id = 509342400
|
||||
|
||||
item_table: Dict[str, TunicItemData] = {
|
||||
item_table: dict[str, TunicItemData] = {
|
||||
"Firecracker x2": TunicItemData(IC.filler, 3, 0, "Bombs"),
|
||||
"Firecracker x3": TunicItemData(IC.filler, 3, 1, "Bombs"),
|
||||
"Firecracker x4": TunicItemData(IC.filler, 3, 2, "Bombs"),
|
||||
@@ -175,7 +176,7 @@ item_table: Dict[str, TunicItemData] = {
|
||||
}
|
||||
|
||||
# items to be replaced by fool traps
|
||||
fool_tiers: List[List[str]] = [
|
||||
fool_tiers: list[list[str]] = [
|
||||
[],
|
||||
["Money x1", "Money x10", "Money x15", "Money x16"],
|
||||
["Money x1", "Money x10", "Money x15", "Money x16", "Money x20"],
|
||||
@@ -214,25 +215,25 @@ slot_data_item_names = [
|
||||
"Gold Questagon",
|
||||
]
|
||||
|
||||
combat_items: List[str] = [name for name, data in item_table.items()
|
||||
combat_items: list[str] = [name for name, data in item_table.items()
|
||||
if data.combat_ic and IC.progression in data.combat_ic]
|
||||
combat_items.extend(["Stick", "Sword", "Sword Upgrade", "Magic Wand", "Hero's Laurels", "Gun"])
|
||||
|
||||
item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()}
|
||||
item_name_to_id: dict[str, int] = {name: base_id + data.item_id_offset for name, data in item_table.items()}
|
||||
|
||||
filler_items: List[str] = [name for name, data in item_table.items() if data.classification == IC.filler and name != "Grass"]
|
||||
filler_items: list[str] = [name for name, data in item_table.items() if data.classification == IC.filler and name != "Grass"]
|
||||
|
||||
|
||||
def get_item_group(item_name: str) -> str:
|
||||
return item_table[item_name].item_group
|
||||
|
||||
|
||||
item_name_groups: Dict[str, Set[str]] = {
|
||||
item_name_groups: dict[str, set[str]] = {
|
||||
group: set(item_names) for group, item_names in groupby(sorted(item_table, key=get_item_group), get_item_group) if group != ""
|
||||
}
|
||||
|
||||
# extra groups for the purpose of aliasing items
|
||||
extra_groups: Dict[str, Set[str]] = {
|
||||
extra_groups: dict[str, set[str]] = {
|
||||
"Laurels": {"Hero's Laurels"},
|
||||
"Orb": {"Magic Orb"},
|
||||
"Dagger": {"Magic Dagger"},
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from typing import Dict, List, Set, NamedTuple, Optional
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
# ladders in overworld, since it is the most complex area for ladder storage
|
||||
class OWLadderInfo(NamedTuple):
|
||||
ladders: Set[str] # ladders where the top or bottom is at the same elevation
|
||||
portals: List[str] # portals at the same elevation, only those without doors
|
||||
regions: List[str] # regions where a melee enemy can hit you out of ladder storage
|
||||
ladders: set[str] # ladders where the top or bottom is at the same elevation
|
||||
portals: list[str] # portals at the same elevation, only those without doors
|
||||
regions: list[str] # regions where a melee enemy can hit you out of ladder storage
|
||||
|
||||
|
||||
# groups for ladders at the same elevation, for use in determing whether you can ls to entrances in diff rulesets
|
||||
ow_ladder_groups: Dict[str, OWLadderInfo] = {
|
||||
ow_ladder_groups: dict[str, OWLadderInfo] = {
|
||||
# lowest elevation
|
||||
"LS Elev 0": OWLadderInfo({"Ladders in Overworld Town", "Ladder to Ruined Atoll", "Ladder to Swamp"},
|
||||
["Swamp Redux 2_conduit", "Overworld Cave_", "Atoll Redux_lower", "Maze Room_",
|
||||
@@ -49,7 +49,7 @@ ow_ladder_groups: Dict[str, OWLadderInfo] = {
|
||||
|
||||
# ladders accessible within different regions of overworld, only those that are relevant
|
||||
# other scenes will just have them hardcoded since this type of structure is not necessary there
|
||||
region_ladders: Dict[str, Set[str]] = {
|
||||
region_ladders: dict[str, set[str]] = {
|
||||
"Overworld": {"Ladders near Weathervane", "Ladders near Overworld Checkpoint", "Ladders near Dark Tomb",
|
||||
"Ladders in Overworld Town", "Ladder to Swamp", "Ladders in Well"},
|
||||
"Overworld Beach": {"Ladder to Ruined Atoll"},
|
||||
@@ -63,11 +63,11 @@ region_ladders: Dict[str, Set[str]] = {
|
||||
class LadderInfo(NamedTuple):
|
||||
origin: str # origin region
|
||||
destination: str # destination portal
|
||||
ladders_req: Optional[str] = None # ladders required to do this
|
||||
ladders_req: str | None = None # ladders required to do this
|
||||
dest_is_region: bool = False # whether it is a region that you are going to
|
||||
|
||||
|
||||
easy_ls: List[LadderInfo] = [
|
||||
easy_ls: list[LadderInfo] = [
|
||||
# In the furnace
|
||||
# Furnace ladder to the fuse entrance
|
||||
LadderInfo("Furnace Ladder Area", "Furnace, Overworld Redux_gyro_upper_north"),
|
||||
@@ -128,7 +128,7 @@ easy_ls: List[LadderInfo] = [
|
||||
]
|
||||
|
||||
# if we can gain elevation or get knocked down, add the harder ones
|
||||
medium_ls: List[LadderInfo] = [
|
||||
medium_ls: list[LadderInfo] = [
|
||||
# region-destination versions of easy ls spots
|
||||
LadderInfo("East Forest", "East Forest Dance Fox Spot", dest_is_region=True),
|
||||
# fortress courtyard knockdowns are never logically relevant, the fuse requires upper
|
||||
@@ -169,7 +169,7 @@ medium_ls: List[LadderInfo] = [
|
||||
LadderInfo("Back of Swamp", "Swamp Redux 2, Overworld Redux_wall"),
|
||||
]
|
||||
|
||||
hard_ls: List[LadderInfo] = [
|
||||
hard_ls: list[LadderInfo] = [
|
||||
# lower ladder, go into the waterfall then above the bonfire, up a ramp, then through the right wall
|
||||
LadderInfo("Beneath the Well Front", "Sewer, Sewer_Boss_", "Ladders in Well"),
|
||||
LadderInfo("Beneath the Well Front", "Sewer, Overworld Redux_west_aqueduct", "Ladders in Well"),
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
from typing import Dict, NamedTuple, Set, Optional, List
|
||||
from .grass import grass_location_table
|
||||
from typing import NamedTuple
|
||||
|
||||
# from .bells import bell_location_table
|
||||
from .breakables import breakable_location_table
|
||||
from .constants import base_id
|
||||
# from .fuses import fuse_location_table
|
||||
from .grass import grass_location_table
|
||||
|
||||
|
||||
class TunicLocationData(NamedTuple):
|
||||
region: str
|
||||
er_region: str # entrance rando region
|
||||
location_group: Optional[str] = None
|
||||
location_group: str | None = None
|
||||
|
||||
|
||||
location_base_id = 509342400
|
||||
|
||||
location_table: Dict[str, TunicLocationData] = {
|
||||
location_table: dict[str, TunicLocationData] = {
|
||||
"Beneath the Well - [Powered Secret Room] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
|
||||
"Beneath the Well - [Entryway] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
|
||||
"Beneath the Well - [Third Room] Beneath Platform Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
|
||||
@@ -243,7 +245,7 @@ location_table: Dict[str, TunicLocationData] = {
|
||||
"Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Entry"),
|
||||
"Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Entry"),
|
||||
"Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"),
|
||||
"Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"),
|
||||
"Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Miniboss Platform"),
|
||||
"Rooted Ziggurat Lower - Guarded By Double Turrets": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
|
||||
"Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"),
|
||||
"Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
|
||||
@@ -307,7 +309,7 @@ location_table: Dict[str, TunicLocationData] = {
|
||||
"West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden after Terry"),
|
||||
"West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden South Checkpoint"),
|
||||
"West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden South Checkpoint"),
|
||||
"West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden before Boss"),
|
||||
"West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden South Checkpoint"),
|
||||
"West Garden - [Central Highlands] After Garden Knight": TunicLocationData("Overworld", "West Garden after Boss", location_group="Bosses"),
|
||||
"West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden South Checkpoint"),
|
||||
"West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"),
|
||||
@@ -316,19 +318,21 @@ location_table: Dict[str, TunicLocationData] = {
|
||||
"Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"),
|
||||
}
|
||||
|
||||
hexagon_locations: Dict[str, str] = {
|
||||
hexagon_locations: dict[str, str] = {
|
||||
"Red Questagon": "Fortress Arena - Siege Engine/Vault Key Pickup",
|
||||
"Green Questagon": "Librarian - Hexagon Green",
|
||||
"Blue Questagon": "Rooted Ziggurat Lower - Hexagon Blue",
|
||||
}
|
||||
|
||||
standard_location_name_to_id: Dict[str, int] = {name: location_base_id + index for index, name in enumerate(location_table)}
|
||||
standard_location_name_to_id: dict[str, int] = {name: base_id + index for index, name in enumerate(location_table)}
|
||||
|
||||
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)
|
||||
|
||||
location_name_groups: Dict[str, Set[str]] = {}
|
||||
location_name_groups: dict[str, set[str]] = {}
|
||||
for loc_name, loc_data in location_table.items():
|
||||
loc_group_name = loc_name.split(" - ", 1)[0]
|
||||
location_name_groups.setdefault(loc_group_name, set()).add(loc_name)
|
||||
|
||||
98
worlds/tunic/logic_helpers.py
Normal file
98
worlds/tunic/logic_helpers.py
Normal file
@@ -0,0 +1,98 @@
|
||||
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:
|
||||
from . import TunicWorld
|
||||
|
||||
|
||||
def randomize_ability_unlocks(world: "TunicWorld") -> dict[str, int]:
|
||||
options = world.options
|
||||
|
||||
abilities = [prayer, holy_cross, icebolt]
|
||||
ability_requirement = [1, 1, 1]
|
||||
world.random.shuffle(abilities)
|
||||
|
||||
if options.hexagon_quest.value and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
|
||||
hexagon_goal = options.hexagon_goal.value
|
||||
# Set ability unlocks to 25, 50, and 75% of goal amount
|
||||
ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4]
|
||||
if any(req == 0 for req in ability_requirement):
|
||||
ability_requirement = [1, 2, 3]
|
||||
|
||||
return dict(zip(abilities, ability_requirement))
|
||||
|
||||
|
||||
def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bool:
|
||||
options = world.options
|
||||
ability_unlocks = world.ability_unlocks
|
||||
if not options.ability_shuffling:
|
||||
return True
|
||||
if options.hexagon_quest and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
|
||||
return state.has(gold_hexagon, world.player, ability_unlocks[ability])
|
||||
return state.has(ability, world.player)
|
||||
|
||||
|
||||
# a check to see if you can whack things in melee at all
|
||||
def has_melee(state: CollectionState, player: int) -> bool:
|
||||
return state.has_any(("Stick", "Sword", "Sword Upgrade"), player)
|
||||
|
||||
|
||||
def has_sword(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Sword", player) or state.has("Sword Upgrade", player, 2)
|
||||
|
||||
|
||||
def laurels_zip(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
return world.options.laurels_zips and state.has(laurels, world.player)
|
||||
|
||||
|
||||
def has_ice_grapple_logic(long_range: bool, difficulty: IceGrappling, state: CollectionState, world: "TunicWorld") -> bool:
|
||||
if world.options.ice_grappling < difficulty:
|
||||
return False
|
||||
if not long_range:
|
||||
return state.has_all((ice_dagger, grapple), world.player)
|
||||
else:
|
||||
return state.has_all((ice_dagger, fire_wand, grapple), world.player) and has_ability(icebolt, state, world)
|
||||
|
||||
|
||||
def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
if not world.options.ladder_storage:
|
||||
return False
|
||||
if world.options.ladder_storage_without_items:
|
||||
return True
|
||||
return has_melee(state, world.player) or state.has_any((grapple, shield), world.player)
|
||||
|
||||
|
||||
def has_mask(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
return world.options.maskless or state.has(mask, world.player)
|
||||
|
||||
|
||||
def has_lantern(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
return world.options.lanternless or state.has(lantern, world.player)
|
||||
|
||||
|
||||
def has_ladder(ladder: str, state: CollectionState, world: "TunicWorld") -> bool:
|
||||
return not world.options.shuffle_ladders or state.has(ladder, world.player)
|
||||
|
||||
|
||||
def can_shop(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
return has_sword(state, world.player) and state.can_reach_region("Shop", world.player)
|
||||
|
||||
|
||||
# for the ones that are not early bushes where ER can screw you over a bit
|
||||
def can_get_past_bushes(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
# add in glass cannon + stick for grass rando
|
||||
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)
|
||||
|
||||
return state.has(fuse_event, player)
|
||||
@@ -1,12 +1,13 @@
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict, Any, TYPE_CHECKING
|
||||
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
import logging
|
||||
from typing import Any, TYPE_CHECKING
|
||||
|
||||
from Options import (DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, TextChoice, PlandoConnections,
|
||||
PerGameCommonOptions, OptionGroup, Removed, Visibility, NamedRange)
|
||||
|
||||
from .er_data import portal_mapping
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
|
||||
@@ -145,17 +146,6 @@ class EntranceRando(TextChoice):
|
||||
default = 0
|
||||
|
||||
|
||||
class FixedShop(Toggle):
|
||||
"""
|
||||
This option has been superseded by the Entrance Layout option.
|
||||
If enabled, it will override the Entrance Layout option.
|
||||
This is kept to keep older yamls working, and will be removed at a later date.
|
||||
"""
|
||||
visibility = Visibility.none
|
||||
internal_name = "fixed_shop"
|
||||
display_name = "Fewer Shops in Entrance Rando"
|
||||
|
||||
|
||||
class EntranceLayout(Choice):
|
||||
"""
|
||||
Decide how the Entrance Randomizer chooses how to pair the entrances.
|
||||
@@ -219,8 +209,8 @@ class GrassRandomizer(Toggle):
|
||||
class LocalFill(NamedRange):
|
||||
"""
|
||||
Choose the percentage of your filler/trap items that will be kept local or distributed to other TUNIC players with this option enabled.
|
||||
This option defaults to 95% if you have Grass Randomizer enabled, 40% if you have Breakable Shuffle enabled, 96% if you have both, and 0% otherwise.
|
||||
If you have Grass Randomizer enabled, this option must be set to 95% or higher to avoid flooding the item pool. The host can remove this restriction by turning off the limit_grass_rando setting in host.yaml.
|
||||
This option defaults to 95% if you have Grass Randomizer enabled, and to 0% otherwise.
|
||||
This option ignores items placed in your local_items or non_local_items.
|
||||
This option does nothing in single player games.
|
||||
"""
|
||||
@@ -332,6 +322,14 @@ class LadderStorageWithoutItems(Toggle):
|
||||
display_name = "Ladder Storage without Items"
|
||||
|
||||
|
||||
class BreakableShuffle(Toggle):
|
||||
"""
|
||||
Turns approximately 250 breakable objects in the game into checks.
|
||||
"""
|
||||
internal_name = "breakable_shuffle"
|
||||
display_name = "Breakable Shuffle"
|
||||
|
||||
|
||||
class HiddenAllRandom(Toggle):
|
||||
"""
|
||||
Sets all options that can be random to random.
|
||||
@@ -342,36 +340,9 @@ class HiddenAllRandom(Toggle):
|
||||
visibility = Visibility.none
|
||||
|
||||
|
||||
class LogicRules(Choice):
|
||||
"""
|
||||
This option has been superseded by the individual trick options.
|
||||
If set to nmg, it will set Ice Grappling to medium and Laurels Zips on.
|
||||
If set to ur, it will do nmg as well as set Ladder Storage to medium.
|
||||
It is here to avoid breaking old yamls, and will be removed at a later date.
|
||||
"""
|
||||
visibility = Visibility.none
|
||||
internal_name = "logic_rules"
|
||||
display_name = "Logic Rules"
|
||||
option_restricted = 0
|
||||
option_no_major_glitches = 1
|
||||
alias_nmg = 1
|
||||
option_unrestricted = 2
|
||||
alias_ur = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class BreakableShuffle(Toggle):
|
||||
"""
|
||||
Turns approximately 250 breakable objects in the game into checks.
|
||||
"""
|
||||
internal_name = "breakable_shuffle"
|
||||
display_name = "Breakable Shuffle"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TunicOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
|
||||
sword_progression: SwordProgression
|
||||
start_with_sword: StartWithSword
|
||||
keys_behind_bosses: KeysBehindBosses
|
||||
@@ -386,6 +357,8 @@ class TunicOptions(PerGameCommonOptions):
|
||||
hexagon_quest_ability_type: HexagonQuestAbilityUnlockType
|
||||
|
||||
shuffle_ladders: ShuffleLadders
|
||||
# shuffle_fuses: ShuffleFuses
|
||||
# shuffle_bells: ShuffleBells
|
||||
grass_randomizer: GrassRandomizer
|
||||
breakable_shuffle: BreakableShuffle
|
||||
local_fill: LocalFill
|
||||
@@ -393,7 +366,6 @@ class TunicOptions(PerGameCommonOptions):
|
||||
entrance_rando: EntranceRando
|
||||
entrance_layout: EntranceLayout
|
||||
decoupled: Decoupled
|
||||
plando_connections: TunicPlandoConnections
|
||||
|
||||
combat_logic: CombatLogic
|
||||
lanternless: Lanternless
|
||||
@@ -402,11 +374,13 @@ class TunicOptions(PerGameCommonOptions):
|
||||
ice_grappling: IceGrappling
|
||||
ladder_storage: LadderStorage
|
||||
ladder_storage_without_items: LadderStorageWithoutItems
|
||||
|
||||
|
||||
plando_connections: TunicPlandoConnections
|
||||
|
||||
all_random: HiddenAllRandom
|
||||
|
||||
fixed_shop: FixedShop # will be removed at a later date
|
||||
logic_rules: Removed # fully removed in the direction pairs update
|
||||
fixed_shop: Removed
|
||||
logic_rules: Removed
|
||||
|
||||
|
||||
tunic_option_groups = [
|
||||
@@ -433,7 +407,7 @@ tunic_option_groups = [
|
||||
]),
|
||||
]
|
||||
|
||||
tunic_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
tunic_option_presets: dict[str, dict[str, Any]] = {
|
||||
"Sync": {
|
||||
"ability_shuffling": True,
|
||||
},
|
||||
@@ -460,14 +434,16 @@ tunic_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
|
||||
def check_options(world: "TunicWorld"):
|
||||
options = world.options
|
||||
if options.hexagon_quest and options.ability_shuffling and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
|
||||
if (options.hexagon_quest and options.ability_shuffling
|
||||
and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons):
|
||||
total_hexes = get_hexagons_in_pool(world)
|
||||
min_hexes = 3
|
||||
|
||||
if options.keys_behind_bosses:
|
||||
min_hexes = 15
|
||||
if total_hexes < min_hexes:
|
||||
logging.warning(f"TUNIC: Not enough Gold Hexagons in {world.player_name}'s item pool for Hexagon Ability Shuffle with the selected options. Ability Shuffle mode will be switched to Pages.")
|
||||
logging.warning(f"TUNIC: Not enough Gold Hexagons in {world.player_name}'s item pool for Hexagon Ability "
|
||||
"Shuffle with the selected options. Ability Shuffle mode will be switched to Pages.")
|
||||
options.hexagon_quest_ability_type.value = HexagonQuestAbilityUnlockType.option_pages
|
||||
|
||||
|
||||
@@ -475,4 +451,4 @@ def get_hexagons_in_pool(world: "TunicWorld"):
|
||||
# Calculate number of hexagons in item pool
|
||||
options = world.options
|
||||
return min(int((Decimal(100 + options.extra_hexagon_percentage) / 100 * options.hexagon_goal)
|
||||
.to_integral_value(rounding=ROUND_HALF_UP)), 100)
|
||||
.to_integral_value(rounding=ROUND_HALF_UP)), 100)
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
tunic_regions: dict[str, tuple[str]] = {
|
||||
"Menu": ("Overworld",),
|
||||
"Overworld": ("Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden",
|
||||
"Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp",
|
||||
"Spirit Arena"),
|
||||
"Overworld Holy Cross": tuple(),
|
||||
"East Forest": tuple(),
|
||||
"Dark Tomb": ("West Garden",),
|
||||
"Beneath the Well": tuple(),
|
||||
"West Garden": tuple(),
|
||||
"Ruined Atoll": ("Frog's Domain", "Library"),
|
||||
"Frog's Domain": tuple(),
|
||||
"Library": tuple(),
|
||||
"Eastern Vault Fortress": ("Beneath the Vault",),
|
||||
"Beneath the Vault": ("Eastern Vault Fortress",),
|
||||
"Quarry Back": ("Quarry", "Monastery"),
|
||||
"Quarry": ("Monastery", "Lower Quarry"),
|
||||
"Monastery": ("Monastery Back",),
|
||||
"Monastery Back": tuple(),
|
||||
"Lower Quarry": ("Rooted Ziggurat",),
|
||||
"Rooted Ziggurat": tuple(),
|
||||
"Swamp": ("Cathedral",),
|
||||
"Cathedral": tuple(),
|
||||
"Spirit Arena": tuple()
|
||||
}
|
||||
@@ -1,402 +0,0 @@
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
|
||||
from worlds.generic.Rules import set_rule, forbid_item, add_rule
|
||||
from BaseClasses import CollectionState
|
||||
from .options import LadderStorage, IceGrappling, HexagonQuestAbilityUnlockType
|
||||
if TYPE_CHECKING:
|
||||
from . import TunicWorld
|
||||
|
||||
laurels = "Hero's Laurels"
|
||||
grapple = "Magic Orb"
|
||||
ice_dagger = "Magic Dagger"
|
||||
fire_wand = "Magic Wand"
|
||||
gun = "Gun"
|
||||
lantern = "Lantern"
|
||||
fairies = "Fairy"
|
||||
coins = "Golden Coin"
|
||||
prayer = "Pages 24-25 (Prayer)"
|
||||
holy_cross = "Pages 42-43 (Holy Cross)"
|
||||
icebolt = "Pages 52-53 (Icebolt)"
|
||||
shield = "Shield"
|
||||
key = "Key"
|
||||
house_key = "Old House Key"
|
||||
vault_key = "Fortress Vault Key"
|
||||
mask = "Scavenger Mask"
|
||||
red_hexagon = "Red Questagon"
|
||||
green_hexagon = "Green Questagon"
|
||||
blue_hexagon = "Blue Questagon"
|
||||
gold_hexagon = "Gold Questagon"
|
||||
|
||||
# "Quarry - [East] Bombable Wall" is excluded from this list since it has slightly different rules
|
||||
bomb_walls = ["East Forest - Bombable Wall", "Eastern Vault Fortress - [East Wing] Bombable Wall",
|
||||
"Overworld - [Central] Bombable Wall", "Overworld - [Southwest] Bombable Wall Near Fountain",
|
||||
"Quarry - [West] Upper Area Bombable Wall", "Ruined Atoll - [Northwest] Bombable Wall"]
|
||||
|
||||
|
||||
def randomize_ability_unlocks(world: "TunicWorld") -> Dict[str, int]:
|
||||
random = world.random
|
||||
options = world.options
|
||||
|
||||
abilities = [prayer, holy_cross, icebolt]
|
||||
ability_requirement = [1, 1, 1]
|
||||
random.shuffle(abilities)
|
||||
|
||||
if options.hexagon_quest.value and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
|
||||
hexagon_goal = options.hexagon_goal.value
|
||||
# Set ability unlocks to 25, 50, and 75% of goal amount
|
||||
ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4]
|
||||
if any(req == 0 for req in ability_requirement):
|
||||
ability_requirement = [1, 2, 3]
|
||||
|
||||
return dict(zip(abilities, ability_requirement))
|
||||
|
||||
|
||||
def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bool:
|
||||
options = world.options
|
||||
ability_unlocks = world.ability_unlocks
|
||||
if not options.ability_shuffling:
|
||||
return True
|
||||
if options.hexagon_quest and options.hexagon_quest_ability_type == HexagonQuestAbilityUnlockType.option_hexagons:
|
||||
return state.has(gold_hexagon, world.player, ability_unlocks[ability])
|
||||
return state.has(ability, world.player)
|
||||
|
||||
|
||||
# a check to see if you can whack things in melee at all
|
||||
def has_melee(state: CollectionState, player: int) -> bool:
|
||||
return state.has_any({"Stick", "Sword", "Sword Upgrade"}, player)
|
||||
|
||||
|
||||
def has_sword(state: CollectionState, player: int) -> bool:
|
||||
return state.has("Sword", player) or state.has("Sword Upgrade", player, 2)
|
||||
|
||||
|
||||
def laurels_zip(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
return world.options.laurels_zips and state.has(laurels, world.player)
|
||||
|
||||
|
||||
def has_ice_grapple_logic(long_range: bool, difficulty: IceGrappling, state: CollectionState, world: "TunicWorld") -> bool:
|
||||
if world.options.ice_grappling < difficulty:
|
||||
return False
|
||||
if not long_range:
|
||||
return state.has_all({ice_dagger, grapple}, world.player)
|
||||
else:
|
||||
return state.has_all({ice_dagger, fire_wand, grapple}, world.player) and has_ability(icebolt, state, world)
|
||||
|
||||
|
||||
def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
if not world.options.ladder_storage:
|
||||
return False
|
||||
if world.options.ladder_storage_without_items:
|
||||
return True
|
||||
return has_melee(state, world.player) or state.has_any((grapple, shield), world.player)
|
||||
|
||||
|
||||
def has_mask(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
return world.options.maskless or state.has(mask, world.player)
|
||||
|
||||
|
||||
def has_lantern(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
return world.options.lanternless or state.has(lantern, world.player)
|
||||
|
||||
|
||||
def set_region_rules(world: "TunicWorld") -> None:
|
||||
player = world.player
|
||||
options = world.options
|
||||
|
||||
world.get_entrance("Overworld -> Overworld Holy Cross").access_rule = \
|
||||
lambda state: has_ability(holy_cross, state, world)
|
||||
world.get_entrance("Overworld -> Beneath the Well").access_rule = \
|
||||
lambda state: has_melee(state, player) or state.has(fire_wand, player)
|
||||
world.get_entrance("Overworld -> Dark Tomb").access_rule = \
|
||||
lambda state: has_lantern(state, world)
|
||||
# laurels in, ladder storage in through the furnace, or ice grapple down the belltower
|
||||
world.get_entrance("Overworld -> West Garden").access_rule = \
|
||||
lambda state: (state.has(laurels, player)
|
||||
or can_ladder_storage(state, world)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
|
||||
world.get_entrance("Overworld -> Eastern Vault Fortress").access_rule = \
|
||||
lambda state: state.has(laurels, player) \
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world) \
|
||||
or can_ladder_storage(state, world)
|
||||
# using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules
|
||||
world.get_entrance("Overworld -> Beneath the Vault").access_rule = \
|
||||
lambda state: (has_lantern(state, world) and has_ability(prayer, state, world)
|
||||
# there's some boxes in the way
|
||||
and (has_melee(state, player) or state.has_any((gun, grapple, fire_wand), player)))
|
||||
world.get_entrance("Ruined Atoll -> Library").access_rule = \
|
||||
lambda state: (state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world)
|
||||
and (has_sword(state, player) or state.has_any((fire_wand, gun), player)))
|
||||
world.get_entrance("Overworld -> Quarry").access_rule = \
|
||||
lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \
|
||||
and (state.has_any({grapple, laurels, gun}, player) or can_ladder_storage(state, world))
|
||||
world.get_entrance("Quarry Back -> Quarry").access_rule = \
|
||||
lambda state: has_sword(state, player) or state.has(fire_wand, player)
|
||||
world.get_entrance("Quarry Back -> Monastery").access_rule = \
|
||||
lambda state: state.has(laurels, player)
|
||||
world.get_entrance("Monastery -> Monastery Back").access_rule = \
|
||||
lambda state: (has_sword(state, player) or state.has(fire_wand, player)
|
||||
or laurels_zip(state, world))
|
||||
world.get_entrance("Quarry -> Lower Quarry").access_rule = \
|
||||
lambda state: has_mask(state, world)
|
||||
world.get_entrance("Lower Quarry -> Rooted Ziggurat").access_rule = \
|
||||
lambda state: state.has(grapple, player) and has_ability(prayer, state, world)
|
||||
world.get_entrance("Swamp -> Cathedral").access_rule = \
|
||||
lambda state: (state.has(laurels, player) and has_ability(prayer, state, world) and has_sword(state, player)) \
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
|
||||
world.get_entrance("Overworld -> Spirit Arena").access_rule = \
|
||||
lambda state: ((state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value
|
||||
else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player)
|
||||
and state.has_group_unique("Hero Relics", player, 6))
|
||||
and has_ability(prayer, state, world) and has_sword(state, player)
|
||||
and state.has_any({lantern, laurels}, player))
|
||||
|
||||
world.get_region("Quarry").connect(world.get_region("Rooted Ziggurat"),
|
||||
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)
|
||||
and has_ability(prayer, state, world))
|
||||
|
||||
if options.ladder_storage >= LadderStorage.option_medium:
|
||||
# ls at any ladder in a safe spot in quarry to get to the monastery rope entrance
|
||||
add_rule(world.get_entrance(entrance_name="Quarry Back -> Monastery"),
|
||||
rule=lambda state: can_ladder_storage(state, world))
|
||||
|
||||
|
||||
def set_location_rules(world: "TunicWorld") -> None:
|
||||
player = world.player
|
||||
|
||||
forbid_item(world.get_location("Secret Gathering Place - 20 Fairy Reward"), fairies, player)
|
||||
|
||||
# Ability Shuffle Exclusive Rules
|
||||
set_rule(world.get_location("Far Shore - Page Pickup"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Fortress Courtyard - Chest Near Cave"),
|
||||
lambda state: has_ability(prayer, state, world)
|
||||
or state.has(laurels, player)
|
||||
or can_ladder_storage(state, world)
|
||||
or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)
|
||||
and has_lantern(state, world)))
|
||||
set_rule(world.get_location("Fortress Courtyard - Page Near Cave"),
|
||||
lambda state: has_ability(prayer, state, world) or state.has(laurels, player)
|
||||
or can_ladder_storage(state, world)
|
||||
or (has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)
|
||||
and has_lantern(state, world)))
|
||||
set_rule(world.get_location("East Forest - Dancing Fox Spirit Holy Cross"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("Forest Grave Path - Holy Cross Code by Grave"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("East Forest - Golden Obelisk Holy Cross"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("Beneath the Well - [Powered Secret Room] Chest"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("West Garden - [North] Behind Holy Cross Door"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("Library Hall - Holy Cross Chest"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("Quarry - [Back Entrance] Bushes Holy Cross"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("Cathedral - Secret Legend Trophy Chest"),
|
||||
lambda state: has_ability(holy_cross, state, world))
|
||||
|
||||
# Overworld
|
||||
set_rule(world.get_location("Overworld - [Southwest] Fountain Page"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("Overworld - [Southwest] Grapple Chest Over Walkway"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
set_rule(world.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
set_rule(world.get_location("Far Shore - Secret Chest"),
|
||||
lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Overworld - [Southeast] Page on Pillar by Swamp"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("Old House - Normal Chest"),
|
||||
lambda state: state.has(house_key, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
|
||||
or laurels_zip(state, world))
|
||||
set_rule(world.get_location("Old House - Holy Cross Chest"),
|
||||
lambda state: has_ability(holy_cross, state, world) and (
|
||||
state.has(house_key, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
|
||||
or laurels_zip(state, world)))
|
||||
set_rule(world.get_location("Old House - Shield Pickup"),
|
||||
lambda state: state.has(house_key, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
|
||||
or laurels_zip(state, world))
|
||||
set_rule(world.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("Overworld - [Southwest] From West Garden"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("Overworld - [West] Chest After Bell"),
|
||||
lambda state: state.has(laurels, player)
|
||||
or (has_lantern(state, world) and has_sword(state, player))
|
||||
or can_ladder_storage(state, world))
|
||||
set_rule(world.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
set_rule(world.get_location("Overworld - [East] Grapple Chest"),
|
||||
lambda state: state.has(grapple, player))
|
||||
set_rule(world.get_location("Special Shop - Secret Page Pickup"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("Sealed Temple - Holy Cross Chest"),
|
||||
lambda state: has_ability(holy_cross, state, world)
|
||||
and (state.has(laurels, player) or (has_lantern(state, world) and (has_sword(state, player)
|
||||
or state.has(fire_wand, player)))
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)))
|
||||
set_rule(world.get_location("Sealed Temple - Page Pickup"),
|
||||
lambda state: state.has(laurels, player)
|
||||
or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player)))
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
set_rule(world.get_location("West Furnace - Lantern Pickup"),
|
||||
lambda state: has_melee(state, player) or state.has_any({fire_wand, laurels}, player))
|
||||
|
||||
set_rule(world.get_location("Secret Gathering Place - 10 Fairy Reward"),
|
||||
lambda state: state.has(fairies, player, 10))
|
||||
set_rule(world.get_location("Secret Gathering Place - 20 Fairy Reward"),
|
||||
lambda state: state.has(fairies, player, 20))
|
||||
set_rule(world.get_location("Coins in the Well - 3 Coins"),
|
||||
lambda state: state.has(coins, player, 3))
|
||||
set_rule(world.get_location("Coins in the Well - 6 Coins"),
|
||||
lambda state: state.has(coins, player, 6))
|
||||
set_rule(world.get_location("Coins in the Well - 10 Coins"),
|
||||
lambda state: state.has(coins, player, 10))
|
||||
set_rule(world.get_location("Coins in the Well - 15 Coins"),
|
||||
lambda state: state.has(coins, player, 15))
|
||||
|
||||
# East Forest
|
||||
set_rule(world.get_location("East Forest - Lower Grapple Chest"),
|
||||
lambda state: state.has(grapple, player))
|
||||
set_rule(world.get_location("East Forest - Lower Dash Chest"),
|
||||
lambda state: state.has_all({grapple, laurels}, player))
|
||||
set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"),
|
||||
lambda state: state.has_all({grapple, ice_dagger, fire_wand}, player)
|
||||
and has_ability(icebolt, state, world))
|
||||
|
||||
# West Garden
|
||||
set_rule(world.get_location("West Garden - [North] Across From Page Pickup"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("West Garden - [West] In Flooded Walkway"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"),
|
||||
lambda state: state.has(laurels, player) and has_ability(holy_cross, state, world))
|
||||
set_rule(world.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House"),
|
||||
lambda state: (state.has(laurels, player) and has_ability(prayer, state, world))
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
set_rule(world.get_location("West Garden - [Central Lowlands] Below Left Walkway"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("West Garden - [Central Highlands] After Garden Knight"),
|
||||
lambda state: state.has(laurels, player)
|
||||
or (has_lantern(state, world) and has_sword(state, player))
|
||||
or can_ladder_storage(state, world))
|
||||
|
||||
# Ruined Atoll
|
||||
set_rule(world.get_location("Ruined Atoll - [West] Near Kevin Block"),
|
||||
lambda state: state.has(laurels, player))
|
||||
# ice grapple push a crab through the door
|
||||
set_rule(world.get_location("Ruined Atoll - [East] Locked Room Lower Chest"),
|
||||
lambda state: state.has(laurels, player) or state.has(key, player, 2)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
set_rule(world.get_location("Ruined Atoll - [East] Locked Room Upper Chest"),
|
||||
lambda state: state.has(laurels, player) or state.has(key, player, 2)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
set_rule(world.get_location("Librarian - Hexagon Green"),
|
||||
lambda state: has_sword(state, player))
|
||||
|
||||
# Frog's Domain
|
||||
set_rule(world.get_location("Frog's Domain - Side Room Grapple Secret"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
set_rule(world.get_location("Frog's Domain - Grapple Above Hot Tub"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
set_rule(world.get_location("Frog's Domain - Escape Chest"),
|
||||
lambda state: state.has_any({grapple, laurels}, player))
|
||||
|
||||
# Library Lab
|
||||
set_rule(world.get_location("Library Lab - Page 1"),
|
||||
lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player))
|
||||
set_rule(world.get_location("Library Lab - Page 2"),
|
||||
lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player))
|
||||
set_rule(world.get_location("Library Lab - Page 3"),
|
||||
lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player))
|
||||
|
||||
# Eastern Vault Fortress
|
||||
# yes, you can clear the leaves with dagger
|
||||
# gun isn't included since it can only break one leaf pile at a time, and we don't check how much mana you have
|
||||
# but really, I expect the player to just throw a bomb at them if they don't have melee
|
||||
set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"),
|
||||
lambda state: state.has(laurels, player) and (has_melee(state, player) or state.has(ice_dagger, player)))
|
||||
set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"),
|
||||
lambda state: has_sword(state, player)
|
||||
and (has_ability(prayer, state, world)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)))
|
||||
set_rule(world.get_location("Fortress Arena - Hexagon Red"),
|
||||
lambda state: state.has(vault_key, player)
|
||||
and (has_ability(prayer, state, world)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)))
|
||||
|
||||
# Beneath the Vault
|
||||
set_rule(world.get_location("Beneath the Fortress - Bridge"),
|
||||
lambda state: has_lantern(state, world) and
|
||||
(has_melee(state, player) or state.has_any((laurels, fire_wand, ice_dagger, gun), player)))
|
||||
set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"),
|
||||
lambda state: has_melee(state, player) and has_lantern(state, world))
|
||||
|
||||
# Quarry
|
||||
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"),
|
||||
lambda state: has_sword(state, player) or state.has_all({fire_wand, laurels}, player))
|
||||
set_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"),
|
||||
lambda state: has_sword(state, player))
|
||||
|
||||
# Swamp
|
||||
set_rule(world.get_location("Cathedral Gauntlet - Gauntlet Reward"),
|
||||
lambda state: (state.has(fire_wand, player) and has_sword(state, player))
|
||||
and (state.has(laurels, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)))
|
||||
set_rule(world.get_location("Swamp - [Entrance] Above Entryway"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial"),
|
||||
lambda state: state.has(laurels, player))
|
||||
set_rule(world.get_location("Swamp - [South Graveyard] 4 Orange Skulls"),
|
||||
lambda state: has_sword(state, player))
|
||||
|
||||
# Hero's Grave
|
||||
set_rule(world.get_location("Hero's Grave - Tooth Relic"),
|
||||
lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Hero's Grave - Mushroom Relic"),
|
||||
lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Hero's Grave - Ash Relic"),
|
||||
lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Hero's Grave - Flowers Relic"),
|
||||
lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Hero's Grave - Effigy Relic"),
|
||||
lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Hero's Grave - Feathers Relic"),
|
||||
lambda state: state.has(laurels, player) and has_ability(prayer, state, world))
|
||||
|
||||
# Bombable Walls
|
||||
for location_name in bomb_walls:
|
||||
# has_sword is there because you can buy bombs in the shop
|
||||
set_rule(world.get_location(location_name),
|
||||
lambda state: state.has(gun, player)
|
||||
or has_sword(state, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
|
||||
add_rule(world.get_location("Cube Cave - Holy Cross Chest"),
|
||||
lambda state: state.has(gun, player)
|
||||
or has_sword(state, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
|
||||
# can't ice grapple to this one, not enough space
|
||||
set_rule(world.get_location("Quarry - [East] Bombable Wall"),
|
||||
lambda state: state.has(gun, player) or has_sword(state, player))
|
||||
|
||||
# Shop
|
||||
set_rule(world.get_location("Shop - Potion 1"),
|
||||
lambda state: has_sword(state, player))
|
||||
set_rule(world.get_location("Shop - Potion 2"),
|
||||
lambda state: has_sword(state, player))
|
||||
set_rule(world.get_location("Shop - Coin 1"),
|
||||
lambda state: has_sword(state, player))
|
||||
set_rule(world.get_location("Shop - Coin 2"),
|
||||
lambda state: has_sword(state, player))
|
||||
@@ -5,13 +5,6 @@ from .bases import TunicTestBase
|
||||
class TestAccess(TunicTestBase):
|
||||
options = {options.CombatLogic.internal_name: options.CombatLogic.option_off}
|
||||
|
||||
# test whether you can get into the temple without laurels
|
||||
def test_temple_access(self) -> None:
|
||||
self.collect_all_but(["Hero's Laurels", "Lantern"])
|
||||
self.assertFalse(self.can_reach_location("Sealed Temple - Page Pickup"))
|
||||
self.collect_by_name(["Lantern"])
|
||||
self.assertTrue(self.can_reach_location("Sealed Temple - Page Pickup"))
|
||||
|
||||
# test that the wells function properly. Since fairies is written the same way, that should succeed too
|
||||
def test_wells(self) -> None:
|
||||
self.collect_all_but(["Golden Coin"])
|
||||
@@ -50,22 +43,12 @@ class TestHexQuestNoShuffle(TunicTestBase):
|
||||
self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup"))
|
||||
|
||||
|
||||
class TestNormalGoal(TunicTestBase):
|
||||
options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_false}
|
||||
|
||||
# test that you need the three colored hexes to reach the Heir in standard
|
||||
def test_normal_goal(self) -> None:
|
||||
location = ["The Heir"]
|
||||
items = [["Red Questagon", "Blue Questagon", "Green Questagon"]]
|
||||
self.assertAccessDependency(location, items)
|
||||
|
||||
|
||||
class TestER(TunicTestBase):
|
||||
options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes,
|
||||
options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true,
|
||||
options.HexagonQuest.internal_name: options.HexagonQuest.option_false,
|
||||
options.CombatLogic.internal_name: options.CombatLogic.option_off,
|
||||
options.FixedShop.internal_name: options.FixedShop.option_true}
|
||||
options.EntranceLayout.internal_name: options.EntranceLayout.option_fixed_shop}
|
||||
|
||||
def test_overworld_hc_chest(self) -> None:
|
||||
# test to see that static connections are working properly -- this chest requires holy cross and is in Overworld
|
||||
@@ -99,7 +82,7 @@ class TestLadderStorage(TunicTestBase):
|
||||
options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes,
|
||||
options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true,
|
||||
options.HexagonQuest.internal_name: options.HexagonQuest.option_false,
|
||||
options.FixedShop.internal_name: options.FixedShop.option_false,
|
||||
options.EntranceLayout.internal_name: options.EntranceLayout.option_standard,
|
||||
options.LadderStorage.internal_name: options.LadderStorage.option_hard,
|
||||
options.LadderStorageWithoutItems.internal_name: options.LadderStorageWithoutItems.option_false,
|
||||
"plando_connections": [
|
||||
|
||||
Reference in New Issue
Block a user