fix(rules): line spells stop at a line-of-effect barrier (SRD aiming-a-spell.md: a line 'extends to the limit of its range or until it strikes a barrier that blocks line of effect') - line_cells truncates at the first blocks_los cell, creatures behind a solid wall are never caught

This commit is contained in:
2026-09-19 13:54:25 +02:00
parent b95533d49f
commit 5250763c35
2 changed files with 72 additions and 6 deletions
+19 -5
View File
@@ -14,8 +14,12 @@ PF1e (CRB, Magic — Aiming a Spell):
line's length. The line "starts from any corner of your square", so the
CASTER's own square is NOT one of the affected squares: the affected
squares are the ones beyond the origin along the ray (SRD Lightning Bolt:
"The bolt begins at your fingertips" — it leaves your square, it does not
fill it).
"The bolt begins at your fingertips" - it leaves your square, it does not
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.
Bursts need no template here: they are circles around a center cell and are
resolved by ``CombatEngine._creatures_in_range`` (unchanged).
@@ -78,10 +82,13 @@ def line_cells(
The line runs along the Bresenham ray from the origin toward ``aim``,
extended to ``length_cells`` squares (CRB: it "extends to the limit of
its range"). The line "starts from any corner of your square" (CRB, Magic
— Aiming a Spell), so the CASTER's own square is NOT affected: the
- Aiming a Spell), so the CASTER's own square is NOT affected: the
``length_cells`` affected squares are the ones *beyond* the origin along
the ray. Out-of-bounds cells simply drop out of the template (creatures
only exist in bounds). A degenerate aim (origin == aim) affects nothing.
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
degenerate aim (origin == aim) affects nothing.
"""
if length_cells <= 0:
return frozenset()
@@ -98,7 +105,14 @@ def line_cells(
# 120 ft / 5 ft = 24 squares, none of them the caster's own (SRD Lightning
# Bolt: "The bolt begins at your fingertips" — it leaves your square).
ray = _bresenham(origin, end)
return frozenset(cell for cell in ray[1 : length_cells + 1] if grid.in_bounds(cell))
cells: list[Pos] = []
for cell in ray[1 : length_cells + 1]:
if not grid.in_bounds(cell):
continue
if grid.terrain_at(cell).blocks_los:
break
cells.append(cell)
return frozenset(cells)
def _bresenham(start: Pos, end: Pos) -> list[Pos]: