Source code for scripts.engine.core.component

from __future__ import annotations

import logging
import sys
from dataclasses import asdict
from typing import TYPE_CHECKING, Union

import numpy as np
from snecs import RegisteredComponent
from snecs.typedefs import EntityID

from scripts.engine.internal import library
from scripts.engine.internal.constant import (
    HeightType,
    PrimaryStat,
    PrimaryStatType,
    ReactionTriggerType,
    RenderLayer,
    SecondaryStat,
    SecondaryStatType,
    SpriteCategory,
    SpriteCategoryType,
)
from scripts.engine.internal.definition import ReactionData
from scripts.engine.internal.event import event_hub, ShrineMenuEvent

if TYPE_CHECKING:
    from typing import Dict, List, Optional, Set, Tuple, Type

    import pygame

    from scripts.engine.internal.action import Affliction, Behaviour, Skill, SkillModifier
    from scripts.engine.internal.definition import TraitSpritePathsData, TraitSpritesData

__all__ = [
    "Exists",
    "IsPlayer",
    "IsActive",
    "CombatStats",
    "WinCondition",
    "MapCondition",
    "Shrine",
    "Position",
    "Aesthetic",
    "Tracked",
    "Resources",
    "Physicality",
    "Identity",
    "Traits",
    "Thought",
    "Knowledge",
    "Afflictions",
    "Opinion",
    "FOV",
    "LightSource",
    "Reaction",
    "Lifespan",
    "Immunities",
    "Sight",
    "NQPComponent",
]


##########################################################
# Components are to hold data that is subject to change.
#########################################################


