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:
2026-05-26 20:35:54 +02:00
committed by archvillainette
parent 88be9e95d3
commit 59c8e407e9
4 changed files with 628 additions and 61 deletions
+281 -6
View File
@@ -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) {
ctx := testWikiTableContext(t, `
tables:
@@ -1967,12 +2239,15 @@ datasets:
func testWikiTableContext(t *testing.T, yamlSource string) *wikiContext {
t.Helper()
ctx := &wikiContext{
featRows: map[string]map[string]any{},
featIDToKey: map[int]string{},
classFeatTables: map[string]wikiTable{},
classBonusTables: map[string]wikiTable{},
classSaveTables: map[string]wikiTable{},
wikiTablesPath: "test-tables.yaml",
featRows: map[string]map[string]any{},
featIDToKey: map[int]string{},
classFeatTables: map[string]wikiTable{},
classBonusTables: map[string]wikiTable{},
classSaveTables: map[string]wikiTable{},
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:weapon_proficiency_simple"] = map[string]any{"FEAT": map[string]any{"tlk": map[string]any{"text": "Simple Weapon Proficiency"}}}