from __future__ import annotations
import logging
import random
from typing import List, Optional, Tuple, Type, TypeVar
import numpy as np
import snecs
import tcod
from snecs import Component, new_entity, Query
from snecs.typedefs import EntityID
from scripts.engine.core import hourglass, utility
from scripts.engine.core.component import (
    Aesthetic,
    Afflictions,
    CombatStats,
    Exists,
    FOV,
    Identity,
    Immunities,
    IsActive,
    IsPlayer,
    Knowledge,
    Lifespan,
    LightSource,
    Opinion,
    Physicality,
    Position,
    Reaction,
    Resources,
    Sight,
    Thought,
    Tracked,
    Traits,
)
from scripts.engine.core.effect import (
    AffectCooldownEffect,
    AffectStatEffect,
    AlterTerrainEffect,
    ApplyAfflictionEffect,
    DamageEffect,
    Effect,
    MoveOtherEffect,
    MoveSelfEffect,
)
from scripts.engine.core.utility import build_sprites_from_paths
from scripts.engine.internal import library
from scripts.engine.internal.action import Affliction, Skill, SkillModifier
from scripts.engine.internal.constant import (
    DirectionType,
    EffectType,
    HitType,
    HitTypeType,
    INFINITE,
    PrimaryStat,
    RenderLayer,
    ResourceType,
    SecondaryStat,
    ShapeType,
    TILE_SIZE,
    TraitGroup,
)
from scripts.engine.internal.data import store
from scripts.engine.internal.definition import (
    ActorData,
    AffectCooldownEffectData,
    AffectStatEffectData,
    AlterTerrainEffectData,
    ApplyAfflictionEffectData,
    DamageEffectData,
    DelayedSkillData,
    EffectData,
    GodData,
    MoveEffectData,
    ProjectileData,
    TerrainData,
)
from scripts.engine.internal.event import event_hub, LoseConditionMetEvent, MessageEvent
from scripts.engine.world_objects import lighting
from scripts.engine.world_objects.tile import Tile
__all__ = ["create_entity"]
############################# LOCAL DEFINITIONS #############################
_C = TypeVar("_C", bound=Component)  # to represent components where we don't know which is being used
get_entitys_components = snecs.all_components
get_components = Query
entity_has_component = snecs.has_component
############################# CREATE - RETURN OBJECT #############################
[docs]def create_entity(components: List[Component] = None) -> EntityID:
    """
    Use each component in a list of components to create an entity
    """
    if components is None:
        _components = []
    else:
        _components = components
    # add Exists, as all entities need it
    _components.append(Exists())
    # create the entity
    entity = new_entity(_components)
    return entity 
[docs]def create_god(god_data: GodData) -> EntityID:
    """
    Create an entity with all of the components to be a god. god_name must be in the gods json file.
    """
    god: List[Component] = []
    god.append(Identity(god_data.name, god_data.description))
    god.append(Opinion(god_data.attitudes))
    god.append(Reaction(god_data.reactions))
    god.append((Resources(INFINITE, INFINITE)))
    entity = create_entity(god)
    logging.debug(f"God Entity, '{god_data.name}', created.")
    return entity 
