fix(rules): pinned creatures cannot attack (definition corrected; apply-gap documented)
This commit is contained in:
@@ -266,6 +266,12 @@ GRAPPLED = Condition(
|
|||||||
|
|
||||||
|
|
||||||
# Pinned: -4 Dex, -2 attack, no movement, can't cast, -4 AC vs melee (CRB)
|
# Pinned: -4 Dex, -2 attack, no movement, can't cast, -4 AC vs melee (CRB)
|
||||||
|
# SRD "Conditions—Pinned": a pinned creature is tightly bound and can only
|
||||||
|
# attempt to escape (verbal/mental actions + the escape check) — it cannot
|
||||||
|
# attack, so cant_attack applies (the engine's move-only filter then blocks
|
||||||
|
# attacks, maneuvers and casts). Documented gap: the engine never APPLIES
|
||||||
|
# pinned (no grapple-maintain loop), so this definition is exercised only
|
||||||
|
# when the condition is injected directly.
|
||||||
PINNED = Condition(
|
PINNED = Condition(
|
||||||
name="pinned",
|
name="pinned",
|
||||||
modifiers=(
|
modifiers=(
|
||||||
@@ -276,6 +282,7 @@ PINNED = Condition(
|
|||||||
),
|
),
|
||||||
cant_move=True,
|
cant_move=True,
|
||||||
cant_charge=True,
|
cant_charge=True,
|
||||||
|
cant_attack=True,
|
||||||
cant_aoo=True,
|
cant_aoo=True,
|
||||||
denies_dex_to_ac=True,
|
denies_dex_to_ac=True,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ from pf1e_simulator.conditions import (
|
|||||||
FATIGUED,
|
FATIGUED,
|
||||||
NAUSEATED,
|
NAUSEATED,
|
||||||
PARALYZED,
|
PARALYZED,
|
||||||
|
PINNED,
|
||||||
PRONE,
|
PRONE,
|
||||||
STAGGERED,
|
STAGGERED,
|
||||||
)
|
)
|
||||||
@@ -326,6 +327,30 @@ class TestNauseatedActionEconomy:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestPinnedCannotAttack:
|
||||||
|
"""Given: a pinned combatant (SRD "Conditions—Pinned": tightly bound,
|
||||||
|
can only attempt to escape — it cannot attack)
|
||||||
|
When: _take_turn is called
|
||||||
|
Then: no attack is resolved."""
|
||||||
|
|
||||||
|
def test_pinned_flag(self) -> None:
|
||||||
|
assert PINNED.cant_attack is True
|
||||||
|
|
||||||
|
def test_pinned_no_attack(self) -> None:
|
||||||
|
hero = _make_hero(bab=6)
|
||||||
|
foe = _make_foe()
|
||||||
|
engine, h, f = _make_engine(hero, foe, [10, 10, 4])
|
||||||
|
h.active_conditions.append(
|
||||||
|
ActiveCondition(condition=PINNED, rounds_left=5, source="test")
|
||||||
|
)
|
||||||
|
engine._roll_initiative()
|
||||||
|
engine._take_turn(h)
|
||||||
|
for entry in engine._transcript:
|
||||||
|
assert "sword vs foe" not in entry, (
|
||||||
|
f"pinned combatant should not attack: {entry}"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ── Test 5: paralyzed → no actions + effective DEX 0 (-4 AC) ─────────────────
|
# ── Test 5: paralyzed → no actions + effective DEX 0 (-4 AC) ─────────────────
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -698,15 +698,17 @@ def test_grappled_attacker_takes_minus2_on_trip_cmb() -> None:
|
|||||||
assert engine._transcript[-1].endswith("-> FAILED")
|
assert engine._transcript[-1].endswith("-> FAILED")
|
||||||
|
|
||||||
|
|
||||||
def test_pinned_attacker_takes_minus2_on_trip_cmb() -> None:
|
def test_pinned_attacker_cannot_attempt_maneuvers() -> None:
|
||||||
# Same -2 applies to a pinned creature (SRD Conditions - Pinned).
|
# SRD Conditions - Pinned: a pinned creature is tightly bound and can
|
||||||
|
# only attempt to escape — the trip is refused outright (cant_attack),
|
||||||
|
# so the -2 CMB never even rolls.
|
||||||
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
|
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
|
||||||
b = make_state(make_combatant("b"), "monsters", (1, 2))
|
b = make_state(make_combatant("b"), "monsters", (1, 2))
|
||||||
_conditioned(a, "pinned")
|
_conditioned(a, "pinned")
|
||||||
engine = make_engine([1, 10], [a, b])
|
engine = make_engine([1, 10], [a, b])
|
||||||
engine._execute_maneuver(a, b, "trip")
|
engine._execute_maneuver(a, b, "trip")
|
||||||
assert not any(c.condition.name == "prone" for c in b.active_conditions)
|
assert not any(c.condition.name == "prone" for c in b.active_conditions)
|
||||||
assert engine._transcript[-1].endswith("-> FAILED")
|
assert "wait (cannot attack)" in engine._transcript[-1]
|
||||||
|
|
||||||
|
|
||||||
def test_grappled_attacker_grapple_check_is_exempt() -> None:
|
def test_grappled_attacker_grapple_check_is_exempt() -> None:
|
||||||
|
|||||||
Reference in New Issue
Block a user