Respect Accessibility Fields For Feats

This commit is contained in:
2026-04-18 09:28:08 +02:00
parent 6daf5bf7a7
commit 897776f432
2 changed files with 140 additions and 8 deletions
+66 -8
View File
@@ -1627,6 +1627,9 @@ func buildFamilyExpansionGeneratedModule(path string, obj map[string]any, ctx *f
}
childToken := childTokenFromExpandedKey(featKey, spec.FamilyKey)
displayName := displayNameForGeneratedSource(spec.ChildSource.Dataset, row)
if spec.IdentitySource == "child_source_value" && childToken != "" && normalizeKeyIdentity(childToken) != normalizeKeyIdentity(slug) {
displayName = displayNameFromSlug(childToken)
}
labelSuffix := upperSnake(displayName)
override := map[string]any{
"key": featKey,
@@ -2569,7 +2572,7 @@ func resolveNativeDataset(dataset nativeCollectedDataset, keyToID map[string]int
classKey := "classes:" + dataset.Dataset.Name[strings.LastIndex(dataset.Dataset.Name, "/")+1:]
featSuccessors := buildFeatSuccessorsIndex(globalRowByKey, keyToID)
classSkills := buildClassSkillsIndex(tableRegistry, classKey)
expanded, err := expandClassesFeatRows(rows, keyToID, globalRowByKey, featSuccessors, classSkills, globalRowByKey)
expanded, err := expandClassesFeatRows(rows, keyToID, globalRowByKey, featSuccessors, classSkills, globalRowByKey, classKey)
if err != nil {
return nil, fmt.Errorf("dataset %s: %w", dataset.Dataset.Name, err)
}
@@ -2617,7 +2620,7 @@ var (
}
)
func expandClassesFeatRows(rows []map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, featSuccessors map[string]string, classSkills map[string]bool, allRowByKey map[string]map[string]any) ([]map[string]any, error) {
func expandClassesFeatRows(rows []map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, featSuccessors map[string]string, classSkills map[string]bool, allRowByKey map[string]map[string]any, classKey string) ([]map[string]any, error) {
injected := make([]map[string]any, 0, len(classFeatGlobalRows)+len(classFeatClassSkillShorthandRows))
existingRefIDs := make(map[string]struct{}, len(rows))
@@ -2656,7 +2659,7 @@ func expandClassesFeatRows(rows []map[string]any, keyToID map[string]int, rowByK
if _, exists := existingRefIDs[refID]; exists {
continue
}
rowItems, err := expandClassesFeatRow(row, keyToID, rowByKey, featSuccessors, classSkills)
rowItems, err := expandClassesFeatRow(row, keyToID, rowByKey, featSuccessors, classSkills, classKey)
if err != nil {
return nil, err
}
@@ -2667,7 +2670,7 @@ func expandClassesFeatRows(rows []map[string]any, keyToID map[string]int, rowByK
expanded := make([]map[string]any, 0, len(rows))
for _, row := range rows {
rowItems, err := expandClassesFeatRow(row, keyToID, rowByKey, featSuccessors, classSkills)
rowItems, err := expandClassesFeatRow(row, keyToID, rowByKey, featSuccessors, classSkills, classKey)
if err != nil {
return nil, err
}
@@ -2684,7 +2687,7 @@ func expandClassesFeatRows(rows []map[string]any, keyToID map[string]int, rowByK
return combined, nil
}
func expandClassesFeatRow(row map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, featSuccessors map[string]string, classSkills map[string]bool) ([]map[string]any, error) {
func expandClassesFeatRow(row map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, featSuccessors map[string]string, classSkills map[string]bool, classKey string) ([]map[string]any, error) {
featRef, ok := row["FeatIndex"].(map[string]any)
if !ok {
return []map[string]any{row}, nil
@@ -2704,7 +2707,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
children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey)
children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey, classKey)
if err != nil {
return nil, err
}
@@ -2745,7 +2748,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) ([]map[string]any, error) {
func expandMasterfeatChildren(masterfeatKey string, row map[string]any, keyToID map[string]int, rowByKey map[string]map[string]any, classKey string) ([]map[string]any, error) {
if _, ok := keyToID[masterfeatKey]; !ok {
return nil, fmt.Errorf("unknown masterfeat reference %q", masterfeatKey)
}
@@ -2761,6 +2764,9 @@ func expandMasterfeatChildren(masterfeatKey string, row map[string]any, keyToID
if !rowReferencesMasterfeat(featRow, masterfeatKey, keyToID) {
continue
}
if !classFeatExpansionCanUseFeat(featRow, classKey, keyToID) {
continue
}
featID, ok := keyToID[key]
if !ok {
continue
@@ -2782,6 +2788,58 @@ func expandMasterfeatChildren(masterfeatKey string, row map[string]any, keyToID
return expanded, nil
}
func classFeatExpansionCanUseFeat(featRow map[string]any, classKey string, keyToID map[string]int) bool {
value, ok := lookupField(featRow, "ALLCLASSESCANUSE")
if !ok || isNullishValue(value) {
return true
}
if strings.TrimSpace(fmt.Sprint(value)) != "0" {
return true
}
if classKey == "" {
return false
}
minLevelClass, ok := lookupField(featRow, "MinLevelClass")
if !ok || isNullishValue(minLevelClass) {
return false
}
return rowRefersToKey(minLevelClass, classKey, keyToID)
}
func rowRefersToKey(value any, targetKey string, keyToID map[string]int) bool {
targetID, hasTargetID := keyToID[targetKey]
switch typed := value.(type) {
case string:
text := strings.TrimSpace(typed)
if text == "" || text == nullValue || text == "****" {
return false
}
if text == targetKey {
return true
}
if hasTargetID {
if id, err := strconv.Atoi(text); err == nil {
return id == targetID
}
}
return false
case int:
return hasTargetID && typed == targetID
case float64:
return hasTargetID && int(typed) == targetID
case map[string]any:
if id, ok := typed["id"].(string); ok {
return rowRefersToKey(id, targetKey, keyToID)
}
if key, ok := typed["key"].(string); ok {
return rowRefersToKey(key, targetKey, keyToID)
}
return false
default:
return false
}
}
func rowReferencesMasterfeat(row map[string]any, masterfeatKey string, keyToID map[string]int) bool {
masterID, ok := keyToID[masterfeatKey]
if !ok {
@@ -2996,7 +3054,7 @@ func expandMasterfeatChildrenFiltered(masterfeatKey string, row map[string]any,
if classSkills == nil || len(classSkills) == 0 {
return []map[string]any{}, nil
}
children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey)
children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey, "")
if err != nil {
return nil, err
}
+74
View File
@@ -526,6 +526,80 @@ func TestBuildNativeExpandsClassesFeatMasterfeatRows(t *testing.T) {
}
}
func TestBuildNativeClassFeatMasterfeatExpansionRespectsAllClassesCanUse(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "feats"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat"))
mkdirAll(t, filepath.Join(root, "topdata", "data", "masterfeats"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "base.json"), `{
"output": "classes.2da",
"columns": ["Label", "Name", "FeatsTable"],
"rows": [
{"id": 4, "key": "classes:fighter", "Label": "Fighter", "Name": "111", "FeatsTable": "cls_feat_fighter"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "lock.json"), `{"classes:fighter":4}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "classes", "feats", "fighter.json"), `{
"key": "classes/feats:fighter",
"output": "cls_feat_fighter.2da",
"columns": ["FeatLabel", "FeatIndex", "List", "GrantedOnLevel", "OnMenu"],
"rows": [
{"FeatIndex": {"id": "masterfeats:weaponfocus"}, "List": "1", "GrantedOnLevel": "-1", "OnMenu": "0"},
{"FeatIndex": {"id": "masterfeats:weaponspecialization"}, "List": "1", "GrantedOnLevel": "4", "OnMenu": "0"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
"output": "feat.2da",
"columns": ["LABEL", "FEAT", "DESCRIPTION", "MASTERFEAT", "ALLCLASSESCANUSE", "MinLevelClass"],
"rows": [
{"id": 100, "key": "feat:weaponfocus_longsword", "LABEL": "WeaponFocusLongsword", "FEAT": "1000", "DESCRIPTION": "1001", "MASTERFEAT": "1", "ALLCLASSESCANUSE": "1", "MinLevelClass": "****"},
{"id": 101, "key": "feat:weaponfocus_creature", "LABEL": "WeaponFocusCreature", "FEAT": "1002", "DESCRIPTION": "1003", "MASTERFEAT": "1", "ALLCLASSESCANUSE": "0", "MinLevelClass": "****"},
{"id": 200, "key": "feat:weaponspecialization_longsword", "LABEL": "WeaponSpecLongsword", "FEAT": "2000", "DESCRIPTION": "2001", "MASTERFEAT": "2", "ALLCLASSESCANUSE": "0", "MinLevelClass": {"id": "classes:fighter"}}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "lock.json"), `{
"feat:weaponfocus_longsword": 100,
"feat:weaponfocus_creature": 101,
"feat:weaponspecialization_longsword": 200
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "masterfeats", "base.json"), `{
"output": "masterfeats.2da",
"columns": ["LABEL", "STRREF", "DESCRIPTION", "ICON"],
"rows": [
{"id": 1, "key": "masterfeats:weaponfocus", "LABEL": "WeaponFocus", "STRREF": "6490", "DESCRIPTION": "436", "ICON": "ife_wepfoc"},
{"id": 2, "key": "masterfeats:weaponspecialization", "LABEL": "WeaponSpecialization", "STRREF": "6491", "DESCRIPTION": "437", "ICON": "ife_wepspec"}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "masterfeats", "lock.json"), `{
"masterfeats:weaponfocus": 1,
"masterfeats:weaponspecialization": 2
}`+"\n")
p := testProject(root)
p.Config.TopData.ReferenceBuilder = ""
result, err := BuildNative(p, nil)
if err != nil {
t.Fatalf("BuildNative failed: %v", err)
}
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "cls_feat_fighter.2da"))
if err != nil {
t.Fatalf("read cls_feat_fighter.2da: %v", err)
}
text := string(got)
if !strings.Contains(text, "WeaponFocusLongsword") {
t.Fatalf("expected globally selectable weapon focus row, got:\n%s", text)
}
if strings.Contains(text, "WeaponFocusCreature") {
t.Fatalf("expected ALLCLASSESCANUSE=0 creature row without class restriction to be filtered, got:\n%s", text)
}
if !strings.Contains(text, "WeaponSpecLongsword") {
t.Fatalf("expected class-restricted ALLCLASSESCANUSE=0 row to remain available to fighter, got:\n%s", text)
}
}
func TestBuildNativeExpandsClassesFeatMasterfeatRowsWithFilter(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "feats"))