TUNIC: Additional Combat Logic Option (#3658)

This commit is contained in:
Scipio Wright 2024-12-15 16:40:36 -05:00 committed by GitHub
parent 0fdc14bc42
commit 6282efb13c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 1310 additions and 257 deletions

View File

@ -1,7 +1,8 @@
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union
from logging import warning
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld
from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
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, 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
@ -10,6 +11,7 @@ from .er_scripts import create_er_regions
from .er_data import portal_mapping, RegionInfo, tunic_er_regions
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage)
from .combat_logic import area_data, CombatState
from worlds.AutoWorld import WebWorld, World
from Options import PlandoConnection
from decimal import Decimal, ROUND_HALF_UP
@ -127,11 +129,21 @@ class TunicWorld(World):
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
self.options.fixed_shop.value = self.options.fixed_shop.option_false
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
self.options.combat_logic.value = passthrough["combat_logic"]
@classmethod
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
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
if tunic.options.combat_logic:
multiworld.state.tunic_need_to_reset_combat_from_collect[tunic.player] = False
multiworld.state.tunic_need_to_reset_combat_from_remove[tunic.player] = False
multiworld.state.tunic_area_combat_state[tunic.player] = {}
for area_name in area_data.keys():
multiworld.state.tunic_area_combat_state[tunic.player][area_name] = CombatState.unchecked
# if it's one of the options, then it isn't a custom seed group
if tunic.options.entrance_rando.value in EntranceRando.options.values():
continue
@ -190,10 +202,12 @@ class TunicWorld(World):
def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
item_data = item_table[name]
return TunicItem(name, classification or item_data.classification, self.item_name_to_id[name], self.player)
# if item_data.combat_ic is None, it'll take item_data.classification instead
itemclass: ItemClassification = ((item_data.combat_ic if self.options.combat_logic else None)
or item_data.classification)
return TunicItem(name, classification or itemclass, self.item_name_to_id[name], self.player)
def create_items(self) -> None:
tunic_items: List[TunicItem] = []
self.slot_data_items = []
@ -322,15 +336,15 @@ class TunicWorld(World):
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"]
self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"]
# ladder rando uses ER with vanilla connections, so that we're not managing more rules files
if self.options.entrance_rando or self.options.shuffle_ladders:
# Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
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:
# for non-ER, non-ladders
# 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)
@ -351,7 +365,8 @@ class TunicWorld(World):
victory_region.locations.append(victory_location)
def set_rules(self) -> None:
if self.options.entrance_rando or self.options.shuffle_ladders:
# same reason as in create_regions, could probably be put into create_regions
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
set_er_location_rules(self)
else:
set_region_rules(self)
@ -360,6 +375,19 @@ class TunicWorld(World):
def get_filler_item_name(self) -> str:
return self.random.choice(filler_items)
# cache whether you can get through combat logic areas
def collect(self, state: CollectionState, item: Item) -> bool:
change = super().collect(state, item)
if change and self.options.combat_logic and item.name in combat_items:
state.tunic_need_to_reset_combat_from_collect[self.player] = True
return change
def remove(self, state: CollectionState, item: Item) -> bool:
change = super().remove(state, item)
if change and self.options.combat_logic and item.name in combat_items:
state.tunic_need_to_reset_combat_from_remove[self.player] = True
return change
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
if self.options.entrance_rando:
hint_data.update({self.player: {}})
@ -426,6 +454,7 @@ class TunicWorld(World):
"maskless": self.options.maskless.value,
"entrance_rando": int(bool(self.options.entrance_rando.value)),
"shuffle_ladders": self.options.shuffle_ladders.value,
"combat_logic": self.options.combat_logic.value,
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
"Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"],

View File

@ -0,0 +1,422 @@
from typing import Dict, List, NamedTuple, Tuple, Optional
from enum import IntEnum
from collections import defaultdict
from BaseClasses import CollectionState
from .rules import has_sword, has_melee
from worlds.AutoWorld import LogicMixin
# the vanilla stats you are expected to have to get through an area, based on where they are in vanilla
class AreaStats(NamedTuple):
att_level: int
def_level: int
potion_level: int # all 3 are before your first bonfire after getting the upgrade page, third costs 1k
hp_level: int
sp_level: int
mp_level: int
potion_count: int
equipment: List[str] = []
is_boss: bool = False
# the vanilla upgrades/equipment you would have
area_data: Dict[str, AreaStats] = {
"Overworld": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Stick"]),
"East Forest": AreaStats(1, 1, 1, 1, 1, 1, 0, ["Sword"]),
"Before Well": AreaStats(1, 1, 1, 1, 1, 1, 3, ["Sword", "Shield"]),
# learn how to upgrade
"Beneath the Well": AreaStats(2, 1, 3, 3, 1, 1, 3, ["Sword", "Shield"]),
"Dark Tomb": AreaStats(2, 2, 3, 3, 1, 1, 3, ["Sword", "Shield"]),
"West Garden": AreaStats(2, 3, 3, 3, 1, 1, 4, ["Sword", "Shield"]),
"Garden Knight": AreaStats(3, 3, 3, 3, 2, 1, 4, ["Sword", "Shield"], is_boss=True),
# get the wand here
"Beneath the Vault": AreaStats(3, 3, 3, 3, 2, 1, 4, ["Sword", "Shield", "Magic"]),
"Eastern Vault Fortress": AreaStats(3, 3, 3, 4, 3, 2, 4, ["Sword", "Shield", "Magic"]),
"Siege Engine": AreaStats(3, 3, 3, 4, 3, 2, 4, ["Sword", "Shield", "Magic"], is_boss=True),
"Frog's Domain": AreaStats(3, 4, 3, 5, 3, 3, 4, ["Sword", "Shield", "Magic"]),
# the second half of Atoll is the part you need the stats for, so putting it after frogs
"Ruined Atoll": AreaStats(4, 4, 3, 5, 3, 3, 5, ["Sword", "Shield", "Magic"]),
"The Librarian": AreaStats(4, 4, 3, 5, 3, 3, 5, ["Sword", "Shield", "Magic"], is_boss=True),
"Quarry": AreaStats(5, 4, 3, 5, 3, 3, 5, ["Sword", "Shield", "Magic"]),
"Rooted Ziggurat": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"]),
"Boss Scavenger": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic"], is_boss=True),
"Swamp": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]),
"Cathedral": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"]),
# marked as boss because the garden knights can't get hurt by stick
"Gauntlet": AreaStats(1, 1, 1, 1, 1, 1, 6, ["Sword", "Shield", "Magic"], is_boss=True),
"The Heir": AreaStats(5, 5, 3, 5, 3, 3, 6, ["Sword", "Shield", "Magic", "Laurels"], is_boss=True),
}
# these are used for caching which areas can currently be reached in state
boss_areas: List[str] = [name for name, data in area_data.items() if data.is_boss and name != "Gauntlet"]
non_boss_areas: List[str] = [name for name, data in area_data.items() if not data.is_boss]
class CombatState(IntEnum):
unchecked = 0
failed = 1
succeeded = 2
def has_combat_reqs(area_name: str, state: CollectionState, player: int) -> bool:
# we're caching whether you've met the combat reqs before if the state didn't change first
# if the combat state is stale, mark each area's combat state as stale
if state.tunic_need_to_reset_combat_from_collect[player]:
state.tunic_need_to_reset_combat_from_collect[player] = False
for name in area_data.keys():
if state.tunic_area_combat_state[player][name] == CombatState.failed:
state.tunic_area_combat_state[player][name] = CombatState.unchecked
if state.tunic_need_to_reset_combat_from_remove[player]:
state.tunic_need_to_reset_combat_from_remove[player] = False
for name in area_data.keys():
if state.tunic_area_combat_state[player][name] == CombatState.succeeded:
state.tunic_area_combat_state[player][name] = CombatState.unchecked
if state.tunic_area_combat_state[player][area_name] > CombatState.unchecked:
return state.tunic_area_combat_state[player][area_name] == CombatState.succeeded
met_combat_reqs = check_combat_reqs(area_name, state, player)
# we want to skip the "none area" since we don't record its results
if area_name not in area_data.keys():
return met_combat_reqs
# loop through the lists and set the easier/harder area states accordingly
if area_name in boss_areas:
area_list = boss_areas
elif area_name in non_boss_areas:
area_list = non_boss_areas
else:
area_list = [area_name]
if met_combat_reqs:
# set the state as true for each area until you get to the area we're looking at
for name in area_list:
state.tunic_area_combat_state[player][name] = CombatState.succeeded
if name == area_name:
break
else:
# set the state as false for the area we're looking at and each area after that
reached_name = False
for name in area_list:
if name == area_name:
reached_name = True
if reached_name:
state.tunic_area_combat_state[player][name] = CombatState.failed
return met_combat_reqs
def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: Optional[AreaStats] = None) -> bool:
data = alt_data or area_data[area_name]
extra_att_needed = 0
extra_def_needed = 0
extra_mp_needed = 0
has_magic = state.has_any({"Magic Wand", "Gun"}, player)
stick_bool = False
sword_bool = False
for item in data.equipment:
if item == "Stick":
if not has_melee(state, player):
if has_magic:
# magic can make up for the lack of stick
extra_mp_needed += 2
extra_att_needed -= 16
else:
return False
else:
stick_bool = True
elif item == "Sword":
if not has_sword(state, player):
# need sword for bosses
if data.is_boss:
return False
if has_magic:
# +4 mp pretty much makes up for the lack of sword, at least in Quarry
extra_mp_needed += 4
# stick is a backup plan, and doesn't scale well, so let's require a little less
extra_att_needed -= 2
elif has_melee(state, player):
# may revise this later based on feedback
extra_att_needed += 3
extra_def_needed += 2
else:
return False
else:
sword_bool = True
elif item == "Shield":
if not state.has("Shield", player):
extra_def_needed += 2
elif item == "Laurels":
if not state.has("Hero's Laurels", player):
# these are entirely based on vibes
extra_att_needed += 2
extra_def_needed += 3
elif item == "Magic":
if not has_magic:
extra_att_needed += 2
extra_def_needed += 2
extra_mp_needed -= 16
modified_stats = AreaStats(data.att_level + extra_att_needed, data.def_level + extra_def_needed, data.potion_level,
data.hp_level, data.sp_level, data.mp_level + extra_mp_needed, data.potion_count)
if not has_required_stats(modified_stats, state, player):
# we may need to check if you would have the required stats if you were missing a weapon
# it's kinda janky, but these only get hit in less than once per 100 generations, so whatever
if sword_bool and "Sword" in data.equipment and "Magic" in data.equipment:
# we need to check if you would have the required stats if you didn't have melee
equip_list = [item for item in data.equipment if item != "Sword"]
more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level,
data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count,
equip_list)
if check_combat_reqs("none", state, player, more_modified_stats):
return True
# and we need to check if you would have the required stats if you didn't have magic
equip_list = [item for item in data.equipment if item != "Magic"]
more_modified_stats = AreaStats(data.att_level + 2, data.def_level + 2, data.potion_level,
data.hp_level, data.sp_level, data.mp_level - 16, data.potion_count,
equip_list)
if check_combat_reqs("none", state, player, more_modified_stats):
return True
return False
elif stick_bool and "Stick" in data.equipment and "Magic" in data.equipment:
# we need to check if you would have the required stats if you didn't have the stick
equip_list = [item for item in data.equipment if item != "Stick"]
more_modified_stats = AreaStats(data.att_level - 16, data.def_level, data.potion_level,
data.hp_level, data.sp_level, data.mp_level + 4, data.potion_count,
equip_list)
if check_combat_reqs("none", state, player, more_modified_stats):
return True
return False
else:
return False
return True
# check if you have the required stats, and the money to afford them
# it may be innaccurate due to poor spending, and it may even require you to "spend poorly"
# but that's fine -- it's already pretty generous to begin with
def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> bool:
money_required = 0
player_att = 0
# check if we actually need the stat before checking state
if data.att_level > 1:
player_att, att_offerings = get_att_level(state, player)
if player_att < data.att_level:
return False
else:
extra_att = player_att - data.att_level
paid_att = max(0, att_offerings - extra_att)
# attack upgrades cost 100 for the first, +50 for each additional
money_per_att = 100
for _ in range(paid_att):
money_required += money_per_att
money_per_att += 50
# adding defense and sp together since they accomplish similar things: making you take less damage
if data.def_level + data.sp_level > 2:
player_def, def_offerings = get_def_level(state, player)
player_sp, sp_offerings = get_sp_level(state, player)
if player_def + player_sp < data.def_level + data.sp_level:
return False
else:
free_def = player_def - def_offerings
free_sp = player_sp - sp_offerings
paid_stats = data.def_level + data.sp_level - free_def - free_sp
sp_to_buy = 0
if paid_stats <= 0:
# if you don't have to pay for any stats, you don't need money for these upgrades
def_to_buy = 0
elif paid_stats <= def_offerings:
# get the amount needed to buy these def offerings
def_to_buy = paid_stats
else:
def_to_buy = def_offerings
sp_to_buy = max(0, paid_stats - def_offerings)
# if you have to buy more than 3 def, it's cheaper to buy 1 extra sp
if def_to_buy > 3 and sp_offerings > 0:
def_to_buy -= 1
sp_to_buy += 1
# def costs 100 for the first, +50 for each additional
money_per_def = 100
for _ in range(def_to_buy):
money_required += money_per_def
money_per_def += 50
# sp costs 200 for the first, +200 for each additional
money_per_sp = 200
for _ in range(sp_to_buy):
money_required += money_per_sp
money_per_sp += 200
# if you have 2 more attack than needed, we can forego needing mp
if data.mp_level > 1 and player_att < data.att_level + 2:
player_mp, mp_offerings = get_mp_level(state, player)
if player_mp < data.mp_level:
return False
else:
extra_mp = player_mp - data.mp_level
paid_mp = max(0, mp_offerings - extra_mp)
# mp costs 300 for the first, +50 for each additional
money_per_mp = 300
for _ in range(paid_mp):
money_required += money_per_mp
money_per_mp += 50
req_effective_hp = calc_effective_hp(data.hp_level, data.potion_level, data.potion_count)
player_potion, potion_offerings = get_potion_level(state, player)
player_hp, hp_offerings = get_hp_level(state, player)
player_potion_count = get_potion_count(state, player)
player_effective_hp = calc_effective_hp(player_hp, player_potion, player_potion_count)
if player_effective_hp < req_effective_hp:
return False
else:
# need a way to determine which of potion offerings or hp offerings you can reduce
# your level if you didn't pay for offerings
free_potion = player_potion - potion_offerings
free_hp = player_hp - hp_offerings
paid_hp_count = 0
paid_potion_count = 0
if calc_effective_hp(free_hp, free_potion, player_potion_count) >= req_effective_hp:
# you don't need to buy upgrades
pass
# if you have no potions, or no potion upgrades, you only need to check your hp upgrades
elif player_potion_count == 0 or potion_offerings == 0:
# check if you have enough hp at each paid hp offering
for i in range(hp_offerings):
paid_hp_count = i + 1
if calc_effective_hp(paid_hp_count, 0, player_potion_count) > req_effective_hp:
break
else:
for i in range(potion_offerings):
paid_potion_count = i + 1
if calc_effective_hp(free_hp, free_potion + paid_potion_count, player_potion_count) > req_effective_hp:
break
for j in range(hp_offerings):
paid_hp_count = j + 1
if (calc_effective_hp(free_hp + paid_hp_count, free_potion + paid_potion_count, player_potion_count)
> req_effective_hp):
break
# hp costs 200 for the first, +50 for each additional
money_per_hp = 200
for _ in range(paid_hp_count):
money_required += money_per_hp
money_per_hp += 50
# potion costs 100 for the first, 300 for the second, 1,000 for the third, and +200 for each additional
# currently we assume you will not buy past the second potion upgrade, but we might change our minds later
money_per_potion = 100
for _ in range(paid_potion_count):
money_required += money_per_potion
if money_per_potion == 100:
money_per_potion = 300
elif money_per_potion == 300:
money_per_potion = 1000
else:
money_per_potion += 200
if money_required > get_money_count(state, player):
return False
return True
# returns a tuple of your max attack level, the number of attack offerings
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)
if sword_level >= 3:
att_upgrades += min(2, sword_level - 2)
# attack falls off, can just cap it at 8 for simplicity
return min(8, 1 + att_offerings + att_upgrades), att_offerings
# 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_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)),
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]:
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),
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]:
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]:
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),
sp_offerings)
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),
mp_offerings)
def get_potion_count(state: CollectionState, player: int) -> int:
return state.count("Potion Flask", player) + state.count("Flask Shard", player) // 3
def calc_effective_hp(hp_level: int, potion_level: int, potion_count: int) -> int:
player_hp = 60 + hp_level * 20
# since you don't tend to use potions efficiently all the time, scale healing by .75
total_healing = int(.75 * potion_count * min(player_hp, 20 + 10 * potion_level))
return player_hp + total_healing
# returns the total amount of progression money the player has
def get_money_count(state: CollectionState, player: int) -> int:
money: int = 0
# this could be done with something to parse the money count at the end of the string, but I don't wanna
money += state.count("Money x255", player) * 255 # 1 in pool
money += state.count("Money x200", player) * 200 # 1 in pool
money += state.count("Money x128", player) * 128 # 3 in pool
# total from regular money: 839
# first effigy is 8, doubles until it reaches 512 at number 7, after effigy 28 they stop dropping money
# with the vanilla count of 12, you get 3,576 money from effigies
effigy_count = min(28, state.count("Effigy", player)) # 12 in pool
money_per_break = 8
for _ in range(effigy_count):
money += money_per_break
money_per_break = min(512, money_per_break * 2)
return money
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]]
def init_mixin(self, _):
# the per-player need to reset the combat state when collecting a combat item
self.tunic_need_to_reset_combat_from_collect = defaultdict(lambda: False)
# the per-player need to reset the combat state when removing a combat item
self.tunic_need_to_reset_combat_from_remove = defaultdict(lambda: False)
# the per-player, per-area state of combat checking -- unchecked, failed, or succeeded
self.tunic_area_combat_state = defaultdict(lambda: defaultdict(lambda: CombatState.unchecked))

