feat(rules): scale healing spells with caster level

PF1e (SRD spells/cure-light-wounds.md): 'cures 1d8 points of damage +1
point per caster level (maximum +5)'; (spells/cure-moderate-wounds.md):
'cures 2d8 points of damage +1 point per caster level (maximum +10)'.
A HealEffect now carries the same DamageFlatScaling spec as damage
effects (S1b) and resolves it through effective_formula(caster_level)
at cast time, so the flat bonus grows with the caster and stops at the
stated cap. The engine passes the caster's level into _apply_heal_effect
(required keyword: no silent level-0 under-heal); Maximize Spell
maximizes the scaled formula. Cure Light/Moderate Wounds ship the SRD
values; inflict_light_wounds already carried its scaling_flat.

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 17:19:58 +02:00
parent a2a80b8690
commit b84238792c
9 changed files with 290 additions and 18 deletions
+24
View File
@@ -182,6 +182,27 @@ class ConditionEffect:
@dataclass(frozen=True)
class HealEffect:
formula: DiceExpr
# PF1e caster-level scaling for healing (SRD spells/cure-light-wounds.md:
# "cures 1d8 points of damage +1 point per caster level (maximum +5)" and
# cure-moderate-wounds.md: "cures 2d8 points of damage +1 point per caster
# level (maximum +10)"). Absent = level-independent healing, the pre-S5b
# behaviour.
scaling_flat: DamageFlatScaling | None = None
def effective_formula(self, caster_level: int) -> DiceExpr:
"""The healing formula resolved for a caster of ``caster_level``.
PF1e (CRB, Magic — Caster Level): the flat bonus grows with the caster
and stops at the stated cap — bonus = min(max, (level // per_levels)
* amount), mirroring ``DamageEffect.effective_formula``.
"""
bonus = self.formula.bonus
if self.scaling_flat is not None:
bonus += min(
self.scaling_flat.max_amount,
(caster_level // self.scaling_flat.per_levels) * self.scaling_flat.amount,
)
return DiceExpr(count=self.formula.count, sides=self.formula.sides, bonus=bonus)
@dataclass(frozen=True)
@@ -529,8 +550,11 @@ def _parse_effect(data: Json) -> SpellEffect:
save_partial=save_partial,
)
if effect_type == "heal":
flat_raw = effect.get("scaling_flat")
scaling_flat = None if flat_raw is None else _parse_flat_scaling(flat_raw)
return HealEffect(
formula=parse_dice(_as_str(effect["formula"])),
scaling_flat=scaling_flat,
)
if effect_type == "buff":
return BuffEffect(