[docs]def create_actor(actor_data: ActorData, spawn_pos: Tuple[int, int], is_player: bool = False) -> EntityID:
    """
    Create an entity with all of the components to be an actor. Returns entity ID.
    """
    components: List[Component] = []
    x, y = spawn_pos
    # get dimensions of actor
    occupied_tiles = []
    for offset in actor_data.position_offsets:
        occupied_tiles.append((offset[0] + x, offset[1] + y))
    #  choose a name
    name = random.choice(actor_data.possible_names)
    # actor components
    if is_player:
        components.append(IsPlayer())
    components.append(Position(*occupied_tiles))
    components.append(Identity(name, actor_data.description))
    components.append(Physicality(True, actor_data.height))
    components.append(Traits(actor_data.trait_names))
    components.append(FOV())
    components.append(Tracked(hourglass.get_time()))
    components.append(Immunities())
    # combat stats
    base_stats = {}
    base_values = library.GAME_CONFIG.base_values
    for primary_stat in [
        PrimaryStat.VIGOUR,
        PrimaryStat.CLOUT,
        PrimaryStat.SKULLDUGGERY,
        PrimaryStat.BUSTLE,
        PrimaryStat.EXACTITUDE,
    ]:
        # apply primary base value
        value = getattr(base_values, primary_stat)
        # loop traits and get values for stats
        for name in actor_data.trait_names:
            data = library.TRAITS[name]
            value += getattr(data, primary_stat)
        base_stats[primary_stat] = value
    stats = CombatStats(**base_stats)  # type: ignore
    # apply base values to secondary
    for secondary_stat in [
        SecondaryStat.MAX_HEALTH,
        SecondaryStat.MAX_STAMINA,
        SecondaryStat.ACCURACY,
        SecondaryStat.RESIST_BURN,
        SecondaryStat.RESIST_CHEMICAL,
        SecondaryStat.RESIST_ASTRAL,
        SecondaryStat.RESIST_COLD,
        SecondaryStat.RESIST_MUNDANE,
        SecondaryStat.RUSH,
    ]:
        stats.amend_base_value(secondary_stat, getattr(library.GAME_CONFIG.base_values, secondary_stat))
    components.append(stats)  # type: ignore
    # sight range
    sight_range = 0
    for name in actor_data.trait_names:
        data = library.TRAITS[name]
        if data.sight_range > sight_range:
            sight_range = data.sight_range
    components.append(Sight(sight_range))
    # set up light
    radius = 2
    colour = (255, 255, 255)
    alpha = 200
    light_id = create_light(spawn_pos, radius, colour, alpha)
    components.append(LightSource(light_id, radius))
    # get info from traits
    traits_paths = []  # for aesthetic
    move = store.skill_registry["Move"]
    basic_attack = store.skill_registry["BasicAttack"]
    known_skills = [move, basic_attack]  # for knowledge
    skill_order = ["BasicAttack"]  # for knowledge
    perm_afflictions_names = []  # for affliction
    behaviour = None
    # loop each trait and get info for skills etc.
    for name in actor_data.trait_names:
        data = library.TRAITS[name]
        traits_paths.append(data.sprite_paths)
        if data.known_skills != ["none"]:
            for skill_name in data.known_skills:
                skill_class = store.skill_registry[skill_name]
                known_skills.append(skill_class)
                skill_order.append(skill_name)
        if data.permanent_afflictions != ["none"]:
            for _name in data.permanent_afflictions:
                perm_afflictions_names.append(_name)
        if data.group == TraitGroup.NPC:
            behaviour = store.behaviour_registry[actor_data.behaviour_name]
    # add aesthetic
    traits_paths.sort(key=lambda path: path.render_order, reverse=True)
    sprites = build_sprites_from_paths(traits_paths)
    # N.B. translation to screen coordinates is handled by the camera
    components.append(Aesthetic(sprites, traits_paths, RenderLayer.ACTOR, (x, y)))
    # add skills to entity
    components.append(Knowledge(known_skills, skill_order))
    # create the entity
    entity = create_entity(components)
    # add permanent afflictions post creation,  since we can only add them once an entity is created
    perm_afflictions = []
    for name in perm_afflictions_names:
        # create the affliction with no specific source
        perm_afflictions.append(create_affliction(name, entity, entity, INFINITE))
    add_component(entity, Afflictions(perm_afflictions))
    # add behaviour  N.B. Can only be added once entity is created
    if behaviour:
        add_component(entity, Thought(behaviour(entity)))
    # give full resources N.B. Can only be added once entity is created
    add_component(entity, Resources(stats.max_health, stats.max_stamina))
    logging.debug(f"Actor Entity, '{name}', created.")
    return entity 
[docs]def create_terrain(terrain_data: TerrainData, spawn_pos: Tuple[int, int], lifespan: int = INFINITE) -> EntityID:
    """
    Create terrain
    """
    components: List[Component] = []
    x, y = spawn_pos
    # get dimensions of actor
    occupied_tiles = []
    for offset in terrain_data.position_offsets:
        occupied_tiles.append((offset[0] + x, offset[1] + y))
    # terrain components
    components.append(Lifespan(lifespan))
    components.append(Position(*occupied_tiles))
    components.append(Identity(terrain_data.name, terrain_data.description))
    components.append(Physicality(terrain_data.blocks_movement, terrain_data.height))
    # add aesthetic N.B. translation to screen coordinates is handled by the camera
    sprites = build_sprites_from_paths([terrain_data.sprite_paths], (TILE_SIZE, TILE_SIZE))
    components.append(Aesthetic(sprites, [terrain_data.sprite_paths], RenderLayer.TERRAIN, (x, y)))
    # add reactions
    components.append(Reaction(terrain_data.reactions))
    # add light
    if terrain_data.light:
        _light = terrain_data.light
        light_id = create_light(spawn_pos, _light.radius, _light.colour, _light.alpha)
        components.append(LightSource(light_id, _light.radius))
    # create the entity
    entity = create_entity(components)
    logging.debug(f"Terrain Entity, '{terrain_data.name}', created.")
    return entity 