[docs]class NQPComponent(RegisteredComponent): """ Subclass snecs' RegisteredComponent to extend with an on_delete method """
[docs] def on_delete(self): pass
########################### FLAGS ##############################
[docs]class Exists(NQPComponent): """ Empty flag for all entities. Used to allow filters to search for a single component i.e. use a single condition. """ __slots__ = ()
[docs] def serialize(self): return True
[docs] @classmethod def deserialize(cls, serialised): return Exists()
[docs]class IsPlayer(NQPComponent): """ Whether the entity is the player. """ __slots__ = () # reduces memory footprint as it prevents the creation of __dict__ and __weakref__ per instance
[docs] def serialize(self): return True
[docs] @classmethod def deserialize(cls, serialised): return IsPlayer()
[docs]class IsActive(NQPComponent): """ Whether the entity is active or not. Used to limit entity processing. """ __slots__ = ()
[docs] def serialize(self): return True
[docs] @classmethod def deserialize(cls, serialised): return IsActive()
[docs]class WinCondition(NQPComponent): """ A flag to show that an entity is a win objective """ __slots__ = ()
[docs] def serialize(self): return True
[docs] @classmethod def deserialize(cls, serialised): return WinCondition()
[docs]class MapCondition(NQPComponent): """ A flag to show that an entity will take the player to the next map """ __slots__ = ()
[docs] def serialize(self): return True
[docs] @classmethod def deserialize(cls, serialised): return MapCondition()
[docs]class Shrine(NQPComponent): """ The component for tracking information relevant to shrines. """
[docs] def __init__(self): super().__init__() self.associated_blessings: List[SkillModifier] = [] self.used = False self.blessings_count = 4
[docs] def serialize(self): return True
[docs] @classmethod def deserialize(cls, serialised): return Shrine()
[docs] def interact(self): import scripts.engine.core.matter if self.associated_blessings == []: self.associated_blessings = scripts.engine.core.matter.create_compatible_blessings(self.blessings_count) if not self.used: event_hub.post(ShrineMenuEvent(self.associated_blessings)) self.used = True
#################### OTHERS #########################
[docs]class Position(NQPComponent): """ An entity's position on the map. At initiation provide all positions the entity holds. After initiation only need to set the top left, or reference position as the other coordinates are held as offsets. """
[docs] def __init__(self, *positions: Tuple[int, int]): # Sort the positions from top-left to down-right if not positions: raise ValueError("Must provide at least 1 coordinate for the entity.") sorted_positions = sorted(positions, key=lambda x: (x[0] ** 2 + x[1] ** 2)) top_left = sorted_positions[0] self.offsets = [(x - top_left[0], y - top_left[1]) for x, y in sorted_positions] self.reference_position = top_left
[docs] def serialize(self): return self.coordinates
[docs] @classmethod def deserialize(cls, serialised): return Position(*serialised)
[docs] def set(self, x: int, y: int): self.reference_position = (x, y)
[docs] def get_outermost(self, direction: Tuple[int, int]) -> Tuple[int, int]: """ Calculate the outermost tile in the direction provided :param direction: Direction to use :return: The position of the outermost tile """ coordinates = self.coordinates # Calculate center center = (sum(c[0] for c in coordinates), sum(c[1] for c in coordinates)) transformed = [np.dot((c[0], c[1]), direction) for c in coordinates] # Find the coordinate that is nearest the direction arg_max = np.argwhere(transformed == np.amax(transformed)) # From all the nearest coordinates find the one nearest to the center of the entity arg_min = np.argmin( np.sqrt((center[0] - transformed[i[0]][0]) ** 2 + (center[1] - transformed[i[0]][1]) ** 2) for i in arg_max ) # type: ignore return coordinates[arg_max[arg_min][0]][0], coordinates[arg_max[arg_min][0]][1]
@property def x(self) -> int: """ :return: The x component of the top-left position """ return self.reference_position[0] @property def y(self) -> int: """ :return: The y component of the top-left position """ return self.reference_position[1] @property def coordinates(self) -> List[Tuple[int, int]]: """ :return: The list of coordinates that this Position represents """ return [(self.x + x, self.y + y) for x, y in self.offsets] def __contains__(self, key: Tuple[int, int]): """ :param key: Coordinate to test against :return: A bool that represents if the Position contains the provided coordinates """ for coordinate in self.coordinates: if coordinate == key: return True return False
[docs]class Aesthetic(NQPComponent): """ An entity's sprite. N.B. translation to screen coordinates is handled by the camera """
[docs] def __init__( self, sprites: TraitSpritesData, sprite_paths: List[TraitSpritePathsData], render_layer: RenderLayer, draw_pos: Tuple[float, float], ): self._sprite_paths: List[TraitSpritePathsData] = sprite_paths self.sprites: TraitSpritesData = sprites self.current_sprite: pygame.Surface = self.sprites.idle self.current_sprite_category: SpriteCategoryType = getattr(self.sprites, SpriteCategory.IDLE) self.render_layer = render_layer draw_x, draw_y = draw_pos self.draw_x: float = draw_x self.draw_y: float = draw_y self.target_draw_x: float = draw_x self.target_draw_y: float = draw_y self.current_sprite_duration: float = 0
[docs] def serialize(self): # loop all sprite paths and convert to dict sprite_paths = [] for sprite_path in self._sprite_paths: sprite_paths.append(asdict(sprite_path)) _dict = { "draw_pos": (self.target_draw_x, self.target_draw_y), # use target to align with actual position "render_layer": self.render_layer, "sprite_paths": sprite_paths, } return _dict
[docs] @classmethod def deserialize(cls, serialised): x, y = serialised["draw_pos"] render_layer = serialised["render_layer"] _sprite_paths = serialised["sprite_paths"] # unpack sprite paths sprite_paths = [] from scripts.engine.internal.definition import TraitSpritePathsData for sprite_path in _sprite_paths: sprite_paths.append(TraitSpritePathsData(**sprite_path)) # convert sprite paths to sprites from scripts.engine.core import utility sprites = utility.build_sprites_from_paths(sprite_paths) return Aesthetic(sprites, sprite_paths, render_layer, (x, y))
[docs] def set_current_sprite(self, sprite_category: SpriteCategoryType): """ Set the current sprite. Set current sprite duration to 0. """ sprite = getattr(self.sprites, sprite_category) self.current_sprite = sprite self.current_sprite_category = sprite_category self.current_sprite_duration = 0
[docs] def set_draw_to_target(self): """ Set draw_x and draw_y to their target values """ self.draw_x = self.target_draw_x self.draw_y = self.target_draw_y
[docs]class Tracked(NQPComponent): """ A component to hold info on activities of an entity """
[docs] def __init__(self, time_spent: int = 0): self.time_spent: int = time_spent
[docs] def serialize(self): return self.time_spent
[docs] @classmethod def deserialize(cls, serialised): return Tracked(serialised)
[docs]class Resources(NQPComponent): """ An entity's resources. Members align to Resource constants. """
[docs] def __init__(self, health: int = 1, stamina: int = 1): self.health: int = health self.stamina: int = stamina
[docs] def serialize(self): return self.health, self.stamina
[docs] @classmethod def deserialize(cls, serialised): return Resources(*serialised)
[docs]class Physicality(NQPComponent): """ An entity's physical existence within the world. """
[docs] def __init__(self, blocks_movement: bool, height: HeightType): self.blocks_movement: bool = blocks_movement self.height: HeightType = height
[docs] def serialize(self): return self.blocks_movement, self.height
[docs] @classmethod def deserialize(cls, serialised): return Physicality(*serialised)
[docs]class Identity(NQPComponent): """ An entity's identity, such as name and description. """
[docs] def __init__(self, name: str, description: str = ""): self.name: str = name self.description: str = description
[docs] def serialize(self): return self.name, self.description
[docs] @classmethod def deserialize(cls, serialised): return cls(*serialised)
[docs]class Traits(NQPComponent): """ An entity's traits. Class, archetype, skill set or otherwise defining group. """
[docs] def __init__(self, trait_names: List[str]): self.names: List[str] = trait_names
[docs] def serialize(self): return self.names
[docs] @classmethod def deserialize(cls, serialised): return Traits(serialised)
[docs]class Thought(NQPComponent): """ An ai behaviour to control an entity. """
[docs] def __init__(self, behaviour: Behaviour): self.behaviour = behaviour
[docs] def serialize(self): _dict = {"behaviour_name": self.behaviour.__class__.__name__, "entity": self.behaviour.entity} return _dict
[docs] @classmethod def deserialize(cls, serialised): from scripts.engine.internal.data import store behaviour = store.behaviour_registry[serialised["behaviour_name"]] return Thought(behaviour(serialised["entity"]))
[docs]class Knowledge(NQPComponent): """ An entity's knowledge, including skills. """
[docs] def __init__( self, skills: List[Type[Skill]], skill_order: Optional[List[str]] = None, cooldowns: Optional[Dict[str, int]] = None, ): skills = skills or [] skill_order = skill_order or [] cooldowns = cooldowns or {} # determine if we need to add cooldowns or not - must be before setting self if cooldowns: _set_cooldown = False else: _set_cooldown = True # dont override skill order if it has been provided if skill_order: _add_to_order = False else: _add_to_order = True self.skill_order: List[str] = skill_order self.cooldowns: Dict[str, int] = cooldowns self.skill_names: List[str] = [] self.skills: Dict[str, Type[Skill]] = {} # dont set skills here, use learn skill self.skill_blessings: Dict[str, List[SkillModifier]] = {} for skill_class in skills: self.add(skill_class, _add_to_order, _set_cooldown)
[docs] def set_skill_cooldown(self, name: str, value: int): """ Sets the cooldown of a skill """ self.cooldowns[name] = max(0, value)
[docs] def add(self, skill: Type[Skill], add_to_order: bool = True, set_cooldown: bool = True): """ Learn a new skill. """ self.skill_names.append(skill.__name__) self.skills[skill.__name__] = skill if add_to_order: self.skill_order.append(skill.__name__) if set_cooldown: self.cooldowns[skill.__name__] = 0
[docs] def can_add_blessing(self, skill: Type[Skill], blessing: SkillModifier) -> bool: """ Check if a blessing can be added to a skill. """ # create blessing list for the target skill if it doesn't exist yet if skill.__name__ not in self.skill_blessings: self.skill_blessings[skill.__name__] = [] # use sets to check for and prevent collisions (modifying the same effect, specified conflicts, duplicates, etc.) used_effects: Set[str] = set() blessing_names: Set[str] = set() for b in self.skill_blessings[skill.__name__]: used_effects = used_effects.union(b.involved_effects) blessing_names = blessing_names.union({b.__class__.__name__}) if used_effects.intersection(blessing.involved_effects): return False elif blessing.__class__.__name__ in blessing_names: return False elif set(blessing.conflicts).intersection(blessing_names): return False elif not set(blessing.skill_types).intersection(set(skill.types)): return False else: # all requirements met return True
[docs] def add_blessing(self, skill: Type[Skill], blessing: SkillModifier) -> bool: """ Add a blessing to a skill. """ # generate blessing level (this will probably be moved somewhere else later) blessing.roll_level() # check if blessing can be applied and apply if possible if self.can_add_blessing(skill, blessing): self.skill_blessings[skill.__name__].append(blessing) return True else: return False
[docs] def remove_blessing(self, skill: Type[Skill], remove_blessing: Type[SkillModifier]) -> bool: """ Attempt to remove a blessing. """ if remove_blessing.removable: if skill.__name__ in self.skill_blessings: for blessing in self.skill_blessings[skill.__name__]: if blessing.__class__.__name__ == remove_blessing.__name__: self.skill_blessings[skill.__name__].remove(blessing) return True return False
[docs] def serialize(self): _dict = {"skill_names": self.skill_names, "cooldowns": self.cooldowns, "skill_order": self.skill_order} return _dict
[docs] @classmethod def deserialize(cls, serialised): skill_names = serialised["skill_names"] cooldowns = serialised["cooldowns"] skill_order = serialised["skill_order"] skills = [] from scripts.engine.internal.data import store for name in skill_names: skills.append(store.skill_registry[name]) return Knowledge(skills, skill_order, cooldowns)
[docs]class Afflictions(NQPComponent): """ An entity's Boons and Banes. held in .active as a list of Affliction. """
[docs] def __init__(self, active: Optional[List[Affliction]] = None): active = active or [] self.active: List[Affliction] = active # TODO - should this be a dict for easier querying?
[docs] def serialize(self): active = {} for affliction in self.active: active[affliction.__class__.__name__] = ( affliction.origin, affliction.affected_entity, affliction.duration, ) _dict = {"active": active} return _dict
[docs] @classmethod def deserialize(cls, serialised): active_dict = serialised["active"] active_instances = [] from scripts.engine.internal.data import store for name, value_tuple in active_dict.items(): _affliction = store.affliction_registry[name] affliction = _affliction(value_tuple[0], value_tuple[1], value_tuple[2]) active_instances.append(affliction) return Afflictions(active_instances)
[docs] def add(self, affliction: Affliction): self.active.append(affliction)
[docs] def remove(self, affliction: Affliction): if affliction in self.active: # remove from active list self.active.remove(affliction)
[docs]class Opinion(NQPComponent): """ An entity's views on other entities. {entity, opinion} """
[docs] def __init__(self, attitudes: Dict[ReactionTriggerType, int], opinions: Optional[Dict[EntityID, int]] = None): opinions = opinions or {} self.opinions: Dict[EntityID, int] = opinions self.attitudes: Dict[ReactionTriggerType, int] = attitudes
[docs] def serialize(self): _dict = {"attitudes": self.attitudes, "opinions": self.opinions} return _dict
[docs] @classmethod def deserialize(cls, serialised): attitudes = serialised["attitudes"] opinions = {} for entity, opinion in serialised["opinions"].items(): opinions[int(entity)] = opinion return Opinion(attitudes, opinions)
[docs]class FOV(NQPComponent): """ An entity's field of view. Always starts blank. """
[docs] def __init__(self): from scripts.engine.core import world game_map = world.get_game_map() self.map: np.array = game_map.block_sight_map.astype(bool) # return of compute_fov is bool
[docs] def serialize(self): fov_map = self.map.tolist() return fov_map
[docs] @classmethod def deserialize(cls, serialised): fov = FOV() fov.map = np.array(serialised) return fov
[docs]class LightSource(NQPComponent): """ An emitter of light. Takes the light_id from a Light. The Light must be added to the Lightbox of the Gamemap separately. """
[docs] def __init__(self, light_id: str, radius: int): self.light_id: str = light_id self.radius: int = radius
[docs] def serialize(self): from scripts.engine.core import world game_map = world.get_game_map() light_box = game_map.light_box light = light_box.get_light(self.light_id) _dict = {"pos": light.position, "radius": self.radius, "colour": light.colour, "alpha": light.alpha} return _dict
[docs] @classmethod def deserialize(cls, serialised): pos = serialised["pos"] radius = serialised["radius"] colour = serialised["colour"] alpha = serialised["alpha"] from scripts.engine.core import matter light_id = matter.create_light(pos, radius, colour, alpha) return LightSource(light_id, radius)
[docs] def on_delete(self): """ Delete the associated light from the Gamemap's Lightbox """ from scripts.engine.core import world light_box = world.get_game_map().light_box light_box.delete_light(self.light_id)
[docs]class Reaction(NQPComponent): """ Holds info about what triggers are in place and what happens as a result """
[docs] def __init__(self, reactions: Dict[ReactionTriggerType, ReactionData]): self.reactions: Dict[ReactionTriggerType, ReactionData] = reactions
[docs] def serialize(self): _dict = {} for trigger, reaction_data in self.reactions.items(): # reaction can be skill name (str) or effect data so need to handle both if isinstance(reaction_data.reaction, str): reaction = reaction_data.reaction effect_dataclass_name = None else: reaction = asdict(reaction_data.reaction) effect_dataclass_name = reaction_data.reaction.__class__.__name__ _dict[trigger] = { "required_opinion": reaction_data.required_opinion, "reaction": reaction, "effect_dataclass_name": effect_dataclass_name, "chance": reaction_data.chance, } return _dict
[docs] @classmethod def deserialize(cls, serialised): reactions = {} for trigger, reaction_data in serialised.items(): # reaction can be skill name (str) or effect data so need to handle both if isinstance(reaction_data["reaction"], str): reaction = reaction_data["reaction"] else: effect_dataclass = getattr( sys.modules["scripts.engine.internal.definition"], reaction_data["effect_dataclass_name"] ) reaction = effect_dataclass(reaction_data["reaction"]) _reaction_data = { "required_opinion": reaction_data["required_opinion"], "reaction": reaction, "chance": reaction_data["chance"], } reactions[trigger] = ReactionData(**_reaction_data) return Reaction(reactions)
[docs]class Lifespan(NQPComponent): """ Holds info relating to the limited lifespan of an entity. E.g. temporary summons. Can be set to INFINITE, which prevents it being reduced each turn. """
[docs] def __init__(self, duration: int): self.duration = duration
[docs] def serialize(self): _dict = {"duration": self.duration} return _dict
[docs] @classmethod def deserialize(cls, serialised): return Lifespan(serialised["duration"])
[docs]class Immunities(NQPComponent): """ Holds the details of anything the entity is immune to. Can be set to INFINITE, which prevents it being reduced each turn. """
[docs] def __init__(self, immunities: Dict[str, int] = None): # handle mutable default if immunities is None: immunities = {} self.active: Dict[str, int] = immunities # name, duration
[docs] def serialize(self): _dict = {"active": self.active} return _dict
[docs] @classmethod def deserialize(cls, serialised): return Immunities(serialised["active"])
[docs]class CombatStats(NQPComponent): """ An entities stats used for combat. """
[docs] def __init__(self, vigour: int, clout: int, skullduggery: int, bustle: int, exactitude: int): """ Set primary stats. Secondary stats pulled from library. """ self._vigour: int = vigour self._clout: int = clout self._skullduggery: int = skullduggery self._bustle: int = bustle self._exactitude: int = exactitude self._vigour_mod: Dict[str, int] = {} # cause, amount self._clout_mod: Dict[str, int] = {} self._skullduggery_mod: Dict[str, int] = {} self._bustle_mod: Dict[str, int] = {} self._exactitude_mod: Dict[str, int] = {} self._max_health: int = 0 self._max_stamina: int = 0 self._accuracy: int = 0 self._resist_burn: int = 0 self._resist_cold: int = 0 self._resist_chemical: int = 0 self._resist_astral: int = 0 self._resist_mundane: int = 0 self._rush: int = 0 self._max_health_mod: Dict[str, int] = {} # cause, amount self._max_stamina_mod: Dict[str, int] = {} self._accuracy_mod: Dict[str, int] = {} self._resist_burn_mod: Dict[str, int] = {} self._resist_cold_mod: Dict[str, int] = {} self._resist_chemical_mod: Dict[str, int] = {} self._resist_astral_mod: Dict[str, int] = {} self._resist_mundane_mod: Dict[str, int] = {} self._rush_mod: Dict[str, int] = {}
[docs] def serialize(self): _dict = { "vigour": self._vigour, "clout": self._clout, "skullduggery": self._skullduggery, "bustle": self._bustle, "exactitude": self._exactitude, "vigour_mod": self._vigour_mod, "clout_mod": self._clout_mod, "skullduggery_mod": self._skullduggery_mod, "bustle_mod": self._bustle_mod, "exactitude_mod": self._exactitude_mod, "max_health": self._max_health, "max_stamina": self._max_stamina, "accuracy": self._accuracy, "resist_burn": self._resist_burn, "resist_cold": self._resist_cold, "resist_chemical": self._resist_chemical, "resist_astral": self._resist_astral, "resist_mundane": self._resist_mundane, "rush": self._rush, "max_health_mod": self._max_health_mod, "max_stamina_mod": self._max_stamina_mod, "accuracy_mod": self._accuracy_mod, "resist_burn_mod": self._resist_burn_mod, "resist_cold_mod": self._resist_cold_mod, "resist_chemical_mod": self._resist_chemical_mod, "resist_astral_mod": self._resist_astral_mod, "resist_mundane_mod": self._resist_mundane_mod, "rush_mod": self._rush_mod, } return _dict
[docs] @classmethod def deserialize(cls, serialised): stats = CombatStats( serialised["vigour"], serialised["clout"], serialised["skullduggery"], serialised["bustle"], serialised["exactitude"], ) stats._vigour_mod = serialised["vigour_mod"] stats._clout_mod = serialised["clout_mod"] stats._skullduggery_mod = serialised["skullduggery_mod"] stats._bustle_mod = serialised["bustle_mod"] stats._exactitude_mod = serialised["exactitude_mod"] stats._max_health = serialised["max_health"] stats._max_stamina = serialised["max_stamina"] stats._accuracy = serialised["accuracy"] stats._resist_burn = serialised["resist_burn"] stats._resist_cold = serialised["resist_cold"] stats._resist_chemical = serialised["resist_chemical"] stats._resist_astral = serialised["resist_astral"] stats._resist_mundane = serialised["resist_mundane"] stats._rush = serialised["rush"] stats._max_health_mod = serialised["max_health_mod"] stats._max_stamina_mod = serialised["max_stamina_mod"] stats._accuracy_mod = serialised["accuracy_mod"] stats._resist_burn_mod = serialised["resist_burn_mod"] stats._resist_cold_mod = serialised["resist_cold_mod"] stats._resist_chemical_mod = serialised["resist_chemical_mod"] stats._resist_astral_mod = serialised["resist_astral_mod"] stats._resist_mundane_mod = serialised["resist_mundane_mod"] stats._rush_mod = serialised["rush_mod"] return stats
[docs] def amend_base_value(self, stat: Union[PrimaryStatType, SecondaryStatType], amount: int): """ Amend the base value of a stat """ current_value = getattr(self, "_" + stat) setattr(self, "_" + stat, current_value + amount)
[docs] def add_mod(self, stat: Union[PrimaryStatType, SecondaryStatType], cause: str, amount: int) -> bool: """ Amend the modifier of a stat. Returns True if successfully amended, else False. """ mod_to_amend = getattr(self, "_" + stat + "_mod") if cause in mod_to_amend: logging.info(f"Stat not modified as {cause} has already been applied.") return False else: mod_to_amend[cause] = amount return True
[docs] def remove_mod(self, cause: str) -> bool: """ Remove a modifier from a stat. Returns True if successfully removed, else False. """ from scripts.engine.core import utility for stat in utility.get_class_members(self.__class__): if cause in stat: assert isinstance(stat, dict) del stat[cause] return True logging.info(f"Modifier not removed as {cause} does not exist in modifier list.") return False
def _get_secondary_stat(self, stat: SecondaryStatType) -> int: """ Get the value of the secondary stat """ stat_data = library.SECONDARY_STAT_MODS[stat] value = getattr(self, "_" + stat.lower()) value += self.vigour * stat_data.vigour_mod value += self.clout * stat_data.clout_mod value += self.skullduggery * stat_data.skullduggery_mod value += self.bustle * stat_data.bustle_mod value += self.exactitude * stat_data.exactitude_mod value += self._get_mod_value(stat) return value def _get_mod_value(self, stat: Union[PrimaryStatType, SecondaryStatType]) -> int: mod = getattr(self, "_" + stat + "_mod") value = 0 for modifier in mod.values(): value += modifier return value @property def vigour(self) -> int: """ Influences healthiness. Never below 1. """ return max(1, self._vigour + self._get_mod_value(PrimaryStat.VIGOUR)) @property def clout(self) -> int: """ Influences forceful things. Never below 1. """ return max(1, self._clout + self._get_mod_value(PrimaryStat.CLOUT)) @property def skullduggery(self) -> int: """ Influences sneaky things. Never below 1. """ return max(1, self._skullduggery + self._get_mod_value(PrimaryStat.SKULLDUGGERY)) @property def bustle(self) -> int: """ Influences speedy things. Never below 1. """ return max(1, self._bustle + self._get_mod_value(PrimaryStat.BUSTLE)) @property def exactitude(self) -> int: """ Influences preciseness. Never below 1. """ return max(1, self._exactitude + self._get_mod_value(PrimaryStat.EXACTITUDE)) @property def max_health(self) -> int: """ Total damage an entity can take before death. """ return max(1, self._get_secondary_stat(SecondaryStat.MAX_HEALTH)) @property def max_stamina(self) -> int: """ An entities energy to take actions. """ return max(1, self._get_secondary_stat(SecondaryStat.MAX_STAMINA)) @property def accuracy(self) -> int: """ An entities likelihood to hit. """ return max(1, self._get_secondary_stat(SecondaryStat.ACCURACY)) @property def resist_burn(self) -> int: """ An entities resistance to burn damage. """ return max(1, self._get_secondary_stat(SecondaryStat.RESIST_BURN)) @property def resist_cold(self) -> int: """ An entities resistance to cold damage. """ return max(1, self._get_secondary_stat(SecondaryStat.RESIST_COLD)) @property def resist_chemical(self) -> int: """ An entities resistance to chemical damage. """ return max(1, self._get_secondary_stat(SecondaryStat.RESIST_CHEMICAL)) @property def resist_astral(self) -> int: """ An entities resistance to astral damage. """ return max(1, self._get_secondary_stat(SecondaryStat.RESIST_ASTRAL)) @property def resist_mundane(self) -> int: """ An entities resistance to mundane damage. """ return max(1, self._get_secondary_stat(SecondaryStat.RESIST_MUNDANE)) @property def rush(self) -> int: """ How quickly an entity does things. Reduce time cost of actions. """ return max(1, self._get_secondary_stat(SecondaryStat.RUSH))
[docs]class Sight(NQPComponent): """ An entity's ability to see. """
[docs] def __init__(self, sight_range: int): self.sight_range: int = sight_range
[docs] def serialize(self): _dict = {"sight_range": self.sight_range} return _dict
[docs] @classmethod def deserialize(cls, serialised): return Sight(**serialised)