"""Corner-rule line of effect and cover. Squares are unit cells on an integer lattice: cell (row, col) spans x in [col, col+1], y in [row, row+1]. A line is blocked when its RELATIVE INTERIOR touches a blocking square's closure (interior, border, or vertex); lines merely starting or ending on a blocking vertex do not count. All math is exact integer arithmetic: parameter t is kept as (num, den) pairs, no floats anywhere. """ from __future__ import annotations from typing import TYPE_CHECKING if TYPE_CHECKING: from collections.abc import Callable from pf1e_simulator.grid import Grid from pf1e_simulator.map import Pos, TerrainType Pt = tuple[int, int] # lattice point (x, y) = (col, row) _Rat = tuple[int, int] # exact rational as (numerator, denominator), den > 0 def _corners(pos: Pos) -> tuple[Pt, Pt, Pt, Pt]: row, col = pos return ((col, row), (col + 1, row), (col, row + 1), (col + 1, row + 1)) def _norm(num: int, den: int) -> _Rat: return (num, den) if den > 0 else (-num, -den) def _le(a: _Rat, b: _Rat) -> bool: return a[0] * b[1] <= b[0] * a[1] def _lt(a: _Rat, b: _Rat) -> bool: return a[0] * b[1] < b[0] * a[1] def _slab(p1: Pt, delta: Pt, lo: int, hi: int, axis: int) -> tuple[_Rat, _Rat] | None: """Closed t-interval where p1 + t*delta lies within [lo, hi] on `axis`.""" start = p1[axis] step = delta[axis] if step == 0: return ((0, 1), (1, 1)) if lo <= start <= hi else None t1 = _norm(lo - start, step) t2 = _norm(hi - start, step) return (t1, t2) if _le(t1, t2) else (t2, t1) def _segment_hits_cell(p1: Pt, p2: Pt, row: int, col: int) -> bool: """True iff the relative interior of p1->p2 touches the closed cell.""" delta = (p2[0] - p1[0], p2[1] - p1[1]) if delta == (0, 0): return False interval_x = _slab(p1, delta, col, col + 1, 0) if interval_x is None: return False interval_y = _slab(p1, delta, row, row + 1, 1) if interval_y is None: return False t_low = interval_x[0] if _le(interval_y[0], interval_x[0]) else interval_y[0] t_high = interval_x[1] if _le(interval_x[1], interval_y[1]) else interval_y[1] return _le(t_low, t_high) and _lt((0, 1), t_high) and _lt(t_low, (1, 1)) class _LineGrid: """Bound view for corner-line checks between two sets of squares of one grid.""" def __init__( self, grid: Grid, exclude: frozenset[Pos], blocking: Callable[[TerrainType], bool], occupied: frozenset[Pos] = frozenset(), ) -> None: self._grid = grid self._exclude = exclude self._blocking = blocking self._occupied = occupied def blocked(self, p1: Pt, p2: Pt) -> bool: min_x = min(p1[0], p2[0]) max_x = max(p1[0], p2[0]) min_y = min(p1[1], p2[1]) max_y = max(p1[1], p2[1]) for col in range(min_x - 1, max_x + 1): for row in range(min_y - 1, max_y + 1): pos = (row, col) if pos in self._exclude or not self._grid.in_bounds(pos): continue if ( pos in self._occupied or self._blocking(self._grid.terrain_at(pos)) ) and _segment_hits_cell(p1, p2, row, col): return True return False def _grants_cover(terrain: TerrainType) -> bool: return terrain.cover or terrain.blocks_los def has_line_of_effect(grid: Grid, a: Pos, b: Pos) -> bool: """LoE iff some corner of a reaches some corner of b unblocked.""" 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_cells | b_cells, lambda terrain: terrain.blocks_los) return any( not lines.blocked(corner_a, corner_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) ) def has_cover( grid: Grid, attacker: Pos, target: 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 corner (PF1e ranged cover rule); Phase 0 documented deviation: melee reuses the same corner rule. For ranged attacks, squares occupied by other creatures also block the lines (soft cover, CRB); melee ignores creatures. """ 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_cells | target_cells, _grants_cover, occupied if ranged else frozenset(), ) return not any( 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) )