draft tutorial revisions
This commit is contained in:
parent
838da4571d
commit
48359b5a48
70 changed files with 6216 additions and 28 deletions
364
docs/templates/roguelike/entities.py
vendored
Normal file
364
docs/templates/roguelike/entities.py
vendored
Normal file
|
|
@ -0,0 +1,364 @@
|
|||
"""
|
||||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue