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)
|
childToken := childTokenFromExpandedKey(featKey, spec.FamilyKey)
|
||||||
displayName := displayNameForGeneratedSource(spec.ChildSource.Dataset, row)
|
displayName := displayNameForGeneratedSource(spec.ChildSource.Dataset, row)
|
||||||
|
if spec.IdentitySource == "child_source_value" && childToken != "" && normalizeKeyIdentity(childToken) != normalizeKeyIdentity(slug) {
|
||||||
|
displayName = displayNameFromSlug(childToken)
|
||||||
|
}
|
||||||
labelSuffix := upperSnake(displayName)
|
labelSuffix := upperSnake(displayName)
|
||||||
override := map[string]any{
|
override := map[string]any{
|
||||||
"key": featKey,
|
"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:]
|
classKey := "classes:" + dataset.Dataset.Name[strings.LastIndex(dataset.Dataset.Name, "/")+1:]
|
||||||
featSuccessors := buildFeatSuccessorsIndex(globalRowByKey, keyToID)
|
featSuccessors := buildFeatSuccessorsIndex(globalRowByKey, keyToID)
|
||||||
classSkills := buildClassSkillsIndex(tableRegistry, classKey)
|
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 {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("dataset %s: %w", dataset.Dataset.Name, err)
|
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))
|
injected := make([]map[string]any, 0, len(classFeatGlobalRows)+len(classFeatClassSkillShorthandRows))
|
||||||
existingRefIDs := make(map[string]struct{}, len(rows))
|
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 {
|
if _, exists := existingRefIDs[refID]; exists {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
rowItems, err := expandClassesFeatRow(row, keyToID, rowByKey, featSuccessors, classSkills)
|
rowItems, err := expandClassesFeatRow(row, keyToID, rowByKey, featSuccessors, classSkills, classKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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))
|
expanded := make([]map[string]any, 0, len(rows))
|
||||||
for _, row := range 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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -2684,7 +2687,7 @@ func expandClassesFeatRows(rows []map[string]any, keyToID map[string]int, rowByK
|
|||||||
return combined, nil
|
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)
|
featRef, ok := row["FeatIndex"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return []map[string]any{row}, nil
|
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
|
return []map[string]any{cloned}, nil
|
||||||
case strings.HasPrefix(refKey, "masterfeats:"):
|
case strings.HasPrefix(refKey, "masterfeats:"):
|
||||||
masterfeatKey := refKey
|
masterfeatKey := refKey
|
||||||
children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey)
|
children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey, classKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if _, ok := keyToID[masterfeatKey]; !ok {
|
||||||
return nil, fmt.Errorf("unknown masterfeat reference %q", masterfeatKey)
|
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) {
|
if !rowReferencesMasterfeat(featRow, masterfeatKey, keyToID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if !classFeatExpansionCanUseFeat(featRow, classKey, keyToID) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
featID, ok := keyToID[key]
|
featID, ok := keyToID[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
@@ -2782,6 +2788,58 @@ func expandMasterfeatChildren(masterfeatKey string, row map[string]any, keyToID
|
|||||||
return expanded, nil
|
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 {
|
func rowReferencesMasterfeat(row map[string]any, masterfeatKey string, keyToID map[string]int) bool {
|
||||||
masterID, ok := keyToID[masterfeatKey]
|
masterID, ok := keyToID[masterfeatKey]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -2996,7 +3054,7 @@ func expandMasterfeatChildrenFiltered(masterfeatKey string, row map[string]any,
|
|||||||
if classSkills == nil || len(classSkills) == 0 {
|
if classSkills == nil || len(classSkills) == 0 {
|
||||||
return []map[string]any{}, nil
|
return []map[string]any{}, nil
|
||||||
}
|
}
|
||||||
children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey)
|
children, err := expandMasterfeatChildren(masterfeatKey, row, keyToID, rowByKey, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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) {
|
func TestBuildNativeExpandsClassesFeatMasterfeatRowsWithFilter(t *testing.T) {
|
||||||
root := testProjectRoot(t)
|
root := testProjectRoot(t)
|
||||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "feats"))
|
mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "feats"))
|
||||||
|
|||||||
Reference in New Issue
Block a user