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 typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union
from logging import warning from logging import warning
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld 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 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 .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 .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
from .er_rules import set_er_location_rules 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 .er_data import portal_mapping, RegionInfo, tunic_er_regions
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections, from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage) LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage)
from .combat_logic import area_data, CombatState
from worlds.AutoWorld import WebWorld, World from worlds.AutoWorld import WebWorld, World
from Options import PlandoConnection from Options import PlandoConnection
from decimal import Decimal, ROUND_HALF_UP from decimal import Decimal, ROUND_HALF_UP
@ -127,11 +129,21 @@ class TunicWorld(World):
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"] self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
self.options.fixed_shop.value = self.options.fixed_shop.option_false self.options.fixed_shop.value = self.options.fixed_shop.option_false
self.options.laurels_location.value = self.options.laurels_location.option_anywhere self.options.laurels_location.value = self.options.laurels_location.option_anywhere
self.options.combat_logic.value = passthrough["combat_logic"]
@classmethod @classmethod
def stage_generate_early(cls, multiworld: MultiWorld) -> None: def stage_generate_early(cls, multiworld: MultiWorld) -> None:
tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC") tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
for tunic in tunic_worlds: 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 it's one of the options, then it isn't a custom seed group
if tunic.options.entrance_rando.value in EntranceRando.options.values(): if tunic.options.entrance_rando.value in EntranceRando.options.values():
continue continue
@ -190,10 +202,12 @@ class TunicWorld(World):
def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem: def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
item_data = item_table[name] 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: def create_items(self) -> None:
tunic_items: List[TunicItem] = [] tunic_items: List[TunicItem] = []
self.slot_data_items = [] 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 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"]
self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"] 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 # Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance
if self.options.entrance_rando or self.options.shuffle_ladders: if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
portal_pairs = create_er_regions(self) portal_pairs = create_er_regions(self)
if self.options.entrance_rando: if self.options.entrance_rando:
# these get interpreted by the game to tell it which entrances to connect # these get interpreted by the game to tell it which entrances to connect
for portal1, portal2 in portal_pairs.items(): for portal1, portal2 in portal_pairs.items():
self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination() self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination()
else: else:
# for non-ER, non-ladders # uses the original rules, easier to navigate and reference
for region_name in tunic_regions: for region_name in tunic_regions:
region = Region(region_name, self.player, self.multiworld) region = Region(region_name, self.player, self.multiworld)
self.multiworld.regions.append(region) self.multiworld.regions.append(region)
@ -351,7 +365,8 @@ class TunicWorld(World):
victory_region.locations.append(victory_location) victory_region.locations.append(victory_location)
def set_rules(self) -> None: 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) set_er_location_rules(self)
else: else:
set_region_rules(self) set_region_rules(self)
@ -360,6 +375,19 @@ class TunicWorld(World):
def get_filler_item_name(self) -> str: def get_filler_item_name(self) -> str:
return self.random.choice(filler_items) 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: def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
if self.options.entrance_rando: if self.options.entrance_rando:
hint_data.update({self.player: {}}) hint_data.update({self.player: {}})
@ -426,6 +454,7 @@ class TunicWorld(World):
"maskless": self.options.maskless.value, "maskless": self.options.maskless.value,
"entrance_rando": int(bool(self.options.entrance_rando.value)), "entrance_rando": int(bool(self.options.entrance_rando.value)),
"shuffle_ladders": self.options.shuffle_ladders.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 Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"], "Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
"Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"], "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

