Files
Code_of_Conquest/api/app/models/abilities.py
2025-11-24 23:10:55 -06:00

238 lines
7.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Ability system for combat actions and spells.
This module defines abilities (attacks, spells, skills) that can be used in combat.
Abilities are loaded from YAML configuration files for data-driven design.
"""
from dataclasses import dataclass, field, asdict
from typing import Dict, Any, List, Optional
import yaml
import os
from pathlib import Path
from app.models.enums import AbilityType, DamageType, EffectType, StatType
from app.models.effects import Effect
from app.models.stats import Stats
@dataclass
class Ability:
"""
Represents an action that can be taken in combat.
Abilities can deal damage, apply effects, heal, or perform other actions.
They are loaded from YAML files for easy game design iteration.
Attributes:
ability_id: Unique identifier
name: Display name
description: What the ability does
ability_type: Category (attack, spell, skill, etc.)
base_power: Base damage or healing value
damage_type: Type of damage dealt (physical, fire, etc.)
scaling_stat: Which stat scales this ability's power (if any)
scaling_factor: Multiplier for scaling stat (default 0.5)
mana_cost: MP required to use this ability
cooldown: Turns before ability can be used again
effects_applied: List of effects applied to target on hit
is_aoe: Whether this affects multiple targets
target_count: Number of targets if AoE (0 = all)
"""
ability_id: str
name: str
description: str
ability_type: AbilityType
base_power: int = 0
damage_type: Optional[DamageType] = None
scaling_stat: Optional[StatType] = None
scaling_factor: float = 0.5
mana_cost: int = 0
cooldown: int = 0
effects_applied: List[Effect] = field(default_factory=list)
is_aoe: bool = False
target_count: int = 1
def calculate_power(self, caster_stats: Stats) -> int:
"""
Calculate final power based on caster's stats.
Formula: base_power + (scaling_stat × scaling_factor)
Minimum power is always 1.
Args:
caster_stats: The caster's effective stats
Returns:
Final power value for damage or healing
"""
power = self.base_power
if self.scaling_stat:
stat_value = getattr(caster_stats, self.scaling_stat.value)
power += int(stat_value * self.scaling_factor)
return max(1, power)
def get_effects_to_apply(self) -> List[Effect]:
"""
Get a copy of effects that should be applied to target(s).
Creates new Effect instances to avoid sharing references.
Returns:
List of Effect instances to apply
"""
return [
Effect(
effect_id=f"{self.ability_id}_{effect.name}_{id(effect)}",
name=effect.name,
effect_type=effect.effect_type,
duration=effect.duration,
power=effect.power,
stat_affected=effect.stat_affected,
stacks=effect.stacks,
max_stacks=effect.max_stacks,
source=self.ability_id,
)
for effect in self.effects_applied
]
def to_dict(self) -> Dict[str, Any]:
"""
Serialize ability to a dictionary.
Returns:
Dictionary containing all ability data
"""
data = asdict(self)
data["ability_type"] = self.ability_type.value
if self.damage_type:
data["damage_type"] = self.damage_type.value
if self.scaling_stat:
data["scaling_stat"] = self.scaling_stat.value
data["effects_applied"] = [effect.to_dict() for effect in self.effects_applied]
return data
@classmethod
def from_dict(cls, data: Dict[str, Any]) -> 'Ability':
"""
Deserialize ability from a dictionary.
Args:
data: Dictionary containing ability data
Returns:
Ability instance
"""
# Convert string values back to enums
ability_type = AbilityType(data["ability_type"])
damage_type = DamageType(data["damage_type"]) if data.get("damage_type") else None
scaling_stat = StatType(data["scaling_stat"]) if data.get("scaling_stat") else None
# Deserialize effects
effects = []
if "effects_applied" in data and data["effects_applied"]:
effects = [Effect.from_dict(e) for e in data["effects_applied"]]
return cls(
ability_id=data["ability_id"],
name=data["name"],
description=data["description"],
ability_type=ability_type,
base_power=data.get("base_power", 0),
damage_type=damage_type,
scaling_stat=scaling_stat,
scaling_factor=data.get("scaling_factor", 0.5),
mana_cost=data.get("mana_cost", 0),
cooldown=data.get("cooldown", 0),
effects_applied=effects,
is_aoe=data.get("is_aoe", False),
target_count=data.get("target_count", 1),
)
def __repr__(self) -> str:
"""String representation of the ability."""
return (
f"Ability({self.name}, {self.ability_type.value}, "
f"power={self.base_power}, cost={self.mana_cost}MP, "
f"cooldown={self.cooldown}t)"
)
class AbilityLoader:
"""
Loads abilities from YAML configuration files.
This allows game designers to define abilities without touching code.
"""
def __init__(self, data_dir: Optional[str] = None):
"""
Initialize the ability loader.
Args:
data_dir: Path to directory containing ability YAML files
Defaults to /app/data/abilities/
"""
if data_dir is None:
# Default to app/data/abilities relative to this file
current_file = Path(__file__)
app_dir = current_file.parent.parent # Go up to /app
data_dir = str(app_dir / "data" / "abilities")
self.data_dir = Path(data_dir)
self._ability_cache: Dict[str, Ability] = {}
def load_ability(self, ability_id: str) -> Optional[Ability]:
"""
Load a single ability by ID.
Args:
ability_id: Unique ability identifier
Returns:
Ability instance or None if not found
"""
# Check cache first
if ability_id in self._ability_cache:
return self._ability_cache[ability_id]
# Load from YAML file
yaml_file = self.data_dir / f"{ability_id}.yaml"
if not yaml_file.exists():
return None
with open(yaml_file, 'r') as f:
data = yaml.safe_load(f)
ability = Ability.from_dict(data)
self._ability_cache[ability_id] = ability
return ability
def load_all_abilities(self) -> Dict[str, Ability]:
"""
Load all abilities from the data directory.
Returns:
Dictionary mapping ability_id to Ability instance
"""
if not self.data_dir.exists():
return {}
abilities = {}
for yaml_file in self.data_dir.glob("*.yaml"):
with open(yaml_file, 'r') as f:
data = yaml.safe_load(f)
ability = Ability.from_dict(data)
abilities[ability.ability_id] = ability
self._ability_cache[ability.ability_id] = ability
return abilities
def clear_cache(self) -> None:
"""Clear the ability cache, forcing reload on next access."""
self._ability_cache.clear()