From d61ab33aa41510ec7d1233742bfb88f85e1e8135 Mon Sep 17 00:00:00 2001 From: vickydotbat Date: Thu, 14 May 2026 09:29:55 +0200 Subject: [PATCH] Class skill injection now points to authored entries --- internal/topdata/native.go | 62 +++++++++++++++++++++++--------- internal/topdata/topdata_test.go | 23 ++++++++++++ 2 files changed, 69 insertions(+), 16 deletions(-) diff --git a/internal/topdata/native.go b/internal/topdata/native.go index e55504c..793bb5b 100644 --- a/internal/topdata/native.go +++ b/internal/topdata/native.go @@ -2466,6 +2466,38 @@ func normalizeKeyIdentity(text string) string { return text } +func resolveCanonicalDatasetKey(key string, keyToID map[string]int) string { + key = strings.TrimSpace(key) + if key == "" { + return key + } + if _, ok := keyToID[key]; ok { + return key + } + prefix, suffix, ok := strings.Cut(key, ":") + if !ok || prefix == "" || suffix == "" { + return key + } + normalizedSuffix := normalizeKeyIdentity(suffix) + match := "" + for candidate := range keyToID { + candidatePrefix, candidateSuffix, ok := strings.Cut(candidate, ":") + if !ok || candidatePrefix != prefix { + continue + } + if normalizeKeyIdentity(candidateSuffix) != normalizedSuffix { + continue + } + if match == "" || candidate < match { + match = candidate + } + } + if match != "" { + return match + } + return key +} + func affinityTitleFromKey(featKey, skillName string) string { if strings.HasPrefix(featKey, "feat:partialskillaffinity") { return fmt.Sprintf("Partial Skill Affinity (%s)", skillName) @@ -2652,8 +2684,8 @@ var ( {"FeatIndex": map[string]any{"id": "feat:horsemenu"}, "List": "3", "GrantedOnLevel": "1", "OnMenu": "1"}, } classFeatClassSkillShorthandRows = []map[string]any{ - {"FeatIndex": map[string]any{"id": "masterfeats:skillfocus", "filter": "classskills"}, "List": "1", "GrantedOnLevel": "-1", "OnMenu": "0"}, - {"FeatIndex": map[string]any{"id": "masterfeats:greaterskillfocus", "filter": "classskills"}, "List": "1", "GrantedOnLevel": "12", "OnMenu": "0"}, + {"FeatIndex": map[string]any{"id": "masterfeats:skill_focus", "filter": "classskills"}, "List": "1", "GrantedOnLevel": "-1", "OnMenu": "0"}, + {"FeatIndex": map[string]any{"id": "masterfeats:greater_skill_focus", "filter": "classskills"}, "List": "1", "GrantedOnLevel": "12", "OnMenu": "0"}, } ) @@ -2743,7 +2775,7 @@ func expandClassesFeatRow(row map[string]any, keyToID map[string]int, rowByKey m } return []map[string]any{cloned}, nil case strings.HasPrefix(refKey, "masterfeats:"): - masterfeatKey := refKey + masterfeatKey := resolveCanonicalDatasetKey(refKey, keyToID) children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey, classKey) if err != nil { return nil, err @@ -2786,6 +2818,7 @@ func expandClassesFeatRow(row map[string]any, keyToID map[string]int, rowByKey m } func expandMasterfeatChildren(masterfeatKey string, row map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, classKey string) ([]map[string]any, error) { + masterfeatKey = resolveCanonicalDatasetKey(masterfeatKey, keyToID) if _, ok := keyToID[masterfeatKey]; !ok { return nil, fmt.Errorf("unknown masterfeat reference %q", masterfeatKey) } @@ -2844,10 +2877,11 @@ func classFeatExpansionCanUseFeat(featRow map[string]any, classKey string, keyTo } func rowRefersToKey(value any, targetKey string, keyToID map[string]int) bool { + targetKey = resolveCanonicalDatasetKey(targetKey, keyToID) targetID, hasTargetID := keyToID[targetKey] switch typed := value.(type) { case string: - text := strings.TrimSpace(typed) + text := resolveCanonicalDatasetKey(strings.TrimSpace(typed), keyToID) if text == "" || text == nullValue || text == "****" { return false } @@ -2878,6 +2912,7 @@ func rowRefersToKey(value any, targetKey string, keyToID map[string]int) bool { } func rowReferencesMasterfeat(row map[string]any, masterfeatKey string, keyToID map[string]int) bool { + masterfeatKey = resolveCanonicalDatasetKey(masterfeatKey, keyToID) masterID, ok := keyToID[masterfeatKey] if !ok { return false @@ -2895,6 +2930,7 @@ func rowReferencesMasterfeat(row map[string]any, masterfeatKey string, keyToID m return int(typed) == masterID case map[string]any: if id, ok := typed["id"].(string); ok { + id = resolveCanonicalDatasetKey(id, keyToID) if id == masterfeatKey { return true } @@ -3369,6 +3405,12 @@ func shouldPreserveLiteralFeatIDObject(row map[string]any, field, refID string) func shouldPreserveFeatMinLevelClass(row map[string]any) bool { rowKey, _ := row["key"].(string) + if prefix, suffix, ok := strings.Cut(strings.TrimSpace(rowKey), ":"); ok && prefix == "masterfeats" { + switch normalizeKeyIdentity(suffix) { + case "weaponfocus", "weaponspecialization", "greaterweaponfocus", "greaterweaponspecialization", "improvedcritical", "overwhelmingcritical": + return true + } + } switch { case strings.HasPrefix(rowKey, "feat:weaponfocus_"): return true @@ -3386,18 +3428,6 @@ func shouldPreserveFeatMinLevelClass(row map[string]any) bool { return true case strings.HasPrefix(rowKey, "feat:epicweaponspecialization_"): return true - case rowKey == "masterfeats:weaponfocus": - return true - case rowKey == "masterfeats:weaponspecialization": - return true - case rowKey == "masterfeats:greaterweaponfocus": - return true - case rowKey == "masterfeats:greaterweaponspecialization": - return true - case rowKey == "masterfeats:improvedcritical": - return true - case rowKey == "masterfeats:overwhelmingcritical": - return true default: return false } diff --git a/internal/topdata/topdata_test.go b/internal/topdata/topdata_test.go index 9bc5b97..ef667b9 100644 --- a/internal/topdata/topdata_test.go +++ b/internal/topdata/topdata_test.go @@ -818,6 +818,29 @@ func TestBuildNativeExpandsClassesFeatClassskillsFilter(t *testing.T) { } } +func TestResolveCanonicalDatasetKeyMatchesMasterfeatAliases(t *testing.T) { + keyToID := map[string]int{ + "masterfeats:skill_focus": 4, + "masterfeats:greater_skill_focus": 15, + "masterfeats:weapon_focus": 1, + "masterfeats:greater_weapon_specialization": 11, + } + + cases := map[string]string{ + "masterfeats:skillfocus": "masterfeats:skill_focus", + "masterfeats:greaterskillfocus": "masterfeats:greater_skill_focus", + "masterfeats:weaponfocus": "masterfeats:weapon_focus", + "masterfeats:greaterweaponspecialization": "masterfeats:greater_weapon_specialization", + "masterfeats:skill_focus": "masterfeats:skill_focus", + } + + for input, want := range cases { + if got := resolveCanonicalDatasetKey(input, keyToID); got != want { + t.Fatalf("resolveCanonicalDatasetKey(%q) = %q, want %q", input, got, want) + } + } +} + func TestValidateProjectRejectsLegacyAuthoredTLKModules(t *testing.T) { root := testProjectRoot(t) mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))