""" Effect system for temporary status modifiers in combat. This module defines the Effect dataclass which represents temporary buffs, debuffs, damage over time, healing over time, stuns, and shields. """ from dataclasses import dataclass, asdict from typing import Dict, Any, Optional from app.models.enums import EffectType, StatType @dataclass class Effect: """ Represents a temporary effect applied to a combatant. Effects are processed at the start of each turn via the tick() method. They can stack up to max_stacks, and duration refreshes on re-application. Attributes: effect_id: Unique identifier for this effect instance name: Display name of the effect effect_type: Type of effect (BUFF, DEBUFF, DOT, HOT, STUN, SHIELD) duration: Turns remaining before effect expires power: Damage/healing per turn OR stat modifier amount stat_affected: Which stat is modified (for BUFF/DEBUFF only) stacks: Number of times this effect has been stacked max_stacks: Maximum number of stacks allowed (default 5) source: Who/what applied this effect (character_id or ability_id) """ effect_id: str name: str effect_type: EffectType duration: int power: int stat_affected: Optional[StatType] = None stacks: int = 1 max_stacks: int = 5 source: str = "" def tick(self) -> Dict[str, Any]: """ Process one turn of this effect. Returns a dictionary describing what happened this turn, including: - effect_name: Name of the effect - effect_type: Type of effect - value: Damage dealt (DOT) or healing done (HOT) - shield_remaining: Current shield strength (SHIELD only) - stunned: True if this is a stun effect (STUN only) - stat_modifier: Amount stats are modified (BUFF/DEBUFF only) - expired: True if effect duration reached 0 Returns: Dictionary with effect processing results """ result = { "effect_name": self.name, "effect_type": self.effect_type.value, "value": 0, "expired": False, } # Process effect based on type if self.effect_type == EffectType.DOT: # Damage over time: deal damage equal to power × stacks result["value"] = self.power * self.stacks result["message"] = f"{self.name} deals {result['value']} damage" elif self.effect_type == EffectType.HOT: # Heal over time: heal equal to power × stacks result["value"] = self.power * self.stacks result["message"] = f"{self.name} heals {result['value']} HP" elif self.effect_type == EffectType.STUN: # Stun: prevents actions this turn result["stunned"] = True result["message"] = f"{self.name} prevents actions" elif self.effect_type == EffectType.SHIELD: # Shield: absorbs damage (power × stacks = shield strength) result["shield_remaining"] = self.power * self.stacks result["message"] = f"{self.name} absorbs up to {result['shield_remaining']} damage" elif self.effect_type in [EffectType.BUFF, EffectType.DEBUFF]: # Buff/Debuff: modify stats # Handle stat_affected being Enum or string if self.stat_affected: stat_value = self.stat_affected.value if hasattr(self.stat_affected, 'value') else self.stat_affected else: stat_value = None result["stat_affected"] = stat_value result["stat_modifier"] = self.power * self.stacks if self.effect_type == EffectType.BUFF: result["message"] = f"{self.name} increases {result['stat_affected']} by {result['stat_modifier']}" else: result["message"] = f"{self.name} decreases {result['stat_affected']} by {result['stat_modifier']}" # Decrease duration self.duration -= 1 if self.duration <= 0: result["expired"] = True result["message"] = f"{self.name} has expired" return result def apply_stack(self, additional_duration: int = 0) -> None: """ Apply an additional stack of this effect. Increases stack count (up to max_stacks) and refreshes duration. If additional_duration is provided, it's added to current duration. Args: additional_duration: Extra turns to add (default 0 = refresh only) """ if self.stacks < self.max_stacks: self.stacks += 1 # Refresh duration or extend it if additional_duration > 0: self.duration = max(self.duration, additional_duration) else: # Find the base duration (current + turns already consumed) # For refresh behavior, we'd need to store original_duration # For now, just use the provided duration pass def reduce_shield(self, damage: int) -> int: """ Reduce shield strength by damage amount. Only applicable for SHIELD effects. Returns remaining damage after shield. Args: damage: Amount of damage to absorb Returns: Remaining damage after shield absorption """ if self.effect_type != EffectType.SHIELD: return damage shield_strength = self.power * self.stacks if damage >= shield_strength: # Shield breaks completely remaining_damage = damage - shield_strength self.power = 0 # Shield depleted self.duration = 0 # Effect expires return remaining_damage else: # Shield partially absorbs damage damage_per_stack = damage / self.stacks self.power = max(0, int(self.power - damage_per_stack)) return 0 def to_dict(self) -> Dict[str, Any]: """ Serialize effect to a dictionary. Returns: Dictionary containing all effect data """ data = asdict(self) # Handle effect_type (could be Enum or string) if hasattr(self.effect_type, 'value'): data["effect_type"] = self.effect_type.value else: data["effect_type"] = self.effect_type # Handle stat_affected (could be Enum, string, or None) if self.stat_affected: if hasattr(self.stat_affected, 'value'): data["stat_affected"] = self.stat_affected.value else: data["stat_affected"] = self.stat_affected return data @classmethod def from_dict(cls, data: Dict[str, Any]) -> 'Effect': """ Deserialize effect from a dictionary. Args: data: Dictionary containing effect data Returns: Effect instance """ # Convert string values back to enums effect_type = EffectType(data["effect_type"]) stat_affected = StatType(data["stat_affected"]) if data.get("stat_affected") else None return cls( effect_id=data["effect_id"], name=data["name"], effect_type=effect_type, duration=data["duration"], power=data["power"], stat_affected=stat_affected, stacks=data.get("stacks", 1), max_stacks=data.get("max_stacks", 5), source=data.get("source", ""), ) def __repr__(self) -> str: """String representation of the effect.""" # Helper to safely get value from Enum or string def safe_value(obj): return obj.value if hasattr(obj, 'value') else obj if self.effect_type in [EffectType.BUFF, EffectType.DEBUFF]: stat_str = safe_value(self.stat_affected) if self.stat_affected else 'N/A' return ( f"Effect({self.name}, {safe_value(self.effect_type)}, " f"{stat_str} " f"{'+' if self.effect_type == EffectType.BUFF else '-'}{self.power * self.stacks}, " f"{self.duration}t, {self.stacks}x)" ) else: return ( f"Effect({self.name}, {safe_value(self.effect_type)}, " f"power={self.power * self.stacks}, " f"duration={self.duration}t, stacks={self.stacks}x)" )