mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
Plumbed through the open5e importer so regen stays safe:
- New cmd/open5e-import/desc_overrides.go holds two per-ID override
maps (spellDescOverride, magicItemDescOverride) and a regex-driven
cleanDesc sanitizer. Override wins outright; otherwise the SRD
first-sentence runs through cleanDesc, which strips the phrases
the S3 acceptance criteria forbid (saving throw[s], spell slot,
within range, 5-foot, DC <n>, "(save DC X)" parentheticals,
"constitution score is N", "out to a range of N feet"). A small
post-pass repairs the orphan stubs the strippers leave behind
(" and." trailers, "must make." after the saving-throw object
is gone).
- gen.go (spells) + magicitems.go now call spellDescription /
magicItemDescription instead of raw firstSentence; same hand-
authored override pattern, same cleanDesc fallthrough.
- Override coverage: the 19 SRD-only spells that show up in
defaultKnownSpells (call_lightning, charm_person, vicious_mockery,
…) plus ~35 high-visibility magic items (Amulet of Health, every
Belt of Giant Strength variant, Cloak of Displacement, etc.).
Tone is outcome-first second-person with bite — these surface
in the spellbook and the curio shop, so they get to be funny.
- tuned.go (R21) + magicitems.go strip "(...)" from emitted Names
via stripNameParenthetical. Slug keeps the variant; only the
display text loses the qualifier. Two bestiary entries
(giant_rat_diseased, deep_gnome_svirfneblin) and stone_of_good_luck
affected.
- Regenerated all three data files. Acceptance grep is clean:
zero hits for any banned phrase in Description/Desc fields.
- New cmd/open5e-import/desc_overrides_test.go covers the
sanitizer regressions, orphan-repair, name-strip, and the
override-wins-but-fallthrough-still-sanitizes path.
Conflicts: none. S4 (magic-item UX) wanted this in first so the
new curio renderer consumes clean text — ready for it now.
123 lines
3.8 KiB
Go
123 lines
3.8 KiB
Go
package main
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// TestCleanDescStripsJargon — the S3 acceptance criteria translated into
|
|
// regression cases. Any phrase listed here must not survive the sanitizer.
|
|
func TestCleanDescStripsJargon(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
in string
|
|
mustNot []string
|
|
}{
|
|
{
|
|
name: "saving throw + DC",
|
|
in: "Each creature in the area must make a Dexterity saving throw (DC 15) or take damage.",
|
|
mustNot: []string{
|
|
"saving throw", "DC 15", "DC15",
|
|
},
|
|
},
|
|
{
|
|
name: "saving throws plural",
|
|
in: "You have advantage on saving throws against spells.",
|
|
mustNot: []string{"saving throws"},
|
|
},
|
|
{
|
|
name: "within range",
|
|
in: "Hurl a bolt at a creature within range.",
|
|
mustNot: []string{"within range"},
|
|
},
|
|
{
|
|
name: "5-foot radius",
|
|
in: "Each creature in a 5-foot radius takes damage.",
|
|
mustNot: []string{"5-foot"},
|
|
},
|
|
{
|
|
name: "spell slot upcast clause",
|
|
in: "When you cast this spell using a spell slot of 2nd level or higher, the damage increases.",
|
|
mustNot: []string{"spell slot"},
|
|
},
|
|
{
|
|
name: "constitution score",
|
|
in: "Your Constitution score is 19 while you wear this amulet.",
|
|
mustNot: []string{"Constitution score"},
|
|
},
|
|
{
|
|
name: "(save DC X) parenthetical",
|
|
in: "You can use an action to cast the detect thoughts spell (save DC 13) from it.",
|
|
mustNot: []string{"(save", "DC 13"},
|
|
},
|
|
{
|
|
name: "wisdom save",
|
|
in: "Forces a wisdom save against the caster.",
|
|
mustNot: []string{"wisdom save"},
|
|
},
|
|
{
|
|
name: "out to a range of feet",
|
|
in: "You have darkvision out to a range of 60 feet.",
|
|
mustNot: []string{"feet", "out to a range"},
|
|
},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
got := cleanDesc(tc.in)
|
|
low := strings.ToLower(got)
|
|
for _, ban := range tc.mustNot {
|
|
if strings.Contains(low, strings.ToLower(ban)) {
|
|
t.Errorf("cleanDesc kept banned phrase %q\n in: %q\n out: %q", ban, tc.in, got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestCleanDescRepairsOrphans — the strippers leave dangling stubs ("must
|
|
// make.", " and."); the post-pass is meant to tidy them so player-facing
|
|
// prose doesn't end mid-clause.
|
|
func TestCleanDescRepairsOrphans(t *testing.T) {
|
|
in := "You gain a +1 bonus to ability checks and saving throws."
|
|
got := cleanDesc(in)
|
|
if strings.HasSuffix(got, " and.") {
|
|
t.Errorf("orphan trailing 'and.' survived: %q", got)
|
|
}
|
|
in2 := "Up to three creatures of your choice that you can see must make charisma saving throws."
|
|
got2 := cleanDesc(in2)
|
|
if strings.Contains(strings.ToLower(got2), "must make.") {
|
|
t.Errorf("orphan 'must make.' survived: %q", got2)
|
|
}
|
|
}
|
|
|
|
// TestStripNameParenthetical — bestiary R21 / magic-item alias names.
|
|
func TestStripNameParenthetical(t *testing.T) {
|
|
cases := map[string]string{
|
|
"Giant Rat (Diseased)": "Giant Rat",
|
|
"Deep Gnome (Svirfneblin)": "Deep Gnome",
|
|
"Stone of Good Luck (Luckstone)": "Stone of Good Luck",
|
|
"Ordinary Name": "Ordinary Name",
|
|
"": "",
|
|
}
|
|
for in, want := range cases {
|
|
if got := stripNameParenthetical(in); got != want {
|
|
t.Errorf("stripNameParenthetical(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestSpellOverrideWins — the curated descriptions for the SRD-only default
|
|
// spells must beat the auto first-sentence path.
|
|
func TestSpellOverrideWins(t *testing.T) {
|
|
got := spellDescription("vicious_mockery", "anything could go here")
|
|
if got != spellDescOverride["vicious_mockery"] {
|
|
t.Errorf("override didn't win: got %q", got)
|
|
}
|
|
// Unknown ID falls through to cleanDesc.
|
|
in := "Hurl a bolt at a creature within range."
|
|
got = spellDescription("unknown_spell_xyz", in)
|
|
if strings.Contains(got, "within range") {
|
|
t.Errorf("fallthrough didn't sanitize: %q", got)
|
|
}
|
|
}
|