fix(rules): scale reposition and drag distance with the margin
PF1e (APG Reposition / Drag): "For every 5 by which your attack exceeds your opponent's CMD, you can move/drag the target an additional 5 feet." Both handlers now walk 1 + margin//5 cells along the chosen (or auto-picked) direction, stopping at the first obstacle or occupied square, mirroring the bull rush scaling. 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:
@@ -264,6 +264,8 @@ _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
|
||||
_DIRTY_TRICK_EXTRA_ROUND_MARGIN = 5 # PF1e APG: +1 round per 5 points above CMD
|
||||
_REPOSITION_EXTRA_PER_MARGIN = 5 # PF1e APG: +5 ft move per 5 points above CMD
|
||||
_DRAG_EXTRA_PER_MARGIN = 5 # PF1e APG: +5 ft drag per 5 points above CMD
|
||||
_TRIP_RETALIATION_MARGIN = 10 # PF1e: trip failing by 10+ knocks the attacker prone
|
||||
_MANEUVER_MAX_SIZE_DIFF = 2 # PF1e: 2+ size categories larger = unmaneuverable
|
||||
_SAVE_DESCRIPTOR_CONDITIONS = frozenset({
|
||||
@@ -4496,30 +4498,30 @@ class CombatEngine:
|
||||
if not m.success:
|
||||
self._maneuver_fail(state, target, "reposition", m)
|
||||
return
|
||||
new_pos: Pos | None = None
|
||||
if direction is not None:
|
||||
pos = (target.pos[0] + direction[0], target.pos[1] + direction[1])
|
||||
if (
|
||||
0 <= pos[0] < self._grid.height
|
||||
and 0 <= pos[1] < self._grid.width
|
||||
and self._cells_free(self._footprint_at(pos, target), target)
|
||||
):
|
||||
new_pos = pos
|
||||
else:
|
||||
# PF1e (APG Reposition): "you may move your target 5 feet to a new
|
||||
# location. For every 5 by which your attack exceeds your opponent's
|
||||
# CMD, you can move the target an additional 5 feet" — the walk stops
|
||||
# at the first obstacle ("You cannot move a creature into a square
|
||||
# that is occupied by a solid object or obstacle").
|
||||
distance = 1 + max(0, m.total - m.target_cmd) // _REPOSITION_EXTRA_PER_MARGIN
|
||||
step = direction
|
||||
if step is None:
|
||||
# First free neighbor (the attacker's own square is occupied, so
|
||||
# it can never be picked here).
|
||||
for dr, dc in _NEIGHBOR_OFFSETS:
|
||||
pos = (target.pos[0] + dr, target.pos[1] + dc)
|
||||
if pos == state.pos:
|
||||
continue
|
||||
if not (0 <= pos[0] < self._grid.height and 0 <= pos[1] < self._grid.width):
|
||||
continue
|
||||
if self._cells_free(self._footprint_at(pos, target), target):
|
||||
new_pos = pos
|
||||
cand = (target.pos[0] + dr, target.pos[1] + dc)
|
||||
if self._cells_free(self._footprint_at(cand, target), target):
|
||||
step = (dr, dc)
|
||||
break
|
||||
if new_pos is not None:
|
||||
target.pos = new_pos
|
||||
outcome = f"SUCCESS (moved to {new_pos})"
|
||||
else:
|
||||
outcome = "SUCCESS (no space)"
|
||||
moved = False
|
||||
if step is not None:
|
||||
for _ in range(distance):
|
||||
pos = (target.pos[0] + step[0], target.pos[1] + step[1])
|
||||
if not self._cells_free(self._footprint_at(pos, target), target):
|
||||
break
|
||||
target.pos = pos
|
||||
moved = True
|
||||
outcome = f"SUCCESS (moved to {target.pos})" if moved else "SUCCESS (no space)"
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: reposition vs "
|
||||
f"{target.combatant.id} d20={m.roll}+{m.attacker_cmb}={m.total} "
|
||||
@@ -4537,20 +4539,30 @@ class CombatEngine:
|
||||
if not m.success:
|
||||
self._maneuver_fail(state, target, "drag", m)
|
||||
return
|
||||
ar, ac = state.pos
|
||||
tr, tc = target.pos
|
||||
adjacent = max(abs(tr - ar), abs(tc - ac)) == 1
|
||||
if direction is not None:
|
||||
beyond = (tr + direction[0], tc + direction[1])
|
||||
else:
|
||||
beyond = self._beyond_cell(state.pos, target.pos)
|
||||
if adjacent and self._cells_free(self._footprint_at(beyond, target), target):
|
||||
# PF1e (APG Drag): "both you and your target are moved 5 feet back,
|
||||
# with your opponent occupying your original space and you in the
|
||||
# space behind that in a straight line. For every 5 by which your
|
||||
# attack exceeds your opponent's CMD, you can drag the target back an
|
||||
# additional 5 feet" — the line stops at the first obstacle.
|
||||
distance = 1 + max(0, m.total - m.target_cmd) // _DRAG_EXTRA_PER_MARGIN
|
||||
dragged = 0
|
||||
for _ in range(distance):
|
||||
ar, ac = state.pos
|
||||
tr, tc = target.pos
|
||||
adjacent = max(abs(tr - ar), abs(tc - ac)) == 1
|
||||
if not adjacent:
|
||||
break
|
||||
if direction is not None:
|
||||
beyond = (tr + direction[0], tc + direction[1])
|
||||
else:
|
||||
beyond = self._beyond_cell(state.pos, target.pos)
|
||||
if not self._cells_free(self._footprint_at(beyond, target), target):
|
||||
break
|
||||
target_old = target.pos
|
||||
target.pos = beyond
|
||||
state.pos = target_old
|
||||
outcome = "SUCCESS (dragged)"
|
||||
else:
|
||||
outcome = "SUCCESS (no space)"
|
||||
dragged += 1
|
||||
outcome = "SUCCESS (dragged)" if dragged else "SUCCESS (no space)"
|
||||
self._log(
|
||||
f"round {self._current_round} {state.combatant.id}: drag vs "
|
||||
f"{target.combatant.id} d20={m.roll}+{m.attacker_cmb}={m.total} "
|
||||
|
||||
Reference in New Issue
Block a user