[docs]def create_projectile(creating_entity: EntityID, tile_pos: Tuple[int, int], data: ProjectileData) -> EntityID:
    """
    Create an entity with all of the components to be a projectile. Returns entity ID.
    """
    skill_name = data.skill_name
    projectile: List[Component] = []
    x, y = tile_pos
    name = get_name(creating_entity)
    projectile_name = f"{name}`s {skill_name}`s projectile"
    desc = f"{skill_name} on its way."
    projectile.append(Identity(projectile_name, desc))
    sprites = build_sprites_from_paths([data.sprite_paths], (TILE_SIZE, TILE_SIZE))
    projectile.append(Aesthetic(sprites, [data.sprite_paths], RenderLayer.ACTOR, (x, y)))
    projectile.append(Tracked(hourglass.get_time_of_last_turn()))  # allocate time to ensure they act next
    projectile.append(Position((x, y)))
    projectile.append(Resources(999, 999))
    projectile.append(Afflictions())
    projectile.append(IsActive())
    # set up light
    radius = 1
    colour = (68, 174, 235)
    alpha = 240
    light_id = create_light((x, y), radius, colour, alpha)
    projectile.append(LightSource(light_id, radius))
    entity = create_entity(projectile)
    # add post-creation components
    behaviour = store.behaviour_registry["Projectile"]
    thought = Thought(behaviour(entity))
    from scripts.engine.internal.action import Projectile
    assert isinstance(thought.behaviour, Projectile)
    thought.behaviour.data = data  # projectile is a  special case and requires the data set
    add_component(entity, thought)
    move = store.skill_registry["Move"]
    known_skills = [move]
    add_component(entity, Knowledge(known_skills))
    logging.debug(f"{projectile_name}`s created at ({x},{y}) heading {data.direction}.")
    return entity 
[docs]def create_delayed_skill(creating_entity: EntityID, tile_pos: Tuple[int, int], data: DelayedSkillData) -> EntityID:
    """
    Create an entity with all of the components to be a delayed skill. Returns Entity ID.
    """
    skill_name = data.skill_name
    delayed_skill: List[Component] = []
    x, y = tile_pos
    name = get_name(creating_entity)
    delayed_skill_name = f"{name}`s {skill_name}`s delayed skill"
    desc = f"{skill_name} incoming."
    delayed_skill.append(Identity(delayed_skill_name, desc))
    sprites = build_sprites_from_paths([data.sprite_paths], (TILE_SIZE, TILE_SIZE))
    delayed_skill.append(Aesthetic(sprites, [data.sprite_paths], RenderLayer.TERRAIN, (x, y)))
    delayed_skill.append(Tracked(hourglass.get_time_of_last_turn() - 1))  # allocate time to ensure they act next
    delayed_skill.append(Position((x, y)))
    delayed_skill.append(IsActive())
    entity = create_entity(delayed_skill)
    # add post-creation components
    behaviour = store.behaviour_registry["DelayedSkill"]
    thought = Thought(behaviour(entity))
    from scripts.engine.internal.action import DelayedSkill
    assert isinstance(thought.behaviour, DelayedSkill)
    thought.behaviour.data = data
    add_component(entity, thought)
    logging.debug(f"{delayed_skill_name}`s created at ({x},{y}) and will trigger in {data.duration} rounds.")
    return entity 
[docs]def create_affliction(name: str, creator: EntityID, target: EntityID, duration: int) -> Affliction:
    """
    Creates an instance of an Affliction provided the name
    """
    affliction = store.affliction_registry[name](creator, target, duration)
    return affliction 
[docs]def create_light(pos: Tuple[int, int], radius: int, colour: Tuple[int, int, int], alpha: int) -> str:
    """
    Create a Light and add it to the current GameMap's Lightbox. Pos is the world position - the light will handle
    scaling to screen.
    Returns the light_id of the Light.
    """
    light_img = utility.get_image("world/light_mask.png", ((radius * 2) * TILE_SIZE, (radius * 2) * TILE_SIZE))
    from scripts.engine.core.world import get_game_map
    light_box = get_game_map().light_box
    light = lighting.Light([pos[0] * TILE_SIZE, pos[1] * TILE_SIZE], radius * TILE_SIZE, light_img)
    light.set_alpha(alpha)
    light.set_colour(colour)
    light_id = light_box.add_light(light)
    return light_id 
