feature/class-wiki-spell-progression (#13)
Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/13 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit is contained in:
@@ -106,7 +106,7 @@ func loadWikiDataProviders(path string) (map[string]wikiDataProviderDefinition,
|
|||||||
if def.ItemsPerColumn <= 0 {
|
if def.ItemsPerColumn <= 0 {
|
||||||
return nil, fmt.Errorf("wiki data providers %s: provider %s items_per_column must be a positive integer", path, name)
|
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 {
|
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)
|
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 nil, nil
|
||||||
}
|
}
|
||||||
return ctx.classSummaryRows(page.Row), 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":
|
case "race_feats":
|
||||||
if page.Category != "racialtypes" {
|
if page.Category != "racialtypes" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
|
|||||||
@@ -90,38 +90,41 @@ type wikiTable struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type wikiContext struct {
|
type wikiContext struct {
|
||||||
dialog map[string]string
|
dialog map[string]string
|
||||||
rowsByKey map[string]map[string]any
|
rowsByKey map[string]map[string]any
|
||||||
featRows map[string]map[string]any
|
featRows map[string]map[string]any
|
||||||
featIDToKey map[int]string
|
featIDToKey map[int]string
|
||||||
skillRows map[string]map[string]any
|
skillRows map[string]map[string]any
|
||||||
skillIDToKey map[int]string
|
skillIDToKey map[int]string
|
||||||
spellRows map[string]map[string]any
|
spellRows map[string]map[string]any
|
||||||
baseitemRows map[string]map[string]any
|
baseitemRows map[string]map[string]any
|
||||||
classRows map[string]map[string]any
|
classRows map[string]map[string]any
|
||||||
classIDToKey map[int]string
|
classIDToKey map[int]string
|
||||||
raceRows map[string]map[string]any
|
raceRows map[string]map[string]any
|
||||||
raceIDToKey map[int]string
|
raceIDToKey map[int]string
|
||||||
masterfeatRows map[string]map[string]any
|
masterfeatRows map[string]map[string]any
|
||||||
classFeatTables map[string]wikiTable
|
classFeatTables map[string]wikiTable
|
||||||
classSkillTables map[string]wikiTable
|
classSkillTables map[string]wikiTable
|
||||||
classBonusTables map[string]wikiTable
|
classBonusTables map[string]wikiTable
|
||||||
classSaveTables map[string]wikiTable
|
classSaveTables map[string]wikiTable
|
||||||
raceFeatTables map[string]wikiTable
|
classSpellGainTables map[string]wikiTable
|
||||||
statuses map[string]map[string]string
|
classSpellKnownTables map[string]wikiTable
|
||||||
implementedFeats map[string]struct{}
|
classSpellbooks map[string]classSpellbookSpec
|
||||||
manualSections []wikiManualSection
|
raceFeatTables map[string]wikiTable
|
||||||
templateDir string
|
statuses map[string]map[string]string
|
||||||
wikiTablesPath string
|
implementedFeats map[string]struct{}
|
||||||
wikiTableDefinitions map[string]wikiTableDefinition
|
manualSections []wikiManualSection
|
||||||
wikiDataPath string
|
templateDir string
|
||||||
wikiDataProviders map[string]wikiDataProviderDefinition
|
wikiTablesPath string
|
||||||
visibilityPolicy *wikiVisibilityDefinitions
|
wikiTableDefinitions map[string]wikiTableDefinition
|
||||||
visibleWikiKeys map[string]bool
|
wikiDataPath string
|
||||||
namespaceTitles map[string]string
|
wikiDataProviders map[string]wikiDataProviderDefinition
|
||||||
namespacePaths map[string]string
|
visibilityPolicy *wikiVisibilityDefinitions
|
||||||
alignmentLinkPattern string
|
visibleWikiKeys map[string]bool
|
||||||
alignmentLinkFormat string
|
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) {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
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{
|
ctx := &wikiContext{
|
||||||
dialog: dialog,
|
dialog: dialog,
|
||||||
rowsByKey: map[string]map[string]any{},
|
rowsByKey: map[string]map[string]any{},
|
||||||
statuses: map[string]map[string]string{},
|
statuses: map[string]map[string]string{},
|
||||||
raceFeatTables: raceFeatTables,
|
raceFeatTables: raceFeatTables,
|
||||||
classFeatTables: classFeatTables,
|
classFeatTables: classFeatTables,
|
||||||
classSkillTables: classSkillTables,
|
classSkillTables: classSkillTables,
|
||||||
classBonusTables: classBonusTables,
|
classBonusTables: classBonusTables,
|
||||||
classSaveTables: classSaveTables,
|
classSaveTables: classSaveTables,
|
||||||
|
classSpellGainTables: classSpellGainTables,
|
||||||
|
classSpellKnownTables: classSpellKnownTables,
|
||||||
|
classSpellbooks: classSpellbooks,
|
||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
ctx.featRows, ctx.featIDToKey = rowsByKeyAndID(featDataset.Rows)
|
ctx.featRows, ctx.featIDToKey = rowsByKeyAndID(featDataset.Rows)
|
||||||
@@ -818,6 +840,20 @@ func loadWikiTables(root string) (map[string]wikiTable, error) {
|
|||||||
return tables, nil
|
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 {
|
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 {
|
if err := generateCategoryPages("classes", ctx.classRows, ctx, writePage); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1267,6 +1303,13 @@ func (ctx *wikiContext) resolveSkillName(key string) string {
|
|||||||
return ""
|
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 {
|
func (ctx *wikiContext) resolveClassName(key string) string {
|
||||||
if row := ctx.classRows[key]; row != nil {
|
if row := ctx.classRows[key]; row != nil {
|
||||||
return ctx.resolveRowName("classes", row)
|
return ctx.resolveRowName("classes", row)
|
||||||
|
|||||||
@@ -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{
|
||||||
|
`<th scope="colgroup" colspan="2">Base spells per day</th>`,
|
||||||
|
`<th scope="colgroup" colspan="2">Known spells</th>`,
|
||||||
|
`<tr><td>1</td><td>2</td><td>1</td><td>4</td><td>2</td></tr>`,
|
||||||
|
`<tr><td>2</td><td>3</td><td>-</td><td>5</td><td>3</td></tr>`,
|
||||||
|
} {
|
||||||
|
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, `<th scope="colgroup" colspan="2">Base spells per day</th>`) {
|
||||||
|
t.Fatalf("expected only two visible spell columns:\n%s", got)
|
||||||
|
}
|
||||||
|
if strings.Contains(got, `<th scope="col">2</th>`) {
|
||||||
|
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}}
|
||||||
|
<h4>{{SpellLevelLabel}}</h4>{{Spells|list:"wiki-list wiki-spellbook-list"}}
|
||||||
|
{{/each}}`, page)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("render spellbook provider: %v", err)
|
||||||
|
}
|
||||||
|
for _, expected := range []string{
|
||||||
|
`<h4>0-level spells</h4><ul class="wiki-list wiki-spellbook-list"><li>[[Spells/Light|Light]]</li></ul>`,
|
||||||
|
`<h4>1st-level spells</h4><ul class="wiki-list wiki-spellbook-list"><li>[[Spells/Bless|Bless]]</li>`,
|
||||||
|
`<li>[[Spells/Cure_Light_Wounds|Cure Light Wounds]]</li>`,
|
||||||
|
} {
|
||||||
|
if !strings.Contains(got, expected) {
|
||||||
|
t.Fatalf("expected %q in rendered spellbook:\n%s", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadWikiTablesFromDirsSupportsCanonicalAndLegacyClassTableDirs(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
mkdirAll(t, filepath.Join(root, "bonusfeats"))
|
||||||
|
mkdirAll(t, filepath.Join(root, "bfeat"))
|
||||||
|
writeFile(t, filepath.Join(root, "bonusfeats", "fighter.json"), `{
|
||||||
|
"key": "classes/bonusfeats:fighter",
|
||||||
|
"output": "cls_bfeat_fight.2da",
|
||||||
|
"rows": [{"id": 1, "Bonus": "1"}]
|
||||||
|
}`+"\n")
|
||||||
|
writeFile(t, filepath.Join(root, "bfeat", "commoner.json"), `{
|
||||||
|
"key": "classes/bonusfeats:commoner",
|
||||||
|
"output": "cls_bfeat_comm.2da",
|
||||||
|
"rows": [{"id": 1, "Bonus": "0"}]
|
||||||
|
}`+"\n")
|
||||||
|
|
||||||
|
tables, err := loadWikiTablesFromDirs(filepath.Join(root, "bonusfeats"), filepath.Join(root, "bfeat"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load wiki tables from aliases: %v", err)
|
||||||
|
}
|
||||||
|
for _, key := range []string{"classes/bonusfeats:fighter", "cls_bfeat_fight", "classes/bonusfeats:commoner", "cls_bfeat_comm"} {
|
||||||
|
if _, ok := tables[key]; !ok {
|
||||||
|
t.Fatalf("expected table key %q in merged alias tables: %#v", key, tables)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWikiTableMissingFieldReportsTableAndColumn(t *testing.T) {
|
func TestWikiTableMissingFieldReportsTableAndColumn(t *testing.T) {
|
||||||
ctx := testWikiTableContext(t, `
|
ctx := testWikiTableContext(t, `
|
||||||
tables:
|
tables:
|
||||||
@@ -1967,12 +2239,15 @@ datasets:
|
|||||||
func testWikiTableContext(t *testing.T, yamlSource string) *wikiContext {
|
func testWikiTableContext(t *testing.T, yamlSource string) *wikiContext {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
ctx := &wikiContext{
|
ctx := &wikiContext{
|
||||||
featRows: map[string]map[string]any{},
|
featRows: map[string]map[string]any{},
|
||||||
featIDToKey: map[int]string{},
|
featIDToKey: map[int]string{},
|
||||||
classFeatTables: map[string]wikiTable{},
|
classFeatTables: map[string]wikiTable{},
|
||||||
classBonusTables: map[string]wikiTable{},
|
classBonusTables: map[string]wikiTable{},
|
||||||
classSaveTables: map[string]wikiTable{},
|
classSaveTables: map[string]wikiTable{},
|
||||||
wikiTablesPath: "test-tables.yaml",
|
classSpellGainTables: map[string]wikiTable{},
|
||||||
|
classSpellKnownTables: map[string]wikiTable{},
|
||||||
|
classSpellbooks: map[string]classSpellbookSpec{},
|
||||||
|
wikiTablesPath: "test-tables.yaml",
|
||||||
}
|
}
|
||||||
ctx.featRows["feat:literate"] = map[string]any{"FEAT": map[string]any{"tlk": map[string]any{"text": "Literate"}}}
|
ctx.featRows["feat:literate"] = map[string]any{"FEAT": map[string]any{"tlk": map[string]any{"text": "Literate"}}}
|
||||||
ctx.featRows["feat:weapon_proficiency_simple"] = map[string]any{"FEAT": map[string]any{"tlk": map[string]any{"text": "Simple Weapon Proficiency"}}}
|
ctx.featRows["feat:weapon_proficiency_simple"] = map[string]any{"FEAT": map[string]any{"tlk": map[string]any{"text": "Simple Weapon Proficiency"}}}
|
||||||
|
|||||||
+256
-12
@@ -4,6 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"html"
|
"html"
|
||||||
"os"
|
"os"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode"
|
"unicode"
|
||||||
@@ -16,11 +17,13 @@ type wikiTableDefinitionsDocument struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type wikiTableDefinition struct {
|
type wikiTableDefinition struct {
|
||||||
Source string `json:"source" yaml:"source"`
|
Source string `json:"source" yaml:"source"`
|
||||||
Class string `json:"class" yaml:"class"`
|
Class string `json:"class" yaml:"class"`
|
||||||
When string `json:"when" yaml:"when"`
|
When string `json:"when" yaml:"when"`
|
||||||
Rows wikiTableRowsConfig `json:"rows" yaml:"rows"`
|
Rows wikiTableRowsConfig `json:"rows" yaml:"rows"`
|
||||||
Columns []wikiTableColumn `json:"columns" yaml:"columns"`
|
ClassFeats wikiDataProviderClassFeatConfig `json:"class_feats" yaml:"class_feats"`
|
||||||
|
Attacks wikiDataProviderAttacksConfig `json:"attacks" yaml:"attacks"`
|
||||||
|
Columns []wikiTableColumn `json:"columns" yaml:"columns"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type wikiTableRowsConfig struct {
|
type wikiTableRowsConfig struct {
|
||||||
@@ -34,7 +37,9 @@ type wikiTableColumn struct {
|
|||||||
Value string `json:"value" yaml:"value"`
|
Value string `json:"value" yaml:"value"`
|
||||||
Empty string `json:"empty" yaml:"empty"`
|
Empty string `json:"empty" yaml:"empty"`
|
||||||
Class string `json:"class" yaml:"class"`
|
Class string `json:"class" yaml:"class"`
|
||||||
|
Style string `json:"style" yaml:"style"`
|
||||||
HeaderClass string `json:"header_class" yaml:"header_class"`
|
HeaderClass string `json:"header_class" yaml:"header_class"`
|
||||||
|
HeaderStyle string `json:"header_style" yaml:"header_style"`
|
||||||
AllowWikiMarkup bool `json:"allow_wiki_markup" yaml:"allow_wiki_markup"`
|
AllowWikiMarkup bool `json:"allow_wiki_markup" yaml:"allow_wiki_markup"`
|
||||||
When string `json:"when" yaml:"when"`
|
When string `json:"when" yaml:"when"`
|
||||||
Children []wikiTableColumn `json:"children" yaml:"children"`
|
Children []wikiTableColumn `json:"children" yaml:"children"`
|
||||||
@@ -69,6 +74,27 @@ func parseWikiTableDefinitions(raw []byte, path string) (map[string]wikiTableDef
|
|||||||
if strings.TrimSpace(def.Source) == "" {
|
if strings.TrimSpace(def.Source) == "" {
|
||||||
return nil, fmt.Errorf("wiki table definitions %s: table %s missing source", path, name)
|
return nil, fmt.Errorf("wiki table definitions %s: table %s missing source", path, name)
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(def.Source) != "class_progression" {
|
||||||
|
if len(def.ClassFeats.Fields) > 0 {
|
||||||
|
return nil, fmt.Errorf("wiki table definitions %s: table %s class feat projections require source class_progression", path, name)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(def.Attacks.Mode) != "" {
|
||||||
|
return nil, fmt.Errorf("wiki table definitions %s: table %s attacks config requires source class_progression", path, name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(def.Source) == "class_progression" {
|
||||||
|
for field, projection := range def.ClassFeats.Fields {
|
||||||
|
if strings.TrimSpace(field) == "" {
|
||||||
|
return nil, fmt.Errorf("wiki table definitions %s: table %s has class feat projection without field name", path, name)
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(projection.When) == "" {
|
||||||
|
return nil, fmt.Errorf("wiki table definitions %s: table %s class feat projection %s missing when", path, name, field)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := validateWikiDataProviderAttacks(path, name, def.Attacks); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
if len(def.Columns) == 0 {
|
if len(def.Columns) == 0 {
|
||||||
return nil, fmt.Errorf("wiki table definitions %s: table %s missing columns", path, name)
|
return nil, fmt.Errorf("wiki table definitions %s: table %s missing columns", path, name)
|
||||||
}
|
}
|
||||||
@@ -131,7 +157,7 @@ func (ctx *wikiContext) renderConfiguredWikiTable(templatePath, tableName string
|
|||||||
if len(rows) == 0 {
|
if len(rows) == 0 {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
columns := visibleWikiTableColumns(def.Columns, map[string]any{}, ctx, renderCtx)
|
columns := visibleWikiTableColumns(def.Columns, aggregateWikiTableVisibilityRow(rows), ctx, renderCtx)
|
||||||
if len(columns) == 0 {
|
if len(columns) == 0 {
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
@@ -190,6 +216,21 @@ func visibleWikiTableColumns(columns []wikiTableColumn, row map[string]any, ctx
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func aggregateWikiTableVisibilityRow(rows []map[string]any) map[string]any {
|
||||||
|
out := map[string]any{}
|
||||||
|
for _, row := range rows {
|
||||||
|
for key, value := range row {
|
||||||
|
if _, exists := out[key]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if truthyWikiTableValue(value) {
|
||||||
|
out[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
type wikiTableHeaderCell struct {
|
type wikiTableHeaderCell struct {
|
||||||
HTML string
|
HTML string
|
||||||
}
|
}
|
||||||
@@ -208,20 +249,21 @@ func buildWikiTableHeaders(columns []wikiTableColumn) ([][]string, []wikiTableCo
|
|||||||
for _, col := range columns {
|
for _, col := range columns {
|
||||||
label := html.EscapeString(defaultStringValue(col.Label, col.Key))
|
label := html.EscapeString(defaultStringValue(col.Label, col.Key))
|
||||||
headerClass := attrClass(col.HeaderClass)
|
headerClass := attrClass(col.HeaderClass)
|
||||||
|
headerStyle := attrStyle(col.HeaderStyle)
|
||||||
if len(col.Children) == 0 {
|
if len(col.Children) == 0 {
|
||||||
leaves = append(leaves, col)
|
leaves = append(leaves, col)
|
||||||
rowspan := ""
|
rowspan := ""
|
||||||
if hasGroups {
|
if hasGroups {
|
||||||
rowspan = ` rowspan="2"`
|
rowspan = ` rowspan="2"`
|
||||||
}
|
}
|
||||||
first = append(first, `<th scope="col"`+rowspan+headerClass+`>`+label+`</th>`)
|
first = append(first, `<th scope="col"`+rowspan+headerClass+headerStyle+`>`+label+`</th>`)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
childLeaves := leafWikiTableColumns(col.Children)
|
childLeaves := leafWikiTableColumns(col.Children)
|
||||||
first = append(first, `<th scope="colgroup" colspan="`+strconv.Itoa(len(childLeaves))+`"`+headerClass+`>`+label+`</th>`)
|
first = append(first, `<th scope="colgroup" colspan="`+strconv.Itoa(len(childLeaves))+`"`+headerClass+headerStyle+`>`+label+`</th>`)
|
||||||
for _, child := range col.Children {
|
for _, child := range col.Children {
|
||||||
childLabel := html.EscapeString(defaultStringValue(child.Label, child.Key))
|
childLabel := html.EscapeString(defaultStringValue(child.Label, child.Key))
|
||||||
second = append(second, `<th scope="col"`+attrClass(child.HeaderClass)+`>`+childLabel+`</th>`)
|
second = append(second, `<th scope="col"`+attrClass(child.HeaderClass)+attrStyle(child.HeaderStyle)+`>`+childLabel+`</th>`)
|
||||||
}
|
}
|
||||||
leaves = append(leaves, childLeaves...)
|
leaves = append(leaves, childLeaves...)
|
||||||
}
|
}
|
||||||
@@ -255,7 +297,7 @@ func (ctx *wikiContext) renderWikiTableCell(col wikiTableColumn, row map[string]
|
|||||||
if !col.AllowWikiMarkup {
|
if !col.AllowWikiMarkup {
|
||||||
text = html.EscapeString(text)
|
text = html.EscapeString(text)
|
||||||
}
|
}
|
||||||
return `<td` + attrClass(col.Class) + `>` + text + `</td>`, nil
|
return `<td` + attrClass(col.Class) + attrStyle(col.Style) + `>` + text + `</td>`, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func attrClass(class string) string {
|
func attrClass(class string) string {
|
||||||
@@ -266,6 +308,14 @@ func attrClass(class string) string {
|
|||||||
return ` class="` + html.EscapeString(class) + `"`
|
return ` class="` + html.EscapeString(class) + `"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func attrStyle(style string) string {
|
||||||
|
style = strings.TrimSpace(style)
|
||||||
|
if style == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return ` style="` + html.EscapeString(style) + `"`
|
||||||
|
}
|
||||||
|
|
||||||
func defaultStringValue(value, fallback string) string {
|
func defaultStringValue(value, fallback string) string {
|
||||||
if strings.TrimSpace(value) != "" {
|
if strings.TrimSpace(value) != "" {
|
||||||
return value
|
return value
|
||||||
@@ -279,7 +329,7 @@ func (ctx *wikiContext) wikiTableSourceRows(def wikiTableDefinition, page wikiTe
|
|||||||
if page.Category != "classes" {
|
if page.Category != "classes" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return ctx.classProgressionRows(def.Rows.Levels, nil, wikiDataProviderAttacksConfig{}, page.Key, page.Row)
|
return ctx.classProgressionRows(def.Rows.Levels, def.ClassFeats.Fields, def.Attacks, page.Key, page.Row)
|
||||||
case "class_feats":
|
case "class_feats":
|
||||||
if page.Category != "classes" {
|
if page.Category != "classes" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -359,6 +409,10 @@ func (ctx *wikiContext) classProgressionRows(levels string, projections map[stri
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
hitDie, _ := asInt(fieldValue(classRow, "HitDie"))
|
hitDie, _ := asInt(fieldValue(classRow, "HitDie"))
|
||||||
|
spellsPerDayByLevel := classSpellProgressionByLevel(ctx.tableForValue(fieldValue(classRow, "SpellGainTable"), ctx.classSpellGainTables))
|
||||||
|
spellsKnownByLevel := classSpellProgressionByLevel(ctx.tableForValue(fieldValue(classRow, "SpellKnownTable"), ctx.classSpellKnownTables))
|
||||||
|
hasPerDay := hasClassSpellProgressionTable(classRow, ctx.classSpellGainTables, "SpellGainTable")
|
||||||
|
hasKnown := hasClassSpellProgressionTable(classRow, ctx.classSpellKnownTables, "SpellKnownTable")
|
||||||
out := []map[string]any{}
|
out := []map[string]any{}
|
||||||
for level := start; level <= end; level++ {
|
for level := start; level <= end; level++ {
|
||||||
row := cloneRowMap(classRow)
|
row := cloneRowMap(classRow)
|
||||||
@@ -389,11 +443,60 @@ func (ctx *wikiContext) classProgressionRows(levels string, projections map[stri
|
|||||||
}
|
}
|
||||||
row["Notes"] = ""
|
row["Notes"] = ""
|
||||||
row["HasSpellProgression"] = hasSpellProgression(classRow)
|
row["HasSpellProgression"] = hasSpellProgression(classRow)
|
||||||
|
row["HasSpellsPerDay"] = hasPerDay
|
||||||
|
row["HasSpellsKnown"] = hasKnown
|
||||||
|
populateClassSpellProgressionFields(row, "SpellsPerDay", spellsPerDayByLevel[level])
|
||||||
|
populateClassSpellProgressionFields(row, "SpellsKnown", spellsKnownByLevel[level])
|
||||||
out = append(out, row)
|
out = append(out, row)
|
||||||
}
|
}
|
||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func classSpellProgressionByLevel(table *wikiTable) map[int]map[int]string {
|
||||||
|
out := map[int]map[int]string{}
|
||||||
|
if table == nil {
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
for _, tableRow := range table.Rows {
|
||||||
|
level, err := asInt(fieldValue(tableRow, "Level"))
|
||||||
|
if err != nil || level < 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values := map[int]string{}
|
||||||
|
for key, value := range tableRow {
|
||||||
|
if !strings.HasPrefix(key, "SpellLevel") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
spellLevel, err := strconv.Atoi(strings.TrimPrefix(key, "SpellLevel"))
|
||||||
|
if err != nil || spellLevel < 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
values[spellLevel] = stringifyWikiTableValue(value)
|
||||||
|
}
|
||||||
|
out[level] = values
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateClassSpellProgressionFields(row map[string]any, prefix string, values map[int]string) {
|
||||||
|
ordered := []string{}
|
||||||
|
for _, spellLevel := range sortedIntMapKeys(values) {
|
||||||
|
value := values[spellLevel]
|
||||||
|
row[prefix+strconv.Itoa(spellLevel)] = value
|
||||||
|
ordered = append(ordered, value)
|
||||||
|
}
|
||||||
|
row[prefix+"ByLevel"] = ordered
|
||||||
|
}
|
||||||
|
|
||||||
|
func sortedIntMapKeys(values map[int]string) []int {
|
||||||
|
keys := make([]int, 0, len(values))
|
||||||
|
for key := range values {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
slices.Sort(keys)
|
||||||
|
return keys
|
||||||
|
}
|
||||||
|
|
||||||
func parseLevelRange(raw string) (int, int, error) {
|
func parseLevelRange(raw string) (int, int, error) {
|
||||||
raw = strings.TrimSpace(raw)
|
raw = strings.TrimSpace(raw)
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
@@ -552,8 +655,124 @@ func (ctx *wikiContext) classSummaryRows(classRow map[string]any) []map[string]a
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isClassSpellcaster(row map[string]any) bool {
|
||||||
|
return stringValue(row, "SpellCaster") == "1"
|
||||||
|
}
|
||||||
|
|
||||||
func hasSpellProgression(row map[string]any) bool {
|
func hasSpellProgression(row map[string]any) bool {
|
||||||
return stringValue(row, "SpellGainTable") != "" || stringValue(row, "SpellKnownTable") != ""
|
return isClassSpellcaster(row) && (hasTableReference(fieldValue(row, "SpellGainTable")) || hasTableReference(fieldValue(row, "SpellKnownTable")))
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasClassSpellProgressionTable(row map[string]any, tables map[string]wikiTable, field string) bool {
|
||||||
|
return isClassSpellcaster(row) && ctxHasTableValue(row, tables, field)
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasTableReference(value any) bool {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case string:
|
||||||
|
return strings.TrimSpace(typed) != "" && strings.TrimSpace(typed) != nullValue
|
||||||
|
case map[string]any:
|
||||||
|
tableKey, _ := typed["table"].(string)
|
||||||
|
return strings.TrimSpace(tableKey) != ""
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ctxHasTableValue(row map[string]any, tables map[string]wikiTable, field string) bool {
|
||||||
|
switch value := fieldValue(row, field).(type) {
|
||||||
|
case string:
|
||||||
|
if _, ok := tables[value]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, ok := tables[outputStem(value)]
|
||||||
|
return ok
|
||||||
|
case map[string]any:
|
||||||
|
tableKey, _ := value["table"].(string)
|
||||||
|
if tableKey == "" {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if _, ok := tables[tableKey]; ok {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
_, ok := tables[outputStem(tableKey)]
|
||||||
|
return ok
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) classSpellbookRows(classKey string, classRow map[string]any) []map[string]any {
|
||||||
|
spec, ok := ctx.classSpellbooks[classKey]
|
||||||
|
if !ok || !ctx.hasClassSpellbook(classKey, classRow) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := []map[string]any{}
|
||||||
|
for _, level := range sortedIntKeys(spec.Levels) {
|
||||||
|
spells := ctx.renderClassSpellbookLevel(spec.Levels[level])
|
||||||
|
if len(spells) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, map[string]any{
|
||||||
|
"SpellLevel": level,
|
||||||
|
"SpellLevelLabel": spellbookLevelLabel(level),
|
||||||
|
"Spells": spells,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) hasClassSpellbook(classKey string, classRow map[string]any) bool {
|
||||||
|
if ctx == nil || !isClassSpellcaster(classRow) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
spec, ok := ctx.classSpellbooks[classKey]
|
||||||
|
if !ok {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, spells := range spec.Levels {
|
||||||
|
if len(spells) > 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *wikiContext) renderClassSpellbookLevel(spellKeys []string) []string {
|
||||||
|
type renderedSpell struct {
|
||||||
|
Name string
|
||||||
|
Link string
|
||||||
|
}
|
||||||
|
rendered := []renderedSpell{}
|
||||||
|
for _, spellKey := range spellKeys {
|
||||||
|
name := ctx.resolveSpellName(spellKey)
|
||||||
|
if name == "" {
|
||||||
|
name = spellKey
|
||||||
|
}
|
||||||
|
link := ctx.renderReference(map[string]any{"id": spellKey}, nil, ctx.resolveSpellName)
|
||||||
|
if link == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
rendered = append(rendered, renderedSpell{Name: name, Link: link})
|
||||||
|
}
|
||||||
|
slices.SortFunc(rendered, func(a, b renderedSpell) int {
|
||||||
|
if cmp := strings.Compare(strings.ToLower(a.Name), strings.ToLower(b.Name)); cmp != 0 {
|
||||||
|
return cmp
|
||||||
|
}
|
||||||
|
return strings.Compare(a.Link, b.Link)
|
||||||
|
})
|
||||||
|
out := make([]string, 0, len(rendered))
|
||||||
|
for _, spell := range rendered {
|
||||||
|
out = append(out, spell.Link)
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func spellbookLevelLabel(level int) string {
|
||||||
|
if level == 0 {
|
||||||
|
return "0-level spells"
|
||||||
|
}
|
||||||
|
return ordinalWikiTableValue(level) + "-level spells"
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *wikiContext) evalWikiTableTemplate(template string, row map[string]any, renderCtx wikiTableRenderContext) (any, error) {
|
func (ctx *wikiContext) evalWikiTableTemplate(template string, row map[string]any, renderCtx wikiTableRenderContext) (any, error) {
|
||||||
@@ -907,6 +1126,15 @@ func (p *wikiExprParser) resolveField(name string) (any, error) {
|
|||||||
if name == "HasSpellProgression" {
|
if name == "HasSpellProgression" {
|
||||||
return hasSpellProgression(p.renderCtx.Page.Row), nil
|
return hasSpellProgression(p.renderCtx.Page.Row), nil
|
||||||
}
|
}
|
||||||
|
if name == "HasSpellsPerDay" {
|
||||||
|
return hasClassSpellProgressionTable(p.renderCtx.Page.Row, p.ctx.classSpellGainTables, "SpellGainTable"), nil
|
||||||
|
}
|
||||||
|
if name == "HasSpellsKnown" {
|
||||||
|
return hasClassSpellProgressionTable(p.renderCtx.Page.Row, p.ctx.classSpellKnownTables, "SpellKnownTable"), nil
|
||||||
|
}
|
||||||
|
if name == "HasClassSpellbook" {
|
||||||
|
return p.ctx.hasClassSpellbook(p.renderCtx.Page.Key, p.renderCtx.Page.Row), nil
|
||||||
|
}
|
||||||
switch name {
|
switch name {
|
||||||
case "true":
|
case "true":
|
||||||
return true, nil
|
return true, nil
|
||||||
@@ -919,12 +1147,28 @@ func (p *wikiExprParser) resolveField(name string) (any, error) {
|
|||||||
if value, ok := lookupField(p.renderCtx.Page.Row, name); ok {
|
if value, ok := lookupField(p.renderCtx.Page.Row, name); ok {
|
||||||
return value, nil
|
return value, nil
|
||||||
}
|
}
|
||||||
|
if isClassSpellProgressionFieldName(name) {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
if p.allowMissingFields {
|
if p.allowMissingFields {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("missing field %q", name)
|
return nil, fmt.Errorf("missing field %q", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isClassSpellProgressionFieldName(name string) bool {
|
||||||
|
for _, prefix := range []string{"SpellsPerDay", "SpellsKnown"} {
|
||||||
|
suffix := strings.TrimPrefix(name, prefix)
|
||||||
|
if suffix == name || suffix == "ByLevel" || suffix == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, err := strconv.Atoi(suffix); err == nil {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
func (p *wikiExprParser) callHelper(name string, args []any) (any, error) {
|
func (p *wikiExprParser) callHelper(name string, args []any) (any, error) {
|
||||||
switch name {
|
switch name {
|
||||||
case "ordinal":
|
case "ordinal":
|
||||||
|
|||||||
Reference in New Issue
Block a user