diff --git a/internal/topdata/wiki_data_providers.go b/internal/topdata/wiki_data_providers.go index cbf16d9..be92fa4 100644 --- a/internal/topdata/wiki_data_providers.go +++ b/internal/topdata/wiki_data_providers.go @@ -106,7 +106,7 @@ func loadWikiDataProviders(path string) (map[string]wikiDataProviderDefinition, if def.ItemsPerColumn <= 0 { return nil, fmt.Errorf("wiki data providers %s: provider %s items_per_column must be a positive integer", path, name) } - case "class_feats", "class_skills", "class_summary", "race_feats", "race_name_forms": + case "class_feats", "class_skills", "class_summary", "class_spellbook", "race_feats", "race_name_forms": if len(def.ClassFeats.Fields) > 0 { return nil, fmt.Errorf("wiki data providers %s: provider %s class feat projections require source class_progression", path, name) } @@ -163,6 +163,11 @@ func (ctx *wikiContext) wikiDataProviderRows(name string, page wikiTemplatePage) return nil, nil } return ctx.classSummaryRows(page.Row), nil + case "class_spellbook": + if page.Category != "classes" { + return nil, nil + } + return ctx.classSpellbookRows(page.Key, page.Row), nil case "race_feats": if page.Category != "racialtypes" { return nil, nil diff --git a/internal/topdata/wiki_native.go b/internal/topdata/wiki_native.go index e3681cd..fabfd01 100644 --- a/internal/topdata/wiki_native.go +++ b/internal/topdata/wiki_native.go @@ -90,38 +90,41 @@ type wikiTable struct { } type wikiContext struct { - dialog map[string]string - rowsByKey map[string]map[string]any - featRows map[string]map[string]any - featIDToKey map[int]string - skillRows map[string]map[string]any - skillIDToKey map[int]string - spellRows map[string]map[string]any - baseitemRows map[string]map[string]any - classRows map[string]map[string]any - classIDToKey map[int]string - raceRows map[string]map[string]any - raceIDToKey map[int]string - masterfeatRows map[string]map[string]any - classFeatTables map[string]wikiTable - classSkillTables map[string]wikiTable - classBonusTables map[string]wikiTable - classSaveTables map[string]wikiTable - raceFeatTables map[string]wikiTable - statuses map[string]map[string]string - implementedFeats map[string]struct{} - manualSections []wikiManualSection - templateDir string - wikiTablesPath string - wikiTableDefinitions map[string]wikiTableDefinition - wikiDataPath string - wikiDataProviders map[string]wikiDataProviderDefinition - visibilityPolicy *wikiVisibilityDefinitions - visibleWikiKeys map[string]bool - namespaceTitles map[string]string - namespacePaths map[string]string - alignmentLinkPattern string - alignmentLinkFormat string + dialog map[string]string + rowsByKey map[string]map[string]any + featRows map[string]map[string]any + featIDToKey map[int]string + skillRows map[string]map[string]any + skillIDToKey map[int]string + spellRows map[string]map[string]any + baseitemRows map[string]map[string]any + classRows map[string]map[string]any + classIDToKey map[int]string + raceRows map[string]map[string]any + raceIDToKey map[int]string + masterfeatRows map[string]map[string]any + classFeatTables map[string]wikiTable + classSkillTables map[string]wikiTable + classBonusTables map[string]wikiTable + classSaveTables map[string]wikiTable + classSpellGainTables map[string]wikiTable + classSpellKnownTables map[string]wikiTable + classSpellbooks map[string]classSpellbookSpec + raceFeatTables map[string]wikiTable + statuses map[string]map[string]string + implementedFeats map[string]struct{} + manualSections []wikiManualSection + templateDir string + wikiTablesPath string + wikiTableDefinitions map[string]wikiTableDefinition + wikiDataPath string + wikiDataProviders map[string]wikiDataProviderDefinition + visibilityPolicy *wikiVisibilityDefinitions + visibleWikiKeys map[string]bool + namespaceTitles map[string]string + namespacePaths map[string]string + alignmentLinkPattern string + alignmentLinkFormat string } func buildWiki(p *project.Project, nativeResult BuildResult, force bool, progress func(string)) (wikiResult, error) { @@ -632,24 +635,43 @@ func loadWikiContext(dataDir, sourceDir string) (*wikiContext, error) { if err != nil { return nil, err } - classBonusTables, err := loadWikiTables(filepath.Join(dataDir, "classes", "bfeat")) + classBonusTables, err := loadWikiTablesFromDirs(filepath.Join(dataDir, "classes", "bonusfeats"), filepath.Join(dataDir, "classes", "bfeat")) if err != nil { return nil, err } - classSaveTables, err := loadWikiTables(filepath.Join(dataDir, "classes", "savthr")) + classSaveTables, err := loadWikiTablesFromDirs(filepath.Join(dataDir, "classes", "savingthrows"), filepath.Join(dataDir, "classes", "savthr")) if err != nil { return nil, err } + classSpellGainTables, err := loadWikiTables(filepath.Join(dataDir, "classes", "spellsgained")) + if err != nil { + return nil, err + } + classSpellKnownTables, err := loadWikiTables(filepath.Join(dataDir, "classes", "spellsknown")) + if err != nil { + return nil, err + } + classSpellbookList, diagnostics := loadClassSpellbookSpecs(filepath.Join(dataDir, "classes", "spellbooks")) + if len(diagnostics) > 0 { + return nil, fmt.Errorf("load class spellbooks for wiki: %s", diagnosticsTextForError(diagnostics)) + } + classSpellbooks := map[string]classSpellbookSpec{} + for _, spec := range classSpellbookList { + classSpellbooks[spec.Class] = spec + } ctx := &wikiContext{ - dialog: dialog, - rowsByKey: map[string]map[string]any{}, - statuses: map[string]map[string]string{}, - raceFeatTables: raceFeatTables, - classFeatTables: classFeatTables, - classSkillTables: classSkillTables, - classBonusTables: classBonusTables, - classSaveTables: classSaveTables, + dialog: dialog, + rowsByKey: map[string]map[string]any{}, + statuses: map[string]map[string]string{}, + raceFeatTables: raceFeatTables, + classFeatTables: classFeatTables, + classSkillTables: classSkillTables, + classBonusTables: classBonusTables, + classSaveTables: classSaveTables, + classSpellGainTables: classSpellGainTables, + classSpellKnownTables: classSpellKnownTables, + classSpellbooks: classSpellbooks, } if ok { ctx.featRows, ctx.featIDToKey = rowsByKeyAndID(featDataset.Rows) @@ -818,6 +840,20 @@ func loadWikiTables(root string) (map[string]wikiTable, error) { return tables, nil } +func loadWikiTablesFromDirs(roots ...string) (map[string]wikiTable, error) { + merged := map[string]wikiTable{} + for _, root := range roots { + tables, err := loadWikiTables(root) + if err != nil { + return nil, err + } + for key, table := range tables { + merged[key] = table + } + } + return merged, nil +} + func generateEntityPages(outputDir string, ctx *wikiContext, writePage func(pageID, content, title, status string) error) error { if err := generateCategoryPages("classes", ctx.classRows, ctx, writePage); err != nil { return err @@ -1267,6 +1303,13 @@ func (ctx *wikiContext) resolveSkillName(key string) string { return "" } +func (ctx *wikiContext) resolveSpellName(key string) string { + if row := ctx.spellRows[key]; row != nil { + return ctx.resolveRowName("spells", row) + } + return "" +} + func (ctx *wikiContext) resolveClassName(key string) string { if row := ctx.classRows[key]; row != nil { return ctx.resolveRowName("classes", row) diff --git a/internal/topdata/wiki_native_test.go b/internal/topdata/wiki_native_test.go index 5fd356e..c1e4d1a 100644 --- a/internal/topdata/wiki_native_test.go +++ b/internal/topdata/wiki_native_test.go @@ -1478,6 +1478,278 @@ tables: } } +func TestWikiTemplateRendersConfiguredClassProgressionSpellColumns(t *testing.T) { + ctx := testWikiTableContext(t, ` +tables: + ClassProgression: + source: class_progression + class: "wiki-table wiki-class-progression" + rows: + levels: 1-2 + columns: + - key: level + label: Lvl + value: "{{Level}}" + - key: per_day + label: Base spells per day + when: "{{HasSpellsPerDay}}" + children: + - key: per_day_0 + label: 0 + value: "{{SpellsPerDay0}}" + empty: "-" + - key: per_day_1 + label: 1 + value: "{{SpellsPerDay1}}" + empty: "-" + - key: known + label: Known spells + when: "{{HasSpellsKnown}}" + children: + - key: known_0 + label: 0 + value: "{{SpellsKnown0}}" + empty: "-" + - key: known_1 + label: 1 + value: "{{SpellsKnown1}}" + empty: "-" +`) + ctx.classSpellGainTables["classes/spellsgained:bard"] = wikiTable{Rows: []map[string]any{ + {"Level": 1, "SpellLevel0": 2, "SpellLevel1": 1}, + {"Level": 2, "SpellLevel0": 3, "SpellLevel1": nil}, + }} + ctx.classSpellKnownTables["classes/spellsknown:bard"] = wikiTable{Rows: []map[string]any{ + {"Level": 1, "SpellLevel0": 4, "SpellLevel1": 2}, + {"Level": 2, "SpellLevel0": 5, "SpellLevel1": 3}, + }} + page := wikiTemplatePage{ + PageID: "classes:bard", + Category: "classes", + Key: "classes:bard", + Title: "Bard", + Row: map[string]any{ + "SpellCaster": 1, + "SpellGainTable": map[string]any{"table": "classes/spellsgained:bard"}, + "SpellKnownTable": map[string]any{"table": "classes/spellsknown:bard"}, + }, + } + + got, err := ctx.renderWikiTemplateString("classes.html", `{{table:ClassProgression}}`, page) + if err != nil { + t.Fatalf("render table template: %v", err) + } + for _, expected := range []string{ + `Base spells per day`, + `Known spells`, + `12142`, + `23-53`, + } { + if !strings.Contains(got, expected) { + t.Fatalf("expected %q in rendered table:\n%s", expected, got) + } + } +} + +func TestWikiTemplateHidesUnusedConfiguredClassSpellColumns(t *testing.T) { + ctx := testWikiTableContext(t, ` +tables: + ClassProgression: + source: class_progression + class: "wiki-table wiki-class-progression" + rows: + levels: 1-2 + columns: + - key: level + label: Lvl + value: "{{Level}}" + - key: per_day + label: Base spells per day + when: "{{HasSpellsPerDay}}" + children: + - key: per_day_0 + label: 0 + value: "{{SpellsPerDay0}}" + when: "{{SpellsPerDay0}}" + - key: per_day_1 + label: 1 + value: "{{SpellsPerDay1}}" + when: "{{SpellsPerDay1}}" + - key: per_day_2 + label: 2 + value: "{{SpellsPerDay2}}" + when: "{{SpellsPerDay2}}" +`) + ctx.classSpellGainTables["classes/spellsgained:bard"] = wikiTable{Rows: []map[string]any{ + {"Level": 1, "SpellLevel0": 2, "SpellLevel1": 1, "SpellLevel2": nil}, + {"Level": 2, "SpellLevel0": 3, "SpellLevel1": 2, "SpellLevel2": nil}, + }} + page := wikiTemplatePage{ + PageID: "classes:bard", + Category: "classes", + Key: "classes:bard", + Title: "Bard", + Row: map[string]any{ + "SpellCaster": 1, + "SpellGainTable": map[string]any{"table": "classes/spellsgained:bard"}, + }, + } + + got, err := ctx.renderWikiTemplateString("classes.html", `{{table:ClassProgression}}`, page) + if err != nil { + t.Fatalf("render table template: %v", err) + } + if !strings.Contains(got, `Base spells per day`) { + t.Fatalf("expected only two visible spell columns:\n%s", got) + } + if strings.Contains(got, `2`) { + t.Fatalf("expected unused spell level 2 column to be hidden:\n%s", got) + } +} + +func TestWikiTemplateOmitsSpellColumnsForPreparedAndNonCasterClasses(t *testing.T) { + ctx := testWikiTableContext(t, ` +tables: + ClassProgression: + source: class_progression + rows: + levels: 1 + columns: + - key: level + label: Lvl + value: "{{Level}}" + - key: per_day + label: Base spells per day + when: "{{HasSpellsPerDay}}" + children: + - key: per_day_0 + label: 0 + value: "{{SpellsPerDay0}}" + - key: known + label: Known spells + when: "{{HasSpellsKnown}}" + children: + - key: known_0 + label: 0 + value: "{{SpellsKnown0}}" +`) + ctx.classSpellGainTables["classes/spellsgained:cleric"] = wikiTable{Rows: []map[string]any{ + {"Level": 1, "SpellLevel0": 3}, + }} + prepared := wikiTemplatePage{ + PageID: "classes:cleric", + Category: "classes", + Key: "classes:cleric", + Title: "Cleric", + Row: map[string]any{ + "SpellCaster": 1, + "SpellGainTable": map[string]any{"table": "classes/spellsgained:cleric"}, + }, + } + nonCaster := wikiTemplatePage{ + PageID: "classes:fighter", + Category: "classes", + Key: "classes:fighter", + Title: "Fighter", + Row: map[string]any{"SpellCaster": 0}, + } + + gotPrepared, err := ctx.renderWikiTemplateString("classes.html", `{{table:ClassProgression}}`, prepared) + if err != nil { + t.Fatalf("render prepared caster table: %v", err) + } + if !strings.Contains(gotPrepared, `Base spells per day`) || strings.Contains(gotPrepared, `Known spells`) { + t.Fatalf("expected prepared caster to render per-day but not known columns:\n%s", gotPrepared) + } + gotNonCaster, err := ctx.renderWikiTemplateString("classes.html", `{{table:ClassProgression}}`, nonCaster) + if err != nil { + t.Fatalf("render non-caster table: %v", err) + } + if strings.Contains(gotNonCaster, `Base spells per day`) || strings.Contains(gotNonCaster, `Known spells`) { + t.Fatalf("expected non-caster to omit spell columns:\n%s", gotNonCaster) + } +} + +func TestWikiTemplateRendersClassSpellbookProviderBySpellLevel(t *testing.T) { + root := t.TempDir() + dataPath := filepath.Join(root, "data.yaml") + writeFile(t, dataPath, ` +providers: + ClassSpellbook: + source: class_spellbook +`+"\n") + providers, err := loadWikiDataProviders(dataPath) + if err != nil { + t.Fatalf("load data providers: %v", err) + } + ctx := testWikiTableContext(t, "") + ctx.wikiDataPath = dataPath + ctx.wikiDataProviders = providers + ctx.spellRows = map[string]map[string]any{ + "spells:light": {"Name": map[string]any{"tlk": map[string]any{"text": "Light"}}}, + "spells:cure_light_wounds": {"Name": map[string]any{"tlk": map[string]any{"text": "Cure Light Wounds"}}}, + "spells:bless": {"Name": map[string]any{"tlk": map[string]any{"text": "Bless"}}}, + } + ctx.classSpellbooks = map[string]classSpellbookSpec{ + "classes:acolyte": { + Class: "classes:acolyte", + Levels: map[int][]string{ + 0: {"spells:light"}, + 1: {"spells:cure_light_wounds", "spells:bless"}, + }, + }, + } + page := wikiTemplatePage{ + PageID: "classes:acolyte", + Category: "classes", + Key: "classes:acolyte", + Title: "Acolyte", + Row: map[string]any{"SpellCaster": 1}, + } + + got, err := ctx.renderWikiTemplateString("classes.html", `{{#each ClassSpellbook}} +

{{SpellLevelLabel}}

{{Spells|list:"wiki-list wiki-spellbook-list"}} +{{/each}}`, page) + if err != nil { + t.Fatalf("render spellbook provider: %v", err) + } + for _, expected := range []string{ + `

0-level spells

`, + `

1st-level spells