Files
sow-tools/internal/topdata/class_feat_accessibility.go
archvillainette cec3466779
test / test (push) Successful in 1m23s
build-binaries / build-binaries (push) Successful in 2m7s
build-image / publish (push) Successful in 14s
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>
2026-07-06 11:20:46 +00:00

99 lines
2.9 KiB
Go

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),
})
}
}
}
}