McRogueFace/docs/templates/roguelike/entities.py

364 lines
9.3 KiB
Python

"""
entities.py - Entity Management for Roguelike Template
This module provides entity creation and management utilities for the
roguelike template. Entities in McRogueFace are game objects that exist
on a Grid, such as the player, enemies, items, and NPCs.
The module includes:
- Entity factory functions for creating common entity types
- Helper functions for entity management
- Simple data containers for entity stats (for future expansion)
Note: McRogueFace entities are simple position + sprite objects. For
complex game logic like AI, combat, and inventory, you'll want to wrap
them in Python classes that reference the underlying Entity.
"""
from __future__ import annotations
from typing import Tuple, Optional, List, Dict, Any, TYPE_CHECKING
from dataclasses import dataclass
if TYPE_CHECKING:
import mcrfpy
from constants import (
SPRITE_PLAYER, SPRITE_ORC, SPRITE_TROLL, SPRITE_GOBLIN,
COLOR_PLAYER, COLOR_ORC, COLOR_TROLL, COLOR_GOBLIN,
ENEMY_TYPES,
)
@dataclass
class EntityStats:
"""
Optional stats container for game entities.
This dataclass can be used to track stats for entities that need them.
Attach it to your entity wrapper class for combat, leveling, etc.
Attributes:
hp: Current hit points
max_hp: Maximum hit points
power: Attack power
defense: Damage reduction
name: Display name for the entity
"""
hp: int = 10
max_hp: int = 10
power: int = 3
defense: int = 0
name: str = "Unknown"
@property
def is_alive(self) -> bool:
"""Check if the entity is still alive."""
return self.hp > 0
def take_damage(self, amount: int) -> int:
"""
Apply damage, accounting for defense.
Args:
amount: Raw damage amount
Returns:
Actual damage dealt after defense
"""
actual_damage = max(0, amount - self.defense)
self.hp = max(0, self.hp - actual_damage)
return actual_damage
def heal(self, amount: int) -> int:
"""
Heal the entity.
Args:
amount: Amount to heal
Returns:
Actual amount healed (may be less if near max HP)
"""
old_hp = self.hp
self.hp = min(self.max_hp, self.hp + amount)
return self.hp - old_hp
def create_player(
grid: mcrfpy.Grid,
texture: mcrfpy.Texture,
x: int,
y: int
) -> mcrfpy.Entity:
"""
Create and place the player entity on the grid.
The player uses the classic '@' symbol (sprite index 64 in CP437).
Args:
grid: The Grid to place the player on
texture: The texture/tileset to use
x: Starting X position
y: Starting Y position
Returns:
The created player Entity
"""
import mcrfpy
player = mcrfpy.Entity(
pos=(x, y),
texture=texture,
sprite_index=SPRITE_PLAYER
)
grid.entities.append(player)
return player
def create_enemy(
grid: mcrfpy.Grid,
texture: mcrfpy.Texture,
x: int,
y: int,
enemy_type: str = "orc"
) -> Tuple[mcrfpy.Entity, EntityStats]:
"""
Create an enemy entity with associated stats.
Enemy types are defined in constants.py. Currently available:
- "orc": Standard enemy, balanced stats
- "troll": Tough enemy, high HP and power
- "goblin": Weak enemy, low stats
Args:
grid: The Grid to place the enemy on
texture: The texture/tileset to use
x: X position
y: Y position
enemy_type: Key from ENEMY_TYPES dict
Returns:
Tuple of (Entity, EntityStats) for the created enemy
"""
import mcrfpy
# Get enemy definition, default to orc if not found
enemy_def = ENEMY_TYPES.get(enemy_type, ENEMY_TYPES["orc"])
entity = mcrfpy.Entity(
pos=(x, y),
texture=texture,
sprite_index=enemy_def["sprite"]
)
grid.entities.append(entity)
stats = EntityStats(
hp=enemy_def["hp"],
max_hp=enemy_def["hp"],
power=enemy_def["power"],
defense=enemy_def["defense"],
name=enemy_def["name"]
)
return entity, stats
def create_enemies_in_rooms(
grid: mcrfpy.Grid,
texture: mcrfpy.Texture,
rooms: list,
enemies_per_room: int = 2,
skip_first_room: bool = True
) -> List[Tuple[mcrfpy.Entity, EntityStats]]:
"""
Populate dungeon rooms with enemies.
This helper function places random enemies throughout the dungeon,
typically skipping the first room (where the player starts).
Args:
grid: The Grid to populate
texture: The texture/tileset to use
rooms: List of RectangularRoom objects from dungeon generation
enemies_per_room: Maximum enemies to spawn per room
skip_first_room: If True, don't spawn enemies in the first room
Returns:
List of (Entity, EntityStats) tuples for all created enemies
"""
import random
enemies = []
enemy_type_keys = list(ENEMY_TYPES.keys())
# Iterate through rooms, optionally skipping the first
rooms_to_populate = rooms[1:] if skip_first_room else rooms
for room in rooms_to_populate:
# Random number of enemies (0 to enemies_per_room)
num_enemies = random.randint(0, enemies_per_room)
# Get available floor tiles in this room
floor_tiles = list(room.inner_tiles())
for _ in range(num_enemies):
if not floor_tiles:
break
# Pick a random position and remove it from available
pos = random.choice(floor_tiles)
floor_tiles.remove(pos)
# Pick a random enemy type (weighted toward weaker enemies)
if random.random() < 0.8:
enemy_type = "orc" # 80% orcs
else:
enemy_type = "troll" # 20% trolls
x, y = pos
entity, stats = create_enemy(grid, texture, x, y, enemy_type)
enemies.append((entity, stats))
return enemies
def get_blocking_entity_at(
entities: List[mcrfpy.Entity],
x: int,
y: int
) -> Optional[mcrfpy.Entity]:
"""
Check if there's a blocking entity at the given position.
Useful for collision detection - checks if an entity exists at
the target position before moving there.
Args:
entities: List of entities to check
x: X coordinate to check
y: Y coordinate to check
Returns:
The entity at that position, or None if empty
"""
for entity in entities:
if entity.pos[0] == x and entity.pos[1] == y:
return entity
return None
def move_entity(
entity: mcrfpy.Entity,
grid: mcrfpy.Grid,
dx: int,
dy: int,
entities: List[mcrfpy.Entity] = None
) -> bool:
"""
Attempt to move an entity by a delta.
Checks for:
- Grid bounds
- Walkable terrain
- Other blocking entities (if entities list provided)
Args:
entity: The entity to move
grid: The grid for terrain collision
dx: Delta X (-1, 0, or 1 typically)
dy: Delta Y (-1, 0, or 1 typically)
entities: Optional list of entities to check for collision
Returns:
True if movement succeeded, False otherwise
"""
dest_x = entity.pos[0] + dx
dest_y = entity.pos[1] + dy
# Check grid bounds
grid_width, grid_height = grid.grid_size
if not (0 <= dest_x < grid_width and 0 <= dest_y < grid_height):
return False
# Check if tile is walkable
if not grid.at(dest_x, dest_y).walkable:
return False
# Check for blocking entities
if entities and get_blocking_entity_at(entities, dest_x, dest_y):
return False
# Move is valid
entity.pos = (dest_x, dest_y)
return True
def distance_between(
entity1: mcrfpy.Entity,
entity2: mcrfpy.Entity
) -> float:
"""
Calculate the Chebyshev distance between two entities.
Chebyshev distance (also called chessboard distance) counts
diagonal moves as 1, which is standard for roguelikes.
Args:
entity1: First entity
entity2: Second entity
Returns:
Distance in tiles (diagonal = 1)
"""
dx = abs(entity1.pos[0] - entity2.pos[0])
dy = abs(entity1.pos[1] - entity2.pos[1])
return max(dx, dy)
def entities_in_radius(
center: mcrfpy.Entity,
entities: List[mcrfpy.Entity],
radius: float
) -> List[mcrfpy.Entity]:
"""
Find all entities within a given radius of a center entity.
Uses Chebyshev distance for roguelike-style radius.
Args:
center: The entity to search around
entities: List of entities to check
radius: Maximum distance in tiles
Returns:
List of entities within the radius (excluding center)
"""
nearby = []
for entity in entities:
if entity is not center:
if distance_between(center, entity) <= radius:
nearby.append(entity)
return nearby
def remove_entity(
entity: mcrfpy.Entity,
grid: mcrfpy.Grid
) -> bool:
"""
Remove an entity from a grid.
Args:
entity: The entity to remove
grid: The grid containing the entity
Returns:
True if removal succeeded, False otherwise
"""
try:
idx = entity.index()
grid.entities.remove(idx)
return True
except (ValueError, AttributeError):
return False