mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 16:42:41 +00:00
Wires the basic camp surface per gogobee_expedition_system.md §5. Two camp types ship: rough (any location, +0.5 SU, partial rest) and standard (cleared room, +1 SU, full long rest). Fortified and base camps remain explicitly rejected at the surface — those wire up in E2 and E4 respectively, so the command tree is forward-compatible. Placement validation per §5.2: boss rooms and trap rooms reject; non- cleared rooms downgrade an intended standard camp to rough. Active- enemy detection is deferred to E2 (it needs the combat-state hooks that don't exist on the expedition path yet). Until !advance is wired into the expedition layer, the player is treated as standing at the entry room (always cleared), so today's validation effectively only fires once room context catches up. Camp-pitch consumes SU immediately and uses flavor.CampEstablished for the narrative line. !camp break struck and recorded. !camp with no arg shows help + current camp state. Tests cover deduction amounts, double-pitch rejection, break, locked- type rejection (fortified/base), insufficient-supply rejection, and boss-room placement guard via a synthetic zone run. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
213 lines
6.0 KiB
Go
213 lines
6.0 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_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 advance current_room to the boss room.
|
|
run, err := startZoneRun(uid, ZoneGoblinWarrens, 1, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
// Walk to the boss room directly (last index).
|
|
bossIdx := -1
|
|
for i, rt := range run.RoomSeq {
|
|
if rt == RoomBoss {
|
|
bossIdx = i
|
|
}
|
|
}
|
|
if bossIdx < 0 {
|
|
t.Fatal("no boss room in seq")
|
|
}
|
|
exp := &Expedition{RunID: run.RunID}
|
|
// Force current_room to bossIdx.
|
|
if _, err := db.Get().Exec(`UPDATE dnd_zone_run SET current_room = ? WHERE run_id = ?`, bossIdx, 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")
|
|
}
|
|
}
|