[docs]def create_pathfinder() -> tcod.path.Pathfinder:
    """
    Create an empty pathfinder using the current game map
    """
    from scripts.engine.core.world import get_game_map
    game_map = get_game_map()
    # combine entity blocking and tile blocking maps
    from scripts.engine.core.world import get_entity_blocking_movement_map
    cost_map = game_map.block_movement_map | get_entity_blocking_movement_map()
    # create graph to represent the map and a pathfinder to navigate
    graph = tcod.path.SimpleGraph(cost=np.asarray(cost_map, dtype=np.int8), cardinal=2, diagonal=0)
    pathfinder = tcod.path.Pathfinder(graph)
    return pathfinder 
[docs]def create_effect(origin: EntityID, target: EntityID, data: EffectData) -> Effect:
    """
    Create an effect from effect data.
    """
    effect_type = data.effect_type
    if effect_type == EffectType.APPLY_AFFLICTION:
        assert isinstance(data, ApplyAfflictionEffectData)
        return _create_apply_affliction_effect(origin, target, data)
    elif effect_type == EffectType.DAMAGE:
        assert isinstance(data, DamageEffectData)
        return _create_damage_effect(origin, target, data)
    elif effect_type == EffectType.MOVE:
        assert isinstance(data, MoveEffectData)
        return _create_move_self_effect(origin, target, data)
    elif effect_type == EffectType.AFFECT_STAT:
        assert isinstance(data, AffectStatEffectData)
        return _create_affect_stat_effect(origin, target, data)
    elif effect_type == EffectType.AFFECT_COOLDOWN:
        assert isinstance(data, AffectCooldownEffectData)
        return _create_affect_cooldown_effect(origin, target, data)
    elif effect_type == EffectType.ALTER_TERRAIN:
        assert isinstance(data, AlterTerrainEffectData)
        return _create_alter_terrain_effect(origin, target, data)
    else:
        raise KeyError(f"Create effect: Effect provided ({effect_type}) was not handled.") 
def _create_apply_affliction_effect(
    origin: EntityID, target: EntityID, data: ApplyAfflictionEffectData
) -> ApplyAfflictionEffect:
    effect = ApplyAfflictionEffect(
        origin=origin,
        target=target,
        success_effects=data.success_effects,
        failure_effects=data.failure_effects,
        affliction_name=data.affliction_name,
        duration=data.duration,
    )
    return effect
def _create_damage_effect(origin: EntityID, target: EntityID, data: DamageEffectData) -> DamageEffect:
    effect = DamageEffect(
        origin=origin,
        target=target,
        success_effects=data.success_effects,
        failure_effects=data.failure_effects,
        stat_to_target=data.stat_to_target,
        accuracy=data.accuracy,
        damage=data.damage,
        damage_type=data.damage_type,
        mod_stat=data.mod_stat,
        mod_amount=data.mod_amount,
        potency=data.potency,
    )
    return effect
def _create_move_self_effect(origin: EntityID, target: EntityID, data: MoveEffectData) -> MoveSelfEffect:
    effect = MoveSelfEffect(
        origin=origin,
        target=target,
        direction=data.direction,
        success_effects=data.success_effects,
        failure_effects=data.failure_effects,
        move_amount=data.move_amount,
    )
    return effect
def _create_move_other_effect(origin: EntityID, target: EntityID, data: MoveEffectData) -> MoveOtherEffect:
    effect = MoveOtherEffect(
        origin=origin,
        target=target,
        direction=data.direction,
        success_effects=data.success_effects,
        failure_effects=data.failure_effects,
        move_amount=data.move_amount,
    )
    return effect
def _create_affect_stat_effect(origin: EntityID, target: EntityID, data: AffectStatEffectData) -> AffectStatEffect:
    effect = AffectStatEffect(
        origin=origin,
        target=target,
        success_effects=data.success_effects,
        failure_effects=data.failure_effects,
        cause_name=data.cause_name,
        stat_to_target=data.stat_to_target,
        affect_amount=data.affect_amount,
    )
    return effect
def _create_affect_cooldown_effect(
    origin: EntityID, target: EntityID, data: AffectCooldownEffectData
) -> AffectCooldownEffect:
    effect = AffectCooldownEffect(
        origin=origin,
        target=target,
        success_effects=data.success_effects,
        failure_effects=data.failure_effects,
        skill_name=data.skill_name,
        affect_amount=data.affect_amount,
    )
    return effect
