Warn when masterfeat expansion silently drops unobtainable feats (#31)
`classFeatExpansionCanUseFeat` excludes `ALLCLASSESCANUSE=0` feats from class feat table masterfeat expansion. When such a feat also has no `MinLevelClass`, no class can ever obtain it — a data bug that previously failed silently (Spell Focus vanishing from the wizard table, Favored Enemy from ranger, proficiencies from commoner). New `validateClassFeatMasterfeatAccessibility` warns for exactly that case; exclusions with an explicit `MinLevelClass` (deliberate class restrictions, e.g. fighter weapon specialization) and `ALLCLASSESCANUSE=1` feats stay silent. No keys are hardcoded — everything is derived from the datasets. Verified: new test + full `go test ./internal/topdata/` pass; against live sow-topdata data it flagged 36 real drops (all fixed by the companion sow-topdata PR, after which it reports 0). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #31 Reviewed-by: xtul <mpiasecki720@protonmail.com> Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit was merged in pull request #31.
This commit is contained in:
@@ -0,0 +1,98 @@
|
||||
package topdata
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// validateClassFeatMasterfeatAccessibility warns when a class feat table
|
||||
// references a masterfeat whose member feats are silently dropped from
|
||||
// expansion by classFeatExpansionCanUseFeat. Only the ambiguous case is
|
||||
// flagged: ALLCLASSESCANUSE=0 with no MinLevelClass, where the feat cannot
|
||||
// be obtained by any class at all. Exclusions with an explicit MinLevelClass
|
||||
// are deliberate cross-class restrictions and stay silent.
|
||||
func validateClassFeatMasterfeatAccessibility(dataDir string, report *ValidationReport) {
|
||||
datasets, err := discoverNativeDatasets(dataDir)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
keyToID := map[string]int{}
|
||||
var featRowByKey map[string]map[string]any
|
||||
type classFeatTable struct {
|
||||
path string
|
||||
classKey string
|
||||
rows []map[string]any
|
||||
}
|
||||
classTables := []classFeatTable{}
|
||||
for _, dataset := range datasets {
|
||||
name := filepath.ToSlash(dataset.Name)
|
||||
isClassFeats := strings.HasPrefix(name, "classes/feats/")
|
||||
if name != "feat" && name != "masterfeats" && name != "classes/core" && !isClassFeats {
|
||||
continue
|
||||
}
|
||||
collected, err := collectNativeDataset(dataset)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for key, id := range collected.LockData {
|
||||
keyToID[key] = id
|
||||
}
|
||||
switch {
|
||||
case name == "feat":
|
||||
featRowByKey = rowsByKey(collected.Rows)
|
||||
case isClassFeats:
|
||||
classTables = append(classTables, classFeatTable{
|
||||
path: dataset.BasePath,
|
||||
classKey: "classes:" + name[strings.LastIndex(name, "/")+1:],
|
||||
rows: collected.Rows,
|
||||
})
|
||||
}
|
||||
}
|
||||
if featRowByKey == nil {
|
||||
return
|
||||
}
|
||||
for _, table := range classTables {
|
||||
for _, row := range table.rows {
|
||||
featRef, ok := row["FeatIndex"].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
refKey, _ := featRef["id"].(string)
|
||||
if !strings.HasPrefix(refKey, "masterfeats:") {
|
||||
continue
|
||||
}
|
||||
masterfeatKey := resolveCanonicalDatasetKey(refKey, keyToID)
|
||||
if _, ok := keyToID[masterfeatKey]; !ok {
|
||||
continue
|
||||
}
|
||||
featKeys := make([]string, 0, len(featRowByKey))
|
||||
for featKey := range featRowByKey {
|
||||
if strings.HasPrefix(featKey, "feat:") {
|
||||
featKeys = append(featKeys, featKey)
|
||||
}
|
||||
}
|
||||
slices.Sort(featKeys)
|
||||
for _, featKey := range featKeys {
|
||||
featRow := featRowByKey[featKey]
|
||||
if !rowReferencesMasterfeat(featRow, masterfeatKey, keyToID) {
|
||||
continue
|
||||
}
|
||||
if classFeatExpansionCanUseFeat(featRow, table.classKey, keyToID) {
|
||||
continue
|
||||
}
|
||||
if minLevelClass, ok := lookupField(featRow, "MinLevelClass"); ok && !isNullishValue(minLevelClass) {
|
||||
continue
|
||||
}
|
||||
report.Diagnostics = append(report.Diagnostics, Diagnostic{
|
||||
Severity: SeverityWarning,
|
||||
Path: table.path,
|
||||
Message: fmt.Sprintf(
|
||||
"%s references %s but %s is excluded from expansion (ALLCLASSESCANUSE=0 with no MinLevelClass makes it unobtainable); override ALLCLASSESCANUSE or set MinLevelClass",
|
||||
table.classKey, masterfeatKey, featKey),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user