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:
@@ -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