mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
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>
104 lines
3.8 KiB
Go
104 lines
3.8 KiB
Go
package plugin
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gogobee/internal/flavor"
|
|
)
|
|
|
|
// TestDnDFlavorPoolsNonEmpty: every pool we wired into the D&D layer must
|
|
// have at least one entry. Catches accidental deletions in the protected
|
|
// flavor files that would silently produce empty narrative.
|
|
func TestDnDFlavorPoolsNonEmpty(t *testing.T) {
|
|
pools := map[string][]string{
|
|
"CombatStart": flavor.CombatStart,
|
|
"CombatVictory": flavor.CombatVictory,
|
|
"BossDeath": flavor.BossDeath,
|
|
"PlayerDeath": flavor.PlayerDeath,
|
|
"ZoneComplete": flavor.ZoneComplete,
|
|
"Nat20": flavor.Nat20,
|
|
"Nat1": flavor.Nat1,
|
|
"LevelUp": flavor.LevelUp,
|
|
"ItemFound": flavor.ItemFound,
|
|
"RestShort": flavor.RestShort,
|
|
"RestLong": flavor.RestLong,
|
|
"HomeLongRest": flavor.HomeLongRest,
|
|
"MistyGreeting": flavor.MistyGreeting,
|
|
"MistyInsightSuccess": flavor.MistyInsightSuccess,
|
|
"MistySkillFail": flavor.MistySkillFail,
|
|
"ArinaGreeting": flavor.ArinaGreeting,
|
|
"ArinaArcanaSuccess": flavor.ArinaArcanaSuccess,
|
|
"ArinaSkillFail": flavor.ArinaSkillFail,
|
|
"ExpeditionStart": flavor.ExpeditionStart,
|
|
}
|
|
for name, pool := range pools {
|
|
if len(pool) == 0 {
|
|
t.Errorf("flavor pool %s is empty — protected file deletion?", name)
|
|
}
|
|
for i, s := range pool {
|
|
if s == "" {
|
|
t.Errorf("flavor.%s[%d] is empty string", name, i)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDnDFlavorHelpersReturnNonEmpty: every helper must produce a non-empty
|
|
// string when its underlying pool has entries.
|
|
func TestDnDFlavorHelpersReturnNonEmpty(t *testing.T) {
|
|
cases := map[string]func() string{
|
|
"dndCombatOpeningLine": dndCombatOpeningLine,
|
|
"dndZoneCompleteLine": dndZoneCompleteLine,
|
|
"dndNat20Line": dndNat20Line,
|
|
"dndNat1Line": dndNat1Line,
|
|
"dndLevelUpFlavorLine": dndLevelUpFlavorLine,
|
|
"dndItemFoundLine": dndItemFoundLine,
|
|
"dndRestShortFlavorLine": dndRestShortFlavorLine,
|
|
"dndCombatClosingLine_Win": func() string { return dndCombatClosingLine(true, false) },
|
|
"dndCombatClosingLine_Boss": func() string { return dndCombatClosingLine(true, true) },
|
|
"dndCombatClosingLine_Death": func() string { return dndCombatClosingLine(false, false) },
|
|
"dndRestLongFlavorLine_Home": func() string { return dndRestLongFlavorLine(true) },
|
|
"dndRestLongFlavorLine_Inn": func() string { return dndRestLongFlavorLine(false) },
|
|
}
|
|
for name, fn := range cases {
|
|
// Run several times — flavor pulls a random pool member, so a single
|
|
// call can flake if the pool happens to have an empty string. The
|
|
// pool-non-empty test above guards against that, but be belt-and-suspenders.
|
|
for i := 0; i < 10; i++ {
|
|
if got := fn(); got == "" {
|
|
t.Errorf("%s returned empty (call #%d)", name, i)
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestDnDGreetingPoolUnion: combined Misty/Arina greeting pools include both
|
|
// legacy openings and the D&D-flavored entries.
|
|
func TestDnDGreetingPoolUnion(t *testing.T) {
|
|
mistyPool := dndMistyGreetingPool()
|
|
if len(mistyPool) != len(flavor.MistyGreeting)+len(mistyOpenings) {
|
|
t.Errorf("Misty pool union size = %d; expected %d (flavor %d + legacy %d)",
|
|
len(mistyPool), len(flavor.MistyGreeting)+len(mistyOpenings),
|
|
len(flavor.MistyGreeting), len(mistyOpenings))
|
|
}
|
|
arinaPool := dndArinaGreetingPool()
|
|
if len(arinaPool) != len(flavor.ArinaGreeting)+len(arinaOpenings) {
|
|
t.Errorf("Arina pool union size = %d; expected %d", len(arinaPool),
|
|
len(flavor.ArinaGreeting)+len(arinaOpenings))
|
|
}
|
|
}
|
|
|
|
func TestDnDItalicize(t *testing.T) {
|
|
if got := dndItalicize(""); got != "" {
|
|
t.Errorf("italicize(empty) = %q, want empty", got)
|
|
}
|
|
if got := dndItalicize(" "); got != "" {
|
|
t.Errorf("italicize(whitespace) = %q, want empty", got)
|
|
}
|
|
got := dndItalicize("hello")
|
|
if got != "\n\n_hello_" {
|
|
t.Errorf("italicize = %q, want \\n\\n_hello_", got)
|
|
}
|
|
}
|