Normalize feat names (#8)

Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/8
Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit is contained in:
2026-05-21 21:03:23 +02:00
committed by archvillainette
parent 59ec5cb41d
commit 74c5ea34e1
5 changed files with 396 additions and 8 deletions
+178
View File
@@ -2954,6 +2954,184 @@ func TestParseFamilyExpansionRejectsInvalidLegacyFamilyKeys(t *testing.T) {
}
}
func TestGeneratedWeaponFeatTitleStyleUsesSourceDisplayNameWithStableIdentity(t *testing.T) {
root := testProjectRoot(t)
writeFeatGeneratedHarness(t, root, map[string]string{
"weapon_focus.json": `{
"family": "weapon_focus",
"family_key": "weapon_focus",
"template": "masterfeats:weaponfocus",
"name_prefix": "Weapon Focus",
"label_prefix": "FEAT_WEAPON_FOCUS",
"constant_prefix": "FEAT_WEAPON_FOCUS",
"identity_source": "child_source_value",
"child_source": {"dataset":"baseitems","column":"WeaponFocusFeat"},
"title_style": {"child_case":"lower","child_parenthetical":"preserve"},
"overrides": {}
}` + "\n",
}, map[string]int{"feat:weapon_focus_dwaxe": 3003})
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), `{
"entries": {
"83310": {"text": "Dwarven Waraxe"}
}
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "base.json"), `{
"output": "baseitems.2da",
"columns": ["label", "Name", "WeaponFocusFeat"],
"rows": [
{"id": 108, "key": "baseitems:dwarvenwaraxe", "label": "dwaxe", "Name": "83310", "WeaponFocusFeat": "3003"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "lock.json"), `{"baseitems:dwarvenwaraxe":108}`+"\n")
module := buildGeneratedFeatModuleForTest(t, root, "weapon_focus.json")
override := generatedFeatOverrideByKey(t, module, "feat:weapon_focus_dwaxe")
if text := generatedFeatOverrideTitle(t, override); text != "Weapon Focus (dwarven waraxe)" {
t.Fatalf("expected source display title for stable generated feat identity, got %q", text)
}
if id, ok := override["id"].(int); !ok || id != 3003 {
t.Fatalf("expected generated title style to preserve compact row id 3003, got %#v", override["id"])
}
}
func TestGeneratedSkillFeatTitleStyleFlattensChildParenthetical(t *testing.T) {
root := testProjectRoot(t)
writeFeatGeneratedHarness(t, root, map[string]string{
"skill_focus.json": `{
"family": "skill_focus",
"family_key": "skill_focus",
"template": "masterfeats:skillfocus",
"name_prefix": "Skill Focus",
"label_prefix": "FEAT_SKILL_FOCUS",
"constant_prefix": "FEAT_SKILL_FOCUS",
"child_ref_field": "REQSKILL",
"child_source": {"dataset":"skills","predicate":"accessible"},
"title_style": {"child_case":"lower","child_parenthetical":"comma"},
"overrides": {}
}` + "\n",
}, map[string]int{})
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
"output": "skills.2da",
"columns": ["Label", "Name", "HideFromLevelUp"],
"rows": [
{"id": 10, "key": "skills:knowledge_local", "Label": "KnowledgeLocal", "Name": {"tlk": {"text": "Knowledge (local)"}}, "HideFromLevelUp": "0"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:knowledge_local":10}`+"\n")
module := buildGeneratedFeatModuleForTest(t, root, "skill_focus.json")
override := generatedFeatOverrideByKey(t, module, "feat:skill_focus_knowledge_local")
if text := generatedFeatOverrideTitle(t, override); text != "Skill Focus (knowledge, local)" {
t.Fatalf("expected flattened generated skill title, got %q", text)
}
}
func TestParseFamilyExpansionTitleStyleDefaultsAndValidation(t *testing.T) {
base := map[string]any{
"family": "skill_focus",
"family_key": "skill_focus",
"template": "masterfeats:skillfocus",
"child_source": map[string]any{"dataset": "skills", "predicate": "accessible"},
}
spec, err := parseFamilyExpansionSpec("default-title-style.json", base)
if err != nil {
t.Fatalf("parse default title style: %v", err)
}
if spec.TitleStyle.ChildCase != "preserve" || spec.TitleStyle.ChildParenthetical != "preserve" {
t.Fatalf("expected default title style to preserve legacy behavior, got %#v", spec.TitleStyle)
}
for _, tc := range []struct {
name string
titleStyle map[string]any
want string
}{
{name: "child case", titleStyle: map[string]any{"child_case": "headline"}, want: "title_style.child_case"},
{name: "child parenthetical", titleStyle: map[string]any{"child_parenthetical": "nested"}, want: "title_style.child_parenthetical"},
} {
t.Run(tc.name, func(t *testing.T) {
obj := map[string]any{}
for key, value := range base {
obj[key] = value
}
obj["title_style"] = tc.titleStyle
_, err := parseFamilyExpansionSpec("invalid-title-style.json", obj)
if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "invalid-title-style.json") {
t.Fatalf("expected %q validation error with file context, got %v", tc.want, err)
}
})
}
}
func buildGeneratedFeatModuleForTest(t *testing.T, root, name string) map[string]any {
t.Helper()
datasets, err := discoverNativeDatasets(filepath.Join(root, "topdata", "data"))
if err != nil {
t.Fatalf("discover datasets: %v", err)
}
var feat nativeDataset
for _, dataset := range datasets {
if dataset.Name == "feat" {
feat = dataset
break
}
}
if feat.Name == "" {
t.Fatal("expected feat dataset")
}
lock, err := loadLockfile(feat.LockPath)
if err != nil {
t.Fatalf("load feat lock: %v", err)
}
ctx, err := newFeatGeneratedContext(feat, lock)
if err != nil {
t.Fatalf("new generated feat context: %v", err)
}
path := filepath.Join(feat.GeneratedDir, name)
obj, err := loadJSONObject(path)
if err != nil {
t.Fatalf("load generated family %s: %v", name, err)
}
module, err := buildFamilyExpansionGeneratedModule(path, obj, ctx)
if err != nil {
t.Fatalf("build generated family %s: %v", name, err)
}
return module
}
func generatedFeatOverrideByKey(t *testing.T, module map[string]any, key string) map[string]any {
t.Helper()
overrides, ok := module["overrides"].([]any)
if !ok {
t.Fatalf("expected generated overrides, got %#v", module)
}
for _, raw := range overrides {
override, ok := raw.(map[string]any)
if ok && override["key"] == key {
return override
}
}
t.Fatalf("expected generated override %q, got %#v", key, overrides)
return nil
}
func generatedFeatOverrideTitle(t *testing.T, override map[string]any) string {
t.Helper()
feat, ok := override["FEAT"].(map[string]any)
if !ok {
t.Fatalf("expected generated FEAT TLK data, got %#v", override)
}
tlk, ok := feat["tlk"].(map[string]any)
if !ok {
t.Fatalf("expected generated FEAT tlk block, got %#v", feat)
}
text, ok := tlk["text"].(string)
if !ok {
t.Fatalf("expected generated FEAT text, got %#v", tlk)
}
return text
}
func TestValidateBaseitemsWeaponFeatColumnCompletenessRejectsPartialCoreFamilies(t *testing.T) {
ctx := &featGeneratedContext{
rowsByDataset: map[string]map[string]map[string]any{