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
+21 -10
View File
@@ -6499,20 +6499,31 @@ class CombatEngine:
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.
``effect.targets`` restricts the selection (SRD bless/bane bursts):
"allies" keeps only creatures on the caster's side (the caster
included — SRD bless "The caster and all allies"), "enemies" keeps
only the opposite side, "all" keeps everyone.
"""
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)
affected = self._creatures_in_range(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
]
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)
affected = [
state
for state in self._states
if state.active and state.occupied_cells() & cells
]
if effect.targets == "allies":
return [s for s in affected if s.side == caster.side]
if effect.targets == "enemies":
return [s for s in affected if s.side != caster.side]
return affected
def _creatures_in_range(
self, center: tuple[int, int], radius_cells: int
+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(