Files
gogobee/internal/plugin/dnd_bestiary_test.go
prosolis 9e1a1f606c Adv 2.0 D&D layer: Phases 1-8 + Phase 9 SP1+SP2
Combines all uncommitted D&D work into one checkpoint:

Phases 1-8 (per gogobee_dnd_session_summary.md):
- Race/class/stats system, !setup flow, !sheet, !respec
- d20-vs-AC combat with race/class passives, auto-migration
- XP/level-up, skill checks, NPC refund hooks
- Equipment slot mapping, rarity, !rest short/long
- Pre-arm active abilities, resource pools
- Bestiary, !roll/!stats/!level, onboarding DM
- Audit fixes A-I, flavor wiring under internal/flavor/
- Phase 8 weapon dice + armor AC tables (gogobee_equipment_appendix)

Phase 9 SP1 — spell system foundations:
- 79 SpellDefinitions (76 in-scope + 3 reaction-deferred)
- dnd_known_spells, dnd_spell_slots tables
- pending_cast / concentration_* columns on dnd_character
- Slot tables for Mage/Cleric/Ranger, DC + attack-bonus math
- Long-rest refreshes slots; respec wipes spell state
- Auto-grant default known list on !setup confirm and auto-migration

Phase 9 SP2 — !cast / !spells / !prepare commands:
- Out-of-combat HEAL and UTILITY resolve immediately
- Damage/control/buff queue as pending_cast for next combat
- Audit-style save-then-debit, slot refund on save failure
- !cast --drop, concentration supersession, prep-cap enforcement
- Reaction spells refused with Phase 11 note

SP3 (combat-time resolution of pending_cast) and SP4 (full prep/learn
tests) are next. Build clean, full test suite green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 14:25:21 -07:00

70 lines
1.7 KiB
Go

package plugin
import "testing"
func TestBestiaryAllWellFormed(t *testing.T) {
if len(dndBestiary) == 0 {
t.Fatal("dndBestiary empty")
}
for id, m := range dndBestiary {
if m.ID != id {
t.Errorf("%s: ID mismatch (%s)", id, m.ID)
}
if m.Name == "" {
t.Errorf("%s: empty Name", id)
}
if m.HP < 1 {
t.Errorf("%s: HP=%d", id, m.HP)
}
if m.AC < 10 {
t.Errorf("%s: AC=%d (want ≥ 10)", id, m.AC)
}
if m.Attack < 1 {
t.Errorf("%s: Attack=%d", id, m.Attack)
}
if m.XPValue < 0 {
t.Errorf("%s: XPValue=%d", id, m.XPValue)
}
}
}
func TestBestiaryByCR(t *testing.T) {
low := dndBestiaryByCR(1.0)
for _, m := range low {
if m.CR > 1.0 {
t.Errorf("CR filter leaked: %s CR=%v", m.Name, m.CR)
}
}
high := dndBestiaryByCR(20.0)
if len(high) != len(dndBestiary) {
t.Errorf("CR≤20 filter returned %d, want all %d", len(high), len(dndBestiary))
}
}
func TestBestiaryToCombatStats(t *testing.T) {
dragon := dndBestiary["ancient_dragon"]
stats, mods := dragon.toCombatStats()
if stats.MaxHP != 367 {
t.Errorf("dragon HP = %d, want 367", stats.MaxHP)
}
if stats.AC != 22 {
t.Errorf("dragon AC = %d, want 22", stats.AC)
}
if stats.AttackBonus != 14 {
t.Errorf("dragon AttackBonus = %d, want 14", stats.AttackBonus)
}
if mods.DamageReduct != 1.0 {
t.Errorf("DamageReduct = %v, want 1.0", mods.DamageReduct)
}
}
func TestBestiaryStarterMonsters(t *testing.T) {
// Section 8.2 of v1.0 lists 6 starter monsters; verify all present.
wantIDs := []string{"goblin", "skeleton", "orc_grunt", "troll", "wyvern", "ancient_dragon"}
for _, id := range wantIDs {
if _, ok := dndBestiary[id]; !ok {
t.Errorf("starter bestiary missing: %s", id)
}
}
}