diff --git a/internal/plugin/adventure.go b/internal/plugin/adventure.go index d4edadf..73804f5 100644 --- a/internal/plugin/adventure.go +++ b/internal/plugin/adventure.go @@ -175,6 +175,7 @@ func (p *AdventurePlugin) Init() error { // have a player_meta row. Idempotent. Required for DBs that didn't go // through the L4-L5h dual-write soak (fresh deploys, restored backups). bootstrapPlayerMetaFromLegacy() + bootstrapRestoreExpeditionStreakDecay() // Rehydrate DM room mappings for existing characters chars, err := loadAllAdvCharacters() diff --git a/internal/plugin/adventure_scheduler.go b/internal/plugin/adventure_scheduler.go index 1fd9f9e..2454880 100644 --- a/internal/plugin/adventure_scheduler.go +++ b/internal/plugin/adventure_scheduler.go @@ -98,6 +98,17 @@ func (p *AdventurePlugin) sendMorningDMs() { continue } + // Active expedition: the expedition cycle delivers its own morning + // briefing at 06:00 UTC (deliverBriefing). The legacy overworld + // morning DM is irrelevant — and confusing — while underground. + if char.Alive { + if exp, err := getActiveExpedition(char.UserID); err != nil { + slog.Warn("adventure: failed to check active expedition for morning DM", "user", char.UserID, "err", err) + } else if exp != nil { + continue + } + } + // If still dead, send death status if !char.Alive { text := renderAdvDeathStatusDM(char.UserID) @@ -375,6 +386,27 @@ func (p *AdventurePlugin) midnightReset() error { continue } + // Active expedition counts as activity. The expedition system tracks + // its own action flow (zone/harvest/combat/transit/extract) and never + // touches the legacy CombatActionsUsed/HarvestActionsUsed counters, so + // HasActedToday() reports false for expeditioners. Treat them like the + // acted-today branch below: advance the streak and bail out. + if exp, err := getActiveExpedition(char.UserID); err != nil { + slog.Warn("adventure: failed to check active expedition for idle reaper", "user", char.UserID, "err", err) + } else if exp != nil { + if char.LastActionDate == yesterday || char.LastActionDate == today { + char.CurrentStreak++ + } else { + char.CurrentStreak = 1 + } + if char.CurrentStreak > char.BestStreak { + char.BestStreak = char.CurrentStreak + } + char.LastActionDate = today + _ = saveAdvCharacter(&char) + continue + } + // Jitter between DMs to avoid Matrix rate limits if dmsSent > 0 { time.Sleep(time.Duration(1000+rand.IntN(2000)) * time.Millisecond) diff --git a/internal/plugin/bootstrap_streak_restore.go b/internal/plugin/bootstrap_streak_restore.go new file mode 100644 index 0000000..f3e0a47 --- /dev/null +++ b/internal/plugin/bootstrap_streak_restore.go @@ -0,0 +1,69 @@ +package plugin + +import ( + "log/slog" + + "gogobee/internal/db" +) + +// bootstrapRestoreExpeditionStreakDecay reverses the one-time streak halving +// applied by midnightReset to players who were on an active expedition. The +// idle reaper used to ignore expedition activity (it only inspected the +// legacy CombatActionsUsed/HarvestActionsUsed counters), so expeditioners +// were shamed and had their streak divided by 2. +// +// Restoration: CurrentStreak doubles (recovering the integer-divide), capped +// at BestStreak. The odd-half is recovered by adding 1 when BestStreak +// allows — pre-decay value was either CurrentStreak*2 or CurrentStreak*2+1, +// and BestStreak is a monotonic ceiling that bounds the original. +// +// Idempotent: gated on a fixed job key so it runs at most once per DB. +func bootstrapRestoreExpeditionStreakDecay() { + const jobName = "idle_reaper_expedition_streak_restore_v1" + if db.JobCompleted(jobName, "once") { + return + } + + chars, err := loadAllAdvCharacters() + if err != nil { + slog.Error("bootstrap: streak restore — failed to load characters", "err", err) + return + } + + restored := 0 + for _, char := range chars { + if !char.StreakDecayed || char.CurrentStreak == 0 { + continue + } + exp, err := getActiveExpedition(char.UserID) + if err != nil { + slog.Warn("bootstrap: streak restore — expedition lookup failed", "user", char.UserID, "err", err) + continue + } + if exp == nil { + continue + } + + old := char.CurrentStreak + candidate := old*2 + 1 + if char.BestStreak > 0 && candidate > char.BestStreak { + candidate = char.BestStreak + } + if candidate < old*2 { + candidate = old * 2 + } + char.CurrentStreak = candidate + char.StreakDecayed = false + if err := saveAdvCharacter(&char); err != nil { + slog.Error("bootstrap: streak restore — save failed", "user", char.UserID, "err", err) + continue + } + slog.Info("bootstrap: streak restored", "user", char.UserID, "old", old, "new", candidate, "best", char.BestStreak) + restored++ + } + + db.MarkJobCompleted(jobName, "once") + if restored > 0 { + slog.Warn("bootstrap: expedition streak decay restored", "count", restored) + } +}