mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
§7.5 awareness pulses on Day 3, 6, 9, 12, ... apply +10 threat at briefing rollover and log a temporal entry (DragonsLairAwarenessPulse flavor). Day-14 awakening sets RegionState["infernax_awake"]=true once, with the §7.5 awakening narration; the pulse line is suppressed that day so the awakening narration carries the moment alone. Combat-side knobs (kobold patrols +1 enemy per active room, 6h wandering cadence, dragon-encounter weighting in the wandering table, forced extraction pressure once awake) are exposed via the infernax_awake region flag — combat-link phase reads it to switch behavior. Boss-defeated suppresses entirely. Reuses flavor.DragonsLairAwarenessPulse / DragonsLairAwakenWarning per feedback_reuse_existing_flavor. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
695 lines
20 KiB
Go
695 lines
20 KiB
Go
package plugin
|
||
|
||
import (
|
||
"strings"
|
||
"testing"
|
||
"time"
|
||
|
||
"gogobee/internal/db"
|
||
"maunium.net/go/mautrix/id"
|
||
)
|
||
|
||
// Phase 12 E3a — Sunken Temple tidal event.
|
||
|
||
// setExpDay rewrites the current_day column for tests.
|
||
func setExpDay(t *testing.T, expID string, day int) {
|
||
t.Helper()
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET current_day = ? WHERE expedition_id = ?`,
|
||
day, expID); err != nil {
|
||
t.Fatalf("set current_day: %v", err)
|
||
}
|
||
}
|
||
|
||
func TestSunkenTemple_TidalWarningDay4(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-tidal-d4:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneSunkenTemple, "",
|
||
ExpeditionSupplies{Current: 30, Max: 30, DailyBurn: 1.5, HarshMod: 1.5})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
setExpDay(t, exp.ID, 3) // → briefing rolls to Day 4
|
||
exp, _ = getExpedition(exp.ID)
|
||
|
||
p := &AdventurePlugin{}
|
||
startSU := exp.Supplies.Current
|
||
if err := p.deliverBriefing(exp, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
got, _ := getExpedition(exp.ID)
|
||
if got.CurrentDay != 4 {
|
||
t.Fatalf("CurrentDay = %d, want 4", got.CurrentDay)
|
||
}
|
||
// Warning day: NO doubled burn — single 1.5 SU burn.
|
||
if got.Supplies.Current != startSU-1.5 {
|
||
t.Errorf("supplies = %v, want %v (single burn)", got.Supplies.Current, startSU-1.5)
|
||
}
|
||
entries, _ := recentExpeditionLog(exp.ID, 10)
|
||
foundWarn := false
|
||
for _, e := range entries {
|
||
if e.Type == "temporal" && strings.Contains(e.Summary, "tidal warning") {
|
||
foundWarn = true
|
||
}
|
||
}
|
||
if !foundWarn {
|
||
t.Error("expected a 'temporal' tidal-warning log entry on Day 4")
|
||
}
|
||
}
|
||
|
||
func TestSunkenTemple_TidalEventDay6_ForcesDoubleBurn(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-tidal-d6:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
// Tier-2 supplies: HarshMod 1.5 normally. Tidal must force 2×.
|
||
exp, err := startExpedition(uid, ZoneSunkenTemple, "",
|
||
ExpeditionSupplies{Current: 30, Max: 30, DailyBurn: 1.5, HarshMod: 1.5})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
setExpDay(t, exp.ID, 5) // → rolls to Day 6
|
||
exp, _ = getExpedition(exp.ID)
|
||
|
||
p := &AdventurePlugin{}
|
||
startSU := exp.Supplies.Current
|
||
if err := p.deliverBriefing(exp, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
got, _ := getExpedition(exp.ID)
|
||
if got.CurrentDay != 6 {
|
||
t.Fatalf("CurrentDay = %d, want 6", got.CurrentDay)
|
||
}
|
||
// Forced 2× burn = 1.5 * 2 = 3.0 SU
|
||
wantBurn := float32(3.0)
|
||
if startSU-got.Supplies.Current != wantBurn {
|
||
t.Errorf("burn = %v, want %v (forced 2× tidal)", startSU-got.Supplies.Current, wantBurn)
|
||
}
|
||
entries, _ := recentExpeditionLog(exp.ID, 10)
|
||
foundEvent := false
|
||
for _, e := range entries {
|
||
if e.Type == "temporal" && strings.Contains(e.Summary, "tidal peak") {
|
||
foundEvent = true
|
||
}
|
||
}
|
||
if !foundEvent {
|
||
t.Error("expected a 'temporal' tidal-peak log entry on Day 6")
|
||
}
|
||
}
|
||
|
||
func TestSunkenTemple_BossDefeatedSilencesTidal(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-tidal-boss:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneSunkenTemple, "",
|
||
ExpeditionSupplies{Current: 30, Max: 30, DailyBurn: 1.5, HarshMod: 1.5})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET boss_defeated = 1, current_day = 5 WHERE expedition_id = ?`,
|
||
exp.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
exp, _ = getExpedition(exp.ID)
|
||
|
||
p := &AdventurePlugin{}
|
||
startSU := exp.Supplies.Current
|
||
if err := p.deliverBriefing(exp, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
|
||
got, _ := getExpedition(exp.ID)
|
||
// Single normal burn, no tidal multiplier.
|
||
if got.Supplies.Current != startSU-1.5 {
|
||
t.Errorf("supplies = %v, want %v (no tidal mult)", got.Supplies.Current, startSU-1.5)
|
||
}
|
||
entries, _ := recentExpeditionLog(exp.ID, 10)
|
||
for _, e := range entries {
|
||
if e.Type == "temporal" {
|
||
t.Errorf("did not expect temporal log entry post-boss; got %+v", e)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestManor_NightlyReset_FiresEveryThreeDays(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-manor-reset:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneManorBlackspire, "",
|
||
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 2, HarshMod: 2})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
p := &AdventurePlugin{}
|
||
|
||
cases := []struct {
|
||
fromDay int
|
||
wantReset bool
|
||
}{
|
||
{1, false}, // → Day 2
|
||
{2, true}, // → Day 3
|
||
{3, false}, // → Day 4
|
||
{4, false}, // → Day 5
|
||
{5, true}, // → Day 6
|
||
{8, true}, // → Day 9
|
||
}
|
||
for _, c := range cases {
|
||
setExpDay(t, exp.ID, c.fromDay)
|
||
// Reset last_briefing_at so the next briefing actually fires.
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET last_briefing_at = NULL WHERE expedition_id = ?`, exp.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fresh, _ := getExpedition(exp.ID)
|
||
// Drain log entries from prior iterations so we count only today's.
|
||
preCount := manorResetLogCount(t, exp.ID)
|
||
if err := p.deliverBriefing(fresh, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got := manorResetLogCount(t, exp.ID)
|
||
gained := got - preCount
|
||
if c.wantReset && gained == 0 {
|
||
t.Errorf("from Day %d → expected a manor-reset entry; got none", c.fromDay)
|
||
}
|
||
if !c.wantReset && gained != 0 {
|
||
t.Errorf("from Day %d → expected no reset; got %d new entries", c.fromDay, gained)
|
||
}
|
||
}
|
||
}
|
||
|
||
func manorResetLogCount(t *testing.T, expID string) int {
|
||
t.Helper()
|
||
rows, err := db.Get().Query(`
|
||
SELECT COUNT(*) FROM dnd_expedition_log
|
||
WHERE expedition_id = ? AND entry_type = 'temporal'
|
||
AND summary LIKE 'manor reset%'`, expID)
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer rows.Close()
|
||
if !rows.Next() {
|
||
return 0
|
||
}
|
||
var n int
|
||
_ = rows.Scan(&n)
|
||
return n
|
||
}
|
||
|
||
func TestManor_BossDefeatedSilencesReset(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-manor-boss:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneManorBlackspire, "",
|
||
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 2, HarshMod: 2})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET boss_defeated = 1, current_day = 2 WHERE expedition_id = ?`,
|
||
exp.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
exp, _ = getExpedition(exp.ID)
|
||
|
||
p := &AdventurePlugin{}
|
||
if err := p.deliverBriefing(exp, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if got := manorResetLogCount(t, exp.ID); got != 0 {
|
||
t.Errorf("boss-defeated manor should not reset; got %d entries", got)
|
||
}
|
||
}
|
||
|
||
func TestManorReset_PredicateOnly(t *testing.T) {
|
||
e := &Expedition{ZoneID: ZoneManorBlackspire}
|
||
for _, d := range []int{0, 1, 2, 4, 5, 7, 8} {
|
||
if ManorReset(e, d) {
|
||
t.Errorf("ManorReset(day %d) = true; want false", d)
|
||
}
|
||
}
|
||
for _, d := range []int{3, 6, 9, 12, 30} {
|
||
if !ManorReset(e, d) {
|
||
t.Errorf("ManorReset(day %d) = false; want true", d)
|
||
}
|
||
}
|
||
// Wrong zone is always false.
|
||
other := &Expedition{ZoneID: ZoneGoblinWarrens}
|
||
if ManorReset(other, 3) {
|
||
t.Error("ManorReset on non-Manor zone should be false")
|
||
}
|
||
}
|
||
|
||
func TestUnderforge_HeatStackIncrementsAndBands(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-uf-heat:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneUnderforge, "",
|
||
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 2, HarshMod: 2})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
p := &AdventurePlugin{}
|
||
|
||
// Drive 11 mornings — heat should top out at 10.
|
||
for i := 0; i < 11; i++ {
|
||
fresh, _ := getExpedition(exp.ID)
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET last_briefing_at = NULL WHERE expedition_id = ?`,
|
||
exp.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// Top up supplies so we don't hit starvation; not relevant here.
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET supplies_json = ? WHERE expedition_id = ?`,
|
||
`{"current":99,"max":99,"daily_burn":2,"harsh_mod":2,"foraged_today":false,"packs_standard":3,"packs_deluxe":0}`,
|
||
exp.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fresh, _ = getExpedition(exp.ID)
|
||
if err := p.deliverBriefing(fresh, time.Now().UTC()); err != nil {
|
||
t.Fatalf("briefing %d: %v", i, err)
|
||
}
|
||
}
|
||
got, _ := getExpedition(exp.ID)
|
||
if got.TemporalStack != 10 {
|
||
t.Errorf("heat stack = %d, want 10 (capped)", got.TemporalStack)
|
||
}
|
||
|
||
// Verify warning + critical narrations both fired.
|
||
rows, _ := db.Get().Query(
|
||
`SELECT summary FROM dnd_expedition_log WHERE expedition_id = ? AND entry_type = 'temporal' ORDER BY entry_id`,
|
||
exp.ID)
|
||
defer rows.Close()
|
||
var summaries []string
|
||
for rows.Next() {
|
||
var s string
|
||
_ = rows.Scan(&s)
|
||
summaries = append(summaries, s)
|
||
}
|
||
want := []string{"warning band", "+50%", "exhaustion"}
|
||
for _, w := range want {
|
||
found := false
|
||
for _, s := range summaries {
|
||
if strings.Contains(s, w) {
|
||
found = true
|
||
break
|
||
}
|
||
}
|
||
if !found {
|
||
t.Errorf("missing temporal entry containing %q in %v", w, summaries)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestUnderforge_HarshAtHeat7(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-uf-harsh:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneUnderforge, "",
|
||
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 2, HarshMod: 2})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
// Heat 6 — should NOT be harsh.
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET temporal_stack = 6, current_day = 7 WHERE expedition_id = ?`,
|
||
exp.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
exp, _ = getExpedition(exp.ID)
|
||
if zoneTemporalHarsh(exp) {
|
||
t.Error("heat=6 should not trigger harsh")
|
||
}
|
||
// Heat 7 — should be harsh.
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET temporal_stack = 7 WHERE expedition_id = ?`,
|
||
exp.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
exp, _ = getExpedition(exp.ID)
|
||
if !zoneTemporalHarsh(exp) {
|
||
t.Error("heat=7 should trigger harsh")
|
||
}
|
||
}
|
||
|
||
func TestUnderforgeHeatBandFor(t *testing.T) {
|
||
cases := []struct {
|
||
stacks int
|
||
want string
|
||
}{
|
||
{0, "none"},
|
||
{1, "ambient"}, {3, "ambient"},
|
||
{4, "warning"}, {6, "warning"},
|
||
{7, "supply"}, {9, "supply"},
|
||
{10, "critical"},
|
||
}
|
||
for _, c := range cases {
|
||
if got := UnderforgeHeatBandFor(c.stacks); got != c.want {
|
||
t.Errorf("UnderforgeHeatBandFor(%d) = %q, want %q", c.stacks, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestUnderforge_FortifiedRestReducesHeat(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-uf-rest:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp := &Expedition{
|
||
ID: "fake-uf",
|
||
ZoneID: ZoneUnderforge,
|
||
TemporalStack: 5,
|
||
}
|
||
// Insert minimal row so updateTemporalStack works.
|
||
if _, err := db.Get().Exec(`
|
||
INSERT INTO dnd_expedition (expedition_id, user_id, zone_id, run_id, status, start_date, current_day, supplies_json, threat_events, region_state, last_activity, temporal_stack)
|
||
VALUES (?, ?, ?, NULL, 'active', CURRENT_TIMESTAMP, 1, '{}', '[]', '{}', CURRENT_TIMESTAMP, 5)`,
|
||
exp.ID, string(uid), string(ZoneUnderforge)); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
defer db.Get().Exec(`DELETE FROM dnd_expedition WHERE expedition_id = ?`, exp.ID)
|
||
|
||
got := reduceUnderforgeHeat(exp)
|
||
if got != 3 {
|
||
t.Errorf("reduceUnderforgeHeat(5) = %d, want 3", got)
|
||
}
|
||
exp.TemporalStack = 1
|
||
got = reduceUnderforgeHeat(exp)
|
||
if got != 0 {
|
||
t.Errorf("reduceUnderforgeHeat(1) = %d, want 0 (no underflow)", got)
|
||
}
|
||
// Wrong zone — no-op.
|
||
other := &Expedition{ZoneID: ZoneGoblinWarrens, TemporalStack: 5}
|
||
if reduceUnderforgeHeat(other) != 5 {
|
||
t.Error("reduceUnderforgeHeat on non-Underforge should be no-op")
|
||
}
|
||
}
|
||
|
||
func TestFeywild_ResolveDistortion_Buckets(t *testing.T) {
|
||
cases := []struct {
|
||
d6 int
|
||
want FeywildDistortion
|
||
}{
|
||
{1, FeywildDistortionNormal},
|
||
{2, FeywildDistortionNormal},
|
||
{3, FeywildDistortionHalf},
|
||
{4, FeywildDistortionHalf},
|
||
{5, FeywildDistortionDouble},
|
||
{6, FeywildDistortionLoop},
|
||
}
|
||
for _, c := range cases {
|
||
if got := feywildResolveDistortion(c.d6); got != c.want {
|
||
t.Errorf("d6=%d → %q, want %q", c.d6, got, c.want)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestFeywild_HalfDay_HalvesBurn(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-fey-half:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneFeywildCrossing, "",
|
||
ExpeditionSupplies{Current: 30, Max: 30, DailyBurn: 3, HarshMod: 2.5})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
prevRoll := feywildRollFn
|
||
defer func() { feywildRollFn = prevRoll }()
|
||
feywildRollFn = func() int { return 3 } // half
|
||
|
||
startSU := exp.Supplies.Current
|
||
if err := (&AdventurePlugin{}).deliverBriefing(exp, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, _ := getExpedition(exp.ID)
|
||
wantBurn := float32(1.5) // 3 * 0.5
|
||
if startSU-got.Supplies.Current != wantBurn {
|
||
t.Errorf("burn = %v, want %v (half-day)", startSU-got.Supplies.Current, wantBurn)
|
||
}
|
||
if today, _ := got.RegionState["feywild_today"].(string); today != "half" {
|
||
t.Errorf("RegionState feywild_today = %q, want %q", today, "half")
|
||
}
|
||
}
|
||
|
||
func TestFeywild_DoubleDay_DoublesBurn(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-fey-double:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneFeywildCrossing, "",
|
||
ExpeditionSupplies{Current: 30, Max: 30, DailyBurn: 3, HarshMod: 2.5})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
prevRoll := feywildRollFn
|
||
defer func() { feywildRollFn = prevRoll }()
|
||
feywildRollFn = func() int { return 5 }
|
||
|
||
startSU := exp.Supplies.Current
|
||
if err := (&AdventurePlugin{}).deliverBriefing(exp, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, _ := getExpedition(exp.ID)
|
||
wantBurn := float32(6.0)
|
||
if startSU-got.Supplies.Current != wantBurn {
|
||
t.Errorf("burn = %v, want %v (double-day)", startSU-got.Supplies.Current, wantBurn)
|
||
}
|
||
}
|
||
|
||
func TestFeywild_TimeLoop_LogsAndDoesNotChangeBurn(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-fey-loop:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneFeywildCrossing, "",
|
||
ExpeditionSupplies{Current: 30, Max: 30, DailyBurn: 3, HarshMod: 2.5})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
prevRoll := feywildRollFn
|
||
defer func() { feywildRollFn = prevRoll }()
|
||
feywildRollFn = func() int { return 6 }
|
||
|
||
startSU := exp.Supplies.Current
|
||
if err := (&AdventurePlugin{}).deliverBriefing(exp, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, _ := getExpedition(exp.ID)
|
||
// Loop has no burn override → default pipeline (no harsh, no siege) = 3 SU.
|
||
if startSU-got.Supplies.Current != 3 {
|
||
t.Errorf("burn = %v, want 3 (loop default)", startSU-got.Supplies.Current)
|
||
}
|
||
entries, _ := recentExpeditionLog(exp.ID, 10)
|
||
found := false
|
||
for _, e := range entries {
|
||
if e.Type == "temporal" && strings.Contains(e.Summary, "time loop") {
|
||
found = true
|
||
}
|
||
}
|
||
if !found {
|
||
t.Error("expected a 'time loop' temporal log entry")
|
||
}
|
||
}
|
||
|
||
func TestFeywild_NormalDay_NoTemporalLog(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-fey-norm:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneFeywildCrossing, "",
|
||
ExpeditionSupplies{Current: 30, Max: 30, DailyBurn: 3, HarshMod: 2.5})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
prevRoll := feywildRollFn
|
||
defer func() { feywildRollFn = prevRoll }()
|
||
feywildRollFn = func() int { return 1 }
|
||
|
||
if err := (&AdventurePlugin{}).deliverBriefing(exp, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
entries, _ := recentExpeditionLog(exp.ID, 10)
|
||
for _, e := range entries {
|
||
if e.Type == "temporal" {
|
||
t.Errorf("normal-day distortion should not log temporal; got %+v", e)
|
||
}
|
||
}
|
||
}
|
||
|
||
func TestDragonsLair_AwarenessPulseFiresEveryThreeDays(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-dragon-pulse:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneDragonsLair, "",
|
||
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 4, HarshMod: 3})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
p := &AdventurePlugin{}
|
||
|
||
// Roll forward to Day 3 — first awareness pulse.
|
||
setExpDay(t, exp.ID, 2)
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET last_briefing_at = NULL, supplies_json = ? WHERE expedition_id = ?`,
|
||
`{"current":99,"max":99,"daily_burn":4,"harsh_mod":3,"foraged_today":false,"packs_standard":3,"packs_deluxe":0}`,
|
||
exp.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fresh, _ := getExpedition(exp.ID)
|
||
prevThreat := fresh.ThreatLevel
|
||
if err := p.deliverBriefing(fresh, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, _ := getExpedition(exp.ID)
|
||
// +10 awareness pulse + ~3 daily threat drift = 13 (mod GMMood 50 = neutral).
|
||
if got.ThreatLevel < prevThreat+10 {
|
||
t.Errorf("threat = %d, want ≥ %d (awareness pulse +10)", got.ThreatLevel, prevThreat+10)
|
||
}
|
||
|
||
// Verify pulse log entry.
|
||
rows, _ := db.Get().Query(
|
||
`SELECT summary FROM dnd_expedition_log WHERE expedition_id = ? AND entry_type = 'temporal' ORDER BY entry_id`,
|
||
exp.ID)
|
||
defer rows.Close()
|
||
foundPulse := false
|
||
for rows.Next() {
|
||
var s string
|
||
_ = rows.Scan(&s)
|
||
if strings.Contains(s, "awareness pulse") {
|
||
foundPulse = true
|
||
}
|
||
}
|
||
if !foundPulse {
|
||
t.Error("expected an 'awareness pulse' temporal log entry")
|
||
}
|
||
}
|
||
|
||
func TestDragonsLair_NonPulseDay_NoThreatJump(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-dragon-nonpulse:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneDragonsLair, "",
|
||
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 4, HarshMod: 3})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
setExpDay(t, exp.ID, 1) // → Day 2; not a pulse day.
|
||
fresh, _ := getExpedition(exp.ID)
|
||
prev := fresh.ThreatLevel
|
||
if err := (&AdventurePlugin{}).deliverBriefing(fresh, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, _ := getExpedition(exp.ID)
|
||
// Only the +3 daily drift should apply.
|
||
if got.ThreatLevel-prev > 5 {
|
||
t.Errorf("non-pulse day threat jump = %d, want ≤ 5", got.ThreatLevel-prev)
|
||
}
|
||
}
|
||
|
||
func TestDragonsLair_Day14_Awakens(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-dragon-wake:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneDragonsLair, "",
|
||
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 4, HarshMod: 3})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
setExpDay(t, exp.ID, 13) // → Day 14
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET supplies_json = ? WHERE expedition_id = ?`,
|
||
`{"current":99,"max":99,"daily_burn":4,"harsh_mod":3,"foraged_today":false,"packs_standard":3,"packs_deluxe":0}`,
|
||
exp.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fresh, _ := getExpedition(exp.ID)
|
||
if err := (&AdventurePlugin{}).deliverBriefing(fresh, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, _ := getExpedition(exp.ID)
|
||
if awake, _ := got.RegionState["infernax_awake"].(bool); !awake {
|
||
t.Error("infernax_awake should be true on Day 14")
|
||
}
|
||
|
||
// Verify both Awaken (and possibly pulse) log entries fired.
|
||
rows, _ := db.Get().Query(
|
||
`SELECT summary FROM dnd_expedition_log WHERE expedition_id = ? AND entry_type = 'temporal'`,
|
||
exp.ID)
|
||
defer rows.Close()
|
||
foundAwaken := false
|
||
for rows.Next() {
|
||
var s string
|
||
_ = rows.Scan(&s)
|
||
if strings.Contains(s, "infernax awakens") {
|
||
foundAwaken = true
|
||
}
|
||
}
|
||
if !foundAwaken {
|
||
t.Error("expected 'infernax awakens' temporal entry")
|
||
}
|
||
}
|
||
|
||
func TestDragonsLair_BossDefeatedSilencesPulses(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-dragon-boss:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneDragonsLair, "",
|
||
ExpeditionSupplies{Current: 60, Max: 60, DailyBurn: 4, HarshMod: 3})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
if _, err := db.Get().Exec(
|
||
`UPDATE dnd_expedition SET boss_defeated = 1, current_day = 5 WHERE expedition_id = ?`,
|
||
exp.ID); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
fresh, _ := getExpedition(exp.ID)
|
||
prev := fresh.ThreatLevel
|
||
if err := (&AdventurePlugin{}).deliverBriefing(fresh, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, _ := getExpedition(exp.ID)
|
||
if got.ThreatLevel != prev {
|
||
t.Errorf("post-boss threat changed: %d → %d", prev, got.ThreatLevel)
|
||
}
|
||
}
|
||
|
||
func TestSunkenTemple_NoTidalOnNonZone(t *testing.T) {
|
||
setupZoneRunTestDB(t)
|
||
uid := id.UserID("@exp-tidal-other:example")
|
||
defer cleanupExpeditions(uid)
|
||
|
||
exp, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
||
ExpeditionSupplies{Current: 20, Max: 20, DailyBurn: 1, HarshMod: 1})
|
||
if err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
setExpDay(t, exp.ID, 5)
|
||
exp, _ = getExpedition(exp.ID)
|
||
|
||
p := &AdventurePlugin{}
|
||
startSU := exp.Supplies.Current
|
||
if err := p.deliverBriefing(exp, time.Now().UTC()); err != nil {
|
||
t.Fatal(err)
|
||
}
|
||
got, _ := getExpedition(exp.ID)
|
||
if got.Supplies.Current != startSU-1 {
|
||
t.Errorf("non-tidal zone burned %v, want 1", startSU-got.Supplies.Current)
|
||
}
|
||
}
|