fix(rules): give Tiny-or-smaller creatures zero reach and square sharing
This commit is contained in:
@@ -306,8 +306,15 @@ def _size_rank(size: str) -> int:
|
||||
return _SIZE_ORDER.index("medium")
|
||||
|
||||
# PF1e natural reach for tall creatures (simplification: no long/quadruped
|
||||
# distinction). Unknown sizes fall back to 5 ft (the historical behavior).
|
||||
# distinction). Tiny, Diminutive and Fine have a natural reach of 0 ft (SRD
|
||||
# "Big and Little Creatures in Combat"): "Creatures that take up less than 1
|
||||
# square of space typically have a natural reach of 0 feet, meaning they can't
|
||||
# reach into adjacent squares." Unknown sizes fall back to 5 ft (the historical
|
||||
# behavior).
|
||||
_SIZE_NATURAL_REACH_FT: dict[str, int] = {
|
||||
"Fine": 0,
|
||||
"Diminutive": 0,
|
||||
"Tiny": 0,
|
||||
"Small": 5,
|
||||
"Medium": 5,
|
||||
"Large": 10,
|
||||
@@ -316,6 +323,28 @@ _SIZE_NATURAL_REACH_FT: dict[str, int] = {
|
||||
"Colossal": 30,
|
||||
}
|
||||
|
||||
# PF1e co-occupancy (SRD "Big and Little Creatures in Combat"): "Very small
|
||||
# creatures take up less than 1 square of space. This means that more than one
|
||||
# such creature can fit into a single square" and "They must enter an
|
||||
# opponent's square to attack in melee" — a Tiny-or-smaller creature shares a
|
||||
# square with a strictly larger one (and the larger one may move through and
|
||||
# end in the smaller's square). Small and larger creatures occupy a full
|
||||
# square and never share. The rank ceiling for "takes up less than 1 square".
|
||||
_TINY_OR_SMALLER_MAX_RANK: int = _size_rank("Tiny")
|
||||
|
||||
|
||||
def _can_cooccupy(size_a: str, size_b: str) -> bool:
|
||||
"""True when two sizes may share a single grid square (PF1e co-occupancy).
|
||||
|
||||
Only a Tiny-or-smaller creature ("less than 1 square") and a STRICTLY
|
||||
larger creature share; two same-size creatures never do.
|
||||
"""
|
||||
rank_a = _size_rank(size_a)
|
||||
rank_b = _size_rank(size_b)
|
||||
if rank_a == rank_b:
|
||||
return False
|
||||
return min(rank_a, rank_b) <= _TINY_OR_SMALLER_MAX_RANK
|
||||
|
||||
# Number of grid cells per side of a creature's footprint (square footprint).
|
||||
# Unknown sizes fall back to 1 (the historical single-cell behavior).
|
||||
SIZE_CELLS: dict[str, int] = {
|
||||
@@ -1378,6 +1407,7 @@ class CombatEngine:
|
||||
"name": state.combatant.name,
|
||||
"level": state.combatant.level,
|
||||
"size": state.combatant.size,
|
||||
"natural_reach_ft": _SIZE_NATURAL_REACH_FT.get(state.combatant.size, 5),
|
||||
"aware": state.combatant.aware,
|
||||
"creature_type": state.combatant.creature_type,
|
||||
"bab": state.combatant.bab,
|
||||
@@ -2279,25 +2309,44 @@ class CombatEngine:
|
||||
)
|
||||
|
||||
def _blocked(self, *exclude: CombatantState) -> frozenset[Pos]:
|
||||
"""Union of occupied cells of all active states except those in ``exclude``."""
|
||||
return frozenset(
|
||||
cell
|
||||
for s in self._states
|
||||
if s.active and all(s is not e for e in exclude)
|
||||
for cell in s.occupied_cells()
|
||||
)
|
||||
"""Occupied cells that block the mover (the first excluded state).
|
||||
|
||||
Union of the active creatures' cells except those in ``exclude``. PF1e
|
||||
co-occupancy (SRD "Big and Little Creatures in Combat") lets a
|
||||
Tiny-or-smaller creature share a square with a strictly larger one, so
|
||||
a co-occupiable creature's cells do NOT block the mover (it may move
|
||||
through and end on them); two same-size creatures (or two Small-or-
|
||||
larger) never share, so their cells still block.
|
||||
"""
|
||||
mover = exclude[0] if exclude else None
|
||||
blocked: set[Pos] = set()
|
||||
for s in self._states:
|
||||
if not s.active or any(s is e for e in exclude):
|
||||
continue
|
||||
if mover is not None and _can_cooccupy(
|
||||
mover.combatant.size, s.combatant.size
|
||||
):
|
||||
continue
|
||||
blocked |= set(s.occupied_cells())
|
||||
return frozenset(blocked)
|
||||
|
||||
def _blocked_hostile(self, mover: CombatantState) -> frozenset[Pos]:
|
||||
"""Occupied cells of active creatures hostile to ``mover``.
|
||||
|
||||
Same-side allies are excluded: PF1e (SRD Movement, Position and
|
||||
Distance) — "You can move through a square occupied by a friendly
|
||||
character, unless you are charging."
|
||||
character, unless you are charging." Co-occupiable creatures (a
|
||||
Tiny-or-smaller mover and a strictly larger occupant, or the reverse)
|
||||
are excluded too — the mover may pass through and end on their squares
|
||||
(SRD "Big and Little Creatures in Combat").
|
||||
"""
|
||||
return frozenset(
|
||||
cell
|
||||
for s in self._states
|
||||
if s.active and s is not mover and s.side != mover.side
|
||||
if s.active
|
||||
and s is not mover
|
||||
and s.side != mover.side
|
||||
and not _can_cooccupy(mover.combatant.size, s.combatant.size)
|
||||
for cell in s.occupied_cells()
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user