View File

@ -248,13 +248,13 @@ portal_mapping: List[Portal] = [
Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point",
destination="Sewer_Boss", tag="_"),
Portal(name="West Garden Exit near Hero's Grave", region="West Garden",
Portal(name="West Garden Exit near Hero's Grave", region="West Garden before Terry",
destination="Overworld Redux", tag="_lower"),
Portal(name="West Garden to Magic Dagger House", region="West Garden",
Portal(name="West Garden to Magic Dagger House", region="West Garden at Dagger House",
destination="archipelagos_house", tag="_"),
Portal(name="West Garden Exit after Boss", region="West Garden after Boss",
destination="Overworld Redux", tag="_upper"),
Portal(name="West Garden Shop", region="West Garden",
Portal(name="West Garden Shop", region="West Garden before Terry",
destination="Shop", tag="_"),
Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region",
destination="Overworld Redux", tag="_lowest"),
@ -308,7 +308,7 @@ portal_mapping: List[Portal] = [
Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper",
destination="Fortress Main", tag="_upper"),
Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path",
Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path Entry",
destination="Fortress Courtyard", tag="_Lower"),
Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region",
destination="RelicVoid", tag="_teleporter_relic plinth"),
@ -433,7 +433,7 @@ portal_mapping: List[Portal] = [
Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom",
destination="ziggurat2020_3", tag="_"),
Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Front",
Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Entry",
destination="ziggurat2020_2", tag="_"),
Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance",
destination="ziggurat2020_FTRoom", tag="_"),
@ -461,7 +461,7 @@ portal_mapping: List[Portal] = [
Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave Region",
destination="RelicVoid", tag="_teleporter_relic plinth"),
Portal(name="Cathedral Main Exit", region="Cathedral",
Portal(name="Cathedral Main Exit", region="Cathedral Entry",
destination="Swamp Redux 2", tag="_main"),
Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet",
destination="Cathedral Arena", tag="_"),
@ -523,7 +523,6 @@ 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
is_fake_region: bool = False
# gets the outlet region name if it exists, the region if it doesn't
@ -563,6 +562,8 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Overworld to West Garden Upper": RegionInfo("Overworld Redux"), # usually leads to garden knight
"Overworld to West Garden from Furnace": RegionInfo("Overworld Redux"), # isolated stairway with one chest
"Overworld Well Ladder": RegionInfo("Overworld Redux"), # just the ladder entrance itself as a region
"Overworld Well Entry Area": RegionInfo("Overworld Redux"), # the page, the bridge, etc.
"Overworld Tunnel to Beach": RegionInfo("Overworld Redux"), # the tunnel with the chest
"Overworld Beach": RegionInfo("Overworld Redux"), # from the two turrets to invisble maze, and lower atoll entry
"Overworld Tunnel Turret": RegionInfo("Overworld Redux"), # the tunnel turret by the southwest beach ladder
"Overworld to Atoll Upper": RegionInfo("Overworld Redux"), # the little ledge before the ladder
@ -624,14 +625,18 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Beneath the Well Front": RegionInfo("Sewer"), # the front, to separate it from the weapon requirement in the mid
"Beneath the Well Main": RegionInfo("Sewer"), # the main section of it, requires a weapon
"Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests
"West Garden": RegionInfo("Archipelagos Redux"),
"West Garden before Terry": RegionInfo("Archipelagos Redux"), # the lower entry point, near hero grave
"West Garden after Terry": RegionInfo("Archipelagos Redux"), # after Terry, up until next chompignons
"West Garden at Dagger House": RegionInfo("Archipelagos Redux"), # just outside magic dagger house
"West Garden South Checkpoint": RegionInfo("Archipelagos Redux"),
"Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats),
"West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, outlet_region="West Garden by Portal"),
"West Garden by Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
"West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
"West Garden Laurels Exit Region": RegionInfo("Archipelagos Redux"),
"West Garden before Boss": RegionInfo("Archipelagos Redux"), # main west garden
"West Garden after Boss": RegionInfo("Archipelagos Redux"),
"West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux", outlet_region="West Garden"),
"West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux", outlet_region="West Garden before Terry"),
"Ruined Atoll": RegionInfo("Atoll Redux"),
"Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"),
"Ruined Atoll Ladder Tops": RegionInfo("Atoll Redux"), # at the top of the 5 ladders in south Atoll
@ -643,8 +648,9 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Frog Stairs Upper": RegionInfo("Frog Stairs"),
"Frog Stairs Lower": RegionInfo("Frog Stairs"),
"Frog Stairs to Frog's Domain": RegionInfo("Frog Stairs"),
"Frog's Domain Entry": RegionInfo("frog cave main"),
"Frog's Domain": RegionInfo("frog cave main"),
"Frog's Domain Entry": RegionInfo("frog cave main"), # just the ladder
"Frog's Domain Front": RegionInfo("frog cave main"), # before combat
"Frog's Domain Main": RegionInfo("frog cave main"),
"Frog's Domain Back": RegionInfo("frog cave main"),
"Library Exterior Tree Region": RegionInfo("Library Exterior", outlet_region="Library Exterior by Tree"),
"Library Exterior by Tree": RegionInfo("Library Exterior"),
@ -658,8 +664,8 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Library Rotunda to Lab": RegionInfo("Library Rotunda"),
"Library Lab": RegionInfo("Library Lab"),
"Library Lab Lower": RegionInfo("Library Lab"),
"Library Portal": RegionInfo("Library Lab", outlet_region="Library Lab on Portal Pad"),
"Library Lab on Portal Pad": RegionInfo("Library Lab"),
"Library Portal": RegionInfo("Library Lab", outlet_region="Library Lab on Portal Pad"),
"Library Lab to Librarian": RegionInfo("Library Lab"),
"Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats),
"Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"),
@ -675,10 +681,12 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Eastern Vault Fortress Gold Door": RegionInfo("Fortress Main"),
"Fortress East Shortcut Upper": RegionInfo("Fortress East"),
"Fortress East Shortcut Lower": RegionInfo("Fortress East"),
"Fortress Grave Path": RegionInfo("Fortress Reliquary"),
"Fortress Grave Path Entry": RegionInfo("Fortress Reliquary"),
"Fortress Grave Path Combat": RegionInfo("Fortress Reliquary"), # the combat is basically just a barrier here
"Fortress Grave Path by Grave": RegionInfo("Fortress Reliquary"),
"Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted),
"Fortress Grave Path Dusty Entrance Region": RegionInfo("Fortress Reliquary"),
"Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary", outlet_region="Fortress Grave Path"),
"Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary", outlet_region="Fortress Grave Path by Grave"),
"Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats),
"Fortress Arena": RegionInfo("Fortress Arena"),
"Fortress Arena Portal": RegionInfo("Fortress Arena", outlet_region="Fortress Arena"),
@ -697,6 +705,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Monastery Rope": RegionInfo("Quarry Redux"),
"Lower Quarry": RegionInfo("Quarry Redux"),
"Even Lower Quarry": RegionInfo("Quarry Redux"),
"Even Lower Quarry Isolated Chest": RegionInfo("Quarry Redux"), # a region for that one chest
"Lower Quarry Zig Door": RegionInfo("Quarry Redux"),
"Rooted Ziggurat Entry": RegionInfo("ziggurat2020_0"),
"Rooted Ziggurat Upper Entry": RegionInfo("ziggurat2020_1"),
@ -704,13 +713,15 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Rooted Ziggurat Upper Back": RegionInfo("ziggurat2020_1"), # after the administrator
"Rooted Ziggurat Middle Top": RegionInfo("ziggurat2020_2"),
"Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"),
"Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side
"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 Back": RegionInfo("ziggurat2020_3"), # the boss side
"Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Front"), # the exit from zig skip, for use with fixed shop on
"Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Entry"), # 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
"Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom", outlet_region="Rooted Ziggurat Portal Room"),
"Rooted Ziggurat Portal Room": RegionInfo("ziggurat2020_FTRoom"),
"Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"),
"Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom", outlet_region="Rooted Ziggurat Portal Room"),
"Swamp Front": RegionInfo("Swamp Redux 2"), # from the main entry to the top of the ladder after south
"Swamp Mid": RegionInfo("Swamp Redux 2"), # from the bottom of the ladder to the cathedral door
"Swamp Ledge under Cathedral Door": RegionInfo("Swamp Redux 2"), # the ledge with the chest and secret door
@ -719,7 +730,8 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance
"Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2", outlet_region="Back of Swamp"),
"Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse
"Cathedral": RegionInfo("Cathedral Redux"),
"Cathedral Entry": RegionInfo("Cathedral Redux"), # the checkpoint and easily-accessible chests
"Cathedral Main": RegionInfo("Cathedral Redux"), # the majority of Cathedral
"Cathedral to Gauntlet": RegionInfo("Cathedral Redux"), # the elevator
"Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats),
"Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"),
@ -741,7 +753,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Purgatory": RegionInfo("Purgatory"),
"Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats),
"Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats),
"Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats)
"Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats),
}
@ -759,6 +771,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld": {
"Overworld Beach":
[],
"Overworld Tunnel to Beach":
[],
"Overworld to Atoll Upper":
[["Hyperdash"]],
"Overworld Belltower":
@ -769,7 +783,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[],
"Overworld Special Shop Entry":
[["Hyperdash"], ["LS1"]],
"Overworld Well Ladder":
"Overworld Well Entry Area":
[],
"Overworld Ruined Passage Door":
[],
@ -847,6 +861,12 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
# "Overworld":
# [],
# },
"Overworld Tunnel to Beach": {
# "Overworld":
# [],
"Overworld Beach":
[],
},
"Overworld Beach": {
# "Overworld":
# [],
@ -873,9 +893,15 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld Beach":
[],
},
"Overworld Well Ladder": {
"Overworld Well Entry Area": {
# "Overworld":
# [],
"Overworld Well Ladder":
[],
},
"Overworld Well Ladder": {
"Overworld Well Entry Area":
[],
},
"Overworld at Patrol Cave": {
"East Overworld":
@ -954,6 +980,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld":
[],
},
"Old House Front": {
"Old House Back":
[],
@ -962,6 +989,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Old House Front":
[["Hyperdash", "Zip"]],
},
"Furnace Fuse": {
"Furnace Ladder Area":
[["Hyperdash"]],
@ -976,6 +1004,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Furnace Ladder Area":
[["Hyperdash"]],
},
"Sealed Temple": {
"Sealed Temple Rafters":
[],
@ -984,10 +1013,12 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Sealed Temple":
[["Hyperdash"]],
},
"Hourglass Cave": {
"Hourglass Cave Tower":
[],
},
"Forest Belltower Upper": {
"Forest Belltower Main":
[],
@ -996,6 +1027,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Forest Belltower Lower":
[],
},
"East Forest": {
"East Forest Dance Fox Spot":
[["Hyperdash"], ["IG1"], ["LS1"]],
@ -1016,6 +1048,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"East Forest":
[],
},
"Guard House 1 East": {
"Guard House 1 West":
[],
@ -1024,6 +1057,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Guard House 1 East":
[["Hyperdash"], ["LS1"]],
},
"Guard House 2 Upper": {
"Guard House 2 Lower":
[],
@ -1032,6 +1066,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Guard House 2 Upper":
[],
},
"Forest Grave Path Main": {
"Forest Grave Path Upper":
[["Hyperdash"], ["LS2"], ["IG3"]],
@ -1052,6 +1087,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Forest Grave Path by Grave":
[],
},
"Beneath the Well Ladder Exit": {
"Beneath the Well Front":
[],
@ -1072,6 +1108,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Beneath the Well Main":
[],
},
"Well Boss": {
"Dark Tomb Checkpoint":
[],
@ -1080,6 +1117,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Well Boss":
[["Hyperdash", "Zip"]],
},
"Dark Tomb Entry Point": {
"Dark Tomb Upper":
[],
@ -1100,44 +1138,72 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Dark Tomb Main":
[],
},
"West Garden": {
"West Garden Laurels Exit Region":
[["Hyperdash"], ["LS1"]],
"West Garden after Boss":
"West Garden before Terry": {
"West Garden after Terry":
[],
"West Garden Hero's Grave Region":
[],
},
"West Garden Hero's Grave Region": {
"West Garden before Terry":
[],
},
"West Garden after Terry": {
"West Garden before Terry":
[],
"West Garden South Checkpoint":
[],
"West Garden Laurels Exit Region":
[["LS1"]],
},
"West Garden South Checkpoint": {
"West Garden before Boss":
[],
"West Garden at Dagger House":
[],
"West Garden after Terry":
[],
},
"West Garden before Boss": {
"West Garden after Boss":
[],
"West Garden South Checkpoint":
[],
},
"West Garden after Boss": {
"West Garden before Boss":
[["Hyperdash"]],
},
"West Garden at Dagger House": {
"West Garden Laurels Exit Region":
[["Hyperdash"]],
"West Garden South Checkpoint":
[],
"West Garden Portal Item":
[["IG2"]],
},
"West Garden Laurels Exit Region": {
"West Garden":
[["Hyperdash"]],
},
"West Garden after Boss": {
"West Garden":
"West Garden at Dagger House":
[["Hyperdash"]],
},
"West Garden Portal Item": {
"West Garden":
"West Garden at Dagger House":
[["IG1"]],
"West Garden by Portal":
[["Hyperdash"]],
},
"West Garden by Portal": {
"West Garden Portal":
[["West Garden South Checkpoint"]],
"West Garden Portal Item":
[["Hyperdash"]],
"West Garden Portal":
[["West Garden"]],
},
"West Garden Portal": {
"West Garden by Portal":
[],
},
"West Garden Hero's Grave Region": {
"West Garden":
[],
},
"Ruined Atoll": {
"Ruined Atoll Lower Entry Area":
[["Hyperdash"], ["LS1"]],
@ -1176,6 +1242,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Ruined Atoll":
[],
},
"Frog Stairs Eye Exit": {
"Frog Stairs Upper":
[],
@ -1196,16 +1263,25 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Frog Stairs Lower":
[],
},
"Frog's Domain Entry": {
"Frog's Domain":
"Frog's Domain Front":
[],
},
"Frog's Domain": {
"Frog's Domain Front": {
"Frog's Domain Entry":
[],
"Frog's Domain Main":
[],
},
"Frog's Domain Main": {
"Frog's Domain Front":
[],
"Frog's Domain Back":
[],
},
# cannot get from frogs back to front
"Library Exterior Ladder Region": {
"Library Exterior by Tree":
[],
@ -1220,6 +1296,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Library Exterior by Tree":
[],
},
"Library Hall Bookshelf": {
"Library Hall":
[],
@ -1240,6 +1317,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Library Hall":
[],
},
"Library Rotunda to Hall": {
"Library Rotunda":
[],
@ -1281,6 +1359,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Library Lab":
[],
},
"Fortress Exterior from East Forest": {
"Fortress Exterior from Overworld":
[],
@ -1321,6 +1400,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Fortress Courtyard":
[],
},
"Beneath the Vault Ladder Exit": {
"Beneath the Vault Main":
[],
@ -1337,6 +1417,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Beneath the Vault Ladder Exit":
[],
},
"Fortress East Shortcut Lower": {
"Fortress East Shortcut Upper":
[["IG1"]],
@ -1345,6 +1426,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Fortress East Shortcut Lower":
[],
},
"Eastern Vault Fortress": {
"Eastern Vault Fortress Gold Door":
[["IG2"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]],
@ -1353,24 +1435,44 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Eastern Vault Fortress":
[["IG1"]],
},
"Fortress Grave Path": {
"Fortress Grave Path Entry": {
"Fortress Grave Path Combat":
[],
# redundant here, keeping a comment to show it's intentional
# "Fortress Grave Path Dusty Entrance Region":
# [["Hyperdash"]],
},
"Fortress Grave Path Combat": {
"Fortress Grave Path Entry":
[],
"Fortress Grave Path by Grave":
[],
},
"Fortress Grave Path by Grave": {
"Fortress Grave Path Entry":
[],
# unnecessary, you can just skip it
# "Fortress Grave Path Combat":
# [],
"Fortress Hero's Grave Region":
[],
"Fortress Grave Path Dusty Entrance Region":
[["Hyperdash"]],
},
"Fortress Grave Path Upper": {
"Fortress Grave Path":
"Fortress Grave Path Entry":
[["IG1"]],
},
"Fortress Grave Path Dusty Entrance Region": {
"Fortress Grave Path":
"Fortress Grave Path by Grave":
[["Hyperdash"]],
},
"Fortress Hero's Grave Region": {
"Fortress Grave Path":
"Fortress Grave Path by Grave":
[],
},
"Fortress Arena": {
"Fortress Arena Portal":
[["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]],
@ -1379,6 +1481,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Fortress Arena":
[],
},
"Lower Mountain": {
"Lower Mountain Stairs":
[],
@ -1387,6 +1490,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Lower Mountain":
[],
},
"Monastery Back": {
"Monastery Front":
[["Hyperdash", "Zip"]],
@ -1401,6 +1505,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Monastery Back":
[],
},
"Quarry Entry": {
"Quarry Portal":
[["Quarry Connector"]],
@ -1436,15 +1541,17 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[],
"Quarry Monastery Entry":
[],
"Lower Quarry Zig Door":
[["IG3"]],
},
"Lower Quarry": {
"Even Lower Quarry":
[],
},
"Even Lower Quarry": {
"Lower Quarry":
"Even Lower Quarry Isolated Chest":
[],
},
"Even Lower Quarry Isolated Chest": {
"Even Lower Quarry":
[],
"Lower Quarry Zig Door":
[["Quarry", "Quarry Connector"], ["IG3"]],
@ -1453,6 +1560,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Quarry Back":
[],
},
"Rooted Ziggurat Upper Entry": {
"Rooted Ziggurat Upper Front":
[],
@ -1465,17 +1573,38 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Rooted Ziggurat Upper Front":
[["Hyperdash"]],
},
"Rooted Ziggurat Middle Top": {
"Rooted Ziggurat Middle Bottom":
[],
},
"Rooted Ziggurat Lower Entry": {
"Rooted Ziggurat Lower Front":
[],
# can zip through to the checkpoint
"Rooted Ziggurat Lower Mid Checkpoint":
[["Hyperdash"]],
},
"Rooted Ziggurat Lower Front": {
"Rooted Ziggurat Lower Entry":
[],
"Rooted Ziggurat Lower Mid Checkpoint":
[],
},
"Rooted Ziggurat Lower Mid Checkpoint": {
"Rooted Ziggurat Lower Entry":
[["Hyperdash"]],
"Rooted Ziggurat Lower Front":
[],
"Rooted Ziggurat Lower Back":
[],
},
"Rooted Ziggurat Lower Back": {
"Rooted Ziggurat Lower Front":
[["Hyperdash"], ["LS2"], ["IG1"]],
"Rooted Ziggurat Lower Entry":
[["LS2"]],
"Rooted Ziggurat Lower Mid Checkpoint":
[["Hyperdash"], ["IG1"]],
"Rooted Ziggurat Portal Room Entrance":
[],
},
@ -1487,20 +1616,22 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Rooted Ziggurat Lower Back":
[],
},
"Rooted Ziggurat Portal Room Exit": {
"Rooted Ziggurat Portal Room":
[],
},
"Rooted Ziggurat Portal Room": {
"Rooted Ziggurat Portal":
[],
"Rooted Ziggurat Portal Room Exit":
[["Rooted Ziggurat Lower Back"]],
"Rooted Ziggurat Portal":
[],
},
"Rooted Ziggurat Portal": {
"Rooted Ziggurat Portal Room":
[],
},
"Swamp Front": {
"Swamp Mid":
[],
@ -1557,14 +1688,26 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Back of Swamp":
[],
},
"Cathedral": {
"Cathedral Entry": {
"Cathedral to Gauntlet":
[],
"Cathedral Main":
[],
},
"Cathedral Main": {
"Cathedral Entry":
[],
"Cathedral to Gauntlet":
[],
},
"Cathedral to Gauntlet": {
"Cathedral":
"Cathedral Entry":
[],
"Cathedral Main":
[],
},
"Cathedral Gauntlet Checkpoint": {
"Cathedral Gauntlet":
[],
@ -1577,6 +1720,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Cathedral Gauntlet":
[["Hyperdash"]],
},
"Far Shore": {
"Far Shore to Spawn Region":
[["Hyperdash"]],
@ -1587,7 +1731,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Far Shore to Library Region":
[["Library Lab"]],
"Far Shore to West Garden Region":
[["West Garden"]],
[["West Garden South Checkpoint"]],
"Far Shore to Fortress Region":
[["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]],
},

View File

@ -1,10 +1,11 @@
from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING
from worlds.generic.Rules import set_rule, forbid_item
from .options import IceGrappling, LadderStorage
from .rules import (has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage,
from worlds.generic.Rules import set_rule, add_rule, forbid_item
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 .combat_logic import has_combat_reqs
from BaseClasses import Region, CollectionState
if TYPE_CHECKING:
@ -43,6 +44,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
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]:
for portal1, portal2 in portal_pairs.items():
if portal1.scene_destination() == portal_sd:
return portal1.name, get_portal_outlet_region(portal2, world)
if portal2.scene_destination() == portal_sd:
return portal2.name, get_portal_outlet_region(portal1, world)
raise Exception("No matches found in get_portal_info")
# input scene destination tag, returns paired portal's name and region
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
if portal2.scene_destination() == portal_sd:
return portal1.name, portal1.region
raise Exception("no matches found in get_paired_portal")
regions["Menu"].connect(
connecting_region=regions["Overworld"])
@ -56,10 +75,18 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Overworld Beach"],
rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
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)
# or state.has_any({laurels, grapple}, player))
# region for combat logic, no need to connect it to beach since it would be the same as the ow -> beach cxn
ow_tunnel_beach = regions["Overworld"].connect(
connecting_region=regions["Overworld Tunnel to Beach"])
regions["Overworld Beach"].connect(
connecting_region=regions["Overworld"],
rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
or state.has_any({laurels, grapple}, player))
connecting_region=regions["Overworld Tunnel to Beach"],
rule=lambda state: state.has(laurels, player) or has_ladder("Ladders in Overworld Town", state, world))
regions["Overworld Beach"].connect(
connecting_region=regions["Overworld West Garden Laurels Entry"],
@ -277,11 +304,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["East Overworld"],
rule=lambda state: state.has(laurels, player))
regions["Overworld"].connect(
# region made for combat logic
ow_to_well_entry = regions["Overworld"].connect(
connecting_region=regions["Overworld Well Entry Area"])
regions["Overworld Well Entry Area"].connect(
connecting_region=regions["Overworld"])
regions["Overworld Well Entry Area"].connect(
connecting_region=regions["Overworld Well Ladder"],
rule=lambda state: has_ladder("Ladders in Well", state, world))
regions["Overworld Well Ladder"].connect(
connecting_region=regions["Overworld"],
connecting_region=regions["Overworld Well Entry Area"],
rule=lambda state: has_ladder("Ladders in Well", state, world))
# nmg: can ice grapple through the door
@ -306,7 +339,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Overworld Fountain Cross Door"].connect(
connecting_region=regions["Overworld"])
regions["Overworld"].connect(
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(
@ -337,6 +370,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
# don't need the ice grapple rule since you can go from ow -> beach -> tunnel
regions["Overworld"].connect(
connecting_region=regions["Overworld Tunnel Turret"],
rule=lambda state: state.has(laurels, player))
@ -473,29 +507,28 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Beneath the Well Ladder Exit"],
rule=lambda state: has_ladder("Ladders in Well", state, world))
regions["Beneath the Well Front"].connect(
btw_front_main = regions["Beneath the Well Front"].connect(
connecting_region=regions["Beneath the Well Main"],
rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
rule=lambda state: has_melee(state, player) or state.has(fire_wand, player))
regions["Beneath the Well Main"].connect(
connecting_region=regions["Beneath the Well Front"],
rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
connecting_region=regions["Beneath the Well Front"])
regions["Beneath the Well Main"].connect(
connecting_region=regions["Beneath the Well Back"],
rule=lambda state: has_ladder("Ladders in Well", state, world))
regions["Beneath the Well Back"].connect(
btw_back_main = regions["Beneath the Well Back"].connect(
connecting_region=regions["Beneath the Well Main"],
rule=lambda state: has_ladder("Ladders in Well", state, world)
and (has_stick(state, player) or state.has(fire_wand, player)))
and (has_melee(state, player) or state.has(fire_wand, player)))
regions["Well Boss"].connect(
well_boss_to_dt = regions["Well Boss"].connect(
connecting_region=regions["Dark Tomb Checkpoint"])
# can laurels through the gate, no setup needed
regions["Dark Tomb Checkpoint"].connect(
connecting_region=regions["Well Boss"],
rule=lambda state: laurels_zip(state, world))
regions["Dark Tomb Entry Point"].connect(
dt_entry_to_upper = regions["Dark Tomb Entry Point"].connect(
connecting_region=regions["Dark Tomb Upper"],
rule=lambda state: has_lantern(state, world))
regions["Dark Tomb Upper"].connect(
@ -512,34 +545,57 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Dark Tomb Main"].connect(
connecting_region=regions["Dark Tomb Dark Exit"])
regions["Dark Tomb Dark Exit"].connect(
dt_exit_to_main = regions["Dark Tomb Dark Exit"].connect(
connecting_region=regions["Dark Tomb Main"],
rule=lambda state: has_lantern(state, world))
# West Garden
# combat logic regions
wg_before_to_after_terry = regions["West Garden before Terry"].connect(
connecting_region=regions["West Garden after Terry"])
wg_after_to_before_terry = regions["West Garden after Terry"].connect(
connecting_region=regions["West Garden before Terry"])
regions["West Garden after Terry"].connect(
connecting_region=regions["West Garden South Checkpoint"])
wg_checkpoint_to_after_terry = regions["West Garden South Checkpoint"].connect(
connecting_region=regions["West Garden after Terry"])
wg_checkpoint_to_dagger = regions["West Garden South Checkpoint"].connect(
connecting_region=regions["West Garden at Dagger House"])
regions["West Garden at Dagger House"].connect(
connecting_region=regions["West Garden South Checkpoint"])
wg_checkpoint_to_before_boss = regions["West Garden South Checkpoint"].connect(
connecting_region=regions["West Garden before Boss"])
regions["West Garden before Boss"].connect(
connecting_region=regions["West Garden South Checkpoint"])
regions["West Garden Laurels Exit Region"].connect(
connecting_region=regions["West Garden"],
connecting_region=regions["West Garden at Dagger House"],
rule=lambda state: state.has(laurels, player))
regions["West Garden"].connect(
regions["West Garden at Dagger House"].connect(
connecting_region=regions["West Garden Laurels Exit Region"],
rule=lambda state: state.has(laurels, player))
# you can grapple Garden Knight to aggro it, then ledge it
regions["West Garden after Boss"].connect(
connecting_region=regions["West Garden"],
# laurels past, or ice grapple it off, or ice grapple to it then fight
after_gk_to_wg = regions["West Garden after Boss"].connect(
connecting_region=regions["West Garden before Boss"],
rule=lambda state: state.has(laurels, player)
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
or (has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)
and has_sword(state, player)))
# ice grapple push Garden Knight off the side
regions["West Garden"].connect(
wg_to_after_gk = regions["West Garden before Boss"].connect(
connecting_region=regions["West Garden after Boss"],
rule=lambda state: state.has(laurels, player) or has_sword(state, player)
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
regions["West Garden"].connect(
regions["West Garden before Terry"].connect(
connecting_region=regions["West Garden Hero's Grave Region"],
rule=lambda state: has_ability(prayer, state, world))
regions["West Garden Hero's Grave Region"].connect(
connecting_region=regions["West Garden"])
connecting_region=regions["West Garden before Terry"])
regions["West Garden Portal"].connect(
connecting_region=regions["West Garden by Portal"])
@ -556,9 +612,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
# can ice grapple to and from the item behind the magic dagger house
regions["West Garden Portal Item"].connect(
connecting_region=regions["West Garden"],
connecting_region=regions["West Garden at Dagger House"],
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["West Garden"].connect(
regions["West Garden at Dagger House"].connect(
connecting_region=regions["West Garden Portal Item"],
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_medium, state, world))
@ -596,7 +652,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Ruined Atoll Portal"].connect(
connecting_region=regions["Ruined Atoll"])
regions["Ruined Atoll"].connect(
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)
@ -629,10 +685,13 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
regions["Frog's Domain Entry"].connect(
connecting_region=regions["Frog's Domain"],
connecting_region=regions["Frog's Domain Front"],
rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
regions["Frog's Domain"].connect(
frogs_front_to_main = regions["Frog's Domain Front"].connect(
connecting_region=regions["Frog's Domain Main"])
regions["Frog's Domain Main"].connect(
connecting_region=regions["Frog's Domain Back"],
rule=lambda state: state.has(grapple, player))
@ -752,7 +811,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
rule=lambda state: state.has(laurels, player)
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Fortress Courtyard Upper"].connect(
fort_upper_lower = regions["Fortress Courtyard Upper"].connect(
connecting_region=regions["Fortress Courtyard"])
# nmg: can ice grapple to the upper ledge
regions["Fortress Courtyard"].connect(
@ -762,12 +821,12 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Fortress Courtyard Upper"].connect(
connecting_region=regions["Fortress Exterior from Overworld"])
regions["Beneath the Vault Ladder Exit"].connect(
btv_front_to_main = regions["Beneath the Vault Ladder Exit"].connect(
connecting_region=regions["Beneath the Vault Main"],
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)
and has_lantern(state, world)
# there's some boxes in the way
and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player)))
and (has_melee(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player)))
# on the reverse trip, you can lure an enemy over to break the boxes if needed
regions["Beneath the Vault Main"].connect(
connecting_region=regions["Beneath the Vault Ladder Exit"],
@ -775,11 +834,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Beneath the Vault Main"].connect(
connecting_region=regions["Beneath the Vault Back"])
regions["Beneath the Vault Back"].connect(
btv_back_to_main = regions["Beneath the Vault Back"].connect(
connecting_region=regions["Beneath the Vault Main"],
rule=lambda state: has_lantern(state, world))
regions["Fortress East Shortcut Upper"].connect(
fort_east_upper_lower = regions["Fortress East Shortcut Upper"].connect(
connecting_region=regions["Fortress East Shortcut Lower"])
regions["Fortress East Shortcut Lower"].connect(
connecting_region=regions["Fortress East Shortcut Upper"],
@ -794,21 +853,31 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Eastern Vault Fortress"],
rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world))
regions["Fortress Grave Path"].connect(
connecting_region=regions["Fortress Grave Path Dusty Entrance Region"],
rule=lambda state: state.has(laurels, player))
regions["Fortress Grave Path Dusty Entrance Region"].connect(
connecting_region=regions["Fortress Grave Path"],
rule=lambda state: state.has(laurels, player))
fort_grave_entry_to_combat = regions["Fortress Grave Path Entry"].connect(
connecting_region=regions["Fortress Grave Path Combat"])
regions["Fortress Grave Path Combat"].connect(
connecting_region=regions["Fortress Grave Path Entry"])
regions["Fortress Grave Path"].connect(
regions["Fortress Grave Path Combat"].connect(
connecting_region=regions["Fortress Grave Path by Grave"])
# run past the enemies
regions["Fortress Grave Path by Grave"].connect(
connecting_region=regions["Fortress Grave Path Entry"])
regions["Fortress Grave Path by Grave"].connect(
connecting_region=regions["Fortress Hero's Grave Region"],
rule=lambda state: has_ability(prayer, state, world))
regions["Fortress Hero's Grave Region"].connect(
connecting_region=regions["Fortress Grave Path"])
connecting_region=regions["Fortress Grave Path by Grave"])
regions["Fortress Grave Path by Grave"].connect(
connecting_region=regions["Fortress Grave Path Dusty Entrance Region"],
rule=lambda state: state.has(laurels, player))
# reverse connection is conditionally made later, depending on whether combat logic is on, and the details of ER
regions["Fortress Grave Path Upper"].connect(
connecting_region=regions["Fortress Grave Path"],
connecting_region=regions["Fortress Grave Path Entry"],
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Fortress Arena"].connect(
@ -831,19 +900,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Quarry Portal"].connect(
connecting_region=regions["Quarry Entry"])
regions["Quarry Entry"].connect(
quarry_entry_to_main = regions["Quarry Entry"].connect(
connecting_region=regions["Quarry"],
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
regions["Quarry"].connect(
connecting_region=regions["Quarry Entry"])
regions["Quarry Back"].connect(
quarry_back_to_main = regions["Quarry Back"].connect(
connecting_region=regions["Quarry"],
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
regions["Quarry"].connect(
connecting_region=regions["Quarry Back"])
regions["Quarry Monastery Entry"].connect(
monastery_to_quarry_main = regions["Quarry Monastery Entry"].connect(
connecting_region=regions["Quarry"],
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
regions["Quarry"].connect(
@ -869,18 +938,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world)
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
# nmg: bring a scav over, then ice grapple through the door, only with ER on to avoid soft lock
regions["Even Lower Quarry"].connect(
connecting_region=regions["Even Lower Quarry Isolated Chest"])
# you grappled down, might as well loot the rest too
lower_quarry_empty_to_combat = regions["Even Lower Quarry Isolated Chest"].connect(
connecting_region=regions["Even Lower Quarry"],
rule=lambda state: has_mask(state, world))
regions["Even Lower Quarry Isolated Chest"].connect(
connecting_region=regions["Lower Quarry Zig Door"],
rule=lambda state: state.has("Activate Quarry Fuse", player)
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
# nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask only with ER on
# don't need the mask for this either, please don't complain about not needing a mask here, you know what you did
regions["Quarry"].connect(
connecting_region=regions["Lower Quarry Zig Door"],
connecting_region=regions["Even Lower Quarry Isolated Chest"],
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world))
regions["Monastery Front"].connect(
monastery_front_to_back = regions["Monastery Front"].connect(
connecting_region=regions["Monastery Back"])
# laurels through the gate, no setup needed
regions["Monastery Back"].connect(
@ -897,7 +972,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Rooted Ziggurat Upper Entry"].connect(
connecting_region=regions["Rooted Ziggurat Upper Front"])
regions["Rooted Ziggurat Upper Front"].connect(
zig_upper_front_back = regions["Rooted Ziggurat Upper Front"].connect(
connecting_region=regions["Rooted Ziggurat Upper Back"],
rule=lambda state: state.has(laurels, player) or has_sword(state, player))
regions["Rooted Ziggurat Upper Back"].connect(
@ -907,13 +982,23 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Rooted Ziggurat Middle Top"].connect(
connecting_region=regions["Rooted Ziggurat Middle Bottom"])
zig_low_entry_to_front = regions["Rooted Ziggurat Lower Entry"].connect(
connecting_region=regions["Rooted Ziggurat Lower Front"])
regions["Rooted Ziggurat Lower Front"].connect(
connecting_region=regions["Rooted Ziggurat Lower Entry"])
regions["Rooted Ziggurat Lower Front"].connect(
connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"])
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)))
# nmg: can ice grapple on the voidlings to the double admin fight, still need to pray at the fuse
regions["Rooted Ziggurat Lower Back"].connect(
connecting_region=regions["Rooted Ziggurat Lower Front"],
# 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(
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)
@ -925,8 +1010,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Rooted Ziggurat Portal Room Entrance"].connect(
connecting_region=regions["Rooted Ziggurat Lower Back"])
regions["Zig Skip Exit"].connect(
connecting_region=regions["Rooted Ziggurat Lower Front"])
# zig skip region only gets made if entrance rando and fewer shops are on
if options.entrance_rando and options.fixed_shop:
regions["Zig Skip Exit"].connect(
connecting_region=regions["Rooted Ziggurat Lower Front"])
regions["Rooted Ziggurat Portal"].connect(
connecting_region=regions["Rooted Ziggurat Portal Room"])
@ -952,7 +1039,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
or state.has(laurels, player)
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
# a whole lot of stuff to basically say "you need to pray at the overworld fuse"
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)
@ -965,7 +1051,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
"Ladder to Swamp",
"Ladders near Weathervane"}, player)
or (state.has("Ladder to Ruined Atoll", player)
and state.can_reach_region("Overworld Beach", player))))))
and state.can_reach_region("Overworld Beach", player)))))
and (not options.combat_logic
or has_combat_reqs("Swamp", state, player)))
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders:
@ -1017,13 +1105,23 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Swamp Hero's Grave Region"].connect(
connecting_region=regions["Back of Swamp"])
regions["Cathedral"].connect(
cath_entry_to_elev = regions["Cathedral Entry"].connect(
connecting_region=regions["Cathedral to Gauntlet"],
rule=lambda state: (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(
connecting_region=regions["Cathedral"])
connecting_region=regions["Cathedral Entry"])
cath_entry_to_main = regions["Cathedral Entry"].connect(
connecting_region=regions["Cathedral Main"])
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"])
@ -1075,7 +1173,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
connecting_region=regions["Far Shore"])
# Misc
regions["Spirit Arena"].connect(
heir_fight = regions["Spirit Arena"].connect(
connecting_region=regions["Spirit Arena Victory"],
rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if
world.options.hexagon_quest else
@ -1219,6 +1317,192 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
for region in ladder_regions.values():
world.multiworld.regions.append(region)
# for combat logic, easiest to replace or add to existing rules
if world.options.combat_logic >= CombatLogic.option_bosses_only:
set_rule(wg_to_after_gk,
lambda state: state.has(laurels, player)
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
or has_combat_reqs("Garden Knight", state, player))
# laurels past, or ice grapple it off, or ice grapple to it and fight
set_rule(after_gk_to_wg,
lambda state: state.has(laurels, player)
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
or (has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)
and has_combat_reqs("Garden Knight", state, player)))
if not world.options.hexagon_quest:
add_rule(heir_fight,
lambda state: has_combat_reqs("The Heir", state, player))
if world.options.combat_logic == CombatLogic.option_on:
# these are redundant with combat logic off
regions["Fortress Grave Path Entry"].connect(
connecting_region=regions["Fortress Grave Path Dusty Entrance Region"],
rule=lambda state: state.has(laurels, player))
regions["Rooted Ziggurat Lower Entry"].connect(
connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"],
rule=lambda state: state.has(laurels, player))
regions["Rooted Ziggurat Lower Mid Checkpoint"].connect(
connecting_region=regions["Rooted Ziggurat Lower Entry"],
rule=lambda state: state.has(laurels, player))
add_rule(ow_to_town_portal,
lambda state: has_combat_reqs("Before Well", state, player))
# need to fight through the rudelings and turret, or just laurels from near the windmill
set_rule(ow_to_well_entry,
lambda state: state.has(laurels, player)
or has_combat_reqs("East Forest", state, player))
set_rule(ow_tunnel_beach,
lambda state: has_combat_reqs("East Forest", state, player))
add_rule(atoll_statue,
lambda state: has_combat_reqs("Ruined Atoll", state, player))
set_rule(frogs_front_to_main,
lambda state: has_combat_reqs("Frog's Domain", state, player))
set_rule(btw_front_main,
lambda state: state.has(laurels, player) or has_combat_reqs("Beneath the Well", state, player))
set_rule(btw_back_main,
lambda state: has_ladder("Ladders in Well", state, world)
and (state.has(laurels, player) or has_combat_reqs("Beneath the Well", state, player)))
set_rule(well_boss_to_dt,
lambda state: has_combat_reqs("Beneath the Well", state, player)
or laurels_zip(state, world))
add_rule(dt_entry_to_upper,
lambda state: has_combat_reqs("Dark Tomb", state, player))
add_rule(dt_exit_to_main,
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)
or has_combat_reqs("West Garden", state, player))
set_rule(wg_after_to_before_terry,
lambda state: state.has_any({laurels, ice_dagger}, player)
or has_combat_reqs("West Garden", state, player))
# laurels through, probably to the checkpoint, or just fight
set_rule(wg_checkpoint_to_after_terry,
lambda state: state.has(laurels, player) or has_combat_reqs("West Garden", state, player))
set_rule(wg_checkpoint_to_before_boss,
lambda state: has_combat_reqs("West Garden", state, player))
add_rule(btv_front_to_main,
lambda state: has_combat_reqs("Beneath the Vault", state, player))
add_rule(btv_back_to_main,
lambda state: has_combat_reqs("Beneath the Vault", state, player))
add_rule(fort_upper_lower,
lambda state: state.has(ice_dagger, player)
or has_combat_reqs("Eastern Vault Fortress", state, player))
set_rule(fort_grave_entry_to_combat,
lambda state: has_combat_reqs("Eastern Vault Fortress", state, player))
set_rule(quarry_entry_to_main,
lambda state: has_combat_reqs("Quarry", state, player))
set_rule(quarry_back_to_main,
lambda state: has_combat_reqs("Quarry", state, player))
set_rule(monastery_to_quarry_main,
lambda state: has_combat_reqs("Quarry", state, player))
set_rule(monastery_front_to_back,
lambda state: has_combat_reqs("Quarry", state, player))
set_rule(lower_quarry_empty_to_combat,
lambda state: has_combat_reqs("Quarry", state, player))
set_rule(zig_upper_front_back,
lambda state: state.has(laurels, player)
or has_combat_reqs("Rooted Ziggurat", state, player))
set_rule(zig_low_entry_to_front,
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,
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))
# 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("Cathedral", state, player)))
set_rule(cath_entry_to_main,
lambda state: has_combat_reqs("Cathedral", state, player))
set_rule(cath_elev_to_main,
lambda state: has_combat_reqs("Cathedral", state, player))
# for spots where you can go into and come out of an entrance to reset enemy aggro
if world.options.entrance_rando:
# for the chest outside of magic dagger house
dagger_entry_paired_name, dagger_entry_paired_region = (
get_paired_portal("Archipelagos Redux, archipelagos_house_"))
try:
dagger_entry_paired_entrance = world.get_entrance(dagger_entry_paired_name)
except KeyError:
# there is no paired entrance, so you must fight or dash past, which is done in the finally
pass
else:
set_rule(wg_checkpoint_to_dagger,
lambda state: dagger_entry_paired_entrance.can_reach(state))
world.multiworld.register_indirect_condition(region=regions["West Garden at Dagger House"],
entrance=dagger_entry_paired_entrance)
finally:
add_rule(wg_checkpoint_to_dagger,
lambda state: state.has(laurels, player) or has_combat_reqs("West Garden", state, player),
combine="or")
# zip past enemies in fortress grave path to enter the dusty entrance, then come back out
fort_dusty_paired_name, fort_dusty_paired_region = get_paired_portal("Fortress Reliquary, Dusty_")
try:
fort_dusty_paired_entrance = world.get_entrance(fort_dusty_paired_name)
except KeyError:
# there is no paired entrance, so you can't run past to deaggro
# the path to dusty can be done via combat, so no need to do anything here
pass
else:
# there is a paired entrance, so you can use that to deaggro enemies
regions["Fortress Grave Path Dusty Entrance Region"].connect(
connecting_region=regions["Fortress Grave Path by Grave"],
rule=lambda state: state.has(laurels, player) and fort_dusty_paired_entrance.can_reach(state))
world.multiworld.register_indirect_condition(region=regions["Fortress Grave Path by Grave"],
entrance=fort_dusty_paired_entrance)
# for activating the ladder switch to get from fortress east upper to lower
fort_east_upper_right_paired_name, fort_east_upper_right_paired_region = (
get_paired_portal("Fortress East, Fortress Courtyard_"))
try:
fort_east_upper_right_paired_entrance = (
world.get_entrance(fort_east_upper_right_paired_name))
except KeyError:
# no paired entrance, so you must fight, which is done in the finally
pass
else:
set_rule(fort_east_upper_lower,
lambda state: fort_east_upper_right_paired_entrance.can_reach(state))
world.multiworld.register_indirect_condition(region=regions["Fortress East Shortcut Lower"],
entrance=fort_east_upper_right_paired_entrance)
finally:
add_rule(fort_east_upper_lower,
lambda state: has_combat_reqs("Eastern Vault Fortress", state, player)
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world),
combine="or")
else:
# if combat logic is on and ER is off, we can make this entrance freely
regions["Fortress Grave Path Dusty Entrance Region"].connect(
connecting_region=regions["Fortress Grave Path by Grave"],
rule=lambda state: state.has(laurels, player))
else:
# if combat logic is off, we can make this entrance freely
regions["Fortress Grave Path Dusty Entrance Region"].connect(
connecting_region=regions["Fortress Grave Path by Grave"],
rule=lambda state: state.has(laurels, player))
def set_er_location_rules(world: "TunicWorld") -> None:
player = world.player
@ -1315,6 +1599,11 @@ def set_er_location_rules(world: "TunicWorld") -> None:
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)))
# Dark Tomb
# added to make combat logic smoother
set_rule(world.get_location("Dark Tomb - 2nd Laser Room"),
lambda state: has_lantern(state, world))
# West Garden
set_rule(world.get_location("West Garden - [North] Across From Page Pickup"),
lambda state: state.has(laurels, player))
@ -1348,11 +1637,11 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# Library Lab
set_rule(world.get_location("Library Lab - Page 1"),
lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player))
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_stick(state, player) or state.has_any((fire_wand, gun), player))
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_stick(state, player) or state.has_any((fire_wand, gun), player))
lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player))
# Eastern Vault Fortress
set_rule(world.get_location("Fortress Arena - Hexagon Red"),
@ -1361,11 +1650,11 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# 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: has_stick(state, player) or state.has(ice_dagger, player))
lambda state: has_melee(state, player) or state.has(ice_dagger, player))
# Beneath the Vault
set_rule(world.get_location("Beneath the Fortress - Bridge"),
lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player))
lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player))
# Quarry
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),
@ -1421,9 +1710,9 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# Events
set_rule(world.get_location("Eastern Bell"),
lambda state: (has_stick(state, player) or state.has(fire_wand, player)))
lambda state: (has_melee(state, player) or state.has(fire_wand, player)))
set_rule(world.get_location("Western Bell"),
lambda state: (has_stick(state, player) or state.has(fire_wand, player)))
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"),
@ -1470,3 +1759,129 @@ def set_er_location_rules(world: "TunicWorld") -> None:
lambda state: has_sword(state, player))
set_rule(world.get_location("Shop - Coin 2"),
lambda state: has_sword(state, player))
def combat_logic_to_loc(loc_name: str, combat_req_area: str, set_instead: bool = False,
dagger: bool = False, laurel: bool = False) -> None:
# dagger means you can use magic dagger instead of combat for that check
# laurel means you can dodge the enemies freely with the laurels
if set_instead:
set_rule(world.get_location(loc_name),
lambda state: has_combat_reqs(combat_req_area, state, player)
or (dagger and state.has(ice_dagger, player))
or (laurel and state.has(laurels, player)))
else:
add_rule(world.get_location(loc_name),
lambda state: has_combat_reqs(combat_req_area, state, player)
or (dagger and state.has(ice_dagger, player))
or (laurel and state.has(laurels, player)))
if world.options.combat_logic >= CombatLogic.option_bosses_only:
# garden knight is in the regions part above
combat_logic_to_loc("Fortress Arena - Siege Engine/Vault Key Pickup", "Siege Engine", set_instead=True)
combat_logic_to_loc("Librarian - Hexagon Green", "The Librarian", set_instead=True)
set_rule(world.get_location("Librarian - Hexagon Green"),
rule=lambda state: has_combat_reqs("The Librarian", state, player)
and has_ladder("Ladders in Library", state, world))
combat_logic_to_loc("Rooted Ziggurat Lower - Hexagon Blue", "Boss Scavenger", set_instead=True)
if world.options.ice_grappling >= IceGrappling.option_medium:
add_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"),
lambda state: has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
combat_logic_to_loc("Cathedral Gauntlet - Gauntlet Reward", "Gauntlet", set_instead=True)
if world.options.combat_logic == CombatLogic.option_on:
combat_logic_to_loc("Overworld - [Northeast] Flowers Holy Cross", "Garden Knight")
combat_logic_to_loc("Overworld - [Northwest] Chest Near Quarry Gate", "Before Well", dagger=True)
combat_logic_to_loc("Overworld - [Northeast] Chest Above Patrol Cave", "Garden Knight", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret", "Overworld", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret 2", "Overworld")
combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "East Forest", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] Fountain Holy Cross", "East Forest", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] South Chest Near Guard", "East Forest", dagger=True)
combat_logic_to_loc("Overworld - [Southwest] Tunnel Guarded By Turret", "East Forest", dagger=True)
combat_logic_to_loc("Overworld - [Northwest] Chest Near Turret", "Before Well")
add_rule(world.get_location("Hourglass Cave - Hourglass Chest"),
lambda state: has_sword(state, player) and (state.has("Shield", player)
# kill the turrets through the wall with a longer sword
or state.has("Sword Upgrade", player, 3)))
add_rule(world.get_location("Hourglass Cave - Holy Cross Chest"),
lambda state: has_sword(state, player) and (state.has("Shield", player)
or state.has("Sword Upgrade", player, 3)))
# the first spider chest they literally do not attack you until you open the chest
# the second one, you can still just walk past them, but I guess /something/ would be wanted
combat_logic_to_loc("East Forest - Beneath Spider Chest", "East Forest", dagger=True, laurel=True)
combat_logic_to_loc("East Forest - Golden Obelisk Holy Cross", "East Forest", dagger=True)
combat_logic_to_loc("East Forest - Dancing Fox Spirit Holy Cross", "East Forest", dagger=True, laurel=True)
combat_logic_to_loc("East Forest - From Guardhouse 1 Chest", "East Forest", dagger=True, laurel=True)
combat_logic_to_loc("East Forest - Above Save Point", "East Forest", dagger=True)
combat_logic_to_loc("East Forest - Above Save Point Obscured", "East Forest", dagger=True)
combat_logic_to_loc("Forest Grave Path - Above Gate", "East Forest", dagger=True, laurel=True)
combat_logic_to_loc("Forest Grave Path - Obscured Chest", "East Forest", dagger=True, laurel=True)
# most of beneath the well is covered by the region access rule
combat_logic_to_loc("Beneath the Well - [Entryway] Chest", "Beneath the Well")
combat_logic_to_loc("Beneath the Well - [Entryway] Obscured Behind Waterfall", "Beneath the Well")
combat_logic_to_loc("Beneath the Well - [Back Corridor] Left Secret", "Beneath the Well")
combat_logic_to_loc("Beneath the Well - [Side Room] Chest By Phrends", "Overworld")
# 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))
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")
# with combat logic on, I presume the player will want to be able to see to avoid the spiders
set_rule(world.get_location("Beneath the Fortress - Bridge"),
lambda state: has_lantern(state, world)
and (state.has_any({laurels, fire_wand, "Gun"}, player) or has_melee(state, player)))
combat_logic_to_loc("Eastern Vault Fortress - [West Wing] Candles Holy Cross", "Eastern Vault Fortress",
dagger=True)
# 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 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))
# can get this one without fighting if you have laurels
add_rule(world.get_location("Frog's Domain - Above Vault"),
lambda state: state.has(laurels, player) or has_combat_reqs("Frog's Domain", state, player))
# with wand, you can get this chest. Non-ER, you need laurels to continue down. ER, you can just torch
set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"),
lambda state: (state.has(fire_wand, player)
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))
# replace the sword rule with this one
combat_logic_to_loc("Swamp - [South Graveyard] 4 Orange Skulls", "Swamp", set_instead=True)
combat_logic_to_loc("Swamp - [South Graveyard] Guarded By Big Skeleton", "Swamp", dagger=True)
# don't really agree with this one but eh
combat_logic_to_loc("Swamp - [South Graveyard] Above Big Skeleton", "Swamp", dagger=True, laurel=True)
# the tentacles deal with everything else reasonably, and you can hide on the island, so no rule for it
add_rule(world.get_location("Swamp - [South Graveyard] Obscured Beneath Telescope"),
lambda state: state.has(laurels, player) # can dash from swamp mid to here and grab it
or has_combat_reqs("Swamp", state, player))
add_rule(world.get_location("Swamp - [Central] South Secret Passage"),
lambda state: state.has(laurels, player) # can dash from swamp front to here and grab it
or has_combat_reqs("Swamp", state, player))
combat_logic_to_loc("Swamp - [South Graveyard] Upper Walkway On Pedestal", "Swamp")
combat_logic_to_loc("Swamp - [Central] Beneath Memorial", "Swamp")
combat_logic_to_loc("Swamp - [Central] Near Ramps Up", "Swamp")
combat_logic_to_loc("Swamp - [Upper Graveyard] Near Telescope", "Swamp")
combat_logic_to_loc("Swamp - [Upper Graveyard] Near Shield Fleemers", "Swamp")
combat_logic_to_loc("Swamp - [Upper Graveyard] Obscured Behind Hill", "Swamp")
# zip through the rubble to sneakily grab this chest, or just fight to it
add_rule(world.get_location("Cathedral - [1F] Near Spikes"),
lambda state: laurels_zip(state, world) or has_combat_reqs("Cathedral", state, player))

