NotQuiteParadise2

Source code for nqp.command.unit

from __future__ import annotations

import random
from typing import Optional, TYPE_CHECKING

import pygame
import snecs

from nqp.core.constants import StatModifiedStatus

if TYPE_CHECKING:
    from typing import List

    from snecs.typedefs import EntityID

    from nqp.core.game import Game

__all__ = ["Unit"]


[docs]class Unit:
[docs] def __init__(self, game: Game, id_: int, unit_type: str, team: str, pos: pygame.Vector2): self._game: Game = game # get unit data unit_data = self._game.data.units[unit_type] base_values = self._game.data.config["unit_base_values"][f"tier_{unit_data['tier']}"] ######### Unit Info ############# self.id = id_ self.type: str = unit_type self.team: str = team # this is derived from the Troupe but can be overridden in combat self.pos: pygame.Vector2 = pos self.is_selected: bool = False self.entities: List[EntityID] = [] self.entity_spread_max = unit_data["entity_spread"] if "entity_spread" in unit_data else 24 self.count: int = unit_data["count"] + base_values["count"] # number of entities spawned self.gold_cost: int = unit_data["gold_cost" ""] + base_values["gold_cost"] self._banner_image = self._game.visual.get_image("banner") self.is_ranged: bool = True if unit_data["ammo"] > 0 else False from nqp.command.unit_behaviour import UnitBehaviour # prevent circular import self.behaviour: UnitBehaviour = UnitBehaviour(self._game, self) self.injuries: int = 0 # border self.border_surface_timer: float = 0 self.border_surface: Optional[pygame.surface] = None self.border_surface_offset: pygame.Vector2 = pygame.Vector2(0, 0) self.border_surface_outline: Optional[pygame.surface] = None self.border_surface_outline_black: Optional[pygame.surface] = None ######### Entity Info ############## # stats that dont use base values self.type: str = unit_data["type"] self.tier: int = unit_data["tier"] # stats that include base values self.health: int = unit_data["health"] + base_values["health"] self.attack: int = unit_data["attack"] + base_values["attack"] self.damage_type: str = unit_data["damage_type"] self.mundane_defence: int = unit_data["mundane_defence"] + base_values["mundane_defence"] self.magic_defence: int = unit_data["magic_defence"] + base_values["magic_defence"] self.range: int = unit_data["range"] + base_values["range"] self.attack_speed: float = unit_data["attack_speed"] + base_values["attack_speed"] self.move_speed: int = unit_data["move_speed"] + base_values["move_speed"] self.crit_chance: int = unit_data["crit_chance"] + base_values["crit_chance"] self.penetration: int = unit_data["penetration"] + base_values["penetration"] self.regen: int = unit_data["regen"] + base_values["regen"] self.dodge: int = unit_data["dodge"] + base_values["dodge"] # ensure faux-null value is respected if unit_data["ammo"] in [-1, 0]: ammo = -1 else: ammo = unit_data["ammo"] + base_values["ammo"] self._ammo: int = ammo # number of ranged shots self.uses_projectiles = self._ammo > 0 self.size: int = unit_data["size"] + base_values["size"] # size of the hitbox self.weight: int = unit_data["weight"] + base_values["weight"] self.projectile_data = ( unit_data["projectile_data"] if "projectile_data" in unit_data else {"img": "arrow", "speed": 100} )
[docs] def update(self, delta_time: float): # refresh the border if self.team == "player": self.border_surface_timer += delta_time if self.border_surface_timer > 0.5: self.border_surface_timer -= 0.5 self.generate_border_surface() self.behaviour.update(delta_time) self.update_position()
@property def is_alive(self): from nqp.world_elements.entity_components import IsDead # prevent circular import for entity in self.entities: if not snecs.has_component(entity, IsDead): return True return False
[docs] def draw_banner(self, surface: pygame.Surface, shift: pygame.Vector2 = (0, 0)): """ Draw's the Unit's banner. """ banner_image = self._banner_image surface.blit( banner_image.surface, ( self.pos.x + shift[0] - banner_image.width // 2, self.pos.y + shift[1] - 20 - banner_image.height, ), )
[docs] def draw_border(self, surface: pygame.Surface, shift: pygame.Vector2 = (0, 0)): """ Draw the border around the unit. """ # TODO: better handling of missing outline files if self.border_surface_outline_black: for d in [(-1, 0), (1, 0), (0, 1), (0, -1)]: surface.blit( self.border_surface_outline_black, ( self.pos.x + shift[0] - self.border_surface_offset[0] + d[0], self.pos.y + shift[1] - self.border_surface_offset[1] + d[1], ), ) if self.border_surface: surface.blit( self.border_surface, ( self.pos.x + shift[0] - self.border_surface_offset[0], self.pos.y + shift[1] - self.border_surface_offset[1], ), ) if self.border_surface_outline: surface.blit( self.border_surface_outline, ( self.pos.x + shift[0] - self.border_surface_offset[0], self.pos.y + shift[1] - self.border_surface_offset[1], ), )
[docs] def spawn_entities(self): """ Spawn the Unit's Entities. Deletes any existing Entities first. """ # prevent circular import error from nqp.world_elements.entity_components import ( Aesthetic, AI, Allegiance, Attributes, Position, RangedAttack, Stats, ) self.delete_entities() for _ in range(self.count): # universal components components = [ Position(self.pos), Aesthetic(self._game.visual.create_animation(self.type, "idle")), Stats(self), Allegiance(self.team, self), Attributes(), ] # conditional components if self.uses_projectiles: img = self._game.visual.get_image(self.projectile_data["img"]) speed = self.projectile_data components.append(RangedAttack(self._ammo, img, speed)) # create entity entity = snecs.new_entity(components) self.entities.append(entity) # add components that need ref to entity from nqp.command.basic_entity_behaviour import BasicEntityBehaviour # prevent circular import snecs.add_component(entity, AI(BasicEntityBehaviour(self._game, self, entity))) self._align_entity_positions_to_unit()
[docs] def delete_entities(self, immediately: bool = False): """ Delete all entities. If "immediately" = False this will happen on the next frame. """ if immediately: delete_func = snecs.delete_entity_immediately else: delete_func = snecs.schedule_for_deletion for entity in self.entities: delete_func(entity) self.entities = []
[docs] def reset_for_combat(self): """ Reset the in combat values ready to begin combat. """ # prevent circular import from nqp.world_elements.entity_components import DamageReceived, IsDead, IsReadyToAttack, RangedAttack, Stats # get stat attrs stat_attrs = Stats.get_stat_names() health = self.health for entity in self.entities: # remove flags if snecs.has_component(entity, IsDead): snecs.remove_component(entity, IsDead) if snecs.has_component(entity, IsReadyToAttack): snecs.remove_component(entity, IsReadyToAttack) if snecs.has_component(entity, DamageReceived): snecs.remove_component(entity, DamageReceived) # reset ammo if snecs.has_component(entity, RangedAttack): ranged = snecs.entity_component(entity, RangedAttack) ranged.ammo.reset() # reset stats if snecs.has_component(entity, Stats): stats = snecs.entity_component(entity, Stats) for stat_name in stat_attrs: getattr(stats, stat_name).base_value = getattr(self, stat_name) self._align_entity_positions_to_unit()
[docs] def update_position(self): """ Update unit position by averaging the positions of all its entities. """ from nqp.world_elements.entity_components import Position # prevent circular import num_entities = len(self.entities) if num_entities > 0: unit_pos = pygame.Vector2(0, 0) for entity in self.entities: entity_position = snecs.entity_component(entity, Position) unit_pos.x += entity_position.x unit_pos.y += entity_position.y self.pos = pygame.Vector2(unit_pos.x / num_entities, unit_pos.y / num_entities)
[docs] def set_position(self, pos: pygame.Vector2): """ Set the unit's position and moves the Entities to match. """ self.pos = pos self._align_entity_positions_to_unit()
def _align_entity_positions_to_unit(self): from nqp.world_elements.entity_components import Position # prevent circular import unit_x = self.pos.x unit_y = self.pos.y max_spread = self.entity_spread_max for entity in self.entities: # randomise position in allowed area scatter_x = random.randint(-max_spread, max_spread) scatter_y = random.randint(-max_spread, max_spread) position = snecs.entity_component(entity, Position) position.pos = pygame.Vector2(unit_x + scatter_x, unit_y + scatter_y)
[docs] def generate_border_surface(self): """ Generate a new border around the Unit. Stub """ if len(self.entities): # FIXME - throws "ValueError: points argument must contain 2 or more points" and draws incorrectly pass
# surf_padding = 20 # outline_padding = 10 # self.border_surface = None # # # get entity positions # from scripts.core.components import Position # prevent circular import # # all_positions = [] # for entity in self.entities: # position = snecs.entity_component(entity, Position) # all_positions.append(position.pos) # # all_x = [p[0] for p in all_positions] # all_y = [p[1] for p in all_positions] # min_x = min(all_x) # min_y = min(all_y) # self.border_surface = pygame.Surface( # (max(all_x) - min_x + surf_padding * 2, max(all_y) - min_y + surf_padding * 2) # ) # self.border_surface_offset = (self.pos.x - min_x + surf_padding, self.pos.y - min_y + surf_padding) # self.border_surface.set_colorkey((0, 0, 0)) # # points = [ # (self.pos.x - outline_padding, self.pos.y), # (self.pos.x, self.pos.y - outline_padding), # (self.pos.x + outline_padding, self.pos.y), # (self.pos.x, self.pos.y + outline_padding), # ] # # placed_points = [] # # for pos in all_positions + points: # new_pos = (pos[0] - min_x + surf_padding, pos[1] - min_y + surf_padding) # angle = math.atan2(pos[1] - self.pos.y, pos[0] - self.pos.x) # new_pos = ( # new_pos[0] + outline_padding * math.cos(angle), # new_pos[1] + outline_padding * math.sin(angle), # ) # for p in placed_points: # pygame.draw.line(self.border_surface, (255, 255, 255), new_pos, p) # placed_points.append(new_pos) # # mask_surf = pygame.mask.from_surface(self.border_surface) # # self.border_surface.fill((0, 0, 0)) # self.border_surface_outline = self.border_surface.copy() # self.border_surface_outline_black = self.border_surface.copy() # # outline = mask_surf.outline(2) # pygame.draw.lines(self.border_surface_outline, (255, 255, 255), False, outline) # pygame.draw.lines(self.border_surface_outline_black, (0, 0, 1), False, outline) # pygame.draw.polygon(self.border_surface, (0, 0, 255), outline) # self.border_surface.set_alpha(80)
[docs] def add_modifier(self, stat: str, amount: int): """ Add a modifier to a stat. Stub """ # FIXME - stubbed pass
[docs] def get_modified_status(self, stat: str) -> StatModifiedStatus: """ Check if a given stat is modified. Stub. """ # FIXME - stubbed pass