diff --git a/internal/topdata/wiki_native_test.go b/internal/topdata/wiki_native_test.go
index fcb7e7a..c478979 100644
--- a/internal/topdata/wiki_native_test.go
+++ b/internal/topdata/wiki_native_test.go
@@ -1740,6 +1740,57 @@ tables:
}
}
+func TestWikiTemplateRendersClassProgressionSpellcastingAdvancementNotes(t *testing.T) {
+ ctx := testWikiTableContext(t, `
+tables:
+ ClassProgression:
+ source: class_progression
+ rows:
+ levels: 1-4
+ columns:
+ - key: level
+ label: Lvl
+ value: "{{Level}}"
+ - key: notes
+ label: Notes
+ value: "{{SpellcastingAdvancementNote + if(present(SpellcastingAdvancementNote) && present(Notes), \" \", \"\") + Notes}}"
+ empty: ""
+`)
+ page := wikiTemplatePage{
+ PageID: "classes:pale_master",
+ Category: "classes",
+ Key: "classes:pale_master",
+ Title: "Pale Master",
+ Row: map[string]any{
+ "SpellCaster": 0,
+ "ArcSpellLvlMod": 2,
+ "DivSpellLvlMod": 0,
+ "meta": map[string]any{
+ "wiki": map[string]any{
+ "progression_notes": map[string]any{
+ "3": "Manual level note.",
+ },
+ },
+ },
+ },
+ }
+
+ got, err := ctx.renderWikiTemplateString("classes.html", `{{table:ClassProgression}}`, page)
+ if err != nil {
+ t.Fatalf("render table template: %v", err)
+ }
+ for _, expected := range []string{
+ `
| 1 | +1 level of existing arcane spellcasting class |
`,
+ `| 2 | |
`,
+ `| 3 | +1 level of existing arcane spellcasting class Manual level note. |
`,
+ `| 4 | |
`,
+ } {
+ 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:
diff --git a/internal/topdata/wiki_tables.go b/internal/topdata/wiki_tables.go
index 3c78c3b..4a6a184 100644
--- a/internal/topdata/wiki_tables.go
+++ b/internal/topdata/wiki_tables.go
@@ -452,6 +452,7 @@ func (ctx *wikiContext) classProgressionRows(levels string, projections map[stri
row["HasSpellProgression"] = hasSpellProgression(classRow)
row["HasSpellsPerDay"] = hasPerDay
row["HasSpellsKnown"] = hasKnown
+ row["SpellcastingAdvancementNote"] = classSpellcastingAdvancementNote(classRow, level)
populateClassSpellProgressionFields(row, "SpellsPerDay", spellsPerDayByLevel[level])
populateClassSpellProgressionFields(row, "SpellsKnown", spellsKnownByLevel[level])
out = append(out, row)
@@ -459,6 +460,35 @@ func (ctx *wikiContext) classProgressionRows(levels string, projections map[stri
return out, nil
}
+func classSpellcastingAdvancementNote(classRow map[string]any, level int) string {
+ if level < 1 || isClassSpellcaster(classRow) {
+ return ""
+ }
+ notes := []string{}
+ if advancesSpellcastingAtLevel(classRow, "ArcSpellLvlMod", level) {
+ notes = append(notes, "+1 level of existing arcane spellcasting class")
+ }
+ if advancesSpellcastingAtLevel(classRow, "DivSpellLvlMod", level) {
+ notes = append(notes, "+1 level of existing divine spellcasting class")
+ }
+ return strings.Join(notes, "; ")
+}
+
+func advancesSpellcastingAtLevel(classRow map[string]any, field string, level int) bool {
+ mod, err := asInt(fieldValue(classRow, field))
+ if err != nil || mod <= 0 {
+ return false
+ }
+ return roundedSpellcastingLevelAdvancement(level, mod) > roundedSpellcastingLevelAdvancement(level-1, mod)
+}
+
+func roundedSpellcastingLevelAdvancement(level, mod int) int {
+ if level <= 0 || mod <= 0 {
+ return 0
+ }
+ return (level*2 + mod) / (mod * 2)
+}
+
func (ctx *wikiContext) classProgressionNote(classRow map[string]any, level int) string {
meta, err := parseExistingMetadata(classRow)
if err != nil || meta == nil {