From 0370e669e57e767f8af23cc08b05468da8a72895 Mon Sep 17 00:00:00 2001 From: Mysteryem Date: Sun, 15 Dec 2024 21:28:51 +0000 Subject: [PATCH 1/6] Pokemon Emerald: Add Mr Briney's House indirect conditions (#4154) The `REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH` and `REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN` entrances require access to the `REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN` entrance in their access rules, so require indirect conditions for the parent_region of the entrance: `REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN`. --- worlds/pokemon_emerald/rules.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/worlds/pokemon_emerald/rules.py b/worlds/pokemon_emerald/rules.py index b8d1efb1a98d..828eb20f7218 100644 --- a/worlds/pokemon_emerald/rules.py +++ b/worlds/pokemon_emerald/rules.py @@ -416,13 +416,16 @@ def get_location(location: str): ) # Dewford Town + entrance = get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH") set_rule( - get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH"), + entrance, lambda state: state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player) and state.has("EVENT_TALK_TO_MR_STONE", world.player) and state.has("EVENT_DELIVER_LETTER", world.player) ) + world.multiworld.register_indirect_condition( + get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance) set_rule( get_entrance("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN"), lambda state: @@ -451,14 +454,17 @@ def get_location(location: str): ) # Route 109 + entrance = get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN") set_rule( - get_entrance("REGION_ROUTE109/BEACH -> REGION_DEWFORD_TOWN/MAIN"), + entrance, lambda state: state.can_reach("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN", "Entrance", world.player) and state.can_reach("REGION_DEWFORD_TOWN/MAIN -> REGION_ROUTE109/BEACH", "Entrance", world.player) and state.has("EVENT_TALK_TO_MR_STONE", world.player) and state.has("EVENT_DELIVER_LETTER", world.player) ) + world.multiworld.register_indirect_condition( + get_entrance("REGION_ROUTE104_MR_BRINEYS_HOUSE/MAIN -> REGION_DEWFORD_TOWN/MAIN").parent_region, entrance) set_rule( get_entrance("REGION_ROUTE109/BEACH -> REGION_ROUTE109/SEA"), hm_rules["HM03 Surf"] From 0fdc14bc42a8af64a17454c42d25f5c037e95309 Mon Sep 17 00:00:00 2001 From: Benjamin S Wolf Date: Sun, 15 Dec 2024 13:29:56 -0800 Subject: [PATCH 2/6] Core: Deduplicate exception output (#4036) When running Generate.py, uncaught exceptions are logged once to a file and twice to the console due to keeping the original excepthook. We can avoid this by filtering the file log out of the stream handler. --- Utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Utils.py b/Utils.py index 50adb18f42be..574c006b503d 100644 --- a/Utils.py +++ b/Utils.py @@ -534,7 +534,8 @@ def handle_exception(exc_type, exc_value, exc_traceback): sys.__excepthook__(exc_type, exc_value, exc_traceback) return logging.getLogger(exception_logger).exception("Uncaught exception", - exc_info=(exc_type, exc_value, exc_traceback)) + exc_info=(exc_type, exc_value, exc_traceback), + extra={"NoStream": exception_logger is None}) return orig_hook(exc_type, exc_value, exc_traceback) handle_exception._wrapped = True From 6282efb13c9842a2c01eb6551d23b5d448f2d91c Mon Sep 17 00:00:00 2001 From: Scipio Wright Date: Sun, 15 Dec 2024 16:40:36 -0500 Subject: [PATCH 3/6] TUNIC: Additional Combat Logic Option (#3658) --- worlds/tunic/__init__.py | 45 ++- worlds/tunic/combat_logic.py | 422 +++++++++++++++++++++ worlds/tunic/er_data.py | 282 ++++++++++---- worlds/tunic/er_rules.py | 557 ++++++++++++++++++++++++---- worlds/tunic/er_scripts.py | 31 +- worlds/tunic/items.py | 70 ++-- worlds/tunic/ladder_storage_data.py | 13 +- worlds/tunic/locations.py | 96 ++--- worlds/tunic/options.py | 18 + worlds/tunic/rules.py | 25 +- worlds/tunic/test/test_access.py | 6 +- 11 files changed, 1309 insertions(+), 256 deletions(-) create mode 100644 worlds/tunic/combat_logic.py diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 4c62b18b140f..29dbf150125c 100644 --- a/worlds/tunic/__init__.py +++ b/worlds/tunic/__init__.py @@ -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_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 @@ def generate_early(self) -> None: 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 @@ def stage_generate_early(cls, multiworld: MultiWorld) -> None: 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 @@ def create_regions(self) -> None: 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 @@ def create_regions(self) -> None: 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 @@ def set_rules(self) -> None: 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 @@ def fill_slot_data(self) -> Dict[str, Any]: "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)"], diff --git a/worlds/tunic/combat_logic.py b/worlds/tunic/combat_logic.py new file mode 100644 index 000000000000..9ff363942c9e --- /dev/null +++ b/worlds/tunic/combat_logic.py @@ -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)) diff --git a/worlds/tunic/er_data.py b/worlds/tunic/er_data.py index 1269f3b85e45..9794f4a87b67 100644 --- a/worlds/tunic/er_data.py +++ b/worlds/tunic/er_data.py @@ -235,12 +235,12 @@ def destination_scene(self) -> str: # the vanilla connection destination="Sewer_Boss", tag="_"), Portal(name="Well Exit towards Furnace", region="Beneath the Well Back", destination="Overworld Redux", tag="_west_aqueduct"), - + Portal(name="Well Boss to Well", region="Well Boss", destination="Sewer", tag="_"), Portal(name="Checkpoint to Dark Tomb", region="Dark Tomb Checkpoint", destination="Crypt Redux", tag="_"), - + Portal(name="Dark Tomb to Overworld", region="Dark Tomb Entry Point", destination="Overworld Redux", tag="_"), Portal(name="Dark Tomb to Furnace", region="Dark Tomb Dark Exit", @@ -248,13 +248,13 @@ def destination_scene(self) -> str: # the vanilla connection 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"), @@ -262,7 +262,7 @@ def destination_scene(self) -> str: # the vanilla connection destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="West Garden to Far Shore", region="West Garden Portal", destination="Transit", tag="_teleporter_archipelagos_teleporter"), - + Portal(name="Magic Dagger House Exit", region="Magic Dagger House", destination="Archipelagos Redux", tag="_"), @@ -308,7 +308,7 @@ def destination_scene(self) -> str: # the vanilla connection 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"), @@ -339,7 +339,7 @@ def destination_scene(self) -> str: # the vanilla connection destination="Frog Stairs", tag="_eye"), Portal(name="Frog Stairs Mouth Entrance", region="Ruined Atoll Frog Mouth", destination="Frog Stairs", tag="_mouth"), - + Portal(name="Frog Stairs Eye Exit", region="Frog Stairs Eye Exit", destination="Atoll Redux", tag="_eye"), Portal(name="Frog Stairs Mouth Exit", region="Frog Stairs Upper", @@ -348,39 +348,39 @@ def destination_scene(self) -> str: # the vanilla connection destination="frog cave main", tag="_Entrance"), Portal(name="Frog Stairs to Frog's Domain's Exit", region="Frog Stairs Lower", destination="frog cave main", tag="_Exit"), - + Portal(name="Frog's Domain Ladder Exit", region="Frog's Domain Entry", destination="Frog Stairs", tag="_Entrance"), Portal(name="Frog's Domain Orb Exit", region="Frog's Domain Back", destination="Frog Stairs", tag="_Exit"), - + Portal(name="Library Exterior Tree", region="Library Exterior Tree Region", destination="Atoll Redux", tag="_"), Portal(name="Library Exterior Ladder", region="Library Exterior Ladder Region", destination="Library Hall", tag="_"), - + Portal(name="Library Hall Bookshelf Exit", region="Library Hall Bookshelf", destination="Library Exterior", tag="_"), Portal(name="Library Hero's Grave", region="Library Hero's Grave Region", destination="RelicVoid", tag="_teleporter_relic plinth"), Portal(name="Library Hall to Rotunda", region="Library Hall to Rotunda", destination="Library Rotunda", tag="_"), - + Portal(name="Library Rotunda Lower Exit", region="Library Rotunda to Hall", destination="Library Hall", tag="_"), Portal(name="Library Rotunda Upper Exit", region="Library Rotunda to Lab", destination="Library Lab", tag="_"), - + Portal(name="Library Lab to Rotunda", region="Library Lab Lower", destination="Library Rotunda", tag="_"), Portal(name="Library to Far Shore", region="Library Portal", destination="Transit", tag="_teleporter_library teleporter"), Portal(name="Library Lab to Librarian Arena", region="Library Lab to Librarian", destination="Library Arena", tag="_"), - + Portal(name="Librarian Arena Exit", region="Library Arena", destination="Library Lab", tag="_"), - + Portal(name="Stairs to Top of the Mountain", region="Lower Mountain Stairs", destination="Mountaintop", tag="_"), Portal(name="Mountain to Quarry", region="Lower Mountain", @@ -433,7 +433,7 @@ def destination_scene(self) -> str: # the vanilla connection 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 @@ def destination_scene(self) -> str: # the vanilla connection 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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "Overworld": { "Overworld Beach": [], + "Overworld Tunnel to Beach": + [], "Overworld to Atoll Upper": [["Hyperdash"]], "Overworld Belltower": @@ -769,7 +783,7 @@ class DeadEnd(IntEnum): [], "Overworld Special Shop Entry": [["Hyperdash"], ["LS1"]], - "Overworld Well Ladder": + "Overworld Well Entry Area": [], "Overworld Ruined Passage Door": [], @@ -847,6 +861,12 @@ class DeadEnd(IntEnum): # "Overworld": # [], # }, + "Overworld Tunnel to Beach": { + # "Overworld": + # [], + "Overworld Beach": + [], + }, "Overworld Beach": { # "Overworld": # [], @@ -873,9 +893,15 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "Overworld": [], }, + "Old House Front": { "Old House Back": [], @@ -962,6 +989,7 @@ class DeadEnd(IntEnum): "Old House Front": [["Hyperdash", "Zip"]], }, + "Furnace Fuse": { "Furnace Ladder Area": [["Hyperdash"]], @@ -976,6 +1004,7 @@ class DeadEnd(IntEnum): "Furnace Ladder Area": [["Hyperdash"]], }, + "Sealed Temple": { "Sealed Temple Rafters": [], @@ -984,10 +1013,12 @@ class DeadEnd(IntEnum): "Sealed Temple": [["Hyperdash"]], }, + "Hourglass Cave": { "Hourglass Cave Tower": [], }, + "Forest Belltower Upper": { "Forest Belltower Main": [], @@ -996,6 +1027,7 @@ class DeadEnd(IntEnum): "Forest Belltower Lower": [], }, + "East Forest": { "East Forest Dance Fox Spot": [["Hyperdash"], ["IG1"], ["LS1"]], @@ -1016,6 +1048,7 @@ class DeadEnd(IntEnum): "East Forest": [], }, + "Guard House 1 East": { "Guard House 1 West": [], @@ -1024,6 +1057,7 @@ class DeadEnd(IntEnum): "Guard House 1 East": [["Hyperdash"], ["LS1"]], }, + "Guard House 2 Upper": { "Guard House 2 Lower": [], @@ -1032,6 +1066,7 @@ class DeadEnd(IntEnum): "Guard House 2 Upper": [], }, + "Forest Grave Path Main": { "Forest Grave Path Upper": [["Hyperdash"], ["LS2"], ["IG3"]], @@ -1044,7 +1079,7 @@ class DeadEnd(IntEnum): }, "Forest Grave Path by Grave": { "Forest Hero's Grave": - [], + [], "Forest Grave Path Main": [["IG1"]], }, @@ -1052,6 +1087,7 @@ class DeadEnd(IntEnum): "Forest Grave Path by Grave": [], }, + "Beneath the Well Ladder Exit": { "Beneath the Well Front": [], @@ -1072,6 +1108,7 @@ class DeadEnd(IntEnum): "Beneath the Well Main": [], }, + "Well Boss": { "Dark Tomb Checkpoint": [], @@ -1080,6 +1117,7 @@ class DeadEnd(IntEnum): "Well Boss": [["Hyperdash", "Zip"]], }, + "Dark Tomb Entry Point": { "Dark Tomb Upper": [], @@ -1100,44 +1138,72 @@ class DeadEnd(IntEnum): "Dark Tomb Main": [], }, - "West Garden": { + + "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": - [["Hyperdash"], ["LS1"]], + [["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 Hero's Grave Region": + [], + "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 @@ class DeadEnd(IntEnum): "Ruined Atoll": [], }, + "Frog Stairs Eye Exit": { "Frog Stairs Upper": [], @@ -1196,16 +1263,25 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "Library Exterior by Tree": [], }, + "Library Hall Bookshelf": { "Library Hall": [], @@ -1240,6 +1317,7 @@ class DeadEnd(IntEnum): "Library Hall": [], }, + "Library Rotunda to Hall": { "Library Rotunda": [], @@ -1281,9 +1359,10 @@ class DeadEnd(IntEnum): "Library Lab": [], }, + "Fortress Exterior from East Forest": { "Fortress Exterior from Overworld": - [], + [], "Fortress Courtyard Upper": [["LS2"]], "Fortress Courtyard": @@ -1291,9 +1370,9 @@ class DeadEnd(IntEnum): }, "Fortress Exterior from Overworld": { "Fortress Exterior from East Forest": - [["Hyperdash"]], + [["Hyperdash"]], "Fortress Exterior near cave": - [], + [], "Fortress Courtyard": [["Hyperdash"], ["IG1"], ["LS1"]], }, @@ -1321,6 +1400,7 @@ class DeadEnd(IntEnum): "Fortress Courtyard": [], }, + "Beneath the Vault Ladder Exit": { "Beneath the Vault Main": [], @@ -1337,6 +1417,7 @@ class DeadEnd(IntEnum): "Beneath the Vault Ladder Exit": [], }, + "Fortress East Shortcut Lower": { "Fortress East Shortcut Upper": [["IG1"]], @@ -1345,6 +1426,7 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "Fortress Arena": [], }, + "Lower Mountain": { "Lower Mountain Stairs": [], @@ -1387,6 +1490,7 @@ class DeadEnd(IntEnum): "Lower Mountain": [], }, + "Monastery Back": { "Monastery Front": [["Hyperdash", "Zip"]], @@ -1401,6 +1505,7 @@ class DeadEnd(IntEnum): "Monastery Back": [], }, + "Quarry Entry": { "Quarry Portal": [["Quarry Connector"]], @@ -1436,15 +1541,17 @@ class DeadEnd(IntEnum): [], "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 @@ class DeadEnd(IntEnum): "Quarry Back": [], }, + "Rooted Ziggurat Upper Entry": { "Rooted Ziggurat Upper Front": [], @@ -1465,17 +1573,38 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "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 @@ class DeadEnd(IntEnum): "Cathedral Gauntlet": [["Hyperdash"]], }, + "Far Shore": { "Far Shore to Spawn Region": [["Hyperdash"]], @@ -1587,7 +1731,7 @@ class DeadEnd(IntEnum): "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"]], }, diff --git a/worlds/tunic/er_rules.py b/worlds/tunic/er_rules.py index 786af0d617a8..163523108345 100644 --- a/worlds/tunic/er_rules.py +++ b/worlds/tunic/er_rules.py @@ -1,10 +1,11 @@ from typing import Dict, FrozenSet, Tuple, TYPE_CHECKING -from worlds.generic.Rules import set_rule, forbid_item -from .options import IceGrappling, LadderStorage -from .rules import (has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage, +from worlds.generic.Rules import set_rule, add_rule, forbid_item +from .options import IceGrappling, LadderStorage, CombatLogic +from .rules import (has_ability, has_sword, has_melee, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage, laurels_zip, bomb_walls) from .er_data import Portal, get_portal_outlet_region from .ladder_storage_data import ow_ladder_groups, region_ladders, easy_ls, medium_ls, hard_ls +from .combat_logic import has_combat_reqs from BaseClasses import Region, CollectionState if TYPE_CHECKING: @@ -43,6 +44,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ player = world.player options = world.options + # input scene destination tag, returns portal's name and paired portal's outlet region or region + def get_portal_info(portal_sd: str) -> Tuple[str, str]: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene_destination() == portal_sd: + return portal1.name, get_portal_outlet_region(portal2, world) + if portal2.scene_destination() == portal_sd: + return portal2.name, get_portal_outlet_region(portal1, world) + raise Exception("No matches found in get_portal_info") + + # input scene destination tag, returns paired portal's name and region + def get_paired_portal(portal_sd: str) -> Tuple[str, str]: + for portal1, portal2 in portal_pairs.items(): + if portal1.scene_destination() == portal_sd: + return portal2.name, portal2.region + if portal2.scene_destination() == portal_sd: + return portal1.name, portal1.region + raise Exception("no matches found in get_paired_portal") + regions["Menu"].connect( connecting_region=regions["Overworld"]) @@ -56,10 +75,18 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Overworld Beach"], rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or state.has_any({laurels, grapple}, player)) + # regions["Overworld Beach"].connect( + # connecting_region=regions["Overworld"], + # rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) + # or state.has_any({laurels, grapple}, player)) + + # region for combat logic, no need to connect it to beach since it would be the same as the ow -> beach cxn + ow_tunnel_beach = regions["Overworld"].connect( + connecting_region=regions["Overworld Tunnel to Beach"]) + regions["Overworld Beach"].connect( - connecting_region=regions["Overworld"], - rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) - or state.has_any({laurels, grapple}, player)) + connecting_region=regions["Overworld Tunnel to Beach"], + rule=lambda state: state.has(laurels, player) or has_ladder("Ladders in Overworld Town", state, world)) regions["Overworld Beach"].connect( connecting_region=regions["Overworld West Garden Laurels Entry"], @@ -277,11 +304,17 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["East Overworld"], rule=lambda state: state.has(laurels, player)) - regions["Overworld"].connect( + # region made for combat logic + ow_to_well_entry = regions["Overworld"].connect( + connecting_region=regions["Overworld Well Entry Area"]) + regions["Overworld Well Entry Area"].connect( + connecting_region=regions["Overworld"]) + + regions["Overworld Well Entry Area"].connect( connecting_region=regions["Overworld Well Ladder"], rule=lambda state: has_ladder("Ladders in Well", state, world)) regions["Overworld Well Ladder"].connect( - connecting_region=regions["Overworld"], + connecting_region=regions["Overworld Well Entry Area"], rule=lambda state: has_ladder("Ladders in Well", state, world)) # nmg: can ice grapple through the door @@ -306,7 +339,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Overworld Fountain Cross Door"].connect( connecting_region=regions["Overworld"]) - regions["Overworld"].connect( + ow_to_town_portal = regions["Overworld"].connect( connecting_region=regions["Overworld Town Portal"], rule=lambda state: has_ability(prayer, state, world)) regions["Overworld Town Portal"].connect( @@ -337,6 +370,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ rule=lambda state: has_ladder("Ladders in Overworld Town", state, world) or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) + # don't need the ice grapple rule since you can go from ow -> beach -> tunnel regions["Overworld"].connect( connecting_region=regions["Overworld Tunnel Turret"], rule=lambda state: state.has(laurels, player)) @@ -473,29 +507,28 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Beneath the Well Ladder Exit"], rule=lambda state: has_ladder("Ladders in Well", state, world)) - regions["Beneath the Well Front"].connect( + btw_front_main = regions["Beneath the Well Front"].connect( connecting_region=regions["Beneath the Well Main"], - rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + rule=lambda state: has_melee(state, player) or state.has(fire_wand, player)) regions["Beneath the Well Main"].connect( - connecting_region=regions["Beneath the Well Front"], - rule=lambda state: has_stick(state, player) or state.has(fire_wand, player)) + connecting_region=regions["Beneath the Well Front"]) regions["Beneath the Well Main"].connect( connecting_region=regions["Beneath the Well Back"], rule=lambda state: has_ladder("Ladders in Well", state, world)) - regions["Beneath the Well Back"].connect( + btw_back_main = regions["Beneath the Well Back"].connect( connecting_region=regions["Beneath the Well Main"], rule=lambda state: has_ladder("Ladders in Well", state, world) - and (has_stick(state, player) or state.has(fire_wand, player))) + and (has_melee(state, player) or state.has(fire_wand, player))) - regions["Well Boss"].connect( + well_boss_to_dt = regions["Well Boss"].connect( connecting_region=regions["Dark Tomb Checkpoint"]) # can laurels through the gate, no setup needed regions["Dark Tomb Checkpoint"].connect( connecting_region=regions["Well Boss"], rule=lambda state: laurels_zip(state, world)) - regions["Dark Tomb Entry Point"].connect( + dt_entry_to_upper = regions["Dark Tomb Entry Point"].connect( connecting_region=regions["Dark Tomb Upper"], rule=lambda state: has_lantern(state, world)) regions["Dark Tomb Upper"].connect( @@ -512,34 +545,57 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Dark Tomb Main"].connect( connecting_region=regions["Dark Tomb Dark Exit"]) - regions["Dark Tomb Dark Exit"].connect( + dt_exit_to_main = regions["Dark Tomb Dark Exit"].connect( connecting_region=regions["Dark Tomb Main"], rule=lambda state: has_lantern(state, world)) # West Garden + # combat logic regions + wg_before_to_after_terry = regions["West Garden before Terry"].connect( + connecting_region=regions["West Garden after Terry"]) + wg_after_to_before_terry = regions["West Garden after Terry"].connect( + connecting_region=regions["West Garden before Terry"]) + + regions["West Garden after Terry"].connect( + connecting_region=regions["West Garden South Checkpoint"]) + wg_checkpoint_to_after_terry = regions["West Garden South Checkpoint"].connect( + connecting_region=regions["West Garden after Terry"]) + + wg_checkpoint_to_dagger = regions["West Garden South Checkpoint"].connect( + connecting_region=regions["West Garden at Dagger House"]) + regions["West Garden at Dagger House"].connect( + connecting_region=regions["West Garden South Checkpoint"]) + + wg_checkpoint_to_before_boss = regions["West Garden South Checkpoint"].connect( + connecting_region=regions["West Garden before Boss"]) + regions["West Garden before Boss"].connect( + connecting_region=regions["West Garden South Checkpoint"]) + regions["West Garden Laurels Exit Region"].connect( - connecting_region=regions["West Garden"], + connecting_region=regions["West Garden at Dagger House"], rule=lambda state: state.has(laurels, player)) - regions["West Garden"].connect( + regions["West Garden at Dagger House"].connect( connecting_region=regions["West Garden Laurels Exit Region"], rule=lambda state: state.has(laurels, player)) - # you can grapple Garden Knight to aggro it, then ledge it - regions["West Garden after Boss"].connect( - connecting_region=regions["West Garden"], + # laurels past, or ice grapple it off, or ice grapple to it then fight + after_gk_to_wg = regions["West Garden after Boss"].connect( + connecting_region=regions["West Garden before Boss"], rule=lambda state: state.has(laurels, player) - or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) + or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world) + or (has_ice_grapple_logic(False, IceGrappling.option_easy, state, world) + and has_sword(state, player))) # ice grapple push Garden Knight off the side - regions["West Garden"].connect( + wg_to_after_gk = regions["West Garden before Boss"].connect( connecting_region=regions["West Garden after Boss"], rule=lambda state: state.has(laurels, player) or has_sword(state, player) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) - regions["West Garden"].connect( + regions["West Garden before Terry"].connect( connecting_region=regions["West Garden Hero's Grave Region"], rule=lambda state: has_ability(prayer, state, world)) regions["West Garden Hero's Grave Region"].connect( - connecting_region=regions["West Garden"]) + connecting_region=regions["West Garden before Terry"]) regions["West Garden Portal"].connect( connecting_region=regions["West Garden by Portal"]) @@ -556,9 +612,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ # can ice grapple to and from the item behind the magic dagger house regions["West Garden Portal Item"].connect( - connecting_region=regions["West Garden"], + connecting_region=regions["West Garden at Dagger House"], rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - regions["West Garden"].connect( + regions["West Garden at Dagger House"].connect( connecting_region=regions["West Garden Portal Item"], rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_medium, state, world)) @@ -596,7 +652,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Ruined Atoll Portal"].connect( connecting_region=regions["Ruined Atoll"]) - regions["Ruined Atoll"].connect( + atoll_statue = regions["Ruined Atoll"].connect( connecting_region=regions["Ruined Atoll Statue"], rule=lambda state: has_ability(prayer, state, world) and (has_ladder("Ladders in South Atoll", state, world) @@ -629,10 +685,13 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) regions["Frog's Domain Entry"].connect( - connecting_region=regions["Frog's Domain"], + connecting_region=regions["Frog's Domain Front"], rule=lambda state: has_ladder("Ladders to Frog's Domain", state, world)) - regions["Frog's Domain"].connect( + frogs_front_to_main = regions["Frog's Domain Front"].connect( + connecting_region=regions["Frog's Domain Main"]) + + regions["Frog's Domain Main"].connect( connecting_region=regions["Frog's Domain Back"], rule=lambda state: state.has(grapple, player)) @@ -752,7 +811,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ rule=lambda state: state.has(laurels, player) or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - regions["Fortress Courtyard Upper"].connect( + fort_upper_lower = regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Courtyard"]) # nmg: can ice grapple to the upper ledge regions["Fortress Courtyard"].connect( @@ -762,12 +821,12 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Fortress Courtyard Upper"].connect( connecting_region=regions["Fortress Exterior from Overworld"]) - regions["Beneath the Vault Ladder Exit"].connect( + btv_front_to_main = regions["Beneath the Vault Ladder Exit"].connect( connecting_region=regions["Beneath the Vault Main"], rule=lambda state: has_ladder("Ladder to Beneath the Vault", state, world) and has_lantern(state, world) # there's some boxes in the way - and (has_stick(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player))) + and (has_melee(state, player) or state.has_any((gun, grapple, fire_wand, laurels), player))) # on the reverse trip, you can lure an enemy over to break the boxes if needed regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Ladder Exit"], @@ -775,11 +834,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Beneath the Vault Main"].connect( connecting_region=regions["Beneath the Vault Back"]) - regions["Beneath the Vault Back"].connect( + btv_back_to_main = regions["Beneath the Vault Back"].connect( connecting_region=regions["Beneath the Vault Main"], rule=lambda state: has_lantern(state, world)) - regions["Fortress East Shortcut Upper"].connect( + fort_east_upper_lower = regions["Fortress East Shortcut Upper"].connect( connecting_region=regions["Fortress East Shortcut Lower"]) regions["Fortress East Shortcut Lower"].connect( connecting_region=regions["Fortress East Shortcut Upper"], @@ -794,21 +853,31 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Eastern Vault Fortress"], rule=lambda state: has_ice_grapple_logic(False, IceGrappling.option_easy, state, world)) - regions["Fortress Grave Path"].connect( - connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], - rule=lambda state: state.has(laurels, player)) - regions["Fortress Grave Path Dusty Entrance Region"].connect( - connecting_region=regions["Fortress Grave Path"], - rule=lambda state: state.has(laurels, player)) + fort_grave_entry_to_combat = regions["Fortress Grave Path Entry"].connect( + connecting_region=regions["Fortress Grave Path Combat"]) + regions["Fortress Grave Path Combat"].connect( + connecting_region=regions["Fortress Grave Path Entry"]) - regions["Fortress Grave Path"].connect( + regions["Fortress Grave Path Combat"].connect( + connecting_region=regions["Fortress Grave Path by Grave"]) + + # run past the enemies + regions["Fortress Grave Path by Grave"].connect( + connecting_region=regions["Fortress Grave Path Entry"]) + + regions["Fortress Grave Path by Grave"].connect( connecting_region=regions["Fortress Hero's Grave Region"], rule=lambda state: has_ability(prayer, state, world)) regions["Fortress Hero's Grave Region"].connect( - connecting_region=regions["Fortress Grave Path"]) + connecting_region=regions["Fortress Grave Path by Grave"]) + + regions["Fortress Grave Path by Grave"].connect( + connecting_region=regions["Fortress Grave Path Dusty Entrance Region"], + rule=lambda state: state.has(laurels, player)) + # reverse connection is conditionally made later, depending on whether combat logic is on, and the details of ER regions["Fortress Grave Path Upper"].connect( - connecting_region=regions["Fortress Grave Path"], + connecting_region=regions["Fortress Grave Path Entry"], rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) regions["Fortress Arena"].connect( @@ -831,19 +900,19 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Quarry Portal"].connect( connecting_region=regions["Quarry Entry"]) - regions["Quarry Entry"].connect( + quarry_entry_to_main = regions["Quarry Entry"].connect( connecting_region=regions["Quarry"], rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) regions["Quarry"].connect( connecting_region=regions["Quarry Entry"]) - regions["Quarry Back"].connect( + quarry_back_to_main = regions["Quarry Back"].connect( connecting_region=regions["Quarry"], rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) regions["Quarry"].connect( connecting_region=regions["Quarry Back"]) - regions["Quarry Monastery Entry"].connect( + monastery_to_quarry_main = regions["Quarry Monastery Entry"].connect( connecting_region=regions["Quarry"], rule=lambda state: state.has(fire_wand, player) or has_sword(state, player)) regions["Quarry"].connect( @@ -869,18 +938,24 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ rule=lambda state: has_ladder("Ladders in Lower Quarry", state, world) or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) - # nmg: bring a scav over, then ice grapple through the door, only with ER on to avoid soft lock regions["Even Lower Quarry"].connect( + connecting_region=regions["Even Lower Quarry Isolated Chest"]) + # you grappled down, might as well loot the rest too + lower_quarry_empty_to_combat = regions["Even Lower Quarry Isolated Chest"].connect( + connecting_region=regions["Even Lower Quarry"], + rule=lambda state: has_mask(state, world)) + + regions["Even Lower Quarry Isolated Chest"].connect( connecting_region=regions["Lower Quarry Zig Door"], rule=lambda state: state.has("Activate Quarry Fuse", player) or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - # nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask only with ER on + # don't need the mask for this either, please don't complain about not needing a mask here, you know what you did regions["Quarry"].connect( - connecting_region=regions["Lower Quarry Zig Door"], + connecting_region=regions["Even Lower Quarry Isolated Chest"], rule=lambda state: has_ice_grapple_logic(True, IceGrappling.option_hard, state, world)) - regions["Monastery Front"].connect( + monastery_front_to_back = regions["Monastery Front"].connect( connecting_region=regions["Monastery Back"]) # laurels through the gate, no setup needed regions["Monastery Back"].connect( @@ -897,7 +972,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Rooted Ziggurat Upper Entry"].connect( connecting_region=regions["Rooted Ziggurat Upper Front"]) - regions["Rooted Ziggurat Upper Front"].connect( + zig_upper_front_back = regions["Rooted Ziggurat Upper Front"].connect( connecting_region=regions["Rooted Ziggurat Upper Back"], rule=lambda state: state.has(laurels, player) or has_sword(state, player)) regions["Rooted Ziggurat Upper Back"].connect( @@ -907,13 +982,23 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Rooted Ziggurat Middle Top"].connect( connecting_region=regions["Rooted Ziggurat Middle Bottom"]) + zig_low_entry_to_front = regions["Rooted Ziggurat Lower Entry"].connect( + connecting_region=regions["Rooted Ziggurat Lower Front"]) + regions["Rooted Ziggurat Lower Front"].connect( + connecting_region=regions["Rooted Ziggurat Lower Entry"]) + regions["Rooted Ziggurat Lower Front"].connect( + connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"]) + zig_low_mid_to_front = regions["Rooted Ziggurat Lower Mid Checkpoint"].connect( + connecting_region=regions["Rooted Ziggurat Lower Front"]) + + zig_low_mid_to_back = regions["Rooted Ziggurat Lower Mid Checkpoint"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"], rule=lambda state: state.has(laurels, player) or (has_sword(state, player) and has_ability(prayer, state, world))) - # nmg: can ice grapple on the voidlings to the double admin fight, still need to pray at the fuse - regions["Rooted Ziggurat Lower Back"].connect( - connecting_region=regions["Rooted Ziggurat Lower Front"], + # can ice grapple to the voidlings to get to the double admin fight, still need to pray at the fuse + zig_low_back_to_mid = regions["Rooted Ziggurat Lower Back"].connect( + connecting_region=regions["Rooted Ziggurat Lower Mid Checkpoint"], rule=lambda state: (state.has(laurels, player) or has_ice_grapple_logic(True, IceGrappling.option_easy, state, world)) and has_ability(prayer, state, world) @@ -925,8 +1010,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Rooted Ziggurat Portal Room Entrance"].connect( connecting_region=regions["Rooted Ziggurat Lower Back"]) - regions["Zig Skip Exit"].connect( - connecting_region=regions["Rooted Ziggurat Lower Front"]) + # zig skip region only gets made if entrance rando and fewer shops are on + if options.entrance_rando and options.fixed_shop: + regions["Zig Skip Exit"].connect( + connecting_region=regions["Rooted Ziggurat Lower Front"]) regions["Rooted Ziggurat Portal"].connect( connecting_region=regions["Rooted Ziggurat Portal Room"]) @@ -952,7 +1039,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ or state.has(laurels, player) or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world)) - # a whole lot of stuff to basically say "you need to pray at the overworld fuse" swamp_mid_to_cath = regions["Swamp Mid"].connect( connecting_region=regions["Swamp to Cathedral Main Entrance Region"], rule=lambda state: (has_ability(prayer, state, world) @@ -965,7 +1051,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ "Ladder to Swamp", "Ladders near Weathervane"}, player) or (state.has("Ladder to Ruined Atoll", player) - and state.can_reach_region("Overworld Beach", player)))))) + and state.can_reach_region("Overworld Beach", player))))) + and (not options.combat_logic + or has_combat_reqs("Swamp", state, player))) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) if options.ladder_storage >= LadderStorage.option_hard and options.shuffle_ladders: @@ -1017,13 +1105,23 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ regions["Swamp Hero's Grave Region"].connect( connecting_region=regions["Back of Swamp"]) - regions["Cathedral"].connect( + cath_entry_to_elev = regions["Cathedral Entry"].connect( connecting_region=regions["Cathedral to Gauntlet"], rule=lambda state: (has_ability(prayer, state, world) or has_ice_grapple_logic(False, IceGrappling.option_medium, state, world)) or options.entrance_rando) # elevator is always there in ER regions["Cathedral to Gauntlet"].connect( - connecting_region=regions["Cathedral"]) + connecting_region=regions["Cathedral Entry"]) + + cath_entry_to_main = regions["Cathedral Entry"].connect( + connecting_region=regions["Cathedral Main"]) + regions["Cathedral Main"].connect( + connecting_region=regions["Cathedral Entry"]) + + cath_elev_to_main = regions["Cathedral to Gauntlet"].connect( + connecting_region=regions["Cathedral Main"]) + regions["Cathedral Main"].connect( + connecting_region=regions["Cathedral to Gauntlet"]) regions["Cathedral Gauntlet Checkpoint"].connect( connecting_region=regions["Cathedral Gauntlet"]) @@ -1075,7 +1173,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_ connecting_region=regions["Far Shore"]) # Misc - regions["Spirit Arena"].connect( + heir_fight = regions["Spirit Arena"].connect( connecting_region=regions["Spirit Arena Victory"], rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if world.options.hexagon_quest else @@ -1219,6 +1317,192 @@ def ls_connect(origin_name: str, portal_sdt: str) -> None: 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)) diff --git a/worlds/tunic/er_scripts.py b/worlds/tunic/er_scripts.py index 05f6177aa57d..aa5833b4db36 100644 --- a/worlds/tunic/er_scripts.py +++ b/worlds/tunic/er_scripts.py @@ -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,16 +42,21 @@ 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(): region = regions[location_table[location_name].er_region] 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 @@ def create_er_regions(world: "TunicWorld") -> Dict[Portal, Portal]: "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 diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index b6ce5d8995a8..f30c1d5d248a 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -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 @@ class TunicItemData(NamedTuple): "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 @@ class TunicItemData(NamedTuple): "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 @@ class TunicItemData(NamedTuple): "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 @@ class TunicItemData(NamedTuple): "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 @@ class TunicItemData(NamedTuple): "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] diff --git a/worlds/tunic/ladder_storage_data.py b/worlds/tunic/ladder_storage_data.py index c6dda42bca79..f2d4b94406ac 100644 --- a/worlds/tunic/ladder_storage_data.py +++ b/worlds/tunic/ladder_storage_data.py @@ -78,9 +78,11 @@ class LadderInfo(NamedTuple): # 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 @@ class LadderInfo(NamedTuple): 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 @@ class LadderInfo(NamedTuple): 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 diff --git a/worlds/tunic/locations.py b/worlds/tunic/locations.py index 442e0c01446d..5ea309fb19d7 100644 --- a/worlds/tunic/locations.py +++ b/worlds/tunic/locations.py @@ -25,17 +25,17 @@ class TunicLocationData(NamedTuple): "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 @@ class TunicLocationData(NamedTuple): "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 @@ class TunicLocationData(NamedTuple): "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 @@ class TunicLocationData(NamedTuple): "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 @@ class TunicLocationData(NamedTuple): "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 @@ class TunicLocationData(NamedTuple): "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"), } diff --git a/worlds/tunic/options.py b/worlds/tunic/options.py index f1d53362f4c9..24247a6cfdcf 100644 --- a/worlds/tunic/options.py +++ b/worlds/tunic/options.py @@ -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, diff --git a/worlds/tunic/rules.py b/worlds/tunic/rules.py index 58c987acbcee..30b7cee9d07b 100644 --- a/worlds/tunic/rules.py +++ b/worlds/tunic/rules.py @@ -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"), diff --git a/worlds/tunic/test/test_access.py b/worlds/tunic/test/test_access.py index bbceb7468ff3..24551a13d547 100644 --- a/worlds/tunic/test/test_access.py +++ b/worlds/tunic/test/test_access.py @@ -3,6 +3,8 @@ 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 @@ def test_normal_goal(self) -> None: 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 From d1823a21ea891c8d949cc0a5371059b265ff0cb4 Mon Sep 17 00:00:00 2001 From: qwint Date: Sun, 15 Dec 2024 16:48:44 -0500 Subject: [PATCH 4/6] HK: add random handling to plandocharmcosts (#4327) --- worlds/hk/Options.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/worlds/hk/Options.py b/worlds/hk/Options.py index 02f04ab18eef..0dc38e744e50 100644 --- a/worlds/hk/Options.py +++ b/worlds/hk/Options.py @@ -294,6 +294,10 @@ def get_costs(self, random_source: Random) -> typing.List[int]: return charms +class CharmCost(Range): + range_end = 6 + + class PlandoCharmCosts(OptionDict): """Allows setting a Charm's Notch costs directly, mapping {name: cost}. This is set after any random Charm Notch costs, if applicable.""" @@ -303,6 +307,27 @@ class PlandoCharmCosts(OptionDict): Optional(name): And(int, lambda n: 6 >= n >= 0, error="Charm costs must be integers in the range 0-6.") for name in charm_names }) + def __init__(self, value): + # To handle keys of random like other options, create an option instance from their values + # Additionally a vanilla keyword is added to plando individual charms to vanilla costs + # and default is disabled so as to not cause confusion + self.value = {} + for key, data in value.items(): + if isinstance(data, str): + if data.lower() == "vanilla" and key in self.valid_keys: + self.value[key] = vanilla_costs[charm_names.index(key)] + continue + elif data.lower() == "default": + # default is too easily confused with vanilla but actually 0 + # skip CharmCost resolution to fail schema afterwords + self.value[key] = data + continue + try: + self.value[key] = CharmCost.from_any(data).value + except ValueError as ex: + # will fail schema afterwords + self.value[key] = data + def get_costs(self, charm_costs: typing.List[int]) -> typing.List[int]: for name, cost in self.value.items(): charm_costs[charm_names.index(name)] = cost From 728d2492020ee3f75d421a7263308c6feb64e56a Mon Sep 17 00:00:00 2001 From: NewSoupVi <57900059+NewSoupVi@users.noreply.github.com> Date: Sun, 15 Dec 2024 23:30:35 +0100 Subject: [PATCH 5/6] Core: Add some more world convenience methods (#3021) * Add some more convenience methods * Typing stuff * Rename the method * beauxq's suggestions * Back to Push Precollected --- worlds/AutoWorld.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/worlds/AutoWorld.py b/worlds/AutoWorld.py index ded8701d3b61..a51071792079 100644 --- a/worlds/AutoWorld.py +++ b/worlds/AutoWorld.py @@ -7,7 +7,7 @@ import time from random import Random from dataclasses import make_dataclass -from typing import (Any, Callable, ClassVar, Dict, FrozenSet, List, Mapping, Optional, Set, TextIO, Tuple, +from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Mapping, Optional, Set, TextIO, Tuple, TYPE_CHECKING, Type, Union) from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions @@ -534,12 +534,24 @@ def create_filler(self) -> "Item": def get_location(self, location_name: str) -> "Location": return self.multiworld.get_location(location_name, self.player) + def get_locations(self) -> "Iterable[Location]": + return self.multiworld.get_locations(self.player) + def get_entrance(self, entrance_name: str) -> "Entrance": return self.multiworld.get_entrance(entrance_name, self.player) + def get_entrances(self) -> "Iterable[Entrance]": + return self.multiworld.get_entrances(self.player) + def get_region(self, region_name: str) -> "Region": return self.multiworld.get_region(region_name, self.player) + def get_regions(self) -> "Iterable[Region]": + return self.multiworld.get_regions(self.player) + + def push_precollected(self, item: Item) -> None: + self.multiworld.push_precollected(item) + @property def player_name(self) -> str: return self.multiworld.get_player_name(self.player) From cacab68b779a28f8401c5a3a34d26d609054bd75 Mon Sep 17 00:00:00 2001 From: Bryce Wilson Date: Mon, 16 Dec 2024 00:06:48 -0800 Subject: [PATCH 6/6] Pokemon Emerald: Remove unnecessary code (#4364) --- worlds/pokemon_emerald/data.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/worlds/pokemon_emerald/data.py b/worlds/pokemon_emerald/data.py index 34bebae2d66a..cd1becf44b22 100644 --- a/worlds/pokemon_emerald/data.py +++ b/worlds/pokemon_emerald/data.py @@ -1459,9 +1459,6 @@ def _init() -> None: for warp, destination in extracted_data["warps"].items(): data.warp_map[warp] = None if destination == "" else destination - if encoded_warp not in data.warp_map: - data.warp_map[encoded_warp] = None - # Create trainer data for i, trainer_json in enumerate(extracted_data["trainers"]): party_json = trainer_json["party"]