fix(rules): bursts cannot affect creatures in total cover from the point of origin (SRD aiming-a-spell.md: a burst "can't affect creatures with total cover from its point of origin (in other words, its effects don't extend around corners)"; Line of Effect: a burst affects only what it has LoE to from its origin) - _area_creatures skips creatures whose occupied cells have no line of effect from the burst center

This commit is contained in:
2026-09-19 14:08:24 +02:00
parent 389e160627
commit 948edc1815
3 changed files with 61 additions and 5 deletions
+2 -1
View File
@@ -29,7 +29,8 @@ PF1e (CRB, Magic — Aiming a Spell):
wall between them shields everything behind it). wall between them shields everything behind it).
Bursts need no template here: they are circles around a center cell and are Bursts need no template here: they are circles around a center cell and are
resolved by ``CombatEngine._creatures_in_range`` (unchanged). resolved by ``CombatEngine._area_creatures`` (which additionally drops
creatures without line of effect from the burst's center).
""" """
from __future__ import annotations from __future__ import annotations
+14 -3
View File
@@ -7116,8 +7116,13 @@ class CombatEngine:
) -> list[CombatantState]: ) -> list[CombatantState]:
"""Creatures intersecting the template of an area effect. """Creatures intersecting the template of an area effect.
Burst: circle of ``radius_ft`` around the resolved center ``aim`` Burst: circle of ``radius_ft`` around the resolved center ``aim``,
(unchanged behaviour). Cone/line: the template originates at the minus every creature that has no line of effect from that center
(PF1e CRB "Aiming a Spell" — Burst: a burst "can't affect creatures
with total cover from its point of origin (in other words, its
effects don't extend around corners)"; CRB "Line of Effect": a burst
"affects only an area, creature, or object to which it has line of
effect from its origin"). Cone/line: the template originates at the
caster's anchor cell and opens toward ``aim`` (PF1e CRB "Aiming a caster's anchor cell and opens toward ``aim`` (PF1e CRB "Aiming a
Spell" — see ``areas.py``); a creature is affected when ANY of its Spell" — see ``areas.py``); a creature is affected when ANY of its
occupied cells lies inside the template. occupied cells lies inside the template.
@@ -7129,7 +7134,13 @@ class CombatEngine:
""" """
span_cells = effect.radius_ft // _SQUARE_FT span_cells = effect.radius_ft // _SQUARE_FT
if effect.shape == "burst": if effect.shape == "burst":
affected = self._creatures_in_range(aim, span_cells) affected = [
state
for state in self._creatures_in_range(aim, span_cells)
if has_line_of_effect_cells(
self._grid, frozenset({aim}), state.occupied_cells()
)
]
else: else:
origin = caster.pos origin = caster.pos
if effect.shape == "cone": if effect.shape == "cone":
+45 -1
View File
@@ -327,10 +327,54 @@ def test_line_behind_a_wall_does_not_hit_sheltered_creatures() -> None:
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# burst: unchanged circle around the resolved center # burst: circle around the center, minus creatures in total cover of it
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
def test_burst_does_not_extend_around_a_solid_barrier() -> None:
"""Given: a 20-ft burst point-cast at center (6,3); wall cells at col 5
(rows 5-7); foe_open (4,3) inside the radius with clear LoE, foe_behind
(6,7) inside the radius but behind the wall (total cover from the
center)
When: the burst resolves (caster at (6,2) is in the radius too)
Then: the caster and foe_open take damage (two 1d6 consumed); foe_behind
is untouched - a burst "can't affect creatures with total cover from its
point of origin" (its effects don't extend around corners)."""
caster = _caster()
walls = frozenset({(5, 5), (6, 5), (7, 5)})
foe_open = make_state(make_combatant("foe_open", hp=100), "monsters", (4, 3))
foe_behind = make_state(make_combatant("foe_behind", hp=100), "monsters", (6, 7))
eng = CombatEngine(
ScriptedRng([4, 4]), _walled_grid(walls), [caster, foe_open, foe_behind],
spell_registry=_REGISTRY,
)
result = eng._execute(
caster, Action(kind="cast_spell", spell_name="Burst Sparks", target_pos=(6, 3))
)
assert result.ok
assert caster.hp == 100 - 4
assert foe_open.hp == 100 - 4
assert foe_behind.hp == 100
def test_burst_open_field_control_hits_creatures_behind_the_center() -> None:
"""Given: the same layout on an OPEN grid (no walls)
When: the 20-ft burst point-cast at (6,3) resolves
Then: all three creatures (caster, foe_open, foe_behind at distance 4)
are inside the radius and affected - the LoE filter drops nothing when
there is no barrier."""
caster = _caster()
foe_open = make_state(make_combatant("foe_open", hp=100), "monsters", (4, 3))
foe_behind = make_state(make_combatant("foe_behind", hp=100), "monsters", (6, 7))
eng = _engine([caster, foe_open, foe_behind], [4, 4, 4])
eng._execute(
caster, Action(kind="cast_spell", spell_name="Burst Sparks", target_pos=(6, 3))
)
assert caster.hp == 100 - 4
assert foe_open.hp == 100 - 4
assert foe_behind.hp == 100 - 4
def test_burst_still_centers_on_the_target_and_ignores_the_caster_side() -> None: def test_burst_still_centers_on_the_target_and_ignores_the_caster_side() -> None:
"""Given: caster (6,2); foe_in (4,4) and foe_out (2,0) around a burst """Given: caster (6,2); foe_in (4,4) and foe_out (2,0) around a burst
centered on target_id=foe_in (radius 20 ft = 4 cells) centered on target_id=foe_in (radius 20 ft = 4 cells)