fix(rules): apply the -2 spell combat penalty to melee attacks

PF1e CRB (Magus Spell Combat): as a full-round action the magus casts a
standard-action spell and makes all his melee attacks at -2. The penalty is
threaded through the full-attack chain (_execute_attack -> _full_attack ->
_full_attack_weapon -> _attack/_resolve_swing -> resolve_attack bonus_override)
so it applies to every attack roll and crit confirmation. Simplification: the
arcane pool cost is still not modeled.

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-11 20:30:19 +02:00
parent e321c67779
commit 0fefb9cdd2
3 changed files with 108 additions and 19 deletions
+39
View File
@@ -336,3 +336,42 @@ def test_misty_silent_when_bardic_rounds_exhausted() -> None:
assert not any(a.kind == "inspire_courage" for a in actions)
assert not any(a.kind == "spell_combat" for a in actions)
def test_spell_combat_melee_attacks_take_minus_two() -> None:
"""CRB Magus Spell Combat: the full attack that accompanies the cast is
made at -2 on every attack roll (vs the same swing in a normal full attack)."""
misty = _combatant(
"misty", resources={"bardic_rounds": 12}, spells=["Create Alcool"]
)
goblin = _combatant("goblin", hp_max=6)
reg = load_spell_registry(SPELLS_DIR)
# Queue: goblin AoO d20 (natural 1, auto-miss), then misty's swing d20=10.
engine, me, enemy = _engine(misty, [goblin], spell_registry=reg, rng_queue=[1, 10])
engine._execute(
me,
Action(
kind="spell_combat", target_id=enemy[0].combatant.id,
spell_name="Create Alcool",
),
)
# Sword attack_bonus 3 + Spell Combat -2 = +1: d20 10 -> total 11 (misses AC 15).
assert any(
"sword vs goblin d20=10+1=11" in line for line in engine._transcript
)
def test_normal_full_attack_has_no_spell_combat_penalty() -> None:
"""Control: the same scripted swing in a plain full attack keeps its +3."""
misty = _combatant("misty", resources={"bardic_rounds": 12})
goblin = _combatant("goblin", hp_max=6)
reg = load_spell_registry(SPELLS_DIR)
engine, me, enemy = _engine(misty, [goblin], spell_registry=reg, rng_queue=[10])
engine._execute(me, Action(kind="full_attack", target_id=enemy[0].combatant.id))
assert any(
"sword vs goblin d20=10+3=13" in line for line in engine._transcript
)