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
+21 -5
View File
@@ -18,8 +18,15 @@ PF1e (CRB, Magic — Aiming a Spell):
fill it). The line also stops at the first square that blocks line of
effect (CRB, Magic - Aiming a Spell, Line: it "extends to the limit of its
range or until it strikes a barrier that blocks line of effect"): the
barrier square itself and everything beyond it are outside the template,
so a bolt never reaches creatures behind a solid wall.
barrier square is the last square of the template and everything beyond
it is outside, so a bolt never reaches creatures behind a solid wall.
- Cones require line of effect from their origin (CRB, Magic - Line of
Effect): "A burst, cone, cylinder, or emanation spell affects only an
area, creature, or object to which it has line of effect from its origin
(a spherical burst's center point, a cone-shaped burst's starting
point, ...)". ``cone_cells`` therefore drops every geometric cell that
has no corner-to-corner line of effect from the origin square (a solid
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).
@@ -29,6 +36,8 @@ from __future__ import annotations
from typing import TYPE_CHECKING
from pf1e_simulator.los import has_line_of_effect
if TYPE_CHECKING:
from pf1e_simulator.grid import Grid
from pf1e_simulator.map import Pos
@@ -44,7 +53,11 @@ def cone_cells(
from the origin is within ``radius_cells`` (5-10-5 grid distance), points
strictly into the cone's forward half-plane, and stays within ±45° of the
aim bearing. The origin cell itself is never affected (CRB: the cone
"shoots away from you"). A degenerate aim (origin == aim) affects nothing.
"shoots away from you"). A cell with no line of effect from the origin
is dropped (CRB, Line of Effect: a cone "affects only an area, creature,
or object to which it has line of effect from its origin" - a solid wall
stops the cone, it does not pass through). A degenerate aim
(origin == aim) affects nothing.
The ±45° test is exact integer arithmetic: with v the cell offset and d
the aim offset, ``cos(angle) >= cos(45°)`` is equivalent to
@@ -70,6 +83,8 @@ def cone_cells(
continue
if grid.distance(origin, cell) > radius_cells:
continue
if not has_line_of_effect(grid, origin, cell):
continue
cells.add(cell)
return frozenset(cells)
@@ -87,7 +102,8 @@ def line_cells(
the ray. Out-of-bounds cells simply drop out of the template (creatures
only exist in bounds). The ray stops at the first cell that blocks line
of effect (CRB, Line: "or until it strikes a barrier that blocks line of
effect") - that barrier cell and everything beyond it are excluded. A
effect") - the barrier cell itself is the last cell of the template (the
line strikes it) and everything beyond it is excluded. A
degenerate aim (origin == aim) affects nothing.
"""
if length_cells <= 0:
@@ -109,9 +125,9 @@ def line_cells(
for cell in ray[1 : length_cells + 1]:
if not grid.in_bounds(cell):
continue
cells.append(cell)
if grid.terrain_at(cell).blocks_los:
break
cells.append(cell)
return frozenset(cells)