View File

@ -22,10 +22,19 @@ class TunicERLocation(Location):
def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
regions: Dict[str, Region] = {}
for region_name, region_data in world.er_regions.items():
regions[region_name] = Region(region_name, world.player, world.multiworld)
if world.options.entrance_rando:
for region_name, region_data in world.er_regions.items():
# if fewer shops is off, zig skip is not made
if region_name == "Zig Skip Exit":
# need to check if there's a seed group for this first
if world.options.entrance_rando.value not in EntranceRando.options.values():
if not world.seed_groups[world.options.entrance_rando.value]["fixed_shop"]:
continue
elif not world.options.fixed_shop:
continue
regions[region_name] = Region(region_name, world.player, world.multiworld)
portal_pairs = pair_portals(world, regions)
# output the entrances to the spoiler log here for convenience
@ -33,8 +42,15 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
for portal1, portal2 in sorted_portal_pairs.items():
world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player)
else:
for region_name, region_data in world.er_regions.items():
# filter out regions that are inaccessible in non-er
if region_name not in ["Zig Skip Exit", "Purgatory"]:
regions[region_name] = Region(region_name, world.player, world.multiworld)
portal_pairs = vanilla_portals(world, regions)
create_randomized_entrances(portal_pairs, regions)
set_er_region_rules(world, regions, portal_pairs)
for location_name, location_id in world.location_name_to_id.items():
@ -42,8 +58,6 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
location = TunicERLocation(world.player, location_name, location_id, region)
region.locations.append(location)
create_randomized_entrances(portal_pairs, regions)
for region in regions.values():
world.multiworld.regions.append(region)
@ -70,7 +84,7 @@ tunic_events: Dict[str, str] = {
"Quarry Connector Fuse": "Quarry Connector",
"Quarry Fuse": "Quarry Entry",
"Ziggurat Fuse": "Rooted Ziggurat Lower Back",
"West Garden Fuse": "West Garden",
"West Garden Fuse": "West Garden South Checkpoint",
"Library Fuse": "Library Lab",
"Place Questagons": "Sealed Temple",
}
@ -108,7 +122,8 @@ def create_shop_region(world: "TunicWorld", regions: Dict[str, Region]) -> None:
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 != "Ziggurat Lower Falling Entrance"]
portal_map = [portal for portal in portal_mapping if portal.name not in
["Ziggurat Lower Falling Entrance", "Purgatory Bottom Exit", "Purgatory Top Exit"]]
while portal_map:
portal1 = portal_map[0]
@ -121,9 +136,6 @@ def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Por
destination="Previous Region", tag="_")
create_shop_region(world, regions)
elif portal2_sdt == "Purgatory, Purgatory_bottom":
portal2_sdt = "Purgatory, Purgatory_top"
for portal in portal_map:
if portal.scene_destination() == portal2_sdt:
portal2 = portal
@ -414,6 +426,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
cr.add(portal.region)
if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks):
continue
# if not waterfall_plando, then we just want to pair secret gathering place now
elif portal.region != "Secret Gathering Place":
continue
portal2 = portal

