Source code for scripts.engine.core.matter

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