Coop: per-gift voting timers (6h) — gifts resolve throughout the day

Replace the single daily-tick gift resolution with independent per-gift
expiries. Each gift now has its own 6h voting window; once that elapses,
the votes are tallied, the sender gets DM feedback, and the live game-room
post is edited to reveal the type and resolved modifier. The modifier
sits on the run waiting for the next floor resolution to merge it in.

Effect:
- Gifts fire continuously throughout the day rather than all at once at
  08:00 UTC, surfacing sender activity in real time.
- Senders get fast feedback (~6h instead of waiting for the next daily
  tick).
- Floor resolution becomes purely "sum pending modifiers from already-
  resolved gifts" — cleaner separation of concerns.
- Atomic per-gift apply via markCoopGiftApplied (UPDATE...WHERE applied_at
  IS NULL) prevents double-application on retry.

Schema additions:
- coop_dungeon_gifts.expires_at (when voting closes)
- coop_dungeon_gifts.applied_at (when modifier merged into a floor)
- Migration entries provided.

Defensive backstop: floor resolution still force-resolves any gifts whose
expiry was missed (e.g., bot down during the timer window).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-04-28 17:55:14 -07:00
parent 4bc541ca85
commit 9f094549b7
4 changed files with 296 additions and 80 deletions

View File

@@ -30,6 +30,11 @@ func (p *AdventurePlugin) coopTicker() {
// Locks: every minute.
p.coopProcessLocks()
// Gift expiries: every minute. Resolves any gift whose individual
// 6h voting window has elapsed. Modifier sits on the run until the
// next floor resolution merges it.
p.coopProcessGiftExpiries()
// Resolutions: once per day at morningHour:00 UTC.
now := time.Now().UTC()
if now.Hour() != p.morningHour || now.Minute() != 0 {