def _create_alter_terrain_effect(
    origin: EntityID, target: EntityID, data: AlterTerrainEffectData
) -> AlterTerrainEffect:
    effect = AlterTerrainEffect(
        origin=origin,
        target=target,
        success_effects=data.success_effects,
        failure_effects=data.failure_effects,
        terrain_name=data.terrain_name,
        affect_amount=data.affect_amount,
    )
    return effect
[docs]def create_compatible_blessings(amount: int) -> List[SkillModifier]:
    player = get_player()
    knowledge = get_entitys_component(player, Knowledge)
    # get all blessing names
    blessing_options = list(store.blessing_registry)
    blessings: List[SkillModifier] = []
    # look for compatible blessings until enough are found
    while len(blessings) < amount:
        # pick a random blessing from the remaining blessings
        random_choice = random.choice(blessing_options)
        # generate the blessing object from the randomly selected blessing
        possible_option = store.blessing_registry[random_choice](player)
        # go through the player's skills
        for skill in knowledge.skills.values():
            # check if the blessing is compatible
            if knowledge.can_add_blessing(skill, possible_option):
                # we know that at least one skill is compatible with the blessing.
                # the full list will be calculated later, so we can break.
                blessings.append(possible_option)
                break
        # remove from the list of options so we don't get stuck in a loop
        blessing_options.remove(random_choice)
        # in case there aren't enough compatible blessings
        if len(blessing_options) == 0:
            break
    return blessings 
############################# GET - RETURN AN EXISTING SOMETHING  #############################
[docs]def get_player() -> EntityID:
    """
    Get the player.
    """
    for entity, (_,) in get_components([IsPlayer]):
        return entity
    raise ValueError("Player not found.") 
[docs]def get_entitys_component(entity: EntityID, component: Type[_C]) -> _C:
    """
    Get an entity's component. Will raise exception if entity does not have the component.  Use entity_has_component
    to check.
    """
    if entity_has_component(entity, component):
        return snecs.entity_component(entity, component)
    elif entity_has_component(entity, Identity):
        name = get_name(entity)
        raise AttributeError(
            f"get_entitys_component:'{name}'({entity}) tried to get {component.__name__}, " f"but it was not found."
        )
    else:
        raise AttributeError(
            f"get_entitys_component:'unknown'({entity}) tried to get {component.__name__}, " f"but it was not found."
        ) 
[docs]def get_name(entity: EntityID) -> str:
    """
    Get an entity's Identity component's name.
    """
    identity = get_entitys_component(entity, Identity)
    if identity:
        name = identity.name
    else:
        name = "not found"
    return name 
[docs]def get_known_skill(entity: EntityID, skill_name: str) -> Type[Skill]:
    """
    Get an entity's known skill from their Knowledge component.
    """
    knowledge = get_entitys_component(entity, Knowledge)
    try:
        return knowledge.skills[skill_name]
    except KeyError:
        raise KeyError(
            f"get_known_skill: '{get_name(entity)}' tried to use a skill, '{skill_name}', they dont  " f"know."
        ) 
[docs]def get_hit_type(to_hit_score: int) -> HitTypeType:
    """
    Get the hit type from the to hit score
    """
    hit_types_data = library.GAME_CONFIG.hit_types
    if to_hit_score >= hit_types_data.crit.value:
        return HitType.CRIT
    elif to_hit_score >= hit_types_data.hit.value:
        return HitType.HIT
    else:
        return HitType.GRAZE 
[docs]def get_affected_entities(
    target_pos: Tuple[int, int], shape: ShapeType, shape_size: int, shape_direction: Optional[Tuple[int, int]] = None
):
    """
    Return a list of entities that are within the shape given, using target position as a centre point. Entity must
    have Position, Resources to be eligible.
    """
    affected_entities = []
    affected_positions = []
    target_x = target_pos[0]
    target_y = target_pos[1]
    # get affected tiles
    coords = utility.get_coords_from_shape(shape, shape_size, shape_direction)
    for coord in coords:
        affected_positions.append((coord[0] + target_x, coord[1] + target_y))
    # get relevant entities in target area
    for entity, (position, *others) in get_components([Position, Resources]):
        assert isinstance(position, Position)  # for mypy typing
        for affected_pos in affected_positions:
            if affected_pos in position:
                affected_entities.append(entity)
    return affected_entities 
[docs]def get_entities_on_tile(tile: Tile) -> List[EntityID]:
    """
    Return a list of all the entities in that tile
    """
    x = tile.x
    y = tile.y
    entities = []
    from scripts.engine.core import query
    for entity, (position,) in query.position:
        assert isinstance(position, Position)
        if (x, y) in position:
            entities.append(entity)
    return entities 
