fix(rules): adjacent melee cover uses the SRD any-line rule (cover.md: 'any line from any corner of your square to the target's square goes through a wall') - the gate previously applied the ranged best-corner rule to distance-1 melee, systematically under-granting cover; non-adjacent melee (reach) keeps the ranged rule, bonus scoring aligned per branch, README 'pire coin' wording updated
This commit is contained in:
+110
-45
@@ -29,6 +29,22 @@ def _corners(pos: Pos) -> tuple[Pt, Pt, Pt, Pt]:
|
||||
return ((col, row), (col + 1, row), (col, row + 1), (col + 1, row + 1))
|
||||
|
||||
|
||||
def _adjacent(a: Pos, b: Pos) -> bool:
|
||||
"""True if the two squares touch (share an edge or a corner)."""
|
||||
return max(abs(a[0] - b[0]), abs(a[1] - b[1])) == 1
|
||||
|
||||
|
||||
def _melee_is_adjacent(
|
||||
attacker_cells: frozenset[Pos], target_cells: frozenset[Pos]
|
||||
) -> bool:
|
||||
"""True if some attacker square touches some target square."""
|
||||
return any(
|
||||
_adjacent(cell_a, cell_t)
|
||||
for cell_a in attacker_cells
|
||||
for cell_t in target_cells
|
||||
)
|
||||
|
||||
|
||||
def _norm(num: int, den: int) -> _Rat:
|
||||
return (num, den) if den > 0 else (-num, -den)
|
||||
|
||||
@@ -176,12 +192,13 @@ def has_cover(
|
||||
ranged: bool,
|
||||
occupied: frozenset[Pos] = frozenset(),
|
||||
) -> bool:
|
||||
"""Cover iff every attacker corner has at least one blocked target corner.
|
||||
"""Cover with the SRD corner rule (see has_cover_cells).
|
||||
|
||||
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.
|
||||
Ranged (and non-adjacent melee): the attacker picks their single best
|
||||
corner (PF1e ranged cover rule). Adjacent melee: the SRD any-line rule
|
||||
(cover.md: "any line from any corner of your square to the target's
|
||||
square"). For ranged attacks, squares occupied by other creatures also
|
||||
block the lines (soft cover, CRB); melee ignores creatures.
|
||||
"""
|
||||
return has_cover_cells(
|
||||
grid,
|
||||
@@ -200,15 +217,22 @@ def has_cover_cells(
|
||||
ranged: bool,
|
||||
occupied: frozenset[Pos] = frozenset(),
|
||||
) -> bool:
|
||||
"""Cover iff every attacker corner has at least one blocked target corner.
|
||||
"""Cover for an attack between two sets of cells, per the SRD corner rules.
|
||||
|
||||
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.
|
||||
Ranged (and non-adjacent melee, which cover.md sends to the ranged
|
||||
rule): the attacker picks their single best cell (and best corner
|
||||
within it), so cover exists only when every attacker corner has at
|
||||
least one blocked target corner. Adjacent melee (some attacker square
|
||||
touching some target square): cover.md grants cover if ANY line from
|
||||
ANY corner of the attacker's square to the target's square goes
|
||||
through a wall, so cover exists as soon as one corner line is blocked.
|
||||
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. For ranged attacks, squares occupied
|
||||
by other creatures block the lines (soft cover); melee ignores them.
|
||||
"""
|
||||
if attacker_cells & target_cells:
|
||||
return False
|
||||
@@ -228,6 +252,27 @@ def has_cover_cells(
|
||||
for cell_a in attacker_cells
|
||||
for corner_a in _corners(cell_a)
|
||||
)
|
||||
if _melee_is_adjacent(attacker_cells, target_cells):
|
||||
# SRD cover.md, adjacent melee: "your target has cover if any line
|
||||
# from any corner of your square to the target's square goes
|
||||
# through a wall". One blocked corner line is enough. With a
|
||||
# multi-square attacker the defender still picks the pair it
|
||||
# attacks through (CRB "Big Creatures and Cover" applies to both
|
||||
# sides), so cover exists only when EVERY touching attacker /
|
||||
# target square pair has at least one blocked corner line.
|
||||
return all(
|
||||
any(
|
||||
lines.blocked(corner_a, corner_t)
|
||||
for corner_a in _corners(cell_a)
|
||||
for corner_t in _corners(cell_t)
|
||||
)
|
||||
for cell_a in attacker_cells
|
||||
for cell_t in target_cells
|
||||
if _adjacent(cell_a, cell_t)
|
||||
)
|
||||
# Non-adjacent melee (reach weapons): cover.md sends these to the
|
||||
# ranged rule, but the attacker may still pick any single square of a
|
||||
# multi-square target (CRB "Big Creatures and Cover").
|
||||
return not any(
|
||||
all(
|
||||
not lines.blocked(corner_a, corner_t)
|
||||
@@ -268,15 +313,15 @@ def cover_bonus_cells(
|
||||
"""Effective cover bonus (0/2/4/8) for an attack between two sets of 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, 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.
|
||||
otherwise the cover level scored under the same rule the gate uses:
|
||||
ranged and non-adjacent melee score each attacker corner by the
|
||||
maximum over the target-corner lines and the attacker keeps its BEST
|
||||
corner (and best own cell); adjacent melee scores each touching
|
||||
attacker/target square pair by the maximum over ALL its corner lines
|
||||
(any blocked line grants cover) and the attacker keeps its best pair
|
||||
of 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:
|
||||
return 0
|
||||
@@ -291,36 +336,56 @@ def cover_bonus_cells(
|
||||
occupied if ranged else frozenset(),
|
||||
bonus_of=terrain_cover_bonus,
|
||||
)
|
||||
# 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. Ranged:
|
||||
# "choose a corner of your square. If any line from this corner to any
|
||||
# corner of the target's square passes through cover, the target has
|
||||
# cover" — each attacker corner is scored by the MAXIMUM over the target's
|
||||
# corner lines (union over all target cells), and the attacker picks its
|
||||
# BEST corner: MIN over attacker corners. Melee: the attacker may also
|
||||
# pick any single square of a multi-square target (CRB "Big Creatures and
|
||||
# Cover"), so the target side is a MIN over cells; the attacker side
|
||||
# stays the worst-corner rule ("any line from ANY corner of your
|
||||
# square").
|
||||
per_cell_best = (
|
||||
min(
|
||||
# PF1e CRB "Cover", rule of corners. Ranged: "choose a corner of your
|
||||
# square. If any line from this corner to any corner of the target's
|
||||
# square passes through cover, the target has cover" - each attacker
|
||||
# corner is scored by the MAXIMUM over the target's corner lines
|
||||
# (union over all target cells), and the attacker picks its BEST
|
||||
# corner: MIN over attacker corners, then MIN over its own cells
|
||||
# (B20). Adjacent melee: "any line from ANY corner of your square to
|
||||
# the target's square" - within a chosen square pair the level is the
|
||||
# MAXIMUM over all sixteen corner lines, and the attacker picks its
|
||||
# best pair: MIN over cells on both sides. Non-adjacent melee
|
||||
# (reach): cover.md sends the attack to the ranged rule, but the
|
||||
# attacker may pick any single square of a multi-square target (CRB
|
||||
# "Big Creatures and Cover"), so the target side is a MIN over cells
|
||||
# while the attacker side stays the best-corner MIN.
|
||||
adjacent_melee = not ranged and _melee_is_adjacent(attacker_cells, target_cells)
|
||||
if adjacent_melee:
|
||||
return min(
|
||||
max(
|
||||
lines.bonus(corner_a, corner_t)
|
||||
for corner_a in _corners(cell_a)
|
||||
for corner_t in _corners(cell_t)
|
||||
)
|
||||
for cell_a in attacker_cells
|
||||
for cell_t in target_cells
|
||||
if _adjacent(cell_a, cell_t)
|
||||
)
|
||||
if not ranged
|
||||
else min(
|
||||
max(
|
||||
lines.bonus(corner_a, corner_t)
|
||||
for cell_t in target_cells
|
||||
for corner_t in _corners(cell_t)
|
||||
if ranged:
|
||||
per_cell_best = (
|
||||
min(
|
||||
max(
|
||||
lines.bonus(corner_a, corner_t)
|
||||
for cell_t in target_cells
|
||||
for corner_t in _corners(cell_t)
|
||||
)
|
||||
for corner_a in _corners(cell_a)
|
||||
)
|
||||
for corner_a in _corners(cell_a)
|
||||
for cell_a in attacker_cells
|
||||
)
|
||||
else:
|
||||
per_cell_best = (
|
||||
min(
|
||||
min(
|
||||
max(
|
||||
lines.bonus(corner_a, corner_t)
|
||||
for corner_t in _corners(cell_t)
|
||||
)
|
||||
for corner_a in _corners(cell_a)
|
||||
)
|
||||
for cell_t in target_cells
|
||||
)
|
||||
for cell_a in attacker_cells
|
||||
)
|
||||
for cell_a in attacker_cells
|
||||
)
|
||||
return min(per_cell_best)
|
||||
|
||||
Reference in New Issue
Block a user