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
+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).