fix(spells): make the bleed spell bleed

PF1e (SRD spells/bleed.md): "Saving Throw Will negates" and the target takes 1 point of damage per round. Two divergences: the JSON declared a Fortitude save, and the bleeding condition never set the engine's bleed_damage field, so the spell was a no-op. Bleed now saves Will; _apply_condition_effect sets bleed_damage = 1 when a spell applies bleeding, and _decrement_durations lands the final tick as the last bleeding condition expires before clearing the field (heal-path clearing already existed in _stop_bleeding). README updated.

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-12 12:46:53 +02:00
parent 8c7e7075ad
commit 910ed1dc7d
5 changed files with 61 additions and 6 deletions
+32
View File
@@ -396,6 +396,38 @@ class TestAreaSavesPerSpellSaveType:
assert not any(ac.condition.name == "stunned" for ac in t.active_conditions)
class TestBleedCantrip:
"""Given: the Bleed cantrip (SRD spells/bleed.md: Will negates; the target
takes 1 point of bleed damage per round)
When: the target fails / succeeds its Will save
Then: failed — bleeding condition, 1 hp lost at the start of its turn;
saved — no condition, no bleed damage."""
def test_failed_save_deals_one_bleed_damage_per_round(self) -> None:
caster = _make_caster()
target = _make_target(hp_max=30)
# Will save d20=2 (+2) vs DC 14 -> fail.
engine, c, t = _make_engine(caster, target, [2])
engine._cast_spell(c, t, "Bleed")
assert any(ac.condition.name == "bleeding" for ac in t.active_conditions)
assert t.bleed_damage == 1
# The target's next turn start: the 1-round condition expires and its
# final bleed tick lands (1 hp).
engine._decrement_durations(t)
assert t.hp == 30 - 1
assert t.bleed_damage == 0
def test_successful_save_does_not_bleed(self) -> None:
caster = _make_caster()
target = _make_target(hp_max=30)
# Will save natural 20 -> auto-success (SRD: Will negates).
engine, c, t = _make_engine(caster, target, [20])
engine._cast_spell(c, t, "Bleed")
assert not any(ac.condition.name == "bleeding" for ac in t.active_conditions)
assert t.bleed_damage == 0
assert t.hp == 30
class TestTargetedSpellSaveNegates:
"""Given: a targeted spell with a save (half_on_save=False)
When: the target succeeds / fails its save