fix(rules): combat maneuvers auto-succeed (natural 20) vs incapacitated targets

This commit is contained in:
2026-09-16 20:21:41 +02:00
parent 06da519fc6
commit 0cccea2fa1
2 changed files with 47 additions and 0 deletions
+9
View File
@@ -4641,6 +4641,15 @@ class CombatEngine:
attacker_cmb = self._maneuver_cmb(state) - pa_amt - grapple_penalty
target_cmd = self._maneuver_cmd(target)
roll = self._rng.d20()
# PF1e (SRD Combat Maneuvers): "If your target is immobilized,
# unconscious, or otherwise incapacitated, your maneuver
# automatically succeeds (treat as if you rolled a natural 20 on
# the attack roll)." The engine's incapacitated notion is the
# helpless set (helpless/paralyzed/asleep/unconscious, CRB
# "Conditions"); the natural 20 also feeds the margin-based
# outcomes (bull rush / overrun / reposition / drag / dirty trick).
if self._is_helpless(target):
roll = _NATURAL_TWENTY
# PF1e (CRB Combat Maneuvers): the provoked-AoO damage rides the
# check total (floored at 0); natural 20/1 keep their rules below.
total = max(0, roll + attacker_cmb - aoo_damage)
+38
View File
@@ -340,6 +340,44 @@ def test_disarm_fail_by_10_drops_attacker_weapon() -> None:
assert any("disarm backfire -> drops sword" in line for line in engine._transcript)
# --- SRD Combat Maneuvers: auto-success against an incapacitated target -------
def test_maneuver_auto_succeeds_vs_helpless_target() -> None:
# SRD Combat Maneuvers: "If your target is immobilized, unconscious, or
# otherwise incapacitated, your maneuver automatically succeeds (treat
# as if you rolled a natural 20 on the attack roll)." b (bab 20) has CMD
# 30: a natural 1 would normally auto-fail, but the helpless target
# forces the natural-20 treatment -> trip succeeds.
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
b = make_state(make_combatant("b", attack_bonus=20), "monsters", (1, 2))
_conditioned(b, "paralyzed")
engine = make_engine([1, 1], [a, b])
engine._execute_maneuver(a, b, "trip")
assert any(c.condition.name == "prone" for c in b.active_conditions)
assert "d20=20" in engine._transcript[-1]
def test_maneuver_auto_succeeds_vs_asleep_target_with_margin() -> None:
# The natural-20 treatment feeds the margin outcomes: a CMB 2 vs CMD 12,
# roll forced to 20 -> total 22, margin 10 -> bull rush pushes 3 cells.
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
b = make_state(make_combatant("b"), "monsters", (1, 2))
_conditioned(b, "asleep")
engine = make_engine([1, 5], [a, b])
engine._execute_maneuver(a, b, "bull_rush")
assert b.pos == (1, 5)
def test_maneuver_normal_roll_still_fails_vs_awake_target() -> None:
# Control: the same natural 1 against a non-helpless target auto-fails.
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
b = make_state(make_combatant("b"), "monsters", (1, 2))
engine = make_engine([1, 1], [a, b])
engine._execute_maneuver(a, b, "trip")
assert not any(c.condition.name == "prone" for c in b.active_conditions)
def test_overrun_knocks_prone_and_moves_through() -> None:
# d20=15+2=17 vs CMD 12: margin 5 -> prone AND moved through (CRB
# "Overrun": prone only when the attack exceeds CMD by 5 or more).