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:
@@ -18,10 +18,11 @@ from typing import TYPE_CHECKING
|
||||
|
||||
import pytest
|
||||
|
||||
from pf1e_simulator.areas import line_cells
|
||||
from pf1e_simulator.combat import Action, CombatantState, CombatEngine
|
||||
from pf1e_simulator.dice import parse_dice
|
||||
from pf1e_simulator.grid import Grid
|
||||
from pf1e_simulator.map import MapSpec, TerrainType
|
||||
from pf1e_simulator.map import MapSpec, Pos, TerrainType
|
||||
from pf1e_simulator.rng import ScriptedRng
|
||||
from pf1e_simulator.spells import (
|
||||
AreaEffect,
|
||||
@@ -103,6 +104,20 @@ def _open_grid(side: int = 12) -> Grid:
|
||||
return Grid.from_spec(spec)
|
||||
|
||||
|
||||
def _walled_grid(walls: frozenset[Pos], side: int = 12) -> Grid:
|
||||
"""Open ``side``x``side`` floor with impassable walls at ``walls``."""
|
||||
legend = {
|
||||
".": TerrainType(type="floor", move_cost=1),
|
||||
"#": TerrainType(type="wall", move_cost=None, blocks_los=True),
|
||||
}
|
||||
rows = tuple(
|
||||
"".join("#" if (r, c) in walls else "." for c in range(side))
|
||||
for r in range(side)
|
||||
)
|
||||
spec = MapSpec(name="walled", terrain=rows, legend=legend)
|
||||
return Grid.from_spec(spec)
|
||||
|
||||
|
||||
def _engine(states: list[CombatantState], rolls: list[int]) -> CombatEngine:
|
||||
return CombatEngine(
|
||||
ScriptedRng(rolls), _open_grid(), states, spell_registry=_REGISTRY
|
||||
@@ -232,6 +247,43 @@ def test_line_follows_a_diagonal_bresenham_ray() -> None:
|
||||
assert foe_off.hp == 100
|
||||
|
||||
|
||||
def test_line_cells_stop_at_a_line_of_effect_barrier() -> None:
|
||||
"""Given: a wall cell (6,6) on the east ray from origin (6,2)
|
||||
When: line_cells builds a 10-cell template
|
||||
Then: cells before the barrier are in the template; the barrier cell and
|
||||
everything beyond it are not (CRB: the line "extends to the limit of its
|
||||
range or until it strikes a barrier that blocks line of effect")."""
|
||||
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, 7) not in cells
|
||||
assert (6, 8) not in cells
|
||||
|
||||
|
||||
def test_line_behind_a_wall_does_not_hit_sheltered_creatures() -> None:
|
||||
"""Given: caster (6,2) aiming east; a wall column at col 6 (rows 4-8);
|
||||
foe_front (6,4) on the near side, foe_behind (6,8) fully behind the wall
|
||||
When: cast the 120-ft line (24 cells - long enough to cross the map)
|
||||
Then: the bolt stops at the barrier: only foe_front is caught (one 1d6
|
||||
consumed); foe_behind is untouched (SRD Lightning Bolt cannot reach
|
||||
around or through a solid wall)."""
|
||||
caster = _caster()
|
||||
walls = frozenset({(4, 6), (5, 6), (6, 6), (7, 6), (8, 6)})
|
||||
foe_front = make_state(make_combatant("foe_front", hp=100), "monsters", (6, 4))
|
||||
foe_behind = make_state(make_combatant("foe_behind", hp=100), "monsters", (6, 8))
|
||||
eng = CombatEngine(
|
||||
ScriptedRng([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="Line Sparks")
|
||||
)
|
||||
assert result.ok
|
||||
assert foe_front.hp == 100 - 4
|
||||
assert foe_behind.hp == 100
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# burst: unchanged circle around the resolved center
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user