feat(combat): support multi-cell occupancy for large and larger creatures

This commit is contained in:
2026-09-05 16:01:28 +02:00
parent 7f57df16a6
commit 30f8905a3e
5 changed files with 915 additions and 189 deletions
+49 -13
View File
@@ -67,18 +67,17 @@ def _segment_hits_cell(p1: Pt, p2: Pt, row: int, col: int) -> bool:
class _LineGrid:
"""Bound view for corner-line checks between two squares of one grid."""
"""Bound view for corner-line checks between two sets of squares of one grid."""
def __init__(
self,
grid: Grid,
a: Pos,
b: Pos,
exclude: frozenset[Pos],
blocking: Callable[[TerrainType], bool],
occupied: frozenset[Pos] = frozenset(),
) -> None:
self._grid = grid
self._exclude = frozenset({a, b})
self._exclude = exclude
self._blocking = blocking
self._occupied = occupied
@@ -105,13 +104,23 @@ def _grants_cover(terrain: TerrainType) -> bool:
def has_line_of_effect(grid: Grid, a: Pos, b: Pos) -> bool:
"""LoE iff some corner of a reaches some corner of b unblocked."""
if a == b:
return has_line_of_effect_cells(grid, frozenset({a}), frozenset({b}))
def has_line_of_effect_cells(
grid: Grid, a_cells: frozenset[Pos], b_cells: frozenset[Pos]
) -> bool:
"""LoE iff some corner of some cell of ``a_cells`` reaches some corner of
some cell of ``b_cells`` unblocked."""
if a_cells & b_cells:
return True
lines = _LineGrid(grid, a, b, lambda terrain: terrain.blocks_los)
lines = _LineGrid(grid, a_cells | b_cells, lambda terrain: terrain.blocks_los)
return any(
not lines.blocked(corner_a, corner_b)
for corner_a in _corners(a)
for corner_b in _corners(b)
for cell_a in a_cells
for corner_a in _corners(cell_a)
for cell_b in b_cells
for corner_b in _corners(cell_b)
)
@@ -130,16 +139,43 @@ def has_cover(
ranged attacks, squares occupied by other creatures also block the
lines (soft cover, CRB); melee ignores creatures.
"""
if attacker == target:
return has_cover_cells(
grid,
frozenset({attacker}),
frozenset({target}),
ranged=ranged,
occupied=occupied,
)
def has_cover_cells(
grid: Grid,
attacker_cells: frozenset[Pos],
target_cells: frozenset[Pos],
*,
ranged: bool,
occupied: frozenset[Pos] = frozenset(),
) -> bool:
"""Cover iff every attacker corner has at least one blocked target corner.
The attacker picks their single best cell (and best corner within it);
the target's corners are the union over all its cells. For 1x1 creatures
this is exactly the single-cell corner rule.
"""
if attacker_cells & target_cells:
return False
lines = _LineGrid(
grid,
attacker,
target,
attacker_cells | target_cells,
_grants_cover,
occupied if ranged else frozenset(),
)
return not any(
all(not lines.blocked(corner_a, corner_t) for corner_t in _corners(target))
for corner_a in _corners(attacker)
all(
not lines.blocked(corner_a, corner_t)
for cell_t in target_cells
for corner_t in _corners(cell_t)
)
for cell_a in attacker_cells
for corner_a in _corners(cell_a)
)