feat(rules): support cone and line spell areas

AreaEffect gains shape: burst|cone|line (CRB Magic - Aiming a Spell). Cones (90 deg, length = radius_ft) and lines (5 ft wide, Bresenham ray) originate at the caster's cell and are aimed at the resolved target or the U7b point-cast center; bursts are unchanged. Shared per-creature save + SR application; the observation area dict already exposes the shape.

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 10:32:37 +02:00
parent 734cfa4e55
commit 4659fae9f6
4 changed files with 462 additions and 9 deletions
+37 -5
View File
@@ -121,6 +121,7 @@ from dataclasses import asdict, dataclass, field
from pathlib import Path
from typing import TYPE_CHECKING, Literal
from pf1e_simulator.areas import cone_cells, line_cells
from pf1e_simulator.conditions import CONDITIONS_BY_NAME, Condition
from pf1e_simulator.dice import dice_to_str
from pf1e_simulator.effects import ActiveBuff, ActiveCondition, StatModifier, resolve_modifiers
@@ -5888,16 +5889,21 @@ class CombatEngine:
center_pos: tuple[int, int] | None = None,
save_type: SaveType = "none",
) -> str:
"""Apply an area effect to all creatures within radius.
"""Apply an area effect to all creatures within its template.
The burst is centered on ``center_pos`` when given (U7b point
targeting), otherwise on the resolved target's position.
PF1e (CRB, Magic - Aiming a Spell): a burst/spread centers on
``center_pos`` when given (U7b point targeting), otherwise on the
resolved target's position; a cone or line starts at the CASTER's
square and is aimed at that same resolved cell (the cone "shoots
away from you in a quarter-circle in the direction you designate",
the line "shoots away from you in a line in the direction you
designate"). Every creature whose occupied cells intersect the
template is subject to the effect.
"""
radius_cells = effect.radius_ft // _SQUARE_FT
center = _burst_center(target, center_pos)
if center is None:
return "no targets in area"
affected = self._creatures_in_range(center, radius_cells)
affected = self._area_creatures(effect, caster, center)
if not affected:
return "no targets in area"
@@ -5933,6 +5939,31 @@ class CombatEngine:
f"{total_conditions} conditions, {total_buffs} buffs"
)
def _area_creatures(
self, effect: AreaEffect, caster: CombatantState, aim: tuple[int, int]
) -> list[CombatantState]:
"""Creatures intersecting the template of an area effect.
Burst: circle of ``radius_ft`` around the resolved center ``aim``
(unchanged behaviour). Cone/line: the template originates at the
caster's anchor cell and opens toward ``aim`` (PF1e CRB "Aiming a
Spell" — see ``areas.py``); a creature is affected when ANY of its
occupied cells lies inside the template.
"""
span_cells = effect.radius_ft // _SQUARE_FT
if effect.shape == "burst":
return self._creatures_in_range(aim, span_cells)
origin = caster.pos
if effect.shape == "cone":
cells = cone_cells(self._grid, origin, aim, span_cells)
else:
cells = line_cells(self._grid, origin, aim, span_cells)
return [
state
for state in self._states
if state.active and state.occupied_cells() & cells
]
def _creatures_in_range(
self, center: tuple[int, int], radius_cells: int
) -> list[CombatantState]:
@@ -6010,6 +6041,7 @@ class CombatEngine:
return BuffEffect(modifiers=effect.modifiers, duration_rounds=duration)
if isinstance(effect, AreaEffect):
return AreaEffect(
shape=effect.shape,
radius_ft=effect.radius_ft,
effects=tuple(
self._metamagic_transform_sub(sub, has_extend=has_extend)