423 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			423 lines
		
	
	
		
			20 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
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))
 |