Combat Backend & Data Models

- Implement Combat Service
- Implement Damage Calculator
- Implement Effect Processor
- Implement Combat Actions
- Created Combat API Endpoints
This commit is contained in:
2025-11-26 15:43:20 -06:00
parent 30c3b800e6
commit 03ab783eeb
22 changed files with 9091 additions and 5 deletions

View File

@@ -0,0 +1,260 @@
"""
Enemy Loader Service - YAML-based enemy template loading.
This service loads enemy definitions from YAML files, providing a data-driven
approach to defining monsters and enemies for combat encounters.
"""
from pathlib import Path
from typing import Dict, List, Optional
import yaml
from app.models.enemy import EnemyTemplate, EnemyDifficulty
from app.utils.logging import get_logger
logger = get_logger(__file__)
class EnemyLoader:
"""
Loads enemy templates from YAML configuration files.
This allows game designers to define enemies without touching code.
Enemy files are organized by difficulty in subdirectories.
"""
def __init__(self, data_dir: Optional[str] = None):
"""
Initialize the enemy loader.
Args:
data_dir: Path to directory containing enemy YAML files
Defaults to /app/data/enemies/
"""
if data_dir is None:
# Default to app/data/enemies relative to this file
current_file = Path(__file__)
app_dir = current_file.parent.parent # Go up to /app
data_dir = str(app_dir / "data" / "enemies")
self.data_dir = Path(data_dir)
self._enemy_cache: Dict[str, EnemyTemplate] = {}
self._loaded = False
logger.info("EnemyLoader initialized", data_dir=str(self.data_dir))
def load_enemy(self, enemy_id: str) -> Optional[EnemyTemplate]:
"""
Load a single enemy template by ID.
Args:
enemy_id: Unique enemy identifier
Returns:
EnemyTemplate instance or None if not found
"""
# Check cache first
if enemy_id in self._enemy_cache:
return self._enemy_cache[enemy_id]
# If not cached, try loading all enemies first
if not self._loaded:
self.load_all_enemies()
if enemy_id in self._enemy_cache:
return self._enemy_cache[enemy_id]
# Try loading from specific YAML file
yaml_file = self.data_dir / f"{enemy_id}.yaml"
if yaml_file.exists():
return self._load_from_file(yaml_file)
# Search in subdirectories
for subdir in self.data_dir.iterdir():
if subdir.is_dir():
yaml_file = subdir / f"{enemy_id}.yaml"
if yaml_file.exists():
return self._load_from_file(yaml_file)
logger.warning("Enemy not found", enemy_id=enemy_id)
return None
def _load_from_file(self, yaml_file: Path) -> Optional[EnemyTemplate]:
"""
Load an enemy template from a specific YAML file.
Args:
yaml_file: Path to the YAML file
Returns:
EnemyTemplate instance or None on error
"""
try:
with open(yaml_file, 'r') as f:
data = yaml.safe_load(f)
enemy = EnemyTemplate.from_dict(data)
self._enemy_cache[enemy.enemy_id] = enemy
logger.debug("Enemy loaded", enemy_id=enemy.enemy_id, file=str(yaml_file))
return enemy
except Exception as e:
logger.error("Failed to load enemy file",
file=str(yaml_file),
error=str(e))
return None
def load_all_enemies(self) -> Dict[str, EnemyTemplate]:
"""
Load all enemy templates from the data directory.
Searches both the root directory and subdirectories for YAML files.
Returns:
Dictionary mapping enemy_id to EnemyTemplate instance
"""
if not self.data_dir.exists():
logger.warning("Enemy data directory not found", path=str(self.data_dir))
return {}
enemies = {}
# Load from root directory
for yaml_file in self.data_dir.glob("*.yaml"):
enemy = self._load_from_file(yaml_file)
if enemy:
enemies[enemy.enemy_id] = enemy
# Load from subdirectories (organized by difficulty)
for subdir in self.data_dir.iterdir():
if subdir.is_dir():
for yaml_file in subdir.glob("*.yaml"):
enemy = self._load_from_file(yaml_file)
if enemy:
enemies[enemy.enemy_id] = enemy
self._loaded = True
logger.info("All enemies loaded", count=len(enemies))
return enemies
def get_enemies_by_difficulty(
self,
difficulty: EnemyDifficulty
) -> List[EnemyTemplate]:
"""
Get all enemies matching a difficulty level.
Args:
difficulty: Difficulty level to filter by
Returns:
List of EnemyTemplate instances
"""
if not self._loaded:
self.load_all_enemies()
return [
enemy for enemy in self._enemy_cache.values()
if enemy.difficulty == difficulty
]
def get_enemies_by_tag(self, tag: str) -> List[EnemyTemplate]:
"""
Get all enemies with a specific tag.
Args:
tag: Tag to filter by (e.g., "undead", "beast", "humanoid")
Returns:
List of EnemyTemplate instances with that tag
"""
if not self._loaded:
self.load_all_enemies()
return [
enemy for enemy in self._enemy_cache.values()
if enemy.has_tag(tag)
]
def get_random_enemies(
self,
count: int = 1,
difficulty: Optional[EnemyDifficulty] = None,
tag: Optional[str] = None,
exclude_bosses: bool = True
) -> List[EnemyTemplate]:
"""
Get random enemies for encounter generation.
Args:
count: Number of enemies to select
difficulty: Optional difficulty filter
tag: Optional tag filter
exclude_bosses: Whether to exclude boss enemies
Returns:
List of randomly selected EnemyTemplate instances
"""
import random
if not self._loaded:
self.load_all_enemies()
# Build candidate list
candidates = list(self._enemy_cache.values())
# Apply filters
if difficulty:
candidates = [e for e in candidates if e.difficulty == difficulty]
if tag:
candidates = [e for e in candidates if e.has_tag(tag)]
if exclude_bosses:
candidates = [e for e in candidates if not e.is_boss()]
if not candidates:
logger.warning("No enemies match filters",
difficulty=difficulty.value if difficulty else None,
tag=tag)
return []
# Select random enemies (with replacement if needed)
if len(candidates) >= count:
return random.sample(candidates, count)
else:
# Not enough unique enemies, allow duplicates
return random.choices(candidates, k=count)
def clear_cache(self) -> None:
"""Clear the enemy cache, forcing reload on next access."""
self._enemy_cache.clear()
self._loaded = False
logger.debug("Enemy cache cleared")
def get_all_cached(self) -> Dict[str, EnemyTemplate]:
"""
Get all cached enemies.
Returns:
Dictionary of cached enemy templates
"""
if not self._loaded:
self.load_all_enemies()
return self._enemy_cache.copy()
# Global instance for convenience
_loader_instance: Optional[EnemyLoader] = None
def get_enemy_loader() -> EnemyLoader:
"""
Get the global EnemyLoader instance.
Returns:
Singleton EnemyLoader instance
"""
global _loader_instance
if _loader_instance is None:
_loader_instance = EnemyLoader()
return _loader_instance