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:
@@ -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{
|
||||
|
||||
Reference in New Issue
Block a user