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
|
// getResumableExpedition returns the most recent 'extracting' row for the
|
||||||
// user, regardless of age (caller checks the 7-day window).
|
// user, regardless of age (caller checks the 7-day window).
|
||||||
func getResumableExpedition(userID id.UserID) (*Expedition, error) {
|
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")
|
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{
|
return advanceResult{
|
||||||
preStream: preStream,
|
preStream: preStream,
|
||||||
intro: intro,
|
intro: intro,
|
||||||
|
|||||||
Reference in New Issue
Block a user