Coop: lock combat actions for the full duration of an active run

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) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-04-28 20:38:00 -07:00
parent c060e13b41
commit 48e5000745
4 changed files with 44 additions and 2 deletions

View File

@@ -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)

View File

@@ -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 := ""

View File

@@ -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)
}

View File

@@ -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)