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:
@@ -1583,6 +1583,17 @@ plutôt que bricolées) :
|
|||||||
en revanche deux créatures de même taille (Small et plus) ne partagent
|
en revanche deux créatures de même taille (Small et plus) ne partagent
|
||||||
jamais une case, et la co-occupation de plusieurs Tiny sur une même case
|
jamais une case, et la co-occupation de plusieurs Tiny sur une même case
|
||||||
n'est pas modélisée.
|
n'est pas modélisée.
|
||||||
|
- **Échec d'incantation pour un personnage sourd hors périmètre** (SRD
|
||||||
|
« Conditions — Deafened » : « a 20% chance of spell failure when casting
|
||||||
|
spells with verbal components ») : le moteur ne porte pas la composante
|
||||||
|
verbale des sorts (`SpellSpec` n'a pas de champ `components` — il
|
||||||
|
faudrait l'encoder sur les 108 sorts du registre) et ne modélise pas non
|
||||||
|
plus le jet de Perception DD 20 basé sur le son ni l'échec automatique
|
||||||
|
associé ; la condition `deafened` reste donc un porteur de modificateurs
|
||||||
|
(−4 d'initiative) sans la faillibilité d'incantation. Le soin des
|
||||||
|
morts-vivants par les sorts de soin (`cure`) est en revanche modélisé :
|
||||||
|
SRD « Cure Light Wounds » — « this spell deals damage to them instead of
|
||||||
|
curing their wounds [...] can attempt a Will save to take half damage ».
|
||||||
|
|
||||||
Limites de sorts (conformance D6-D9, documentées plutôt que bricolées) :
|
Limites de sorts (conformance D6-D9, documentées plutôt que bricolées) :
|
||||||
|
|
||||||
|
|||||||
@@ -13,5 +13,5 @@
|
|||||||
"scaling_flat": { "amount": 1, "per_levels": 1, "max": 5 }
|
"scaling_flat": { "amount": 1, "per_levels": 1, "max": 5 }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Cures 1d8 points of damage +1 point per caster level (maximum +5). Will save negates (harmless)."
|
"description": "Cures 1d8 points of damage +1 point per caster level (maximum +5). Will half (harmless); since undead are powered by negative energy, the spell deals damage to them instead (Will half, SRD cure light wounds)."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,5 @@
|
|||||||
"scaling_flat": { "amount": 1, "per_levels": 1, "max": 10 }
|
"scaling_flat": { "amount": 1, "per_levels": 1, "max": 10 }
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"description": "Cures 2d8 points of damage +1 point per caster level (maximum +10). Will save negates (harmless)."
|
"description": "Cures 2d8 points of damage +1 point per caster level (maximum +10). Will half (harmless); since undead are powered by negative energy, the spell deals damage to them instead (Will half, SRD cure moderate wounds)."
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6287,7 +6287,7 @@ class CombatEngine:
|
|||||||
if isinstance(effect, HealEffect):
|
if isinstance(effect, HealEffect):
|
||||||
return self._apply_heal_effect(
|
return self._apply_heal_effect(
|
||||||
effect, target, caster_level=caster.combatant.level,
|
effect, target, caster_level=caster.combatant.level,
|
||||||
empower=empower, maximize=maximize,
|
empower=empower, maximize=maximize, saved=saved,
|
||||||
)
|
)
|
||||||
return self._apply_buff_effect(effect, target, spell_name=spell_name)
|
return self._apply_buff_effect(effect, target, spell_name=spell_name)
|
||||||
|
|
||||||
@@ -6475,6 +6475,7 @@ class CombatEngine:
|
|||||||
caster_level: int,
|
caster_level: int,
|
||||||
empower: bool,
|
empower: bool,
|
||||||
maximize: bool,
|
maximize: bool,
|
||||||
|
saved: bool,
|
||||||
) -> str:
|
) -> str:
|
||||||
formula = effect.effective_formula(caster_level)
|
formula = effect.effective_formula(caster_level)
|
||||||
heal = formula.roll(self._rng)
|
heal = formula.roll(self._rng)
|
||||||
@@ -6482,6 +6483,17 @@ class CombatEngine:
|
|||||||
heal = formula.max_value()
|
heal = formula.max_value()
|
||||||
if empower:
|
if empower:
|
||||||
heal = int(heal * 1.5)
|
heal = int(heal * 1.5)
|
||||||
|
if target.combatant.creature_type == "undead":
|
||||||
|
# PF1e (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."
|
||||||
|
# SR is resolved upstream (_spell_delivery_blocked); the rolled
|
||||||
|
# save halves here. Cure damage is spell energy: never reduced by
|
||||||
|
# DR, and it does not stop bleeding or stabilize.
|
||||||
|
dmg = heal // 2 if saved else heal
|
||||||
|
target.hp -= dmg
|
||||||
|
return f"{dmg} damage (undead)"
|
||||||
old_hp = target.hp
|
old_hp = target.hp
|
||||||
target.hp = min(target.hp + heal, target.combatant.hp_max)
|
target.hp = min(target.hp + heal, target.combatant.hp_max)
|
||||||
# PF1e (CRB, Stable Characters and Recovery): "If any sort of healing
|
# PF1e (CRB, Stable Characters and Recovery): "If any sort of healing
|
||||||
|
|||||||
@@ -103,7 +103,7 @@ def test_heal_spell_clears_bleed() -> None:
|
|||||||
a.conditions.append(BLEEDING)
|
a.conditions.append(BLEEDING)
|
||||||
result = eng._apply_heal_effect(
|
result = eng._apply_heal_effect(
|
||||||
HealEffect(formula=parse_dice("1d8")), a, caster_level=1,
|
HealEffect(formula=parse_dice("1d8")), a, caster_level=1,
|
||||||
empower=False, maximize=False,
|
empower=False, maximize=False, saved=False,
|
||||||
)
|
)
|
||||||
assert result == "+5 HP"
|
assert result == "+5 HP"
|
||||||
assert a.hp == 55
|
assert a.hp == 55
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -221,7 +221,7 @@ def test_heal_spell_stabilizes_dying_creature() -> None:
|
|||||||
eng._rng = ScriptedRng([1]) # 1d8 -> 1: only 1 point cured
|
eng._rng = ScriptedRng([1]) # 1d8 -> 1: only 1 point cured
|
||||||
result = eng._apply_heal_effect(
|
result = eng._apply_heal_effect(
|
||||||
HealEffect(formula=parse_dice("1d8")), s, caster_level=1,
|
HealEffect(formula=parse_dice("1d8")), s, caster_level=1,
|
||||||
empower=False, maximize=False,
|
empower=False, maximize=False, saved=False,
|
||||||
)
|
)
|
||||||
assert result == "+1 HP"
|
assert result == "+1 HP"
|
||||||
assert s.hp == -2
|
assert s.hp == -2
|
||||||
@@ -253,7 +253,7 @@ def test_heal_to_positive_hp_does_not_stabilize() -> None:
|
|||||||
eng._rng = ScriptedRng([8])
|
eng._rng = ScriptedRng([8])
|
||||||
eng._apply_heal_effect(
|
eng._apply_heal_effect(
|
||||||
HealEffect(formula=parse_dice("1d8")), s, caster_level=1,
|
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.hp == 5
|
||||||
assert s.stabilized_hp is None
|
assert s.stabilized_hp is None
|
||||||
|
|||||||
Reference in New Issue
Block a user