"""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 == []