fix(rules): apply maneuver failure consequences

PF1e (SRD Trip): "If your attack fails by 10 or more, you are knocked prone instead" - a trip whose total lands at CMD-10 or below now knocks the attacker prone (1 round). PF1e (SRD Overrun): "If your overrun attempt fails, you stop in the space directly in front of the opponent, or the nearest open space in front of the creature" - a failed overrun from beyond adjacency walks the attacker to the free approach cell nearest the target; an already-adjacent attacker stays put. 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:
2026-09-12 09:34:57 +02:00
parent 346e300eab
commit 1b60cd049b
3 changed files with 106 additions and 1 deletions
+45
View File
@@ -261,6 +261,7 @@ _AID_ANOTHER_BONUS = 2 # PF1e: a successful aid grants +2 (next attack or AC)
_RUN_SPEED_FACTOR = 4 # PF1e: running is a full-round action at 4x speed
_OVERRUN_PRONE_MARGIN = 5 # PF1e: overrun knocks prone only on a 5+ CMB margin
_BULL_RUSH_EXTRA_PER_MARGIN = 5 # PF1e: +5 ft push per 5 points above CMD
_TRIP_RETALIATION_MARGIN = 10 # PF1e: trip failing by 10+ knocks the attacker prone
_SAVE_DESCRIPTOR_CONDITIONS = frozenset({
"fear", "cold", "electricity", "fey", "divination", "poison", "disease",
"mind_affecting",
@@ -4119,6 +4120,20 @@ class CombatEngine:
) -> None:
if not m.success:
self._maneuver_fail(state, target, "trip", m)
# PF1e (SRD Trip): "If your attack fails by 10 or more, you are
# knocked prone instead."
if m.total <= m.target_cmd - _TRIP_RETALIATION_MARGIN:
state.active_conditions.append(
ActiveCondition(
condition=CONDITIONS_BY_NAME["prone"],
rounds_left=1,
source="maneuver: trip retaliation",
)
)
self._log(
f"round {self._current_round} {state.combatant.id}: "
f"trip backfire -> attacker prone"
)
return
target.active_conditions.append(
ActiveCondition(
@@ -4264,6 +4279,7 @@ class CombatEngine:
) -> None:
if not m.success:
self._maneuver_fail(state, target, "overrun", m)
self._overrun_fail_stop(state, target)
return
# PF1e (CRB "Overrun"): a successful maneuver moves you through the
# target's square; the target is knocked prone only if the attack
@@ -4293,6 +4309,35 @@ class CombatEngine:
f"CMD {m.target_cmd} -> {outcome}"
)
def _overrun_fail_stop(
self, state: CombatantState, target: CombatantState
) -> None:
"""Park a failed overrun on the open approach cell in front of the target.
PF1e (SRD Overrun): "If your overrun attempt fails, you stop in the
space directly in front of the opponent, or the nearest open space
in front of the creature if there are other creatures occupying that
space." An already-adjacent attacker is in front: no move, no log.
"""
ar, ac = state.pos
tr, tc = target.pos
steps = max(abs(tr - ar), abs(tc - ac))
if steps <= 1:
return
dr = 1 if tr - ar > 0 else (-1 if tr - ar < 0 else 0)
dc = 1 if tc - ac > 0 else (-1 if tc - ac < 0 else 0)
for i in range(steps - 1, 0, -1):
cell = (ar + dr * i, ac + dc * i)
if self._cells_free(self._footprint_at(cell, state), state):
start = state.pos
state.pos = cell
self._log(
f"round {self._current_round} {state.combatant.id}: "
f"overrun blocked, stopped ({start[0]},{start[1]})->"
f"({cell[0]},{cell[1]})"
)
return
def _maneuver_dirty_trick(
self,
state: CombatantState,