fix(rules): combat maneuvers auto-succeed (natural 20) vs incapacitated targets
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user