View File

@ -1,5 +1,5 @@
from itertools import groupby
from typing import Dict, List, Set, NamedTuple
from typing import Dict, List, Set, NamedTuple, Optional
from BaseClasses import ItemClassification as IC
@ -8,6 +8,8 @@ class TunicItemData(NamedTuple):
quantity_in_item_pool: int
item_id_offset: int
item_group: str = ""
# classification if combat logic is on
combat_ic: Optional[IC] = None
item_base_id = 509342400
@ -27,7 +29,7 @@ item_table: Dict[str, TunicItemData] = {
"Lure x2": TunicItemData(IC.filler, 1, 11, "Consumables"),
"Pepper x2": TunicItemData(IC.filler, 4, 12, "Consumables"),
"Ivy x3": TunicItemData(IC.filler, 2, 13, "Consumables"),
"Effigy": TunicItemData(IC.useful, 12, 14, "Money"),
"Effigy": TunicItemData(IC.useful, 12, 14, "Money", combat_ic=IC.progression),
"HP Berry": TunicItemData(IC.filler, 2, 15, "Consumables"),
"HP Berry x2": TunicItemData(IC.filler, 4, 16, "Consumables"),
"HP Berry x3": TunicItemData(IC.filler, 2, 17, "Consumables"),
@ -44,32 +46,32 @@ item_table: Dict[str, TunicItemData] = {
"Hero's Laurels": TunicItemData(IC.progression | IC.useful, 1, 28),
"Lantern": TunicItemData(IC.progression, 1, 29),
"Gun": TunicItemData(IC.progression | IC.useful, 1, 30, "Weapons"),
"Shield": TunicItemData(IC.useful, 1, 31),
"Shield": TunicItemData(IC.useful, 1, 31, combat_ic=IC.progression | IC.useful),
"Dath Stone": TunicItemData(IC.useful, 1, 32),
"Hourglass": TunicItemData(IC.useful, 1, 33),
"Old House Key": TunicItemData(IC.progression, 1, 34, "Keys"),
"Key": TunicItemData(IC.progression, 2, 35, "Keys"),
"Fortress Vault Key": TunicItemData(IC.progression, 1, 36, "Keys"),
"Flask Shard": TunicItemData(IC.useful, 12, 37),
"Potion Flask": TunicItemData(IC.useful, 5, 38, "Flask"),
"Flask Shard": TunicItemData(IC.useful, 12, 37, combat_ic=IC.progression),
"Potion Flask": TunicItemData(IC.useful, 5, 38, "Flask", combat_ic=IC.progression),
"Golden Coin": TunicItemData(IC.progression, 17, 39),
"Card Slot": TunicItemData(IC.useful, 4, 40),
"Red Questagon": TunicItemData(IC.progression_skip_balancing, 1, 41, "Hexagons"),
"Green Questagon": TunicItemData(IC.progression_skip_balancing, 1, 42, "Hexagons"),
"Blue Questagon": TunicItemData(IC.progression_skip_balancing, 1, 43, "Hexagons"),
"Gold Questagon": TunicItemData(IC.progression_skip_balancing, 0, 44, "Hexagons"),
"ATT Offering": TunicItemData(IC.useful, 4, 45, "Offerings"),
"DEF Offering": TunicItemData(IC.useful, 4, 46, "Offerings"),
"Potion Offering": TunicItemData(IC.useful, 3, 47, "Offerings"),
"HP Offering": TunicItemData(IC.useful, 6, 48, "Offerings"),
"MP Offering": TunicItemData(IC.useful, 3, 49, "Offerings"),
"SP Offering": TunicItemData(IC.useful, 2, 50, "Offerings"),
"Hero Relic - ATT": TunicItemData(IC.progression_skip_balancing, 1, 51, "Hero Relics"),
"Hero Relic - DEF": TunicItemData(IC.progression_skip_balancing, 1, 52, "Hero Relics"),
"Hero Relic - HP": TunicItemData(IC.progression_skip_balancing, 1, 53, "Hero Relics"),
"Hero Relic - MP": TunicItemData(IC.progression_skip_balancing, 1, 54, "Hero Relics"),
"Hero Relic - POTION": TunicItemData(IC.progression_skip_balancing, 1, 55, "Hero Relics"),
"Hero Relic - SP": TunicItemData(IC.progression_skip_balancing, 1, 56, "Hero Relics"),
"ATT Offering": TunicItemData(IC.useful, 4, 45, "Offerings", combat_ic=IC.progression),
"DEF Offering": TunicItemData(IC.useful, 4, 46, "Offerings", combat_ic=IC.progression),
"Potion Offering": TunicItemData(IC.useful, 3, 47, "Offerings", combat_ic=IC.progression),
"HP Offering": TunicItemData(IC.useful, 6, 48, "Offerings", combat_ic=IC.progression),
"MP Offering": TunicItemData(IC.useful, 3, 49, "Offerings", combat_ic=IC.progression),
"SP Offering": TunicItemData(IC.useful, 2, 50, "Offerings", combat_ic=IC.progression),
"Hero Relic - ATT": TunicItemData(IC.progression_skip_balancing, 1, 51, "Hero Relics", combat_ic=IC.progression),
"Hero Relic - DEF": TunicItemData(IC.progression_skip_balancing, 1, 52, "Hero Relics", combat_ic=IC.progression),
"Hero Relic - HP": TunicItemData(IC.progression_skip_balancing, 1, 53, "Hero Relics", combat_ic=IC.progression),
"Hero Relic - MP": TunicItemData(IC.progression_skip_balancing, 1, 54, "Hero Relics", combat_ic=IC.progression),
"Hero Relic - POTION": TunicItemData(IC.progression_skip_balancing, 1, 55, "Hero Relics", combat_ic=IC.progression),
"Hero Relic - SP": TunicItemData(IC.progression_skip_balancing, 1, 56, "Hero Relics", combat_ic=IC.progression),
"Orange Peril Ring": TunicItemData(IC.useful, 1, 57, "Cards"),
"Tincture": TunicItemData(IC.useful, 1, 58, "Cards"),
"Scavenger Mask": TunicItemData(IC.progression, 1, 59, "Cards"),
@ -86,18 +88,18 @@ item_table: Dict[str, TunicItemData] = {
"Louder Echo": TunicItemData(IC.useful, 1, 70, "Cards"),
"Aura's Gem": TunicItemData(IC.useful, 1, 71, "Cards"),
"Bone Card": TunicItemData(IC.useful, 1, 72, "Cards"),
"Mr Mayor": TunicItemData(IC.useful, 1, 73, "Golden Treasures"),
"Secret Legend": TunicItemData(IC.useful, 1, 74, "Golden Treasures"),
"Sacred Geometry": TunicItemData(IC.useful, 1, 75, "Golden Treasures"),
"Vintage": TunicItemData(IC.useful, 1, 76, "Golden Treasures"),
"Just Some Pals": TunicItemData(IC.useful, 1, 77, "Golden Treasures"),
"Regal Weasel": TunicItemData(IC.useful, 1, 78, "Golden Treasures"),
"Spring Falls": TunicItemData(IC.useful, 1, 79, "Golden Treasures"),
"Power Up": TunicItemData(IC.useful, 1, 80, "Golden Treasures"),
"Back To Work": TunicItemData(IC.useful, 1, 81, "Golden Treasures"),
"Phonomath": TunicItemData(IC.useful, 1, 82, "Golden Treasures"),
"Dusty": TunicItemData(IC.useful, 1, 83, "Golden Treasures"),
"Forever Friend": TunicItemData(IC.useful, 1, 84, "Golden Treasures"),
"Mr Mayor": TunicItemData(IC.useful, 1, 73, "Golden Treasures", combat_ic=IC.progression),
"Secret Legend": TunicItemData(IC.useful, 1, 74, "Golden Treasures", combat_ic=IC.progression),
"Sacred Geometry": TunicItemData(IC.useful, 1, 75, "Golden Treasures", combat_ic=IC.progression),
"Vintage": TunicItemData(IC.useful, 1, 76, "Golden Treasures", combat_ic=IC.progression),
"Just Some Pals": TunicItemData(IC.useful, 1, 77, "Golden Treasures", combat_ic=IC.progression),
"Regal Weasel": TunicItemData(IC.useful, 1, 78, "Golden Treasures", combat_ic=IC.progression),
"Spring Falls": TunicItemData(IC.useful, 1, 79, "Golden Treasures", combat_ic=IC.progression),
"Power Up": TunicItemData(IC.useful, 1, 80, "Golden Treasures", combat_ic=IC.progression),
"Back To Work": TunicItemData(IC.useful, 1, 81, "Golden Treasures", combat_ic=IC.progression),
"Phonomath": TunicItemData(IC.useful, 1, 82, "Golden Treasures", combat_ic=IC.progression),
"Dusty": TunicItemData(IC.useful, 1, 83, "Golden Treasures", combat_ic=IC.progression),
"Forever Friend": TunicItemData(IC.useful, 1, 84, "Golden Treasures", combat_ic=IC.progression),
"Fool Trap": TunicItemData(IC.trap, 0, 85),
"Money x1": TunicItemData(IC.filler, 3, 86, "Money"),
"Money x10": TunicItemData(IC.filler, 1, 87, "Money"),
@ -112,9 +114,9 @@ item_table: Dict[str, TunicItemData] = {
"Money x50": TunicItemData(IC.filler, 7, 96, "Money"),
"Money x64": TunicItemData(IC.filler, 1, 97, "Money"),
"Money x100": TunicItemData(IC.filler, 5, 98, "Money"),
"Money x128": TunicItemData(IC.useful, 3, 99, "Money"),
"Money x200": TunicItemData(IC.useful, 1, 100, "Money"),
"Money x255": TunicItemData(IC.useful, 1, 101, "Money"),
"Money x128": TunicItemData(IC.useful, 3, 99, "Money", combat_ic=IC.progression),
"Money x200": TunicItemData(IC.useful, 1, 100, "Money", combat_ic=IC.progression),
"Money x255": TunicItemData(IC.useful, 1, 101, "Money", combat_ic=IC.progression),
"Pages 0-1": TunicItemData(IC.useful, 1, 102, "Pages"),
"Pages 2-3": TunicItemData(IC.useful, 1, 103, "Pages"),
"Pages 4-5": TunicItemData(IC.useful, 1, 104, "Pages"),
@ -206,6 +208,10 @@ slot_data_item_names = [
"Gold Questagon",
]
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"])
item_name_to_id: Dict[str, int] = {name: item_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]

View File

@ -78,9 +78,11 @@ easy_ls: List[LadderInfo] = [
# West Garden
# exit after Garden Knight
LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_upper"),
LadderInfo("West Garden before Boss", "Archipelagos Redux, Overworld Redux_upper"),
# West Garden laurels exit
LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_lowest"),
LadderInfo("West Garden after Terry", "Archipelagos Redux, Overworld Redux_lowest"),
# Magic dagger house, only relevant with combat logic on
LadderInfo("West Garden after Terry", "Archipelagos Redux, archipelagos_house_"),
# Atoll, use the little ladder you fix at the beginning
LadderInfo("Ruined Atoll", "Atoll Redux, Overworld Redux_lower"),
@ -159,7 +161,8 @@ medium_ls: List[LadderInfo] = [
LadderInfo("Quarry Back", "Quarry Redux, Monastery_back"),
LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_2_"),
LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Front", dest_is_region=True),
LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Entry", dest_is_region=True),
LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Mid Checkpoint", dest_is_region=True),
# Swamp to Overworld upper
LadderInfo("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", "Ladders in Swamp"),
@ -172,9 +175,9 @@ hard_ls: List[LadderInfo] = [
LadderInfo("Beneath the Well Front", "Sewer, Overworld Redux_west_aqueduct", "Ladders in Well"),
LadderInfo("Beneath the Well Front", "Beneath the Well Back", "Ladders in Well", dest_is_region=True),
# go through the hexagon engraving above the vault door
LadderInfo("Frog's Domain", "frog cave main, Frog Stairs_Exit", "Ladders to Frog's Domain"),
LadderInfo("Frog's Domain Front", "frog cave main, Frog Stairs_Exit", "Ladders to Frog's Domain"),
# the turret at the end here is not affected by enemy rando
LadderInfo("Frog's Domain", "Frog's Domain Back", "Ladders to Frog's Domain", dest_is_region=True),
LadderInfo("Frog's Domain Front", "Frog's Domain Back", "Ladders to Frog's Domain", dest_is_region=True),
# todo: see if we can use that new laurels strat here
# LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_FTRoom_"),
# go behind the cathedral to reach the door, pretty easily doable

View File

@ -25,17 +25,17 @@ location_table: Dict[str, TunicLocationData] = {
"Beneath the Well - [Side Room] Chest By Phrends": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
"Beneath the Well - [Second Room] Page": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Dark Tomb Checkpoint - [Passage To Dark Tomb] Page Pickup": TunicLocationData("Overworld", "Dark Tomb Checkpoint"),
"Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral Entry"), # entry because special rules
"Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral Entry"),
"Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral Entry"),
"Dark Tomb - Spike Maze Near Exit": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Dark Exit"),
"Dark Tomb - 1st Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - Spike Maze Upper Walkway": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - Skulls Chest": TunicLocationData("Dark Tomb", "Dark Tomb Upper"),
@ -81,25 +81,25 @@ location_table: Dict[str, TunicLocationData] = {
"Eastern Vault Fortress - [East Wing] Bombable Wall": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Eastern Vault Fortress - [West Wing] Page Pickup": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Fortress Grave Path - Upper Walkway": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path Upper"),
"Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"),
"Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"),
"Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path by Grave"),
"Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path by Grave"),
"Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"),
"Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
"Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
"Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Main"),
"Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
"Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
"Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
"Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
"Frog's Domain - Escape Chest": TunicLocationData("Frog's Domain", "Frog's Domain Back"),
"Frog's Domain - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Main Room Top Floor": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Main Room Bottom Floor": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Side Room Secret Passage": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain Front"),
"Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain Front"),
"Frog's Domain - Main Room Top Floor": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
"Frog's Domain - Main Room Bottom Floor": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
"Frog's Domain - Side Room Secret Passage": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
"Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
"Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain Front"),
"Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
"Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="Bosses"),
"Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="Holy Cross"),
"Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"),
@ -131,7 +131,7 @@ location_table: Dict[str, TunicLocationData] = {
"Overworld - [Southwest] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld Beach"),
"Overworld - [Southwest] Chest Guarded By Turret": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Shadowy Corner Chest": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld Tunnel to Beach"),
"Overworld - [Southwest] Grapple Chest Over Walkway": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld after Envoy"),
"Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld Swamp Lower Entry"),
@ -158,7 +158,7 @@ location_table: Dict[str, TunicLocationData] = {
"Overworld - [Northwest] Page on Pillar by Dark Tomb": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Upper Overworld"),
"Overworld - [West] Page On Teleporter": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld Well Entry Area"),
"Patrol Cave - Normal Chest": TunicLocationData("Overworld", "Patrol Cave"),
"Ruined Shop - Chest 1": TunicLocationData("Overworld", "Ruined Shop"),
"Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"),
@ -233,17 +233,17 @@ location_table: Dict[str, TunicLocationData] = {
"Quarry - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "Even Lower Quarry"),
"Quarry - [West] Lower Area Below Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Lower Area Isolated Chest": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Even Lower Quarry"),
"Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Even Lower Quarry Isolated Chest"),
"Quarry - [West] Lower Area After Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Rooted Ziggurat Upper - Near Bridge Switch": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Front"),
"Rooted Ziggurat Upper - Beneath Bridge To Administrator": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Back"),
"Rooted Ziggurat Tower - Inside Tower": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Middle Top"),
"Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"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 - 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 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"),
"Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back", location_group="Bosses"),
"Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
@ -290,26 +290,26 @@ location_table: Dict[str, TunicLocationData] = {
"Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"),
"West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"),
"Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"),
"West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
"West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
"West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden"),
"West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
"West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden"),
"West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Below Left Walkway": TunicLocationData("West Garden", "West Garden"),
"West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden"),
"West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden"),
"West Garden - [North] Obscured Beneath Hero's Memorial": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden"),
"West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden before Boss", location_group="Holy Cross"),
"West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden after Terry", location_group="Holy Cross"),
"West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden at Dagger House"),
"West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden South Checkpoint"),
"West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden before Terry", location_group="Holy Cross"),
"West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden before Boss"),
"West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden after Terry"),
"West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden before Terry"),
"West Garden - [Central Lowlands] Below Left Walkway": TunicLocationData("West Garden", "West Garden after Terry"),
"West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden after Terry"),
"West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden after Terry"),
"West Garden - [North] Obscured Beneath Hero's Memorial": TunicLocationData("West Garden", "West Garden before Terry"),
"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] 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"),
"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"),
"West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden"),
"West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden before Terry"),
"West Garden House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"),
"Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"),
}

