refactor(combat): single source active_buffs/active_conditions

This commit is contained in:
2026-09-04 22:55:55 +02:00
parent 073da40fca
commit 2e4d192522
2 changed files with 23 additions and 16 deletions
-11
View File
@@ -1066,7 +1066,6 @@ class CombatEngine:
source="maneuver: trip",
)
)
target.conditions.append(CONDITIONS_BY_NAME["prone"])
self._log(
f"round {self._current_round} {state.combatant.id}: trip vs "
f"{target.combatant.id} d20={m.roll}+{m.attacker_cmb}={m.total} "
@@ -1136,7 +1135,6 @@ class CombatEngine:
source=f"maneuver: grapple by {state.combatant.id}",
)
)
target.conditions.append(CONDITIONS_BY_NAME["grappled"])
target.grappled_with = state.combatant.id
state.grappled_with = target.combatant.id
self._log(
@@ -1304,7 +1302,6 @@ class CombatEngine:
source=f"hex: evil_eye by {state.combatant.id}",
)
)
target.conditions.append(CONDITIONS_BY_NAME["shaken"])
self._log(
f"round {self._current_round} {state.combatant.id}: evil eye on "
f"{target.combatant.id} -> shaken (1r)"
@@ -1328,7 +1325,6 @@ class CombatEngine:
source=f"hex: slumber by {state.combatant.id}",
)
)
target.conditions.append(CONDITIONS_BY_NAME["asleep"])
self._log(
f"round {self._current_round} {state.combatant.id}: slumber on "
f"{target.combatant.id} -> asleep (1r)"
@@ -1452,11 +1448,6 @@ class CombatEngine:
source=f"inspire courage by {state.combatant.id}",
)
)
# Also update old effects field for backwards compatibility
ally.effects.extend((
StatModifier(target="attack", value=1, bonus_type="morale"),
StatModifier(target="damage", value=1, bonus_type="morale"),
))
self._log(
f"round {self._current_round} {state.combatant.id}: "
f"inspire courage +1 morale attack/damage to {len(affected)} allies"
@@ -1665,7 +1656,6 @@ class CombatEngine:
source=f"spell: {spell_name}",
)
)
target.conditions.append(cond)
return f"{effect.condition_name} ({effect.duration_rounds}r)"
def _apply_heal_effect(
@@ -1696,7 +1686,6 @@ class CombatEngine:
source=f"spell: {spell_name}",
)
)
target.effects.extend(effect.modifiers)
return f"buff {len(effect.modifiers)} mods ({effect.duration_rounds}r)"
def _apply_area_effect(
+23 -5
View File
@@ -255,14 +255,14 @@ class TestHoldPerson:
target = _make_target()
engine, c, t = _make_engine(caster, target, [2])
engine._cast_spell(c, t, "Hold Person")
assert any(cond.name == "paralyzed" for cond in t.conditions)
assert any(ac.condition.name == "paralyzed" for ac in t.active_conditions)
def test_successful_save_no_condition(self) -> None:
caster = _make_caster()
target = _make_target()
engine, c, t = _make_engine(caster, target, [20])
engine._cast_spell(c, t, "Hold Person")
assert not any(cond.name == "paralyzed" for cond in t.conditions)
assert not any(ac.condition.name == "paralyzed" for ac in t.active_conditions)
def test_transcript_shows_negated_on_save(self) -> None:
caster = _make_caster()
@@ -282,7 +282,7 @@ class TestFear:
target = _make_target()
engine, c, t = _make_engine(caster, target, [2])
engine._cast_spell(c, t, "Fear")
assert any(cond.name == "frightened" for cond in t.conditions)
assert any(ac.condition.name == "frightened" for ac in t.active_conditions)
class TestCureLightWounds:
@@ -317,8 +317,9 @@ class TestBullsStrength:
target = _make_target()
engine, c, t = _make_engine(caster, target, [3, 3], target_pos=(1, 0))
engine._cast_spell(c, t, "Bull's Strength")
atk_mods = [e for e in t.effects if e.target == "attack"]
dmg_mods = [e for e in t.effects if e.target == "damage"]
buff_mods = [m for b in t.active_buffs for m in b.modifiers]
atk_mods = [e for e in buff_mods if e.target == "attack"]
dmg_mods = [e for e in buff_mods if e.target == "damage"]
assert len(atk_mods) == 1
assert len(dmg_mods) == 1
assert atk_mods[0].value == 1
@@ -461,3 +462,20 @@ class TestSpellSlots:
engine._cast_spell(c, t, "Magic Missile") # spends the only level-1 slot
assert c.spell_slots_left[1] == 0
assert _choose_spell_action(engine, c, t) is None
class TestActiveIsSingleSource:
"""Given: active_* is the single source of truth for conditions and buffs
When: _cast_spell applies a condition and a buff
Then: the legacy conditions/effects lists stay empty while the active_* lists are populated."""
def test_legacy_lists_stay_empty_active_lists_populated(self) -> None:
caster = _make_caster()
target = _make_target()
engine, c, t = _make_engine(caster, target, [2, 3, 3], target_pos=(1, 0))
engine._cast_spell(c, t, "Hold Person")
engine._cast_spell(c, t, "Bull's Strength")
assert t.conditions == []
assert t.effects == []
assert any(ac.condition.name == "paralyzed" for ac in t.active_conditions)
assert any(b.source.startswith("spell:") for b in t.active_buffs)