############################# QUERIES #############################
[docs]def entity_has_immunity(entity: EntityID, name: str) -> bool:
    """
    Check if an entity has immunity to the named Action.
    """
    try:
        immunity = get_entitys_component(entity, Immunities)
        if name in immunity.active:
            return True
        else:
            return False
    except AttributeError:
        logging.info(f"entity_has_immunity: `{get_name(entity)}`, ({entity}), does not have Immunities Component.")
        return False 
[docs]def can_use_skill(entity: EntityID, skill_name: str) -> bool:
    """
    Check if entity can use skill. Checks cooldown and resource affordability.
    """
    player = get_player()
    skill = get_known_skill(entity, skill_name)
    # if we dont have skill we cant do anything
    if not skill:
        return False
    # flags
    not_on_cooldown = False
    can_afford = _can_afford_cost(entity, skill.resource_type, skill.resource_cost)
    knowledge = get_entitys_component(entity, Knowledge)
    if knowledge:
        cooldown: int = knowledge.cooldowns[skill_name]
        if cooldown <= 0:
            not_on_cooldown = True
    else:
        not_on_cooldown = False
        cooldown = INFINITE
    if can_afford and not_on_cooldown:
        return True
    # log/inform lack of affordability
    if not can_afford:
        # is it the player that can't afford it?
        if entity == player:
            event_hub.post(MessageEvent("I cannot afford to do that."))
        else:
            logging.warning(
                f"can_use_skill: '{get_name(entity)}' tried to use {skill_name}, which they can`t" f"afford."
            )
    # log/inform on cooldown
    if not not_on_cooldown:
        # is it the player that's can't afford it?
        if entity == player:
            event_hub.post(MessageEvent("I'm not ready to do that, yet."))
        else:
            if cooldown == INFINITE:
                cooldown_msg = "unknown"
            else:
                cooldown_msg = str(cooldown)
            logging.warning(
                f"can_use_skill: '{get_name(entity)}' tried to use {skill_name}, but needs to wait "
                f" {cooldown_msg} more rounds."
            )
    # we've reached the end; no good.
    return False 
def _can_afford_cost(entity: EntityID, resource: ResourceType, cost: int) -> bool:
    """
    Check if entity can afford the resource cost
    """
    resources = get_entitys_component(entity, Resources)
    # Check if cost can be paid
    value = getattr(resources, resource.lower())
    if value - cost >= 0:
        return True
    else:
        return False
############################# AMEND STATE #############################
[docs]def add_immunity(entity: EntityID, immunity_name: str, duration: int):
    """
    Add an immunity to an Entity's Immunities Component. If entity has no Immunities Component one will be added.
    """
    try:
        immunity = get_entitys_component(entity, Immunities)
        immunity.active[immunity_name] = duration
    except AttributeError:
        add_component(entity, Immunities({immunity_name: duration}))
    logging.debug(f"'{get_name(entity)}' is now immune to {immunity_name} for {duration} rounds.") 
[docs]def pay_resource_cost(entity: EntityID, resource: ResourceType, cost: int) -> bool:
    """
    Remove the resource cost from the using entity
    """
    resources = get_entitys_component(entity, Resources)
    name = get_name(entity)
    try:
        resource_value = getattr(resources, resource.lower())
        if resource_value != INFINITE:
            resource_left = resource_value - cost
            setattr(resources, resource.lower(), resource_left)
            logging.info(f"'{name}' paid {cost} {resource} and has {resource_left} left.")
            return True
        else:
            logging.info(f"'{name}' paid nothing as they have infinite {resource}.")
    except AttributeError:
        logging.warning(
            f"pay_resource_cost: '{name}' tried to pay {cost} {resource} but Resources component not " f"found."
        )
    return False 
[docs]def use_skill(user: EntityID, skill: Type[Skill], target_tile: Tile, direction: DirectionType) -> bool:
    """
    Use the specified skill on the target tile, usually creating a projectile. Returns True is successful if
    criteria to use skill was met, False if not.
    """
    # we init so that any overrides of the Skill.init are applied
    skill_cast = skill(user, target_tile, direction)
    # ensure they are the right target type
    from scripts.engine.core.world import tile_has_tags
    if tile_has_tags(user, skill_cast.target_tile, skill_cast.cast_tags):
        result = skill_cast.use()
        return result
    else:
        logging.info(
            f"Could not use skill, ({target_tile.x},{target_tile.y}) does not have required tags "
            f"({skill_cast.cast_tags})."
        )
    return False 
