fix(rules): scale the bull rush push distance with the margin
PF1e (SRD Bull Rush): "your target is pushed back 5 feet. For every 5 by which your attack exceeds your opponent's CMD you can push the target back an additional 5 feet." _bull_rush_push (and the explicit-direction branch) now walk 1 + margin//5 cells, stopping at the first obstacle; a natural-20 auto-success below CMD floors the margin at 0 (base push only). 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:
@@ -260,6 +260,7 @@ _AID_ANOTHER_DC = 10 # PF1e: aid another is an attack roll against AC 10
|
||||
_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
|
||||
_SAVE_DESCRIPTOR_CONDITIONS = frozenset({
|
||||
"fear", "cold", "electricity", "fey", "divination", "poison", "disease",
|
||||
"mind_affecting",
|
||||
@@ -4165,13 +4166,21 @@ class CombatEngine:
|
||||
if not m.success:
|
||||
self._maneuver_fail(state, target, "bull_rush", m)
|
||||
return
|
||||
# PF1e (SRD Bull Rush): "your target is pushed back 5 feet. For every
|
||||
# 5 by which your attack exceeds your opponent's CMD you can push the
|
||||
# target back an additional 5 feet." A natural-20 auto-success below
|
||||
# CMD still gets only the base 5 ft, hence the margin floor of 0.
|
||||
distance = 1 + max(0, m.total - m.target_cmd) // _BULL_RUSH_EXTRA_PER_MARGIN
|
||||
if direction is not None:
|
||||
new_pos = (target.pos[0] + direction[0], target.pos[1] + direction[1])
|
||||
pushed = self._cells_free(self._footprint_at(new_pos, target), target)
|
||||
if pushed:
|
||||
pushed = False
|
||||
for _ in range(distance):
|
||||
new_pos = (target.pos[0] + direction[0], target.pos[1] + direction[1])
|
||||
if not self._cells_free(self._footprint_at(new_pos, target), target):
|
||||
break
|
||||
target.pos = new_pos
|
||||
pushed = True
|
||||
else:
|
||||
pushed = self._bull_rush_push(state, target)
|
||||
pushed = self._bull_rush_push(state, target, distance)
|
||||
outcome = "SUCCESS (pushed)" if pushed else "SUCCESS (no space)"
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: bull rush vs "
|
||||
@@ -6154,8 +6163,14 @@ class CombatEngine:
|
||||
"""Size modifier for CMB/CMD: Medium=0, Small=-1, Large=1, etc."""
|
||||
return _SIZE_MODIFIERS.get(state.combatant.size.lower(), 0)
|
||||
|
||||
def _bull_rush_push(self, attacker: CombatantState, target: CombatantState) -> bool:
|
||||
"""Push target 5ft away from attacker. Returns True if push succeeded."""
|
||||
def _bull_rush_push(
|
||||
self, attacker: CombatantState, target: CombatantState, distance: int
|
||||
) -> bool:
|
||||
"""Push target up to ``distance`` cells away from attacker.
|
||||
|
||||
The push stops at the first obstacle; returns True if the target
|
||||
moved at least one cell.
|
||||
"""
|
||||
# Calculate direction away from attacker
|
||||
ar, ac = attacker.pos
|
||||
tr, tc = target.pos
|
||||
@@ -6166,11 +6181,14 @@ class CombatEngine:
|
||||
dr = 1 if dr > 0 else -1
|
||||
if dc != 0:
|
||||
dc = 1 if dc > 0 else -1
|
||||
new_pos = (tr + dr, tc + dc)
|
||||
if not self._cells_free(self._footprint_at(new_pos, target), target):
|
||||
return False
|
||||
target.pos = new_pos
|
||||
return True
|
||||
moved = False
|
||||
for _ in range(distance):
|
||||
new_pos = (target.pos[0] + dr, target.pos[1] + dc)
|
||||
if not self._cells_free(self._footprint_at(new_pos, target), target):
|
||||
break
|
||||
target.pos = new_pos
|
||||
moved = True
|
||||
return moved
|
||||
|
||||
def _beyond_cell(self, attacker_pos: Pos, target_pos: Pos) -> Pos:
|
||||
"""The 1-cell position on the far side of the target from the attacker."""
|
||||
|
||||
Reference in New Issue
Block a user