362 lines
8.8 KiB
Python
362 lines
8.8 KiB
Python
"""
|
||
Unit tests for Effect dataclass.
|
||
|
||
Tests all effect types, tick() method, stacking, and serialization.
|
||
"""
|
||
|
||
import pytest
|
||
from app.models.effects import Effect
|
||
from app.models.enums import EffectType, StatType
|
||
|
||
|
||
def test_effect_creation():
|
||
"""Test creating an Effect instance."""
|
||
effect = Effect(
|
||
effect_id="burn_1",
|
||
name="Burning",
|
||
effect_type=EffectType.DOT,
|
||
duration=3,
|
||
power=5,
|
||
)
|
||
|
||
assert effect.effect_id == "burn_1"
|
||
assert effect.name == "Burning"
|
||
assert effect.effect_type == EffectType.DOT
|
||
assert effect.duration == 3
|
||
assert effect.power == 5
|
||
assert effect.stacks == 1
|
||
assert effect.max_stacks == 5
|
||
|
||
|
||
def test_dot_effect_tick():
|
||
"""Test DOT (damage over time) effect ticking."""
|
||
effect = Effect(
|
||
effect_id="poison_1",
|
||
name="Poisoned",
|
||
effect_type=EffectType.DOT,
|
||
duration=3,
|
||
power=10,
|
||
stacks=2,
|
||
)
|
||
|
||
result = effect.tick()
|
||
|
||
assert result["effect_type"] == "dot"
|
||
assert result["value"] == 20 # 10 power × 2 stacks
|
||
assert result["expired"] == False
|
||
assert effect.duration == 2 # Reduced by 1
|
||
|
||
|
||
def test_hot_effect_tick():
|
||
"""Test HOT (heal over time) effect ticking."""
|
||
effect = Effect(
|
||
effect_id="regen_1",
|
||
name="Regeneration",
|
||
effect_type=EffectType.HOT,
|
||
duration=5,
|
||
power=8,
|
||
stacks=1,
|
||
)
|
||
|
||
result = effect.tick()
|
||
|
||
assert result["effect_type"] == "hot"
|
||
assert result["value"] == 8 # 8 power × 1 stack
|
||
assert result["expired"] == False
|
||
assert effect.duration == 4
|
||
|
||
|
||
def test_stun_effect_tick():
|
||
"""Test STUN effect ticking."""
|
||
effect = Effect(
|
||
effect_id="stun_1",
|
||
name="Stunned",
|
||
effect_type=EffectType.STUN,
|
||
duration=1,
|
||
power=0,
|
||
)
|
||
|
||
result = effect.tick()
|
||
|
||
assert result["effect_type"] == "stun"
|
||
assert result.get("stunned") == True
|
||
assert effect.duration == 0
|
||
|
||
|
||
def test_shield_effect_tick():
|
||
"""Test SHIELD effect ticking."""
|
||
effect = Effect(
|
||
effect_id="shield_1",
|
||
name="Shield",
|
||
effect_type=EffectType.SHIELD,
|
||
duration=3,
|
||
power=50,
|
||
)
|
||
|
||
result = effect.tick()
|
||
|
||
assert result["effect_type"] == "shield"
|
||
assert result["shield_remaining"] == 50
|
||
assert effect.duration == 2
|
||
|
||
|
||
def test_buff_effect_tick():
|
||
"""Test BUFF effect ticking."""
|
||
effect = Effect(
|
||
effect_id="str_buff_1",
|
||
name="Strength Boost",
|
||
effect_type=EffectType.BUFF,
|
||
duration=4,
|
||
power=5,
|
||
stat_affected=StatType.STRENGTH,
|
||
stacks=2,
|
||
)
|
||
|
||
result = effect.tick()
|
||
|
||
assert result["effect_type"] == "buff"
|
||
assert result["stat_affected"] == "strength"
|
||
assert result["stat_modifier"] == 10 # 5 power × 2 stacks
|
||
assert effect.duration == 3
|
||
|
||
|
||
def test_debuff_effect_tick():
|
||
"""Test DEBUFF effect ticking."""
|
||
effect = Effect(
|
||
effect_id="weak_1",
|
||
name="Weakened",
|
||
effect_type=EffectType.DEBUFF,
|
||
duration=2,
|
||
power=3,
|
||
stat_affected=StatType.STRENGTH,
|
||
)
|
||
|
||
result = effect.tick()
|
||
|
||
assert result["effect_type"] == "debuff"
|
||
assert result["stat_affected"] == "strength"
|
||
assert result["stat_modifier"] == 3
|
||
assert effect.duration == 1
|
||
|
||
|
||
def test_effect_expiration():
|
||
"""Test that effect expires when duration reaches 0."""
|
||
effect = Effect(
|
||
effect_id="burn_1",
|
||
name="Burning",
|
||
effect_type=EffectType.DOT,
|
||
duration=1,
|
||
power=5,
|
||
)
|
||
|
||
result = effect.tick()
|
||
|
||
assert result["expired"] == True
|
||
assert effect.duration == 0
|
||
|
||
|
||
def test_effect_stacking():
|
||
"""Test apply_stack() increases stacks up to max."""
|
||
effect = Effect(
|
||
effect_id="poison_1",
|
||
name="Poison",
|
||
effect_type=EffectType.DOT,
|
||
duration=3,
|
||
power=5,
|
||
max_stacks=5,
|
||
)
|
||
|
||
assert effect.stacks == 1
|
||
|
||
effect.apply_stack()
|
||
assert effect.stacks == 2
|
||
|
||
effect.apply_stack()
|
||
assert effect.stacks == 3
|
||
|
||
# Apply 3 more to reach max
|
||
effect.apply_stack()
|
||
effect.apply_stack()
|
||
effect.apply_stack()
|
||
assert effect.stacks == 5 # Capped at max_stacks
|
||
|
||
# Try to apply one more - should still be 5
|
||
effect.apply_stack()
|
||
assert effect.stacks == 5
|
||
|
||
|
||
def test_shield_damage_absorption():
|
||
"""Test reduce_shield() method."""
|
||
effect = Effect(
|
||
effect_id="shield_1",
|
||
name="Shield",
|
||
effect_type=EffectType.SHIELD,
|
||
duration=3,
|
||
power=50,
|
||
stacks=1,
|
||
)
|
||
|
||
# Shield absorbs 20 damage
|
||
remaining = effect.reduce_shield(20)
|
||
assert remaining == 0 # All damage absorbed
|
||
assert effect.power == 30 # Shield reduced by 20
|
||
|
||
# Shield absorbs 20 more
|
||
remaining = effect.reduce_shield(20)
|
||
assert remaining == 0
|
||
assert effect.power == 10
|
||
|
||
# Shield takes 15 damage, breaks completely
|
||
remaining = effect.reduce_shield(15)
|
||
assert remaining == 5 # 5 damage passes through
|
||
assert effect.power == 0
|
||
assert effect.duration == 0 # Effect expires
|
||
|
||
|
||
def test_shield_with_stacks():
|
||
"""Test shield absorption with multiple stacks."""
|
||
effect = Effect(
|
||
effect_id="shield_1",
|
||
name="Shield",
|
||
effect_type=EffectType.SHIELD,
|
||
duration=3,
|
||
power=20,
|
||
stacks=3,
|
||
)
|
||
|
||
# Total shield = 20 × 3 = 60
|
||
# Apply 50 damage
|
||
remaining = effect.reduce_shield(50)
|
||
assert remaining == 0 # All absorbed
|
||
# Shield reduced: 50 / 3 stacks = 16.67 per stack
|
||
assert effect.power < 20 # Power reduced
|
||
|
||
|
||
def test_effect_serialization():
|
||
"""Test to_dict() serialization."""
|
||
effect = Effect(
|
||
effect_id="burn_1",
|
||
name="Burning",
|
||
effect_type=EffectType.DOT,
|
||
duration=3,
|
||
power=5,
|
||
stacks=2,
|
||
max_stacks=5,
|
||
source="fireball",
|
||
)
|
||
|
||
data = effect.to_dict()
|
||
|
||
assert data["effect_id"] == "burn_1"
|
||
assert data["name"] == "Burning"
|
||
assert data["effect_type"] == "dot"
|
||
assert data["duration"] == 3
|
||
assert data["power"] == 5
|
||
assert data["stacks"] == 2
|
||
assert data["max_stacks"] == 5
|
||
assert data["source"] == "fireball"
|
||
|
||
|
||
def test_effect_deserialization():
|
||
"""Test from_dict() deserialization."""
|
||
data = {
|
||
"effect_id": "regen_1",
|
||
"name": "Regeneration",
|
||
"effect_type": "hot",
|
||
"duration": 5,
|
||
"power": 10,
|
||
"stat_affected": None,
|
||
"stacks": 1,
|
||
"max_stacks": 5,
|
||
"source": "potion",
|
||
}
|
||
|
||
effect = Effect.from_dict(data)
|
||
|
||
assert effect.effect_id == "regen_1"
|
||
assert effect.name == "Regeneration"
|
||
assert effect.effect_type == EffectType.HOT
|
||
assert effect.duration == 5
|
||
assert effect.power == 10
|
||
assert effect.stacks == 1
|
||
|
||
|
||
def test_effect_with_stat_deserialization():
|
||
"""Test deserializing effect with stat_affected."""
|
||
data = {
|
||
"effect_id": "buff_1",
|
||
"name": "Strength Boost",
|
||
"effect_type": "buff",
|
||
"duration": 3,
|
||
"power": 5,
|
||
"stat_affected": "strength",
|
||
"stacks": 1,
|
||
"max_stacks": 5,
|
||
"source": "spell",
|
||
}
|
||
|
||
effect = Effect.from_dict(data)
|
||
|
||
assert effect.stat_affected == StatType.STRENGTH
|
||
assert effect.effect_type == EffectType.BUFF
|
||
|
||
|
||
def test_effect_round_trip_serialization():
|
||
"""Test that serialization and deserialization preserve data."""
|
||
original = Effect(
|
||
effect_id="test_effect",
|
||
name="Test Effect",
|
||
effect_type=EffectType.DEBUFF,
|
||
duration=10,
|
||
power=15,
|
||
stat_affected=StatType.CONSTITUTION,
|
||
stacks=3,
|
||
max_stacks=5,
|
||
source="enemy_spell",
|
||
)
|
||
|
||
# Serialize then deserialize
|
||
data = original.to_dict()
|
||
restored = Effect.from_dict(data)
|
||
|
||
assert restored.effect_id == original.effect_id
|
||
assert restored.name == original.name
|
||
assert restored.effect_type == original.effect_type
|
||
assert restored.duration == original.duration
|
||
assert restored.power == original.power
|
||
assert restored.stat_affected == original.stat_affected
|
||
assert restored.stacks == original.stacks
|
||
assert restored.max_stacks == original.max_stacks
|
||
assert restored.source == original.source
|
||
|
||
|
||
def test_effect_repr():
|
||
"""Test string representation."""
|
||
effect = Effect(
|
||
effect_id="poison_1",
|
||
name="Poison",
|
||
effect_type=EffectType.DOT,
|
||
duration=3,
|
||
power=5,
|
||
stacks=2,
|
||
)
|
||
|
||
repr_str = repr(effect)
|
||
|
||
assert "Poison" in repr_str
|
||
assert "dot" in repr_str
|
||
|
||
|
||
def test_non_shield_reduce_shield():
|
||
"""Test that reduce_shield() on non-SHIELD effects returns full damage."""
|
||
effect = Effect(
|
||
effect_id="burn_1",
|
||
name="Burning",
|
||
effect_type=EffectType.DOT,
|
||
duration=3,
|
||
power=5,
|
||
)
|
||
|
||
remaining = effect.reduce_shield(50)
|
||
assert remaining == 50 # All damage passes through for non-shield effects
|