fix(rules): charge and run walks enforce the diagonal corner rule (SRD measuring-distance.md: 'You can't move diagonally past a corner') - _charge_path_clear and _run_line_path never checked diagonal_allowed, so a charge/run could cut diagonally through a wall corner; both walks now stop/reject a diagonal step whose flanks are not both passable, same predicate the Dijkstra descent uses

This commit is contained in:
2026-09-19 15:44:12 +02:00
parent 27ce33ba67
commit 5e478bfd27
3 changed files with 196 additions and 3 deletions
+34 -2
View File
@@ -3988,13 +3988,33 @@ class CombatEngine:
def _charge_path_clear(
self, path: list[Pos], state: CombatantState
) -> bool:
"""True if the footprint fits at every anchor on the charge path."""
"""True if the footprint fits at every anchor on the charge path.
PF1e (SRD "Measuring Distance"): "You can't move diagonally past a
corner" - a diagonal step whose two flanking squares are not both
passable is illegal, so a charge may never cut a wall corner (the
same corner rule the Dijkstra descent enforces in
``_best_move_step``). PF1e (SRD "Charge"): "If any line from your
starting space to the ending space passes through a square that
blocks movement, slows movement, or contains a creature (even an
ally), you can't charge" - the straight Bresenham anchor line is the
engine's model of that path, walked under the full-footprint gates
below.
"""
prev = state.pos
for pos in path:
if not self._cells_free(self._footprint_at(pos, state), state):
return False
terrain = self._grid.terrain_at(pos)
if terrain.move_cost is not None and terrain.move_cost > 1:
return False
if (
prev[0] != pos[0]
and prev[1] != pos[1]
and not self._grid.diagonal_allowed(prev, pos)
):
return False
prev = pos
return True
def _charge(
@@ -7860,7 +7880,9 @@ class CombatEngine:
speed budget and stopped at the first obstacle, mirroring the charge's
straight-line clear-path logic (CRB "Charge"): impassable terrain, a
difficult-terrain cell (CRB "Difficult Terrain": "You can't run or
charge across difficult terrain"), or a blocking creature. A hostile
charge across difficult terrain"), a diagonal step past a blocked
corner (SRD "Measuring Distance": "You can't move diagonally past a
corner"), or a blocking creature. A hostile
creature (including the target itself) halts the walk one cell short,
so an auto-directed run ends adjacent to the target — except one the
mover may pass through (three size categories apart, SRD "Moving
@@ -7879,6 +7901,16 @@ class CombatEngine:
current = state.pos
diagonals_used = 0
for nxt in _bresenham_line(state.pos, goal):
# PF1e (SRD "Measuring Distance"): "You can't move diagonally
# past a corner" - a diagonal step with a blocked flank is
# illegal, so a run stops at the corner just like an obstacle
# (same rule _best_move_step enforces for the Dijkstra descent).
if (
current[0] != nxt[0]
and current[1] != nxt[1]
and not self._grid.diagonal_allowed(current, nxt)
):
break
footprint = self._footprint_at(nxt, state)
if self._footprint_touches_difficult(footprint):
break