mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 08:52:42 +00:00
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>
This commit is contained in:
103
internal/plugin/dnd_flavor_test.go
Normal file
103
internal/plugin/dnd_flavor_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user