first commit
This commit is contained in:
237
api/app/models/abilities.py
Normal file
237
api/app/models/abilities.py
Normal file
@@ -0,0 +1,237 @@
|
||||
"""
|
||||
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()
|
||||
Reference in New Issue
Block a user