fix(rules): grappled/pinned -2 applies to non-grapple/escape combat maneuver checks
This commit is contained in:
@@ -177,6 +177,7 @@ _CHARGE_ATTACK_BONUS = 2 # PF1e: +2 attack roll on a charge
|
||||
_CHARGE_AC_PENALTY = 2 # PF1e: -2 AC until start of next turn after charging
|
||||
_CHARGE_MIN_CELLS = 2 # PF1e: charge must move at least 10 ft (2 squares)
|
||||
_UP_CLOSE_RANGE_FT = 30 # PF1e: Up Close and Deadly works within 30 ft
|
||||
_GRAPPLED_CMB_PENALTY = 2 # PF1e: grappled/pinned -2 on non-grapple/escape CMB
|
||||
# PF1e: Point-Blank Shot grants +1/+1 within 30 ft with ranged weapons
|
||||
_POINT_BLANK_RANGE_FT = 30
|
||||
_FIRING_MELEE_PENALTY = -4 # PF1e: ranged attack into melee
|
||||
@@ -4580,7 +4581,22 @@ class CombatEngine:
|
||||
# check is not penalized (a skill check is not a CMB check) — see
|
||||
# _maneuver_escape.
|
||||
pa_amt = self._power_attack_amt(state)
|
||||
attacker_cmb = self._maneuver_cmb(state) - pa_amt
|
||||
# PF1e (SRD Conditions - Grappled / Pinned): a grappled or pinned
|
||||
# creature takes a -2 penalty on "all attack rolls AND combat
|
||||
# maneuver checks, except for those made to escape or break a
|
||||
# grapple". Escape returned to its own handler above; initiating a
|
||||
# grapple is the exempt check. _maneuver_cmb stays a pure stat
|
||||
# (the concentration DC reads the grappler's CMB, not a check).
|
||||
grapple_penalty = (
|
||||
_GRAPPLED_CMB_PENALTY
|
||||
if maneuver != "grapple"
|
||||
and (
|
||||
self._has_condition_named(state, "grappled")
|
||||
or self._has_condition_named(state, "pinned")
|
||||
)
|
||||
else 0
|
||||
)
|
||||
attacker_cmb = self._maneuver_cmb(state) - pa_amt - grapple_penalty
|
||||
target_cmd = self._maneuver_cmd(target)
|
||||
roll = self._rng.d20()
|
||||
# PF1e (CRB Combat Maneuvers): the provoked-AoO damage rides the
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user