build-binaries / build-binaries (push) Successful in 2m26s
Closes #94. Generates the racial radial rows at build time from the `UsableFeat` column in the race feats tables, replacing the hand-maintained racial block in `sow-topdata` `data/classes/feats/global.json`. ## Why Racial spell-like abilities reach the in-game radial only via an `OnMenu` row in every `cls_feat_<class>.2da`. Racial feats are granted by race, never by a class, so nothing adds them automatically — they were hand-written and prepended into every class table. The list drifts: this build already had **24** usable racial feats in the race tables but only **23** hand rows, so one activatable feat was silently missing. ## What - `racialUsableFeatRules()` scans `race_feat_*.2da` datasets, collects `UsableFeat=1` feats, emits one rule each: `List=3, GrantedOnLevel=99, OnMenu=1`. Deduped across races, sorted for deterministic output. - Row shape is exactly the in-game-verified hand rows. `List=3` keeps it off every level-up selection list; `GrantedOnLevel=99` is above the level cap so no class ever actually grants it (`nLevelGranted` is `uint8_t`); `OnMenu=1` renders the button once the creature possesses the feat. Possession stays chargen / the login racial-feat sync (`sow-codebase#359`). - Reuses the existing `globalRules` injection path (same dedup, feat-existence check, label lookup) but applies **unconditionally**, so a leftover hand row in `global.json` deduplicates to a no-op — the `sow-topdata` cleanup lands separately. - Not sourced from `feat.2da`: a global feat flag would inject unrelated class abilities (e.g. a shadowdancer ability) into every class radial via multiclass. ## Tests - `racial_feat_rules_test.go`: usable-only, non-usable excluded, cross-race dedup, non-`race_feat_` ignored, deterministic order. - Existing `cls_feat` global-injection build tests still pass with the new parameter. - End-to-end: with the racial block removed from `global.json`, a real `build-topdata` emits `TieflingDarkness 3 99 1` once per table across all 21, 24 racial rows total. ## Merge order The `sow-topdata` `global.json` racial-row deletion depends on this — it must ship first (or the racial radial vanishes on the next topdata build with the released tool). 🤖 Generated with [Claude Code](https://claude.com/claude-code)Reviewed-on: #95 Co-authored-by: vickydotbat <vickydotbat@tutamail.com>
55 lines
1.6 KiB
Go
55 lines
1.6 KiB
Go
package topdata
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
|
)
|
|
|
|
func raceFeatDataset(output string, rows ...map[string]any) nativeCollectedDataset {
|
|
return nativeCollectedDataset{
|
|
Dataset: nativeDataset{OutputName: output},
|
|
Rows: rows,
|
|
}
|
|
}
|
|
|
|
func usableRow(featID string, usable any) map[string]any {
|
|
row := map[string]any{"FeatIndex": map[string]any{"id": featID}}
|
|
if usable != nil {
|
|
row["UsableFeat"] = usable
|
|
}
|
|
return row
|
|
}
|
|
|
|
func TestRacialUsableFeatRules(t *testing.T) {
|
|
collected := []nativeCollectedDataset{
|
|
raceFeatDataset("race_feat_ddrw.2da",
|
|
usableRow("feat:keen_sense", nil), // passive, no UsableFeat -> skipped
|
|
usableRow("feat:darkvision", 1), // usable
|
|
usableRow("feat:use_poison", 0), // explicitly not usable -> skipped
|
|
usableRow("feat:drow/faerie_fire", "1"),// usable, string form
|
|
),
|
|
raceFeatDataset("race_feat_tief.2da",
|
|
usableRow("feat:darkvision", 1), // duplicate across races -> collapses to one
|
|
usableRow("feat:tiefling/darkness", 1),
|
|
),
|
|
raceFeatDataset("feat.2da", // not a race feats table -> ignored entirely
|
|
usableRow("feat:power_attack", 1),
|
|
),
|
|
}
|
|
|
|
got := racialUsableFeatRules(collected)
|
|
|
|
want := []project.TopDataClassFeatGlobalRule{
|
|
{Feat: "feat:darkvision", List: "3", GrantedOnLevel: "99", OnMenu: "1"},
|
|
{Feat: "feat:drow/faerie_fire", List: "3", GrantedOnLevel: "99", OnMenu: "1"},
|
|
{Feat: "feat:tiefling/darkness", List: "3", GrantedOnLevel: "99", OnMenu: "1"},
|
|
}
|
|
|
|
// Slice is sorted by Feat, so order is deterministic.
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("rules: got %+v, want %+v", got, want)
|
|
}
|
|
}
|