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

209 lines
7.5 KiB
Python
Raw Permalink 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.
"""
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
result["stat_affected"] = self.stat_affected.value if self.stat_affected else None
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)
data["effect_type"] = self.effect_type.value
if self.stat_affected:
data["stat_affected"] = self.stat_affected.value
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."""
if self.effect_type in [EffectType.BUFF, EffectType.DEBUFF]:
return (
f"Effect({self.name}, {self.effect_type.value}, "
f"{self.stat_affected.value if self.stat_affected else 'N/A'} "
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}, {self.effect_type.value}, "
f"power={self.power * self.stacks}, "
f"duration={self.duration}t, stacks={self.stacks}x)"
)