feat(grid): apply elevation to movement cost and high ground
This commit is contained in:
@@ -32,17 +32,39 @@ _DIRECTIONS: tuple[Pos, ...] = (
|
||||
|
||||
|
||||
class Grid:
|
||||
"""Terrain view of a MapSpec with movement and distance queries."""
|
||||
"""Terrain view of a MapSpec with movement and distance queries.
|
||||
|
||||
def __init__(self, cells: tuple[tuple[TerrainType, ...], ...]) -> None:
|
||||
An optional elevation layer stores a height per cell; climbing one level
|
||||
costs one extra square of movement (PF1e), descending is free.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
cells: tuple[tuple[TerrainType, ...], ...],
|
||||
elevation: tuple[tuple[int, ...], ...] | None = None,
|
||||
) -> None:
|
||||
self._cells = cells
|
||||
self._height = len(cells)
|
||||
self._width = len(cells[0]) if cells else 0
|
||||
self._elevation = elevation
|
||||
|
||||
@classmethod
|
||||
def from_spec(cls, spec: MapSpec) -> Grid:
|
||||
cells = tuple(tuple(spec.legend[ch] for ch in row) for row in spec.terrain)
|
||||
return cls(cells)
|
||||
elevation = None
|
||||
if spec.elevation is not None:
|
||||
height = len(cells)
|
||||
width = len(cells[0]) if cells else 0
|
||||
if len(spec.elevation) != height or (
|
||||
height and len(spec.elevation[0]) != width
|
||||
):
|
||||
msg = "elevation layer dimensions do not match terrain"
|
||||
raise ValueError(msg)
|
||||
elevation = tuple(
|
||||
tuple(0 if ch == "." else int(ch) for ch in row)
|
||||
for row in spec.elevation
|
||||
)
|
||||
return cls(cells, elevation=elevation)
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
@@ -63,6 +85,13 @@ class Grid:
|
||||
def passable(self, pos: Pos) -> bool:
|
||||
return self.in_bounds(pos) and self.terrain_at(pos).move_cost is not None
|
||||
|
||||
def elevation_at(self, pos: Pos) -> int:
|
||||
"""Height of a cell; 0 when no elevation layer or out of bounds."""
|
||||
if self._elevation is None or not self.in_bounds(pos):
|
||||
return 0
|
||||
row, col = pos
|
||||
return self._elevation[row][col]
|
||||
|
||||
def distance(self, a: Pos, b: Pos) -> int:
|
||||
"""Distance in squares under the 5-10-5-10 diagonal rule."""
|
||||
d_row = abs(a[0] - b[0])
|
||||
@@ -70,17 +99,23 @@ class Grid:
|
||||
return max(d_row, d_col) + min(d_row, d_col) // 2
|
||||
|
||||
def step_cost(self, frm: Pos, to: Pos, diagonals_used: int) -> int:
|
||||
"""Cost in squares of one step; diagonals_used is the count so far."""
|
||||
"""Cost in squares of one step; diagonals_used is the count so far.
|
||||
|
||||
Climbing one elevation level adds one square per level; descending is
|
||||
free and never reduces the terrain cost.
|
||||
"""
|
||||
cost = self.terrain_at(to).move_cost
|
||||
if cost is None:
|
||||
msg = f"cannot step into impassable square {to}"
|
||||
raise ValueError(msg)
|
||||
is_diagonal = frm[0] != to[0] and frm[1] != to[1]
|
||||
if not is_diagonal:
|
||||
return cost
|
||||
return cost + max(0, self.elevation_at(to) - self.elevation_at(frm))
|
||||
if cost >= _DIFFICULT_TERRAIN_COST:
|
||||
return 3
|
||||
return 1 + (diagonals_used % 2)
|
||||
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)
|
||||
)
|
||||
|
||||
def diagonal_allowed(self, frm: Pos, to: Pos) -> bool:
|
||||
flank_a = (frm[0], to[1])
|
||||
|
||||
Reference in New Issue
Block a user