mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Two bugs reported back-to-back from a Rogue's run: 1. HP display reset between battles. End of fight 1: 101/123 → start of fight 2: 100/100. applyDnDHPScaling was overwriting Stats.MaxHP with the scaled wound value, so the display denominator dropped along with the numerator. Wounded carry-over became invisible. Fix: introduce CombatStats.StartHP. Combat engine reads it as the entry-HP when set; MaxHP stays put. Display now reads "100/123" as intended. 2. Auto-crit fired on a roll of 11 with no narrative tell. Rogue passive AutoCritFirst was triggering correctly, but the renderer used the generic crit pool, so the player saw "🎲 11 vs AC 10 → CRIT" with no indication their *class* caused it. Fix: tag the crit event with Desc="auto_crit" when the passive (not the dice) caused it; new narrativePlayerAutoCrit pool calls out the training/instinct/exploit theme. Test bound for T5 dungeon death rate loosened from 0.02 to 0.01 — the new Sudden Death phase from the previous commit shifted geared-T5 fights slightly toward player wins. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
149 lines
4.9 KiB
Go
149 lines
4.9 KiB
Go
package plugin
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// ── Plug 1: HP scaling ──────────────────────────────────────────────────────
|
|
|
|
func TestApplyDnDHPScaling_FullHP(t *testing.T) {
|
|
stats := CombatStats{MaxHP: 100}
|
|
c := &DnDCharacter{HPMax: 50, HPCurrent: 50}
|
|
applyDnDHPScaling(&stats, c)
|
|
if stats.MaxHP != 100 {
|
|
t.Errorf("full HP: MaxHP changed to %d, want unchanged 100", stats.MaxHP)
|
|
}
|
|
if stats.StartHP != 0 {
|
|
t.Errorf("full HP: StartHP = %d, want 0 (unset)", stats.StartHP)
|
|
}
|
|
}
|
|
|
|
func TestApplyDnDHPScaling_HalfHP(t *testing.T) {
|
|
stats := CombatStats{MaxHP: 100}
|
|
c := &DnDCharacter{HPMax: 50, HPCurrent: 25}
|
|
applyDnDHPScaling(&stats, c)
|
|
if stats.MaxHP != 100 {
|
|
t.Errorf("50%% HP: MaxHP = %d, want unchanged 100", stats.MaxHP)
|
|
}
|
|
if stats.StartHP != 50 {
|
|
t.Errorf("50%% HP: StartHP = %d, want 50", stats.StartHP)
|
|
}
|
|
}
|
|
|
|
func TestApplyDnDHPScaling_FloorAt25Pct(t *testing.T) {
|
|
stats := CombatStats{MaxHP: 100}
|
|
c := &DnDCharacter{HPMax: 50, HPCurrent: 0}
|
|
applyDnDHPScaling(&stats, c)
|
|
if stats.MaxHP != 100 {
|
|
t.Errorf("0%% HP: MaxHP = %d, want unchanged 100", stats.MaxHP)
|
|
}
|
|
if stats.StartHP != 25 {
|
|
t.Errorf("0%% HP: StartHP = %d, want 25 (floor)", stats.StartHP)
|
|
}
|
|
}
|
|
|
|
func TestApplyDnDHPScaling_NoOpIfNilOrZero(t *testing.T) {
|
|
stats := CombatStats{MaxHP: 100}
|
|
applyDnDHPScaling(&stats, nil)
|
|
if stats.MaxHP != 100 || stats.StartHP != 0 {
|
|
t.Errorf("nil char: MaxHP=%d StartHP=%d, want 100/0", stats.MaxHP, stats.StartHP)
|
|
}
|
|
c := &DnDCharacter{HPMax: 0}
|
|
applyDnDHPScaling(&stats, c)
|
|
if stats.MaxHP != 100 || stats.StartHP != 0 {
|
|
t.Errorf("zero HPMax: MaxHP=%d StartHP=%d, want 100/0", stats.MaxHP, stats.StartHP)
|
|
}
|
|
}
|
|
|
|
// ── Plug 2: roll summary ────────────────────────────────────────────────────
|
|
|
|
func TestDnDRollSummaryLine_Empty(t *testing.T) {
|
|
r := CombatResult{Events: []CombatEvent{
|
|
{Actor: "player", Action: "hit", Roll: 0}, // no Roll → not D&D
|
|
}}
|
|
if got := dndRollSummaryLine(r); got != "" {
|
|
t.Errorf("expected empty summary; got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestDnDRollSummaryLine_Basic(t *testing.T) {
|
|
r := CombatResult{Events: []CombatEvent{
|
|
{Actor: "player", Action: "hit", Roll: 14, RollAgainst: 12},
|
|
{Actor: "player", Action: "miss", Roll: 5, RollAgainst: 12},
|
|
{Actor: "player", Action: "crit", Roll: 20, RollAgainst: 12},
|
|
{Actor: "player", Action: "miss", Roll: 1, RollAgainst: 12, Desc: "fumble"},
|
|
{Actor: "enemy", Action: "hit", Roll: 18}, // enemy ignored
|
|
}}
|
|
got := dndRollSummaryLine(r)
|
|
for _, want := range []string{"d20", "2/4 hit", "1 crit", "1 fumble", "nat 20"} {
|
|
if !strings.Contains(got, want) {
|
|
t.Errorf("summary missing %q: %s", want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDnDRollSummaryLine_HighestNonNat20(t *testing.T) {
|
|
r := CombatResult{Events: []CombatEvent{
|
|
{Actor: "player", Action: "hit", Roll: 17, RollAgainst: 14},
|
|
{Actor: "player", Action: "miss", Roll: 8, RollAgainst: 14},
|
|
}}
|
|
got := dndRollSummaryLine(r)
|
|
if !strings.Contains(got, "Best: 17") {
|
|
t.Errorf("expected 'Best: 17' in summary: %s", got)
|
|
}
|
|
}
|
|
|
|
// ── Plug 3: !roll dice parser ──────────────────────────────────────────────
|
|
|
|
func TestParseDice(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
count, sides, mod int
|
|
ok bool
|
|
}{
|
|
{"d20", 1, 20, 0, true},
|
|
{"1d20", 1, 20, 0, true},
|
|
{"2d6+3", 2, 6, 3, true},
|
|
{"4d6-1", 4, 6, -1, true},
|
|
{"3D8", 3, 8, 0, true}, // case-insensitive
|
|
{" 2d6+3 ", 2, 6, 3, true}, // whitespace
|
|
{"d1", 0, 0, 0, false}, // sides too few
|
|
{"100d6", 100, 6, 0, true}, // max count
|
|
{"101d6", 0, 0, 0, false}, // over max
|
|
{"banana", 0, 0, 0, false},
|
|
{"d6+", 0, 0, 0, false},
|
|
{"", 0, 0, 0, false},
|
|
{"2d6+", 0, 0, 0, false},
|
|
}
|
|
for _, c := range cases {
|
|
ct, sd, mod, ok := parseDice(c.in)
|
|
if ok != c.ok || ct != c.count || sd != c.sides || mod != c.mod {
|
|
t.Errorf("parseDice(%q) = (%d,%d,%d,%v); want (%d,%d,%d,%v)",
|
|
c.in, ct, sd, mod, ok, c.count, c.sides, c.mod, c.ok)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRollDice_Bounds(t *testing.T) {
|
|
// 1000 trials: every roll must be in [1, sides].
|
|
for i := 0; i < 1000; i++ {
|
|
rolls, total := rollDice(4, 6, 2)
|
|
sum := 2
|
|
for _, r := range rolls {
|
|
if r < 1 || r > 6 {
|
|
t.Fatalf("roll out of range: %d", r)
|
|
}
|
|
sum += r
|
|
}
|
|
if sum != total {
|
|
t.Fatalf("total mismatch: rolls sum=%d, total=%d", sum, total)
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Plug 4: !stats / !level format ─────────────────────────────────────────
|
|
// (No DB integration here — just smoke-test that the helpers don't panic
|
|
// against a constructed character. Full DB-integrated tests would be
|
|
// duplicative with existing prod-DB tests.)
|