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:
2026-09-19 22:27:46 +02:00
parent ea835293a0
commit 4665a1c245
37 changed files with 266 additions and 12 deletions
+32
View File
@@ -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]