fix(rules): resistance is a +1 resistance bonus on fort/ref/will saves for 1 minute - SRD spells/resistance.md grants the subject a +1 resistance bonus on saving throws for 1 minute (Will negates, harmless); the resistance bonus type already exists in the stacking model (effects.py BonusType), so the buff is expressible as a BuffEffect and was only deferred because _find_buff_spell returns the FIRST BuffEffect in known-spell order and cantrips cost no slots - encoding it would have made default_policy cast Resistance every round (for Tammara it would preempt Divine Favor and never kill anything); resistance.json now carries a BuffEffect (+1 resistance on fort/ref/will, 10 rounds = 1 minute) and _find_buff_spell skips level-0 spells (cantrips have no spell slot in the engine's budget - spell_slots_left has no level-0 key - and the policy deliberately never auto-casts them, a cantrip buff would be cast every round degrading the policy), so Resistance cannot preempt a leveled buff (Divine Favor) in the known-spell order; leveled buffs keep the historical first-match behavior (if the first one's slots are exhausted the buff branch gives up - the golden pins that); the resistance bonus type does not stack with itself (resolve_modifiers keeps the higher value); +9 tests (test_resistance.py: buff shape, cast on a willing ally consumes no d20 and applies without a save roll, the bonus rides save rolls, no self-stacking, expiry at 10 rounds, _find_buff_spell skips the slot-less cantrip / returns None without a leveled buff, _choose_spell_action proposes Divine Favor and never Resistance); golden untouched; 1714 tests green

This commit is contained in:
2026-09-20 10:10:32 +02:00
parent 08f9af9e3e
commit 1238c40e43
3 changed files with 279 additions and 5 deletions
+18 -3
View File
@@ -8837,11 +8837,26 @@ def _find_offensive_spell(
return None
def _find_buff_spell(engine: CombatEngine, spells_known: list[str]) -> str | None:
"""Find a buff spell."""
def _find_buff_spell(
engine: CombatEngine, spells_known: list[str]
) -> str | None:
"""Find a buff spell the policy will actually cast.
PF1e cantrips (level 0) have no spell slot in the engine's budget
(``spell_slots_left`` has no level-0 key) and the policy deliberately
never auto-casts them (a cantrip buff would be cast every round, degrading
the policy), so a cantrip buff such as Resistance must not preempt a
leveled buff (Divine Favor) in the known-spell order. Leveled buffs keep
the historical first-match behavior: if the first one's slots are
exhausted the buff branch gives up (the golden pins that).
"""
for name in spells_known:
spell = engine.spell_registry.get(name)
if spell and any(isinstance(e, BuffEffect) for e in spell.effects):
if (
spell
and spell.level > 0
and any(isinstance(e, BuffEffect) for e in spell.effects)
):
return name
return None