Respect Accessibility Fields For Feats
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
|
||||
Reference in New Issue
Block a user