Fix Generated Feat Identity Canonicalization

This commit is contained in:
2026-04-17 21:35:54 +02:00
parent f32dd224fe
commit 1e9a81a6cf
3 changed files with 338 additions and 4 deletions
+119 -2
View File
@@ -2191,10 +2191,11 @@ func writeFeatGeneratedHarness(t *testing.T, root string, generatedFiles map[str
{"id": 5, "key": "masterfeats:greaterskillfocus", "LABEL": "GreaterSkillFocus", "STRREF": {"tlk": {"key": "masterfeats:greaterskillfocus.name", "text": "Greater Skill Focus"}}, "DESCRIPTION": {"tlk": {"key": "masterfeats:greaterskillfocus.description", "text": "Choose a skill again."}}, "ICON": "ife_skfoc", "CRValue": "1", "ReqSkillMinRanks": "6"},
{"id": 6, "key": "masterfeats:weaponfocus", "LABEL": "WeaponFocus", "STRREF": {"tlk": {"key": "masterfeats:weaponfocus.name", "text": "Weapon Focus"}}, "DESCRIPTION": {"tlk": {"key": "masterfeats:weaponfocus.description", "text": "Focus on one weapon."}}, "ICON": "ife_wepfoc", "CRValue": "1", "MINATTACKBONUS": "1", "ALLCLASSESCANUSE": "1", "PreReqEpic": "0", "ReqAction": "1", "TOOLSCATEGORIES": "1"},
{"id": 7, "key": "masterfeats:improvedcritical", "LABEL": "ImprovedCritical", "STRREF": {"tlk": {"key": "masterfeats:improvedcritical.name", "text": "Improved Critical"}}, "DESCRIPTION": {"tlk": {"key": "masterfeats:improvedcritical.description", "text": "Improved critical threat."}}, "ICON": "ife_impcrit", "CRValue": "1", "MINATTACKBONUS": "8", "ALLCLASSESCANUSE": "1", "PreReqEpic": "0", "ReqAction": "1", "TOOLSCATEGORIES": "1"},
{"id": 8, "key": "masterfeats:weaponproficiencycommoner", "LABEL": "WeaponProficiencyCommoner", "STRREF": {"tlk": {"key": "masterfeats:weaponproficiencycommoner.name", "text": "Commoner Weapon Proficiency"}}, "DESCRIPTION": {"tlk": {"key": "masterfeats:weaponproficiencycommoner.description", "text": "Simple commoner weapon proficiency."}}, "ICON": "ife_weppro", "CRValue": "0.2", "SUCCESSOR": "9", "ALLCLASSESCANUSE": "1", "PreReqEpic": "0", "ReqAction": "1"}
{"id": 8, "key": "masterfeats:weaponproficiencycommoner", "LABEL": "WeaponProficiencyCommoner", "STRREF": {"tlk": {"key": "masterfeats:weaponproficiencycommoner.name", "text": "Commoner Weapon Proficiency"}}, "DESCRIPTION": {"tlk": {"key": "masterfeats:weaponproficiencycommoner.description", "text": "Simple commoner weapon proficiency."}}, "ICON": "ife_weppro", "CRValue": "0.2", "SUCCESSOR": "9", "ALLCLASSESCANUSE": "1", "PreReqEpic": "0", "ReqAction": "1"},
{"id": 9, "key": "masterfeats:greaterweaponfocus", "LABEL": "GreaterWeaponFocus", "STRREF": {"tlk": {"key": "masterfeats:greaterweaponfocus.name", "text": "Greater Weapon Focus"}}, "DESCRIPTION": {"tlk": {"key": "masterfeats:greaterweaponfocus.description", "text": "Focus harder on one weapon."}}, "ICON": "ife_gwepfoc", "CRValue": "1", "MINATTACKBONUS": "8", "ALLCLASSESCANUSE": "0", "PreReqEpic": "0", "ReqAction": "1", "TOOLSCATEGORIES": "1"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "masterfeats", "lock.json"), `{"masterfeats:skillfocus":4,"masterfeats:greaterskillfocus":5,"masterfeats:weaponfocus":6,"masterfeats:improvedcritical":7,"masterfeats:weaponproficiencycommoner":8}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "masterfeats", "lock.json"), `{"masterfeats:skillfocus":4,"masterfeats:greaterskillfocus":5,"masterfeats:weaponfocus":6,"masterfeats:improvedcritical":7,"masterfeats:weaponproficiencycommoner":8,"masterfeats:greaterweaponfocus":9}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
"output": "skills.2da",
"columns": ["Label", "Name", "Description", "Icon", "Untrained", "KeyAbility", "ArmorCheckPenalty", "AllClassesCanUse", "Category", "MaxCR", "Constant", "HostileSkill", "HideFromLevelUp"],
@@ -2310,6 +2311,122 @@ func TestPreferredGeneratedFeatKeyReusesLegacyTLKStateKey(t *testing.T) {
}
}
func TestResolveGeneratedFeatIdentityTranslatesLegacyFamilySourceID(t *testing.T) {
ctx := &featGeneratedContext{
lockData: map[string]int{"feat:epicweaponfocus_club": 619},
supplementalID: map[string]int{},
tlkStateKeys: map[string]struct{}{},
existingFeat: map[string]struct{}{"feat:epicweaponfocus_club": {}},
}
got, rowID, hasID, err := ctx.resolveGeneratedFeatIdentityBySource("greaterweaponfocus", "club", "619")
if err != nil {
t.Fatalf("resolveGeneratedFeatIdentityBySource: %v", err)
}
if got != "feat:greaterweaponfocus_club" || !hasID || rowID != 619 {
t.Fatalf("expected translated greater weapon focus identity at id 619, got key=%q id=%d hasID=%v", got, rowID, hasID)
}
}
func TestResolveGeneratedFeatIdentityMovesExplicitCanonicalRefToLegacyAliasID(t *testing.T) {
ctx := &featGeneratedContext{
lockData: map[string]int{
"feat:greaterweaponfocus_shortspear": 1289,
"feat:epicweaponfocus_shortspear": 627,
},
supplementalID: map[string]int{},
tlkStateKeys: map[string]struct{}{},
}
got, rowID, hasID, err := ctx.resolveGeneratedFeatIdentityBySource("greaterweaponfocus", "shortspear", map[string]any{"id": "feat:greaterweaponfocus_shortspear"})
if err != nil {
t.Fatalf("resolveGeneratedFeatIdentityBySource: %v", err)
}
if got != "feat:greaterweaponfocus_shortspear" || !hasID || rowID != 627 {
t.Fatalf("expected explicit canonical ref to move to legacy alias id 627, got key=%q id=%d hasID=%v", got, rowID, hasID)
}
}
func TestResolveGeneratedFeatIdentityUsesLegacyAliasForNoSourceFamily(t *testing.T) {
ctx := &featGeneratedContext{
lockData: map[string]int{
"feat:skillfocus_animalhandling": 1307,
"feat:skillfocus_animalempathy": 34,
"feat:greaterskillfocus_appraise": 1316,
"feat:epicskillfocus_appraise": 588,
"feat:epicskillfocus_animalempathy": 587,
},
supplementalID: map[string]int{},
tlkStateKeys: map[string]struct{}{},
}
spec := familyExpansionSpec{FamilyKey: "skillfocus"}
got, rowID, hasID, err := ctx.resolveGeneratedFeatIdentity(spec, "animalhandling", map[string]any{})
if err != nil {
t.Fatalf("resolveGeneratedFeatIdentity skill focus renamed skill: %v", err)
}
if got != "feat:skillfocus_animalhandling" || !hasID || rowID != 34 {
t.Fatalf("expected skill focus renamed child to take legacy alias id 34, got key=%q id=%d hasID=%v", got, rowID, hasID)
}
spec = familyExpansionSpec{FamilyKey: "greaterskillfocus"}
got, rowID, hasID, err = ctx.resolveGeneratedFeatIdentity(spec, "appraise", map[string]any{})
if err != nil {
t.Fatalf("resolveGeneratedFeatIdentity: %v", err)
}
if got != "feat:greaterskillfocus_appraise" || !hasID || rowID != 588 {
t.Fatalf("expected no-source generated family to take legacy alias id 588, got key=%q id=%d hasID=%v", got, rowID, hasID)
}
got, rowID, hasID, err = ctx.resolveGeneratedFeatIdentity(spec, "animalhandling", map[string]any{})
if err != nil {
t.Fatalf("resolveGeneratedFeatIdentity renamed skill: %v", err)
}
if got != "feat:greaterskillfocus_animalhandling" || !hasID || rowID != 587 {
t.Fatalf("expected renamed no-source generated family to take legacy alias id 587, got key=%q id=%d hasID=%v", got, rowID, hasID)
}
}
func TestBuildFamilyExpansionMovesCanonicalLockToLegacyAliasID(t *testing.T) {
root := testProjectRoot(t)
writeFeatGeneratedHarness(t, root, map[string]string{
"greater_skill_focus.json": `{
"family": "greater_skill_focus",
"family_key": "greaterskillfocus",
"template": "masterfeats:greaterskillfocus",
"name_prefix": "Greater Skill Focus",
"label_prefix": "FEAT_GREATER_SKILL_FOCUS",
"constant_prefix": "FEAT_GREATER_SKILL_FOCUS",
"child_ref_field": "REQSKILL",
"child_source": {"dataset":"skills","predicate":"accessible"},
"overrides": {"skills:concentration":{"ICON":"ife_greater_concentration"}}
}` + "\n",
}, map[string]int{
"feat:greaterskillfocus_concentration": 1317,
"feat:epicskillfocus_concentration": 589,
})
result, err := BuildNative(testProject(root), nil)
if err != nil {
t.Fatalf("BuildNative: %v", err)
}
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "feat.2da"))
if err != nil {
t.Fatalf("read feat.2da: %v", err)
}
text := string(got)
if !strings.Contains(text, "589\tFEAT_GREATER_SKILL_FOCUS_CONCENTRATION") {
t.Fatalf("expected greater skill focus to move to legacy alias id 589, got:\n%s", text)
}
lockRaw, err := os.ReadFile(filepath.Join(root, "topdata", "data", "feat", "lock.json"))
if err != nil {
t.Fatalf("read feat lock: %v", err)
}
lockText := string(lockRaw)
if !strings.Contains(lockText, `"feat:greaterskillfocus_concentration": 589`) {
t.Fatalf("expected canonical lock to move to legacy alias id, got:\n%s", lockText)
}
if strings.Contains(lockText, "epicskillfocus_concentration") {
t.Fatalf("expected stale epic skill focus lock to be pruned, got:\n%s", lockText)
}
}
func TestPruneRetiredGeneratedFeatTLKEntries(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "generated"))