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:
@@ -29,6 +29,10 @@ SPELLS_DIR = Path(__file__).resolve().parent.parent.parent / "data" / "spells"
|
||||
|
||||
SpellRange = Literal["personal", "touch", "close", "medium", "long"]
|
||||
SaveType = Literal["fort", "ref", "will", "none"]
|
||||
# PF1e area templates (CRB, Magic - Aiming a Spell): burst (circle around a
|
||||
# point), cone (90° quarter circle from the caster), line (5-ft ray from the
|
||||
# caster to its length).
|
||||
AreaShape = Literal["burst", "cone", "line"]
|
||||
# PF1e casting times, normalized to budget codes (see combat._cast_time_slot).
|
||||
CastTime = Literal["standard", "swift", "immediate", "free", "full_round", "one_round", "long"]
|
||||
|
||||
@@ -66,14 +70,21 @@ class BuffEffect:
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class AreaEffect:
|
||||
"""Area of effect spell (burst only for Phase 0).
|
||||
"""Area of effect spell (burst / cone / line).
|
||||
|
||||
- shape: only "burst" supported (circle around target point)
|
||||
- radius_ft: radius in feet (e.g., 20 for fireball)
|
||||
PF1e (CRB, Magic - Aiming a Spell): a burst/spread centers on a chosen
|
||||
point; a cone "shoots away from you in a quarter-circle in the direction
|
||||
you designate"; a line "shoots away from you in a line in the direction
|
||||
you designate" and "affects all creatures in squares through which the
|
||||
line passes". Cones and lines therefore originate at the CASTER and are
|
||||
aimed at the resolved target cell (or the U7b point-cast center).
|
||||
|
||||
- shape: "burst" (circle), "cone" (90° quarter circle) or "line" (5 ft wide)
|
||||
- radius_ft: burst radius / cone length / line length, in feet
|
||||
- effects: the effects applied to each creature in the area
|
||||
- save_for_half: if True, successful save halves damage
|
||||
"""
|
||||
shape: Literal["burst"] = "burst"
|
||||
shape: AreaShape = "burst"
|
||||
radius_ft: int = 20
|
||||
effects: tuple[SpellEffect, ...] = ()
|
||||
save_for_half: bool = True
|
||||
@@ -195,6 +206,10 @@ def _is_save_type(value: str) -> TypeGuard[SaveType]:
|
||||
return value in ("fort", "ref", "will", "none")
|
||||
|
||||
|
||||
def _is_area_shape(value: str) -> TypeGuard[AreaShape]:
|
||||
return value in get_args(AreaShape)
|
||||
|
||||
|
||||
def _is_cast_time(value: str) -> TypeGuard[CastTime]:
|
||||
return value in get_args(CastTime)
|
||||
|
||||
@@ -281,7 +296,12 @@ def _parse_effect(data: Json) -> SpellEffect:
|
||||
duration_rounds=_as_int(effect.get("duration_rounds", 0)),
|
||||
)
|
||||
if effect_type == "area":
|
||||
shape_raw = _as_str(effect.get("shape", "burst"))
|
||||
if not _is_area_shape(shape_raw):
|
||||
msg = f"unknown area shape: {shape_raw!r} (expected one of {get_args(AreaShape)})"
|
||||
raise ValueError(msg)
|
||||
return AreaEffect(
|
||||
shape=shape_raw,
|
||||
radius_ft=_as_int(effect.get("radius_ft", 20)),
|
||||
effects=tuple(_parse_effect(e) for e in _as_list(effect["effects"])),
|
||||
save_for_half=bool(effect.get("save_for_half", True)),
|
||||
|
||||
Reference in New Issue
Block a user