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