[docs]def apply_skill(skill: Skill) -> bool:
    """
    Resolve the skill's effects. Returns True is successful if criteria to apply skill was met, False if not.
    """
    success = None
    # ensure they are the right target type
    from scripts.engine.core.world import tile_has_tags
    if tile_has_tags(skill.user, skill.target_tile, skill.target_tags):
        for entity, effects in skill.apply():
            if entity not in skill.ignore_entities:
                effect_queue = list(effects)
                while effect_queue:
                    effect = effect_queue.pop()
                    result, new_effects = effect.evaluate()
                    effect_queue.extend(new_effects)
                    # only take result of initial effect
                    if not success:
                        success = result
        if success:
            logging.debug(
                f"'{get_name(skill.user)}' successfully applied {skill.name} to ({skill.target_tile.x},"
                f"{skill.target_tile.y})."
            )
            return True
        else:
            logging.debug(
                f"'{get_name(skill.user)}' unsuccessfully applied {skill.name} to ({skill.target_tile.x},"
                f"{skill.target_tile.y})."
            )
            return False
    else:
        logging.info(
            f"Could not apply skill '{skill.__class__.__name__}', target tile does not have required "
            f"tags ({skill.target_tags})."
        )
    return False 
[docs]def set_skill_on_cooldown(entity: EntityID, skill_name: str, cooldown_duration: int):
    """
    Sets an entity's skill on cooldown.
    """
    knowledge = get_entitys_component(entity, Knowledge)
    knowledge.set_skill_cooldown(skill_name, cooldown_duration) 
[docs]def apply_affliction(affliction: Affliction) -> bool:
    """
    Apply the affliction's effects. Returns True is successful if criteria to trigger the affliction was met,
    False if not.
    """
    target = affliction.affected_entity
    position = get_entitys_component(target, Position)
    from scripts.engine.core.world import get_tile
    target_tile = get_tile((position.x, position.y))
    # ensure they are the right target type
    from scripts.engine.core.world import tile_has_tags
    if tile_has_tags(affliction.origin, target_tile, affliction.target_tags):
        for entity, effects in affliction.apply():
            effect_queue = list(effects)
            while effect_queue:
                effect = effect_queue.pop()
                effect_queue.extend(effect.evaluate()[1])
        return True
    else:
        logging.info(
            f'Could not apply affliction "{affliction.name}", target tile does not have required '
            f"tags ({affliction.target_tags})."
        )
    return False 
[docs]def trigger_affliction(affliction: Affliction):
    """
    Trigger the affliction on the affected entity.
    """
    for entity, effects in affliction.trigger():
        effect_queue = list(effects)
        while effect_queue:
            effect = effect_queue.pop()
            effect_queue.extend(effect.evaluate()[1]) 
[docs]def take_turn(entity: EntityID) -> bool:
    """
    Process the entity's Thought component. If no component found then EndTurn event is fired.
    """
    logging.debug(f"'{get_name(entity)}' is beginning their turn.")
    try:
        behaviour = get_entitys_component(entity, Thought)
        behaviour.behaviour.act()
        return True
    except AttributeError:
        logging.critical(f"'{get_name(entity)}' has no behaviour to use.")
    return False 
[docs]def apply_damage(entity: EntityID, damage: int) -> bool:
    """
    Remove damage from entity's health. Return True is damage was applied.
    """
    if damage <= 0:
        logging.info(f"Damage was {damage} and therefore nothing was done.")
        return False
    try:
        resource = get_entitys_component(entity, Resources)
        resource.health -= damage
        logging.info(f"'{get_name(entity)}' takes {damage} and has {resource.health} health remaining.")
        return True
    except AttributeError:
        logging.warning(f"apply_damage: '{get_name(entity)}' has no resource so couldn`t apply damage.")
    return False 
[docs]def spend_time(entity: EntityID, time_spent: int) -> bool:
    """
    Add time_spent to the entity's total time spent.
    """
    try:
        tracked = get_entitys_component(entity, Tracked)
        tracked.time_spent += time_spent
        return True
    except AttributeError:
        logging.warning(f"spend_time: '{get_name(entity)}' has no tracked to spend time.")
    return False 
[docs]def choose_target(entity: EntityID) -> Optional[EntityID]:
    """
    Choose an appropriate target for the entity.
    N.B. CURRENTLY JUST RETURNS PLAYER
    """
    return get_player() 
