fix(spells): apply a shaken effect when a fear save succeeds

ConditionEffect gains save_partial (JSON 'on_success': condition_name + duration_rounds). On a successful save that would otherwise negate, _apply_condition_effect now imposes the weaker condition (SRD fear.md: panicked on a failed Will save, shaken for 1 round on a success). Extended _metamagic_transform_sub to preserve/double it; extracted _impose_condition helper. Updated fear.json, added failed/successful-save and loader tests in test_spells.py, and the README.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-09-13 12:32:45 +02:00
parent a59ebee37a
commit c96b36d3f9
5 changed files with 92 additions and 19 deletions
+29 -3
View File
@@ -172,6 +172,18 @@ class TestSpellLoading:
assert isinstance(eff, ConditionEffect)
assert eff.condition_name == "paralyzed"
assert eff.save_negates is True
assert eff.save_partial is None
def test_fear_condition_has_save_partial(self) -> None:
# PF1e (SRD spells/fear.md): a successful Will save imposes shaken 1
# round instead of panicked — the partial-save spec.
spell = load_spell(_SPELLS_DIR / "fear.json")
area = spell.effects[0]
assert isinstance(area, AreaEffect)
eff = area.effects[0]
assert isinstance(eff, ConditionEffect)
assert eff.condition_name == "panicked"
assert eff.save_partial == ("shaken", 1)
def test_cure_light_has_heal_effect(self) -> None:
spell = load_spell(_SPELLS_DIR / "cure_light_wounds.json")
@@ -534,9 +546,9 @@ class TestHoldPerson:
class TestFear:
"""Given: a caster casting Fear (SRD spells/fear.md: panicked unless the
Will save succeeds)
When: target fails save
Then: panicked condition is applied."""
Will save succeeds; a successful save makes the creature shaken 1 round)
When: target fails / succeeds the save
Then: panicked is applied / shaken (partial save) is applied instead."""
def test_failed_save_applies_panicked(self) -> None:
caster = _make_caster()
@@ -544,6 +556,20 @@ class TestFear:
engine, c, t = _make_engine(caster, target, [2])
engine._cast_spell(c, t, "Fear")
assert any(ac.condition.name == "panicked" for ac in t.active_conditions)
assert not any(ac.condition.name == "shaken" for ac in t.active_conditions)
def test_successful_save_applies_shaken(self) -> None:
"""Given: the target succeeds its Will save (natural 20)
When: Fear resolves
Then: it is shaken for 1 round (partial save), not panicked."""
caster = _make_caster()
target = _make_target()
engine, c, t = _make_engine(caster, target, [20])
engine._cast_spell(c, t, "Fear")
shaken = [ac for ac in t.active_conditions if ac.condition.name == "shaken"]
assert len(shaken) == 1
assert shaken[0].rounds_left == 1
assert not any(ac.condition.name == "panicked" for ac in t.active_conditions)
class TestCureLightWounds: