fix(rules): let a natural 20 auto-succeed on escape
PF1e (SRD Combat Maneuvers): "Rolling a natural 20 while attempting a combat maneuver is always a success (except when attempting to escape from bonds)" - escaping a grapple is a combat maneuver, not bonds. _maneuver_escape now auto-succeeds on a natural 20 below CMD (the natural 1 auto-failure is unchanged). README updated. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -122,6 +122,46 @@ def test_escape_from_grapple_consumes_single_d20() -> None:
|
||||
assert not any(c.condition.name == "grappled" for c in a.active_conditions)
|
||||
|
||||
|
||||
def _grappled_by(a: CombatantState, b: CombatantState) -> None:
|
||||
a.active_conditions.append(
|
||||
ActiveCondition(
|
||||
condition=CONDITIONS_BY_NAME["grappled"],
|
||||
rounds_left=1,
|
||||
source="test",
|
||||
)
|
||||
)
|
||||
a.grappled_with = b.combatant.id
|
||||
|
||||
|
||||
def test_escape_natural_20_auto_succeeds_below_cmd() -> None:
|
||||
# SRD Combat Maneuvers: "Rolling a natural 20 while attempting a combat
|
||||
# maneuver is always a success (except when attempting to escape from
|
||||
# bonds)" — a grapple is not bonds. Escape bonus 2, b CMD 30 (bab 20):
|
||||
# 20+2=22 < 30, so only the natural-20 rule can succeed this.
|
||||
a = make_state(make_combatant("a", attack_bonus=2), "players", (1, 1))
|
||||
b = make_state(make_combatant("b", attack_bonus=20), "monsters", (1, 2))
|
||||
_grappled_by(a, b)
|
||||
engine = make_engine([20], [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)
|
||||
|
||||
|
||||
def test_escape_natural_1_still_fails_above_cmd() -> None:
|
||||
# Natural 1 stays an automatic failure even when the total clears CMD:
|
||||
# escape_artist rank 20 (+3 class) gives bonus 23 vs CMD 12.
|
||||
a_spec = make_combatant("a", attack_bonus=2).model_copy(
|
||||
update={"skills": {"escape_artist": SkillRank(rank=20, class_skill=True)}}
|
||||
)
|
||||
a = make_state(a_spec, "players", (1, 1))
|
||||
b = make_state(make_combatant("b"), "monsters", (1, 2))
|
||||
_grappled_by(a, b)
|
||||
engine = make_engine([1], [a, b])
|
||||
engine._maneuver_escape(a, b)
|
||||
assert a.grappled_with == b.combatant.id
|
||||
assert any(c.condition.name == "grappled" for c in a.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