fix(rules): cure spells damage undead instead of healing

SRD Cure Light Wounds: 'Since undead are powered by negative energy,
this spell deals damage to them instead of curing their wounds. An
undead creature can apply spell resistance, and can attempt a Will save
to take half damage.'

- _apply_heal_effect takes the rolled save; an undead target takes the
  rolled healing as damage (halved on a successful save; SR already
  resolved upstream, DR never applies to spell energy, no bleeding stop
  or stabilization). Living targets are bit-identical.
- cure_light/cure_moderate descriptions fixed: the SRD save line is
  'Will half (harmless)', not 'negates'.
- Deafened 20% verbal spell failure DEFERRED to the README « Limites
  hors perimetre »: SpellSpec carries no components data (would need a
  verbal flag on all 108 spells) and sound-based Perception is not
  modeled either.
This commit is contained in:
2026-09-16 15:48:36 +02:00
parent 3e19d2974e
commit cad35e2e13
7 changed files with 109 additions and 6 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ def test_heal_spell_clears_bleed() -> None:
a.conditions.append(BLEEDING)
result = eng._apply_heal_effect(
HealEffect(formula=parse_dice("1d8")), a, caster_level=1,
empower=False, maximize=False,
empower=False, maximize=False, saved=False,
)
assert result == "+5 HP"
assert a.hp == 55
+80
View File
@@ -0,0 +1,80 @@
"""Cure spells damage undead instead of healing (Task 11 conformance).
SRD "Cure Light Wounds" (spells/cure-light-wounds.md): "Since undead are
powered by negative energy, this spell deals damage to them instead of curing
their wounds. An undead creature can apply spell resistance, and can attempt
a Will save to take half damage." The engine resolves SR upstream and the
rolled Will save halves the damage here; living targets keep healing exactly
as before.
"""
from __future__ import annotations
from typing import TYPE_CHECKING
from tests.test_spells import _make_caster, _make_engine, _make_target
if TYPE_CHECKING:
from pf1e_simulator.models import Combatant
def _undead(cid: str = "skeleton", *, hp_max: int = 20) -> Combatant:
return _make_target(cid=cid, hp_max=hp_max).model_copy(
update={"creature_type": "undead"}
)
def _caster() -> Combatant:
return _make_caster().model_copy(update={"spells": ["Cure Light Wounds"]})
class TestCureDamagesUndead:
"""Given: a level-5 caster (Cure Light Wounds = 1d8+5) and an undead target
When: the cure spell is delivered
Then: the undead takes damage instead of healing.
"""
def test_failed_save_deals_full_damage(self) -> None:
"""Given: an undead at 20 hp
When: cured and it fails the Will save (d20=1), the die rolls 8
Then: it takes the full 13 damage (8+5) instead of healing."""
engine, c, t = _make_engine(
_caster(), _undead(), [1, 8], target_pos=(1, 0), target_side="players"
)
engine._cast_spell(c, t, "Cure Light Wounds")
assert t.hp == 7
assert any("13 damage (undead)" in line for line in engine._transcript)
def test_successful_save_halves_the_damage(self) -> None:
"""Given: an undead at 20 hp
When: cured and it succeeds the Will save (natural 20), the die rolls 8
Then: it takes half damage (13 // 2 = 6)."""
engine, c, t = _make_engine(
_caster(), _undead(), [20, 8], target_pos=(1, 0), target_side="players"
)
engine._cast_spell(c, t, "Cure Light Wounds")
assert t.hp == 14
assert any("6 damage (undead)" in line for line in engine._transcript)
def test_undead_destroyed_by_cure_damage(self) -> None:
"""Given: an undead at 10 hp
When: a failed-save cure deals 13 damage
Then: the undead is destroyed (undead die at hp <= 0)."""
engine, c, t = _make_engine(
_caster(), _undead(hp_max=10), [1, 8], target_pos=(1, 0), target_side="players"
)
engine._cast_spell(c, t, "Cure Light Wounds")
assert t.dead is True
def test_living_target_still_heals(self) -> None:
"""Given: a living ally at 5/20 hp
When: cured with a die roll of 8
Then: it heals 13 hp (behavior unchanged for the living)."""
engine, c, t = _make_engine(
_caster(), _make_target(), [1, 8], target_pos=(1, 0), target_side="players"
)
t.hp = 5
engine._cast_spell(c, t, "Cure Light Wounds")
assert t.hp == 18
assert any("+13 HP" in line for line in engine._transcript)
+2 -2
View File
@@ -221,7 +221,7 @@ def test_heal_spell_stabilizes_dying_creature() -> None:
eng._rng = ScriptedRng([1]) # 1d8 -> 1: only 1 point cured
result = eng._apply_heal_effect(
HealEffect(formula=parse_dice("1d8")), s, caster_level=1,
empower=False, maximize=False,
empower=False, maximize=False, saved=False,
)
assert result == "+1 HP"
assert s.hp == -2
@@ -253,7 +253,7 @@ def test_heal_to_positive_hp_does_not_stabilize() -> None:
eng._rng = ScriptedRng([8])
eng._apply_heal_effect(
HealEffect(formula=parse_dice("1d8")), s, caster_level=1,
empower=False, maximize=False,
empower=False, maximize=False, saved=False,
)
assert s.hp == 5
assert s.stabilized_hp is None