fix(rules): pick the best target square for melee cover
PF1e (CRB Cover, Big Creatures and Cover): when making a melee attack against a multi-square creature you can pick any of the squares it occupies to determine cover. has_cover_cells/cover_bonus_cells now aggregate the target side by the MINIMUM over target cells for melee and keep the maximum (any line to any corner) for ranged; the attacker-side minimum is unchanged. 1x1 geometry is bit-identical; no data case changes today, locked by a unit test (Large target: melee 0, ranged +4). Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
+38
-11
@@ -202,9 +202,13 @@ def has_cover_cells(
|
||||
) -> 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.
|
||||
The attacker picks their single best cell (and best corner within it).
|
||||
Target side: ranged attacks check ANY line to ANY corner of ANY target
|
||||
cell (union over the target's cells); melee attacks may pick any single
|
||||
square of a multi-square target (CRB "Big Creatures and Cover"), so
|
||||
cover exists only when no attacker corner sees every corner of SOME
|
||||
target cell unblocked. For 1x1 creatures both sides are the single-cell
|
||||
corner rule.
|
||||
"""
|
||||
if attacker_cells & target_cells:
|
||||
return False
|
||||
@@ -214,14 +218,24 @@ def has_cover_cells(
|
||||
_grants_cover,
|
||||
occupied if ranged else frozenset(),
|
||||
)
|
||||
if ranged:
|
||||
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)
|
||||
)
|
||||
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)
|
||||
for cell_t in target_cells
|
||||
)
|
||||
|
||||
|
||||
@@ -255,10 +269,13 @@ def cover_bonus_cells(
|
||||
|
||||
0 when there is no cover (see has_cover_cells for the corner rule);
|
||||
otherwise the cover level of the attacker's BEST occupied cell: each
|
||||
attacker cell is scored by the per-cell corner rule against all of the
|
||||
target's cells, and the attacker picks the lowest-scoring (clearest)
|
||||
cell. Interposed creatures grant soft cover (+4) on ranged attacks only,
|
||||
exactly as in has_cover_cells. For a 1x1 attacker this is exactly the
|
||||
attacker cell is scored by the per-cell corner rule, and the attacker
|
||||
picks the lowest-scoring (clearest) cell. Target side: ranged attacks are
|
||||
scored by the maximum over ALL the target's cells; melee attacks may pick
|
||||
any single square of the target (CRB "Big Creatures and Cover"), so each
|
||||
attacker cell is scored by the MINIMUM over the target's cells. Interposed
|
||||
creatures grant soft cover (+4) on ranged attacks only, exactly as in
|
||||
has_cover_cells. For 1x1 attacker and target this is exactly the
|
||||
single-cell value.
|
||||
"""
|
||||
if not attacker_cells or not target_cells:
|
||||
@@ -277,10 +294,20 @@ def cover_bonus_cells(
|
||||
# PF1e CRB "Cover", rule of corners: the attacker chooses the corner (and,
|
||||
# for a multi-square creature, the occupied square) that gives it the best
|
||||
# shot, so it suffers only the MINIMUM cover over its own cells. The
|
||||
# target side is unchanged: RAW checks ANY line to ANY target corner, so
|
||||
# each attacker cell is scored by the MAXIMUM over its corner lines.
|
||||
# target side follows the same choice rule: RAW checks ANY line to ANY
|
||||
# corner of the (chosen) square, scored by the MAXIMUM over corner lines —
|
||||
# union over all target cells for ranged, best single cell for melee.
|
||||
per_cell_best = (
|
||||
max(
|
||||
min(
|
||||
max(
|
||||
lines.bonus(corner_a, corner_t)
|
||||
for corner_a in _corners(cell_a)
|
||||
for corner_t in _corners(cell_t)
|
||||
)
|
||||
for cell_t in target_cells
|
||||
)
|
||||
if not ranged
|
||||
else max(
|
||||
lines.bonus(corner_a, corner_t)
|
||||
for corner_a in _corners(cell_a)
|
||||
for cell_t in target_cells
|
||||
|
||||
Reference in New Issue
Block a user