from __future__ import annotations import copy import math from typing import Optional, Tuple, Type, TypeVar, TYPE_CHECKING, Union from render_order import RenderOrder if TYPE_CHECKING: from components.ai import BaseAI from components.consumable import Consumable from components.equippable import Equippable from components.fighter import Fighter from components.inventory import Inventory from components.level import Level from game_map import GameMap T = TypeVar("T", bound="Entity") class Entity: """ A generic object to represent players, enemies, items, etc. """ parent: Union[GameMap, Inventory] def __init__( self, parent: Optional[GameMap] = None, x: int = 0, y: int = 0, char: str = "?", color: Tuple[int, int, int] = (255, 255, 255), name: str = "", blocks_movement: bool = False, render_order: RenderOrder = RenderOrder.CORPSE, ): self.x = x self.y = y self.char = char self.color = color self.name = name self.blocks_movement = blocks_movement self.render_order = render_order if parent: # If gamemap isn't provided now, it will be later. self.parent = parent parent.entities.add(self) @property def gamemap(self) -> GameMap: return self.parent.gamemap def spawn(self: T, gamemap: GameMap, x: int, y: int) -> T: """Spawn a copy of this instance at the given location.""" clone = copy.deepcopy(self) clone.x = x clone.y = y clone.parent = gamemap gamemap.entities.add(clone) return clone def place(self, x: int, y: int, gamemap: Optional[GameMap] = None) -> None: """Place this entity at a new location. Handles moving across GameMaps.""" self.x = x self.y = y if gamemap: if hasattr(self, "parent"): # Possibly uninitialized if self.parent is self.gamemap: self.gamemap.entities.remove(self) self.parent = gamemap gamemap.entities.add(self) def distance(self, x: int, y: int) -> float: """ Return the distance between the current entity and the given (x,y) coordinate. """ return math.sqrt((x - self.x) ** 2 + (y - self.y) ** 2) def move(self, dx: int, dy: int): # Move the entity by a given amount self.x += dx self.y += dy class Actor(Entity): def __init__( self, *, x: int = 0, y: int = 0, char: str = "?", color: Tuple[int, int, int] = (255, 255, 255), name: str = "", ai_cls: Type[BaseAI], fighter: Fighter, inventory: Inventory, level: Level, ): super().__init__( x=x, y=y, char=char, color=color, name=name, blocks_movement=True, render_order=RenderOrder.ACTOR, ) self.ai: Optional[BaseAI] = ai_cls(self) self.fighter = fighter self.fighter.parent = self self.inventory = inventory self.inventory.parent = self self.level = level self.level.parent = self @property def is_alive(self) -> bool: """Returns True as long as this actor can perform actions.""" return bool(self.ai) class Item(Entity): def __init__( self, *, x: int = 0, y: int = 0, char: str = "?", color: Tuple[int, int, int] = (255, 255, 255), name: str = "", consumable: Optional[Consumable] = None, equippable: Optional[Equippable] = None, ): super().__init__( x=x, y=y, char=char, color=color, name=name, blocks_movement=False, render_order=RenderOrder.ITEM, ) self.consumable = consumable if self.consumable: self.consumable.parent = self self.equippable = equippable if self.equippable: self.equippable.parent = self