mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 00:32:40 +00:00
Adds `gen tuned` to cmd/open5e-import: a deterministic formula that scales every raw SRD stat block down to an engine-ready DnDMonsterTemplate. HP/AC/ AttackBonus are verbatim SRD (AC clamped to the engine min 10); the Attack stat is interpolated from attackByCRPoints, a CR→Attack anchor table lifted from the hand-tuned dndBestiary (CR is the calibration axis the 2026-05-10 rebalance used — raw SRD per-hit damage is ignored). Speed/BlockRate are coarse baselines from SpeedWalk/AC. bestiary_tuned.go merges the 322 generated templates into dndBestiary, but hand-authored roster entries win — the merge only fills IDs the roster does not already define, so playtested numbers and wired abilities are untouched. Abilities are deliberately not wired: every generated entry has a nil Ability, with the SRD multiattack/trait text parked in Notes as raw material for the follow-up ability-wiring pass.
72 lines
2.5 KiB
Go
72 lines
2.5 KiB
Go
package plugin
|
|
|
|
import "testing"
|
|
|
|
// TestTunedBestiarySRD_Populated guards the generated tuned table: every SRD
|
|
// monster should derive a structurally sane DnDMonsterTemplate. Regenerate with
|
|
// `go run ./cmd/open5e-import gen tuned` if the SRD set or formula changes.
|
|
func TestTunedBestiarySRD_Populated(t *testing.T) {
|
|
if len(tunedBestiarySRD) < 300 {
|
|
t.Fatalf("tunedBestiarySRD has %d entries, want the full SRD set (~322)", len(tunedBestiarySRD))
|
|
}
|
|
for id, m := range tunedBestiarySRD {
|
|
if m.ID != id {
|
|
t.Errorf("%s: ID field = %q, want map key", id, m.ID)
|
|
}
|
|
if m.Name == "" {
|
|
t.Errorf("%s: empty Name", id)
|
|
}
|
|
if m.HP <= 0 {
|
|
t.Errorf("%s: HP=%d, want positive", id, m.HP)
|
|
}
|
|
if m.AC < 10 {
|
|
t.Errorf("%s: AC=%d, want >= 10 (engine minimum)", id, m.AC)
|
|
}
|
|
if m.Attack < 1 {
|
|
t.Errorf("%s: Attack=%d, want >= 1", id, m.Attack)
|
|
}
|
|
if m.Speed < 6 || m.Speed > 18 {
|
|
t.Errorf("%s: Speed=%d, want within [6,18]", id, m.Speed)
|
|
}
|
|
if m.Ability != nil {
|
|
t.Errorf("%s: Ability is set — the generator never wires abilities", id)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestTunedBestiarySRD_MergedIntoRoster confirms the init-time merge ran and
|
|
// that hand-authored dndBestiary entries win over the generated baseline.
|
|
func TestTunedBestiarySRD_MergedIntoRoster(t *testing.T) {
|
|
// A tuned-only creature (no hand-authored entry) should be in the roster.
|
|
if _, ok := dndBestiary["tarrasque"]; !ok {
|
|
t.Error("tarrasque missing from dndBestiary — tuned merge did not run")
|
|
}
|
|
// Owlbear is hand-authored; the roster entry must keep the playtested
|
|
// values (and its wired Ability), not the generated baseline.
|
|
ob, ok := dndBestiary["owlbear"]
|
|
if !ok {
|
|
t.Fatal("owlbear missing from dndBestiary")
|
|
}
|
|
if ob.Ability == nil {
|
|
t.Error("owlbear lost its hand-authored Ability — tuned baseline clobbered the roster entry")
|
|
}
|
|
}
|
|
|
|
// TestAttackByCR_KnownAnchors spot-checks the Attack curve against playtested
|
|
// roster values so a formula regression surfaces as a mismatch.
|
|
func TestTunedBestiarySRD_KnownEntry(t *testing.T) {
|
|
ab, ok := tunedBestiarySRD["aboleth"]
|
|
if !ok {
|
|
t.Fatal("aboleth missing from tuned bestiary")
|
|
}
|
|
// HP/AC verbatim SRD; Attack from the CR-10 anchor; AttackBonus from the
|
|
// hardest-hitting attack (Tentacle, +9).
|
|
if ab.HP != 135 || ab.AC != 17 || ab.Attack != 16 || ab.AttackBonus != 9 {
|
|
t.Errorf("aboleth = HP %d / AC %d / Attack %d / AB %d, want 135 / 17 / 16 / 9",
|
|
ab.HP, ab.AC, ab.Attack, ab.AttackBonus)
|
|
}
|
|
if ab.XPValue != 5900 {
|
|
t.Errorf("aboleth XPValue = %d, want 5900", ab.XPValue)
|
|
}
|
|
}
|