Source code for scripts.engine.core.effect

from __future__ import annotations

import logging
from abc import ABC, abstractmethod
from typing import Tuple, TYPE_CHECKING

from snecs.typedefs import EntityID

import scripts.engine.core.matter
from scripts.engine.core import query, world
from scripts.engine.core.component import Afflictions, CombatStats, Identity, Knowledge, Lifespan, Position, Resources
from scripts.engine.internal import library
from scripts.engine.internal.constant import DamageType, DamageTypeType, Direction, DirectionType, PrimaryStatType, PrimaryStat, TileTag, HitType, HitTypeType
from scripts.engine.internal.event import (
    AffectCooldownEvent,
    AffectStatEvent,
    AfflictionEvent,
    AlterTerrainEvent,
    DamageEvent,
    event_hub,
    MoveEvent,
)

if TYPE_CHECKING:
    from typing import List, Dict, Optional

__all__ = [
    "Effect",
    "DamageEffect",
    "MoveSelfEffect",
    "MoveOtherEffect",
    "AffectStatEffect",
    "ApplyAfflictionEffect",
    "AffectCooldownEffect",
    "AlterTerrainEffect",
]


[docs]class Effect(ABC): """ A collection of parameters and instructions to apply a change to an entity's or tile's state. """
[docs] def __init__( self, origin: EntityID, target: EntityID, success_effects: List[Effect], failure_effects: List[Effect], potency: float = 1.0, ): self.origin = origin self.target = target self.success_effects: List[Effect] = success_effects self.failure_effects: List[Effect] = failure_effects self.potency: float = potency
[docs] @abstractmethod def evaluate(self): """ Evaluate the effect, triggering more if needed. Must be overridden by subclass """ pass
[docs]class AftershockEffect(Effect):
[docs] def __init__( self, origin: EntityID, target: EntityID, success_effects: List[Effect], failure_effects: List[Effect], ): super().__init__(origin, target, success_effects, failure_effects)
[docs] def evaluate(self) -> Tuple[bool, List[Effect]]: center_position = scripts.engine.core.matter.get_entitys_component(self.target, Position) affected_tiles = [(-1, -1), (0, -1), (1, -1), (-1, 0), (1, 0), (-1, 1), (0, 1), (1, 1)] entities_hit = [] for tile in affected_tiles: target_tile_pos = (tile[0] + center_position.x, tile[1] + center_position.y) target_tile = world.get_tile(target_tile_pos) if world.tile_has_tag(self.origin, target_tile, TileTag.ACTOR): entities_hit += scripts.engine.core.matter.get_entities_on_tile(target_tile) for entity in entities_hit: damage_effect = DamageEffect( origin=self.origin, success_effects=[], failure_effects=[], target=entity, stat_to_target=PrimaryStat.VIGOUR, accuracy=library.GAME_CONFIG.base_values.accuracy, damage=int(library.GAME_CONFIG.base_values.damage * self.potency), damage_type=DamageType.MUNDANE, mod_stat=PrimaryStat.CLOUT, mod_amount=0.1, ) self.success_effects.append(damage_effect) return True, self.success_effects
[docs]class DamageEffect(Effect):
[docs] def __init__( self, origin: EntityID, target: EntityID, success_effects: List[Effect], failure_effects: List[Effect], stat_to_target: PrimaryStatType, accuracy: int, damage: int, damage_type: DamageTypeType, mod_stat: PrimaryStatType, mod_amount: float, potency: float = 1.0, ): super().__init__(origin, target, success_effects, failure_effects, potency) self.accuracy = accuracy self.stat_to_target = stat_to_target self.damage = damage self.damage_type = damage_type self.mod_amount = mod_amount self.mod_stat = mod_stat self.hit_type_effects: Dict[HitTypeType, List[Effect]] = {HitType.HIT: [], HitType.GRAZE: [], HitType.CRIT: []} self.force_hit_type: Optional[HitTypeType] = None
[docs] def evaluate(self) -> Tuple[bool, List[Effect]]: """ Resolve the damage effect and return the conditional effects based on if the damage is greater than 0. """ logging.debug("Evaluating Damage Effect...") if self.damage <= 0: logging.info(f"Damage given to DamageEffect is {self.damage} and was therefore not executed.") return False, self.failure_effects elif not scripts.engine.core.matter.entity_has_component(self.target, Resources): logging.info(f"Target doesnt have resources so damage cannot be applied.") return False, self.failure_effects elif not scripts.engine.core.matter.entity_has_component(self.target, CombatStats): logging.warning(f"Target doesnt have combatstats so damage cannot be calculated.") return False, self.failure_effects elif not scripts.engine.core.matter.entity_has_component(self.origin, CombatStats): logging.warning(f"Attacker doesnt have combatstats so damage cannot be calculated.") return False, self.failure_effects # get combat stats defenders_stats = scripts.engine.core.matter.get_entitys_component(self.target, CombatStats) attackers_stats = scripts.engine.core.matter.get_entitys_component(self.origin, CombatStats) # get hit type stat_to_target_value = getattr(defenders_stats, self.stat_to_target.lower()) to_hit_score = scripts.engine.core.matter.calculate_to_hit_score( attackers_stats.accuracy, self.accuracy, stat_to_target_value ) if self.force_hit_type: hit_type = self.force_hit_type else: hit_type = scripts.engine.core.matter.get_hit_type(to_hit_score) # calculate damage resist_value = getattr(defenders_stats, "resist_" + self.damage_type.lower()) mod_value = getattr(attackers_stats, self.mod_stat.lower()) damage = scripts.engine.core.matter.calculate_damage(self.damage, mod_value, resist_value, hit_type) # apply the damage if scripts.engine.core.matter.apply_damage(self.target, damage): # add effects relevant to the hit type to the stack self.success_effects += self.hit_type_effects[hit_type] defenders_resources = scripts.engine.core.matter.get_entitys_component(self.target, Resources) # post interaction event event = DamageEvent( origin=self.origin, target=self.target, amount=damage, damage_type=self.damage_type, remaining_hp=defenders_resources.health, ) event_hub.post(event) # check if target is dead if damage >= defenders_resources.health: scripts.engine.core.matter.kill_entity(self.target) return True, self.success_effects else: return False, self.failure_effects
[docs]class MoveSelfEffect(Effect):
[docs] def __init__( self, origin: EntityID, target: EntityID, success_effects: List[Effect], failure_effects: List[Effect], direction: DirectionType, move_amount: int, ): super().__init__(origin, target, success_effects, failure_effects) self.direction = direction self.move_amount = move_amount
[docs] def evaluate(self) -> Tuple[bool, List[Effect]]: """ Resolve the move effect and return the conditional effects based on if the target moved the full amount. """ logging.debug("Evaluating Move Self Effect...") entity = self.target success = False # confirm targeting self if self.origin != entity: logging.debug( f"Failed to move {scripts.engine.core.matter.get_name(entity)} as they are not the originator." ) return False, self.failure_effects # check target has position if not scripts.engine.core.matter.entity_has_component(entity, Position): logging.debug(f"Failed to move {scripts.engine.core.matter.get_name(entity)} as they have no Position.") return False, self.failure_effects dir_x, dir_y = self.direction pos = scripts.engine.core.matter.get_entitys_component(entity, Position) # loop each target tile in turn for _ in range(0, self.move_amount): new_x = pos.x + dir_x new_y = pos.y + dir_y blocked = world.is_direction_blocked(entity, dir_x, dir_y) success = not blocked if not blocked: # named _position as typing was inferring from position above _position = scripts.engine.core.matter.get_entitys_component(entity, Position) # update position if _position: logging.debug( f"->'{scripts.engine.core.matter.get_name(self.target)}' moved from ({pos.x},{pos.y}) to ({new_x}," f"{new_y})." ) _position.set(new_x, new_y) # post interaction event event = MoveEvent( origin=self.origin, target=self.target, direction=self.direction, new_pos=(new_x, new_y) ) event_hub.post(event) success = True if success: return True, self.success_effects else: return False, self.failure_effects
[docs]class MoveOtherEffect(Effect):
[docs] def __init__( self, origin: EntityID, target: EntityID, success_effects: List[Effect], failure_effects: List[Effect], direction: DirectionType, move_amount: int, ): super().__init__(origin, target, success_effects, failure_effects) self.direction = direction self.move_amount = move_amount
[docs] def evaluate(self) -> Tuple[bool, List[Effect]]: """ Resolve the move effect and return the conditional effects based on if the target moved the full amount. """ logging.debug("Evaluating Move Other Effect...") success = False entity = self.target # confirm not targeting self if self.origin == entity: logging.debug(f"Failed to move {scripts.engine.core.matter.get_name(entity)} as they are the originator.") return False, self.failure_effects # check target has position if not scripts.engine.core.matter.entity_has_component(entity, Position): logging.debug(f"Failed to move {scripts.engine.core.matter.get_name(entity)} as they have no Position.") return False, self.failure_effects pos = scripts.engine.core.matter.get_entitys_component(entity, Position) dir_x, dir_y = self.direction # loop each target tile in turn for _ in range(0, self.move_amount): new_x = pos.x + dir_x new_y = pos.y + dir_y blocked = world.is_direction_blocked(entity, dir_x, dir_y) success = not blocked if not blocked: # named _position as typing was inferring from position above _position = scripts.engine.core.matter.get_entitys_component(entity, Position) # update position if _position: logging.debug( f"->'{scripts.engine.core.matter.get_name(self.target)}' moved from ({pos.x},{pos.y}) to ({new_x}," f"{new_y})." ) _position.set(new_x, new_y) # post interaction event event = MoveEvent( origin=self.origin, target=self.target, direction=self.direction, new_pos=(new_x, new_y) ) event_hub.post(event) success = True if success: return True, self.success_effects else: return False, self.failure_effects
[docs]class AffectStatEffect(Effect):
[docs] def __init__( self, origin: EntityID, target: EntityID, success_effects: List[Effect], failure_effects: List[Effect], cause_name: str, stat_to_target: PrimaryStatType, affect_amount: int, ): super().__init__(origin, target, success_effects, failure_effects) self.stat_to_target = stat_to_target self.affect_amount = affect_amount self.cause_name = cause_name
[docs] def evaluate(self) -> Tuple[bool, List[Effect]]: """ Log the affliction and the stat modification in the Affliction component. """ logging.debug("Evaluating Affect Stat Effect...") success = False stats = scripts.engine.core.matter.get_entitys_component(self.target, CombatStats) # if successfully applied if stats.add_mod(self.stat_to_target, self.cause_name, self.affect_amount): # post interaction event event = AffectStatEvent( origin=self.origin, target=self.target, stat_to_target=self.stat_to_target, amount=self.affect_amount, ) event_hub.post(event) success = True if success: return True, self.success_effects else: return False, self.failure_effects
[docs]class ApplyAfflictionEffect(Effect):
[docs] def __init__( self, origin: EntityID, target: EntityID, success_effects: List[Effect], failure_effects: List[Effect], affliction_name: str, duration: int, ): super().__init__(origin, target, success_effects, failure_effects) self.affliction_name = affliction_name self.duration = duration
[docs] def evaluate(self) -> Tuple[bool, List[Effect]]: """ Applies an affliction to an entity """ logging.debug("Evaluating Apply Affliction Effect...") affliction_name = self.affliction_name origin = self.origin target = self.target duration = self.duration affliction_instance = scripts.engine.core.matter.create_affliction(affliction_name, origin, target, duration) # check for immunities if scripts.engine.core.matter.entity_has_immunity(target, affliction_name): logging.debug( f"'{scripts.engine.core.matter.get_name(self.origin)}' failed to apply {affliction_name} to " f"'{scripts.engine.core.matter.get_name(self.target)}' as they are immune." ) return False, self.failure_effects # add the affliction to the afflictions component if scripts.engine.core.matter.entity_has_component(target, Afflictions): afflictions = scripts.engine.core.matter.get_entitys_component(target, Afflictions) afflictions.add(affliction_instance) scripts.engine.core.matter.apply_affliction(affliction_instance) # add immunities to prevent further applications for the duration scripts.engine.core.matter.add_immunity(target, affliction_name, duration + 2) # post interaction event event = AfflictionEvent( origin=origin, target=target, affliction_name=affliction_name, ) event_hub.post(event) return True, self.success_effects # didn't have the component, fail return False, self.failure_effects
[docs]class AffectCooldownEffect(Effect):
[docs] def __init__( self, origin: EntityID, target: EntityID, success_effects: List[Effect], failure_effects: List[Effect], skill_name: str, affect_amount: int, ): super().__init__(origin, target, success_effects, failure_effects) self.skill_name = skill_name self.affect_amount = affect_amount
[docs] def evaluate(self) -> Tuple[bool, List[Effect]]: """ Reduces the cooldown of a skill of an entity """ logging.debug("Evaluating Reduce Skill Cooldown Effect...") knowledge = scripts.engine.core.matter.get_entitys_component(self.target, Knowledge) if knowledge: current_cooldown = knowledge.cooldowns[self.skill_name] knowledge.set_skill_cooldown(self.skill_name, current_cooldown - self.affect_amount) # post interaction event event = AffectCooldownEvent( origin=self.origin, target=self.target, amount=self.affect_amount, ) event_hub.post(event) logging.debug( f"Reduced cooldown of skill '{self.skill_name}' from {current_cooldown} to " f"{knowledge.cooldowns[self.skill_name]}" ) return True, self.success_effects return False, self.failure_effects
[docs]class AlterTerrainEffect(Effect):
[docs] def __init__( self, origin: EntityID, target: EntityID, success_effects: List[Effect], failure_effects: List[Effect], terrain_name: str, affect_amount: int, ): super().__init__(origin, target, success_effects, failure_effects) self.terrain_name = terrain_name self.affect_amount = affect_amount
[docs] def evaluate(self) -> Tuple[bool, List[Effect]]: """ Create or reduce the duration of temporary terrain. """ logging.debug("Evaluating Alter Terrain Effect...") # check duration if self.affect_amount <= 0: success = self._reduce_terrain_duration() else: success = self._create_terrain() # return effect sets if success: return True, self.success_effects else: return False, self.failure_effects
def _create_terrain(self) -> bool: result = False duplicate = False terrain_name = self.terrain_name target_pos = scripts.engine.core.matter.get_entitys_component(self.target, Position) # check target location doesnt already have the given terrain for entity, (position, identity, lifespan) in query.position_and_identity_and_lifespan: assert isinstance(position, Position) assert isinstance(identity, Identity) assert isinstance(lifespan, Lifespan) if identity.name == terrain_name and position.x == target_pos.x and position.y == target_pos.y: duplicate = True break # can we create the terrain? if not duplicate: # create target terrain_data = library.TERRAIN[terrain_name] scripts.engine.core.matter.create_terrain(terrain_data, (target_pos.x, target_pos.y), self.affect_amount) result = True # post interaction event event = AlterTerrainEvent( origin=self.origin, target=self.target, terrain_name=self.terrain_name, duration=self.affect_amount ) event_hub.post(event) return result def _reduce_terrain_duration(self) -> bool: result = False terrain_name = self.terrain_name target_pos = scripts.engine.core.matter.get_entitys_component(self.target, Position) # check there is a terrain at target to reduce duration of for entity, (position, identity, lifespan) in query.position_and_identity_and_lifespan: assert isinstance(position, Position) assert isinstance(identity, Identity) assert isinstance(lifespan, Lifespan) if identity.name == terrain_name and position.x == target_pos.x and position.y == target_pos.y: lifespan.duration -= self.affect_amount result = True break if result: # post interaction event event = AlterTerrainEvent( origin=self.origin, target=self.target, terrain_name=self.terrain_name, duration=self.affect_amount ) event_hub.post(event) return result