Skill focus identity resolution

This commit is contained in:
2026-05-24 20:44:52 +02:00
parent b2a58cb011
commit 34692fa9b4
2 changed files with 173 additions and 0 deletions
+36
View File
@@ -2472,6 +2472,9 @@ func (c *featGeneratedContext) resolveGeneratedFeatIdentity(spec familyExpansion
return c.resolveGeneratedFeatIdentityBySource(spec.FamilyKey, slug, rawSource)
}
featKey := c.preferredGeneratedFeatKey(spec.FamilyKey, slug)
if aliasKey, aliasID, ok := c.generatedFeatKeyFromSameFamilyChildAlias(spec.FamilyKey, slug); ok {
return aliasKey, aliasID, true, nil
}
if legacyKey, legacyID, ok := c.generatedFeatKeyFromLegacyAlias(spec.FamilyKey, slug); ok {
return legacyKey, legacyID, true, nil
}
@@ -2655,6 +2658,15 @@ func generatedAliasLockForKey(canonicalKey, lockedKey string, aliases generatedF
return false
}
legacyChildren := legacyFamilyChildAliases(identity.Parent, identity.Child)
parentPrefix := identity.Parent + "_"
if strings.HasPrefix(lockedKey, parentPrefix) {
lockedChild := strings.TrimPrefix(lockedKey, parentPrefix)
for _, legacyChild := range legacyChildren {
if normalizeKeyIdentity(lockedChild) == normalizeKeyIdentity(legacyChild) {
return true
}
}
}
for _, alias := range configuredGeneratedFamilyLegacyAliases(identity.Parent, aliases) {
prefix := alias + "_"
if !strings.HasPrefix(lockedKey, prefix) {
@@ -2737,6 +2749,30 @@ func (c *featGeneratedContext) generatedFeatKeyForID(familyKey string, rowID int
return "", false
}
func (c *featGeneratedContext) generatedFeatKeyFromSameFamilyChildAlias(familyKey, sourceSlug string) (string, int, bool) {
targets := legacyFamilyChildAliases(familyKey, sourceSlug)
canonicalKey := c.preferredGeneratedFeatKey(familyKey, sourceSlug)
prefix := "feat:" + familyKey + "_"
normalizedSource := normalizeKeyIdentity(sourceSlug)
for _, candidates := range []map[string]int{c.lockData, c.supplementalID} {
for _, target := range targets {
if normalizeKeyIdentity(target) == normalizedSource {
continue
}
for key, rowID := range candidates {
if rowID <= 0 || !strings.HasPrefix(key, prefix) {
continue
}
child := strings.TrimPrefix(key, prefix)
if normalizeKeyIdentity(child) == normalizeKeyIdentity(target) {
return canonicalKey, rowID, true
}
}
}
}
return "", 0, false
}
func (c *featGeneratedContext) generatedFeatKeyFromLegacyAlias(familyKey, sourceSlug string) (string, int, bool) {
targets := legacyFamilyChildAliases(familyKey, sourceSlug)
canonicalKey := c.preferredGeneratedFeatKey(familyKey, sourceSlug)
+137
View File
@@ -9594,6 +9594,143 @@ func TestBuildGeneratedSkillFocusUsesOverriddenCanonicalSkillKey(t *testing.T) {
}
}
func TestBuildGeneratedSkillFocusReusesHardcodedSkillFeatRowsForRenamedSkills(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",
"default_fields": {
"CRValue": "0.5",
"ReqSkillMinRanks": "1",
"ALLCLASSESCANUSE": "1",
"TOOLSCATEGORIES": "6",
"PreReqEpic": "0",
"ReqAction": "1"
},
"apply_after_modules": true,
"child_ref_field": "REQSKILL",
"child_source": {
"dataset": "skills",
"predicate": "accessible"
},
"overrides": {
"skills:craft_woodworking": {
"ICON": "ife_X1SFCrTrap"
}
}
}` + "\n",
"greater_skill_focus.json": `{
"family": "greater_skill_focus",
"family_key": "greater_skill_focus",
"template": "masterfeats:greaterskillfocus",
"name_prefix": "Greater Skill Focus",
"label_prefix": "FEAT_GREATER_SKILL_FOCUS",
"constant_prefix": "FEAT_GREATER_SKILL_FOCUS",
"legacy_family_keys": ["epic_skill_focus"],
"default_fields": {
"CRValue": "0.2",
"ReqSkillMinRanks": "15",
"ALLCLASSESCANUSE": "1",
"TOOLSCATEGORIES": "6",
"PreReqEpic": "0",
"ReqAction": "1"
},
"apply_after_modules": true,
"child_ref_field": "REQSKILL",
"child_source": {
"dataset": "skills",
"predicate": "accessible"
},
"overrides": {
"skills:craft_woodworking": {
"ICON": "ife_X2EpSkFCrTr"
}
}
}` + "\n",
}, map[string]int{
"feat:skill_focus_craft_trap": 407,
"feat:skill_focus_craft_woodworking": 1372,
"feat:greater_skill_focus_craft_trap": 590,
"feat:greater_skill_focus_craft_woodworking": 1314,
})
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"],
"rows": [
{"id": 22, "key": "skills:craft_woodworking", "Label": "CraftWoodworking", "Name": "2000", "Description": "2001", "Icon": "isk_craftwood", "Untrained": "1", "KeyAbility": "INT", "ArmorCheckPenalty": "0", "AllClassesCanUse": "1", "Category": "****", "MaxCR": "****", "Constant": "SKILL_CRAFT_WOODWORKING", "HostileSkill": "0", "HideFromLevelUp": "0"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:craft_woodworking":22}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
"output": "feat.2da",
"compare_reference": false,
"columns": [
"LABEL","FEAT","DESCRIPTION","ICON","MINATTACKBONUS","MINSTR","MINDEX","MININT","MINWIS","MINCON","MINCHA",
"MINSPELLLVL","PREREQFEAT1","PREREQFEAT2","GAINMULTIPLE","EFFECTSSTACK","ALLCLASSESCANUSE","CATEGORY","MAXCR",
"SPELLID","SUCCESSOR","CRValue","USESPERDAY","MASTERFEAT","TARGETSELF","OrReqFeat0","OrReqFeat1","OrReqFeat2",
"OrReqFeat3","OrReqFeat4","REQSKILL","ReqSkillMinRanks","REQSKILL2","ReqSkillMinRanks2","Constant","TOOLSCATEGORIES",
"HostileFeat","MinLevel","MinLevelClass","MaxLevel","MinFortSave","PreReqEpic","ReqAction"
],
"rows": [
{"id": 407, "key": "feat:skill_focus_craft_trap", "LABEL": "FEAT_SKILL_FOCUS_CRAFT_TRAP", "REQSKILL": 22, "MASTERFEAT": 4, "Constant": "FEAT_SKILL_FOCUS_CRAFT_TRAP"},
{"id": 590, "key": "feat:epic_skill_focus_craft_trap", "LABEL": "FEAT_EPIC_SKILL_FOCUS_CRAFT_TRAP", "REQSKILL": 22, "MASTERFEAT": 5, "Constant": "FEAT_EPIC_SKILL_FOCUS_CRAFT_TRAP"}
]
}`+"\n")
result, err := BuildNative(testProject(root), nil)
if err != nil {
t.Fatalf("BuildNative failed: %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, "407\tFEAT_SKILL_FOCUS_CRAFT_WOODWORKING\t") {
t.Fatalf("expected generated skill focus woodworking to reuse hardcoded Craft Trap row 407, got:\n%s", text)
}
if !strings.Contains(text, "590\tFEAT_GREATER_SKILL_FOCUS_CRAFT_WOODWORKING\t") {
t.Fatalf("expected generated greater skill focus woodworking to reuse hardcoded epic Craft Trap row 590, got:\n%s", text)
}
for _, unexpected := range []string{
"1372\tFEAT_SKILL_FOCUS_CRAFT_WOODWORKING\t",
"1314\tFEAT_GREATER_SKILL_FOCUS_CRAFT_WOODWORKING\t",
"FEAT_SKILL_FOCUS_CRAFT_TRAP\t",
"FEAT_EPIC_SKILL_FOCUS_CRAFT_TRAP\t",
} {
if strings.Contains(text, unexpected) {
t.Fatalf("expected no duplicate stale skill focus row %q, got:\n%s", unexpected, 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)
for _, expected := range []string{
`"feat:skill_focus_craft_woodworking": 407`,
`"feat:greater_skill_focus_craft_woodworking": 590`,
} {
if !strings.Contains(lockText, expected) {
t.Fatalf("expected lock to contain %s, got:\n%s", expected, lockText)
}
}
for _, stale := range []string{
"feat:skill_focus_craft_trap",
"feat:greater_skill_focus_craft_trap",
"feat:epic_skill_focus_craft_trap",
} {
if strings.Contains(lockText, stale) {
t.Fatalf("expected stale lock %s to be pruned, got:\n%s", stale, lockText)
}
}
}
func TestFeatGeneratedContextIgnoresExplicitlyRetiredFeatKeys(t *testing.T) {
root := testProjectRoot(t)
writeFeatGeneratedHarness(t, root, map[string]string{