BAB tables
This commit is contained in:
@@ -3,6 +3,7 @@ package topdata
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
@@ -16,6 +17,7 @@ type wikiDataProviderDefinition struct {
|
|||||||
Source string `json:"source" yaml:"source"`
|
Source string `json:"source" yaml:"source"`
|
||||||
Levels string `json:"levels" yaml:"levels"`
|
Levels string `json:"levels" yaml:"levels"`
|
||||||
ClassFeats wikiDataProviderClassFeatConfig `json:"class_feats" yaml:"class_feats"`
|
ClassFeats wikiDataProviderClassFeatConfig `json:"class_feats" yaml:"class_feats"`
|
||||||
|
Attacks wikiDataProviderAttacksConfig `json:"attacks" yaml:"attacks"`
|
||||||
Provider string `json:"provider" yaml:"provider"`
|
Provider string `json:"provider" yaml:"provider"`
|
||||||
Columns int `json:"columns" yaml:"columns"`
|
Columns int `json:"columns" yaml:"columns"`
|
||||||
ItemsPerColumn int `json:"items_per_column" yaml:"items_per_column"`
|
ItemsPerColumn int `json:"items_per_column" yaml:"items_per_column"`
|
||||||
@@ -30,6 +32,28 @@ type wikiDataProviderProjectionField struct {
|
|||||||
When string `json:"when" yaml:"when"`
|
When string `json:"when" yaml:"when"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type wikiDataProviderAttacksConfig struct {
|
||||||
|
Field string `json:"field" yaml:"field"`
|
||||||
|
CountField string `json:"count_field" yaml:"count_field"`
|
||||||
|
Mode string `json:"mode" yaml:"mode"`
|
||||||
|
Step int `json:"step" yaml:"step"`
|
||||||
|
Attacks int `json:"attacks" yaml:"attacks"`
|
||||||
|
Thresholds []wikiDataProviderAttackThreshold `json:"thresholds" yaml:"thresholds"`
|
||||||
|
Overrides []wikiDataProviderAttackOverrideRule `json:"overrides" yaml:"overrides"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type wikiDataProviderAttackThreshold struct {
|
||||||
|
MinBAB int `json:"min_bab" yaml:"min_bab"`
|
||||||
|
Attacks int `json:"attacks" yaml:"attacks"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type wikiDataProviderAttackOverrideRule struct {
|
||||||
|
Classes []string `json:"classes" yaml:"classes"`
|
||||||
|
Levels any `json:"levels" yaml:"levels"`
|
||||||
|
Attacks int `json:"attacks" yaml:"attacks"`
|
||||||
|
Add int `json:"add" yaml:"add"`
|
||||||
|
}
|
||||||
|
|
||||||
func loadWikiDataProviders(path string) (map[string]wikiDataProviderDefinition, error) {
|
func loadWikiDataProviders(path string) (map[string]wikiDataProviderDefinition, error) {
|
||||||
raw, err := os.ReadFile(path)
|
raw, err := os.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -60,10 +84,16 @@ func loadWikiDataProviders(path string) (map[string]wikiDataProviderDefinition,
|
|||||||
return nil, fmt.Errorf("wiki data providers %s: provider %s class feat projection %s missing when", path, name, field)
|
return nil, fmt.Errorf("wiki data providers %s: provider %s class feat projection %s missing when", path, name, field)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err := validateWikiDataProviderAttacks(path, name, def.Attacks); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
case "columns":
|
case "columns":
|
||||||
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)
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(def.Attacks.Mode) != "" {
|
||||||
|
return nil, fmt.Errorf("wiki data providers %s: provider %s attacks config requires source class_progression", path, name)
|
||||||
|
}
|
||||||
if strings.TrimSpace(def.Provider) == "" {
|
if strings.TrimSpace(def.Provider) == "" {
|
||||||
return nil, fmt.Errorf("wiki data providers %s: provider %s columns provider missing provider", path, name)
|
return nil, fmt.Errorf("wiki data providers %s: provider %s columns provider missing provider", path, name)
|
||||||
}
|
}
|
||||||
@@ -80,6 +110,9 @@ func loadWikiDataProviders(path string) (map[string]wikiDataProviderDefinition,
|
|||||||
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)
|
||||||
}
|
}
|
||||||
|
if strings.TrimSpace(def.Attacks.Mode) != "" {
|
||||||
|
return nil, fmt.Errorf("wiki data providers %s: provider %s attacks config requires source class_progression", path, name)
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("wiki data providers %s: provider %s has unknown source %q", path, name, def.Source)
|
return nil, fmt.Errorf("wiki data providers %s: provider %s has unknown source %q", path, name, def.Source)
|
||||||
}
|
}
|
||||||
@@ -114,7 +147,7 @@ func (ctx *wikiContext) wikiDataProviderRows(name string, page wikiTemplatePage)
|
|||||||
if page.Category != "classes" {
|
if page.Category != "classes" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
return ctx.classProgressionRows(def.Levels, def.ClassFeats.Fields, page.Row)
|
return ctx.classProgressionRows(def.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
|
||||||
@@ -145,6 +178,123 @@ func (ctx *wikiContext) wikiDataProviderRows(name string, page wikiTemplatePage)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateWikiDataProviderAttacks(path, provider string, cfg wikiDataProviderAttacksConfig) error {
|
||||||
|
if strings.TrimSpace(cfg.Mode) == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch strings.TrimSpace(cfg.Mode) {
|
||||||
|
case "fixed":
|
||||||
|
if cfg.Attacks < 1 {
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks fixed mode requires positive attacks", path, provider)
|
||||||
|
}
|
||||||
|
case "bab_thresholds":
|
||||||
|
if len(cfg.Thresholds) == 0 {
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks bab_thresholds mode requires thresholds", path, provider)
|
||||||
|
}
|
||||||
|
for i, threshold := range cfg.Thresholds {
|
||||||
|
if threshold.MinBAB < 0 {
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks threshold %d min_bab must be non-negative", path, provider, i+1)
|
||||||
|
}
|
||||||
|
if threshold.Attacks < 1 {
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks threshold %d attacks must be positive", path, provider, i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks has unknown mode %q", path, provider, cfg.Mode)
|
||||||
|
}
|
||||||
|
for i, override := range cfg.Overrides {
|
||||||
|
if len(override.Classes) == 0 {
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks override %d missing classes", path, provider, i+1)
|
||||||
|
}
|
||||||
|
for _, class := range override.Classes {
|
||||||
|
if strings.TrimSpace(class) == "" {
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks override %d has empty class", path, provider, i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err := parseWikiAttackLevels(override.Levels); err != nil {
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks override %d bad levels: %w", path, provider, i+1, err)
|
||||||
|
}
|
||||||
|
if override.Attacks != 0 && override.Add != 0 {
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks override %d must not set both attacks and add", path, provider, i+1)
|
||||||
|
}
|
||||||
|
if override.Attacks == 0 && override.Add == 0 {
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks override %d must set attacks or add", path, provider, i+1)
|
||||||
|
}
|
||||||
|
if override.Attacks < 0 {
|
||||||
|
return fmt.Errorf("wiki data providers %s: provider %s attacks override %d attacks must be positive", path, provider, i+1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseWikiAttackLevels(raw any) (map[int]struct{}, error) {
|
||||||
|
out := map[int]struct{}{}
|
||||||
|
addLevel := func(value any) error {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case int:
|
||||||
|
if typed < 1 {
|
||||||
|
return fmt.Errorf("level must be positive")
|
||||||
|
}
|
||||||
|
out[typed] = struct{}{}
|
||||||
|
return nil
|
||||||
|
case int64:
|
||||||
|
if typed < 1 {
|
||||||
|
return fmt.Errorf("level must be positive")
|
||||||
|
}
|
||||||
|
out[int(typed)] = struct{}{}
|
||||||
|
return nil
|
||||||
|
case float64:
|
||||||
|
level := int(typed)
|
||||||
|
if typed != float64(level) || level < 1 {
|
||||||
|
return fmt.Errorf("level must be a positive integer")
|
||||||
|
}
|
||||||
|
out[level] = struct{}{}
|
||||||
|
return nil
|
||||||
|
case string:
|
||||||
|
text := strings.TrimSpace(typed)
|
||||||
|
if text == "" {
|
||||||
|
return fmt.Errorf("level must not be empty")
|
||||||
|
}
|
||||||
|
if strings.Contains(text, "-") {
|
||||||
|
start, end, err := parseLevelRange(text)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for level := start; level <= end; level++ {
|
||||||
|
out[level] = struct{}{}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
level, err := strconv.Atoi(text)
|
||||||
|
if err != nil || level < 1 {
|
||||||
|
return fmt.Errorf("level must be a positive integer")
|
||||||
|
}
|
||||||
|
out[level] = struct{}{}
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("unsupported level value")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch typed := raw.(type) {
|
||||||
|
case nil:
|
||||||
|
return nil, fmt.Errorf("levels is required")
|
||||||
|
case []any:
|
||||||
|
if len(typed) == 0 {
|
||||||
|
return nil, fmt.Errorf("levels must not be empty")
|
||||||
|
}
|
||||||
|
for _, value := range typed {
|
||||||
|
if err := addLevel(value); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
if err := addLevel(raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
func wikiColumnProviderRows(sourceRows []map[string]any, configuredColumns, itemsPerColumn int, forceColumns bool) []map[string]any {
|
func wikiColumnProviderRows(sourceRows []map[string]any, configuredColumns, itemsPerColumn int, forceColumns bool) []map[string]any {
|
||||||
if configuredColumns <= 0 || itemsPerColumn <= 0 {
|
if configuredColumns <= 0 || itemsPerColumn <= 0 {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -492,6 +492,122 @@ providers:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWikiTemplateClassProgressionProviderUsesDefaultNWNAttackSequences(t *testing.T) {
|
||||||
|
ctx := &wikiContext{
|
||||||
|
wikiDataPath: "test-data.yaml",
|
||||||
|
wikiDataProviders: map[string]wikiDataProviderDefinition{
|
||||||
|
"ClassProgression": {Source: "class_progression", Levels: "1-16"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
page := wikiTemplatePage{
|
||||||
|
PageID: "classes:fighter",
|
||||||
|
Category: "classes",
|
||||||
|
Key: "classes:fighter",
|
||||||
|
Title: "Fighter",
|
||||||
|
Row: map[string]any{
|
||||||
|
"AttackBonusTable": "cls_atk_1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ctx.renderWikiTemplateString("classes.html", `{{#each ClassProgression}}{{Level}}:{{BABValue}}:{{AttackCount}}:{{AttackSequence}}
|
||||||
|
{{/each}}`, page)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("render template: %v", err)
|
||||||
|
}
|
||||||
|
for _, expected := range []string{
|
||||||
|
"1:1:1:+1",
|
||||||
|
"6:6:2:+6/+1",
|
||||||
|
"11:11:3:+11/+6/+1",
|
||||||
|
"16:16:4:+16/+11/+6/+1",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(got, expected) {
|
||||||
|
t.Fatalf("expected %q in rendered progression:\n%s", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiTemplateClassProgressionProviderSupportsFixedAttacksAndOverrides(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
dataPath := filepath.Join(root, "data.yaml")
|
||||||
|
writeFile(t, dataPath, `
|
||||||
|
providers:
|
||||||
|
ClassProgression:
|
||||||
|
source: class_progression
|
||||||
|
levels: 1-12
|
||||||
|
attacks:
|
||||||
|
mode: fixed
|
||||||
|
attacks: 2
|
||||||
|
step: -5
|
||||||
|
overrides:
|
||||||
|
- classes: ["classes:fighter"]
|
||||||
|
levels: 11
|
||||||
|
attacks: 3
|
||||||
|
`+"\n")
|
||||||
|
providers, err := loadWikiDataProviders(dataPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("load data providers: %v", err)
|
||||||
|
}
|
||||||
|
ctx := &wikiContext{
|
||||||
|
wikiDataPath: dataPath,
|
||||||
|
wikiDataProviders: providers,
|
||||||
|
}
|
||||||
|
page := wikiTemplatePage{
|
||||||
|
PageID: "classes:fighter",
|
||||||
|
Category: "classes",
|
||||||
|
Key: "classes:fighter",
|
||||||
|
Title: "Fighter",
|
||||||
|
Row: map[string]any{
|
||||||
|
"AttackBonusTable": "cls_atk_1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ctx.renderWikiTemplateString("classes.html", `{{#each ClassProgression}}{{Level}}:{{AttackCount}}:{{AttackSequence}}
|
||||||
|
{{/each}}`, page)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("render template: %v", err)
|
||||||
|
}
|
||||||
|
for _, expected := range []string{
|
||||||
|
"1:2:+1/-4",
|
||||||
|
"6:2:+6/+1",
|
||||||
|
"11:3:+11/+6/+1",
|
||||||
|
"12:2:+12/+7",
|
||||||
|
} {
|
||||||
|
if !strings.Contains(got, expected) {
|
||||||
|
t.Fatalf("expected %q in rendered progression:\n%s", expected, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWikiTemplateCountHelperSupportsListGrammar(t *testing.T) {
|
||||||
|
ctx := &wikiContext{}
|
||||||
|
page := wikiTemplatePage{
|
||||||
|
PageID: "classes:test",
|
||||||
|
Category: "classes",
|
||||||
|
Key: "classes:test",
|
||||||
|
Title: "Test Class",
|
||||||
|
Row: map[string]any{
|
||||||
|
"AvailableFeats": []any{"One", "Two"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := ctx.renderWikiTemplateString("classes.html", `{{AvailableFeats|join:", "}} {{if(count(AvailableFeats) > 1, "become", "becomes")}} available.`, page)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("render plural template: %v", err)
|
||||||
|
}
|
||||||
|
if got != "One, Two become available." {
|
||||||
|
t.Fatalf("unexpected plural grammar: %q", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
page.Row["AvailableFeats"] = []any{"One"}
|
||||||
|
got, err = ctx.renderWikiTemplateString("classes.html", `{{AvailableFeats|join:", "}} {{if(count(AvailableFeats) > 1, "become", "becomes")}} available.`, page)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("render singular template: %v", err)
|
||||||
|
}
|
||||||
|
if got != "One becomes available." {
|
||||||
|
t.Fatalf("unexpected singular grammar: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWikiDataProvidersRejectEmptyClassFeatProjectionCondition(t *testing.T) {
|
func TestWikiDataProvidersRejectEmptyClassFeatProjectionCondition(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
dataPath := filepath.Join(root, "data.yaml")
|
dataPath := filepath.Join(root, "data.yaml")
|
||||||
@@ -516,6 +632,34 @@ providers:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestWikiDataProvidersRejectInvalidAttackOverrideConfig(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
dataPath := filepath.Join(root, "data.yaml")
|
||||||
|
writeFile(t, dataPath, `
|
||||||
|
providers:
|
||||||
|
ClassProgression:
|
||||||
|
source: class_progression
|
||||||
|
attacks:
|
||||||
|
mode: fixed
|
||||||
|
attacks: 2
|
||||||
|
overrides:
|
||||||
|
- classes: ["classes:fighter"]
|
||||||
|
levels: 11
|
||||||
|
attacks: 3
|
||||||
|
add: 1
|
||||||
|
`+"\n")
|
||||||
|
|
||||||
|
_, err := loadWikiDataProviders(dataPath)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("expected invalid attack override config")
|
||||||
|
}
|
||||||
|
for _, expected := range []string{"data.yaml", "ClassProgression", "attacks", "override"} {
|
||||||
|
if !strings.Contains(err.Error(), expected) {
|
||||||
|
t.Fatalf("expected error to contain %q, got %v", expected, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestWikiDataProvidersRejectInvalidColumnProviderConfig(t *testing.T) {
|
func TestWikiDataProvidersRejectInvalidColumnProviderConfig(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
dataPath := filepath.Join(root, "data.yaml")
|
dataPath := filepath.Join(root, "data.yaml")
|
||||||
|
|||||||
@@ -279,7 +279,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, page.Row)
|
return ctx.classProgressionRows(def.Rows.Levels, nil, wikiDataProviderAttacksConfig{}, page.Key, page.Row)
|
||||||
case "class_feats":
|
case "class_feats":
|
||||||
if page.Category != "classes" {
|
if page.Category != "classes" {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@@ -303,7 +303,7 @@ func (ctx *wikiContext) wikiTableSourceRows(def wikiTableDefinition, page wikiTe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ctx *wikiContext) classProgressionRows(levels string, projections map[string]wikiDataProviderProjectionField, classRow map[string]any) ([]map[string]any, error) {
|
func (ctx *wikiContext) classProgressionRows(levels string, projections map[string]wikiDataProviderProjectionField, attacks wikiDataProviderAttacksConfig, classKey string, classRow map[string]any) ([]map[string]any, error) {
|
||||||
start, end, err := parseLevelRange(levels)
|
start, end, err := parseLevelRange(levels)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -363,7 +363,17 @@ func (ctx *wikiContext) classProgressionRows(levels string, projections map[stri
|
|||||||
for level := start; level <= end; level++ {
|
for level := start; level <= end; level++ {
|
||||||
row := cloneRowMap(classRow)
|
row := cloneRowMap(classRow)
|
||||||
row["Level"] = level
|
row["Level"] = level
|
||||||
row["BAB"] = formatSignedInt(classBABAtLevel(stringValue(classRow, "AttackBonusTable"), level))
|
bab := classBABAtLevel(stringValue(classRow, "AttackBonusTable"), level)
|
||||||
|
row["BABValue"] = bab
|
||||||
|
row["BAB"] = formatSignedInt(bab)
|
||||||
|
attackCount := classAttackCount(attacks, classKey, bab, level)
|
||||||
|
if attackCount < 1 {
|
||||||
|
attackCount = 1
|
||||||
|
}
|
||||||
|
countField := defaultStringValue(attacks.CountField, "AttackCount")
|
||||||
|
sequenceField := defaultStringValue(attacks.Field, "AttackSequence")
|
||||||
|
row[countField] = attackCount
|
||||||
|
row[sequenceField] = formatAttackSequence(bab, attackCount, attackStep(attacks))
|
||||||
if saveRow := saveRows[strconv.Itoa(level)]; saveRow != nil {
|
if saveRow := saveRows[strconv.Itoa(level)]; saveRow != nil {
|
||||||
for key, value := range saveRow {
|
for key, value := range saveRow {
|
||||||
row[key] = value
|
row[key] = value
|
||||||
@@ -418,6 +428,96 @@ func classBABAtLevel(table string, level int) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func classAttackCount(cfg wikiDataProviderAttacksConfig, classKey string, bab, level int) int {
|
||||||
|
count := baseClassAttackCount(cfg, bab)
|
||||||
|
for _, override := range cfg.Overrides {
|
||||||
|
if !wikiAttackOverrideMatches(override, classKey, level) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if override.Attacks > 0 {
|
||||||
|
count = override.Attacks
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
count += override.Add
|
||||||
|
}
|
||||||
|
if count < 1 {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func baseClassAttackCount(cfg wikiDataProviderAttacksConfig, bab int) int {
|
||||||
|
switch strings.TrimSpace(cfg.Mode) {
|
||||||
|
case "fixed":
|
||||||
|
if cfg.Attacks > 0 {
|
||||||
|
return cfg.Attacks
|
||||||
|
}
|
||||||
|
case "bab_thresholds":
|
||||||
|
return attackCountFromThresholds(cfg.Thresholds, bab)
|
||||||
|
case "":
|
||||||
|
return attackCountFromThresholds(defaultWikiAttackThresholds(), bab)
|
||||||
|
}
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func attackCountFromThresholds(thresholds []wikiDataProviderAttackThreshold, bab int) int {
|
||||||
|
count := 1
|
||||||
|
for _, threshold := range thresholds {
|
||||||
|
if threshold.Attacks > 0 && bab >= threshold.MinBAB {
|
||||||
|
count = threshold.Attacks
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
}
|
||||||
|
|
||||||
|
func defaultWikiAttackThresholds() []wikiDataProviderAttackThreshold {
|
||||||
|
return []wikiDataProviderAttackThreshold{
|
||||||
|
{MinBAB: 1, Attacks: 1},
|
||||||
|
{MinBAB: 6, Attacks: 2},
|
||||||
|
{MinBAB: 11, Attacks: 3},
|
||||||
|
{MinBAB: 16, Attacks: 4},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func wikiAttackOverrideMatches(override wikiDataProviderAttackOverrideRule, classKey string, level int) bool {
|
||||||
|
foundClass := false
|
||||||
|
for _, candidate := range override.Classes {
|
||||||
|
if strings.TrimSpace(candidate) == classKey {
|
||||||
|
foundClass = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !foundClass {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
levels, err := parseWikiAttackLevels(override.Levels)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, ok := levels[level]
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func attackStep(cfg wikiDataProviderAttacksConfig) int {
|
||||||
|
if cfg.Step == 0 {
|
||||||
|
return -5
|
||||||
|
}
|
||||||
|
return cfg.Step
|
||||||
|
}
|
||||||
|
|
||||||
|
func formatAttackSequence(bab, attacks, step int) string {
|
||||||
|
if attacks < 1 {
|
||||||
|
attacks = 1
|
||||||
|
}
|
||||||
|
values := make([]string, 0, attacks)
|
||||||
|
current := bab
|
||||||
|
for i := 0; i < attacks; i++ {
|
||||||
|
values = append(values, formatSignedInt(current))
|
||||||
|
current += step
|
||||||
|
}
|
||||||
|
return strings.Join(values, "/")
|
||||||
|
}
|
||||||
|
|
||||||
func formatSignedInt(value int) string {
|
func formatSignedInt(value int) string {
|
||||||
if value > 0 {
|
if value > 0 {
|
||||||
return "+" + strconv.Itoa(value)
|
return "+" + strconv.Itoa(value)
|
||||||
@@ -884,6 +984,11 @@ func (p *wikiExprParser) callHelper(name string, args []any) (any, error) {
|
|||||||
default:
|
default:
|
||||||
return stringifyWikiTableValue(args[0]), nil
|
return stringifyWikiTableValue(args[0]), nil
|
||||||
}
|
}
|
||||||
|
case "count":
|
||||||
|
if len(args) != 1 {
|
||||||
|
return nil, fmt.Errorf("count expects 1 argument")
|
||||||
|
}
|
||||||
|
return countWikiTemplateValue(args[0]), nil
|
||||||
case "nonzero":
|
case "nonzero":
|
||||||
if len(args) != 1 {
|
if len(args) != 1 {
|
||||||
return nil, fmt.Errorf("nonzero expects 1 argument")
|
return nil, fmt.Errorf("nonzero expects 1 argument")
|
||||||
@@ -974,6 +1079,30 @@ func (p *wikiExprParser) callHelper(name string, args []any) (any, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func countWikiTemplateValue(value any) int {
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case nil:
|
||||||
|
return 0
|
||||||
|
case []string:
|
||||||
|
return len(typed)
|
||||||
|
case []any:
|
||||||
|
count := 0
|
||||||
|
for _, item := range typed {
|
||||||
|
if presentWikiValue(item) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count
|
||||||
|
case map[string]any:
|
||||||
|
return len(typed)
|
||||||
|
default:
|
||||||
|
if presentWikiValue(value) {
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (p *wikiExprParser) hiddenWikiReference(value any, targetDataset string) (bool, error) {
|
func (p *wikiExprParser) hiddenWikiReference(value any, targetDataset string) (bool, error) {
|
||||||
targetDataset = strings.TrimSpace(targetDataset)
|
targetDataset = strings.TrimSpace(targetDataset)
|
||||||
if !isWikiVisibilityDataset(targetDataset) {
|
if !isWikiVisibilityDataset(targetDataset) {
|
||||||
|
|||||||
@@ -185,6 +185,8 @@ func (ctx *wikiContext) applyWikiTemplateFilter(value any, filter string, state
|
|||||||
return text, nil
|
return text, nil
|
||||||
case "join":
|
case "join":
|
||||||
return joinWikiTemplateValue(value, firstArg(args)), nil
|
return joinWikiTemplateValue(value, firstArg(args)), nil
|
||||||
|
case "count":
|
||||||
|
return countWikiTemplateValue(value), nil
|
||||||
case "list":
|
case "list":
|
||||||
return wikiTemplateHTML(renderWikiTemplateList(value, firstArg(args))), nil
|
return wikiTemplateHTML(renderWikiTemplateList(value, firstArg(args))), nil
|
||||||
case "ordinal":
|
case "ordinal":
|
||||||
|
|||||||
Reference in New Issue
Block a user