- Implement Combat Service - Implement Damage Calculator - Implement Effect Processor - Implement Combat Actions - Created Combat API Endpoints
261 lines
7.7 KiB
Python
261 lines
7.7 KiB
Python
"""
|
|
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
|