fix(rules): a grazed caster cell no longer shortens a line spell - SRD Aiming a Spell: a line 'starts from any corner of your square and extends to the limit of its range' (Lightning Bolt: 'The bolt begins at your fingertips'), so the caster's own squares are origin space the line starts PAST and must not consume one of the length_cells squares; line_cells took a fixed ray[1:length_cells+1] window and dropped caster cells inside it, so a 2x2 caster's line came out one square short per grazed footprint cell (the old code documented this as a simplification); the ray is now extended (reach = length_cells + 1 + len(caster_cells)) and caster cells are skipped WITHOUT spending a length slot, so a multi-cell line runs its full length starting just past the footprint; 1x1 casters are bit-identical (the only skipped cell is the origin at ray[0], already outside the counted window); the barrier/LoE stop and out-of-bounds semantics are unchanged (a straight ray never re-enters a convex map); golden untouched (no line spell in it); +3 tests (grazed-cell full-length template, 1x1 bit-identical regression, engine-level reach of a foe the short line missed)
This commit is contained in:
+39
-27
@@ -11,12 +11,15 @@ PF1e (CRB, Magic — Aiming a Spell):
|
||||
designate. It starts from any corner of your square and extends to the
|
||||
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 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 line also stops at the first square that blocks line of
|
||||
Bresenham ray from the origin through the aim cell, truncated to the
|
||||
line's length. The line "starts from any corner of your square", so the
|
||||
CASTER's own space is NOT one of the affected squares AND does not shorten
|
||||
the effect: the whole footprint (every cell of a multi-cell caster, not
|
||||
just the anchor) is skipped without spending one of the ``length_cells``
|
||||
slots, so the line runs its full length starting just past the caster
|
||||
(SRD Lightning Bolt: "The bolt begins at your fingertips" - it leaves your
|
||||
square, it does not fill it, and a 120 ft bolt is 24 squares beyond the
|
||||
caster). 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 is the last square of the template and everything beyond
|
||||
@@ -142,17 +145,20 @@ 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 whole space is NOT affected: every
|
||||
caster cell the ray grazes (the anchor ``origin`` included, i.e. all of
|
||||
``caster_cells``) is outside the template. For a 1x1 caster that is just
|
||||
the origin, so the ``length_cells`` affected squares are the ones
|
||||
*beyond* the origin along the ray. Documented simplification: a caster
|
||||
cell the ray crosses still consumes one of the ``length_cells`` slots, so
|
||||
a multi-cell caster's line is at most its own footprint shorter.
|
||||
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") - the barrier cell itself is the last cell of the template (the
|
||||
- Aiming a Spell), so the CASTER's whole space is NOT affected AND does
|
||||
not shorten the effect: every caster cell the ray grazes (the anchor
|
||||
``origin`` included, i.e. all of ``caster_cells``) is origin space skipped
|
||||
WITHOUT spending one of the ``length_cells`` slots. The line is therefore
|
||||
``length_cells`` squares long starting just past the caster's footprint
|
||||
(SRD Lightning Bolt: "The bolt begins at your fingertips" - it leaves your
|
||||
square, and a 120 ft bolt is 24 squares beyond the caster, not beyond the
|
||||
anchor). For a 1x1 caster the only skipped cell is the origin (ray[0]),
|
||||
so the result is bit-identical to counting the ``length_cells`` squares
|
||||
*beyond* the origin. Out-of-bounds cells drop out of the template but
|
||||
never re-enter (a straight ray leaves a convex map once), so creatures
|
||||
only ever 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") - 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.
|
||||
"""
|
||||
@@ -163,21 +169,27 @@ def line_cells(
|
||||
if d_row == 0 and d_col == 0:
|
||||
return frozenset()
|
||||
skip = caster_cells | {origin}
|
||||
# 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))
|
||||
# Extend the aim vector so the ray still yields `length_cells` real
|
||||
# squares AFTER the caster's own space is skipped: the dominant-axis step
|
||||
# count of a Bresenham line is max(|d_row|, |d_col|); the +1 drops the
|
||||
# origin square and the extra `len(caster_cells)` steps cover every
|
||||
# footprint square the ray can graze before it leaves the caster's space
|
||||
# (those are origin space and spend no length slot).
|
||||
dominant = max(abs(d_row), abs(d_col))
|
||||
reach = length_cells + 1 + len(caster_cells)
|
||||
scale = reach / dominant
|
||||
end = (origin[0] + round(d_row * scale), origin[1] + round(d_col * scale))
|
||||
# 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)
|
||||
cells: list[Pos] = []
|
||||
for cell in ray[1 : length_cells + 1]:
|
||||
if cell in skip or not grid.in_bounds(cell):
|
||||
continue
|
||||
for cell in ray[1:]:
|
||||
if cell in skip:
|
||||
continue # origin space: the line starts past it, no slot spent
|
||||
if not grid.in_bounds(cell):
|
||||
continue # off the map: a straight ray never re-enters
|
||||
cells.append(cell)
|
||||
if grid.terrain_at(cell).blocks_los:
|
||||
break # the line strikes this barrier: last cell of the template
|
||||
if len(cells) >= length_cells:
|
||||
break
|
||||
return frozenset(cells)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user