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:
2026-09-11 23:29:59 +02:00
parent 843cbf881c
commit 8a8b813889
3 changed files with 70 additions and 14 deletions
+24
View File
@@ -150,3 +150,27 @@ def test_ranged_touch_attack_still_gets_cover() -> None:
# Touch AC 13 + 4 standard cover: cover is NOT removed for touch.
assert result.ac == 17
assert result.hit is False # 12 + 4 = 16 < 17
# ---------------------------------------------------------------------------
# (f) A5: melee vs a multi-square target picks the BEST target square
# ---------------------------------------------------------------------------
def test_melee_vs_large_target_picks_best_target_square() -> None:
grid = _grid(("..#...", "......", "......"))
attacker = frozenset({(1, 0)})
large_target = frozenset({(0, 3), (0, 4), (1, 3), (1, 4)})
# Ranged: RAW checks ANY line to ANY target corner — the wall covers the
# target's top row, so the worst cell's +4 stands.
assert cover_bonus_cells(grid, attacker, large_target, ranged=True) == 4
# Melee (CRB "Big Creatures and Cover"): "when making a melee attack
# against such a creature, you can pick any of the squares it occupies to
# determine if it has cover against you" — the attacker picks a clear
# bottom-row square, so the target has no cover.
assert cover_bonus_cells(grid, attacker, large_target, ranged=False) == 0
# 1x1 regressions: the aggregation choice is invisible for single cells.
assert cover_bonus_cells(grid, attacker, frozenset({(0, 3)}), ranged=False) == 4
assert cover_bonus_cells(grid, attacker, frozenset({(0, 3)}), ranged=True) == 4
assert cover_bonus_cells(grid, attacker, frozenset({(1, 3)}), ranged=False) == 0
assert cover_bonus_cells(grid, attacker, frozenset({(1, 3)}), ranged=True) == 0