fix(rules): make the run reachability check avoid difficult terrain
PF1e (CRB Movement): "You can't run or charge across difficult terrain". _execute_run checked the explicit destination against the full cost field (which routes through difficult terrain at 2x) but descended with avoid_difficult=True, so a destination beyond rubble was accepted (ok=True) yet silently truncated at the obstacle. _destination_reachable now takes the same avoid_difficult constraint (difficult cells removed from the field; a destination whose footprint touches difficult terrain is not a legal run endpoint either), so verdict and path agree. The test that pinned the old silent-stop-short convention now expects unreachable_destination; 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:
@@ -3680,7 +3680,8 @@ class CombatEngine:
|
||||
already applies to any other blockage). On completion the combatant
|
||||
is ``running`` until the start of its next turn, which denies it its
|
||||
Dexterity bonus to AC. An explicit destination that cannot be
|
||||
honoured within the 4x budget is rejected with
|
||||
honoured within the 4x budget WITHOUT crossing difficult terrain (the
|
||||
same constraint the descent applies) is rejected with
|
||||
``unreachable_destination``; with neither target nor destination the
|
||||
U1 gate rejects the action with ``missing_target``.
|
||||
"""
|
||||
@@ -3697,7 +3698,7 @@ class CombatEngine:
|
||||
if action.destination is not None:
|
||||
reachable = self._destination_reachable(
|
||||
state, action.destination, speed_factor=_RUN_SPEED_FACTOR,
|
||||
friendly_passable=True,
|
||||
friendly_passable=True, avoid_difficult=True,
|
||||
)
|
||||
if not reachable:
|
||||
self._log(
|
||||
@@ -6091,13 +6092,17 @@ class CombatEngine:
|
||||
*,
|
||||
speed_factor: int = 1,
|
||||
friendly_passable: bool = False,
|
||||
avoid_difficult: bool = False,
|
||||
) -> bool:
|
||||
"""True when destination is passable, unoccupied and within this speed.
|
||||
|
||||
``speed_factor`` widens the budget (run: 4x speed, U5e).
|
||||
``friendly_passable`` routes the cost field around allies' squares
|
||||
(move/run may cross them, SRD Movement); the destination itself must
|
||||
still be completely free.
|
||||
still be completely free. ``avoid_difficult`` removes difficult cells
|
||||
from the cost field, mirroring the descent constraint of a run
|
||||
(CRB: "You can't run or charge across difficult terrain") so the
|
||||
reachability verdict and the walked path agree.
|
||||
"""
|
||||
speed_cells = int(
|
||||
_speed_ft(state)
|
||||
@@ -6109,10 +6114,25 @@ class CombatEngine:
|
||||
return destination == state.pos
|
||||
if not self._cells_free(self._footprint_at(destination, state), state):
|
||||
return False
|
||||
if avoid_difficult and self._footprint_touches_difficult(
|
||||
self._footprint_at(destination, state)
|
||||
):
|
||||
return False
|
||||
blocked = self._blocked_hostile(state) if friendly_passable else self._blocked(state)
|
||||
if avoid_difficult:
|
||||
blocked = blocked | self._difficult_cells()
|
||||
field = self._grid.reachable(state.pos, speed_cells, blocked)
|
||||
return destination in field
|
||||
|
||||
def _difficult_cells(self) -> frozenset[Pos]:
|
||||
"""Every passable cell on the grid whose terrain costs more than one."""
|
||||
return frozenset(
|
||||
(row, col)
|
||||
for row in range(self._grid.height)
|
||||
for col in range(self._grid.width)
|
||||
if self._is_difficult_cell((row, col))
|
||||
)
|
||||
|
||||
def _walk_steps(
|
||||
self, state: CombatantState, path: list[Pos]
|
||||
) -> list[Pos]:
|
||||
|
||||
Reference in New Issue
Block a user