View File

@ -168,6 +168,22 @@ class TunicPlandoConnections(PlandoConnections):
duplicate_exits = True
class CombatLogic(Choice):
"""
If enabled, the player will logically require a combination of stat upgrade items and equipment to get some checks or navigate to some areas, with a goal of matching the vanilla combat difficulty.
The player may still be expected to run past enemies, reset aggro (by using a checkpoint or doing a scene transition), or find sneaky paths to checks.
This option marks many more items as progression and may force weapons much earlier than normal.
Bosses Only makes it so that additional combat logic is only added to the boss fights and the Gauntlet.
If disabled, the standard, looser logic is used. The standard logic does not include stat upgrades, just minimal weapon requirements, such as requiring a Sword or Magic Wand for Quarry, or not requiring a weapon for Swamp.
"""
internal_name = "combat_logic"
display_name = "More Combat Logic"
option_off = 0
option_bosses_only = 1
option_on = 2
default = 0
class LaurelsZips(Toggle):
"""
Choose whether to include using the Hero's Laurels to zip through gates, doors, and tricky spots.
@ -259,6 +275,7 @@ class TunicOptions(PerGameCommonOptions):
hexagon_goal: HexagonGoal
extra_hexagon_percentage: ExtraHexagonPercentage
laurels_location: LaurelsLocation
combat_logic: CombatLogic
lanternless: Lanternless
maskless: Maskless
laurels_zips: LaurelsZips
@ -272,6 +289,7 @@ class TunicOptions(PerGameCommonOptions):
tunic_option_groups = [
OptionGroup("Logic Options", [
CombatLogic,
Lanternless,
Maskless,
LaurelsZips,

View File

@ -56,9 +56,8 @@ def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bo
# a check to see if you can whack things in melee at all
def has_stick(state: CollectionState, player: int) -> bool:
return (state.has("Stick", player) or state.has("Sword Upgrade", player, 1)
or state.has("Sword", player))
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:
@ -83,7 +82,7 @@ def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool:
return False
if world.options.ladder_storage_without_items:
return True
return has_stick(state, world.player) or state.has_any((grapple, shield), world.player)
return has_melee(state, world.player) or state.has_any((grapple, shield), world.player)
def has_mask(state: CollectionState, world: "TunicWorld") -> bool:
@ -101,7 +100,7 @@ def set_region_rules(world: "TunicWorld") -> None:
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_stick(state, player) or state.has(fire_wand, player)
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
@ -117,7 +116,7 @@ def set_region_rules(world: "TunicWorld") -> None:
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_stick(state, player) or state.has_any((gun, grapple, fire_wand), player)))
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)
world.get_entrance("Overworld -> Quarry").access_rule = \
@ -237,7 +236,7 @@ def set_location_rules(world: "TunicWorld") -> None:
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_stick(state, player) or state.has_any({fire_wand, laurels}, player))
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))
@ -301,18 +300,18 @@ def set_location_rules(world: "TunicWorld") -> None:
# Library Lab
set_rule(world.get_location("Library Lab - Page 1"),
lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player))
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_stick(state, player) or state.has_any((fire_wand, gun), player))
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_stick(state, player) or state.has_any((fire_wand, gun), player))
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_stick(state, player) or state.has(ice_dagger, player)))
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)
@ -324,9 +323,9 @@ def set_location_rules(world: "TunicWorld") -> None:
# Beneath the Vault
set_rule(world.get_location("Beneath the Fortress - Bridge"),
lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player))
lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player))
set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"),
lambda state: has_stick(state, player) and has_lantern(state, world))
lambda state: has_melee(state, player) and has_lantern(state, world))
# Quarry
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),

View File

@ -3,6 +3,8 @@ from .. import options
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"])
@ -61,7 +63,9 @@ class TestNormalGoal(TunicTestBase):
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.HexagonQuest.internal_name: options.HexagonQuest.option_false,
options.CombatLogic.internal_name: options.CombatLogic.option_off,
options.FixedShop.internal_name: options.FixedShop.option_true}
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