@ -235,12 +235,12 @@ portal_mapping: List[Portal] = [
destination="Sewer_Boss", tag="_"), destination="Sewer_Boss", tag="_"),
Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", Portal(name="Well Exit towards Furnace", region="Beneath the Well Back",
destination="Overworld Redux", tag="_west_aqueduct"), destination="Overworld Redux", tag="_west_aqueduct"),
Portal(name="Well Boss to Well", region="Well Boss", Portal(name="Well Boss to Well", region="Well Boss",
destination="Sewer", tag="_"), destination="Sewer", tag="_"),
Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint",
destination="Crypt Redux", tag="_"), destination="Crypt Redux", tag="_"),
Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point",
destination="Overworld Redux", tag="_"), destination="Overworld Redux", tag="_"),
Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit",
@ -248,13 +248,13 @@ portal_mapping: List[Portal] = [
Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point", Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point",
destination="Sewer_Boss", tag="_"), 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"), 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="_"), destination="archipelagos_house", tag="_"),
Portal(name="West Garden Exit after Boss", region="West Garden after Boss", Portal(name="West Garden Exit after Boss", region="West Garden after Boss",
destination="Overworld Redux", tag="_upper"), 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="_"), destination="Shop", tag="_"),
Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region", Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region",
destination="Overworld Redux", tag="_lowest"), destination="Overworld Redux", tag="_lowest"),
@ -262,7 +262,7 @@ portal_mapping: List[Portal] = [
destination="RelicVoid", tag="_teleporter_relic plinth"), destination="RelicVoid", tag="_teleporter_relic plinth"),
Portal(name="West Garden to Far Shore", region="West Garden Portal", Portal(name="West Garden to Far Shore", region="West Garden Portal",
destination="Transit", tag="_teleporter_archipelagos_teleporter"), destination="Transit", tag="_teleporter_archipelagos_teleporter"),
Portal(name="Magic Dagger House Exit", region="Magic Dagger House", Portal(name="Magic Dagger House Exit", region="Magic Dagger House",
destination="Archipelagos Redux", tag="_"), destination="Archipelagos Redux", tag="_"),
@ -308,7 +308,7 @@ portal_mapping: List[Portal] = [
Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper", Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper",
destination="Fortress Main", tag="_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"), destination="Fortress Courtyard", tag="_Lower"),
Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region", Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region",
destination="RelicVoid", tag="_teleporter_relic plinth"), destination="RelicVoid", tag="_teleporter_relic plinth"),
@ -339,7 +339,7 @@ portal_mapping: List[Portal] = [
destination="Frog Stairs", tag="_eye"), destination="Frog Stairs", tag="_eye"),
Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth",
destination="Frog Stairs", tag="_mouth"), destination="Frog Stairs", tag="_mouth"),
Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit", Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit",
destination="Atoll Redux", tag="_eye"), destination="Atoll Redux", tag="_eye"),
Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper", Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper",
@ -348,39 +348,39 @@ portal_mapping: List[Portal] = [
destination="frog cave main", tag="_Entrance"), destination="frog cave main", tag="_Entrance"),
Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower", Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower",
destination="frog cave main", tag="_Exit"), destination="frog cave main", tag="_Exit"),
Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry", Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry",
destination="Frog Stairs", tag="_Entrance"), destination="Frog Stairs", tag="_Entrance"),
Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back",
destination="Frog Stairs", tag="_Exit"), destination="Frog Stairs", tag="_Exit"),
Portal(name="Library Exterior Tree", region="Library Exterior Tree Region", Portal(name="Library Exterior Tree", region="Library Exterior Tree Region",
destination="Atoll Redux", tag="_"), destination="Atoll Redux", tag="_"),
Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region", Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region",
destination="Library Hall", tag="_"), destination="Library Hall", tag="_"),
Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf", Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf",
destination="Library Exterior", tag="_"), destination="Library Exterior", tag="_"),
Portal(name="Library Hero's Grave", region="Library Hero's Grave Region", Portal(name="Library Hero's Grave", region="Library Hero's Grave Region",
destination="RelicVoid", tag="_teleporter_relic plinth"), destination="RelicVoid", tag="_teleporter_relic plinth"),
Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda", Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda",
destination="Library Rotunda", tag="_"), destination="Library Rotunda", tag="_"),
Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall", Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall",
destination="Library Hall", tag="_"), destination="Library Hall", tag="_"),
Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab", Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab",
destination="Library Lab", tag="_"), destination="Library Lab", tag="_"),
Portal(name="Library Lab to Rotunda", region="Library Lab Lower", Portal(name="Library Lab to Rotunda", region="Library Lab Lower",
destination="Library Rotunda", tag="_"), destination="Library Rotunda", tag="_"),
Portal(name="Library to Far Shore", region="Library Portal", Portal(name="Library to Far Shore", region="Library Portal",
destination="Transit", tag="_teleporter_library teleporter"), destination="Transit", tag="_teleporter_library teleporter"),
Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian", Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian",
destination="Library Arena", tag="_"), destination="Library Arena", tag="_"),
Portal(name="Librarian Arena Exit", region="Library Arena", Portal(name="Librarian Arena Exit", region="Library Arena",
destination="Library Lab", tag="_"), destination="Library Lab", tag="_"),
Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs", Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs",
destination="Mountaintop", tag="_"), destination="Mountaintop", tag="_"),
Portal(name="Mountain to Quarry", region="Lower Mountain", Portal(name="Mountain to Quarry", region="Lower Mountain",
@ -433,7 +433,7 @@ portal_mapping: List[Portal] = [
Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom", Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom",
destination="ziggurat2020_3", tag="_"), 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="_"), destination="ziggurat2020_2", tag="_"),
Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance", Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance",
destination="ziggurat2020_FTRoom", tag="_"), destination="ziggurat2020_FTRoom", tag="_"),
@ -461,7 +461,7 @@ portal_mapping: List[Portal] = [
Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave Region", Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave Region",
destination="RelicVoid", tag="_teleporter_relic plinth"), 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"), destination="Swamp Redux 2", tag="_main"),
Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet", Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet",
destination="Cathedral Arena", tag="_"), destination="Cathedral Arena", tag="_"),
@ -523,7 +523,6 @@ class RegionInfo(NamedTuple):
game_scene: str # the name of the scene in the actual game game_scene: str # the name of the scene in the actual game
dead_end: int = 0 # if a region has only one exit dead_end: int = 0 # if a region has only one exit
outlet_region: Optional[str] = None outlet_region: Optional[str] = None
is_fake_region: bool = False
# gets the outlet region name if it exists, the region if it doesn't # 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 Upper": RegionInfo("Overworld Redux"), # usually leads to garden knight
"Overworld to West Garden from Furnace": RegionInfo("Overworld Redux"), # isolated stairway with one chest "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 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 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 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 "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 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 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 "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), "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 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 by Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
"West Garden Portal Item": 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 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 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": RegionInfo("Atoll Redux"),
"Ruined Atoll Lower Entry Area": 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 "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 Upper": RegionInfo("Frog Stairs"),
"Frog Stairs Lower": RegionInfo("Frog Stairs"), "Frog Stairs Lower": RegionInfo("Frog Stairs"),
"Frog Stairs to Frog's Domain": RegionInfo("Frog Stairs"), "Frog Stairs to Frog's Domain": RegionInfo("Frog Stairs"),
"Frog's Domain Entry": RegionInfo("frog cave main"), "Frog's Domain Entry": RegionInfo("frog cave main"), # just the ladder
"Frog's Domain": RegionInfo("frog cave main"), "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"), "Frog's Domain Back": RegionInfo("frog cave main"),
"Library Exterior Tree Region": RegionInfo("Library Exterior", outlet_region="Library Exterior by Tree"), "Library Exterior Tree Region": RegionInfo("Library Exterior", outlet_region="Library Exterior by Tree"),
"Library Exterior by Tree": RegionInfo("Library Exterior"), "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 Rotunda to Lab": RegionInfo("Library Rotunda"),
"Library Lab": RegionInfo("Library Lab"), "Library Lab": RegionInfo("Library Lab"),
"Library Lab Lower": 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 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 Lab to Librarian": RegionInfo("Library Lab"),
"Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats), "Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats),
"Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"), "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"), "Eastern Vault Fortress Gold Door": RegionInfo("Fortress Main"),
"Fortress East Shortcut Upper": RegionInfo("Fortress East"), "Fortress East Shortcut Upper": RegionInfo("Fortress East"),
"Fortress East Shortcut Lower": 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 Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted),
"Fortress Grave Path Dusty Entrance Region": RegionInfo("Fortress Reliquary"), "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 Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats),
"Fortress Arena": RegionInfo("Fortress Arena"), "Fortress Arena": RegionInfo("Fortress Arena"),
"Fortress Arena Portal": RegionInfo("Fortress Arena", outlet_region="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"), "Monastery Rope": RegionInfo("Quarry Redux"),
"Lower Quarry": RegionInfo("Quarry Redux"), "Lower Quarry": RegionInfo("Quarry Redux"),
"Even 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"), "Lower Quarry Zig Door": RegionInfo("Quarry Redux"),
"Rooted Ziggurat Entry": RegionInfo("ziggurat2020_0"), "Rooted Ziggurat Entry": RegionInfo("ziggurat2020_0"),
"Rooted Ziggurat Upper Entry": RegionInfo("ziggurat2020_1"), "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 Upper Back": RegionInfo("ziggurat2020_1"), # after the administrator
"Rooted Ziggurat Middle Top": RegionInfo("ziggurat2020_2"), "Rooted Ziggurat Middle Top": RegionInfo("ziggurat2020_2"),
"Rooted Ziggurat Middle Bottom": 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 "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 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": RegionInfo("ziggurat2020_FTRoom", outlet_region="Rooted Ziggurat Portal Room"),
"Rooted Ziggurat Portal Room": RegionInfo("ziggurat2020_FTRoom"), "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 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 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 "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 "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"), "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 "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 to Gauntlet": RegionInfo("Cathedral Redux"), # the elevator
"Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats), "Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats),
"Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"), "Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"),
@ -741,7 +753,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
"Purgatory": RegionInfo("Purgatory"), "Purgatory": RegionInfo("Purgatory"),
"Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats), "Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats),
"Spirit Arena": RegionInfo("Spirit Arena", 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": {
"Overworld Beach": "Overworld Beach":
[], [],
"Overworld Tunnel to Beach":
[],
"Overworld to Atoll Upper": "Overworld to Atoll Upper":
[["Hyperdash"]], [["Hyperdash"]],
"Overworld Belltower": "Overworld Belltower":
@ -769,7 +783,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[], [],
"Overworld Special Shop Entry": "Overworld Special Shop Entry":
[["Hyperdash"], ["LS1"]], [["Hyperdash"], ["LS1"]],
"Overworld Well Ladder": "Overworld Well Entry Area":
[], [],
"Overworld Ruined Passage Door": "Overworld Ruined Passage Door":
[], [],
@ -847,6 +861,12 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
# "Overworld": # "Overworld":
# [], # [],
# }, # },
"Overworld Tunnel to Beach": {
# "Overworld":
# [],
"Overworld Beach":
[],
},
"Overworld Beach": { "Overworld Beach": {
# "Overworld": # "Overworld":
# [], # [],
@ -873,9 +893,15 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld Beach": "Overworld Beach":
[], [],
}, },
"Overworld Well Ladder": { "Overworld Well Entry Area": {
# "Overworld": # "Overworld":
# [], # [],
"Overworld Well Ladder":
[],
},
"Overworld Well Ladder": {
"Overworld Well Entry Area":
[],
}, },
"Overworld at Patrol Cave": { "Overworld at Patrol Cave": {
"East Overworld": "East Overworld":
@ -954,6 +980,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Overworld": "Overworld":
[], [],
}, },
"Old House Front": { "Old House Front": {
"Old House Back": "Old House Back":
[], [],
@ -962,6 +989,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Old House Front": "Old House Front":
[["Hyperdash", "Zip"]], [["Hyperdash", "Zip"]],
}, },
"Furnace Fuse": { "Furnace Fuse": {
"Furnace Ladder Area": "Furnace Ladder Area":
[["Hyperdash"]], [["Hyperdash"]],
@ -976,6 +1004,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Furnace Ladder Area": "Furnace Ladder Area":
[["Hyperdash"]], [["Hyperdash"]],
}, },
"Sealed Temple": { "Sealed Temple": {
"Sealed Temple Rafters": "Sealed Temple Rafters":
[], [],
@ -984,10 +1013,12 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Sealed Temple": "Sealed Temple":
[["Hyperdash"]], [["Hyperdash"]],
}, },
"Hourglass Cave": { "Hourglass Cave": {
"Hourglass Cave Tower": "Hourglass Cave Tower":
[], [],
}, },
"Forest Belltower Upper": { "Forest Belltower Upper": {
"Forest Belltower Main": "Forest Belltower Main":
[], [],
@ -996,6 +1027,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Forest Belltower Lower": "Forest Belltower Lower":
[], [],
}, },
"East Forest": { "East Forest": {
"East Forest Dance Fox Spot": "East Forest Dance Fox Spot":
[["Hyperdash"], ["IG1"], ["LS1"]], [["Hyperdash"], ["IG1"], ["LS1"]],
@ -1016,6 +1048,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"East Forest": "East Forest":
[], [],
}, },
"Guard House 1 East": { "Guard House 1 East": {
"Guard House 1 West": "Guard House 1 West":
[], [],
@ -1024,6 +1057,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Guard House 1 East": "Guard House 1 East":
[["Hyperdash"], ["LS1"]], [["Hyperdash"], ["LS1"]],
}, },
"Guard House 2 Upper": { "Guard House 2 Upper": {
"Guard House 2 Lower": "Guard House 2 Lower":
[], [],
@ -1032,6 +1066,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Guard House 2 Upper": "Guard House 2 Upper":
[], [],
}, },
"Forest Grave Path Main": { "Forest Grave Path Main": {
"Forest Grave Path Upper": "Forest Grave Path Upper":
[["Hyperdash"], ["LS2"], ["IG3"]], [["Hyperdash"], ["LS2"], ["IG3"]],
@ -1044,7 +1079,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
}, },
"Forest Grave Path by Grave": { "Forest Grave Path by Grave": {
"Forest Hero's Grave": "Forest Hero's Grave":
[], [],
"Forest Grave Path Main": "Forest Grave Path Main":
[["IG1"]], [["IG1"]],
}, },
@ -1052,6 +1087,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Forest Grave Path by Grave": "Forest Grave Path by Grave":
[], [],
}, },
"Beneath the Well Ladder Exit": { "Beneath the Well Ladder Exit": {
"Beneath the Well Front": "Beneath the Well Front":
[], [],
@ -1072,6 +1108,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Beneath the Well Main": "Beneath the Well Main":
[], [],
}, },
"Well Boss": { "Well Boss": {
"Dark Tomb Checkpoint": "Dark Tomb Checkpoint":
[], [],
@ -1080,6 +1117,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Well Boss": "Well Boss":
[["Hyperdash", "Zip"]], [["Hyperdash", "Zip"]],
}, },
"Dark Tomb Entry Point": { "Dark Tomb Entry Point": {
"Dark Tomb Upper": "Dark Tomb Upper":
[], [],
@ -1100,44 +1138,72 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Dark Tomb Main": "Dark Tomb Main":
[], [],
}, },
"West Garden": {
"West Garden Laurels Exit Region": "West Garden before Terry": {
[["Hyperdash"], ["LS1"]], "West Garden after Terry":
"West Garden after Boss": [],
[],
"West Garden Hero's Grave Region": "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": "West Garden Portal Item":
[["IG2"]], [["IG2"]],
}, },
"West Garden Laurels Exit Region": { "West Garden Laurels Exit Region": {
"West Garden": "West Garden at Dagger House":
[["Hyperdash"]],
},
"West Garden after Boss": {
"West Garden":
[["Hyperdash"]], [["Hyperdash"]],
}, },
"West Garden Portal Item": { "West Garden Portal Item": {
"West Garden": "West Garden at Dagger House":
[["IG1"]], [["IG1"]],
"West Garden by Portal": "West Garden by Portal":
[["Hyperdash"]], [["Hyperdash"]],
}, },
"West Garden by Portal": { "West Garden by Portal": {
"West Garden Portal":
[["West Garden South Checkpoint"]],
"West Garden Portal Item": "West Garden Portal Item":
[["Hyperdash"]], [["Hyperdash"]],
"West Garden Portal":
[["West Garden"]],
}, },
"West Garden Portal": { "West Garden Portal": {
"West Garden by Portal": "West Garden by Portal":
[], [],
}, },
"West Garden Hero's Grave Region": {
"West Garden":
[],
},
"Ruined Atoll": { "Ruined Atoll": {
"Ruined Atoll Lower Entry Area": "Ruined Atoll Lower Entry Area":
[["Hyperdash"], ["LS1"]], [["Hyperdash"], ["LS1"]],
@ -1176,6 +1242,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Ruined Atoll": "Ruined Atoll":
[], [],
}, },
"Frog Stairs Eye Exit": { "Frog Stairs Eye Exit": {
"Frog Stairs Upper": "Frog Stairs Upper":
[], [],
@ -1196,16 +1263,25 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Frog Stairs Lower": "Frog Stairs Lower":
[], [],
}, },
"Frog's Domain Entry": { "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 Entry":
[], [],
"Frog's Domain Main":
[],
},
"Frog's Domain Main": {
"Frog's Domain Front":
[],
"Frog's Domain Back": "Frog's Domain Back":
[], [],
}, },
# cannot get from frogs back to front
"Library Exterior Ladder Region": { "Library Exterior Ladder Region": {
"Library Exterior by Tree": "Library Exterior by Tree":
[], [],
@ -1220,6 +1296,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Library Exterior by Tree": "Library Exterior by Tree":
[], [],
}, },
"Library Hall Bookshelf": { "Library Hall Bookshelf": {
"Library Hall": "Library Hall":
[], [],
@ -1240,6 +1317,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Library Hall": "Library Hall":
[], [],
}, },
"Library Rotunda to Hall": { "Library Rotunda to Hall": {
"Library Rotunda": "Library Rotunda":
[], [],
@ -1281,9 +1359,10 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Library Lab": "Library Lab":
[], [],
}, },
"Fortress Exterior from East Forest": { "Fortress Exterior from East Forest": {
"Fortress Exterior from Overworld": "Fortress Exterior from Overworld":
[], [],
"Fortress Courtyard Upper": "Fortress Courtyard Upper":
[["LS2"]], [["LS2"]],
"Fortress Courtyard": "Fortress Courtyard":
@ -1291,9 +1370,9 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
}, },
"Fortress Exterior from Overworld": { "Fortress Exterior from Overworld": {
"Fortress Exterior from East Forest": "Fortress Exterior from East Forest":
[["Hyperdash"]], [["Hyperdash"]],
"Fortress Exterior near cave": "Fortress Exterior near cave":
[], [],
"Fortress Courtyard": "Fortress Courtyard":
[["Hyperdash"], ["IG1"], ["LS1"]], [["Hyperdash"], ["IG1"], ["LS1"]],
}, },
@ -1321,6 +1400,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Fortress Courtyard": "Fortress Courtyard":
[], [],
}, },
"Beneath the Vault Ladder Exit": { "Beneath the Vault Ladder Exit": {
"Beneath the Vault Main": "Beneath the Vault Main":
[], [],
@ -1337,6 +1417,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Beneath the Vault Ladder Exit": "Beneath the Vault Ladder Exit":
[], [],
}, },
"Fortress East Shortcut Lower": { "Fortress East Shortcut Lower": {
"Fortress East Shortcut Upper": "Fortress East Shortcut Upper":
[["IG1"]], [["IG1"]],
@ -1345,6 +1426,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Fortress East Shortcut Lower": "Fortress East Shortcut Lower":
[], [],
}, },
"Eastern Vault Fortress": { "Eastern Vault Fortress": {
"Eastern Vault Fortress Gold Door": "Eastern Vault Fortress Gold Door":
[["IG2"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]], [["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": "Eastern Vault Fortress":
[["IG1"]], [["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 Hero's Grave Region":
[], [],
"Fortress Grave Path Dusty Entrance Region": "Fortress Grave Path Dusty Entrance Region":
[["Hyperdash"]], [["Hyperdash"]],
}, },
"Fortress Grave Path Upper": { "Fortress Grave Path Upper": {
"Fortress Grave Path": "Fortress Grave Path Entry":
[["IG1"]], [["IG1"]],
}, },
"Fortress Grave Path Dusty Entrance Region": { "Fortress Grave Path Dusty Entrance Region": {
"Fortress Grave Path": "Fortress Grave Path by Grave":
[["Hyperdash"]], [["Hyperdash"]],
}, },
"Fortress Hero's Grave Region": { "Fortress Hero's Grave Region": {
"Fortress Grave Path": "Fortress Grave Path by Grave":
[], [],
}, },
"Fortress Arena": { "Fortress Arena": {
"Fortress Arena Portal": "Fortress Arena Portal":
[["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]], [["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": "Fortress Arena":
[], [],
}, },
"Lower Mountain": { "Lower Mountain": {
"Lower Mountain Stairs": "Lower Mountain Stairs":
[], [],
@ -1387,6 +1490,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Lower Mountain": "Lower Mountain":
[], [],
}, },
"Monastery Back": { "Monastery Back": {
"Monastery Front": "Monastery Front":
[["Hyperdash", "Zip"]], [["Hyperdash", "Zip"]],
@ -1401,6 +1505,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Monastery Back": "Monastery Back":
[], [],
}, },
"Quarry Entry": { "Quarry Entry": {
"Quarry Portal": "Quarry Portal":
[["Quarry Connector"]], [["Quarry Connector"]],
@ -1436,15 +1541,17 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
[], [],
"Quarry Monastery Entry": "Quarry Monastery Entry":
[], [],
"Lower Quarry Zig Door":
[["IG3"]],
}, },
"Lower Quarry": { "Lower Quarry": {
"Even Lower Quarry": "Even 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": "Lower Quarry Zig Door":
[["Quarry", "Quarry Connector"], ["IG3"]], [["Quarry", "Quarry Connector"], ["IG3"]],
@ -1453,6 +1560,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Quarry Back": "Quarry Back":
[], [],
}, },
"Rooted Ziggurat Upper Entry": { "Rooted Ziggurat Upper Entry": {
"Rooted Ziggurat Upper Front": "Rooted Ziggurat Upper Front":
[], [],
@ -1465,17 +1573,38 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Rooted Ziggurat Upper Front": "Rooted Ziggurat Upper Front":
[["Hyperdash"]], [["Hyperdash"]],
}, },
"Rooted Ziggurat Middle Top": { "Rooted Ziggurat Middle Top": {
"Rooted Ziggurat Middle Bottom": "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 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 Back": { "Rooted Ziggurat Lower Back": {
"Rooted Ziggurat Lower Front": "Rooted Ziggurat Lower Entry":
[["Hyperdash"], ["LS2"], ["IG1"]], [["LS2"]],
"Rooted Ziggurat Lower Mid Checkpoint":
[["Hyperdash"], ["IG1"]],
"Rooted Ziggurat Portal Room Entrance": "Rooted Ziggurat Portal Room Entrance":
[], [],
}, },
@ -1487,20 +1616,22 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Rooted Ziggurat Lower Back": "Rooted Ziggurat Lower Back":
[], [],
}, },
"Rooted Ziggurat Portal Room Exit": { "Rooted Ziggurat Portal Room Exit": {
"Rooted Ziggurat Portal Room": "Rooted Ziggurat Portal Room":
[], [],
}, },
"Rooted Ziggurat Portal Room": { "Rooted Ziggurat Portal Room": {
"Rooted Ziggurat Portal":
[],
"Rooted Ziggurat Portal Room Exit": "Rooted Ziggurat Portal Room Exit":
[["Rooted Ziggurat Lower Back"]], [["Rooted Ziggurat Lower Back"]],
"Rooted Ziggurat Portal":
[],
}, },
"Rooted Ziggurat Portal": { "Rooted Ziggurat Portal": {
"Rooted Ziggurat Portal Room": "Rooted Ziggurat Portal Room":
[], [],
}, },
"Swamp Front": { "Swamp Front": {
"Swamp Mid": "Swamp Mid":
[], [],
@ -1557,14 +1688,26 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Back of Swamp": "Back of Swamp":
[], [],
}, },
"Cathedral": {
"Cathedral Entry": {
"Cathedral to Gauntlet":
[],
"Cathedral Main":
[],
},
"Cathedral Main": {
"Cathedral Entry":
[],
"Cathedral to Gauntlet": "Cathedral to Gauntlet":
[], [],
}, },
"Cathedral to Gauntlet": { "Cathedral to Gauntlet": {
"Cathedral": "Cathedral Entry":
[],
"Cathedral Main":
[], [],
}, },
"Cathedral Gauntlet Checkpoint": { "Cathedral Gauntlet Checkpoint": {
"Cathedral Gauntlet": "Cathedral Gauntlet":
[], [],
@ -1577,6 +1720,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Cathedral Gauntlet": "Cathedral Gauntlet":
[["Hyperdash"]], [["Hyperdash"]],
}, },
"Far Shore": { "Far Shore": {
"Far Shore to Spawn Region": "Far Shore to Spawn Region":
[["Hyperdash"]], [["Hyperdash"]],
@ -1587,7 +1731,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
"Far Shore to Library Region": "Far Shore to Library Region":
[["Library Lab"]], [["Library Lab"]],
"Far Shore to West Garden Region": "Far Shore to West Garden Region":
[["West Garden"]], [["West Garden South Checkpoint"]],
"Far Shore to Fortress Region": "Far Shore to Fortress Region":
[["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]], [["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 typing import Dict, FrozenSet, Tuple, TYPE_CHECKING
from worlds.generic.Rules import set_rule, forbid_item from worlds.generic.Rules import set_rule, add_rule, forbid_item
from .options import IceGrappling, LadderStorage from .options import IceGrappling, LadderStorage, CombatLogic
from .rules import (has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage, from .rules import (has_ability, has_sword, has_melee, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage,
laurels_zip, bomb_walls) laurels_zip, bomb_walls)
from .er_data import Portal, get_portal_outlet_region 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 .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 from BaseClasses import Region, CollectionState
if TYPE_CHECKING: if TYPE_CHECKING:
@ -43,6 +44,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
player = world.player player = world.player
options = world.options 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( regions["Menu"].connect(
connecting_region=regions["Overworld"]) 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"], connecting_region=regions["Overworld Beach"],
rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
or state.has_any({laurels, grapple}, player)) or state.has_any({laurels, grapple}, player))
# regions["Overworld Beach"].connect(
# connecting_region=regions["Overworld"],
# rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
# 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( regions["Overworld Beach"].connect(
connecting_region=regions["Overworld"], connecting_region=regions["Overworld Tunnel to Beach"],
rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) rule=lambda state: state.has(laurels, player) or has_ladder("Ladders in Overworld Town", state, world))
or state.has_any({laurels, grapple}, player))
regions["Overworld Beach"].connect( regions["Overworld Beach"].connect(
connecting_region=regions["Overworld West Garden Laurels Entry"], 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"], connecting_region=regions["East Overworld"],
rule=lambda state: state.has(laurels, player)) 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"], connecting_region=regions["Overworld Well Ladder"],
rule=lambda state: has_ladder("Ladders in Well", state, world)) rule=lambda state: has_ladder("Ladders in Well", state, world))
regions["Overworld Well Ladder"].connect( 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)) rule=lambda state: has_ladder("Ladders in Well", state, world))
# nmg: can ice grapple through the door # 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( regions["Overworld Fountain Cross Door"].connect(
connecting_region=regions["Overworld"]) connecting_region=regions["Overworld"])
regions["Overworld"].connect( ow_to_town_portal = regions["Overworld"].connect(
connecting_region=regions["Overworld Town Portal"], connecting_region=regions["Overworld Town Portal"],
rule=lambda state: has_ability(prayer, state, world)) rule=lambda state: has_ability(prayer, state, world))
regions["Overworld Town Portal"].connect( 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) rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
or has_ice_grapple_logic(True, IceGrappling.option_easy, 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( regions["Overworld"].connect(
connecting_region=regions["Overworld Tunnel Turret"], connecting_region=regions["Overworld Tunnel Turret"],
rule=lambda state: state.has(laurels, player)) 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"], connecting_region=regions["Beneath the Well Ladder Exit"],
rule=lambda state: has_ladder("Ladders in Well", state, world)) 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"], 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( regions["Beneath the Well Main"].connect(
connecting_region=regions["Beneath the Well Front"], connecting_region=regions["Beneath the Well Front"])
rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
regions["Beneath the Well Main"].connect( regions["Beneath the Well Main"].connect(
connecting_region=regions["Beneath the Well Back"], connecting_region=regions["Beneath the Well Back"],
rule=lambda state: has_ladder("Ladders in Well", state, world)) 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"], connecting_region=regions["Beneath the Well Main"],
rule=lambda state: has_ladder("Ladders in Well", state, world) 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"]) connecting_region=regions["Dark Tomb Checkpoint"])
# can laurels through the gate, no setup needed # can laurels through the gate, no setup needed
regions["Dark Tomb Checkpoint"].connect( regions["Dark Tomb Checkpoint"].connect(
connecting_region=regions["Well Boss"], connecting_region=regions["Well Boss"],
rule=lambda state: laurels_zip(state, world)) 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"], connecting_region=regions["Dark Tomb Upper"],
rule=lambda state: has_lantern(state, world)) rule=lambda state: has_lantern(state, world))
regions["Dark Tomb Upper"].connect( 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( regions["Dark Tomb Main"].connect(
connecting_region=regions["Dark Tomb Dark Exit"]) 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"], connecting_region=regions["Dark Tomb Main"],
rule=lambda state: has_lantern(state, world)) rule=lambda state: has_lantern(state, world))
# West Garden # 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( 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)) 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"], connecting_region=regions["West Garden Laurels Exit Region"],
rule=lambda state: state.has(laurels, player)) rule=lambda state: state.has(laurels, player))
# you can grapple Garden Knight to aggro it, then ledge it # laurels past, or ice grapple it off, or ice grapple to it then fight
regions["West Garden after Boss"].connect( after_gk_to_wg = regions["West Garden after Boss"].connect(
connecting_region=regions["West Garden"], connecting_region=regions["West Garden before Boss"],
rule=lambda state: state.has(laurels, player) 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 # 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"], connecting_region=regions["West Garden after Boss"],
rule=lambda state: state.has(laurels, player) or has_sword(state, player) rule=lambda state: state.has(laurels, player) or has_sword(state, player)
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) 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"], connecting_region=regions["West Garden Hero's Grave Region"],
rule=lambda state: has_ability(prayer, state, world)) rule=lambda state: has_ability(prayer, state, world))
regions["West Garden Hero's Grave Region"].connect( 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( regions["West Garden Portal"].connect(
connecting_region=regions["West Garden by Portal"]) 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 # can ice grapple to and from the item behind the magic dagger house
regions["West Garden Portal Item"].connect( 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)) 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"], connecting_region=regions["West Garden Portal Item"],
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_medium, state, world)) 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( regions["Ruined Atoll Portal"].connect(
connecting_region=regions["Ruined Atoll"]) connecting_region=regions["Ruined Atoll"])
regions["Ruined Atoll"].connect( atoll_statue = regions["Ruined Atoll"].connect(
connecting_region=regions["Ruined Atoll Statue"], connecting_region=regions["Ruined Atoll Statue"],
rule=lambda state: has_ability(prayer, state, world) rule=lambda state: has_ability(prayer, state, world)
and (has_ladder("Ladders in South Atoll", 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)) rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
regions["Frog's Domain Entry"].connect( 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)) 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"], connecting_region=regions["Frog's Domain Back"],
rule=lambda state: state.has(grapple, player)) 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) rule=lambda state: state.has(laurels, player)
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) 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"]) connecting_region=regions["Fortress Courtyard"])
# nmg: can ice grapple to the upper ledge # nmg: can ice grapple to the upper ledge
regions["Fortress Courtyard"].connect( 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( regions["Fortress Courtyard Upper"].connect(
connecting_region=regions["Fortress Exterior from Overworld"]) 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"], connecting_region=regions["Beneath the Vault Main"],
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world) rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)
and has_lantern(state, world) and has_lantern(state, world)
# there's some boxes in the way # 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 # on the reverse trip, you can lure an enemy over to break the boxes if needed
regions["Beneath the Vault Main"].connect( regions["Beneath the Vault Main"].connect(
connecting_region=regions["Beneath the Vault Ladder Exit"], 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( regions["Beneath the Vault Main"].connect(
connecting_region=regions["Beneath the Vault Back"]) 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"], connecting_region=regions["Beneath the Vault Main"],
rule=lambda state: has_lantern(state, world)) 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"]) connecting_region=regions["Fortress East Shortcut Lower"])
regions["Fortress East Shortcut Lower"].connect( regions["Fortress East Shortcut Lower"].connect(
connecting_region=regions["Fortress East Shortcut Upper"], 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"], connecting_region=regions["Eastern Vault Fortress"],
rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world))
regions["Fortress Grave Path"].connect( fort_grave_entry_to_combat = regions["Fortress Grave Path Entry"].connect(
connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], connecting_region=regions["Fortress Grave Path Combat"])
rule=lambda state: state.has(laurels, player)) regions["Fortress Grave Path Combat"].connect(
regions["Fortress Grave Path Dusty Entrance Region"].connect( connecting_region=regions["Fortress Grave Path Entry"])
connecting_region=regions["Fortress Grave Path"],
rule=lambda state: state.has(laurels, player))
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"], connecting_region=regions["Fortress Hero's Grave Region"],
rule=lambda state: has_ability(prayer, state, world)) rule=lambda state: has_ability(prayer, state, world))
regions["Fortress Hero's Grave Region"].connect( 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( 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)) rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
regions["Fortress Arena"].connect( regions["Fortress Arena"].connect(
@ -831,19 +900,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
regions["Quarry Portal"].connect( regions["Quarry Portal"].connect(
connecting_region=regions["Quarry Entry"]) connecting_region=regions["Quarry Entry"])
regions["Quarry Entry"].connect( quarry_entry_to_main = regions["Quarry Entry"].connect(
connecting_region=regions["Quarry"], connecting_region=regions["Quarry"],
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
regions["Quarry"].connect( regions["Quarry"].connect(
connecting_region=regions["Quarry Entry"]) connecting_region=regions["Quarry Entry"])
regions["Quarry Back"].connect( quarry_back_to_main = regions["Quarry Back"].connect(
connecting_region=regions["Quarry"], connecting_region=regions["Quarry"],
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
regions["Quarry"].connect( regions["Quarry"].connect(
connecting_region=regions["Quarry Back"]) connecting_region=regions["Quarry Back"])
regions["Quarry Monastery Entry"].connect( monastery_to_quarry_main = regions["Quarry Monastery Entry"].connect(
connecting_region=regions["Quarry"], connecting_region=regions["Quarry"],
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
regions["Quarry"].connect( 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) rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world)
or has_ice_grapple_logic(True, IceGrappling.option_easy, 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( 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"], connecting_region=regions["Lower Quarry Zig Door"],
rule=lambda state: state.has("Activate Quarry Fuse", player) rule=lambda state: state.has("Activate Quarry Fuse", player)
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) 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( 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)) 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"]) connecting_region=regions["Monastery Back"])
# laurels through the gate, no setup needed # laurels through the gate, no setup needed
regions["Monastery Back"].connect( 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( regions["Rooted Ziggurat Upper Entry"].connect(
connecting_region=regions["Rooted Ziggurat Upper Front"]) 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"], connecting_region=regions["Rooted Ziggurat Upper Back"],
rule=lambda state: state.has(laurels, player) or has_sword(state, player)) rule=lambda state: state.has(laurels, player) or has_sword(state, player))
regions["Rooted Ziggurat Upper Back"].connect( 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( regions["Rooted Ziggurat Middle Top"].connect(
connecting_region=regions["Rooted Ziggurat Middle Bottom"]) 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( 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"], connecting_region=regions["Rooted Ziggurat Lower Back"],
rule=lambda state: state.has(laurels, player) rule=lambda state: state.has(laurels, player)
or (has_sword(state, player) and has_ability(prayer, state, world))) 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 # can ice grapple to the voidlings to get to the double admin fight, still need to pray at the fuse
regions["Rooted Ziggurat Lower Back"].connect( zig_low_back_to_mid = regions["Rooted Ziggurat Lower Back"].connect(
connecting_region=regions["Rooted Ziggurat Lower Front"], connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"],
rule=lambda state: (state.has(laurels, player) rule=lambda state: (state.has(laurels, player)
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
and has_ability(prayer, 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( regions["Rooted Ziggurat Portal Room Entrance"].connect(
connecting_region=regions["Rooted Ziggurat Lower Back"]) connecting_region=regions["Rooted Ziggurat Lower Back"])
regions["Zig Skip Exit"].connect( # zig skip region only gets made if entrance rando and fewer shops are on
connecting_region=regions["Rooted Ziggurat Lower Front"]) 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( regions["Rooted Ziggurat Portal"].connect(
connecting_region=regions["Rooted Ziggurat Portal Room"]) 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 state.has(laurels, player)
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) 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( swamp_mid_to_cath = regions["Swamp Mid"].connect(
connecting_region=regions["Swamp to Cathedral Main Entrance Region"], connecting_region=regions["Swamp to Cathedral Main Entrance Region"],
rule=lambda state: (has_ability(prayer, state, world) 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", "Ladder to Swamp",
"Ladders near Weathervane"}, player) "Ladders near Weathervane"}, player)
or (state.has("Ladder to Ruined Atoll", 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)) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders: 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( regions["Swamp Hero's Grave Region"].connect(
connecting_region=regions["Back of Swamp"]) connecting_region=regions["Back of Swamp"])
regions["Cathedral"].connect( cath_entry_to_elev = regions["Cathedral Entry"].connect(
connecting_region=regions["Cathedral to Gauntlet"], connecting_region=regions["Cathedral to Gauntlet"],
rule=lambda state: (has_ability(prayer, state, world) rule=lambda state: (has_ability(prayer, state, world)
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
or options.entrance_rando) # elevator is always there in ER or options.entrance_rando) # elevator is always there in ER
regions["Cathedral to Gauntlet"].connect( 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( regions["Cathedral Gauntlet Checkpoint"].connect(
connecting_region=regions["Cathedral Gauntlet"]) 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"]) connecting_region=regions["Far Shore"])
# Misc # Misc
regions["Spirit Arena"].connect( heir_fight = regions["Spirit Arena"].connect(
connecting_region=regions["Spirit Arena Victory"], connecting_region=regions["Spirit Arena Victory"],
rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if
world.options.hexagon_quest else 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(): for region in ladder_regions.values():
world.multiworld.regions.append(region) 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: def set_er_location_rules(world: "TunicWorld") -> None:
player = world.player 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: ( set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), lambda state: (
state.has_all({grapple, ice_dagger, fire_wand}, player) and has_ability(icebolt, state, world))) state.has_all({grapple, ice_dagger, fire_wand}, player) and has_ability(icebolt, state, world)))
# Dark Tomb
# added to make combat logic smoother
set_rule(world.get_location("Dark Tomb - 2nd Laser Room"),
lambda state: has_lantern(state, world))
# West Garden # West Garden
set_rule(world.get_location("West Garden - [North] Across From Page Pickup"), set_rule(world.get_location("West Garden - [North] Across From Page Pickup"),
lambda state: state.has(laurels, player)) lambda state: state.has(laurels, player))
@ -1348,11 +1637,11 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# Library Lab # Library Lab
set_rule(world.get_location("Library Lab - Page 1"), 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"), 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"), 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 # Eastern Vault Fortress
set_rule(world.get_location("Fortress Arena - Hexagon Red"), 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 # 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 # 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"), 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 # Beneath the Vault
set_rule(world.get_location("Beneath the Fortress - Bridge"), 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 # Quarry
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),
@ -1421,9 +1710,9 @@ def set_er_location_rules(world: "TunicWorld") -> None:
# Events # Events
set_rule(world.get_location("Eastern Bell"), 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"), 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"), set_rule(world.get_location("Furnace Fuse"),
lambda state: has_ability(prayer, state, world)) lambda state: has_ability(prayer, state, world))
set_rule(world.get_location("South and West Fortress Exterior Fuses"), 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)) lambda state: has_sword(state, player))
set_rule(world.get_location("Shop - Coin 2"), set_rule(world.get_location("Shop - Coin 2"),
lambda state: has_sword(state, player)) 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]: def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
regions: Dict[str, Region] = {} 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: 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) portal_pairs = pair_portals(world, regions)
# output the entrances to the spoiler log here for convenience # output the entrances to the spoiler log here for convenience
@ -33,16 +42,21 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
for portal1, portal2 in sorted_portal_pairs.items(): for portal1, portal2 in sorted_portal_pairs.items():
world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player) world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player)
else: 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) portal_pairs = vanilla_portals(world, regions)
create_randomized_entrances(portal_pairs, regions)
set_er_region_rules(world, regions, portal_pairs) set_er_region_rules(world, regions, portal_pairs)
for location_name, location_id in world.location_name_to_id.items(): for location_name, location_id in world.location_name_to_id.items():
region = regions[location_table[location_name].er_region] region = regions[location_table[location_name].er_region]
location = TunicERLocation(world.player, location_name, location_id, region) location = TunicERLocation(world.player, location_name, location_id, region)
region.locations.append(location) region.locations.append(location)
create_randomized_entrances(portal_pairs, regions)
for region in regions.values(): for region in regions.values():
world.multiworld.regions.append(region) world.multiworld.regions.append(region)
@ -70,7 +84,7 @@ tunic_events: Dict[str, str] = {
"Quarry Connector Fuse": "Quarry Connector", "Quarry Connector Fuse": "Quarry Connector",
"Quarry Fuse": "Quarry Entry", "Quarry Fuse": "Quarry Entry",
"Ziggurat Fuse": "Rooted Ziggurat Lower Back", "Ziggurat Fuse": "Rooted Ziggurat Lower Back",
"West Garden Fuse": "West Garden", "West Garden Fuse": "West Garden South Checkpoint",
"Library Fuse": "Library Lab", "Library Fuse": "Library Lab",
"Place Questagons": "Sealed Temple", "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]: def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]:
portal_pairs: 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 # 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: while portal_map:
portal1 = portal_map[0] portal1 = portal_map[0]
@ -121,9 +136,6 @@ def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Por
destination="Previous Region", tag="_") destination="Previous Region", tag="_")
create_shop_region(world, regions) create_shop_region(world, regions)
elif portal2_sdt == "Purgatory, Purgatory_bottom":
portal2_sdt = "Purgatory, Purgatory_top"
for portal in portal_map: for portal in portal_map:
if portal.scene_destination() == portal2_sdt: if portal.scene_destination() == portal2_sdt:
portal2 = portal portal2 = portal
@ -414,6 +426,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
cr.add(portal.region) cr.add(portal.region)
if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks): if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks):
continue continue
# if not waterfall_plando, then we just want to pair secret gathering place now
elif portal.region != "Secret Gathering Place": elif portal.region != "Secret Gathering Place":
continue continue
portal2 = portal portal2 = portal

View File

@ -1,5 +1,5 @@
from itertools import groupby 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 from BaseClasses import ItemClassification as IC
@ -8,6 +8,8 @@ class TunicItemData(NamedTuple):
quantity_in_item_pool: int quantity_in_item_pool: int
item_id_offset: int item_id_offset: int
item_group: str = "" item_group: str = ""
# classification if combat logic is on
combat_ic: Optional[IC] = None
item_base_id = 509342400 item_base_id = 509342400
@ -27,7 +29,7 @@ item_table: Dict[str, TunicItemData] = {
"Lure x2": TunicItemData(IC.filler, 1, 11, "Consumables"), "Lure x2": TunicItemData(IC.filler, 1, 11, "Consumables"),
"Pepper x2": TunicItemData(IC.filler, 4, 12, "Consumables"), "Pepper x2": TunicItemData(IC.filler, 4, 12, "Consumables"),
"Ivy x3": TunicItemData(IC.filler, 2, 13, "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": TunicItemData(IC.filler, 2, 15, "Consumables"),
"HP Berry x2": TunicItemData(IC.filler, 4, 16, "Consumables"), "HP Berry x2": TunicItemData(IC.filler, 4, 16, "Consumables"),
"HP Berry x3": TunicItemData(IC.filler, 2, 17, "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), "Hero's Laurels": TunicItemData(IC.progression | IC.useful, 1, 28),
"Lantern": TunicItemData(IC.progression, 1, 29), "Lantern": TunicItemData(IC.progression, 1, 29),
"Gun": TunicItemData(IC.progression | IC.useful, 1, 30, "Weapons"), "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), "Dath Stone": TunicItemData(IC.useful, 1, 32),
"Hourglass": TunicItemData(IC.useful, 1, 33), "Hourglass": TunicItemData(IC.useful, 1, 33),
"Old House Key": TunicItemData(IC.progression, 1, 34, "Keys"), "Old House Key": TunicItemData(IC.progression, 1, 34, "Keys"),
"Key": TunicItemData(IC.progression, 2, 35, "Keys"), "Key": TunicItemData(IC.progression, 2, 35, "Keys"),
"Fortress Vault Key": TunicItemData(IC.progression, 1, 36, "Keys"), "Fortress Vault Key": TunicItemData(IC.progression, 1, 36, "Keys"),
"Flask Shard": TunicItemData(IC.useful, 12, 37), "Flask Shard": TunicItemData(IC.useful, 12, 37, combat_ic=IC.progression),
"Potion Flask": TunicItemData(IC.useful, 5, 38, "Flask"), "Potion Flask": TunicItemData(IC.useful, 5, 38, "Flask", combat_ic=IC.progression),
"Golden Coin": TunicItemData(IC.progression, 17, 39), "Golden Coin": TunicItemData(IC.progression, 17, 39),
"Card Slot": TunicItemData(IC.useful, 4, 40), "Card Slot": TunicItemData(IC.useful, 4, 40),
"Red Questagon": TunicItemData(IC.progression_skip_balancing, 1, 41, "Hexagons"), "Red Questagon": TunicItemData(IC.progression_skip_balancing, 1, 41, "Hexagons"),
"Green Questagon": TunicItemData(IC.progression_skip_balancing, 1, 42, "Hexagons"), "Green Questagon": TunicItemData(IC.progression_skip_balancing, 1, 42, "Hexagons"),
"Blue Questagon": TunicItemData(IC.progression_skip_balancing, 1, 43, "Hexagons"), "Blue Questagon": TunicItemData(IC.progression_skip_balancing, 1, 43, "Hexagons"),
"Gold Questagon": TunicItemData(IC.progression_skip_balancing, 0, 44, "Hexagons"), "Gold Questagon": TunicItemData(IC.progression_skip_balancing, 0, 44, "Hexagons"),
"ATT Offering": TunicItemData(IC.useful, 4, 45, "Offerings"), "ATT Offering": TunicItemData(IC.useful, 4, 45, "Offerings", combat_ic=IC.progression),
"DEF Offering": TunicItemData(IC.useful, 4, 46, "Offerings"), "DEF Offering": TunicItemData(IC.useful, 4, 46, "Offerings", combat_ic=IC.progression),
"Potion Offering": TunicItemData(IC.useful, 3, 47, "Offerings"), "Potion Offering": TunicItemData(IC.useful, 3, 47, "Offerings", combat_ic=IC.progression),
"HP Offering": TunicItemData(IC.useful, 6, 48, "Offerings"), "HP Offering": TunicItemData(IC.useful, 6, 48, "Offerings", combat_ic=IC.progression),
"MP Offering": TunicItemData(IC.useful, 3, 49, "Offerings"), "MP Offering": TunicItemData(IC.useful, 3, 49, "Offerings", combat_ic=IC.progression),
"SP Offering": TunicItemData(IC.useful, 2, 50, "Offerings"), "SP Offering": TunicItemData(IC.useful, 2, 50, "Offerings", combat_ic=IC.progression),
"Hero Relic - ATT": TunicItemData(IC.progression_skip_balancing, 1, 51, "Hero Relics"), "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"), "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"), "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"), "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"), "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"), "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"), "Orange Peril Ring": TunicItemData(IC.useful, 1, 57, "Cards"),
"Tincture": TunicItemData(IC.useful, 1, 58, "Cards"), "Tincture": TunicItemData(IC.useful, 1, 58, "Cards"),
"Scavenger Mask": TunicItemData(IC.progression, 1, 59, "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"), "Louder Echo": TunicItemData(IC.useful, 1, 70, "Cards"),
"Aura's Gem": TunicItemData(IC.useful, 1, 71, "Cards"), "Aura's Gem": TunicItemData(IC.useful, 1, 71, "Cards"),
"Bone Card": TunicItemData(IC.useful, 1, 72, "Cards"), "Bone Card": TunicItemData(IC.useful, 1, 72, "Cards"),
"Mr Mayor": TunicItemData(IC.useful, 1, 73, "Golden Treasures"), "Mr Mayor": TunicItemData(IC.useful, 1, 73, "Golden Treasures", combat_ic=IC.progression),
"Secret Legend": TunicItemData(IC.useful, 1, 74, "Golden Treasures"), "Secret Legend": TunicItemData(IC.useful, 1, 74, "Golden Treasures", combat_ic=IC.progression),
"Sacred Geometry": TunicItemData(IC.useful, 1, 75, "Golden Treasures"), "Sacred Geometry": TunicItemData(IC.useful, 1, 75, "Golden Treasures", combat_ic=IC.progression),
"Vintage": TunicItemData(IC.useful, 1, 76, "Golden Treasures"), "Vintage": TunicItemData(IC.useful, 1, 76, "Golden Treasures", combat_ic=IC.progression),
"Just Some Pals": TunicItemData(IC.useful, 1, 77, "Golden Treasures"), "Just Some Pals": TunicItemData(IC.useful, 1, 77, "Golden Treasures", combat_ic=IC.progression),
"Regal Weasel": TunicItemData(IC.useful, 1, 78, "Golden Treasures"), "Regal Weasel": TunicItemData(IC.useful, 1, 78, "Golden Treasures", combat_ic=IC.progression),
"Spring Falls": TunicItemData(IC.useful, 1, 79, "Golden Treasures"), "Spring Falls": TunicItemData(IC.useful, 1, 79, "Golden Treasures", combat_ic=IC.progression),
"Power Up": TunicItemData(IC.useful, 1, 80, "Golden Treasures"), "Power Up": TunicItemData(IC.useful, 1, 80, "Golden Treasures", combat_ic=IC.progression),
"Back To Work": TunicItemData(IC.useful, 1, 81, "Golden Treasures"), "Back To Work": TunicItemData(IC.useful, 1, 81, "Golden Treasures", combat_ic=IC.progression),
"Phonomath": TunicItemData(IC.useful, 1, 82, "Golden Treasures"), "Phonomath": TunicItemData(IC.useful, 1, 82, "Golden Treasures", combat_ic=IC.progression),
"Dusty": TunicItemData(IC.useful, 1, 83, "Golden Treasures"), "Dusty": TunicItemData(IC.useful, 1, 83, "Golden Treasures", combat_ic=IC.progression),
"Forever Friend": TunicItemData(IC.useful, 1, 84, "Golden Treasures"), "Forever Friend": TunicItemData(IC.useful, 1, 84, "Golden Treasures", combat_ic=IC.progression),
"Fool Trap": TunicItemData(IC.trap, 0, 85), "Fool Trap": TunicItemData(IC.trap, 0, 85),
"Money x1": TunicItemData(IC.filler, 3, 86, "Money"), "Money x1": TunicItemData(IC.filler, 3, 86, "Money"),
"Money x10": TunicItemData(IC.filler, 1, 87, "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 x50": TunicItemData(IC.filler, 7, 96, "Money"),
"Money x64": TunicItemData(IC.filler, 1, 97, "Money"), "Money x64": TunicItemData(IC.filler, 1, 97, "Money"),
"Money x100": TunicItemData(IC.filler, 5, 98, "Money"), "Money x100": TunicItemData(IC.filler, 5, 98, "Money"),
"Money x128": TunicItemData(IC.useful, 3, 99, "Money"), "Money x128": TunicItemData(IC.useful, 3, 99, "Money", combat_ic=IC.progression),
"Money x200": TunicItemData(IC.useful, 1, 100, "Money"), "Money x200": TunicItemData(IC.useful, 1, 100, "Money", combat_ic=IC.progression),
"Money x255": TunicItemData(IC.useful, 1, 101, "Money"), "Money x255": TunicItemData(IC.useful, 1, 101, "Money", combat_ic=IC.progression),
"Pages 0-1": TunicItemData(IC.useful, 1, 102, "Pages"), "Pages 0-1": TunicItemData(IC.useful, 1, 102, "Pages"),
"Pages 2-3": TunicItemData(IC.useful, 1, 103, "Pages"), "Pages 2-3": TunicItemData(IC.useful, 1, 103, "Pages"),
"Pages 4-5": TunicItemData(IC.useful, 1, 104, "Pages"), "Pages 4-5": TunicItemData(IC.useful, 1, 104, "Pages"),
@ -206,6 +208,10 @@ slot_data_item_names = [
"Gold Questagon", "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()} 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] 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 # West Garden
# exit after Garden Knight # 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 # 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 # Atoll, use the little ladder you fix at the beginning
LadderInfo("Ruined Atoll", "Atoll Redux, Overworld Redux_lower"), LadderInfo("Ruined Atoll", "Atoll Redux, Overworld Redux_lower"),
@ -159,7 +161,8 @@ medium_ls: List[LadderInfo] = [
LadderInfo("Quarry Back", "Quarry Redux, Monastery_back"), LadderInfo("Quarry Back", "Quarry Redux, Monastery_back"),
LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_2_"), 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 # Swamp to Overworld upper
LadderInfo("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", "Ladders in Swamp"), 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", "Sewer, Overworld Redux_west_aqueduct", "Ladders in Well"),
LadderInfo("Beneath the Well Front", "Beneath the Well Back", "Ladders in Well", dest_is_region=True), 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 # 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 # 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 # todo: see if we can use that new laurels strat here
# LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_FTRoom_"), # LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_FTRoom_"),
# go behind the cathedral to reach the door, pretty easily doable # 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 - [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"), "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"), "Dark Tomb Checkpoint - [Passage To Dark Tomb] Page Pickup": TunicLocationData("Overworld", "Dark Tomb Checkpoint"),
"Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral Entry"), # entry because special rules
"Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral Entry"),
"Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral Main"),
"Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral"), "Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral Entry"),
"Dark Tomb - Spike Maze Near Exit": TunicLocationData("Dark Tomb", "Dark Tomb Main"), "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 - 1st Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - Spike Maze Upper Walkway": 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"), "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 - [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"), "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 - 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 - 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"), "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"), "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 - 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 - 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 - 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 - 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"), "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 - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
"Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain"), "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 - 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 - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain Front"),
"Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain"), "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"), "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"), "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"), "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"), "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"), "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"), "Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
"Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="Bosses"), "Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="Bosses"),
"Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="Holy Cross"), "Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="Holy Cross"),
"Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"), "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] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld Beach"),
"Overworld - [Southwest] Chest Guarded By Turret": TunicLocationData("Overworld", "Overworld"), "Overworld - [Southwest] Chest Guarded By Turret": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Shadowy Corner Chest": 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 - [Southwest] Grapple Chest Over Walkway": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld after Envoy"), "Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld after Envoy"),
"Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld Swamp Lower Entry"), "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] Page on Pillar by Dark Tomb": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Upper Overworld"), "Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Upper Overworld"),
"Overworld - [West] Page On Teleporter": TunicLocationData("Overworld", "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"), "Patrol Cave - Normal Chest": TunicLocationData("Overworld", "Patrol Cave"),
"Ruined Shop - Chest 1": TunicLocationData("Overworld", "Ruined Shop"), "Ruined Shop - Chest 1": TunicLocationData("Overworld", "Ruined Shop"),
"Ruined Shop - Chest 2": 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 - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "Even Lower Quarry"),
"Quarry - [West] Lower Area Below Bridge": TunicLocationData("Lower Quarry", "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 - [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"), "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 - 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 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 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 - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Entry"),
"Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"), "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 Front"), "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 Front"), "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 - 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 - 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"), "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"), "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"), "Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"),
"West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"), "West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"),
"Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"), "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 - [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", 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"), "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"), "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", location_group="Holy Cross"), "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"), "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"), "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"), "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"), "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"), "West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden after Terry"),
"West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden"), "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"), "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"), "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"), "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"), "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"), "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 - [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 - [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"), "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"), "Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"),
} }

View File

@ -168,6 +168,22 @@ class TunicPlandoConnections(PlandoConnections):
duplicate_exits = True 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): class LaurelsZips(Toggle):
""" """
Choose whether to include using the Hero's Laurels to zip through gates, doors, and tricky spots. 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 hexagon_goal: HexagonGoal
extra_hexagon_percentage: ExtraHexagonPercentage extra_hexagon_percentage: ExtraHexagonPercentage
laurels_location: LaurelsLocation laurels_location: LaurelsLocation
combat_logic: CombatLogic
lanternless: Lanternless lanternless: Lanternless
maskless: Maskless maskless: Maskless
laurels_zips: LaurelsZips laurels_zips: LaurelsZips
@ -272,6 +289,7 @@ class TunicOptions(PerGameCommonOptions):
tunic_option_groups = [ tunic_option_groups = [
OptionGroup("Logic Options", [ OptionGroup("Logic Options", [
CombatLogic,
Lanternless, Lanternless,
Maskless, Maskless,
LaurelsZips, 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 # a check to see if you can whack things in melee at all
def has_stick(state: CollectionState, player: int) -> bool: def has_melee(state: CollectionState, player: int) -> bool:
return (state.has("Stick", player) or state.has("Sword Upgrade", player, 1) return state.has_any({"Stick", "Sword", "Sword Upgrade"}, player)
or state.has("Sword", player))
def has_sword(state: CollectionState, player: int) -> bool: def has_sword(state: CollectionState, player: int) -> bool:
@ -83,7 +82,7 @@ def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool:
return False return False
if world.options.ladder_storage_without_items: if world.options.ladder_storage_without_items:
return True 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: 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 = \ world.get_entrance("Overworld -> Overworld Holy Cross").access_rule = \
lambda state: has_ability(holy_cross, state, world) lambda state: has_ability(holy_cross, state, world)
world.get_entrance("Overworld -> Beneath the Well").access_rule = \ 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 = \ world.get_entrance("Overworld -> Dark Tomb").access_rule = \
lambda state: has_lantern(state, world) lambda state: has_lantern(state, world)
# laurels in, ladder storage in through the furnace, or ice grapple down the belltower # 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 = \ world.get_entrance("Overworld -> Beneath the Vault").access_rule = \
lambda state: (has_lantern(state, world) and has_ability(prayer, state, world) lambda state: (has_lantern(state, world) and has_ability(prayer, state, world)
# there's some boxes in the way # 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 = \ world.get_entrance("Ruined Atoll -> Library").access_rule = \
lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world) lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world)
world.get_entrance("Overworld -> Quarry").access_rule = \ 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_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)) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
set_rule(world.get_location("West Furnace - Lantern Pickup"), 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"), set_rule(world.get_location("Secret Gathering Place - 10 Fairy Reward"),
lambda state: state.has(fairies, player, 10)) lambda state: state.has(fairies, player, 10))
@ -301,18 +300,18 @@ def set_location_rules(world: "TunicWorld") -> None:
# Library Lab # Library Lab
set_rule(world.get_location("Library Lab - Page 1"), 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"), 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"), 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 # Eastern Vault Fortress
# yes, you can clear the leaves with dagger # 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 # 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 # 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"), 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"), set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"),
lambda state: has_sword(state, player) lambda state: has_sword(state, player)
and (has_ability(prayer, state, world) and (has_ability(prayer, state, world)
@ -324,9 +323,9 @@ def set_location_rules(world: "TunicWorld") -> None:
# Beneath the Vault # Beneath the Vault
set_rule(world.get_location("Beneath the Fortress - Bridge"), 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"), 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 # Quarry
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"), set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),

View File

@ -3,6 +3,8 @@ from .. import options
class TestAccess(TunicTestBase): class TestAccess(TunicTestBase):
options = {options.CombatLogic.internal_name: options.CombatLogic.option_off}
# test whether you can get into the temple without laurels # test whether you can get into the temple without laurels
def test_temple_access(self) -> None: def test_temple_access(self) -> None:
self.collect_all_but(["Hero's Laurels", "Lantern"]) self.collect_all_but(["Hero's Laurels", "Lantern"])
@ -61,7 +63,9 @@ class TestNormalGoal(TunicTestBase):
class TestER(TunicTestBase): class TestER(TunicTestBase):
options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes, options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes,
options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true, 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: 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 # test to see that static connections are working properly -- this chest requires holy cross and is in Overworld