Family Expansions
This commit is contained in:
@@ -2150,6 +2150,9 @@ func TestValidateProjectRejectsInvalidFeatGeneratedFile(t *testing.T) {
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "generated", "skill_focus.json"), `{
|
||||
"family": "skill_focus",
|
||||
"family_key": "skillfocus",
|
||||
"template": "masterfeats:skillfocus",
|
||||
"label_prefix": "FEAT_SKILL_FOCUS",
|
||||
"constant_prefix": "FEAT_SKILL_FOCUS",
|
||||
"child_source": {"dataset":"skills","predicate":"accessible"},
|
||||
"overrides": {}
|
||||
}`+"\n")
|
||||
@@ -2160,7 +2163,7 @@ func TestValidateProjectRejectsInvalidFeatGeneratedFile(t *testing.T) {
|
||||
}
|
||||
text := diagnosticsText(report.Diagnostics)
|
||||
for _, want := range []string{
|
||||
"compact feat generated file must contain a non-empty template string",
|
||||
"family expansion file must contain a non-empty name_prefix",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected diagnostic %q, got:\n%s", want, text)
|
||||
@@ -2308,6 +2311,29 @@ func TestPreferredGeneratedFeatKeyReusesLegacyTLKStateKey(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestPruneRetiredGeneratedFeatTLKEntries(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "generated"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "generated", "skill_focus.json"), `{
|
||||
"family": "anything",
|
||||
"family_key": "skillfocus",
|
||||
"template": "masterfeats:skillfocus",
|
||||
"name_prefix": "Skill Focus",
|
||||
"label_prefix": "FEAT_SKILL_FOCUS",
|
||||
"constant_prefix": "FEAT_SKILL_FOCUS",
|
||||
"template_fields": ["DESCRIPTION", "ICON"],
|
||||
"child_source": {"dataset":"skills","predicate":"accessible"}
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "generated", "weapon_focus.json"), `{
|
||||
"family": "anything_else",
|
||||
"family_key": "weaponfocus",
|
||||
"template": "masterfeats:weaponfocus",
|
||||
"name_prefix": "Weapon Focus",
|
||||
"label_prefix": "FEAT_WEAPON_FOCUS",
|
||||
"constant_prefix": "FEAT_WEAPON_FOCUS",
|
||||
"template_fields": ["DESCRIPTION", "ICON"],
|
||||
"identity_source": "child_source_value",
|
||||
"child_source": {"dataset":"baseitems","column":"WeaponFocusFeat"}
|
||||
}`+"\n")
|
||||
entries := map[string]tlkStateMapping{
|
||||
"feat:skillfocus_craftalchemy.name": {ID: 10, Retired: true},
|
||||
"feat:weaponproficiencycommoner_club.description": {ID: 11, Retired: true},
|
||||
@@ -2316,13 +2342,13 @@ func TestPruneRetiredGeneratedFeatTLKEntries(t *testing.T) {
|
||||
"masterfeats:skillfocus.name": {ID: 14, Retired: true},
|
||||
}
|
||||
|
||||
pruneRetiredGeneratedFeatTLKEntries(entries)
|
||||
pruneRetiredGeneratedFeatTLKEntries(filepath.Join(root, "topdata"), entries)
|
||||
|
||||
if _, ok := entries["feat:skillfocus_craftalchemy.name"]; ok {
|
||||
t.Fatal("expected retired generated skill focus TLK key to be pruned")
|
||||
}
|
||||
if _, ok := entries["feat:weaponproficiencycommoner_club.description"]; ok {
|
||||
t.Fatal("expected retired generated proficiency TLK key to be pruned")
|
||||
if _, ok := entries["feat:weaponproficiencycommoner_club.description"]; !ok {
|
||||
t.Fatal("expected module-authored proficiency TLK key to be preserved")
|
||||
}
|
||||
if _, ok := entries["feat:weaponfocus_club.name"]; !ok {
|
||||
t.Fatal("expected active generated feat TLK key to be preserved")
|
||||
@@ -2338,16 +2364,15 @@ func TestPruneRetiredGeneratedFeatTLKEntries(t *testing.T) {
|
||||
func TestBuildSupportsGeneratedFeatFamilies(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeFeatGeneratedHarness(t, root, map[string]string{
|
||||
"skill_focus.json": `{"family":"skill_focus","family_key":"skillfocus","template":"masterfeats:skillfocus","child_source":{"dataset":"skills","predicate":"accessible"},"overrides":{"skills:concentration":{"ICON":"ife_skfoc_override"}}}` + "\n",
|
||||
"greater_skill_focus.json": `{"family":"greater_skill_focus","family_key":"greaterskillfocus","template":"masterfeats:greaterskillfocus","child_source":{"dataset":"skills","predicate":"accessible"}}` + "\n",
|
||||
"weapon_focus.json": `{"family":"weapon_focus","family_key":"weaponfocus","template":"masterfeats:weaponfocus","child_source":{"dataset":"baseitems","column":"WeaponFocusFeat"},"overrides":{"baseitems:testclub":{"ICON":"ife_wepfoc_override"}}}` + "\n",
|
||||
"improved_critical.json": `{"family":"improved_critical","family_key":"improvedcritical","template":"masterfeats:improvedcritical","child_source":{"dataset":"baseitems","column":"WeaponImprovedCriticalFeat"}}` + "\n",
|
||||
"proficiencies.json": `{"family":"proficiencies","entries":{"feat:weaponproficiencycommoner_club":{"LABEL":"CommonerWeaponProficiencyCLUB","FEAT":{"tlk":{"key":"feat:weaponproficiencycommoner_club.name","text":"Commoner Weapon Proficiency (Club)"}},"DESCRIPTION":{"field":"DESCRIPTION","ref":"masterfeats:weaponproficiencycommoner"},"ICON":{"field":"ICON","ref":"masterfeats:weaponproficiencycommoner"},"MASTERFEAT":{"id":"masterfeats:weaponproficiencycommoner"},"SUCCESSOR":{"field":"SUCCESSOR","ref":"masterfeats:weaponproficiencycommoner"},"ALLCLASSESCANUSE":{"field":"ALLCLASSESCANUSE","ref":"masterfeats:weaponproficiencycommoner"},"PreReqEpic":{"field":"PreReqEpic","ref":"masterfeats:weaponproficiencycommoner"},"ReqAction":{"field":"ReqAction","ref":"masterfeats:weaponproficiencycommoner"},"CRValue":{"field":"CRValue","ref":"masterfeats:weaponproficiencycommoner"},"Constant":"FEAT_WEAPON_PROFICIENCY_COMMONER_CLUB"}}}` + "\n",
|
||||
"skill_focus.json": `{"family":"skill_family_from_data","family_key":"skillfocus","template":"masterfeats:skillfocus","name_prefix":"Skill Focus","label_prefix":"FEAT_SKILL_FOCUS","constant_prefix":"FEAT_SKILL_FOCUS","template_fields":["DESCRIPTION","ICON","CRValue","ReqSkillMinRanks","ALLCLASSESCANUSE","PreReqEpic","ReqAction"],"default_fields":{"TOOLSCATEGORIES":"2"},"child_ref_field":"REQSKILL","allow_existing_only":true,"child_source":{"dataset":"skills","predicate":"accessible"},"overrides":{"skills:concentration":{"ICON":"ife_skfoc_override"}}}` + "\n",
|
||||
"greater_skill_focus.json": `{"family":"greater_skill_family_from_data","family_key":"greaterskillfocus","template":"masterfeats:greaterskillfocus","name_prefix":"Greater Skill Focus","label_prefix":"FEAT_GREATER_SKILL_FOCUS","constant_prefix":"FEAT_GREATER_SKILL_FOCUS","template_fields":["DESCRIPTION","ICON","CRValue","ReqSkillMinRanks","ALLCLASSESCANUSE","PreReqEpic","ReqAction"],"default_fields":{"TOOLSCATEGORIES":"2"},"child_ref_field":"REQSKILL","allow_existing_only":true,"child_source":{"dataset":"skills","predicate":"accessible"}}` + "\n",
|
||||
"weapon_focus.json": `{"family":"custom_weapon_mastery","family_key":"weaponfocus","template":"masterfeats:weaponfocus","name_prefix":"Weapon Focus","label_prefix":"FEAT_WEAPON_FOCUS","constant_prefix":"FEAT_WEAPON_FOCUS","template_fields":["DESCRIPTION","ICON","MINATTACKBONUS","ALLCLASSESCANUSE","PreReqEpic","ReqAction","CRValue","MinLevel","MinLevelClass","MINSTR"],"default_fields":{"TOOLSCATEGORIES":"1"},"identity_source":"child_source_value","child_source":{"dataset":"baseitems","column":"WeaponFocusFeat"},"overrides":{"baseitems:testclub":{"ICON":"ife_wepfoc_override"}}}` + "\n",
|
||||
"improved_critical.json": `{"family":"custom_critical_chain","family_key":"improvedcritical","template":"masterfeats:improvedcritical","name_prefix":"Improved Critical","label_prefix":"FEAT_IMPROVED_CRITICAL","constant_prefix":"FEAT_IMPROVED_CRITICAL","template_fields":["DESCRIPTION","ICON","MINATTACKBONUS","ALLCLASSESCANUSE","PreReqEpic","ReqAction","CRValue","MinLevel","MinLevelClass","MINSTR"],"default_fields":{"TOOLSCATEGORIES":"1"},"identity_source":"child_source_value","auto_prereq_fields":{"PREREQFEAT1":"weaponfocus"},"child_source":{"dataset":"baseitems","column":"WeaponImprovedCriticalFeat"}}` + "\n",
|
||||
}, map[string]int{"feat:skillfocus_concentration": 173, "feat:greaterskillfocus_concentration": 589, "feat:skillaffinityathletics": 1253, "feat:weaponfocus_testclub": 3003, "feat:improvedcritical_testclub": 3004, "feat:weaponproficiencycommoner_club": 1123, "feat:specialattacks": 1187, "feat:improvedfeint": 1194})
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "modules", "activecombat"))
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "modules", "proficiency"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "modules", "activecombat", "specialattacks.json"), `{"entries":{"feat:specialattacks":{"LABEL":"FEAT_SPECIAL_ATTACKS","FEAT":{"tlk":{"key":"feat:specialattacks.name","text":"Special Attacks"}},"DESCRIPTION":{"tlk":{"key":"feat:specialattacks.description","text":"Access special attacks."}},"ICON":"ife_specialatk","CATEGORY":"22","HostileFeat":"1","MAXCR":"0","SPELLID":{"id":"spells:specialattacks"},"Constant":"FEAT_SPECIAL_ATTACKS","TOOLSCATEGORIES":"2","PreReqEpic":"0","ReqAction":"0"},"feat:improvedfeint":{"LABEL":"FEAT_IMPROVED_FEINT","FEAT":{"tlk":{"key":"feat:improvedfeint.name","text":"Improved Feint"}},"DESCRIPTION":{"tlk":{"key":"feat:improvedfeint.description","text":"Improve your feint."}},"ICON":"ife_feint","PREREQFEAT1":{"id":"feat:specialattacks"},"Constant":"FEAT_IMPROVED_FEINT","TOOLSCATEGORIES":{"field":"TOOLSCATEGORIES","ref":"feat:specialattacks"},"PreReqEpic":"0","ReqAction":"1"}}}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "modules", "proficiency", "standalone.json"), `{"entries":{"feat:towershieldproficiency":{"LABEL":"FEAT_TOWER_SHIELD_PROFICIENCY","FEAT":{"tlk":{"key":"feat:towershieldproficiency.name","text":"Tower Shield Proficiency"}},"DESCRIPTION":{"tlk":{"key":"feat:towershieldproficiency.description","text":"Use tower shields."}},"ICON":"ife_towshprof","PREREQFEAT1":{"id":"feat:shieldproficiency"},"Constant":"FEAT_TOWER_SHIELD_PROFICIENCY","ReqAction":"1","PreReqEpic":"0"}},"overrides":[{"key":"feat:shieldproficiency","id":32,"DESCRIPTION":{"tlk":{"key":"feat:shieldproficiency.description","text":"Use shields."}}}]}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "modules", "proficiency", "standalone.json"), `{"entries":{"feat:towershieldproficiency":{"LABEL":"FEAT_TOWER_SHIELD_PROFICIENCY","FEAT":{"tlk":{"key":"feat:towershieldproficiency.name","text":"Tower Shield Proficiency"}},"DESCRIPTION":{"tlk":{"key":"feat:towershieldproficiency.description","text":"Use tower shields."}},"ICON":"ife_towshprof","PREREQFEAT1":{"id":"feat:shieldproficiency"},"Constant":"FEAT_TOWER_SHIELD_PROFICIENCY","ReqAction":"1","PreReqEpic":"0"}},"overrides":[{"key":"feat:shieldproficiency","id":32,"DESCRIPTION":{"tlk":{"key":"feat:shieldproficiency.description","text":"Use shields."}}},{"key":"feat:weaponproficiencycommoner_club","id":1123,"LABEL":"CommonerWeaponProficiencyCLUB","Constant":"FEAT_WEAPON_PROFICIENCY_CLUB","FEAT":{"tlk":{"key":"feat:weaponproficiencycommoner_club.name","text":"Commoner Weapon Proficiency (club)"}},"DESCRIPTION":{"field":"DESCRIPTION","ref":"masterfeats:weaponproficiencycommoner"},"ICON":{"field":"ICON","ref":"masterfeats:weaponproficiencycommoner"},"MASTERFEAT":{"id":"masterfeats:weaponproficiencycommoner"},"SUCCESSOR":{"field":"SUCCESSOR","ref":"masterfeats:weaponproficiencycommoner"},"ALLCLASSESCANUSE":{"field":"ALLCLASSESCANUSE","ref":"masterfeats:weaponproficiencycommoner"},"PreReqEpic":{"field":"PreReqEpic","ref":"masterfeats:weaponproficiencycommoner"},"ReqAction":{"field":"ReqAction","ref":"masterfeats:weaponproficiencycommoner"},"CRValue":{"field":"CRValue","ref":"masterfeats:weaponproficiencycommoner"}}]}`+"\n")
|
||||
|
||||
result, err := BuildNative(testProject(root), nil)
|
||||
if err != nil {
|
||||
@@ -2366,6 +2391,151 @@ func TestBuildSupportsGeneratedFeatFamilies(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFamilyExpansionUsesAuthoredSpecForCoverageAndBaselines(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeFeatGeneratedHarness(t, root, map[string]string{
|
||||
"skill_focus.json": `{"family":"whatever_the_author_wants","family_key":"skillfocus","template":"masterfeats:skillfocus","name_prefix":"Skill Focus","label_prefix":"FEAT_SKILL_FOCUS","constant_prefix":"FEAT_SKILL_FOCUS","template_fields":["DESCRIPTION","ICON","CRValue","ReqSkillMinRanks","ALLCLASSESCANUSE","PreReqEpic","ReqAction"],"default_fields":{"TOOLSCATEGORIES":"2"},"child_ref_field":"REQSKILL","child_source":{"dataset":"skills","predicate":"accessible"}}` + "\n",
|
||||
"weapon_focus.json": `{"family":"another_authored_name","family_key":"weaponfocus","template":"masterfeats:weaponfocus","name_prefix":"Weapon Focus","label_prefix":"FEAT_WEAPON_FOCUS","constant_prefix":"FEAT_WEAPON_FOCUS","template_fields":["DESCRIPTION","ICON","MINATTACKBONUS","ALLCLASSESCANUSE","PreReqEpic","ReqAction","CRValue","MinLevel","MinLevelClass","MINSTR"],"default_fields":{"TOOLSCATEGORIES":"9","ALLCLASSESCANUSE":"0"},"identity_source":"child_source_value","child_source":{"dataset":"baseitems","column":"WeaponFocusFeat"}}` + "\n",
|
||||
}, map[string]int{"feat:skillfocus_concentration": 173, "feat:skillfocus_athletics": 174, "feat:weaponfocus_testclub": 3003})
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "modules", "weapons"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "modules", "weapons", "testclub.json"), `{"overrides":[{"key":"feat:weaponfocus_testclub","id":3003,"LABEL":"FEAT_WEAPON_FOCUS_TEST_CLUB","Constant":"FEAT_WEAPON_FOCUS_TEST_CLUB","FEAT":{"tlk":{"key":"feat:weaponfocus_testclub.name","text":"Weapon Focus (Test Club)"}}}]}`+"\n")
|
||||
|
||||
result, err := BuildNative(testProject(root), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNative: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "feat.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read feat.2da: %v", err)
|
||||
}
|
||||
text := string(got)
|
||||
|
||||
for _, want := range []string{
|
||||
"FEAT_SKILL_FOCUS_CONCENTRATION",
|
||||
"FEAT_SKILL_FOCUS_ATHLETICS",
|
||||
"FEAT_WEAPON_FOCUS_TEST_CLUB",
|
||||
} {
|
||||
if !strings.Contains(text, want) {
|
||||
t.Fatalf("expected feat output to contain %q, got:\n%s", want, text)
|
||||
}
|
||||
}
|
||||
|
||||
type rowData map[string]string
|
||||
parse2DA := func(data string) map[string]rowData {
|
||||
lines := strings.Split(data, "\n")
|
||||
header := strings.Split(lines[2], "\t")
|
||||
rows := map[string]rowData{}
|
||||
for _, line := range lines[3:] {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(line, "\t")
|
||||
if len(parts) < len(header)+1 {
|
||||
continue
|
||||
}
|
||||
row := rowData{}
|
||||
for i, col := range header {
|
||||
row[col] = parts[i+1]
|
||||
}
|
||||
rows[parts[0]] = row
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
featRows := parse2DA(text)
|
||||
masterRaw, err := os.ReadFile(filepath.Join(result.Output2DADir, "masterfeats.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read masterfeats.2da: %v", err)
|
||||
}
|
||||
masterRows := parse2DA(string(masterRaw))
|
||||
|
||||
assertFieldEquals := func(featID, masterID, field string) {
|
||||
got := featRows[featID][field]
|
||||
want := masterRows[masterID][field]
|
||||
if got != want {
|
||||
t.Fatalf("expected %s field %s=%q from masterfeat %s, got %q", featID, field, want, masterID, got)
|
||||
}
|
||||
}
|
||||
|
||||
assertFieldEquals("173", "4", "DESCRIPTION")
|
||||
assertFieldEquals("173", "4", "CRValue")
|
||||
assertFieldEquals("173", "4", "ReqSkillMinRanks")
|
||||
assertFieldEquals("173", "4", "ALLCLASSESCANUSE")
|
||||
assertFieldEquals("173", "4", "PreReqEpic")
|
||||
assertFieldEquals("173", "4", "ReqAction")
|
||||
if got := featRows["173"]["REQSKILL"]; got != "1" {
|
||||
t.Fatalf("expected skill child ref to bind to REQSKILL=1, got %q", got)
|
||||
}
|
||||
if got := featRows["173"]["TOOLSCATEGORIES"]; got != "2" {
|
||||
t.Fatalf("expected skill default TOOLSCATEGORIES=2, got %q", got)
|
||||
}
|
||||
|
||||
if got := featRows["3003"]["TOOLSCATEGORIES"]; got != "9" {
|
||||
t.Fatalf("expected weapon shared default TOOLSCATEGORIES=9, got %q", got)
|
||||
}
|
||||
if got := featRows["3003"]["ALLCLASSESCANUSE"]; got != "0" {
|
||||
t.Fatalf("expected weapon shared default ALLCLASSESCANUSE=0 on existing row, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildFamilyExpansionCanApplySharedAuthoredFieldsAfterModules(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeFeatGeneratedHarness(t, root, map[string]string{
|
||||
"skill_focus.json": `{"family":"skill_focus","family_key":"skillfocus","template":"masterfeats:skillfocus","name_prefix":"Skill Focus","label_prefix":"FEAT_SKILL_FOCUS","constant_prefix":"FEAT_SKILL_FOCUS","default_fields":{"DESCRIPTION":{"tlk":{"key":"feat:skillfocus.shared.description","text":"Family-owned description."}},"CRValue":"0.5","ReqSkillMinRanks":"1","ALLCLASSESCANUSE":"1","TOOLSCATEGORIES":"6","PreReqEpic":"0","ReqAction":"1"},"apply_after_modules":true,"child_ref_field":"REQSKILL","allow_existing_only":true,"child_source":{"dataset":"skills","predicate":"accessible"},"overrides":{"skills:concentration":{"ICON":"ife_family_owned_conc"}}}` + "\n",
|
||||
}, map[string]int{"feat:skillfocus_concentration": 173})
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "modules", "skillfeat"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "modules", "skillfeat", "focus.json"), `{"overrides":[{"key":"feat:skillfocus_concentration","id":173,"LABEL":"FEAT_SKILL_FOCUS_CONCENTRATION","FEAT":{"tlk":{"key":"feat:skillfocus_concentration.name","text":"Skill Focus (Concentration)"}},"DESCRIPTION":{"tlk":{"key":"feat:skillfocus_concentration.description","text":"Wrong module description."}},"ICON":"ife_wrong_module","CRValue":"9.9","MASTERFEAT":{"id":"masterfeats:skillfocus"},"REQSKILL":{"id":"skills:concentration"},"ReqSkillMinRanks":"99","TOOLSCATEGORIES":"2","ALLCLASSESCANUSE":"0","PreReqEpic":"1","ReqAction":"0","Constant":"FEAT_SKILL_FOCUS_CONCENTRATION"}]}`+"\n")
|
||||
|
||||
result, err := BuildNative(testProject(root), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNative: %v", err)
|
||||
}
|
||||
|
||||
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "feat.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read feat.2da: %v", err)
|
||||
}
|
||||
lines := strings.Split(string(got), "\n")
|
||||
header := strings.Split(lines[2], "\t")
|
||||
var row map[string]string
|
||||
for _, line := range lines[3:] {
|
||||
if !strings.HasPrefix(line, "173\t") {
|
||||
continue
|
||||
}
|
||||
parts := strings.Split(line, "\t")
|
||||
row = map[string]string{}
|
||||
for i, col := range header {
|
||||
row[col] = parts[i+1]
|
||||
}
|
||||
break
|
||||
}
|
||||
if row == nil {
|
||||
t.Fatal("expected feat row 173 to exist")
|
||||
}
|
||||
if got := row["ICON"]; got != "ife_family_owned_conc" {
|
||||
t.Fatalf("expected family override ICON after modules, got %q", got)
|
||||
}
|
||||
if got := row["CRValue"]; got != "0.5" {
|
||||
t.Fatalf("expected family shared CRValue after modules, got %q", got)
|
||||
}
|
||||
if got := row["ReqSkillMinRanks"]; got != "1" {
|
||||
t.Fatalf("expected family shared ReqSkillMinRanks after modules, got %q", got)
|
||||
}
|
||||
if got := row["ALLCLASSESCANUSE"]; got != "1" {
|
||||
t.Fatalf("expected family shared ALLCLASSESCANUSE after modules, got %q", got)
|
||||
}
|
||||
if got := row["TOOLSCATEGORIES"]; got != "6" {
|
||||
t.Fatalf("expected family shared TOOLSCATEGORIES after modules, got %q", got)
|
||||
}
|
||||
if got := row["PreReqEpic"]; got != "0" {
|
||||
t.Fatalf("expected family shared PreReqEpic after modules, got %q", got)
|
||||
}
|
||||
if got := row["ReqAction"]; got != "1" {
|
||||
t.Fatalf("expected family shared ReqAction after modules, got %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildGeneratedFeatSpecialAttacksRequiresSpellsTarget(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeFeatGeneratedHarness(t, root, map[string]string{}, map[string]int{"feat:specialattacks": 1187})
|
||||
@@ -2391,7 +2561,7 @@ func TestBuildGeneratedFeatSpecialAttacksRequiresSelfRefTarget(t *testing.T) {
|
||||
func TestBuildGeneratedFeatRequiresMasterfeatAndSkillTargets(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeFeatGeneratedHarness(t, root, map[string]string{
|
||||
"skill_focus.json": `{"family":"skill_focus","family_key":"skillfocus","template":"masterfeats:missing","child_source":{"dataset":"skills","predicate":"accessible"}}` + "\n",
|
||||
"skill_focus.json": `{"family":"skill_focus","family_key":"skillfocus","template":"masterfeats:missing","name_prefix":"Skill Focus","label_prefix":"FEAT_SKILL_FOCUS","constant_prefix":"FEAT_SKILL_FOCUS","template_fields":["DESCRIPTION","ICON"],"default_fields":{"TOOLSCATEGORIES":"2"},"child_ref_field":"REQSKILL","allow_existing_only":true,"child_source":{"dataset":"skills","predicate":"accessible"}}` + "\n",
|
||||
}, map[string]int{"feat:skillfocus_concentration": 173})
|
||||
_, err := BuildNative(testProject(root), nil)
|
||||
if err == nil {
|
||||
@@ -5938,6 +6108,45 @@ func TestBuildUsesReferenceLockIDsForUnmigratedKeyRefs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildBaseDatasetAllowsModuleDeclaredColumns(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data", "custom", "modules"))
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "custom", "base.json"), `{
|
||||
"output": "custom.2da",
|
||||
"columns": ["Label"],
|
||||
"rows": [
|
||||
{"id": 0, "key": "custom:a", "Label": "A"},
|
||||
{"id": 1, "key": "custom:b", "Label": "B"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "custom", "modules", "extend.json"), `{
|
||||
"columns": ["Value"],
|
||||
"overrides": [
|
||||
{"id": 1, "Value": "2"}
|
||||
],
|
||||
"entries": {
|
||||
"custom:c": {
|
||||
"Label": "C",
|
||||
"Value": "3"
|
||||
}
|
||||
}
|
||||
}`+"\n")
|
||||
|
||||
result, err := BuildNative(testProject(root), nil)
|
||||
if err != nil {
|
||||
t.Fatalf("BuildNative failed: %v", err)
|
||||
}
|
||||
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "custom.2da"))
|
||||
if err != nil {
|
||||
t.Fatalf("read custom.2da: %v", err)
|
||||
}
|
||||
want := "2DA V2.0\n\nLabel\tValue\n0\tA\t****\n1\tB\t2\n2\tC\t3\n"
|
||||
if string(got) != want {
|
||||
t.Fatalf("unexpected custom.2da output:\n%s", string(got))
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeProjectImportsLegacyAppearanceAndPortraits(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
mkdirAll(t, filepath.Join(root, "topdata", "data"))
|
||||
|
||||
Reference in New Issue
Block a user