fix(rules): grappled/pinned -2 applies to non-grapple/escape combat maneuver checks

This commit is contained in:
2026-09-16 19:33:38 +02:00
parent 68a03a34f6
commit cf8a07311d
2 changed files with 77 additions and 1 deletions
+60
View File
@@ -671,3 +671,63 @@ def test_adjacent_grapple_does_not_move_the_target() -> None:
assert b.pos == (1, 2)
assert b.grappled_with == "a"
assert not any("grapple pulls" in line for line in engine._transcript)
# --- Grappled / pinned -2 on combat maneuver checks --------------------------
def _conditioned(a: CombatantState, name: str) -> None:
a.active_conditions.append(
ActiveCondition(
condition=CONDITIONS_BY_NAME[name], rounds_left=1, source="test"
)
)
def test_grappled_attacker_takes_minus2_on_trip_cmb() -> None:
# SRD Conditions - Grappled: "-2 penalty on all attack rolls and combat
# maneuver checks, except for those made to escape or break a grapple."
# a CMB 2 - 2 = 0 vs b CMD 12: d20 10 -> total 10 < 12 -> FAILED (the
# pre-fix engine would have succeeded at 10 + 2 = 12).
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
b = make_state(make_combatant("b"), "monsters", (1, 2))
_conditioned(a, "grappled")
engine = make_engine([1, 10], [a, b])
engine._execute_maneuver(a, b, "trip")
assert not any(c.condition.name == "prone" for c in b.active_conditions)
assert engine._transcript[-1].endswith("-> FAILED")
def test_pinned_attacker_takes_minus2_on_trip_cmb() -> None:
# Same -2 applies to a pinned creature (SRD Conditions - Pinned).
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
b = make_state(make_combatant("b"), "monsters", (1, 2))
_conditioned(a, "pinned")
engine = make_engine([1, 10], [a, b])
engine._execute_maneuver(a, b, "trip")
assert not any(c.condition.name == "prone" for c in b.active_conditions)
assert engine._transcript[-1].endswith("-> FAILED")
def test_grappled_attacker_grapple_check_is_exempt() -> None:
# The -2 does NOT apply to grapple checks (escape or break a grapple):
# d20 10 + CMB 2 = 12 >= CMD 12 -> SUCCESS.
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
b = make_state(make_combatant("b"), "monsters", (1, 2))
_conditioned(a, "grappled")
engine = make_engine([1, 10], [a, b])
engine._execute_maneuver(a, b, "grapple")
assert b.grappled_with == "a"
assert engine._transcript[-1].endswith("SUCCESS (grappled)")
def test_grappled_escape_check_is_exempt() -> None:
# Escape is handled by _maneuver_escape which never applies the penalty:
# escape bonus = CMB 2 (no -2), d20 10 -> total 12 >= CMD 12 -> freed.
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
b = make_state(make_combatant("b"), "monsters", (1, 2))
_grappled_by(a, b)
engine = make_engine([10], [a, b])
engine._maneuver_escape(a, b)
assert a.grappled_with is None
assert not any(c.condition.name == "grappled" for c in a.active_conditions)