From 48e5000745449937d1eabdb64bdc5ed0e1e68f92 Mon Sep 17 00:00:00 2001 From: prosolis <5590409+prosolis@users.noreply.github.com> Date: Tue, 28 Apr 2026 20:38:00 -0700 Subject: [PATCH] Coop: lock combat actions for the full duration of an active run MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Players in a co-op were getting their combat action back every midnight reset and offered the dungeon option in the morning DM. Per spec they should be "off in the Co-op" and unable to also solo-grind dungeons. Fix at the DB layer (so all CanDoCombat() paths agree): - New helper lockCoopCombatActions() sets combat_actions_used=99 for any user in an active coop run. Called immediately after every resetAllAdvDailyActions() โ€” at startup and at midnight. - Render layer: morning DM now shows "combat locked (in Co-op #N, day X/Y)" and replaces the dungeon section with an explanatory line pointing them at !coop status. Combat returns to normal automatically once the run completes/wipes โ€” the next midnight reset finds status != 'active' and skips the lock. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/plugin/adventure.go | 3 +++ internal/plugin/adventure_render.go | 19 +++++++++++++++++-- internal/plugin/adventure_scheduler.go | 7 +++++++ internal/plugin/coop_dungeon_db.go | 17 +++++++++++++++++ 4 files changed, 44 insertions(+), 2 deletions(-) diff --git a/internal/plugin/adventure.go b/internal/plugin/adventure.go index bff3278..609334a 100644 --- a/internal/plugin/adventure.go +++ b/internal/plugin/adventure.go @@ -128,6 +128,9 @@ func (p *AdventurePlugin) Init() error { if err := resetAllAdvDailyActions(); err != nil { slog.Error("adventure: startup daily reset failed", "err", err) } + if err := lockCoopCombatActions(); err != nil { + slog.Error("adventure: startup coop combat lock failed", "err", err) + } // Revive any characters whose DeadUntil has expired p.catchUpRespawns(chars) diff --git a/internal/plugin/adventure_render.go b/internal/plugin/adventure_render.go index d2a246b..9fe96c6 100644 --- a/internal/plugin/adventure_render.go +++ b/internal/plugin/adventure_render.go @@ -283,11 +283,26 @@ func renderAdvMorningDM(char *AdventureCharacter, equip map[EquipmentSlot]*AdvEq harvestMax++ } combatLeft := combatMax - char.CombatActionsUsed + if combatLeft < 0 { + combatLeft = 0 + } harvestLeft := harvestMax - char.HarvestActionsUsed - sb.WriteString(fmt.Sprintf("๐Ÿ“‹ **Actions:** %d/%d combat ยท %d/%d harvest\n\n", combatLeft, combatMax, harvestLeft, harvestMax)) + + // Co-op participants have combat locked for the duration of the run. + coopRun, _ := loadCoopRunForUser(char.UserID) + inCoop := coopRun != nil && coopRun.Status == "active" + + if inCoop { + sb.WriteString(fmt.Sprintf("๐Ÿ“‹ **Actions:** combat locked (in Co-op #%d, day %d/%d) ยท %d/%d harvest\n\n", + coopRun.ID, coopRun.CurrentDay, coopRun.TotalDays, harvestLeft, harvestMax)) + } else { + sb.WriteString(fmt.Sprintf("๐Ÿ“‹ **Actions:** %d/%d combat ยท %d/%d harvest\n\n", combatLeft, combatMax, harvestLeft, harvestMax)) + } // Location choices - if char.CanDoCombat(isHol) { + if inCoop { + sb.WriteString(fmt.Sprintf("**1๏ธโƒฃ Dungeon:** _(off in the Co-op, no solo combat โ€” `!coop status` for run state)_\n")) + } else if char.CanDoCombat(isHol) { sb.WriteString("**1๏ธโƒฃ Dungeon:**\n") for _, el := range advEligibleLocations(char, equip, AdvActivityDungeon, bonuses) { warn := "" diff --git a/internal/plugin/adventure_scheduler.go b/internal/plugin/adventure_scheduler.go index 77fa2b5..dd9bae3 100644 --- a/internal/plugin/adventure_scheduler.go +++ b/internal/plugin/adventure_scheduler.go @@ -422,6 +422,13 @@ func (p *AdventurePlugin) midnightReset() error { slog.Warn("adventure: daily action reset failed, retrying", "attempt", attempt+1, "err", resetErr) time.Sleep(time.Duration(attempt+1) * 2 * time.Second) } + if resetErr == nil { + // Re-lock combat for active co-op participants after the universal + // reset would otherwise have given them a fresh combat action. + if err := lockCoopCombatActions(); err != nil { + slog.Error("adventure: post-reset coop combat lock failed", "err", err) + } + } if resetErr != nil { return fmt.Errorf("reset daily actions after 3 attempts: %w", resetErr) } diff --git a/internal/plugin/coop_dungeon_db.go b/internal/plugin/coop_dungeon_db.go index 38a60a5..3543f63 100644 --- a/internal/plugin/coop_dungeon_db.go +++ b/internal/plugin/coop_dungeon_db.go @@ -172,6 +172,23 @@ func completeCoopRun(runID int, status string, reward int) error { return err } +// lockCoopCombatActions sets combat_actions_used to a value that exceeds the +// holiday cap on every active co-op participant. Called after the midnight +// reset (which would otherwise zero them) and at bot startup. Combat stays +// locked for the full duration of the run โ€” players in a co-op cannot also +// solo-grind dungeons or arena. +func lockCoopCombatActions() error { + d := db.Get() + _, err := d.Exec(`UPDATE adventure_characters + SET combat_actions_used = 99 + WHERE user_id IN ( + SELECT m.user_id FROM coop_dungeon_members m + JOIN coop_dungeon_runs r ON r.id = m.run_id + WHERE r.status = 'active' + )`) + return err +} + func setCoopRunInvitePostID(runID int, postID id.EventID) error { d := db.Get() _, err := d.Exec(`UPDATE coop_dungeon_runs SET invite_post_id = ? WHERE id = ?`, string(postID), runID)