fix(rules): exclude the caster from a line spell's area

CRB Magic - Aiming a Spell: a line 'starts from any corner of your square', so the caster's own square is outside the template (SRD Lightning Bolt: 'The bolt begins at your fingertips' - it leaves the square, it does not fill it). line_cells now drops the origin and takes the length_cells squares beyond it; a degenerate aim affects nothing. Updated the Lightning Bolt and Line Sparks tests (caster no longer caught) and the README.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
2026-09-13 11:30:30 +02:00
parent ee8c8f80ff
commit 642f449b9e
4 changed files with 38 additions and 24 deletions
+20 -12
View File
@@ -11,8 +11,11 @@ PF1e (CRB, Magic — Aiming a Spell):
limit of its range [...]. A line-shaped spell affects all creatures in
squares through which the line passes." — modeled here as the 5-ft-wide
Bresenham ray from the origin through the aim cell, truncated to the
line's length (the origin cell counts as the first square: the bolt
"begins at your fingertips", SRD Lightning Bolt).
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).
Bursts need no template here: they are circles around a center cell and are
resolved by ``CombatEngine._creatures_in_range`` (unchanged).
@@ -74,23 +77,28 @@ 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 origin cell is the first square of the line. Out-of-bounds
cells simply drop out of the template (creatures only exist in bounds).
A degenerate aim (origin == aim) affects only the origin cell.
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
``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.
"""
if length_cells <= 0:
return frozenset()
d_row = aim[0] - origin[0]
d_col = aim[1] - origin[1]
if d_row == 0 and d_col == 0:
return frozenset({origin}) if grid.in_bounds(origin) else frozenset()
# Extend the aim vector so the ray spans `length_cells` squares (the
# dominant axis step count of a Bresenham line is max(|d_row|, |d_col|)).
scale = length_cells / max(abs(d_row), abs(d_col))
return frozenset()
# Extend the aim vector so the ray reaches `length_cells` squares BEYOND
# the origin (the dominant-axis step count of a Bresenham line is
# max(|d_row|, |d_col|)); the +1 accounts for the origin square we drop.
scale = (length_cells + 1) / max(abs(d_row), abs(d_col))
end = (origin[0] + round(d_row * scale), origin[1] + round(d_col * scale))
# The line is `length_cells` squares LONG: the origin square counts as
# the first of them (120 ft / 5 ft = 24 squares for Lightning Bolt).
return frozenset(_bresenham(origin, end)[:length_cells])
# The line is `length_cells` squares LONG starting just past the caster:
# 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))
def _bresenham(start: Pos, end: Pos) -> list[Pos]: