TUNIC: Additional Combat Logic Option (#3658)
This commit is contained in:
parent
0fdc14bc42
commit
6282efb13c
|
@ -1,7 +1,8 @@
|
|||
from typing import Dict, List, Any, Tuple, TypedDict, ClassVar, Union
|
||||
from logging import warning
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld
|
||||
from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification, MultiWorld, CollectionState
|
||||
from .items import (item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names,
|
||||
combat_items)
|
||||
from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations
|
||||
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
|
||||
from .er_rules import set_er_location_rules
|
||||
|
@ -10,6 +11,7 @@ from .er_scripts import create_er_regions
|
|||
from .er_data import portal_mapping, RegionInfo, tunic_er_regions
|
||||
from .options import (TunicOptions, EntranceRando, tunic_option_groups, tunic_option_presets, TunicPlandoConnections,
|
||||
LaurelsLocation, LogicRules, LaurelsZips, IceGrappling, LadderStorage)
|
||||
from .combat_logic import area_data, CombatState
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from Options import PlandoConnection
|
||||
from decimal import Decimal, ROUND_HALF_UP
|
||||
|
@ -127,11 +129,21 @@ class TunicWorld(World):
|
|||
self.options.shuffle_ladders.value = passthrough["shuffle_ladders"]
|
||||
self.options.fixed_shop.value = self.options.fixed_shop.option_false
|
||||
self.options.laurels_location.value = self.options.laurels_location.option_anywhere
|
||||
self.options.combat_logic.value = passthrough["combat_logic"]
|
||||
|
||||
@classmethod
|
||||
def stage_generate_early(cls, multiworld: MultiWorld) -> None:
|
||||
tunic_worlds: Tuple[TunicWorld] = multiworld.get_game_worlds("TUNIC")
|
||||
for tunic in tunic_worlds:
|
||||
# setting up state combat logic stuff, see has_combat_reqs for its use
|
||||
# and this is magic so pycharm doesn't like it, unfortunately
|
||||
if tunic.options.combat_logic:
|
||||
multiworld.state.tunic_need_to_reset_combat_from_collect[tunic.player] = False
|
||||
multiworld.state.tunic_need_to_reset_combat_from_remove[tunic.player] = False
|
||||
multiworld.state.tunic_area_combat_state[tunic.player] = {}
|
||||
for area_name in area_data.keys():
|
||||
multiworld.state.tunic_area_combat_state[tunic.player][area_name] = CombatState.unchecked
|
||||
|
||||
# if it's one of the options, then it isn't a custom seed group
|
||||
if tunic.options.entrance_rando.value in EntranceRando.options.values():
|
||||
continue
|
||||
|
@ -190,10 +202,12 @@ class TunicWorld(World):
|
|||
|
||||
def create_item(self, name: str, classification: ItemClassification = None) -> TunicItem:
|
||||
item_data = item_table[name]
|
||||
return TunicItem(name, classification or item_data.classification, self.item_name_to_id[name], self.player)
|
||||
# if item_data.combat_ic is None, it'll take item_data.classification instead
|
||||
itemclass: ItemClassification = ((item_data.combat_ic if self.options.combat_logic else None)
|
||||
or item_data.classification)
|
||||
return TunicItem(name, classification or itemclass, self.item_name_to_id[name], self.player)
|
||||
|
||||
def create_items(self) -> None:
|
||||
|
||||
tunic_items: List[TunicItem] = []
|
||||
self.slot_data_items = []
|
||||
|
||||
|
@ -322,15 +336,15 @@ class TunicWorld(World):
|
|||
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = passthrough["Hexagon Quest Holy Cross"]
|
||||
self.ability_unlocks["Pages 52-53 (Icebolt)"] = passthrough["Hexagon Quest Icebolt"]
|
||||
|
||||
# ladder rando uses ER with vanilla connections, so that we're not managing more rules files
|
||||
if self.options.entrance_rando or self.options.shuffle_ladders:
|
||||
# Ladders and Combat Logic uses ER rules with vanilla connections for easier maintenance
|
||||
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
|
||||
portal_pairs = create_er_regions(self)
|
||||
if self.options.entrance_rando:
|
||||
# these get interpreted by the game to tell it which entrances to connect
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination()
|
||||
else:
|
||||
# for non-ER, non-ladders
|
||||
# uses the original rules, easier to navigate and reference
|
||||
for region_name in tunic_regions:
|
||||
region = Region(region_name, self.player, self.multiworld)
|
||||
self.multiworld.regions.append(region)
|
||||
|
@ -351,7 +365,8 @@ class TunicWorld(World):
|
|||
victory_region.locations.append(victory_location)
|
||||
|
||||
def set_rules(self) -> None:
|
||||
if self.options.entrance_rando or self.options.shuffle_ladders:
|
||||
# same reason as in create_regions, could probably be put into create_regions
|
||||
if self.options.entrance_rando or self.options.shuffle_ladders or self.options.combat_logic:
|
||||
set_er_location_rules(self)
|
||||
else:
|
||||
set_region_rules(self)
|
||||
|
@ -360,6 +375,19 @@ class TunicWorld(World):
|
|||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choice(filler_items)
|
||||
|
||||
# cache whether you can get through combat logic areas
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
change = super().collect(state, item)
|
||||
if change and self.options.combat_logic and item.name in combat_items:
|
||||
state.tunic_need_to_reset_combat_from_collect[self.player] = True
|
||||
return change
|
||||
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
change = super().remove(state, item)
|
||||
if change and self.options.combat_logic and item.name in combat_items:
|
||||
state.tunic_need_to_reset_combat_from_remove[self.player] = True
|
||||
return change
|
||||
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]) -> None:
|
||||
if self.options.entrance_rando:
|
||||
hint_data.update({self.player: {}})
|
||||
|
@ -426,6 +454,7 @@ class TunicWorld(World):
|
|||
"maskless": self.options.maskless.value,
|
||||
"entrance_rando": int(bool(self.options.entrance_rando.value)),
|
||||
"shuffle_ladders": self.options.shuffle_ladders.value,
|
||||
"combat_logic": self.options.combat_logic.value,
|
||||
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
|
||||
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
|
||||
"Hexagon Quest Icebolt": self.ability_unlocks["Pages 52-53 (Icebolt)"],
|
||||
|
|
|
@ -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))
|
|
@ -248,13 +248,13 @@ portal_mapping: List[Portal] = [
|
|||
Portal(name="Dark Tomb to Checkpoint", region="Dark Tomb Entry Point",
|
||||
destination="Sewer_Boss", tag="_"),
|
||||
|
||||
Portal(name="West Garden Exit near Hero's Grave", region="West Garden",
|
||||
Portal(name="West Garden Exit near Hero's Grave", region="West Garden before Terry",
|
||||
destination="Overworld Redux", tag="_lower"),
|
||||
Portal(name="West Garden to Magic Dagger House", region="West Garden",
|
||||
Portal(name="West Garden to Magic Dagger House", region="West Garden at Dagger House",
|
||||
destination="archipelagos_house", tag="_"),
|
||||
Portal(name="West Garden Exit after Boss", region="West Garden after Boss",
|
||||
destination="Overworld Redux", tag="_upper"),
|
||||
Portal(name="West Garden Shop", region="West Garden",
|
||||
Portal(name="West Garden Shop", region="West Garden before Terry",
|
||||
destination="Shop", tag="_"),
|
||||
Portal(name="West Garden Laurels Exit", region="West Garden Laurels Exit Region",
|
||||
destination="Overworld Redux", tag="_lowest"),
|
||||
|
@ -308,7 +308,7 @@ portal_mapping: List[Portal] = [
|
|||
Portal(name="East Fortress to Interior Upper", region="Fortress East Shortcut Upper",
|
||||
destination="Fortress Main", tag="_upper"),
|
||||
|
||||
Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path",
|
||||
Portal(name="Fortress Grave Path Lower Exit", region="Fortress Grave Path Entry",
|
||||
destination="Fortress Courtyard", tag="_Lower"),
|
||||
Portal(name="Fortress Hero's Grave", region="Fortress Hero's Grave Region",
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth"),
|
||||
|
@ -433,7 +433,7 @@ portal_mapping: List[Portal] = [
|
|||
Portal(name="Ziggurat Tower to Ziggurat Lower", region="Rooted Ziggurat Middle Bottom",
|
||||
destination="ziggurat2020_3", tag="_"),
|
||||
|
||||
Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Front",
|
||||
Portal(name="Ziggurat Lower to Ziggurat Tower", region="Rooted Ziggurat Lower Entry",
|
||||
destination="ziggurat2020_2", tag="_"),
|
||||
Portal(name="Ziggurat Portal Room Entrance", region="Rooted Ziggurat Portal Room Entrance",
|
||||
destination="ziggurat2020_FTRoom", tag="_"),
|
||||
|
@ -461,7 +461,7 @@ portal_mapping: List[Portal] = [
|
|||
Portal(name="Swamp Hero's Grave", region="Swamp Hero's Grave Region",
|
||||
destination="RelicVoid", tag="_teleporter_relic plinth"),
|
||||
|
||||
Portal(name="Cathedral Main Exit", region="Cathedral",
|
||||
Portal(name="Cathedral Main Exit", region="Cathedral Entry",
|
||||
destination="Swamp Redux 2", tag="_main"),
|
||||
Portal(name="Cathedral Elevator", region="Cathedral to Gauntlet",
|
||||
destination="Cathedral Arena", tag="_"),
|
||||
|
@ -523,7 +523,6 @@ class RegionInfo(NamedTuple):
|
|||
game_scene: str # the name of the scene in the actual game
|
||||
dead_end: int = 0 # if a region has only one exit
|
||||
outlet_region: Optional[str] = None
|
||||
is_fake_region: bool = False
|
||||
|
||||
|
||||
# gets the outlet region name if it exists, the region if it doesn't
|
||||
|
@ -563,6 +562,8 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
|||
"Overworld to West Garden Upper": RegionInfo("Overworld Redux"), # usually leads to garden knight
|
||||
"Overworld to West Garden from Furnace": RegionInfo("Overworld Redux"), # isolated stairway with one chest
|
||||
"Overworld Well Ladder": RegionInfo("Overworld Redux"), # just the ladder entrance itself as a region
|
||||
"Overworld Well Entry Area": RegionInfo("Overworld Redux"), # the page, the bridge, etc.
|
||||
"Overworld Tunnel to Beach": RegionInfo("Overworld Redux"), # the tunnel with the chest
|
||||
"Overworld Beach": RegionInfo("Overworld Redux"), # from the two turrets to invisble maze, and lower atoll entry
|
||||
"Overworld Tunnel Turret": RegionInfo("Overworld Redux"), # the tunnel turret by the southwest beach ladder
|
||||
"Overworld to Atoll Upper": RegionInfo("Overworld Redux"), # the little ledge before the ladder
|
||||
|
@ -624,14 +625,18 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
|||
"Beneath the Well Front": RegionInfo("Sewer"), # the front, to separate it from the weapon requirement in the mid
|
||||
"Beneath the Well Main": RegionInfo("Sewer"), # the main section of it, requires a weapon
|
||||
"Beneath the Well Back": RegionInfo("Sewer"), # the back two portals, and all 4 upper chests
|
||||
"West Garden": RegionInfo("Archipelagos Redux"),
|
||||
"West Garden before Terry": RegionInfo("Archipelagos Redux"), # the lower entry point, near hero grave
|
||||
"West Garden after Terry": RegionInfo("Archipelagos Redux"), # after Terry, up until next chompignons
|
||||
"West Garden at Dagger House": RegionInfo("Archipelagos Redux"), # just outside magic dagger house
|
||||
"West Garden South Checkpoint": RegionInfo("Archipelagos Redux"),
|
||||
"Magic Dagger House": RegionInfo("archipelagos_house", dead_end=DeadEnd.all_cats),
|
||||
"West Garden Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted, outlet_region="West Garden by Portal"),
|
||||
"West Garden by Portal": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
|
||||
"West Garden Portal Item": RegionInfo("Archipelagos Redux", dead_end=DeadEnd.restricted),
|
||||
"West Garden Laurels Exit Region": RegionInfo("Archipelagos Redux"),
|
||||
"West Garden before Boss": RegionInfo("Archipelagos Redux"), # main west garden
|
||||
"West Garden after Boss": RegionInfo("Archipelagos Redux"),
|
||||
"West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux", outlet_region="West Garden"),
|
||||
"West Garden Hero's Grave Region": RegionInfo("Archipelagos Redux", outlet_region="West Garden before Terry"),
|
||||
"Ruined Atoll": RegionInfo("Atoll Redux"),
|
||||
"Ruined Atoll Lower Entry Area": RegionInfo("Atoll Redux"),
|
||||
"Ruined Atoll Ladder Tops": RegionInfo("Atoll Redux"), # at the top of the 5 ladders in south Atoll
|
||||
|
@ -643,8 +648,9 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
|||
"Frog Stairs Upper": RegionInfo("Frog Stairs"),
|
||||
"Frog Stairs Lower": RegionInfo("Frog Stairs"),
|
||||
"Frog Stairs to Frog's Domain": RegionInfo("Frog Stairs"),
|
||||
"Frog's Domain Entry": RegionInfo("frog cave main"),
|
||||
"Frog's Domain": RegionInfo("frog cave main"),
|
||||
"Frog's Domain Entry": RegionInfo("frog cave main"), # just the ladder
|
||||
"Frog's Domain Front": RegionInfo("frog cave main"), # before combat
|
||||
"Frog's Domain Main": RegionInfo("frog cave main"),
|
||||
"Frog's Domain Back": RegionInfo("frog cave main"),
|
||||
"Library Exterior Tree Region": RegionInfo("Library Exterior", outlet_region="Library Exterior by Tree"),
|
||||
"Library Exterior by Tree": RegionInfo("Library Exterior"),
|
||||
|
@ -658,8 +664,8 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
|||
"Library Rotunda to Lab": RegionInfo("Library Rotunda"),
|
||||
"Library Lab": RegionInfo("Library Lab"),
|
||||
"Library Lab Lower": RegionInfo("Library Lab"),
|
||||
"Library Portal": RegionInfo("Library Lab", outlet_region="Library Lab on Portal Pad"),
|
||||
"Library Lab on Portal Pad": RegionInfo("Library Lab"),
|
||||
"Library Portal": RegionInfo("Library Lab", outlet_region="Library Lab on Portal Pad"),
|
||||
"Library Lab to Librarian": RegionInfo("Library Lab"),
|
||||
"Library Arena": RegionInfo("Library Arena", dead_end=DeadEnd.all_cats),
|
||||
"Fortress Exterior from East Forest": RegionInfo("Fortress Courtyard"),
|
||||
|
@ -675,10 +681,12 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
|||
"Eastern Vault Fortress Gold Door": RegionInfo("Fortress Main"),
|
||||
"Fortress East Shortcut Upper": RegionInfo("Fortress East"),
|
||||
"Fortress East Shortcut Lower": RegionInfo("Fortress East"),
|
||||
"Fortress Grave Path": RegionInfo("Fortress Reliquary"),
|
||||
"Fortress Grave Path Entry": RegionInfo("Fortress Reliquary"),
|
||||
"Fortress Grave Path Combat": RegionInfo("Fortress Reliquary"), # the combat is basically just a barrier here
|
||||
"Fortress Grave Path by Grave": RegionInfo("Fortress Reliquary"),
|
||||
"Fortress Grave Path Upper": RegionInfo("Fortress Reliquary", dead_end=DeadEnd.restricted),
|
||||
"Fortress Grave Path Dusty Entrance Region": RegionInfo("Fortress Reliquary"),
|
||||
"Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary", outlet_region="Fortress Grave Path"),
|
||||
"Fortress Hero's Grave Region": RegionInfo("Fortress Reliquary", outlet_region="Fortress Grave Path by Grave"),
|
||||
"Fortress Leaf Piles": RegionInfo("Dusty", dead_end=DeadEnd.all_cats),
|
||||
"Fortress Arena": RegionInfo("Fortress Arena"),
|
||||
"Fortress Arena Portal": RegionInfo("Fortress Arena", outlet_region="Fortress Arena"),
|
||||
|
@ -697,6 +705,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
|||
"Monastery Rope": RegionInfo("Quarry Redux"),
|
||||
"Lower Quarry": RegionInfo("Quarry Redux"),
|
||||
"Even Lower Quarry": RegionInfo("Quarry Redux"),
|
||||
"Even Lower Quarry Isolated Chest": RegionInfo("Quarry Redux"), # a region for that one chest
|
||||
"Lower Quarry Zig Door": RegionInfo("Quarry Redux"),
|
||||
"Rooted Ziggurat Entry": RegionInfo("ziggurat2020_0"),
|
||||
"Rooted Ziggurat Upper Entry": RegionInfo("ziggurat2020_1"),
|
||||
|
@ -704,13 +713,15 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
|||
"Rooted Ziggurat Upper Back": RegionInfo("ziggurat2020_1"), # after the administrator
|
||||
"Rooted Ziggurat Middle Top": RegionInfo("ziggurat2020_2"),
|
||||
"Rooted Ziggurat Middle Bottom": RegionInfo("ziggurat2020_2"),
|
||||
"Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the vanilla entry point side
|
||||
"Rooted Ziggurat Lower Entry": RegionInfo("ziggurat2020_3"), # the vanilla entry point side
|
||||
"Rooted Ziggurat Lower Front": RegionInfo("ziggurat2020_3"), # the front for combat logic
|
||||
"Rooted Ziggurat Lower Mid Checkpoint": RegionInfo("ziggurat2020_3"), # the mid-checkpoint before double admin
|
||||
"Rooted Ziggurat Lower Back": RegionInfo("ziggurat2020_3"), # the boss side
|
||||
"Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Front"), # the exit from zig skip, for use with fixed shop on
|
||||
"Zig Skip Exit": RegionInfo("ziggurat2020_3", dead_end=DeadEnd.special, outlet_region="Rooted Ziggurat Lower Entry"), # for use with fixed shop on
|
||||
"Rooted Ziggurat Portal Room Entrance": RegionInfo("ziggurat2020_3", outlet_region="Rooted Ziggurat Lower Back"), # the door itself on the zig 3 side
|
||||
"Rooted Ziggurat Portal": RegionInfo("ziggurat2020_FTRoom", outlet_region="Rooted Ziggurat Portal Room"),
|
||||
"Rooted Ziggurat Portal Room": RegionInfo("ziggurat2020_FTRoom"),
|
||||
"Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom"),
|
||||
"Rooted Ziggurat Portal Room Exit": RegionInfo("ziggurat2020_FTRoom", outlet_region="Rooted Ziggurat Portal Room"),
|
||||
"Swamp Front": RegionInfo("Swamp Redux 2"), # from the main entry to the top of the ladder after south
|
||||
"Swamp Mid": RegionInfo("Swamp Redux 2"), # from the bottom of the ladder to the cathedral door
|
||||
"Swamp Ledge under Cathedral Door": RegionInfo("Swamp Redux 2"), # the ledge with the chest and secret door
|
||||
|
@ -719,7 +730,8 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
|||
"Back of Swamp": RegionInfo("Swamp Redux 2"), # the area with hero grave and gauntlet entrance
|
||||
"Swamp Hero's Grave Region": RegionInfo("Swamp Redux 2", outlet_region="Back of Swamp"),
|
||||
"Back of Swamp Laurels Area": RegionInfo("Swamp Redux 2"), # the spots you need laurels to traverse
|
||||
"Cathedral": RegionInfo("Cathedral Redux"),
|
||||
"Cathedral Entry": RegionInfo("Cathedral Redux"), # the checkpoint and easily-accessible chests
|
||||
"Cathedral Main": RegionInfo("Cathedral Redux"), # the majority of Cathedral
|
||||
"Cathedral to Gauntlet": RegionInfo("Cathedral Redux"), # the elevator
|
||||
"Cathedral Secret Legend Room": RegionInfo("Cathedral Redux", dead_end=DeadEnd.all_cats),
|
||||
"Cathedral Gauntlet Checkpoint": RegionInfo("Cathedral Arena"),
|
||||
|
@ -741,7 +753,7 @@ tunic_er_regions: Dict[str, RegionInfo] = {
|
|||
"Purgatory": RegionInfo("Purgatory"),
|
||||
"Shop": RegionInfo("Shop", dead_end=DeadEnd.all_cats),
|
||||
"Spirit Arena": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats),
|
||||
"Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats)
|
||||
"Spirit Arena Victory": RegionInfo("Spirit Arena", dead_end=DeadEnd.all_cats),
|
||||
}
|
||||
|
||||
|
||||
|
@ -759,6 +771,8 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Overworld": {
|
||||
"Overworld Beach":
|
||||
[],
|
||||
"Overworld Tunnel to Beach":
|
||||
[],
|
||||
"Overworld to Atoll Upper":
|
||||
[["Hyperdash"]],
|
||||
"Overworld Belltower":
|
||||
|
@ -769,7 +783,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
[],
|
||||
"Overworld Special Shop Entry":
|
||||
[["Hyperdash"], ["LS1"]],
|
||||
"Overworld Well Ladder":
|
||||
"Overworld Well Entry Area":
|
||||
[],
|
||||
"Overworld Ruined Passage Door":
|
||||
[],
|
||||
|
@ -847,6 +861,12 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
# "Overworld":
|
||||
# [],
|
||||
# },
|
||||
"Overworld Tunnel to Beach": {
|
||||
# "Overworld":
|
||||
# [],
|
||||
"Overworld Beach":
|
||||
[],
|
||||
},
|
||||
"Overworld Beach": {
|
||||
# "Overworld":
|
||||
# [],
|
||||
|
@ -873,9 +893,15 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Overworld Beach":
|
||||
[],
|
||||
},
|
||||
"Overworld Well Ladder": {
|
||||
"Overworld Well Entry Area": {
|
||||
# "Overworld":
|
||||
# [],
|
||||
"Overworld Well Ladder":
|
||||
[],
|
||||
},
|
||||
"Overworld Well Ladder": {
|
||||
"Overworld Well Entry Area":
|
||||
[],
|
||||
},
|
||||
"Overworld at Patrol Cave": {
|
||||
"East Overworld":
|
||||
|
@ -954,6 +980,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Overworld":
|
||||
[],
|
||||
},
|
||||
|
||||
"Old House Front": {
|
||||
"Old House Back":
|
||||
[],
|
||||
|
@ -962,6 +989,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Old House Front":
|
||||
[["Hyperdash", "Zip"]],
|
||||
},
|
||||
|
||||
"Furnace Fuse": {
|
||||
"Furnace Ladder Area":
|
||||
[["Hyperdash"]],
|
||||
|
@ -976,6 +1004,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Furnace Ladder Area":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
|
||||
"Sealed Temple": {
|
||||
"Sealed Temple Rafters":
|
||||
[],
|
||||
|
@ -984,10 +1013,12 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Sealed Temple":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
|
||||
"Hourglass Cave": {
|
||||
"Hourglass Cave Tower":
|
||||
[],
|
||||
},
|
||||
|
||||
"Forest Belltower Upper": {
|
||||
"Forest Belltower Main":
|
||||
[],
|
||||
|
@ -996,6 +1027,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Forest Belltower Lower":
|
||||
[],
|
||||
},
|
||||
|
||||
"East Forest": {
|
||||
"East Forest Dance Fox Spot":
|
||||
[["Hyperdash"], ["IG1"], ["LS1"]],
|
||||
|
@ -1016,6 +1048,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"East Forest":
|
||||
[],
|
||||
},
|
||||
|
||||
"Guard House 1 East": {
|
||||
"Guard House 1 West":
|
||||
[],
|
||||
|
@ -1024,6 +1057,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Guard House 1 East":
|
||||
[["Hyperdash"], ["LS1"]],
|
||||
},
|
||||
|
||||
"Guard House 2 Upper": {
|
||||
"Guard House 2 Lower":
|
||||
[],
|
||||
|
@ -1032,6 +1066,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Guard House 2 Upper":
|
||||
[],
|
||||
},
|
||||
|
||||
"Forest Grave Path Main": {
|
||||
"Forest Grave Path Upper":
|
||||
[["Hyperdash"], ["LS2"], ["IG3"]],
|
||||
|
@ -1052,6 +1087,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Forest Grave Path by Grave":
|
||||
[],
|
||||
},
|
||||
|
||||
"Beneath the Well Ladder Exit": {
|
||||
"Beneath the Well Front":
|
||||
[],
|
||||
|
@ -1072,6 +1108,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Beneath the Well Main":
|
||||
[],
|
||||
},
|
||||
|
||||
"Well Boss": {
|
||||
"Dark Tomb Checkpoint":
|
||||
[],
|
||||
|
@ -1080,6 +1117,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Well Boss":
|
||||
[["Hyperdash", "Zip"]],
|
||||
},
|
||||
|
||||
"Dark Tomb Entry Point": {
|
||||
"Dark Tomb Upper":
|
||||
[],
|
||||
|
@ -1100,44 +1138,72 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Dark Tomb Main":
|
||||
[],
|
||||
},
|
||||
"West Garden": {
|
||||
"West Garden Laurels Exit Region":
|
||||
[["Hyperdash"], ["LS1"]],
|
||||
"West Garden after Boss":
|
||||
|
||||
"West Garden before Terry": {
|
||||
"West Garden after Terry":
|
||||
[],
|
||||
"West Garden Hero's Grave Region":
|
||||
[],
|
||||
},
|
||||
"West Garden Hero's Grave Region": {
|
||||
"West Garden before Terry":
|
||||
[],
|
||||
},
|
||||
"West Garden after Terry": {
|
||||
"West Garden before Terry":
|
||||
[],
|
||||
"West Garden South Checkpoint":
|
||||
[],
|
||||
"West Garden Laurels Exit Region":
|
||||
[["LS1"]],
|
||||
},
|
||||
"West Garden South Checkpoint": {
|
||||
"West Garden before Boss":
|
||||
[],
|
||||
"West Garden at Dagger House":
|
||||
[],
|
||||
"West Garden after Terry":
|
||||
[],
|
||||
},
|
||||
"West Garden before Boss": {
|
||||
"West Garden after Boss":
|
||||
[],
|
||||
"West Garden South Checkpoint":
|
||||
[],
|
||||
},
|
||||
"West Garden after Boss": {
|
||||
"West Garden before Boss":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
"West Garden at Dagger House": {
|
||||
"West Garden Laurels Exit Region":
|
||||
[["Hyperdash"]],
|
||||
"West Garden South Checkpoint":
|
||||
[],
|
||||
"West Garden Portal Item":
|
||||
[["IG2"]],
|
||||
},
|
||||
"West Garden Laurels Exit Region": {
|
||||
"West Garden":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
"West Garden after Boss": {
|
||||
"West Garden":
|
||||
"West Garden at Dagger House":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
"West Garden Portal Item": {
|
||||
"West Garden":
|
||||
"West Garden at Dagger House":
|
||||
[["IG1"]],
|
||||
"West Garden by Portal":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
"West Garden by Portal": {
|
||||
"West Garden Portal":
|
||||
[["West Garden South Checkpoint"]],
|
||||
"West Garden Portal Item":
|
||||
[["Hyperdash"]],
|
||||
"West Garden Portal":
|
||||
[["West Garden"]],
|
||||
},
|
||||
"West Garden Portal": {
|
||||
"West Garden by Portal":
|
||||
[],
|
||||
},
|
||||
"West Garden Hero's Grave Region": {
|
||||
"West Garden":
|
||||
[],
|
||||
},
|
||||
|
||||
"Ruined Atoll": {
|
||||
"Ruined Atoll Lower Entry Area":
|
||||
[["Hyperdash"], ["LS1"]],
|
||||
|
@ -1176,6 +1242,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Ruined Atoll":
|
||||
[],
|
||||
},
|
||||
|
||||
"Frog Stairs Eye Exit": {
|
||||
"Frog Stairs Upper":
|
||||
[],
|
||||
|
@ -1196,16 +1263,25 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Frog Stairs Lower":
|
||||
[],
|
||||
},
|
||||
|
||||
"Frog's Domain Entry": {
|
||||
"Frog's Domain":
|
||||
"Frog's Domain Front":
|
||||
[],
|
||||
},
|
||||
"Frog's Domain": {
|
||||
"Frog's Domain Front": {
|
||||
"Frog's Domain Entry":
|
||||
[],
|
||||
"Frog's Domain Main":
|
||||
[],
|
||||
},
|
||||
"Frog's Domain Main": {
|
||||
"Frog's Domain Front":
|
||||
[],
|
||||
"Frog's Domain Back":
|
||||
[],
|
||||
},
|
||||
|
||||
# cannot get from frogs back to front
|
||||
"Library Exterior Ladder Region": {
|
||||
"Library Exterior by Tree":
|
||||
[],
|
||||
|
@ -1220,6 +1296,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Library Exterior by Tree":
|
||||
[],
|
||||
},
|
||||
|
||||
"Library Hall Bookshelf": {
|
||||
"Library Hall":
|
||||
[],
|
||||
|
@ -1240,6 +1317,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Library Hall":
|
||||
[],
|
||||
},
|
||||
|
||||
"Library Rotunda to Hall": {
|
||||
"Library Rotunda":
|
||||
[],
|
||||
|
@ -1281,6 +1359,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Library Lab":
|
||||
[],
|
||||
},
|
||||
|
||||
"Fortress Exterior from East Forest": {
|
||||
"Fortress Exterior from Overworld":
|
||||
[],
|
||||
|
@ -1321,6 +1400,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Fortress Courtyard":
|
||||
[],
|
||||
},
|
||||
|
||||
"Beneath the Vault Ladder Exit": {
|
||||
"Beneath the Vault Main":
|
||||
[],
|
||||
|
@ -1337,6 +1417,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Beneath the Vault Ladder Exit":
|
||||
[],
|
||||
},
|
||||
|
||||
"Fortress East Shortcut Lower": {
|
||||
"Fortress East Shortcut Upper":
|
||||
[["IG1"]],
|
||||
|
@ -1345,6 +1426,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Fortress East Shortcut Lower":
|
||||
[],
|
||||
},
|
||||
|
||||
"Eastern Vault Fortress": {
|
||||
"Eastern Vault Fortress Gold Door":
|
||||
[["IG2"], ["Fortress Exterior from Overworld", "Beneath the Vault Back", "Fortress Courtyard Upper"]],
|
||||
|
@ -1353,24 +1435,44 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Eastern Vault Fortress":
|
||||
[["IG1"]],
|
||||
},
|
||||
"Fortress Grave Path": {
|
||||
|
||||
"Fortress Grave Path Entry": {
|
||||
"Fortress Grave Path Combat":
|
||||
[],
|
||||
# redundant here, keeping a comment to show it's intentional
|
||||
# "Fortress Grave Path Dusty Entrance Region":
|
||||
# [["Hyperdash"]],
|
||||
},
|
||||
"Fortress Grave Path Combat": {
|
||||
"Fortress Grave Path Entry":
|
||||
[],
|
||||
"Fortress Grave Path by Grave":
|
||||
[],
|
||||
},
|
||||
"Fortress Grave Path by Grave": {
|
||||
"Fortress Grave Path Entry":
|
||||
[],
|
||||
# unnecessary, you can just skip it
|
||||
# "Fortress Grave Path Combat":
|
||||
# [],
|
||||
"Fortress Hero's Grave Region":
|
||||
[],
|
||||
"Fortress Grave Path Dusty Entrance Region":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
"Fortress Grave Path Upper": {
|
||||
"Fortress Grave Path":
|
||||
"Fortress Grave Path Entry":
|
||||
[["IG1"]],
|
||||
},
|
||||
"Fortress Grave Path Dusty Entrance Region": {
|
||||
"Fortress Grave Path":
|
||||
"Fortress Grave Path by Grave":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
"Fortress Hero's Grave Region": {
|
||||
"Fortress Grave Path":
|
||||
"Fortress Grave Path by Grave":
|
||||
[],
|
||||
},
|
||||
|
||||
"Fortress Arena": {
|
||||
"Fortress Arena Portal":
|
||||
[["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]],
|
||||
|
@ -1379,6 +1481,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Fortress Arena":
|
||||
[],
|
||||
},
|
||||
|
||||
"Lower Mountain": {
|
||||
"Lower Mountain Stairs":
|
||||
[],
|
||||
|
@ -1387,6 +1490,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Lower Mountain":
|
||||
[],
|
||||
},
|
||||
|
||||
"Monastery Back": {
|
||||
"Monastery Front":
|
||||
[["Hyperdash", "Zip"]],
|
||||
|
@ -1401,6 +1505,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Monastery Back":
|
||||
[],
|
||||
},
|
||||
|
||||
"Quarry Entry": {
|
||||
"Quarry Portal":
|
||||
[["Quarry Connector"]],
|
||||
|
@ -1436,15 +1541,17 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
[],
|
||||
"Quarry Monastery Entry":
|
||||
[],
|
||||
"Lower Quarry Zig Door":
|
||||
[["IG3"]],
|
||||
},
|
||||
"Lower Quarry": {
|
||||
"Even Lower Quarry":
|
||||
[],
|
||||
},
|
||||
"Even Lower Quarry": {
|
||||
"Lower Quarry":
|
||||
"Even Lower Quarry Isolated Chest":
|
||||
[],
|
||||
},
|
||||
"Even Lower Quarry Isolated Chest": {
|
||||
"Even Lower Quarry":
|
||||
[],
|
||||
"Lower Quarry Zig Door":
|
||||
[["Quarry", "Quarry Connector"], ["IG3"]],
|
||||
|
@ -1453,6 +1560,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Quarry Back":
|
||||
[],
|
||||
},
|
||||
|
||||
"Rooted Ziggurat Upper Entry": {
|
||||
"Rooted Ziggurat Upper Front":
|
||||
[],
|
||||
|
@ -1465,17 +1573,38 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Rooted Ziggurat Upper Front":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
|
||||
"Rooted Ziggurat Middle Top": {
|
||||
"Rooted Ziggurat Middle Bottom":
|
||||
[],
|
||||
},
|
||||
|
||||
"Rooted Ziggurat Lower Entry": {
|
||||
"Rooted Ziggurat Lower Front":
|
||||
[],
|
||||
# can zip through to the checkpoint
|
||||
"Rooted Ziggurat Lower Mid Checkpoint":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
"Rooted Ziggurat Lower Front": {
|
||||
"Rooted Ziggurat Lower Entry":
|
||||
[],
|
||||
"Rooted Ziggurat Lower Mid Checkpoint":
|
||||
[],
|
||||
},
|
||||
"Rooted Ziggurat Lower Mid Checkpoint": {
|
||||
"Rooted Ziggurat Lower Entry":
|
||||
[["Hyperdash"]],
|
||||
"Rooted Ziggurat Lower Front":
|
||||
[],
|
||||
"Rooted Ziggurat Lower Back":
|
||||
[],
|
||||
},
|
||||
"Rooted Ziggurat Lower Back": {
|
||||
"Rooted Ziggurat Lower Front":
|
||||
[["Hyperdash"], ["LS2"], ["IG1"]],
|
||||
"Rooted Ziggurat Lower Entry":
|
||||
[["LS2"]],
|
||||
"Rooted Ziggurat Lower Mid Checkpoint":
|
||||
[["Hyperdash"], ["IG1"]],
|
||||
"Rooted Ziggurat Portal Room Entrance":
|
||||
[],
|
||||
},
|
||||
|
@ -1487,20 +1616,22 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Rooted Ziggurat Lower Back":
|
||||
[],
|
||||
},
|
||||
|
||||
"Rooted Ziggurat Portal Room Exit": {
|
||||
"Rooted Ziggurat Portal Room":
|
||||
[],
|
||||
},
|
||||
"Rooted Ziggurat Portal Room": {
|
||||
"Rooted Ziggurat Portal":
|
||||
[],
|
||||
"Rooted Ziggurat Portal Room Exit":
|
||||
[["Rooted Ziggurat Lower Back"]],
|
||||
"Rooted Ziggurat Portal":
|
||||
[],
|
||||
},
|
||||
"Rooted Ziggurat Portal": {
|
||||
"Rooted Ziggurat Portal Room":
|
||||
[],
|
||||
},
|
||||
|
||||
"Swamp Front": {
|
||||
"Swamp Mid":
|
||||
[],
|
||||
|
@ -1557,14 +1688,26 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Back of Swamp":
|
||||
[],
|
||||
},
|
||||
"Cathedral": {
|
||||
|
||||
"Cathedral Entry": {
|
||||
"Cathedral to Gauntlet":
|
||||
[],
|
||||
"Cathedral Main":
|
||||
[],
|
||||
},
|
||||
"Cathedral Main": {
|
||||
"Cathedral Entry":
|
||||
[],
|
||||
"Cathedral to Gauntlet":
|
||||
[],
|
||||
},
|
||||
"Cathedral to Gauntlet": {
|
||||
"Cathedral":
|
||||
"Cathedral Entry":
|
||||
[],
|
||||
"Cathedral Main":
|
||||
[],
|
||||
},
|
||||
|
||||
"Cathedral Gauntlet Checkpoint": {
|
||||
"Cathedral Gauntlet":
|
||||
[],
|
||||
|
@ -1577,6 +1720,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Cathedral Gauntlet":
|
||||
[["Hyperdash"]],
|
||||
},
|
||||
|
||||
"Far Shore": {
|
||||
"Far Shore to Spawn Region":
|
||||
[["Hyperdash"]],
|
||||
|
@ -1587,7 +1731,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
|||
"Far Shore to Library Region":
|
||||
[["Library Lab"]],
|
||||
"Far Shore to West Garden Region":
|
||||
[["West Garden"]],
|
||||
[["West Garden South Checkpoint"]],
|
||||
"Far Shore to Fortress Region":
|
||||
[["Fortress Exterior from Overworld", "Beneath the Vault Back", "Eastern Vault Fortress"]],
|
||||
},
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING
|
||||
from worlds.generic.Rules import set_rule, forbid_item
|
||||
from .options import IceGrappling, LadderStorage
|
||||
from .rules import (has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage,
|
||||
from worlds.generic.Rules import set_rule, add_rule, forbid_item
|
||||
from .options import IceGrappling, LadderStorage, CombatLogic
|
||||
from .rules import (has_ability, has_sword, has_melee, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage,
|
||||
laurels_zip, bomb_walls)
|
||||
from .er_data import Portal, get_portal_outlet_region
|
||||
from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls
|
||||
from .combat_logic import has_combat_reqs
|
||||
from BaseClasses import Region, CollectionState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -43,6 +44,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
player = world.player
|
||||
options = world.options
|
||||
|
||||
# input scene destination tag, returns portal's name and paired portal's outlet region or region
|
||||
def get_portal_info(portal_sd: str) -> Tuple[str, str]:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
if portal1.scene_destination() == portal_sd:
|
||||
return portal1.name, get_portal_outlet_region(portal2, world)
|
||||
if portal2.scene_destination() == portal_sd:
|
||||
return portal2.name, get_portal_outlet_region(portal1, world)
|
||||
raise Exception("No matches found in get_portal_info")
|
||||
|
||||
# input scene destination tag, returns paired portal's name and region
|
||||
def get_paired_portal(portal_sd: str) -> Tuple[str, str]:
|
||||
for portal1, portal2 in portal_pairs.items():
|
||||
if portal1.scene_destination() == portal_sd:
|
||||
return portal2.name, portal2.region
|
||||
if portal2.scene_destination() == portal_sd:
|
||||
return portal1.name, portal1.region
|
||||
raise Exception("no matches found in get_paired_portal")
|
||||
|
||||
regions["Menu"].connect(
|
||||
connecting_region=regions["Overworld"])
|
||||
|
||||
|
@ -56,10 +75,18 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
connecting_region=regions["Overworld Beach"],
|
||||
rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
|
||||
or state.has_any({laurels, grapple}, player))
|
||||
# regions["Overworld Beach"].connect(
|
||||
# connecting_region=regions["Overworld"],
|
||||
# rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
|
||||
# or state.has_any({laurels, grapple}, player))
|
||||
|
||||
# region for combat logic, no need to connect it to beach since it would be the same as the ow -> beach cxn
|
||||
ow_tunnel_beach = regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Tunnel to Beach"])
|
||||
|
||||
regions["Overworld Beach"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
|
||||
or state.has_any({laurels, grapple}, player))
|
||||
connecting_region=regions["Overworld Tunnel to Beach"],
|
||||
rule=lambda state: state.has(laurels, player) or has_ladder("Ladders in Overworld Town", state, world))
|
||||
|
||||
regions["Overworld Beach"].connect(
|
||||
connecting_region=regions["Overworld West Garden Laurels Entry"],
|
||||
|
@ -277,11 +304,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
connecting_region=regions["East Overworld"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
regions["Overworld"].connect(
|
||||
# region made for combat logic
|
||||
ow_to_well_entry = regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Well Entry Area"])
|
||||
regions["Overworld Well Entry Area"].connect(
|
||||
connecting_region=regions["Overworld"])
|
||||
|
||||
regions["Overworld Well Entry Area"].connect(
|
||||
connecting_region=regions["Overworld Well Ladder"],
|
||||
rule=lambda state: has_ladder("Ladders in Well", state, world))
|
||||
regions["Overworld Well Ladder"].connect(
|
||||
connecting_region=regions["Overworld"],
|
||||
connecting_region=regions["Overworld Well Entry Area"],
|
||||
rule=lambda state: has_ladder("Ladders in Well", state, world))
|
||||
|
||||
# nmg: can ice grapple through the door
|
||||
|
@ -306,7 +339,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
regions["Overworld Fountain Cross Door"].connect(
|
||||
connecting_region=regions["Overworld"])
|
||||
|
||||
regions["Overworld"].connect(
|
||||
ow_to_town_portal = regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Town Portal"],
|
||||
rule=lambda state: has_ability(prayer, state, world))
|
||||
regions["Overworld Town Portal"].connect(
|
||||
|
@ -337,6 +370,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
rule=lambda state: has_ladder("Ladders in Overworld Town", state, world)
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
|
||||
# don't need the ice grapple rule since you can go from ow -> beach -> tunnel
|
||||
regions["Overworld"].connect(
|
||||
connecting_region=regions["Overworld Tunnel Turret"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
@ -473,29 +507,28 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
connecting_region=regions["Beneath the Well Ladder Exit"],
|
||||
rule=lambda state: has_ladder("Ladders in Well", state, world))
|
||||
|
||||
regions["Beneath the Well Front"].connect(
|
||||
btw_front_main = regions["Beneath the Well Front"].connect(
|
||||
connecting_region=regions["Beneath the Well Main"],
|
||||
rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
|
||||
rule=lambda state: has_melee(state, player) or state.has(fire_wand, player))
|
||||
regions["Beneath the Well Main"].connect(
|
||||
connecting_region=regions["Beneath the Well Front"],
|
||||
rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
|
||||
connecting_region=regions["Beneath the Well Front"])
|
||||
|
||||
regions["Beneath the Well Main"].connect(
|
||||
connecting_region=regions["Beneath the Well Back"],
|
||||
rule=lambda state: has_ladder("Ladders in Well", state, world))
|
||||
regions["Beneath the Well Back"].connect(
|
||||
btw_back_main = regions["Beneath the Well Back"].connect(
|
||||
connecting_region=regions["Beneath the Well Main"],
|
||||
rule=lambda state: has_ladder("Ladders in Well", state, world)
|
||||
and (has_stick(state, player) or state.has(fire_wand, player)))
|
||||
and (has_melee(state, player) or state.has(fire_wand, player)))
|
||||
|
||||
regions["Well Boss"].connect(
|
||||
well_boss_to_dt = regions["Well Boss"].connect(
|
||||
connecting_region=regions["Dark Tomb Checkpoint"])
|
||||
# can laurels through the gate, no setup needed
|
||||
regions["Dark Tomb Checkpoint"].connect(
|
||||
connecting_region=regions["Well Boss"],
|
||||
rule=lambda state: laurels_zip(state, world))
|
||||
|
||||
regions["Dark Tomb Entry Point"].connect(
|
||||
dt_entry_to_upper = regions["Dark Tomb Entry Point"].connect(
|
||||
connecting_region=regions["Dark Tomb Upper"],
|
||||
rule=lambda state: has_lantern(state, world))
|
||||
regions["Dark Tomb Upper"].connect(
|
||||
|
@ -512,34 +545,57 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
|
||||
regions["Dark Tomb Main"].connect(
|
||||
connecting_region=regions["Dark Tomb Dark Exit"])
|
||||
regions["Dark Tomb Dark Exit"].connect(
|
||||
dt_exit_to_main = regions["Dark Tomb Dark Exit"].connect(
|
||||
connecting_region=regions["Dark Tomb Main"],
|
||||
rule=lambda state: has_lantern(state, world))
|
||||
|
||||
# West Garden
|
||||
# combat logic regions
|
||||
wg_before_to_after_terry = regions["West Garden before Terry"].connect(
|
||||
connecting_region=regions["West Garden after Terry"])
|
||||
wg_after_to_before_terry = regions["West Garden after Terry"].connect(
|
||||
connecting_region=regions["West Garden before Terry"])
|
||||
|
||||
regions["West Garden after Terry"].connect(
|
||||
connecting_region=regions["West Garden South Checkpoint"])
|
||||
wg_checkpoint_to_after_terry = regions["West Garden South Checkpoint"].connect(
|
||||
connecting_region=regions["West Garden after Terry"])
|
||||
|
||||
wg_checkpoint_to_dagger = regions["West Garden South Checkpoint"].connect(
|
||||
connecting_region=regions["West Garden at Dagger House"])
|
||||
regions["West Garden at Dagger House"].connect(
|
||||
connecting_region=regions["West Garden South Checkpoint"])
|
||||
|
||||
wg_checkpoint_to_before_boss = regions["West Garden South Checkpoint"].connect(
|
||||
connecting_region=regions["West Garden before Boss"])
|
||||
regions["West Garden before Boss"].connect(
|
||||
connecting_region=regions["West Garden South Checkpoint"])
|
||||
|
||||
regions["West Garden Laurels Exit Region"].connect(
|
||||
connecting_region=regions["West Garden"],
|
||||
connecting_region=regions["West Garden at Dagger House"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["West Garden"].connect(
|
||||
regions["West Garden at Dagger House"].connect(
|
||||
connecting_region=regions["West Garden Laurels Exit Region"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
# you can grapple Garden Knight to aggro it, then ledge it
|
||||
regions["West Garden after Boss"].connect(
|
||||
connecting_region=regions["West Garden"],
|
||||
# laurels past, or ice grapple it off, or ice grapple to it then fight
|
||||
after_gk_to_wg = regions["West Garden after Boss"].connect(
|
||||
connecting_region=regions["West Garden before Boss"],
|
||||
rule=lambda state: state.has(laurels, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
|
||||
or (has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)
|
||||
and has_sword(state, player)))
|
||||
# ice grapple push Garden Knight off the side
|
||||
regions["West Garden"].connect(
|
||||
wg_to_after_gk = regions["West Garden before Boss"].connect(
|
||||
connecting_region=regions["West Garden after Boss"],
|
||||
rule=lambda state: state.has(laurels, player) or has_sword(state, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
|
||||
regions["West Garden"].connect(
|
||||
regions["West Garden before Terry"].connect(
|
||||
connecting_region=regions["West Garden Hero's Grave Region"],
|
||||
rule=lambda state: has_ability(prayer, state, world))
|
||||
regions["West Garden Hero's Grave Region"].connect(
|
||||
connecting_region=regions["West Garden"])
|
||||
connecting_region=regions["West Garden before Terry"])
|
||||
|
||||
regions["West Garden Portal"].connect(
|
||||
connecting_region=regions["West Garden by Portal"])
|
||||
|
@ -556,9 +612,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
|
||||
# can ice grapple to and from the item behind the magic dagger house
|
||||
regions["West Garden Portal Item"].connect(
|
||||
connecting_region=regions["West Garden"],
|
||||
connecting_region=regions["West Garden at Dagger House"],
|
||||
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
regions["West Garden"].connect(
|
||||
regions["West Garden at Dagger House"].connect(
|
||||
connecting_region=regions["West Garden Portal Item"],
|
||||
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_medium, state, world))
|
||||
|
||||
|
@ -596,7 +652,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
regions["Ruined Atoll Portal"].connect(
|
||||
connecting_region=regions["Ruined Atoll"])
|
||||
|
||||
regions["Ruined Atoll"].connect(
|
||||
atoll_statue = regions["Ruined Atoll"].connect(
|
||||
connecting_region=regions["Ruined Atoll Statue"],
|
||||
rule=lambda state: has_ability(prayer, state, world)
|
||||
and (has_ladder("Ladders in South Atoll", state, world)
|
||||
|
@ -629,10 +685,13 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
|
||||
|
||||
regions["Frog's Domain Entry"].connect(
|
||||
connecting_region=regions["Frog's Domain"],
|
||||
connecting_region=regions["Frog's Domain Front"],
|
||||
rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world))
|
||||
|
||||
regions["Frog's Domain"].connect(
|
||||
frogs_front_to_main = regions["Frog's Domain Front"].connect(
|
||||
connecting_region=regions["Frog's Domain Main"])
|
||||
|
||||
regions["Frog's Domain Main"].connect(
|
||||
connecting_region=regions["Frog's Domain Back"],
|
||||
rule=lambda state: state.has(grapple, player))
|
||||
|
||||
|
@ -752,7 +811,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
rule=lambda state: state.has(laurels, player)
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
|
||||
regions["Fortress Courtyard Upper"].connect(
|
||||
fort_upper_lower = regions["Fortress Courtyard Upper"].connect(
|
||||
connecting_region=regions["Fortress Courtyard"])
|
||||
# nmg: can ice grapple to the upper ledge
|
||||
regions["Fortress Courtyard"].connect(
|
||||
|
@ -762,12 +821,12 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
regions["Fortress Courtyard Upper"].connect(
|
||||
connecting_region=regions["Fortress Exterior from Overworld"])
|
||||
|
||||
regions["Beneath the Vault Ladder Exit"].connect(
|
||||
btv_front_to_main = regions["Beneath the Vault Ladder Exit"].connect(
|
||||
connecting_region=regions["Beneath the Vault Main"],
|
||||
rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world)
|
||||
and has_lantern(state, world)
|
||||
# there's some boxes in the way
|
||||
and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player)))
|
||||
and (has_melee(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player)))
|
||||
# on the reverse trip, you can lure an enemy over to break the boxes if needed
|
||||
regions["Beneath the Vault Main"].connect(
|
||||
connecting_region=regions["Beneath the Vault Ladder Exit"],
|
||||
|
@ -775,11 +834,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
|
||||
regions["Beneath the Vault Main"].connect(
|
||||
connecting_region=regions["Beneath the Vault Back"])
|
||||
regions["Beneath the Vault Back"].connect(
|
||||
btv_back_to_main = regions["Beneath the Vault Back"].connect(
|
||||
connecting_region=regions["Beneath the Vault Main"],
|
||||
rule=lambda state: has_lantern(state, world))
|
||||
|
||||
regions["Fortress East Shortcut Upper"].connect(
|
||||
fort_east_upper_lower = regions["Fortress East Shortcut Upper"].connect(
|
||||
connecting_region=regions["Fortress East Shortcut Lower"])
|
||||
regions["Fortress East Shortcut Lower"].connect(
|
||||
connecting_region=regions["Fortress East Shortcut Upper"],
|
||||
|
@ -794,21 +853,31 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
connecting_region=regions["Eastern Vault Fortress"],
|
||||
rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world))
|
||||
|
||||
regions["Fortress Grave Path"].connect(
|
||||
connecting_region=regions["Fortress Grave Path Dusty Entrance Region"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["Fortress Grave Path Dusty Entrance Region"].connect(
|
||||
connecting_region=regions["Fortress Grave Path"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
fort_grave_entry_to_combat = regions["Fortress Grave Path Entry"].connect(
|
||||
connecting_region=regions["Fortress Grave Path Combat"])
|
||||
regions["Fortress Grave Path Combat"].connect(
|
||||
connecting_region=regions["Fortress Grave Path Entry"])
|
||||
|
||||
regions["Fortress Grave Path"].connect(
|
||||
regions["Fortress Grave Path Combat"].connect(
|
||||
connecting_region=regions["Fortress Grave Path by Grave"])
|
||||
|
||||
# run past the enemies
|
||||
regions["Fortress Grave Path by Grave"].connect(
|
||||
connecting_region=regions["Fortress Grave Path Entry"])
|
||||
|
||||
regions["Fortress Grave Path by Grave"].connect(
|
||||
connecting_region=regions["Fortress Hero's Grave Region"],
|
||||
rule=lambda state: has_ability(prayer, state, world))
|
||||
regions["Fortress Hero's Grave Region"].connect(
|
||||
connecting_region=regions["Fortress Grave Path"])
|
||||
connecting_region=regions["Fortress Grave Path by Grave"])
|
||||
|
||||
regions["Fortress Grave Path by Grave"].connect(
|
||||
connecting_region=regions["Fortress Grave Path Dusty Entrance Region"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
# reverse connection is conditionally made later, depending on whether combat logic is on, and the details of ER
|
||||
|
||||
regions["Fortress Grave Path Upper"].connect(
|
||||
connecting_region=regions["Fortress Grave Path"],
|
||||
connecting_region=regions["Fortress Grave Path Entry"],
|
||||
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
|
||||
regions["Fortress Arena"].connect(
|
||||
|
@ -831,19 +900,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
regions["Quarry Portal"].connect(
|
||||
connecting_region=regions["Quarry Entry"])
|
||||
|
||||
regions["Quarry Entry"].connect(
|
||||
quarry_entry_to_main = regions["Quarry Entry"].connect(
|
||||
connecting_region=regions["Quarry"],
|
||||
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
|
||||
regions["Quarry"].connect(
|
||||
connecting_region=regions["Quarry Entry"])
|
||||
|
||||
regions["Quarry Back"].connect(
|
||||
quarry_back_to_main = regions["Quarry Back"].connect(
|
||||
connecting_region=regions["Quarry"],
|
||||
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
|
||||
regions["Quarry"].connect(
|
||||
connecting_region=regions["Quarry Back"])
|
||||
|
||||
regions["Quarry Monastery Entry"].connect(
|
||||
monastery_to_quarry_main = regions["Quarry Monastery Entry"].connect(
|
||||
connecting_region=regions["Quarry"],
|
||||
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
|
||||
regions["Quarry"].connect(
|
||||
|
@ -869,18 +938,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world)
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
|
||||
# nmg: bring a scav over, then ice grapple through the door, only with ER on to avoid soft lock
|
||||
regions["Even Lower Quarry"].connect(
|
||||
connecting_region=regions["Even Lower Quarry Isolated Chest"])
|
||||
# you grappled down, might as well loot the rest too
|
||||
lower_quarry_empty_to_combat = regions["Even Lower Quarry Isolated Chest"].connect(
|
||||
connecting_region=regions["Even Lower Quarry"],
|
||||
rule=lambda state: has_mask(state, world))
|
||||
|
||||
regions["Even Lower Quarry Isolated Chest"].connect(
|
||||
connecting_region=regions["Lower Quarry Zig Door"],
|
||||
rule=lambda state: state.has("Activate Quarry Fuse", player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
|
||||
|
||||
# nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask only with ER on
|
||||
# don't need the mask for this either, please don't complain about not needing a mask here, you know what you did
|
||||
regions["Quarry"].connect(
|
||||
connecting_region=regions["Lower Quarry Zig Door"],
|
||||
connecting_region=regions["Even Lower Quarry Isolated Chest"],
|
||||
rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world))
|
||||
|
||||
regions["Monastery Front"].connect(
|
||||
monastery_front_to_back = regions["Monastery Front"].connect(
|
||||
connecting_region=regions["Monastery Back"])
|
||||
# laurels through the gate, no setup needed
|
||||
regions["Monastery Back"].connect(
|
||||
|
@ -897,7 +972,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
regions["Rooted Ziggurat Upper Entry"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Upper Front"])
|
||||
|
||||
regions["Rooted Ziggurat Upper Front"].connect(
|
||||
zig_upper_front_back = regions["Rooted Ziggurat Upper Front"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Upper Back"],
|
||||
rule=lambda state: state.has(laurels, player) or has_sword(state, player))
|
||||
regions["Rooted Ziggurat Upper Back"].connect(
|
||||
|
@ -907,13 +982,23 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
regions["Rooted Ziggurat Middle Top"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Middle Bottom"])
|
||||
|
||||
zig_low_entry_to_front = regions["Rooted Ziggurat Lower Entry"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Front"])
|
||||
regions["Rooted Ziggurat Lower Front"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Entry"])
|
||||
|
||||
regions["Rooted Ziggurat Lower Front"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"])
|
||||
zig_low_mid_to_front = regions["Rooted Ziggurat Lower Mid Checkpoint"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Front"])
|
||||
|
||||
zig_low_mid_to_back = regions["Rooted Ziggurat Lower Mid Checkpoint"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Back"],
|
||||
rule=lambda state: state.has(laurels, player)
|
||||
or (has_sword(state, player) and has_ability(prayer, state, world)))
|
||||
# nmg: can ice grapple on the voidlings to the double admin fight, still need to pray at the fuse
|
||||
regions["Rooted Ziggurat Lower Back"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Front"],
|
||||
# can ice grapple to the voidlings to get to the double admin fight, still need to pray at the fuse
|
||||
zig_low_back_to_mid = regions["Rooted Ziggurat Lower Back"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"],
|
||||
rule=lambda state: (state.has(laurels, player)
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
and has_ability(prayer, state, world)
|
||||
|
@ -925,6 +1010,8 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
regions["Rooted Ziggurat Portal Room Entrance"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Back"])
|
||||
|
||||
# zig skip region only gets made if entrance rando and fewer shops are on
|
||||
if options.entrance_rando and options.fixed_shop:
|
||||
regions["Zig Skip Exit"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Front"])
|
||||
|
||||
|
@ -952,7 +1039,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
or state.has(laurels, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
|
||||
|
||||
# a whole lot of stuff to basically say "you need to pray at the overworld fuse"
|
||||
swamp_mid_to_cath = regions["Swamp Mid"].connect(
|
||||
connecting_region=regions["Swamp to Cathedral Main Entrance Region"],
|
||||
rule=lambda state: (has_ability(prayer, state, world)
|
||||
|
@ -965,7 +1051,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
"Ladder to Swamp",
|
||||
"Ladders near Weathervane"}, player)
|
||||
or (state.has("Ladder to Ruined Atoll", player)
|
||||
and state.can_reach_region("Overworld Beach", player))))))
|
||||
and state.can_reach_region("Overworld Beach", player)))))
|
||||
and (not options.combat_logic
|
||||
or has_combat_reqs("Swamp", state, player)))
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
|
||||
if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders:
|
||||
|
@ -1017,13 +1105,23 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
regions["Swamp Hero's Grave Region"].connect(
|
||||
connecting_region=regions["Back of Swamp"])
|
||||
|
||||
regions["Cathedral"].connect(
|
||||
cath_entry_to_elev = regions["Cathedral Entry"].connect(
|
||||
connecting_region=regions["Cathedral to Gauntlet"],
|
||||
rule=lambda state: (has_ability(prayer, state, world)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
or options.entrance_rando) # elevator is always there in ER
|
||||
regions["Cathedral to Gauntlet"].connect(
|
||||
connecting_region=regions["Cathedral"])
|
||||
connecting_region=regions["Cathedral Entry"])
|
||||
|
||||
cath_entry_to_main = regions["Cathedral Entry"].connect(
|
||||
connecting_region=regions["Cathedral Main"])
|
||||
regions["Cathedral Main"].connect(
|
||||
connecting_region=regions["Cathedral Entry"])
|
||||
|
||||
cath_elev_to_main = regions["Cathedral to Gauntlet"].connect(
|
||||
connecting_region=regions["Cathedral Main"])
|
||||
regions["Cathedral Main"].connect(
|
||||
connecting_region=regions["Cathedral to Gauntlet"])
|
||||
|
||||
regions["Cathedral Gauntlet Checkpoint"].connect(
|
||||
connecting_region=regions["Cathedral Gauntlet"])
|
||||
|
@ -1075,7 +1173,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
connecting_region=regions["Far Shore"])
|
||||
|
||||
# Misc
|
||||
regions["Spirit Arena"].connect(
|
||||
heir_fight = regions["Spirit Arena"].connect(
|
||||
connecting_region=regions["Spirit Arena Victory"],
|
||||
rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if
|
||||
world.options.hexagon_quest else
|
||||
|
@ -1219,6 +1317,192 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
|||
for region in ladder_regions.values():
|
||||
world.multiworld.regions.append(region)
|
||||
|
||||
# for combat logic, easiest to replace or add to existing rules
|
||||
if world.options.combat_logic >= CombatLogic.option_bosses_only:
|
||||
set_rule(wg_to_after_gk,
|
||||
lambda state: state.has(laurels, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
|
||||
or has_combat_reqs("Garden Knight", state, player))
|
||||
# laurels past, or ice grapple it off, or ice grapple to it and fight
|
||||
set_rule(after_gk_to_wg,
|
||||
lambda state: state.has(laurels, player)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
|
||||
or (has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)
|
||||
and has_combat_reqs("Garden Knight", state, player)))
|
||||
|
||||
if not world.options.hexagon_quest:
|
||||
add_rule(heir_fight,
|
||||
lambda state: has_combat_reqs("The Heir", state, player))
|
||||
|
||||
if world.options.combat_logic == CombatLogic.option_on:
|
||||
# these are redundant with combat logic off
|
||||
regions["Fortress Grave Path Entry"].connect(
|
||||
connecting_region=regions["Fortress Grave Path Dusty Entrance Region"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
regions["Rooted Ziggurat Lower Entry"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
regions["Rooted Ziggurat Lower Mid Checkpoint"].connect(
|
||||
connecting_region=regions["Rooted Ziggurat Lower Entry"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
add_rule(ow_to_town_portal,
|
||||
lambda state: has_combat_reqs("Before Well", state, player))
|
||||
# need to fight through the rudelings and turret, or just laurels from near the windmill
|
||||
set_rule(ow_to_well_entry,
|
||||
lambda state: state.has(laurels, player)
|
||||
or has_combat_reqs("East Forest", state, player))
|
||||
set_rule(ow_tunnel_beach,
|
||||
lambda state: has_combat_reqs("East Forest", state, player))
|
||||
|
||||
add_rule(atoll_statue,
|
||||
lambda state: has_combat_reqs("Ruined Atoll", state, player))
|
||||
set_rule(frogs_front_to_main,
|
||||
lambda state: has_combat_reqs("Frog's Domain", state, player))
|
||||
|
||||
set_rule(btw_front_main,
|
||||
lambda state: state.has(laurels, player) or has_combat_reqs("Beneath the Well", state, player))
|
||||
set_rule(btw_back_main,
|
||||
lambda state: has_ladder("Ladders in Well", state, world)
|
||||
and (state.has(laurels, player) or has_combat_reqs("Beneath the Well", state, player)))
|
||||
set_rule(well_boss_to_dt,
|
||||
lambda state: has_combat_reqs("Beneath the Well", state, player)
|
||||
or laurels_zip(state, world))
|
||||
|
||||
add_rule(dt_entry_to_upper,
|
||||
lambda state: has_combat_reqs("Dark Tomb", state, player))
|
||||
add_rule(dt_exit_to_main,
|
||||
lambda state: has_combat_reqs("Dark Tomb", state, player))
|
||||
|
||||
set_rule(wg_before_to_after_terry,
|
||||
lambda state: state.has_any({laurels, ice_dagger}, player)
|
||||
or has_combat_reqs("West Garden", state, player))
|
||||
set_rule(wg_after_to_before_terry,
|
||||
lambda state: state.has_any({laurels, ice_dagger}, player)
|
||||
or has_combat_reqs("West Garden", state, player))
|
||||
# laurels through, probably to the checkpoint, or just fight
|
||||
set_rule(wg_checkpoint_to_after_terry,
|
||||
lambda state: state.has(laurels, player) or has_combat_reqs("West Garden", state, player))
|
||||
set_rule(wg_checkpoint_to_before_boss,
|
||||
lambda state: has_combat_reqs("West Garden", state, player))
|
||||
|
||||
add_rule(btv_front_to_main,
|
||||
lambda state: has_combat_reqs("Beneath the Vault", state, player))
|
||||
add_rule(btv_back_to_main,
|
||||
lambda state: has_combat_reqs("Beneath the Vault", state, player))
|
||||
|
||||
add_rule(fort_upper_lower,
|
||||
lambda state: state.has(ice_dagger, player)
|
||||
or has_combat_reqs("Eastern Vault Fortress", state, player))
|
||||
set_rule(fort_grave_entry_to_combat,
|
||||
lambda state: has_combat_reqs("Eastern Vault Fortress", state, player))
|
||||
|
||||
set_rule(quarry_entry_to_main,
|
||||
lambda state: has_combat_reqs("Quarry", state, player))
|
||||
set_rule(quarry_back_to_main,
|
||||
lambda state: has_combat_reqs("Quarry", state, player))
|
||||
set_rule(monastery_to_quarry_main,
|
||||
lambda state: has_combat_reqs("Quarry", state, player))
|
||||
set_rule(monastery_front_to_back,
|
||||
lambda state: has_combat_reqs("Quarry", state, player))
|
||||
set_rule(lower_quarry_empty_to_combat,
|
||||
lambda state: has_combat_reqs("Quarry", state, player))
|
||||
|
||||
set_rule(zig_upper_front_back,
|
||||
lambda state: state.has(laurels, player)
|
||||
or has_combat_reqs("Rooted Ziggurat", state, player))
|
||||
set_rule(zig_low_entry_to_front,
|
||||
lambda state: has_combat_reqs("Rooted Ziggurat", state, player))
|
||||
set_rule(zig_low_mid_to_front,
|
||||
lambda state: has_combat_reqs("Rooted Ziggurat", state, player))
|
||||
set_rule(zig_low_mid_to_back,
|
||||
lambda state: state.has(laurels, player)
|
||||
or (has_ability(prayer, state, world) and has_combat_reqs("Rooted Ziggurat", state, player)))
|
||||
set_rule(zig_low_back_to_mid,
|
||||
lambda state: (state.has(laurels, player)
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world))
|
||||
and has_ability(prayer, state, world)
|
||||
and has_combat_reqs("Rooted Ziggurat", state, player))
|
||||
|
||||
# only activating the fuse requires combat logic
|
||||
set_rule(cath_entry_to_elev,
|
||||
lambda state: options.entrance_rando
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)
|
||||
or (has_ability(prayer, state, world) and has_combat_reqs("Cathedral", state, player)))
|
||||
|
||||
set_rule(cath_entry_to_main,
|
||||
lambda state: has_combat_reqs("Cathedral", state, player))
|
||||
set_rule(cath_elev_to_main,
|
||||
lambda state: has_combat_reqs("Cathedral", state, player))
|
||||
|
||||
# for spots where you can go into and come out of an entrance to reset enemy aggro
|
||||
if world.options.entrance_rando:
|
||||
# for the chest outside of magic dagger house
|
||||
dagger_entry_paired_name, dagger_entry_paired_region = (
|
||||
get_paired_portal("Archipelagos Redux, archipelagos_house_"))
|
||||
try:
|
||||
dagger_entry_paired_entrance = world.get_entrance(dagger_entry_paired_name)
|
||||
except KeyError:
|
||||
# there is no paired entrance, so you must fight or dash past, which is done in the finally
|
||||
pass
|
||||
else:
|
||||
set_rule(wg_checkpoint_to_dagger,
|
||||
lambda state: dagger_entry_paired_entrance.can_reach(state))
|
||||
world.multiworld.register_indirect_condition(region=regions["West Garden at Dagger House"],
|
||||
entrance=dagger_entry_paired_entrance)
|
||||
finally:
|
||||
add_rule(wg_checkpoint_to_dagger,
|
||||
lambda state: state.has(laurels, player) or has_combat_reqs("West Garden", state, player),
|
||||
combine="or")
|
||||
|
||||
# zip past enemies in fortress grave path to enter the dusty entrance, then come back out
|
||||
fort_dusty_paired_name, fort_dusty_paired_region = get_paired_portal("Fortress Reliquary, Dusty_")
|
||||
try:
|
||||
fort_dusty_paired_entrance = world.get_entrance(fort_dusty_paired_name)
|
||||
except KeyError:
|
||||
# there is no paired entrance, so you can't run past to deaggro
|
||||
# the path to dusty can be done via combat, so no need to do anything here
|
||||
pass
|
||||
else:
|
||||
# there is a paired entrance, so you can use that to deaggro enemies
|
||||
regions["Fortress Grave Path Dusty Entrance Region"].connect(
|
||||
connecting_region=regions["Fortress Grave Path by Grave"],
|
||||
rule=lambda state: state.has(laurels, player) and fort_dusty_paired_entrance.can_reach(state))
|
||||
world.multiworld.register_indirect_condition(region=regions["Fortress Grave Path by Grave"],
|
||||
entrance=fort_dusty_paired_entrance)
|
||||
|
||||
# for activating the ladder switch to get from fortress east upper to lower
|
||||
fort_east_upper_right_paired_name, fort_east_upper_right_paired_region = (
|
||||
get_paired_portal("Fortress East, Fortress Courtyard_"))
|
||||
try:
|
||||
fort_east_upper_right_paired_entrance = (
|
||||
world.get_entrance(fort_east_upper_right_paired_name))
|
||||
except KeyError:
|
||||
# no paired entrance, so you must fight, which is done in the finally
|
||||
pass
|
||||
else:
|
||||
set_rule(fort_east_upper_lower,
|
||||
lambda state: fort_east_upper_right_paired_entrance.can_reach(state))
|
||||
world.multiworld.register_indirect_condition(region=regions["Fortress East Shortcut Lower"],
|
||||
entrance=fort_east_upper_right_paired_entrance)
|
||||
finally:
|
||||
add_rule(fort_east_upper_lower,
|
||||
lambda state: has_combat_reqs("Eastern Vault Fortress", state, player)
|
||||
or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world),
|
||||
combine="or")
|
||||
|
||||
else:
|
||||
# if combat logic is on and ER is off, we can make this entrance freely
|
||||
regions["Fortress Grave Path Dusty Entrance Region"].connect(
|
||||
connecting_region=regions["Fortress Grave Path by Grave"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
else:
|
||||
# if combat logic is off, we can make this entrance freely
|
||||
regions["Fortress Grave Path Dusty Entrance Region"].connect(
|
||||
connecting_region=regions["Fortress Grave Path by Grave"],
|
||||
rule=lambda state: state.has(laurels, player))
|
||||
|
||||
|
||||
def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
player = world.player
|
||||
|
@ -1315,6 +1599,11 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
|||
set_rule(world.get_location("East Forest - Ice Rod Grapple Chest"), lambda state: (
|
||||
state.has_all({grapple, ice_dagger, fire_wand}, player) and has_ability(icebolt, state, world)))
|
||||
|
||||
# Dark Tomb
|
||||
# added to make combat logic smoother
|
||||
set_rule(world.get_location("Dark Tomb - 2nd Laser Room"),
|
||||
lambda state: has_lantern(state, world))
|
||||
|
||||
# West Garden
|
||||
set_rule(world.get_location("West Garden - [North] Across From Page Pickup"),
|
||||
lambda state: state.has(laurels, player))
|
||||
|
@ -1348,11 +1637,11 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
|||
|
||||
# Library Lab
|
||||
set_rule(world.get_location("Library Lab - Page 1"),
|
||||
lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player))
|
||||
lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player))
|
||||
set_rule(world.get_location("Library Lab - Page 2"),
|
||||
lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player))
|
||||
lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player))
|
||||
set_rule(world.get_location("Library Lab - Page 3"),
|
||||
lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player))
|
||||
lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player))
|
||||
|
||||
# Eastern Vault Fortress
|
||||
set_rule(world.get_location("Fortress Arena - Hexagon Red"),
|
||||
|
@ -1361,11 +1650,11 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
|||
# gun isn't included since it can only break one leaf pile at a time, and we don't check how much mana you have
|
||||
# but really, I expect the player to just throw a bomb at them if they don't have melee
|
||||
set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"),
|
||||
lambda state: has_stick(state, player) or state.has(ice_dagger, player))
|
||||
lambda state: has_melee(state, player) or state.has(ice_dagger, player))
|
||||
|
||||
# Beneath the Vault
|
||||
set_rule(world.get_location("Beneath the Fortress - Bridge"),
|
||||
lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player))
|
||||
lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player))
|
||||
|
||||
# Quarry
|
||||
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),
|
||||
|
@ -1421,9 +1710,9 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
|||
|
||||
# Events
|
||||
set_rule(world.get_location("Eastern Bell"),
|
||||
lambda state: (has_stick(state, player) or state.has(fire_wand, player)))
|
||||
lambda state: (has_melee(state, player) or state.has(fire_wand, player)))
|
||||
set_rule(world.get_location("Western Bell"),
|
||||
lambda state: (has_stick(state, player) or state.has(fire_wand, player)))
|
||||
lambda state: (has_melee(state, player) or state.has(fire_wand, player)))
|
||||
set_rule(world.get_location("Furnace Fuse"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("South and West Fortress Exterior Fuses"),
|
||||
|
@ -1470,3 +1759,129 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
|||
lambda state: has_sword(state, player))
|
||||
set_rule(world.get_location("Shop - Coin 2"),
|
||||
lambda state: has_sword(state, player))
|
||||
|
||||
def combat_logic_to_loc(loc_name: str, combat_req_area: str, set_instead: bool = False,
|
||||
dagger: bool = False, laurel: bool = False) -> None:
|
||||
# dagger means you can use magic dagger instead of combat for that check
|
||||
# laurel means you can dodge the enemies freely with the laurels
|
||||
if set_instead:
|
||||
set_rule(world.get_location(loc_name),
|
||||
lambda state: has_combat_reqs(combat_req_area, state, player)
|
||||
or (dagger and state.has(ice_dagger, player))
|
||||
or (laurel and state.has(laurels, player)))
|
||||
else:
|
||||
add_rule(world.get_location(loc_name),
|
||||
lambda state: has_combat_reqs(combat_req_area, state, player)
|
||||
or (dagger and state.has(ice_dagger, player))
|
||||
or (laurel and state.has(laurels, player)))
|
||||
|
||||
if world.options.combat_logic >= CombatLogic.option_bosses_only:
|
||||
# garden knight is in the regions part above
|
||||
combat_logic_to_loc("Fortress Arena - Siege Engine/Vault Key Pickup", "Siege Engine", set_instead=True)
|
||||
combat_logic_to_loc("Librarian - Hexagon Green", "The Librarian", set_instead=True)
|
||||
set_rule(world.get_location("Librarian - Hexagon Green"),
|
||||
rule=lambda state: has_combat_reqs("The Librarian", state, player)
|
||||
and has_ladder("Ladders in Library", state, world))
|
||||
combat_logic_to_loc("Rooted Ziggurat Lower - Hexagon Blue", "Boss Scavenger", set_instead=True)
|
||||
if world.options.ice_grappling >= IceGrappling.option_medium:
|
||||
add_rule(world.get_location("Rooted Ziggurat Lower - Hexagon Blue"),
|
||||
lambda state: has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
combat_logic_to_loc("Cathedral Gauntlet - Gauntlet Reward", "Gauntlet", set_instead=True)
|
||||
|
||||
if world.options.combat_logic == CombatLogic.option_on:
|
||||
combat_logic_to_loc("Overworld - [Northeast] Flowers Holy Cross", "Garden Knight")
|
||||
combat_logic_to_loc("Overworld - [Northwest] Chest Near Quarry Gate", "Before Well", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Northeast] Chest Above Patrol Cave", "Garden Knight", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret", "Overworld", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] West Beach Guarded By Turret 2", "Overworld")
|
||||
combat_logic_to_loc("Overworld - [Southwest] Bombable Wall Near Fountain", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] Fountain Holy Cross", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] South Chest Near Guard", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Southwest] Tunnel Guarded By Turret", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("Overworld - [Northwest] Chest Near Turret", "Before Well")
|
||||
|
||||
add_rule(world.get_location("Hourglass Cave - Hourglass Chest"),
|
||||
lambda state: has_sword(state, player) and (state.has("Shield", player)
|
||||
# kill the turrets through the wall with a longer sword
|
||||
or state.has("Sword Upgrade", player, 3)))
|
||||
add_rule(world.get_location("Hourglass Cave - Holy Cross Chest"),
|
||||
lambda state: has_sword(state, player) and (state.has("Shield", player)
|
||||
or state.has("Sword Upgrade", player, 3)))
|
||||
|
||||
# the first spider chest they literally do not attack you until you open the chest
|
||||
# the second one, you can still just walk past them, but I guess /something/ would be wanted
|
||||
combat_logic_to_loc("East Forest - Beneath Spider Chest", "East Forest", dagger=True, laurel=True)
|
||||
combat_logic_to_loc("East Forest - Golden Obelisk Holy Cross", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("East Forest - Dancing Fox Spirit Holy Cross", "East Forest", dagger=True, laurel=True)
|
||||
combat_logic_to_loc("East Forest - From Guardhouse 1 Chest", "East Forest", dagger=True, laurel=True)
|
||||
combat_logic_to_loc("East Forest - Above Save Point", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("East Forest - Above Save Point Obscured", "East Forest", dagger=True)
|
||||
combat_logic_to_loc("Forest Grave Path - Above Gate", "East Forest", dagger=True, laurel=True)
|
||||
combat_logic_to_loc("Forest Grave Path - Obscured Chest", "East Forest", dagger=True, laurel=True)
|
||||
|
||||
# most of beneath the well is covered by the region access rule
|
||||
combat_logic_to_loc("Beneath the Well - [Entryway] Chest", "Beneath the Well")
|
||||
combat_logic_to_loc("Beneath the Well - [Entryway] Obscured Behind Waterfall", "Beneath the Well")
|
||||
combat_logic_to_loc("Beneath the Well - [Back Corridor] Left Secret", "Beneath the Well")
|
||||
combat_logic_to_loc("Beneath the Well - [Side Room] Chest By Phrends", "Overworld")
|
||||
|
||||
# laurels past the enemies, then use the wand or gun to take care of the fairies that chased you
|
||||
add_rule(world.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest"),
|
||||
lambda state: state.has_any({fire_wand, "Gun"}, player))
|
||||
combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Faeries", "West Garden")
|
||||
combat_logic_to_loc("West Garden - [Central Lowlands] Chest Beneath Save Point", "West Garden")
|
||||
combat_logic_to_loc("West Garden - [West Highlands] Upper Left Walkway", "West Garden")
|
||||
|
||||
# with combat logic on, I presume the player will want to be able to see to avoid the spiders
|
||||
set_rule(world.get_location("Beneath the Fortress - Bridge"),
|
||||
lambda state: has_lantern(state, world)
|
||||
and (state.has_any({laurels, fire_wand, "Gun"}, player) or has_melee(state, player)))
|
||||
|
||||
combat_logic_to_loc("Eastern Vault Fortress - [West Wing] Candles Holy Cross", "Eastern Vault Fortress",
|
||||
dagger=True)
|
||||
|
||||
# could just do the last two, but this outputs better in the spoiler log
|
||||
# dagger is maybe viable here, but it's sketchy -- activate ladder switch, save to reset enemies, climb up
|
||||
combat_logic_to_loc("Upper and Central Fortress Exterior Fuses", "Eastern Vault Fortress")
|
||||
combat_logic_to_loc("Beneath the Vault Fuse", "Beneath the Vault")
|
||||
combat_logic_to_loc("Eastern Vault West Fuses", "Eastern Vault Fortress")
|
||||
|
||||
# if you come in from the left, you only need to fight small crabs
|
||||
add_rule(world.get_location("Ruined Atoll - [South] Near Birds"),
|
||||
lambda state: has_melee(state, player) or state.has_any({laurels, "Gun"}, player))
|
||||
|
||||
# can get this one without fighting if you have laurels
|
||||
add_rule(world.get_location("Frog's Domain - Above Vault"),
|
||||
lambda state: state.has(laurels, player) or has_combat_reqs("Frog's Domain", state, player))
|
||||
|
||||
# with wand, you can get this chest. Non-ER, you need laurels to continue down. ER, you can just torch
|
||||
set_rule(world.get_location("Rooted Ziggurat Upper - Near Bridge Switch"),
|
||||
lambda state: (state.has(fire_wand, player)
|
||||
and (state.has(laurels, player) or world.options.entrance_rando))
|
||||
or has_combat_reqs("Rooted Ziggurat", state, player))
|
||||
set_rule(world.get_location("Rooted Ziggurat Lower - After Guarded Fuse"),
|
||||
lambda state: has_ability(prayer, state, world)
|
||||
and has_combat_reqs("Rooted Ziggurat", state, player))
|
||||
|
||||
# replace the sword rule with this one
|
||||
combat_logic_to_loc("Swamp - [South Graveyard] 4 Orange Skulls", "Swamp", set_instead=True)
|
||||
combat_logic_to_loc("Swamp - [South Graveyard] Guarded By Big Skeleton", "Swamp", dagger=True)
|
||||
# don't really agree with this one but eh
|
||||
combat_logic_to_loc("Swamp - [South Graveyard] Above Big Skeleton", "Swamp", dagger=True, laurel=True)
|
||||
# the tentacles deal with everything else reasonably, and you can hide on the island, so no rule for it
|
||||
add_rule(world.get_location("Swamp - [South Graveyard] Obscured Beneath Telescope"),
|
||||
lambda state: state.has(laurels, player) # can dash from swamp mid to here and grab it
|
||||
or has_combat_reqs("Swamp", state, player))
|
||||
add_rule(world.get_location("Swamp - [Central] South Secret Passage"),
|
||||
lambda state: state.has(laurels, player) # can dash from swamp front to here and grab it
|
||||
or has_combat_reqs("Swamp", state, player))
|
||||
combat_logic_to_loc("Swamp - [South Graveyard] Upper Walkway On Pedestal", "Swamp")
|
||||
combat_logic_to_loc("Swamp - [Central] Beneath Memorial", "Swamp")
|
||||
combat_logic_to_loc("Swamp - [Central] Near Ramps Up", "Swamp")
|
||||
combat_logic_to_loc("Swamp - [Upper Graveyard] Near Telescope", "Swamp")
|
||||
combat_logic_to_loc("Swamp - [Upper Graveyard] Near Shield Fleemers", "Swamp")
|
||||
combat_logic_to_loc("Swamp - [Upper Graveyard] Obscured Behind Hill", "Swamp")
|
||||
|
||||
# zip through the rubble to sneakily grab this chest, or just fight to it
|
||||
add_rule(world.get_location("Cathedral - [1F] Near Spikes"),
|
||||
lambda state: laurels_zip(state, world) or has_combat_reqs("Cathedral", state, player))
|
||||
|
|
|
@ -22,10 +22,19 @@ class TunicERLocation(Location):
|
|||
|
||||
def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
||||
regions: Dict[str, Region] = {}
|
||||
for region_name, region_data in world.er_regions.items():
|
||||
regions[region_name] = Region(region_name, world.player, world.multiworld)
|
||||
|
||||
if world.options.entrance_rando:
|
||||
for region_name, region_data in world.er_regions.items():
|
||||
# if fewer shops is off, zig skip is not made
|
||||
if region_name == "Zig Skip Exit":
|
||||
# need to check if there's a seed group for this first
|
||||
if world.options.entrance_rando.value not in EntranceRando.options.values():
|
||||
if not world.seed_groups[world.options.entrance_rando.value]["fixed_shop"]:
|
||||
continue
|
||||
elif not world.options.fixed_shop:
|
||||
continue
|
||||
regions[region_name] = Region(region_name, world.player, world.multiworld)
|
||||
|
||||
portal_pairs = pair_portals(world, regions)
|
||||
|
||||
# output the entrances to the spoiler log here for convenience
|
||||
|
@ -33,8 +42,15 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||
for portal1, portal2 in sorted_portal_pairs.items():
|
||||
world.multiworld.spoiler.set_entrance(portal1, portal2, "both", world.player)
|
||||
else:
|
||||
for region_name, region_data in world.er_regions.items():
|
||||
# filter out regions that are inaccessible in non-er
|
||||
if region_name not in ["Zig Skip Exit", "Purgatory"]:
|
||||
regions[region_name] = Region(region_name, world.player, world.multiworld)
|
||||
|
||||
portal_pairs = vanilla_portals(world, regions)
|
||||
|
||||
create_randomized_entrances(portal_pairs, regions)
|
||||
|
||||
set_er_region_rules(world, regions, portal_pairs)
|
||||
|
||||
for location_name, location_id in world.location_name_to_id.items():
|
||||
|
@ -42,8 +58,6 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]:
|
|||
location = TunicERLocation(world.player, location_name, location_id, region)
|
||||
region.locations.append(location)
|
||||
|
||||
create_randomized_entrances(portal_pairs, regions)
|
||||
|
||||
for region in regions.values():
|
||||
world.multiworld.regions.append(region)
|
||||
|
||||
|
@ -70,7 +84,7 @@ tunic_events: Dict[str, str] = {
|
|||
"Quarry Connector Fuse": "Quarry Connector",
|
||||
"Quarry Fuse": "Quarry Entry",
|
||||
"Ziggurat Fuse": "Rooted Ziggurat Lower Back",
|
||||
"West Garden Fuse": "West Garden",
|
||||
"West Garden Fuse": "West Garden South Checkpoint",
|
||||
"Library Fuse": "Library Lab",
|
||||
"Place Questagons": "Sealed Temple",
|
||||
}
|
||||
|
@ -108,7 +122,8 @@ def create_shop_region(world: "TunicWorld", regions: Dict[str, Region]) -> None:
|
|||
def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal, Portal]:
|
||||
portal_pairs: Dict[Portal, Portal] = {}
|
||||
# we don't want the zig skip exit for vanilla portals, since it shouldn't be considered for logic here
|
||||
portal_map = [portal for portal in portal_mapping if portal.name != "Ziggurat Lower Falling Entrance"]
|
||||
portal_map = [portal for portal in portal_mapping if portal.name not in
|
||||
["Ziggurat Lower Falling Entrance", "Purgatory Bottom Exit", "Purgatory Top Exit"]]
|
||||
|
||||
while portal_map:
|
||||
portal1 = portal_map[0]
|
||||
|
@ -121,9 +136,6 @@ def vanilla_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Por
|
|||
destination="Previous Region", tag="_")
|
||||
create_shop_region(world, regions)
|
||||
|
||||
elif portal2_sdt == "Purgatory, Purgatory_bottom":
|
||||
portal2_sdt = "Purgatory, Purgatory_top"
|
||||
|
||||
for portal in portal_map:
|
||||
if portal.scene_destination() == portal2_sdt:
|
||||
portal2 = portal
|
||||
|
@ -414,6 +426,7 @@ def pair_portals(world: "TunicWorld", regions: Dict[str, Region]) -> Dict[Portal
|
|||
cr.add(portal.region)
|
||||
if "Secret Gathering Place" not in update_reachable_regions(cr, traversal_reqs, has_laurels, logic_tricks):
|
||||
continue
|
||||
# if not waterfall_plando, then we just want to pair secret gathering place now
|
||||
elif portal.region != "Secret Gathering Place":
|
||||
continue
|
||||
portal2 = portal
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from itertools import groupby
|
||||
from typing import Dict, List, Set, NamedTuple
|
||||
from typing import Dict, List, Set, NamedTuple, Optional
|
||||
from BaseClasses import ItemClassification as IC
|
||||
|
||||
|
||||
|
@ -8,6 +8,8 @@ class TunicItemData(NamedTuple):
|
|||
quantity_in_item_pool: int
|
||||
item_id_offset: int
|
||||
item_group: str = ""
|
||||
# classification if combat logic is on
|
||||
combat_ic: Optional[IC] = None
|
||||
|
||||
|
||||
item_base_id = 509342400
|
||||
|
@ -27,7 +29,7 @@ item_table: Dict[str, TunicItemData] = {
|
|||
"Lure x2": TunicItemData(IC.filler, 1, 11, "Consumables"),
|
||||
"Pepper x2": TunicItemData(IC.filler, 4, 12, "Consumables"),
|
||||
"Ivy x3": TunicItemData(IC.filler, 2, 13, "Consumables"),
|
||||
"Effigy": TunicItemData(IC.useful, 12, 14, "Money"),
|
||||
"Effigy": TunicItemData(IC.useful, 12, 14, "Money", combat_ic=IC.progression),
|
||||
"HP Berry": TunicItemData(IC.filler, 2, 15, "Consumables"),
|
||||
"HP Berry x2": TunicItemData(IC.filler, 4, 16, "Consumables"),
|
||||
"HP Berry x3": TunicItemData(IC.filler, 2, 17, "Consumables"),
|
||||
|
@ -44,32 +46,32 @@ item_table: Dict[str, TunicItemData] = {
|
|||
"Hero's Laurels": TunicItemData(IC.progression | IC.useful, 1, 28),
|
||||
"Lantern": TunicItemData(IC.progression, 1, 29),
|
||||
"Gun": TunicItemData(IC.progression | IC.useful, 1, 30, "Weapons"),
|
||||
"Shield": TunicItemData(IC.useful, 1, 31),
|
||||
"Shield": TunicItemData(IC.useful, 1, 31, combat_ic=IC.progression | IC.useful),
|
||||
"Dath Stone": TunicItemData(IC.useful, 1, 32),
|
||||
"Hourglass": TunicItemData(IC.useful, 1, 33),
|
||||
"Old House Key": TunicItemData(IC.progression, 1, 34, "Keys"),
|
||||
"Key": TunicItemData(IC.progression, 2, 35, "Keys"),
|
||||
"Fortress Vault Key": TunicItemData(IC.progression, 1, 36, "Keys"),
|
||||
"Flask Shard": TunicItemData(IC.useful, 12, 37),
|
||||
"Potion Flask": TunicItemData(IC.useful, 5, 38, "Flask"),
|
||||
"Flask Shard": TunicItemData(IC.useful, 12, 37, combat_ic=IC.progression),
|
||||
"Potion Flask": TunicItemData(IC.useful, 5, 38, "Flask", combat_ic=IC.progression),
|
||||
"Golden Coin": TunicItemData(IC.progression, 17, 39),
|
||||
"Card Slot": TunicItemData(IC.useful, 4, 40),
|
||||
"Red Questagon": TunicItemData(IC.progression_skip_balancing, 1, 41, "Hexagons"),
|
||||
"Green Questagon": TunicItemData(IC.progression_skip_balancing, 1, 42, "Hexagons"),
|
||||
"Blue Questagon": TunicItemData(IC.progression_skip_balancing, 1, 43, "Hexagons"),
|
||||
"Gold Questagon": TunicItemData(IC.progression_skip_balancing, 0, 44, "Hexagons"),
|
||||
"ATT Offering": TunicItemData(IC.useful, 4, 45, "Offerings"),
|
||||
"DEF Offering": TunicItemData(IC.useful, 4, 46, "Offerings"),
|
||||
"Potion Offering": TunicItemData(IC.useful, 3, 47, "Offerings"),
|
||||
"HP Offering": TunicItemData(IC.useful, 6, 48, "Offerings"),
|
||||
"MP Offering": TunicItemData(IC.useful, 3, 49, "Offerings"),
|
||||
"SP Offering": TunicItemData(IC.useful, 2, 50, "Offerings"),
|
||||
"Hero Relic - ATT": TunicItemData(IC.progression_skip_balancing, 1, 51, "Hero Relics"),
|
||||
"Hero Relic - DEF": TunicItemData(IC.progression_skip_balancing, 1, 52, "Hero Relics"),
|
||||
"Hero Relic - HP": TunicItemData(IC.progression_skip_balancing, 1, 53, "Hero Relics"),
|
||||
"Hero Relic - MP": TunicItemData(IC.progression_skip_balancing, 1, 54, "Hero Relics"),
|
||||
"Hero Relic - POTION": TunicItemData(IC.progression_skip_balancing, 1, 55, "Hero Relics"),
|
||||
"Hero Relic - SP": TunicItemData(IC.progression_skip_balancing, 1, 56, "Hero Relics"),
|
||||
"ATT Offering": TunicItemData(IC.useful, 4, 45, "Offerings", combat_ic=IC.progression),
|
||||
"DEF Offering": TunicItemData(IC.useful, 4, 46, "Offerings", combat_ic=IC.progression),
|
||||
"Potion Offering": TunicItemData(IC.useful, 3, 47, "Offerings", combat_ic=IC.progression),
|
||||
"HP Offering": TunicItemData(IC.useful, 6, 48, "Offerings", combat_ic=IC.progression),
|
||||
"MP Offering": TunicItemData(IC.useful, 3, 49, "Offerings", combat_ic=IC.progression),
|
||||
"SP Offering": TunicItemData(IC.useful, 2, 50, "Offerings", combat_ic=IC.progression),
|
||||
"Hero Relic - ATT": TunicItemData(IC.progression_skip_balancing, 1, 51, "Hero Relics", combat_ic=IC.progression),
|
||||
"Hero Relic - DEF": TunicItemData(IC.progression_skip_balancing, 1, 52, "Hero Relics", combat_ic=IC.progression),
|
||||
"Hero Relic - HP": TunicItemData(IC.progression_skip_balancing, 1, 53, "Hero Relics", combat_ic=IC.progression),
|
||||
"Hero Relic - MP": TunicItemData(IC.progression_skip_balancing, 1, 54, "Hero Relics", combat_ic=IC.progression),
|
||||
"Hero Relic - POTION": TunicItemData(IC.progression_skip_balancing, 1, 55, "Hero Relics", combat_ic=IC.progression),
|
||||
"Hero Relic - SP": TunicItemData(IC.progression_skip_balancing, 1, 56, "Hero Relics", combat_ic=IC.progression),
|
||||
"Orange Peril Ring": TunicItemData(IC.useful, 1, 57, "Cards"),
|
||||
"Tincture": TunicItemData(IC.useful, 1, 58, "Cards"),
|
||||
"Scavenger Mask": TunicItemData(IC.progression, 1, 59, "Cards"),
|
||||
|
@ -86,18 +88,18 @@ item_table: Dict[str, TunicItemData] = {
|
|||
"Louder Echo": TunicItemData(IC.useful, 1, 70, "Cards"),
|
||||
"Aura's Gem": TunicItemData(IC.useful, 1, 71, "Cards"),
|
||||
"Bone Card": TunicItemData(IC.useful, 1, 72, "Cards"),
|
||||
"Mr Mayor": TunicItemData(IC.useful, 1, 73, "Golden Treasures"),
|
||||
"Secret Legend": TunicItemData(IC.useful, 1, 74, "Golden Treasures"),
|
||||
"Sacred Geometry": TunicItemData(IC.useful, 1, 75, "Golden Treasures"),
|
||||
"Vintage": TunicItemData(IC.useful, 1, 76, "Golden Treasures"),
|
||||
"Just Some Pals": TunicItemData(IC.useful, 1, 77, "Golden Treasures"),
|
||||
"Regal Weasel": TunicItemData(IC.useful, 1, 78, "Golden Treasures"),
|
||||
"Spring Falls": TunicItemData(IC.useful, 1, 79, "Golden Treasures"),
|
||||
"Power Up": TunicItemData(IC.useful, 1, 80, "Golden Treasures"),
|
||||
"Back To Work": TunicItemData(IC.useful, 1, 81, "Golden Treasures"),
|
||||
"Phonomath": TunicItemData(IC.useful, 1, 82, "Golden Treasures"),
|
||||
"Dusty": TunicItemData(IC.useful, 1, 83, "Golden Treasures"),
|
||||
"Forever Friend": TunicItemData(IC.useful, 1, 84, "Golden Treasures"),
|
||||
"Mr Mayor": TunicItemData(IC.useful, 1, 73, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Secret Legend": TunicItemData(IC.useful, 1, 74, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Sacred Geometry": TunicItemData(IC.useful, 1, 75, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Vintage": TunicItemData(IC.useful, 1, 76, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Just Some Pals": TunicItemData(IC.useful, 1, 77, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Regal Weasel": TunicItemData(IC.useful, 1, 78, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Spring Falls": TunicItemData(IC.useful, 1, 79, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Power Up": TunicItemData(IC.useful, 1, 80, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Back To Work": TunicItemData(IC.useful, 1, 81, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Phonomath": TunicItemData(IC.useful, 1, 82, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Dusty": TunicItemData(IC.useful, 1, 83, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Forever Friend": TunicItemData(IC.useful, 1, 84, "Golden Treasures", combat_ic=IC.progression),
|
||||
"Fool Trap": TunicItemData(IC.trap, 0, 85),
|
||||
"Money x1": TunicItemData(IC.filler, 3, 86, "Money"),
|
||||
"Money x10": TunicItemData(IC.filler, 1, 87, "Money"),
|
||||
|
@ -112,9 +114,9 @@ item_table: Dict[str, TunicItemData] = {
|
|||
"Money x50": TunicItemData(IC.filler, 7, 96, "Money"),
|
||||
"Money x64": TunicItemData(IC.filler, 1, 97, "Money"),
|
||||
"Money x100": TunicItemData(IC.filler, 5, 98, "Money"),
|
||||
"Money x128": TunicItemData(IC.useful, 3, 99, "Money"),
|
||||
"Money x200": TunicItemData(IC.useful, 1, 100, "Money"),
|
||||
"Money x255": TunicItemData(IC.useful, 1, 101, "Money"),
|
||||
"Money x128": TunicItemData(IC.useful, 3, 99, "Money", combat_ic=IC.progression),
|
||||
"Money x200": TunicItemData(IC.useful, 1, 100, "Money", combat_ic=IC.progression),
|
||||
"Money x255": TunicItemData(IC.useful, 1, 101, "Money", combat_ic=IC.progression),
|
||||
"Pages 0-1": TunicItemData(IC.useful, 1, 102, "Pages"),
|
||||
"Pages 2-3": TunicItemData(IC.useful, 1, 103, "Pages"),
|
||||
"Pages 4-5": TunicItemData(IC.useful, 1, 104, "Pages"),
|
||||
|
@ -206,6 +208,10 @@ slot_data_item_names = [
|
|||
"Gold Questagon",
|
||||
]
|
||||
|
||||
combat_items: List[str] = [name for name, data in item_table.items()
|
||||
if data.combat_ic and IC.progression in data.combat_ic]
|
||||
combat_items.extend(["Stick", "Sword", "Sword Upgrade", "Magic Wand", "Hero's Laurels"])
|
||||
|
||||
item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()}
|
||||
|
||||
filler_items: List[str] = [name for name, data in item_table.items() if data.classification == IC.filler]
|
||||
|
|
|
@ -78,9 +78,11 @@ easy_ls: List[LadderInfo] = [
|
|||
|
||||
# West Garden
|
||||
# exit after Garden Knight
|
||||
LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_upper"),
|
||||
LadderInfo("West Garden before Boss", "Archipelagos Redux, Overworld Redux_upper"),
|
||||
# West Garden laurels exit
|
||||
LadderInfo("West Garden", "Archipelagos Redux, Overworld Redux_lowest"),
|
||||
LadderInfo("West Garden after Terry", "Archipelagos Redux, Overworld Redux_lowest"),
|
||||
# Magic dagger house, only relevant with combat logic on
|
||||
LadderInfo("West Garden after Terry", "Archipelagos Redux, archipelagos_house_"),
|
||||
|
||||
# Atoll, use the little ladder you fix at the beginning
|
||||
LadderInfo("Ruined Atoll", "Atoll Redux, Overworld Redux_lower"),
|
||||
|
@ -159,7 +161,8 @@ medium_ls: List[LadderInfo] = [
|
|||
LadderInfo("Quarry Back", "Quarry Redux, Monastery_back"),
|
||||
|
||||
LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_2_"),
|
||||
LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Front", dest_is_region=True),
|
||||
LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Entry", dest_is_region=True),
|
||||
LadderInfo("Rooted Ziggurat Lower Back", "Rooted Ziggurat Lower Mid Checkpoint", dest_is_region=True),
|
||||
|
||||
# Swamp to Overworld upper
|
||||
LadderInfo("Swamp Mid", "Swamp Redux 2, Overworld Redux_wall", "Ladders in Swamp"),
|
||||
|
@ -172,9 +175,9 @@ hard_ls: List[LadderInfo] = [
|
|||
LadderInfo("Beneath the Well Front", "Sewer, Overworld Redux_west_aqueduct", "Ladders in Well"),
|
||||
LadderInfo("Beneath the Well Front", "Beneath the Well Back", "Ladders in Well", dest_is_region=True),
|
||||
# go through the hexagon engraving above the vault door
|
||||
LadderInfo("Frog's Domain", "frog cave main, Frog Stairs_Exit", "Ladders to Frog's Domain"),
|
||||
LadderInfo("Frog's Domain Front", "frog cave main, Frog Stairs_Exit", "Ladders to Frog's Domain"),
|
||||
# the turret at the end here is not affected by enemy rando
|
||||
LadderInfo("Frog's Domain", "Frog's Domain Back", "Ladders to Frog's Domain", dest_is_region=True),
|
||||
LadderInfo("Frog's Domain Front", "Frog's Domain Back", "Ladders to Frog's Domain", dest_is_region=True),
|
||||
# todo: see if we can use that new laurels strat here
|
||||
# LadderInfo("Rooted Ziggurat Lower Back", "ziggurat2020_3, ziggurat2020_FTRoom_"),
|
||||
# go behind the cathedral to reach the door, pretty easily doable
|
||||
|
|
|
@ -25,17 +25,17 @@ location_table: Dict[str, TunicLocationData] = {
|
|||
"Beneath the Well - [Side Room] Chest By Phrends": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
|
||||
"Beneath the Well - [Second Room] Page": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
|
||||
"Dark Tomb Checkpoint - [Passage To Dark Tomb] Page Pickup": TunicLocationData("Overworld", "Dark Tomb Checkpoint"),
|
||||
"Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"),
|
||||
"Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral"),
|
||||
"Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral"),
|
||||
"Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral"),
|
||||
"Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral"),
|
||||
"Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral"),
|
||||
"Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"),
|
||||
"Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral"),
|
||||
"Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral"),
|
||||
"Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral Main"),
|
||||
"Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral Entry"), # entry because special rules
|
||||
"Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral Main"),
|
||||
"Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral Main"),
|
||||
"Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral Entry"),
|
||||
"Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral Main"),
|
||||
"Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral Main"),
|
||||
"Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral Main"),
|
||||
"Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral Entry"),
|
||||
"Dark Tomb - Spike Maze Near Exit": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
|
||||
"Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
|
||||
"Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Dark Exit"),
|
||||
"Dark Tomb - 1st Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
|
||||
"Dark Tomb - Spike Maze Upper Walkway": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
|
||||
"Dark Tomb - Skulls Chest": TunicLocationData("Dark Tomb", "Dark Tomb Upper"),
|
||||
|
@ -81,25 +81,25 @@ location_table: Dict[str, TunicLocationData] = {
|
|||
"Eastern Vault Fortress - [East Wing] Bombable Wall": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
|
||||
"Eastern Vault Fortress - [West Wing] Page Pickup": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
|
||||
"Fortress Grave Path - Upper Walkway": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path Upper"),
|
||||
"Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"),
|
||||
"Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"),
|
||||
"Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path by Grave"),
|
||||
"Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path by Grave"),
|
||||
"Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"),
|
||||
"Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||
"Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||
"Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Main"),
|
||||
"Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||
"Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
|
||||
"Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||
"Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||
"Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
|
||||
"Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
|
||||
"Frog's Domain - Escape Chest": TunicLocationData("Frog's Domain", "Frog's Domain Back"),
|
||||
"Frog's Domain - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||
"Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||
"Frog's Domain - Main Room Top Floor": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||
"Frog's Domain - Main Room Bottom Floor": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||
"Frog's Domain - Side Room Secret Passage": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||
"Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||
"Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||
"Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain"),
|
||||
"Frog's Domain - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain Front"),
|
||||
"Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain Front"),
|
||||
"Frog's Domain - Main Room Top Floor": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
|
||||
"Frog's Domain - Main Room Bottom Floor": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
|
||||
"Frog's Domain - Side Room Secret Passage": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
|
||||
"Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
|
||||
"Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain Front"),
|
||||
"Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain Main"),
|
||||
"Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena", location_group="Bosses"),
|
||||
"Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", location_group="Holy Cross"),
|
||||
"Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"),
|
||||
|
@ -131,7 +131,7 @@ location_table: Dict[str, TunicLocationData] = {
|
|||
"Overworld - [Southwest] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld Beach"),
|
||||
"Overworld - [Southwest] Chest Guarded By Turret": TunicLocationData("Overworld", "Overworld"),
|
||||
"Overworld - [Northwest] Shadowy Corner Chest": TunicLocationData("Overworld", "Overworld"),
|
||||
"Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld"),
|
||||
"Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld Tunnel to Beach"),
|
||||
"Overworld - [Southwest] Grapple Chest Over Walkway": TunicLocationData("Overworld", "Overworld"),
|
||||
"Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld after Envoy"),
|
||||
"Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld Swamp Lower Entry"),
|
||||
|
@ -158,7 +158,7 @@ location_table: Dict[str, TunicLocationData] = {
|
|||
"Overworld - [Northwest] Page on Pillar by Dark Tomb": TunicLocationData("Overworld", "Overworld"),
|
||||
"Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Upper Overworld"),
|
||||
"Overworld - [West] Page On Teleporter": TunicLocationData("Overworld", "Overworld"),
|
||||
"Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld"),
|
||||
"Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld Well Entry Area"),
|
||||
"Patrol Cave - Normal Chest": TunicLocationData("Overworld", "Patrol Cave"),
|
||||
"Ruined Shop - Chest 1": TunicLocationData("Overworld", "Ruined Shop"),
|
||||
"Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"),
|
||||
|
@ -233,17 +233,17 @@ location_table: Dict[str, TunicLocationData] = {
|
|||
"Quarry - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "Even Lower Quarry"),
|
||||
"Quarry - [West] Lower Area Below Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"),
|
||||
"Quarry - [West] Lower Area Isolated Chest": TunicLocationData("Lower Quarry", "Lower Quarry"),
|
||||
"Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Even Lower Quarry"),
|
||||
"Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Even Lower Quarry Isolated Chest"),
|
||||
"Quarry - [West] Lower Area After Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"),
|
||||
"Rooted Ziggurat Upper - Near Bridge Switch": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Front"),
|
||||
"Rooted Ziggurat Upper - Beneath Bridge To Administrator": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Back"),
|
||||
"Rooted Ziggurat Tower - Inside Tower": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Middle Top"),
|
||||
"Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
|
||||
"Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
|
||||
"Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
|
||||
"Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
|
||||
"Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Entry"),
|
||||
"Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Entry"),
|
||||
"Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"),
|
||||
"Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"),
|
||||
"Rooted Ziggurat Lower - Guarded By Double Turrets": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
|
||||
"Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
|
||||
"Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Mid Checkpoint"),
|
||||
"Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
|
||||
"Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back", location_group="Bosses"),
|
||||
"Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
|
||||
|
@ -290,26 +290,26 @@ location_table: Dict[str, TunicLocationData] = {
|
|||
"Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"),
|
||||
"West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"),
|
||||
"Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"),
|
||||
"West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
|
||||
"West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
|
||||
"West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", location_group="Holy Cross"),
|
||||
"West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [Central Lowlands] Below Left Walkway": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [North] Obscured Beneath Hero's Memorial": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden before Boss", location_group="Holy Cross"),
|
||||
"West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden after Terry", location_group="Holy Cross"),
|
||||
"West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden at Dagger House"),
|
||||
"West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden South Checkpoint"),
|
||||
"West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden before Terry", location_group="Holy Cross"),
|
||||
"West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden before Boss"),
|
||||
"West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden after Terry"),
|
||||
"West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden before Terry"),
|
||||
"West Garden - [Central Lowlands] Below Left Walkway": TunicLocationData("West Garden", "West Garden after Terry"),
|
||||
"West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden after Terry"),
|
||||
"West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden after Terry"),
|
||||
"West Garden - [North] Obscured Beneath Hero's Memorial": TunicLocationData("West Garden", "West Garden before Terry"),
|
||||
"West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden after Terry"),
|
||||
"West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden South Checkpoint"),
|
||||
"West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden South Checkpoint"),
|
||||
"West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden before Boss"),
|
||||
"West Garden - [Central Highlands] After Garden Knight": TunicLocationData("Overworld", "West Garden after Boss", location_group="Bosses"),
|
||||
"West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden South Checkpoint"),
|
||||
"West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"),
|
||||
"West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden"),
|
||||
"West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden before Terry"),
|
||||
"West Garden House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"),
|
||||
"Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"),
|
||||
}
|
||||
|
|
|
@ -168,6 +168,22 @@ class TunicPlandoConnections(PlandoConnections):
|
|||
duplicate_exits = True
|
||||
|
||||
|
||||
class CombatLogic(Choice):
|
||||
"""
|
||||
If enabled, the player will logically require a combination of stat upgrade items and equipment to get some checks or navigate to some areas, with a goal of matching the vanilla combat difficulty.
|
||||
The player may still be expected to run past enemies, reset aggro (by using a checkpoint or doing a scene transition), or find sneaky paths to checks.
|
||||
This option marks many more items as progression and may force weapons much earlier than normal.
|
||||
Bosses Only makes it so that additional combat logic is only added to the boss fights and the Gauntlet.
|
||||
If disabled, the standard, looser logic is used. The standard logic does not include stat upgrades, just minimal weapon requirements, such as requiring a Sword or Magic Wand for Quarry, or not requiring a weapon for Swamp.
|
||||
"""
|
||||
internal_name = "combat_logic"
|
||||
display_name = "More Combat Logic"
|
||||
option_off = 0
|
||||
option_bosses_only = 1
|
||||
option_on = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class LaurelsZips(Toggle):
|
||||
"""
|
||||
Choose whether to include using the Hero's Laurels to zip through gates, doors, and tricky spots.
|
||||
|
@ -259,6 +275,7 @@ class TunicOptions(PerGameCommonOptions):
|
|||
hexagon_goal: HexagonGoal
|
||||
extra_hexagon_percentage: ExtraHexagonPercentage
|
||||
laurels_location: LaurelsLocation
|
||||
combat_logic: CombatLogic
|
||||
lanternless: Lanternless
|
||||
maskless: Maskless
|
||||
laurels_zips: LaurelsZips
|
||||
|
@ -272,6 +289,7 @@ class TunicOptions(PerGameCommonOptions):
|
|||
|
||||
tunic_option_groups = [
|
||||
OptionGroup("Logic Options", [
|
||||
CombatLogic,
|
||||
Lanternless,
|
||||
Maskless,
|
||||
LaurelsZips,
|
||||
|
|
|
@ -56,9 +56,8 @@ def has_ability(ability: str, state: CollectionState, world: "TunicWorld") -> bo
|
|||
|
||||
|
||||
# a check to see if you can whack things in melee at all
|
||||
def has_stick(state: CollectionState, player: int) -> bool:
|
||||
return (state.has("Stick", player) or state.has("Sword Upgrade", player, 1)
|
||||
or state.has("Sword", player))
|
||||
def has_melee(state: CollectionState, player: int) -> bool:
|
||||
return state.has_any({"Stick", "Sword", "Sword Upgrade"}, player)
|
||||
|
||||
|
||||
def has_sword(state: CollectionState, player: int) -> bool:
|
||||
|
@ -83,7 +82,7 @@ def can_ladder_storage(state: CollectionState, world: "TunicWorld") -> bool:
|
|||
return False
|
||||
if world.options.ladder_storage_without_items:
|
||||
return True
|
||||
return has_stick(state, world.player) or state.has_any((grapple, shield), world.player)
|
||||
return has_melee(state, world.player) or state.has_any((grapple, shield), world.player)
|
||||
|
||||
|
||||
def has_mask(state: CollectionState, world: "TunicWorld") -> bool:
|
||||
|
@ -101,7 +100,7 @@ def set_region_rules(world: "TunicWorld") -> None:
|
|||
world.get_entrance("Overworld -> Overworld Holy Cross").access_rule = \
|
||||
lambda state: has_ability(holy_cross, state, world)
|
||||
world.get_entrance("Overworld -> Beneath the Well").access_rule = \
|
||||
lambda state: has_stick(state, player) or state.has(fire_wand, player)
|
||||
lambda state: has_melee(state, player) or state.has(fire_wand, player)
|
||||
world.get_entrance("Overworld -> Dark Tomb").access_rule = \
|
||||
lambda state: has_lantern(state, world)
|
||||
# laurels in, ladder storage in through the furnace, or ice grapple down the belltower
|
||||
|
@ -117,7 +116,7 @@ def set_region_rules(world: "TunicWorld") -> None:
|
|||
world.get_entrance("Overworld -> Beneath the Vault").access_rule = \
|
||||
lambda state: (has_lantern(state, world) and has_ability(prayer, state, world)
|
||||
# there's some boxes in the way
|
||||
and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand), player)))
|
||||
and (has_melee(state, player) or state.has_any((gun, grapple, fire_wand), player)))
|
||||
world.get_entrance("Ruined Atoll -> Library").access_rule = \
|
||||
lambda state: state.has_any({grapple, laurels}, player) and has_ability(prayer, state, world)
|
||||
world.get_entrance("Overworld -> Quarry").access_rule = \
|
||||
|
@ -237,7 +236,7 @@ def set_location_rules(world: "TunicWorld") -> None:
|
|||
or (has_lantern(state, world) and (has_sword(state, player) or state.has(fire_wand, player)))
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world))
|
||||
set_rule(world.get_location("West Furnace - Lantern Pickup"),
|
||||
lambda state: has_stick(state, player) or state.has_any({fire_wand, laurels}, player))
|
||||
lambda state: has_melee(state, player) or state.has_any({fire_wand, laurels}, player))
|
||||
|
||||
set_rule(world.get_location("Secret Gathering Place - 10 Fairy Reward"),
|
||||
lambda state: state.has(fairies, player, 10))
|
||||
|
@ -301,18 +300,18 @@ def set_location_rules(world: "TunicWorld") -> None:
|
|||
|
||||
# Library Lab
|
||||
set_rule(world.get_location("Library Lab - Page 1"),
|
||||
lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player))
|
||||
lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player))
|
||||
set_rule(world.get_location("Library Lab - Page 2"),
|
||||
lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player))
|
||||
lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player))
|
||||
set_rule(world.get_location("Library Lab - Page 3"),
|
||||
lambda state: has_stick(state, player) or state.has_any((fire_wand, gun), player))
|
||||
lambda state: has_melee(state, player) or state.has_any((fire_wand, gun), player))
|
||||
|
||||
# Eastern Vault Fortress
|
||||
# yes, you can clear the leaves with dagger
|
||||
# gun isn't included since it can only break one leaf pile at a time, and we don't check how much mana you have
|
||||
# but really, I expect the player to just throw a bomb at them if they don't have melee
|
||||
set_rule(world.get_location("Fortress Leaf Piles - Secret Chest"),
|
||||
lambda state: state.has(laurels, player) and (has_stick(state, player) or state.has(ice_dagger, player)))
|
||||
lambda state: state.has(laurels, player) and (has_melee(state, player) or state.has(ice_dagger, player)))
|
||||
set_rule(world.get_location("Fortress Arena - Siege Engine/Vault Key Pickup"),
|
||||
lambda state: has_sword(state, player)
|
||||
and (has_ability(prayer, state, world)
|
||||
|
@ -324,9 +323,9 @@ def set_location_rules(world: "TunicWorld") -> None:
|
|||
|
||||
# Beneath the Vault
|
||||
set_rule(world.get_location("Beneath the Fortress - Bridge"),
|
||||
lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player))
|
||||
lambda state: has_melee(state, player) or state.has_any({laurels, fire_wand}, player))
|
||||
set_rule(world.get_location("Beneath the Fortress - Obscured Behind Waterfall"),
|
||||
lambda state: has_stick(state, player) and has_lantern(state, world))
|
||||
lambda state: has_melee(state, player) and has_lantern(state, world))
|
||||
|
||||
# Quarry
|
||||
set_rule(world.get_location("Quarry - [Central] Above Ladder Dash Chest"),
|
||||
|
|
|
@ -3,6 +3,8 @@ from .. import options
|
|||
|
||||
|
||||
class TestAccess(TunicTestBase):
|
||||
options = {options.CombatLogic.internal_name: options.CombatLogic.option_off}
|
||||
|
||||
# test whether you can get into the temple without laurels
|
||||
def test_temple_access(self) -> None:
|
||||
self.collect_all_but(["Hero's Laurels", "Lantern"])
|
||||
|
@ -61,7 +63,9 @@ class TestNormalGoal(TunicTestBase):
|
|||
class TestER(TunicTestBase):
|
||||
options = {options.EntranceRando.internal_name: options.EntranceRando.option_yes,
|
||||
options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true,
|
||||
options.HexagonQuest.internal_name: options.HexagonQuest.option_false}
|
||||
options.HexagonQuest.internal_name: options.HexagonQuest.option_false,
|
||||
options.CombatLogic.internal_name: options.CombatLogic.option_off,
|
||||
options.FixedShop.internal_name: options.FixedShop.option_true}
|
||||
|
||||
def test_overworld_hc_chest(self) -> None:
|
||||
# test to see that static connections are working properly -- this chest requires holy cross and is in Overworld
|
||||
|
|
Loading…
Reference in New Issue