mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
The two legacy columns are no longer needed at runtime. CurrentRoom is derived from len(VisitedNodes)-1 on read (lockstep with what the dual-write era stored), and RoomSeq is a transient in-memory field populated only when a fresh run is started. - INSERT, SELECT, and UPDATE statements drop the two columns. The schema still carries them — they retire in G9c. - scanZoneRun derives CurrentRoom and falls back to a linear-derived CurrentNode only if a row predates the G4 dual-write deploy. - markRoomCleared rewritten to walk the registered graph (first outgoing edge / dead-end completion). Tests that use it as an end-to-end run driver continue to pass. - advanceZoneRunNode drops its current_room dual-write. - adventure_activity_unified reads visited_nodes for the daily "X/N rooms" progress fragment. - Camp boss-room test pivots to setting current_node directly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
301 lines
8.6 KiB
Go
301 lines
8.6 KiB
Go
package plugin
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"gogobee/internal/db"
|
|
"maunium.net/go/mautrix/id"
|
|
)
|
|
|
|
func campTestCharacter(t *testing.T, uid id.UserID, level int) {
|
|
t.Helper()
|
|
if err := createAdvCharacter(uid, "campcmd"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c := &DnDCharacter{
|
|
UserID: uid, Race: RaceHuman, Class: ClassFighter, Level: level,
|
|
STR: 14, DEX: 12, CON: 14, INT: 10, WIS: 10, CHA: 10,
|
|
HPMax: 20, HPCurrent: 20, ArmorClass: 14,
|
|
}
|
|
if err := SaveDnDCharacter(c); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestCampCmd_NoExpeditionBlocked(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-noexp:example")
|
|
campTestCharacter(t, uid, 1)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
p := &AdventurePlugin{}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "rough"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exp, _ := getActiveExpedition(uid)
|
|
if exp != nil {
|
|
t.Error("no expedition should exist")
|
|
}
|
|
}
|
|
|
|
func TestCampCmd_RoughDeducts0_5SU(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-rough:example")
|
|
campTestCharacter(t, uid, 1)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
if _, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
|
ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1, HarshMod: 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := &AdventurePlugin{}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "rough"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exp, _ := getActiveExpedition(uid)
|
|
if exp.Camp == nil || !exp.Camp.Active {
|
|
t.Fatal("expected camp pitched")
|
|
}
|
|
if exp.Camp.Type != CampTypeRough {
|
|
t.Errorf("camp type = %q, want rough", exp.Camp.Type)
|
|
}
|
|
if exp.Supplies.Current != 4.5 {
|
|
t.Errorf("supplies = %v, want 4.5 after 0.5 SU rough cost", exp.Supplies.Current)
|
|
}
|
|
}
|
|
|
|
func TestCampCmd_StandardDeducts1SU(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-standard:example")
|
|
campTestCharacter(t, uid, 1)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
if _, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
|
ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1, HarshMod: 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := &AdventurePlugin{}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "standard"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exp, _ := getActiveExpedition(uid)
|
|
if exp.Camp == nil || exp.Camp.Type != CampTypeStandard {
|
|
t.Errorf("camp = %+v, want standard", exp.Camp)
|
|
}
|
|
if exp.Supplies.Current != 4.0 {
|
|
t.Errorf("supplies = %v, want 4.0 after 1.0 SU standard cost", exp.Supplies.Current)
|
|
}
|
|
}
|
|
|
|
func TestCampCmd_RejectsDoublePitch(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-double:example")
|
|
campTestCharacter(t, uid, 1)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
if _, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
|
ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1, HarshMod: 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := &AdventurePlugin{}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "rough"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "standard"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exp, _ := getActiveExpedition(uid)
|
|
if exp.Camp == nil || exp.Camp.Type != CampTypeRough {
|
|
t.Errorf("camp should still be rough after rejected second pitch, got %+v", exp.Camp)
|
|
}
|
|
}
|
|
|
|
func TestCampCmd_BreakClearsCamp(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-break:example")
|
|
campTestCharacter(t, uid, 1)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
if _, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
|
ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1, HarshMod: 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := &AdventurePlugin{}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "rough"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "break"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exp, _ := getActiveExpedition(uid)
|
|
if exp.Camp != nil {
|
|
t.Errorf("camp should be cleared, got %+v", exp.Camp)
|
|
}
|
|
}
|
|
|
|
func TestCampCmd_RejectsFortifiedAndBase(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-locked:example")
|
|
campTestCharacter(t, uid, 1)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
if _, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
|
ExpeditionSupplies{Current: 5, Max: 5, DailyBurn: 1, HarshMod: 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := &AdventurePlugin{}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "fortified"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "base"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exp, _ := getActiveExpedition(uid)
|
|
if exp.Camp != nil {
|
|
t.Errorf("camp should not be pitched for locked types, got %+v", exp.Camp)
|
|
}
|
|
}
|
|
|
|
func TestCampCmd_BaseRejectsNonBaseCampSiteRegion(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-base-bad-site:example")
|
|
campTestCharacter(t, uid, 12)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
exp, err := startExpedition(uid, ZoneUnderdark, "",
|
|
ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Surface Tunnels has BaseCampSite=false. Even if we mark it
|
|
// cleared, !camp base should reject.
|
|
if _, err := MarkRegionBossDefeated(exp, "underdark_surface_tunnels"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := &AdventurePlugin{}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "base"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, _ := getActiveExpedition(uid)
|
|
if loaded.Camp != nil {
|
|
t.Error("base camp should not pitch in non-base-camp-site region")
|
|
}
|
|
}
|
|
|
|
func TestCampCmd_BaseRejectsUnclearedRegion(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-base-uncleared:example")
|
|
campTestCharacter(t, uid, 12)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
exp, err := startExpedition(uid, ZoneUnderdark, "",
|
|
ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Move to Drow Outpost (BaseCampSite=true) but don't clear it.
|
|
if err := setCurrentRegion(exp, "underdark_drow_outpost"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := &AdventurePlugin{}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "base"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, _ := getActiveExpedition(uid)
|
|
if loaded.Camp != nil {
|
|
t.Error("base camp should not pitch before region boss is cleared")
|
|
}
|
|
}
|
|
|
|
func TestCampCmd_BasePitchPersistsWaypoint(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-base-pitch:example")
|
|
campTestCharacter(t, uid, 12)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
exp, err := startExpedition(uid, ZoneUnderdark, "",
|
|
ExpeditionSupplies{Current: 10, Max: 10, DailyBurn: 1, HarshMod: 1})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := setCurrentRegion(exp, "underdark_drow_outpost"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := MarkRegionBossDefeated(exp, "underdark_drow_outpost"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
p := &AdventurePlugin{}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "base"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
loaded, _ := getActiveExpedition(uid)
|
|
if loaded.Camp == nil || loaded.Camp.Type != CampTypeBase {
|
|
t.Fatalf("expected base camp pitched; got %+v", loaded.Camp)
|
|
}
|
|
if loaded.Supplies.Current != 7 { // 10 - 3 SU base cost
|
|
t.Errorf("supplies after base pitch = %v, want 7", loaded.Supplies.Current)
|
|
}
|
|
if !HasBaseCampAt(loaded, "underdark_drow_outpost") {
|
|
t.Error("base camp waypoint not persisted to RegionState")
|
|
}
|
|
}
|
|
|
|
func TestCampCmd_InsufficientSuppliesRejected(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-broke:example")
|
|
campTestCharacter(t, uid, 1)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
if _, err := startExpedition(uid, ZoneGoblinWarrens, "",
|
|
ExpeditionSupplies{Current: 0.3, Max: 5, DailyBurn: 1, HarshMod: 1}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
p := &AdventurePlugin{}
|
|
if err := p.handleCampCmd(MessageContext{Sender: uid}, "rough"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
exp, _ := getActiveExpedition(uid)
|
|
if exp.Camp != nil {
|
|
t.Errorf("camp should not be pitched with 0.3 SU vs 0.5 cost, got %+v", exp.Camp)
|
|
}
|
|
}
|
|
|
|
func TestCampLocationCheck_BossRoomBlocks(t *testing.T) {
|
|
setupZoneRunTestDB(t)
|
|
uid := id.UserID("@camp-boss:example")
|
|
defer cleanupZoneRuns(uid)
|
|
defer cleanupExpeditions(uid)
|
|
|
|
// Pre-create a zone run, then warp current_node to the graph's boss.
|
|
run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
g, ok := loadZoneGraph(run.ZoneID)
|
|
if !ok {
|
|
t.Fatalf("no graph for zone %q", run.ZoneID)
|
|
}
|
|
bossNodeID := ""
|
|
for nid, n := range g.Nodes {
|
|
if n.IsBoss {
|
|
bossNodeID = nid
|
|
break
|
|
}
|
|
}
|
|
if bossNodeID == "" {
|
|
t.Fatal("no boss node in graph")
|
|
}
|
|
exp := &Expedition{RunID: run.RunID}
|
|
if _, err := db.Get().Exec(`UPDATE dnd_zone_run SET current_node = ? WHERE run_id = ?`, bossNodeID, run.RunID); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cleared, problem := campLocationCheck(exp)
|
|
if problem == "" {
|
|
t.Error("expected boss-room rejection")
|
|
}
|
|
if cleared {
|
|
t.Error("boss room should not read as cleared")
|
|
}
|
|
}
|