diff --git a/worlds/tunic/__init__.py b/worlds/tunic/__init__.py index 3ad6be59c1..897b1a028a 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 @@ -347,6 +348,21 @@ class TunicWorld(World): def get_filler_item_name(self) -> str: return self.random.choice(filler_items) + # cache whether you can get through combat logic areas + def collect(self, state: CollectionState, item: Item) -> bool: + change = super().collect(state, item) + if change: + if item.name in combat_items: + state.prog_items[self.player]["need_to_reset_combat_state"] = 1 + return change + + def remove(self, state: CollectionState, item: Item) -> bool: + change = super().remove(state, item) + if change: + if item.name in combat_items: + state.prog_items[self.player]["need_to_reset_combat_state"] = 1 + 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: {}}) diff --git a/worlds/tunic/combat_logic.py b/worlds/tunic/combat_logic.py index 00a0c6681b..543a1c7d3c 100644 --- a/worlds/tunic/combat_logic.py +++ b/worlds/tunic/combat_logic.py @@ -45,7 +45,61 @@ area_data: Dict[str, AreaStats] = { } -def has_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: Optional[AreaStats] = None) -> bool: +# 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] + + +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 + player_state = state.prog_items[player] + # if the combat state is stale, mark each area's combat state as stale + if player_state["need_to_reset_combat_state"]: + player_state["need_to_reset_combat_state"] = 0 + for name in boss_areas: + player_state["combat_state_calced_for_" + name] = 0 + for name in non_boss_areas: + player_state["combat_state_calced_for_" + name] = 0 + player_state["combat_state_calced_for_Gauntlet"] = 0 + + met_combat_reqs = check_combat_reqs(area_name, state, player) + if player_state["combat_state_calced_for_" + area_name]: + 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 area_name in area_list: + 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: + player_state["combat_state_calced_for_" + name] = 1 + player_state["combat_reqs_met_for_" + name] = 1 + 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: + player_state["combat_state_calced_for_" + name] = 1 + player_state["combat_reqs_met_for_" + name] = 0 + + return check_combat_reqs(area_name, state, player) + + +def check_combat_reqs(area_name: str, state: CollectionState, player: int, alt_data: Optional[AreaStats] = None) -> bool: + # if our cache says we've already calced this, we don't need to go through these calculations again + if state.prog_items[player]["combat_state_calced_for_" + area_name]: + return bool(state.prog_items[player]["combat_reqs_met_for_" + area_name]) + data = alt_data or area_data[area_name] extra_att_needed = 0 extra_def_needed = 0 @@ -108,7 +162,7 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int, alt_dat 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 has_combat_reqs("none", state, player, more_modified_stats): + 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 @@ -116,7 +170,7 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int, alt_dat 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 has_combat_reqs("none", state, player, more_modified_stats): + if check_combat_reqs("none", state, player, more_modified_stats): return True elif stick_bool and "Stick" in data.equipment and "Magic" in data.equipment: @@ -125,7 +179,7 @@ def has_combat_reqs(area_name: str, state: CollectionState, player: int, alt_dat 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 has_combat_reqs("none", state, player, more_modified_stats): + if check_combat_reqs("none", state, player, more_modified_stats): return True else: return False @@ -163,12 +217,11 @@ def has_required_stats(data: AreaStats, state: CollectionState, player: int) -> free_def = player_def - def_offerings free_sp = player_sp - sp_offerings paid_stats = data.def_level + data.sp_level - free_def - free_sp - def_to_buy = 0 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 - pass + def_to_buy = 0 elif paid_stats <= def_offerings: # get the amount needed to buy these def offerings def_to_buy = paid_stats diff --git a/worlds/tunic/items.py b/worlds/tunic/items.py index 6e05998c80..5c269595bf 100644 --- a/worlds/tunic/items.py +++ b/worlds/tunic/items.py @@ -208,6 +208,10 @@ slot_data_item_names = [ "Gold Questagon", ] +combat_items: List[str] = [name for name, data in item_table.items() + if data.combat_ic and IC.progression in data.combat_ic] +combat_items.extend(["Stick", "Sword", "Sword Upgrade", "Magic Wand", "Hero's Laurels"]) + item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()} filler_items: List[str] = [name for name, data in item_table.items() if data.classification == IC.filler]