feat(spells): allow spells to ignore spell resistance

SpellSpec gains spell_resistance: bool = True (JSON 'spell_resistance'). When false, _spell_delivery_blocked skips the targeted SR check and _apply_area_effect skips the per-creature SR check (threaded through the effect chain). Verified each imported spell against the local KB and set false on the 28 whose SRD block says 'Spell Resistance: no' (Acid Arrow, Flurry of Snowballs, Web, Darkness, Sanctuary, the Protection from X spells, etc.). Added no-SR targeted/area tests and a high-SR control in test_spell_resistance.py, and the README.

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-13 12:44:35 +02:00
parent c96b36d3f9
commit 9768f5ae1d
32 changed files with 142 additions and 9 deletions
+8 -2
View File
@@ -5478,6 +5478,7 @@ class CombatEngine:
if (
target is not None
and not has_area
and spell.spell_resistance
and not self._check_spell_resistance(caster, target)
):
return True, False
@@ -5735,7 +5736,7 @@ class CombatEngine:
maximize="Maximize Spell" in metamagic,
dc=dc, center_pos=center_pos, save_type=spell.save,
crit=spell_crit, ranged_touch=per_projectile_attack,
attack_penalty=attack_penalty,
attack_penalty=attack_penalty, spell_resistance=spell.spell_resistance,
)
save_str = _save_label(save_result)
if target is None and center_pos is not None:
@@ -5794,6 +5795,7 @@ class CombatEngine:
crit: bool = False,
ranged_touch: bool = False,
attack_penalty: int = 0,
spell_resistance: bool = True,
) -> list[str]:
saved = save_result is not None and save_result.success
return [
@@ -5803,6 +5805,7 @@ class CombatEngine:
empower=empower, maximize=maximize, saved=saved,
dc=dc, center_pos=center_pos, save_type=save_type,
crit=crit, ranged_touch=ranged_touch, attack_penalty=attack_penalty,
spell_resistance=spell_resistance,
)
for effect in effects
]
@@ -5825,6 +5828,7 @@ class CombatEngine:
crit: bool = False,
ranged_touch: bool = False,
attack_penalty: int = 0,
spell_resistance: bool = True,
) -> str:
if isinstance(effect, AreaEffect):
return self._apply_area_effect(
@@ -5832,6 +5836,7 @@ class CombatEngine:
caster=caster, spell_name=spell_name, saved=saved,
empower=empower, maximize=maximize, dc=dc,
center_pos=center_pos, save_type=save_type,
spell_resistance=spell_resistance,
)
if isinstance(effect, SummonEffect):
return self._apply_summon_effect(effect, caster, caster_live, spell_name)
@@ -6100,6 +6105,7 @@ class CombatEngine:
dc: int = 15,
center_pos: tuple[int, int] | None = None,
save_type: SaveType = "none",
spell_resistance: bool = True,
) -> str:
"""Apply an area effect to all creatures within its template.
@@ -6125,7 +6131,7 @@ class CombatEngine:
total_buffs = 0
for affected_state in affected:
if not self._check_spell_resistance(caster, affected_state):
if spell_resistance and not self._check_spell_resistance(caster, affected_state):
continue
creature_saved = self._area_save(
affected_state, target, effect, saved=saved, dc=dc,
+6
View File
@@ -238,6 +238,11 @@ class SpellSpec:
# area/save-only spells keep this False.
requires_ranged_touch: bool = False
cast_time: CastTime = "standard"
# PF1e (CRB, Magic — Spell Resistance): most spells are subject to a
# target's SR, but the SRD stat block marks a handful "Spell Resistance:
# no" (Acid Arrow, Flurry of Snowballs, ...). False = skip the SR check
# entirely (targeted and per-creature in areas). Default True.
spell_resistance: bool = True
effects: tuple[SpellEffect, ...] = ()
description: str = ""
@@ -573,6 +578,7 @@ def load_spell(path: str | Path) -> SpellSpec:
base_dc=_as_int(data.get("base_dc", 10)),
requires_ranged_touch=bool(data.get("requires_ranged_touch", False)),
cast_time=cast_time,
spell_resistance=bool(data.get("spell_resistance", True)),
effects=effects,
description=_as_str(data.get("description", "")),
)