feat(rules): restrict bless to allies in a 50-ft burst

SRD spells/bless.md: 'The caster and all allies within a 50-ft. burst,
centered on the caster'. Add an AreaEffect.targets filter (all/allies/
enemies) applied where area creatures are collected; bless.json now
declares radius_ft 50 and targets allies (the caster included).
This commit is contained in:
2026-09-16 11:14:46 +02:00
parent 312f9fd6b8
commit e43066e0ef
4 changed files with 87 additions and 12 deletions
+19
View File
@@ -33,6 +33,10 @@ SaveType = Literal["fort", "ref", "will", "none"]
# point), cone (90° quarter circle from the caster), line (5-ft ray from the
# caster to its length).
AreaShape = Literal["burst", "cone", "line"]
# PF1e area targeting (SRD spells/bless.md, spells/bane.md): a burst may
# affect only allies ("The caster and all allies within a 50-ft. burst") or
# only enemies ("Enemies within the area..."); the default affects everyone.
AreaTargets = Literal["all", "allies", "enemies"]
# PF1e casting times, normalized to budget codes (see combat._cast_time_slot).
CastTime = Literal["standard", "swift", "immediate", "free", "full_round", "one_round", "long"]
@@ -226,11 +230,14 @@ class AreaEffect:
- 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
- targets: which creatures in the area are affected ("all", "allies",
"enemies" — SRD bless/bane bursts)
"""
shape: AreaShape = "burst"
radius_ft: int = 20
effects: tuple[SpellEffect, ...] = ()
save_for_half: bool = True
targets: AreaTargets = "all"
@dataclass(frozen=True)
class SummonEffect:
@@ -358,6 +365,10 @@ def _is_area_shape(value: str) -> TypeGuard[AreaShape]:
return value in get_args(AreaShape)
def _is_area_targets(value: str) -> TypeGuard[AreaTargets]:
return value in get_args(AreaTargets)
def _is_cast_time(value: str) -> TypeGuard[CastTime]:
return value in get_args(CastTime)
@@ -566,11 +577,19 @@ def _parse_effect(data: Json) -> SpellEffect:
if not _is_area_shape(shape_raw):
msg = f"unknown area shape: {shape_raw!r} (expected one of {get_args(AreaShape)})"
raise ValueError(msg)
targets_raw = _as_str(effect.get("targets", "all"))
if not _is_area_targets(targets_raw):
msg = (
f"unknown area targets: {targets_raw!r} "
f"(expected one of {get_args(AreaTargets)})"
)
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)),
targets=targets_raw,
)
if effect_type == "summon":
return SummonEffect(