fix(rules): cones require line of effect from their origin (SRD aiming-a-spell.md: a cone 'affects only an area, creature, or object to which it has line of effect from its origin') - cone_cells drops geometrically-inside cells without corner-to-corner LoE from the caster square; line/cone barrier-cell semantics aligned (both templates include the barrier square the effect strikes, exclude everything behind it)

This commit is contained in:
2026-09-19 14:04:34 +02:00
parent 5250763c35
commit 389e160627
2 changed files with 65 additions and 7 deletions
+44 -2
View File
@@ -18,7 +18,7 @@ from typing import TYPE_CHECKING
import pytest
from pf1e_simulator.areas import line_cells
from pf1e_simulator.areas import cone_cells, line_cells
from pf1e_simulator.combat import Action, CombatantState, CombatEngine
from pf1e_simulator.dice import parse_dice
from pf1e_simulator.grid import Grid
@@ -185,6 +185,48 @@ def test_cone_covers_the_full_90_degree_arc() -> None:
assert foe_outside.hp == 100
def test_cone_cells_require_line_of_effect_from_the_origin() -> None:
"""Given: a wall column at col 4 (rows 4-8) between origin (6,2) and the
cell (6,5) that sits on the cone axis at distance 3
When: cone_cells builds a 3-cell east cone
Then: (6,5) is dropped - geometrically inside but without line of effect
from the origin (CRB, Line of Effect: a cone "affects only an area,
creature, or object to which it has line of effect from its origin");
on the same geometry without walls it stays in the template."""
walls = frozenset({(4, 4), (5, 4), (6, 4), (7, 4), (8, 4)})
grid = _walled_grid(walls)
cells = cone_cells(grid, (6, 2), (6, 3), 3)
assert (6, 3) in cells
assert (6, 4) in cells # the cone reaches the barrier square itself
assert (6, 5) not in cells
open_cells = cone_cells(_open_grid(), (6, 2), (6, 3), 3)
assert (6, 5) in open_cells # control: nothing else filters it out
def test_cone_behind_a_solid_wall_does_not_hit_sheltered_creatures() -> None:
"""Given: caster (6,2) aiming the 15-ft cone east at foe_front (6,3); a
wall column at col 4 (rows 4-8); foe_behind (6,5) sits on the cone axis
behind that wall
When: the cone resolves
Then: only foe_front is caught (one 1d6 consumed after the adjacent
foe_front's scripted -4 AoO d20 misses); foe_behind is untouched - the
cone stops at the solid barrier, it does not pass through it."""
caster = _caster()
walls = frozenset({(4, 4), (5, 4), (6, 4), (7, 4), (8, 4)})
foe_front = make_state(make_combatant("foe_front", hp=100), "monsters", (6, 3))
foe_behind = make_state(make_combatant("foe_behind", hp=100), "monsters", (6, 5))
eng = CombatEngine(
ScriptedRng([4, 4]), _walled_grid(walls), [caster, foe_front, foe_behind],
spell_registry=_REGISTRY,
)
result = eng._execute(
caster, Action(kind="cast_spell", target_id="foe_front", spell_name="Cone Sparks")
)
assert result.ok
assert foe_front.hp == 100 - 4
assert foe_behind.hp == 100
def test_cone_directed_by_target_pos_point_cast() -> None:
"""Given: the same scene, no target_id — the cone is aimed at an explicit
cell (6,5) via target_pos (U7b point cast)
@@ -256,7 +298,7 @@ def test_line_cells_stop_at_a_line_of_effect_barrier() -> None:
grid = _walled_grid(frozenset({(6, 6)}))
cells = line_cells(grid, (6, 2), (6, 8), 10)
assert (6, 5) in cells
assert (6, 6) not in cells
assert (6, 6) in cells # the bolt strikes the barrier square itself
assert (6, 7) not in cells
assert (6, 8) not in cells