75 lines
2.8 KiB
Python
75 lines
2.8 KiB
Python
"""A multi-cell caster's WHOLE footprint is outside its own cone/line.
|
|
|
|
PF1e SRD "Aiming a Spell" (aiming-a-spell.md): a cone and a line "start from
|
|
any corner of your square" -- for a creature with a space larger than one
|
|
square, every occupied square is part of the origin space, so none of them is
|
|
ever inside the creature's own area (the template "shoots away from you").
|
|
The engine used to exclude only the anchor cell, so a Large caster's other
|
|
cells fell inside its own cone/line template.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pf1e_simulator.areas import cone_cells, line_cells
|
|
from pf1e_simulator.combat import CombatEngine
|
|
from pf1e_simulator.grid import Grid
|
|
from pf1e_simulator.map import MapSpec, TerrainType
|
|
from pf1e_simulator.rng import ScriptedRng
|
|
from pf1e_simulator.spells import AreaEffect
|
|
from tests.test_observation_enriched import make_combatant, make_state
|
|
|
|
_FLOOR = TerrainType(type="floor", move_cost=1)
|
|
|
|
|
|
def _open_grid() -> Grid:
|
|
return Grid.from_spec(
|
|
MapSpec(
|
|
name="footprint-origin",
|
|
terrain=tuple(["." * 8] * 8),
|
|
legend={".": _FLOOR},
|
|
)
|
|
)
|
|
|
|
|
|
# A Large caster anchored at (1,1) occupies the 2x2 block below. Aiming east
|
|
# from the anchor, the ray/cone grazes the caster's own cells (1,2) and (2,2).
|
|
_FOOTPRINT = frozenset({(1, 1), (1, 2), (2, 1), (2, 2)})
|
|
|
|
|
|
def test_cone_excludes_the_whole_caster_footprint() -> None:
|
|
grid = _open_grid()
|
|
cells = cone_cells(grid, (1, 1), (1, 6), 4, caster_cells=_FOOTPRINT)
|
|
assert not cells & _FOOTPRINT
|
|
# The exclusion is the footprint only: the cone keeps everything past it.
|
|
assert (1, 3) in cells
|
|
assert (2, 3) in cells
|
|
|
|
|
|
def test_line_excludes_the_whole_caster_footprint() -> None:
|
|
grid = _open_grid()
|
|
cells = line_cells(grid, (1, 1), (1, 6), 6, caster_cells=_FOOTPRINT)
|
|
assert (1, 2) not in cells
|
|
assert (1, 3) in cells
|
|
assert (1, 7) in cells
|
|
|
|
|
|
def test_single_cell_caster_templates_are_unchanged() -> None:
|
|
"""1x1 regression: the footprint is just the anchor, which the old code
|
|
already dropped -- both templates must be identical to the pre-fix set."""
|
|
grid = _open_grid()
|
|
old_cone = cone_cells(grid, (1, 1), (1, 6), 4)
|
|
new_cone = cone_cells(grid, (1, 1), (1, 6), 4, caster_cells=frozenset({(1, 1)}))
|
|
assert old_cone == new_cone
|
|
old_line = line_cells(grid, (1, 1), (1, 6), 6)
|
|
new_line = line_cells(grid, (1, 1), (1, 6), 6, caster_cells=frozenset({(1, 1)}))
|
|
assert old_line == new_line
|
|
|
|
|
|
def test_large_caster_is_not_caught_by_its_own_cone() -> None:
|
|
caster_spec = make_combatant("ogre", size="Large", hp=20)
|
|
caster = make_state(caster_spec, "players", (1, 1))
|
|
engine = CombatEngine(ScriptedRng([]), _open_grid(), [caster])
|
|
effect = AreaEffect(shape="cone", radius_ft=30)
|
|
affected = engine._area_creatures(effect, caster, (1, 6))
|
|
assert affected == []
|