Files
gogobee/internal/plugin/dnd_spell_aoe_test.go
prosolis b53e516bf0 Adv 2.0 D&D Phase 9 SP4: mage spellbook cap + level-up nudge
- Enforce per-level Mage known-spell cap in handleSpellsLearn; surface
  spellbook budget in renderSpellsList.
- Mage level-up DM now nudges with "Spells available to learn: N" via
  extracted buildLevelUpMessage.
- Extract halveSavedDamage and enemySpellSaveMod for clarity; document
  single-target AoE limitation in applySpellDamageSave.
- Add tests: mage learn cap, prepare flow, AoE behavior, spell save
  rounding, spells migration.
2026-05-09 14:25:21 -07:00

94 lines
2.8 KiB
Go

package plugin
import (
"strings"
"testing"
)
// Phase 9 SP4 — AoE flag spells must still resolve correctly through
// applySpellDamageSave even though combat has only one enemy. Verify Fireball,
// Burning Hands, and Cone of Cold land legal damage in both save-fail and
// save-success branches.
func TestApplySpellDamageSave_FireballRange(t *testing.T) {
spell, ok := lookupSpell("fireball")
if !ok {
t.Fatal("fireball missing from registry")
}
if !spell.AOE {
t.Errorf("fireball should be AOE")
}
c := &DnDCharacter{Class: ClassMage, Level: 5, INT: 18}
enemy := &CombatStats{AttackBonus: 0, AC: 12}
// DC 99 → save virtually always fails → full damage. 8d6 = 8..48.
for i := 0; i < 30; i++ {
mods := &CombatModifiers{}
applySpellDamageSave(spell, 99, c, mods, enemy, 3)
if mods.SpellPreDamage < 8 || mods.SpellPreDamage > 48 {
t.Errorf("fireball DC99 dmg=%d outside 8..48", mods.SpellPreDamage)
break
}
}
// DC 1 → save virtually always succeeds → half damage (floor 1).
// 8d6/2 = 4..24.
for i := 0; i < 30; i++ {
mods := &CombatModifiers{}
applySpellDamageSave(spell, 1, c, mods, enemy, 3)
if mods.SpellPreDamage < 1 || mods.SpellPreDamage > 24 {
t.Errorf("fireball DC1 (saved) dmg=%d outside 1..24", mods.SpellPreDamage)
break
}
if !strings.Contains(mods.SpellPreDamageDesc, "saved") {
t.Errorf("fireball DC1 desc=%q, want 'saved'", mods.SpellPreDamageDesc)
break
}
}
}
func TestApplySpellDamageSave_BurningHands(t *testing.T) {
spell, _ := lookupSpell("burning_hands")
if !spell.AOE {
t.Errorf("burning_hands should be AOE")
}
c := &DnDCharacter{Class: ClassMage, Level: 1, INT: 16}
enemy := &CombatStats{AttackBonus: 0}
// L1 cast, 3d6 → 3..18 on full damage.
for i := 0; i < 30; i++ {
mods := &CombatModifiers{}
applySpellDamageSave(spell, 99, c, mods, enemy, 1)
if mods.SpellPreDamage < 3 || mods.SpellPreDamage > 18 {
t.Errorf("burning_hands dmg=%d outside 3..18", mods.SpellPreDamage)
break
}
}
// Upcast to L3: rollSpellDamageDice adds (3-1)=2 dice → 5d6 = 5..30.
for i := 0; i < 30; i++ {
mods := &CombatModifiers{}
applySpellDamageSave(spell, 99, c, mods, enemy, 3)
if mods.SpellPreDamage < 5 || mods.SpellPreDamage > 30 {
t.Errorf("burning_hands upcast L3 dmg=%d outside 5..30", mods.SpellPreDamage)
break
}
}
}
func TestApplySpellDamageSave_ConeOfCold(t *testing.T) {
spell, _ := lookupSpell("cone_of_cold")
if !spell.AOE {
t.Errorf("cone_of_cold should be AOE")
}
c := &DnDCharacter{Class: ClassMage, Level: 9, INT: 18}
enemy := &CombatStats{AttackBonus: 0}
// 8d8 = 8..64.
for i := 0; i < 30; i++ {
mods := &CombatModifiers{}
applySpellDamageSave(spell, 99, c, mods, enemy, 5)
if mods.SpellPreDamage < 8 || mods.SpellPreDamage > 64 {
t.Errorf("cone_of_cold dmg=%d outside 8..64", mods.SpellPreDamage)
break
}
}
}