209 lines
7.5 KiB
Python
209 lines
7.5 KiB
Python
"""
|
||
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)"
|
||
)
|