Adv 2.0 Phase L3: delete co-op dungeons; refund in-flight runs on startup

Drop the coop_dungeon system per migration plan §5. 11 coop_*.go files
removed; cleanup_l3.go added with an idempotent per-run status-guarded
UPDATE that cancels any open/active runs and refunds member contributions
plus unsettled bets via EuroPlugin.Credit. Tables retained for now;
schema purge deferred to a future GOGOBEE_COOP_PURGE pass.

adventure.go drops !coop dispatch, coopTicker, the startup combat-lock
hook, and the !coop help line. adventure_scheduler.go loses the
activeCoopMemberSet skip branch and the post-reset combat-lock call.
adventure_render.go drops the teaser plus the in-coop status block;
replaced with a one-week morning-DM closure announcement
(TODO: remove after 2026-05-16). TestPickCoopTeaserCandidate_SkipsLeader
removed.

go vet ./... and go test ./... clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-05-09 09:17:10 -07:00
parent 3b4dfa44d3
commit d300008812
17 changed files with 133 additions and 4916 deletions

View File

@@ -213,55 +213,6 @@ func renderAdvCharacterSheet(char *AdventureCharacter, equip map[EquipmentSlot]*
// ── Morning DM ───────────────────────────────────────────────────────────────
// renderCoopTeaser surfaces an open co-op run the player could join, so the
// system isn't a footnote in help text. Returns "" when there's nothing to
// surface (no open runs, or the player is already locked into one). When
// runs exist that the player is under-levelled for, we still hint at them
// as a stretch goal — joining as a liability is a real choice.
func renderCoopTeaser(char *AdventureCharacter) string {
runs, err := loadOpenCoopRuns()
if err != nil || len(runs) == 0 {
return ""
}
pick, stretch := pickCoopTeaserCandidate(runs, char)
tag := ""
if pick == nil {
pick = stretch
tag = " *(below recommended level — would join as liability)*"
}
if pick == nil {
return ""
}
leaderName := string(pick.LeaderID)
if c, err := loadAdvCharacter(pick.LeaderID); err == nil && c != nil && c.DisplayName != "" {
leaderName = c.DisplayName
}
def := coopTierTable[pick.Tier]
return fmt.Sprintf("🛡️ **Open Co-op #%d** — Tier %d (%s) led by %s · `!coop join %d`%s",
pick.ID, pick.Tier, def.difficulty, leaderName, pick.ID, tag)
}
// pickCoopTeaserCandidate scans the open coop runs and returns up to two
// picks: a primary (where the player meets the level gate) and a stretch
// (any other run, joined as a liability). Skips runs the player already
// leads. Pure function — testable without DB.
func pickCoopTeaserCandidate(runs []*CoopRun, char *AdventureCharacter) (match, stretch *CoopRun) {
for _, r := range runs {
if r.LeaderID == char.UserID {
continue
}
def := coopTierTable[r.Tier]
if char.CombatLevel >= def.minLevel && match == nil {
match = r
} else if stretch == nil {
stretch = r
}
}
return match, stretch
}
// renderCraftingTeaser surfaces the crafting system before and just after
// the Foraging-10 auto-unlock, so it doesn't stay invisible to players who
// never type !adventure recipes. Returns "" when the teaser shouldn't fire
@@ -379,23 +330,10 @@ func renderAdvMorningDM(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEq
sb.WriteString("\n\n")
}
// Co-op participants have combat locked for the duration of the run.
// Under Adv 2.0 the per-day combat/harvest action caps are no longer
// surfaced — harvesting is gated by per-room nodes, supplies, and the
// threat clock; combat is gated by zones/expeditions/arena themselves.
coopRun, _ := loadCoopRunForUser(char.UserID)
inCoop := coopRun != nil && coopRun.Status == "active"
if inCoop {
sb.WriteString(fmt.Sprintf("📋 **Co-op #%d** — day %d/%d, combat locked for the duration of the run\n\n",
coopRun.ID, coopRun.CurrentDay, coopRun.TotalDays))
} else {
// Co-op teaser — show open runs the player could join.
if line := renderCoopTeaser(char); line != "" {
sb.WriteString(line)
sb.WriteString("\n")
}
}
// L3 closure announcement — surfaced for one-week post-deploy soak so
// active co-op participants see why the system is gone. Remove after
// 2026-05-16 (TODO: drop announcement block at that revisit).
sb.WriteString("📋 **Co-op dungeons closed for now** — `!expedition` is the way forward; a better co-op design is on the way.\n\n")
// Rival nudge — a pending challenge waits for action.
if line := renderRivalNudge(char); line != "" {
@@ -416,9 +354,6 @@ func renderAdvMorningDM(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEq
sb.WriteString("• `!expedition` — overview & open expeditions\n")
sb.WriteString("• `!expedition start <zone>` — begin a new run\n")
sb.WriteString("• `!forage` · `!mine` · `!scavenge` · `!fish` · `!essence` · `!commune` — harvest in cleared rooms\n")
if inCoop {
sb.WriteString(fmt.Sprintf("_(combat is locked while you're in Co-op #%d — `!coop status` for run state)_\n", coopRun.ID))
}
sb.WriteString("\n")
sb.WriteString("**🏘️ In town:**\n")