feat(rules): scale spell damage with caster level

DamageEffect gains optional scaling (1dX per N levels, capped) and scaling_flat (+X per N levels, capped) specs; effective_formula resolves them against the caster's level at cast time, so scaled dice flow through crits, energy resistance and metamagic like the base formula (CRB Magic - Caster Level). Loader rejects mismatched die types. Data: Fireball/Lightning Bolt 1d6/lvl max 10d6, Burning Hands 1d4/lvl max 5d4, Shocking Grasp 1d6/lvl max 5d6, Inflict Light Wounds +1/lvl max +5 (SRD-verified). Cure Light Wounds stays flat: its golden transcript pins 1d8+1 and HealEffect scaling is out of scope.

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:48:09 +02:00
parent 4659fae9f6
commit a6cc5aef36
11 changed files with 427 additions and 46 deletions
+5 -5
View File
@@ -136,13 +136,13 @@ class TestAoEDcEqualsCasterSpellDc:
def test_area_save_uses_caster_dc(self) -> None:
caster = _make_caster(level=5, int_score=18)
target = _make_target(hp_max=200)
queue = [1, 12, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
queue = [1, 12, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
engine, c, t = _make_engine(caster, target, queue)
hp_before_caster = c.hp
hp_before_target = t.hp
engine._cast_spell(c, t, "Fireball")
assert t.hp == hp_before_target - 18
assert c.hp == hp_before_caster - 18
assert t.hp == hp_before_target - 15
assert c.hp == hp_before_caster - 15
class TestSpellDamageBypassesDr:
@@ -156,8 +156,8 @@ class TestSpellDamageBypassesDr:
target_with_dr = target.model_copy(update={
"dr": DamageReduction(amount=5, bypass=frozenset()),
})
queue = [2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
queue = [2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3]
engine, c, t = _make_engine(caster, target_with_dr, queue)
hp_before = t.hp
engine._cast_spell(c, t, "Fireball")
assert t.hp == hp_before - 18
assert t.hp == hp_before - 15