From 948edc1815e06bb50bcf786dfb5accd7b5c4627c Mon Sep 17 00:00:00 2001 From: ctan Date: Sat, 19 Sep 2026 14:08:24 +0200 Subject: [PATCH] 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 --- src/pf1e_simulator/areas.py | 3 ++- src/pf1e_simulator/combat.py | 17 ++++++++++--- tests/test_area_shapes.py | 46 +++++++++++++++++++++++++++++++++++- 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/src/pf1e_simulator/areas.py b/src/pf1e_simulator/areas.py index 79cf419..fafafc1 100644 --- a/src/pf1e_simulator/areas.py +++ b/src/pf1e_simulator/areas.py @@ -29,7 +29,8 @@ PF1e (CRB, Magic — Aiming a Spell): wall between them shields everything behind it). 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 diff --git a/src/pf1e_simulator/combat.py b/src/pf1e_simulator/combat.py index 4ca64fa..585073c 100644 --- a/src/pf1e_simulator/combat.py +++ b/src/pf1e_simulator/combat.py @@ -7116,8 +7116,13 @@ class CombatEngine: ) -> list[CombatantState]: """Creatures intersecting the template of an area effect. - Burst: circle of ``radius_ft`` around the resolved center ``aim`` - (unchanged behaviour). Cone/line: the template originates at the + Burst: circle of ``radius_ft`` around the resolved center ``aim``, + 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 Spell" — see ``areas.py``); a creature is affected when ANY of its occupied cells lies inside the template. @@ -7129,7 +7134,13 @@ class CombatEngine: """ span_cells = effect.radius_ft // _SQUARE_FT 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: origin = caster.pos if effect.shape == "cone": diff --git a/tests/test_area_shapes.py b/tests/test_area_shapes.py index addd778..2d1adb7 100644 --- a/tests/test_area_shapes.py +++ b/tests/test_area_shapes.py @@ -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: """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)