fix(rules): a multi-cell creature moving under mixed terrain pays the difficult rate for its whole footprint - SRD Movement, Position and Distance: 'If you occupy squares with different kinds of terrain, you can move only as fast as the most difficult terrain you occupy will allow'; the charge/run/5-foot-step already gated on the whole footprint (_footprint_touches_difficult) but the move/withdraw DESCENT priced every step by the anchor cell alone (grid.step_cost), so a 2x2 creature straddling rubble crossed at full speed; the descent now prices a step by the most difficult square under the footprint via _footprint_step_cost -> a new grid.step_cost_with_terrain (the 5-10-5 + elevation formula stays centralized in grid.py, step_cost delegates to it); 1x1 creatures are bit-identical (footprint == anchor cell); the direction Dijkstra field remains anchor-priced (making it creature-aware rewrites grid.reachable wholesale - the documented deferral); golden untouched (open-floor arena, no difficult terrain); +7 tests (trailing-square orthogonal/diagonal cost, 1x1 bit-identical regression, full-lane move budget 3-vs-6 squares, withdraw hampering)
This commit is contained in:
@@ -8378,7 +8378,7 @@ class CombatEngine:
|
||||
and not self._has_free_descent(state, nxt, to_target, nxt_cost)
|
||||
):
|
||||
continue
|
||||
step = self._grid.step_cost(current, nxt, diagonals_used)
|
||||
step = self._footprint_step_cost(state, current, nxt, diagonals_used)
|
||||
if step > budget:
|
||||
continue
|
||||
score = step + nxt_cost
|
||||
@@ -8424,6 +8424,30 @@ class CombatEngine:
|
||||
"""True if any footprint cell is difficult terrain (move cost > 1)."""
|
||||
return any(self._is_difficult_cell(cell) for cell in footprint)
|
||||
|
||||
def _footprint_step_cost(
|
||||
self, state: CombatantState, frm: Pos, to: Pos, diagonals_used: int
|
||||
) -> int:
|
||||
"""Cost of one anchor step priced by the WHOLE destination footprint.
|
||||
|
||||
SRD "Movement, Position, and Distance": "If you occupy squares with
|
||||
different kinds of terrain, you can move only as fast as the most
|
||||
difficult terrain you occupy will allow." A 2x2 creature straddling
|
||||
rubble is hampered even when its ANCHOR square is plain floor, so the
|
||||
step is priced by the most difficult square the footprint would cover
|
||||
at ``to`` (the 5-10-5 + elevation formula stays in ``grid.py``). For a
|
||||
1x1 creature the footprint is the anchor cell, so this is exactly
|
||||
``grid.step_cost`` - bit-identical to the old anchor-only pricing.
|
||||
"""
|
||||
footprint = self._footprint_at(to, state)
|
||||
hardest = 1
|
||||
for cell in footprint:
|
||||
if not self._grid.in_bounds(cell):
|
||||
continue
|
||||
cost = self._grid.terrain_at(cell).move_cost
|
||||
if cost is not None and cost > hardest:
|
||||
hardest = cost
|
||||
return self._grid.step_cost_with_terrain(frm, to, diagonals_used, hardest)
|
||||
|
||||
def _is_difficult_cell(self, pos: Pos) -> bool:
|
||||
"""True for a passable cell whose terrain costs more than one square."""
|
||||
if not self._grid.in_bounds(pos):
|
||||
@@ -8494,7 +8518,7 @@ class CombatEngine:
|
||||
nxt_cost = self._footprint_cost(footprint, from_threat)
|
||||
if nxt_cost is None or nxt_cost <= current_cost:
|
||||
continue
|
||||
step = self._grid.step_cost(current, nxt, diagonals_used)
|
||||
step = self._footprint_step_cost(state, current, nxt, diagonals_used)
|
||||
if step > budget:
|
||||
continue
|
||||
score = -(nxt_cost * 100) + step
|
||||
|
||||
@@ -108,14 +108,29 @@ class Grid:
|
||||
if cost is None:
|
||||
msg = f"cannot step into impassable square {to}"
|
||||
raise ValueError(msg)
|
||||
return self.step_cost_with_terrain(frm, to, diagonals_used, cost)
|
||||
|
||||
def step_cost_with_terrain(
|
||||
self, frm: Pos, to: Pos, diagonals_used: int, cost: int
|
||||
) -> int:
|
||||
"""Cost of one step given an explicit destination terrain ``cost``.
|
||||
|
||||
The same 5-10-5 + elevation rule as ``step_cost`` (the single home of
|
||||
that formula), but the caller supplies the terrain cost instead of
|
||||
reading it from the anchor cell. This lets a multi-cell creature price
|
||||
a step by the MOST DIFFICULT square its whole footprint would occupy
|
||||
(SRD "Movement, Position, and Distance": "If you occupy squares with
|
||||
different kinds of terrain, you can move only as fast as the most
|
||||
difficult terrain you occupy will allow") rather than by the anchor
|
||||
square alone.
|
||||
"""
|
||||
is_diagonal = frm[0] != to[0] and frm[1] != to[1]
|
||||
climb = max(0, self.elevation_at(to) - self.elevation_at(frm))
|
||||
if not is_diagonal:
|
||||
return cost + max(0, self.elevation_at(to) - self.elevation_at(frm))
|
||||
return cost + climb
|
||||
if cost >= _DIFFICULT_TERRAIN_COST:
|
||||
return 3 + max(0, self.elevation_at(to) - self.elevation_at(frm))
|
||||
return 1 + (diagonals_used % 2) + max(
|
||||
0, self.elevation_at(to) - self.elevation_at(frm)
|
||||
)
|
||||
return 3 + climb
|
||||
return 1 + (diagonals_used % 2) + climb
|
||||
|
||||
def diagonal_allowed(self, frm: Pos, to: Pos) -> bool:
|
||||
flank_a = (frm[0], to[1])
|
||||
|
||||
Reference in New Issue
Block a user