mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Restore expedition completion on zone clear (lost in merge)
finalizeExpeditionOnZoneClear and its wiring into the run-complete seam were introduced in 73b7809 but that commit never made it into the current history — it's not an ancestor of HEAD, so the function, its call site in advanceOnceWithOpts, and its tests were all absent. Symptom: clearing a zone (boss down, no outgoing edges) set the run's boss_defeated flag but never flipped the wrapping expedition to 'complete' or retired the run. The expedition sat 'active' pointing at a boss_defeated run, so getActiveZoneRun (filters boss_defeated=0) returned nil and the next '!expedition run' errored with 'No active zone run'. The ambient ticker also kept DMing about a finished dungeon. Re-apply the bridge verbatim against current HEAD (deps verified present: IsMultiRegionZone / CurrentRegion / MarkRegionBossDefeated / completeExpedition / retireAllRegionRuns / AwardCompletionMilestones) and restore the test.
This commit is contained in:
@@ -110,6 +110,66 @@ func forceExtractExpeditionForRunLoss(userID id.UserID, reason string) {
|
||||
}
|
||||
}
|
||||
|
||||
// finalizeExpeditionOnZoneClear bridges the run-complete seam (boss down,
|
||||
// no outgoing edges) into expedition completion — the success-path twin of
|
||||
// forceExtractExpeditionForRunLoss. When the cleared run is the active
|
||||
// expedition's current run AND the clear finishes the whole zone (a
|
||||
// single-region zone, or the zone-boss region of a multi-region zone), it
|
||||
// flips the expedition to 'complete', records boss_defeated, and awards
|
||||
// completion milestones. Returns the rendered milestone lines for the caller
|
||||
// to append to the run-complete message.
|
||||
//
|
||||
// No-op (nil) for standalone runs and for mid-zone region clears, which
|
||||
// leave the expedition active so inter-region travel can continue. Without
|
||||
// this, a cleared zone leaves the expedition 'active' forever and the
|
||||
// ambient ticker keeps DMing about a dungeon the player already finished.
|
||||
func (p *AdventurePlugin) finalizeExpeditionOnZoneClear(userID id.UserID, runID string) []string {
|
||||
exp, err := getActiveExpedition(userID)
|
||||
if err != nil || exp == nil {
|
||||
return nil
|
||||
}
|
||||
if exp.RunID != runID {
|
||||
return nil // the completed run isn't this expedition's current run
|
||||
}
|
||||
|
||||
if IsMultiRegionZone(exp.ZoneID) {
|
||||
region, ok := CurrentRegion(exp)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if _, err := MarkRegionBossDefeated(exp, region.ID); err != nil {
|
||||
slog.Warn("expedition: mark region boss defeated",
|
||||
"user", userID, "expedition", exp.ID, "region", region.ID, "err", err)
|
||||
}
|
||||
if !region.IsZoneBoss {
|
||||
return nil // region cleared; expedition continues to the next region
|
||||
}
|
||||
} else {
|
||||
// Single-region zone: there's no region registry to flip through
|
||||
// MarkRegionBossDefeated, so set the zone-level flag directly.
|
||||
exp.BossDefeated = true
|
||||
if _, err := db.Get().Exec(`
|
||||
UPDATE dnd_expedition
|
||||
SET boss_defeated = 1,
|
||||
last_activity = CURRENT_TIMESTAMP
|
||||
WHERE expedition_id = ?`, exp.ID); err != nil {
|
||||
slog.Warn("expedition: set boss defeated on zone clear",
|
||||
"user", userID, "expedition", exp.ID, "err", err)
|
||||
}
|
||||
}
|
||||
|
||||
// completeExpedition must run before AwardCompletionMilestones — the
|
||||
// latter gates on status == 'complete'.
|
||||
if err := completeExpedition(exp.ID, ExpeditionStatusComplete); err != nil {
|
||||
slog.Warn("expedition: complete on zone clear",
|
||||
"user", userID, "expedition", exp.ID, "err", err)
|
||||
return nil
|
||||
}
|
||||
exp.Status = ExpeditionStatusComplete
|
||||
_ = retireAllRegionRuns(exp)
|
||||
return p.AwardCompletionMilestones(exp, false)
|
||||
}
|
||||
|
||||
// getResumableExpedition returns the most recent 'extracting' row for the
|
||||
// user, regardless of age (caller checks the 7-day window).
|
||||
func getResumableExpedition(userID id.UserID) (*Expedition, error) {
|
||||
|
||||
116
internal/plugin/dnd_expedition_zone_clear_test.go
Normal file
116
internal/plugin/dnd_expedition_zone_clear_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package plugin
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
// Single-region zone: clearing the run completes the wrapping expedition
|
||||
// and flips BossDefeated.
|
||||
func TestFinalizeExpeditionOnZoneClear_SingleRegionCompletes(t *testing.T) {
|
||||
setupZoneRunTestDB(t)
|
||||
uid := id.UserID("@zclear-single:example.org")
|
||||
defer cleanupExpeditions(uid)
|
||||
|
||||
exp, err := startExpedition(uid, ZoneSunkenTemple, "run-1",
|
||||
ExpeditionSupplies{Max: 10, Current: 10, DailyBurn: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if exp.RunID != "run-1" {
|
||||
t.Fatalf("setup: RunID = %q", exp.RunID)
|
||||
}
|
||||
|
||||
p := &AdventurePlugin{}
|
||||
p.finalizeExpeditionOnZoneClear(uid, "run-1")
|
||||
|
||||
if act, _ := getActiveExpedition(uid); act != nil {
|
||||
t.Fatalf("expedition still active after zone clear: status=%s", act.Status)
|
||||
}
|
||||
loaded, _ := getExpedition(exp.ID)
|
||||
if loaded.Status != ExpeditionStatusComplete {
|
||||
t.Errorf("status = %q, want %q", loaded.Status, ExpeditionStatusComplete)
|
||||
}
|
||||
if !loaded.BossDefeated {
|
||||
t.Error("BossDefeated not set after single-region zone clear")
|
||||
}
|
||||
}
|
||||
|
||||
// A run that isn't the expedition's current run must not complete it.
|
||||
func TestFinalizeExpeditionOnZoneClear_RunMismatchNoop(t *testing.T) {
|
||||
setupZoneRunTestDB(t)
|
||||
uid := id.UserID("@zclear-mismatch:example.org")
|
||||
defer cleanupExpeditions(uid)
|
||||
|
||||
exp, err := startExpedition(uid, ZoneSunkenTemple, "run-1",
|
||||
ExpeditionSupplies{Max: 10, Current: 10, DailyBurn: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p := &AdventurePlugin{}
|
||||
p.finalizeExpeditionOnZoneClear(uid, "some-other-run")
|
||||
|
||||
loaded, _ := getExpedition(exp.ID)
|
||||
if loaded.Status != ExpeditionStatusActive {
|
||||
t.Errorf("status = %q, want active (mismatched run should be no-op)", loaded.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-region zone: clearing a non-boss region marks it cleared but leaves
|
||||
// the expedition active so inter-region travel can continue.
|
||||
func TestFinalizeExpeditionOnZoneClear_MidZoneRegionStaysActive(t *testing.T) {
|
||||
setupZoneRunTestDB(t)
|
||||
uid := id.UserID("@zclear-midzone:example.org")
|
||||
defer cleanupExpeditions(uid)
|
||||
|
||||
// CurrentRegion defaults to the first region (underdark_surface_tunnels,
|
||||
// not the zone boss).
|
||||
exp, err := startExpedition(uid, ZoneUnderdark, "run-1",
|
||||
ExpeditionSupplies{Max: 10, Current: 10, DailyBurn: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p := &AdventurePlugin{}
|
||||
p.finalizeExpeditionOnZoneClear(uid, "run-1")
|
||||
|
||||
loaded, _ := getExpedition(exp.ID)
|
||||
if loaded.Status != ExpeditionStatusActive {
|
||||
t.Errorf("status = %q, want active after non-boss region clear", loaded.Status)
|
||||
}
|
||||
if !IsRegionCleared(loaded, "underdark_surface_tunnels") {
|
||||
t.Error("first region not marked cleared")
|
||||
}
|
||||
if loaded.BossDefeated {
|
||||
t.Error("BossDefeated set on a non-boss region clear")
|
||||
}
|
||||
}
|
||||
|
||||
// Multi-region zone: clearing the zone-boss region completes the expedition.
|
||||
func TestFinalizeExpeditionOnZoneClear_ZoneBossRegionCompletes(t *testing.T) {
|
||||
setupZoneRunTestDB(t)
|
||||
uid := id.UserID("@zclear-zoneboss:example.org")
|
||||
defer cleanupExpeditions(uid)
|
||||
|
||||
exp, err := startExpedition(uid, ZoneUnderdark, "run-1",
|
||||
ExpeditionSupplies{Max: 10, Current: 10, DailyBurn: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := setCurrentRegion(exp, "underdark_deep_throne"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
p := &AdventurePlugin{}
|
||||
p.finalizeExpeditionOnZoneClear(uid, "run-1")
|
||||
|
||||
loaded, _ := getExpedition(exp.ID)
|
||||
if loaded.Status != ExpeditionStatusComplete {
|
||||
t.Errorf("status = %q, want complete after zone-boss region clear", loaded.Status)
|
||||
}
|
||||
if !loaded.BossDefeated {
|
||||
t.Error("BossDefeated not set after zone-boss region clear")
|
||||
}
|
||||
}
|
||||
@@ -604,6 +604,16 @@ func (p *AdventurePlugin) advanceOnceWithOpts(ctx MessageContext, compact bool)
|
||||
b.WriteString("• " + id + "\n")
|
||||
}
|
||||
}
|
||||
// Success-path expedition close-out: flip the wrapping expedition to
|
||||
// 'complete' (when this clear finishes the whole zone) and surface any
|
||||
// completion milestones. No-op for standalone runs / mid-zone region
|
||||
// clears.
|
||||
if lines := p.finalizeExpeditionOnZoneClear(ctx.Sender, run.RunID); len(lines) > 0 {
|
||||
b.WriteString("\n")
|
||||
for _, line := range lines {
|
||||
b.WriteString(line)
|
||||
}
|
||||
}
|
||||
return advanceResult{
|
||||
preStream: preStream,
|
||||
intro: intro,
|
||||
|
||||
Reference in New Issue
Block a user