Available Feats Support
This commit is contained in:
@@ -13,8 +13,17 @@ type wikiDataProvidersDocument struct {
|
||||
}
|
||||
|
||||
type wikiDataProviderDefinition struct {
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Levels string `json:"levels" yaml:"levels"`
|
||||
Source string `json:"source" yaml:"source"`
|
||||
Levels string `json:"levels" yaml:"levels"`
|
||||
ClassFeats wikiDataProviderClassFeatConfig `json:"class_feats" yaml:"class_feats"`
|
||||
}
|
||||
|
||||
type wikiDataProviderClassFeatConfig struct {
|
||||
Fields map[string]wikiDataProviderProjectionField `json:"fields" yaml:"fields"`
|
||||
}
|
||||
|
||||
type wikiDataProviderProjectionField struct {
|
||||
When string `json:"when" yaml:"when"`
|
||||
}
|
||||
|
||||
func loadWikiDataProviders(path string) (map[string]wikiDataProviderDefinition, error) {
|
||||
@@ -37,7 +46,19 @@ func loadWikiDataProviders(path string) (map[string]wikiDataProviderDefinition,
|
||||
return nil, fmt.Errorf("wiki data providers %s: provider %s missing source", path, name)
|
||||
}
|
||||
switch strings.TrimSpace(def.Source) {
|
||||
case "class_progression", "class_feats", "class_skills", "class_summary", "race_feats", "race_name_forms":
|
||||
case "class_progression":
|
||||
for field, projection := range def.ClassFeats.Fields {
|
||||
if strings.TrimSpace(field) == "" {
|
||||
return nil, fmt.Errorf("wiki data providers %s: provider %s has class feat projection without field name", path, name)
|
||||
}
|
||||
if strings.TrimSpace(projection.When) == "" {
|
||||
return nil, fmt.Errorf("wiki data providers %s: provider %s class feat projection %s missing when", path, name, field)
|
||||
}
|
||||
}
|
||||
case "class_feats", "class_skills", "class_summary", "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)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("wiki data providers %s: provider %s has unknown source %q", path, name, def.Source)
|
||||
}
|
||||
@@ -58,7 +79,7 @@ func (ctx *wikiContext) wikiDataProviderRows(name string, page wikiTemplatePage)
|
||||
if page.Category != "classes" {
|
||||
return nil, nil
|
||||
}
|
||||
return ctx.classProgressionRows(wikiTableDefinition{Rows: wikiTableRowsConfig{Levels: def.Levels}}, page.Row)
|
||||
return ctx.classProgressionRows(def.Levels, def.ClassFeats.Fields, page.Row)
|
||||
case "class_feats":
|
||||
if page.Category != "classes" {
|
||||
return nil, nil
|
||||
|
||||
@@ -311,6 +311,87 @@ func TestWikiTemplateEachBlockRendersExplicitClassProgressionTable(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiTemplateClassProgressionProviderProjectsConfiguredClassFeats(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
dataPath := filepath.Join(root, "data.yaml")
|
||||
writeFile(t, dataPath, `
|
||||
providers:
|
||||
ClassProgression:
|
||||
source: class_progression
|
||||
levels: 1-4
|
||||
class_feats:
|
||||
fields:
|
||||
GrantedFeats:
|
||||
when: "{{GrantedOnLevel > 0 && List == 3}}"
|
||||
AvailableFeats:
|
||||
when: "{{GrantedOnLevel > 0 && List != 3}}"
|
||||
`+"\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.featRows["feat:weapon_focus"] = map[string]any{"FEAT": map[string]any{"tlk": map[string]any{"text": "Weapon Focus"}}}
|
||||
ctx.classFeatTables["classes/feats:fighter"] = wikiTable{OutputStem: "cls_feat_fight", Rows: []map[string]any{
|
||||
{"FeatIndex": map[string]any{"id": "feat:literate"}, "GrantedOnLevel": "1", "List": "3"},
|
||||
{"FeatIndex": map[string]any{"id": "feat:weapon_specialization"}, "GrantedOnLevel": "4", "List": "1"},
|
||||
{"FeatIndex": map[string]any{"id": "feat:weapon_focus"}, "GrantedOnLevel": "4", "List": "1"},
|
||||
{"FeatIndex": map[string]any{"id": "feat:weapon_proficiency_simple"}, "GrantedOnLevel": "1", "List": "3"},
|
||||
}}
|
||||
page := wikiTemplatePage{
|
||||
PageID: "classes:fighter",
|
||||
Category: "classes",
|
||||
Key: "classes:fighter",
|
||||
Title: "Fighter",
|
||||
Row: map[string]any{
|
||||
"AttackBonusTable": "cls_atk_1",
|
||||
"BonusFeatsTable": map[string]any{"table": "classes/bonusfeats:fighter"},
|
||||
"FeatsTable": map[string]any{"table": "classes/feats:fighter"},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := ctx.renderWikiTemplateString("classes.html", `{{#each ClassProgression}}
|
||||
{{Level}}|{{GrantedFeats|join:", "|wiki}}|{{AvailableFeats|join:", "|wiki}}|{{BonusFeat}}
|
||||
{{/each}}`, page)
|
||||
if err != nil {
|
||||
t.Fatalf("render template: %v", err)
|
||||
}
|
||||
for _, expected := range []string{
|
||||
`1|[[feat/literate|Literate]], [[feat/simple-weapon-proficiency|Simple Weapon Proficiency]]||1`,
|
||||
`4||[[feat/weapon-specialization|Weapon Specialization]], [[feat/weapon-focus|Weapon Focus]]|1`,
|
||||
} {
|
||||
if !strings.Contains(got, expected) {
|
||||
t.Fatalf("expected %q in rendered progression:\n%s", expected, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiDataProvidersRejectEmptyClassFeatProjectionCondition(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
dataPath := filepath.Join(root, "data.yaml")
|
||||
writeFile(t, dataPath, `
|
||||
providers:
|
||||
ClassProgression:
|
||||
source: class_progression
|
||||
class_feats:
|
||||
fields:
|
||||
AvailableFeats:
|
||||
when: ""
|
||||
`+"\n")
|
||||
|
||||
_, err := loadWikiDataProviders(dataPath)
|
||||
if err == nil {
|
||||
t.Fatal("expected invalid class feat projection condition")
|
||||
}
|
||||
for _, expected := range []string{"data.yaml", "ClassProgression", "AvailableFeats"} {
|
||||
if !strings.Contains(err.Error(), expected) {
|
||||
t.Fatalf("expected error to contain %q, got %v", expected, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWikiTemplateEachBlockRendersClassSkillsAsIndividualListItems(t *testing.T) {
|
||||
ctx := &wikiContext{
|
||||
wikiDataProviders: map[string]wikiDataProviderDefinition{
|
||||
|
||||
@@ -279,7 +279,7 @@ func (ctx *wikiContext) wikiTableSourceRows(def wikiTableDefinition, page wikiTe
|
||||
if page.Category != "classes" {
|
||||
return nil, nil
|
||||
}
|
||||
return ctx.classProgressionRows(def, page.Row)
|
||||
return ctx.classProgressionRows(def.Rows.Levels, nil, page.Row)
|
||||
case "class_feats":
|
||||
if page.Category != "classes" {
|
||||
return nil, nil
|
||||
@@ -303,8 +303,8 @@ func (ctx *wikiContext) wikiTableSourceRows(def wikiTableDefinition, page wikiTe
|
||||
}
|
||||
}
|
||||
|
||||
func (ctx *wikiContext) classProgressionRows(def wikiTableDefinition, classRow map[string]any) ([]map[string]any, error) {
|
||||
start, end, err := parseLevelRange(def.Rows.Levels)
|
||||
func (ctx *wikiContext) classProgressionRows(levels string, projections map[string]wikiDataProviderProjectionField, classRow map[string]any) ([]map[string]any, error) {
|
||||
start, end, err := parseLevelRange(levels)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -317,7 +317,15 @@ func (ctx *wikiContext) classProgressionRows(def wikiTableDefinition, classRow m
|
||||
}
|
||||
}
|
||||
}
|
||||
granted := map[int][]string{}
|
||||
if len(projections) == 0 {
|
||||
projections = map[string]wikiDataProviderProjectionField{
|
||||
"GrantedFeats": {When: "{{GrantedOnLevel > 0}}"},
|
||||
}
|
||||
}
|
||||
classFeatFields := map[string]map[int][]string{}
|
||||
for field := range projections {
|
||||
classFeatFields[field] = map[int][]string{}
|
||||
}
|
||||
if table := ctx.tableForValue(fieldValue(classRow, "FeatsTable"), ctx.classFeatTables); table != nil {
|
||||
for _, featRow := range table.Rows {
|
||||
level, err := strconv.Atoi(stringValue(featRow, "GrantedOnLevel"))
|
||||
@@ -325,8 +333,18 @@ func (ctx *wikiContext) classProgressionRows(def wikiTableDefinition, classRow m
|
||||
continue
|
||||
}
|
||||
name := ctx.renderReference(fieldValue(featRow, "FeatIndex"), ctx.featIDToKey, ctx.resolveFeatName)
|
||||
if name != "" {
|
||||
granted[level] = append(granted[level], name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
renderCtx := wikiTableRenderContext{TableName: "class_progression", Path: ctx.wikiDataPath}
|
||||
for field, projection := range projections {
|
||||
value, err := ctx.evalWikiTableTemplate(projection.When, featRow, renderCtx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("class progression field %s condition %q: %w", field, projection.When, err)
|
||||
}
|
||||
if truthyWikiTableValue(value) {
|
||||
classFeatFields[field][level] = append(classFeatFields[field][level], name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -351,7 +369,9 @@ func (ctx *wikiContext) classProgressionRows(def wikiTableDefinition, classRow m
|
||||
row[key] = value
|
||||
}
|
||||
}
|
||||
row["GrantedFeats"] = granted[level]
|
||||
for field, values := range classFeatFields {
|
||||
row[field] = values[level]
|
||||
}
|
||||
row["BonusFeat"] = bonusByLevel[level]
|
||||
row["HPMin"] = level
|
||||
if hitDie > 0 {
|
||||
|
||||
Reference in New Issue
Block a user