fix(rules): harmless spells on willing targets forgo the save roll - SpellSpec gains a data-driven 'harmless' flag set on the 29 SRD '(harmless)' spells (verified one by one against the KB); a same-side/self target consumes NO save die (SRD saving-throw.md: '(harmless) ... can attempt a saving throw if it desires' + 'Voluntarily Giving up a Saving Throw'), while hostile targets and undead receiving a cure (harmful to them, Will half for undead damage) keep the roll; snapshot spell summaries expose 'harmless'; golden transcript unchanged (no harmless-save cast in it); +8 tests, 3 fixtures re-pinned for the removed die
This commit is contained in:
@@ -6240,6 +6240,7 @@ class CombatEngine:
|
||||
"summary": "",
|
||||
"half_on_save": False,
|
||||
"save_negates": False,
|
||||
"harmless": False,
|
||||
"area": None,
|
||||
"cast_time": "standard",
|
||||
}
|
||||
@@ -6254,6 +6255,7 @@ class CombatEngine:
|
||||
"summary": _spell_summary(spell),
|
||||
"half_on_save": half_on_save,
|
||||
"save_negates": spell.save != "none" and not half_on_save,
|
||||
"harmless": spell.harmless,
|
||||
"area": _spell_area_dict(spell),
|
||||
"cast_time": spell.cast_time,
|
||||
}
|
||||
@@ -6710,6 +6712,35 @@ class CombatEngine:
|
||||
return False
|
||||
return True
|
||||
|
||||
def _save_forgone(
|
||||
self, caster: CombatantState, target: CombatantState, spell: SpellSpec
|
||||
) -> bool:
|
||||
"""True when a willing target forgoes the save of a harmless spell.
|
||||
|
||||
PF1e (SRD "Spell Descriptions — Saving Throw",
|
||||
rules/prpg-core-rulebook/saving-throw.md): "(harmless): The spell is
|
||||
usually beneficial, not harmful, but a targeted creature can attempt
|
||||
a saving throw if it desires" and "Voluntarily Giving up a Saving
|
||||
Throw: A creature can voluntarily forego a saving throw and willingly
|
||||
accept a spell's result". The engine's willingness proxy is the side:
|
||||
self and same-side creatures are willing, so a spell flagged
|
||||
``harmless`` rolled on them consumes NO save die. Two faithful
|
||||
exceptions keep the roll: a hostile target is not willing (it may
|
||||
attempt the save), and a cure spell is HARMFUL to an undead target
|
||||
(SRD cure-light-wounds.md: it "deals damage to them instead of
|
||||
curing their wounds [...] can attempt a Will save to take half
|
||||
damage"), so an undead never forgoes that save.
|
||||
"""
|
||||
if not spell.harmless:
|
||||
return False
|
||||
if target is not caster and target.side != caster.side:
|
||||
return False
|
||||
return not any(
|
||||
isinstance(effect, HealEffect)
|
||||
and target.combatant.creature_type == "undead"
|
||||
for effect in spell.effects
|
||||
)
|
||||
|
||||
def _apply_and_log_cast(
|
||||
self,
|
||||
caster: CombatantState,
|
||||
@@ -6744,6 +6775,7 @@ class CombatEngine:
|
||||
target, spell.save, dc, save_descriptors=spell_save_descriptors(spell)
|
||||
)
|
||||
if spell.save != "none" and target is not None
|
||||
and not self._save_forgone(caster, target, spell)
|
||||
else None
|
||||
)
|
||||
caster_live = self._stats[caster.combatant.id]
|
||||
|
||||
@@ -364,6 +364,14 @@ class SpellSpec:
|
||||
# no" (Acid Arrow, Flurry of Snowballs, ...). False = skip the SR check
|
||||
# entirely (targeted and per-creature in areas). Default True.
|
||||
spell_resistance: bool = True
|
||||
# PF1e (SRD, Magic — Spell Descriptions, saving throw notation):
|
||||
# "(harmless): The spell is usually beneficial, not harmful, but a
|
||||
# targeted creature can attempt a saving throw if it desires"
|
||||
# (rules/prpg-core-rulebook/saving-throw.md). A willing target forgoes
|
||||
# the save ("Voluntarily Giving up a Saving Throw: A creature can
|
||||
# voluntarily forego a saving throw and willingly accept a spell's
|
||||
# result"), so the engine skips the save roll on same-side targets.
|
||||
harmless: bool = False
|
||||
effects: tuple[SpellEffect, ...] = ()
|
||||
description: str = ""
|
||||
|
||||
@@ -803,6 +811,7 @@ def load_spell(path: str | Path) -> SpellSpec:
|
||||
requires_ranged_touch=bool(data.get("requires_ranged_touch", False)),
|
||||
cast_time=cast_time,
|
||||
spell_resistance=bool(data.get("spell_resistance", True)),
|
||||
harmless=bool(data.get("harmless", False)),
|
||||
effects=effects,
|
||||
description=_as_str(data.get("description", "")),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user