319 lines
8.2 KiB
Python
319 lines
8.2 KiB
Python
|
|
"""
|
||
|
|
entities.py - Player and Enemy Entity Definitions
|
||
|
|
|
||
|
|
Defines the game actors with stats, rendering, and basic behaviors.
|
||
|
|
Uses composition with McRogueFace Entity objects for rendering.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from dataclasses import dataclass, field
|
||
|
|
from typing import Optional, List, Tuple, TYPE_CHECKING
|
||
|
|
import mcrfpy
|
||
|
|
|
||
|
|
from constants import (
|
||
|
|
PLAYER_START_HP, PLAYER_START_ATTACK, PLAYER_START_DEFENSE,
|
||
|
|
SPRITE_PLAYER, ENEMY_STATS, FOV_RADIUS
|
||
|
|
)
|
||
|
|
|
||
|
|
if TYPE_CHECKING:
|
||
|
|
from dungeon import Dungeon
|
||
|
|
|
||
|
|
|
||
|
|
@dataclass
|
||
|
|
class Fighter:
|
||
|
|
"""
|
||
|
|
Combat statistics component for entities that can fight.
|
||
|
|
|
||
|
|
Attributes:
|
||
|
|
hp: Current hit points
|
||
|
|
max_hp: Maximum hit points
|
||
|
|
attack: Attack power
|
||
|
|
defense: Damage reduction
|
||
|
|
"""
|
||
|
|
hp: int
|
||
|
|
max_hp: int
|
||
|
|
attack: int
|
||
|
|
defense: int
|
||
|
|
|
||
|
|
@property
|
||
|
|
def is_alive(self) -> bool:
|
||
|
|
"""Check if this fighter is still alive."""
|
||
|
|
return self.hp > 0
|
||
|
|
|
||
|
|
@property
|
||
|
|
def hp_percent(self) -> float:
|
||
|
|
"""Return HP as a percentage (0.0 to 1.0)."""
|
||
|
|
if self.max_hp <= 0:
|
||
|
|
return 0.0
|
||
|
|
return self.hp / self.max_hp
|
||
|
|
|
||
|
|
def heal(self, amount: int) -> int:
|
||
|
|
"""
|
||
|
|
Heal by the given amount, up to max_hp.
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
The actual amount healed.
|
||
|
|
"""
|
||
|
|
old_hp = self.hp
|
||
|
|
self.hp = min(self.hp + amount, self.max_hp)
|
||
|
|
return self.hp - old_hp
|
||
|
|
|
||
|
|
def take_damage(self, amount: int) -> int:
|
||
|
|
"""
|
||
|
|
Take damage, reduced by defense.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
amount: Raw damage before defense calculation
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
The actual damage taken after defense.
|
||
|
|
"""
|
||
|
|
# Defense reduces damage, minimum 0
|
||
|
|
actual_damage = max(0, amount - self.defense)
|
||
|
|
self.hp = max(0, self.hp - actual_damage)
|
||
|
|
return actual_damage
|
||
|
|
|
||
|
|
|
||
|
|
class Actor:
|
||
|
|
"""
|
||
|
|
Base class for all game actors (player and enemies).
|
||
|
|
|
||
|
|
Wraps a McRogueFace Entity and adds game logic.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, x: int, y: int, sprite: int, name: str,
|
||
|
|
texture: mcrfpy.Texture, grid: mcrfpy.Grid,
|
||
|
|
fighter: Fighter):
|
||
|
|
"""
|
||
|
|
Create a new actor.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
x: Starting X position
|
||
|
|
y: Starting Y position
|
||
|
|
sprite: Sprite index for rendering
|
||
|
|
name: Display name of this actor
|
||
|
|
texture: Texture for the entity sprite
|
||
|
|
grid: Grid to add the entity to
|
||
|
|
fighter: Combat statistics
|
||
|
|
"""
|
||
|
|
self.name = name
|
||
|
|
self.fighter = fighter
|
||
|
|
self.grid = grid
|
||
|
|
self._x = x
|
||
|
|
self._y = y
|
||
|
|
|
||
|
|
# Create the McRogueFace entity
|
||
|
|
self.entity = mcrfpy.Entity((x, y), texture, sprite)
|
||
|
|
grid.entities.append(self.entity)
|
||
|
|
|
||
|
|
@property
|
||
|
|
def x(self) -> int:
|
||
|
|
return self._x
|
||
|
|
|
||
|
|
@x.setter
|
||
|
|
def x(self, value: int) -> None:
|
||
|
|
self._x = value
|
||
|
|
self.entity.pos = (value, self._y)
|
||
|
|
|
||
|
|
@property
|
||
|
|
def y(self) -> int:
|
||
|
|
return self._y
|
||
|
|
|
||
|
|
@y.setter
|
||
|
|
def y(self, value: int) -> None:
|
||
|
|
self._y = value
|
||
|
|
self.entity.pos = (self._x, value)
|
||
|
|
|
||
|
|
@property
|
||
|
|
def pos(self) -> Tuple[int, int]:
|
||
|
|
return (self._x, self._y)
|
||
|
|
|
||
|
|
@pos.setter
|
||
|
|
def pos(self, value: Tuple[int, int]) -> None:
|
||
|
|
self._x, self._y = value
|
||
|
|
self.entity.pos = value
|
||
|
|
|
||
|
|
@property
|
||
|
|
def is_alive(self) -> bool:
|
||
|
|
return self.fighter.is_alive
|
||
|
|
|
||
|
|
def move(self, dx: int, dy: int) -> None:
|
||
|
|
"""Move by the given delta."""
|
||
|
|
self.x += dx
|
||
|
|
self.y += dy
|
||
|
|
|
||
|
|
def move_to(self, x: int, y: int) -> None:
|
||
|
|
"""Move to an absolute position."""
|
||
|
|
self.pos = (x, y)
|
||
|
|
|
||
|
|
def distance_to(self, other: 'Actor') -> int:
|
||
|
|
"""Calculate Manhattan distance to another actor."""
|
||
|
|
return abs(self.x - other.x) + abs(self.y - other.y)
|
||
|
|
|
||
|
|
def remove(self) -> None:
|
||
|
|
"""Remove this actor's entity from the grid."""
|
||
|
|
try:
|
||
|
|
idx = self.entity.index()
|
||
|
|
self.grid.entities.remove(idx)
|
||
|
|
except (ValueError, RuntimeError):
|
||
|
|
pass # Already removed
|
||
|
|
|
||
|
|
|
||
|
|
class Player(Actor):
|
||
|
|
"""
|
||
|
|
The player character with additional player-specific functionality.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, x: int, y: int, texture: mcrfpy.Texture,
|
||
|
|
grid: mcrfpy.Grid):
|
||
|
|
fighter = Fighter(
|
||
|
|
hp=PLAYER_START_HP,
|
||
|
|
max_hp=PLAYER_START_HP,
|
||
|
|
attack=PLAYER_START_ATTACK,
|
||
|
|
defense=PLAYER_START_DEFENSE
|
||
|
|
)
|
||
|
|
super().__init__(
|
||
|
|
x=x, y=y,
|
||
|
|
sprite=SPRITE_PLAYER,
|
||
|
|
name="Player",
|
||
|
|
texture=texture,
|
||
|
|
grid=grid,
|
||
|
|
fighter=fighter
|
||
|
|
)
|
||
|
|
self.xp = 0
|
||
|
|
self.level = 1
|
||
|
|
self.dungeon_level = 1
|
||
|
|
|
||
|
|
def gain_xp(self, amount: int) -> bool:
|
||
|
|
"""
|
||
|
|
Gain experience points.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
amount: XP to gain
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
True if the player leveled up
|
||
|
|
"""
|
||
|
|
self.xp += amount
|
||
|
|
xp_to_level = self.xp_for_next_level
|
||
|
|
|
||
|
|
if self.xp >= xp_to_level:
|
||
|
|
self.level_up()
|
||
|
|
return True
|
||
|
|
return False
|
||
|
|
|
||
|
|
@property
|
||
|
|
def xp_for_next_level(self) -> int:
|
||
|
|
"""XP required for the next level."""
|
||
|
|
return self.level * 100
|
||
|
|
|
||
|
|
def level_up(self) -> None:
|
||
|
|
"""Level up the player, improving stats."""
|
||
|
|
self.level += 1
|
||
|
|
|
||
|
|
# Improve stats
|
||
|
|
hp_increase = 5
|
||
|
|
attack_increase = 1
|
||
|
|
defense_increase = 1 if self.level % 3 == 0 else 0
|
||
|
|
|
||
|
|
self.fighter.max_hp += hp_increase
|
||
|
|
self.fighter.hp += hp_increase # Heal the increase amount
|
||
|
|
self.fighter.attack += attack_increase
|
||
|
|
self.fighter.defense += defense_increase
|
||
|
|
|
||
|
|
def update_fov(self, dungeon: 'Dungeon') -> None:
|
||
|
|
"""
|
||
|
|
Update field of view based on player position.
|
||
|
|
|
||
|
|
Uses entity.update_visibility() for TCOD FOV calculation.
|
||
|
|
"""
|
||
|
|
# Update the entity's visibility data
|
||
|
|
self.entity.update_visibility()
|
||
|
|
|
||
|
|
# Apply FOV to dungeon tiles
|
||
|
|
for x in range(dungeon.width):
|
||
|
|
for y in range(dungeon.height):
|
||
|
|
state = self.entity.at(x, y)
|
||
|
|
tile = dungeon.get_tile(x, y)
|
||
|
|
|
||
|
|
if tile:
|
||
|
|
tile.visible = state.visible
|
||
|
|
if state.visible:
|
||
|
|
tile.explored = True
|
||
|
|
|
||
|
|
|
||
|
|
class Enemy(Actor):
|
||
|
|
"""
|
||
|
|
An enemy actor with AI behavior.
|
||
|
|
"""
|
||
|
|
|
||
|
|
def __init__(self, x: int, y: int, enemy_type: str,
|
||
|
|
texture: mcrfpy.Texture, grid: mcrfpy.Grid):
|
||
|
|
"""
|
||
|
|
Create a new enemy.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
x: Starting X position
|
||
|
|
y: Starting Y position
|
||
|
|
enemy_type: Key into ENEMY_STATS dictionary
|
||
|
|
texture: Texture for the entity sprite
|
||
|
|
grid: Grid to add the entity to
|
||
|
|
"""
|
||
|
|
stats = ENEMY_STATS.get(enemy_type, ENEMY_STATS['goblin'])
|
||
|
|
|
||
|
|
fighter = Fighter(
|
||
|
|
hp=stats['hp'],
|
||
|
|
max_hp=stats['hp'],
|
||
|
|
attack=stats['attack'],
|
||
|
|
defense=stats['defense']
|
||
|
|
)
|
||
|
|
|
||
|
|
super().__init__(
|
||
|
|
x=x, y=y,
|
||
|
|
sprite=stats['sprite'],
|
||
|
|
name=stats['name'],
|
||
|
|
texture=texture,
|
||
|
|
grid=grid,
|
||
|
|
fighter=fighter
|
||
|
|
)
|
||
|
|
|
||
|
|
self.enemy_type = enemy_type
|
||
|
|
self.xp_reward = stats['xp']
|
||
|
|
|
||
|
|
# AI state
|
||
|
|
self.target: Optional[Actor] = None
|
||
|
|
self.path: List[Tuple[int, int]] = []
|
||
|
|
|
||
|
|
|
||
|
|
def create_player(x: int, y: int, texture: mcrfpy.Texture,
|
||
|
|
grid: mcrfpy.Grid) -> Player:
|
||
|
|
"""
|
||
|
|
Factory function to create the player.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
x: Starting X position
|
||
|
|
y: Starting Y position
|
||
|
|
texture: Texture for player sprite
|
||
|
|
grid: Grid to add player to
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
A new Player instance
|
||
|
|
"""
|
||
|
|
return Player(x, y, texture, grid)
|
||
|
|
|
||
|
|
|
||
|
|
def create_enemy(x: int, y: int, enemy_type: str,
|
||
|
|
texture: mcrfpy.Texture, grid: mcrfpy.Grid) -> Enemy:
|
||
|
|
"""
|
||
|
|
Factory function to create an enemy.
|
||
|
|
|
||
|
|
Args:
|
||
|
|
x: Starting X position
|
||
|
|
y: Starting Y position
|
||
|
|
enemy_type: Type of enemy ('goblin', 'orc', 'troll')
|
||
|
|
texture: Texture for enemy sprite
|
||
|
|
grid: Grid to add enemy to
|
||
|
|
|
||
|
|
Returns:
|
||
|
|
A new Enemy instance
|
||
|
|
"""
|
||
|
|
return Enemy(x, y, enemy_type, texture, grid)
|