[docs]def kill_entity(entity: EntityID):
    """
    Add entity to the deletion stack and removes them from the turn queue.
    """
    # if not player
    if entity != get_player():
        # delete from world
        delete_entity(entity)
        turn_queue = hourglass.get_turn_queue()
        # if turn holder create new queue without them
        if entity == hourglass.get_turn_holder():
            # ensure the game state reflects the new queue
            hourglass.next_turn(entity)
        elif entity in turn_queue:
            # remove from turn queue
            turn_queue.pop(entity)
    else:
        event_hub.post(LoseConditionMetEvent()) 
[docs]def delete_entity(entity: EntityID):
    """
    Queues entity for removal from the world_objects. Happens at the next run of process.
    """
    if entity:
        if snecs.exists(entity, snecs.world.default_world):
            snecs.schedule_for_deletion(entity)
            name = get_name(entity)
            logging.info(f"'{name}' ({entity}) added to stack to be deleted on next frame.")
        else:
            logging.warning(f"Tried to delete entity {entity} but they don't exist!")
    else:
        logging.error("Tried to delete an entity but entity was None.") 
[docs]def add_component(entity: EntityID, component: Component):
    """
    Add a component to the entity
    """
    snecs.add_component(entity, component) 
[docs]def remove_component(entity: EntityID, component: Type[Component]):
    """
    Remove a component from the entity
    """
    snecs.remove_component(entity, component) 
[docs]def remove_affliction(entity: EntityID, affliction: Affliction):
    """
    Remove affliction from active list and undo any stat modification.
    """
    afflictions = get_entitys_component(entity, Afflictions)
    if afflictions:
        afflictions.remove(affliction)
        # remove stat modification
        if EffectType.AFFECT_STAT in affliction.identity_tags and entity_has_component(entity, CombatStats):
            stats = get_entitys_component(entity, CombatStats)
            stats.remove_mod(affliction.name) 
[docs]def learn_skill(entity: EntityID, skill_name: str):
    """
    Add the skill name to the entity's knowledge component.
    """
    if not entity_has_component(entity, Knowledge):
        logging.warning(f"{get_name(entity)} has no Knowledge component and cannot learn {skill_name}.")
        return
    knowledge = get_entitys_component(entity, Knowledge)
    skill_class = store.skill_registry[skill_name]
    knowledge.add(skill_class) 
############################# CALCULATIONS #############################
[docs]def calculate_damage(base_damage: int, damage_mod_amount: int, resist_value: int, hit_type: HitTypeType) -> int:
    """
    Work out the damage to be dealt.
    """
    logging.debug(f"Calculate damage...")
    # mitigate damage with defence
    mitigated_damage = (base_damage + damage_mod_amount) - resist_value
    # get modifiers
    hit_types_data = library.GAME_CONFIG.hit_types
    # apply to hit modifier to damage
    if hit_type == HitType.CRIT:
        hit_mod = hit_types_data.crit.modifier
    elif hit_type == HitType.HIT:
        hit_mod = hit_types_data.hit.modifier
    else:
        hit_mod = hit_types_data.graze.modifier
    modified_damage = mitigated_damage * hit_mod
    # round down the dmg
    int_modified_damage = int(modified_damage)
    # never less than 1
    if int_modified_damage <= 0:
        logging.debug(f"-> Damage ended at {int_modified_damage} so replaced with minimum damage value of 1.")
        int_modified_damage = 1
    # log the info
    logging.debug(
        f"-> Base Damage:{base_damage}, +Damage Mod:{damage_mod_amount} ({base_damage + damage_mod_amount}), "
        f"-Resistance:{resist_value} ({format(mitigated_damage, '.2f')}), "
        f"*Hit Mod:{hit_mod} ({format(modified_damage, '.2f')}), "
        f"Result (rounded): {int_modified_damage}"
    )
    return int_modified_damage 
[docs]def calculate_to_hit_score(attacker_accuracy: int, skill_accuracy: int, stat_to_target_value: int) -> int:
    """
    Get the to hit score based on the stats of both entities and a random roll.
    """
    logging.debug(f"Get to hit scores...")
    # roll for the random value to add to the to_hit score.
    roll = random.randint(-3, 3)
    modified_to_hit_score = attacker_accuracy + skill_accuracy + roll
    mitigated_to_hit_score = modified_to_hit_score - stat_to_target_value
    # log the info
    log_string = f"-> Roll:{roll}, Modified:{modified_to_hit_score}, Mitigated:{mitigated_to_hit_score}."
    logging.debug(log_string)
    return mitigated_to_hit_score