Add housing/pets/Thom Krooke, wordle overhaul, boost system, backup strategy

Part 3 (Housing, Thom Krooke & Pets):
- Housing system with tiered upgrades, mortgage loans, FRED API rates
- Thom Krooke realtor NPC with !thom commands and !thom pay extra principal
- Pet system with arrival, naming, leveling, combat, morning defense
- Pet ditch recovery scales with level, name validation

Wordle overhaul:
- Remove daily expiration — puzzles persist until solved/failed
- Auto-start new puzzle after solve, fail, or skip
- Sequential puzzle IDs instead of date-based
- Auto-create puzzle on first guess if none active

Adventure fixes:
- Double XP/money boost toggle (!adv boost, admin only)
- Fix ensureCharacter wiping existing data on query errors
- Fix rival RPS format string bug
- Fix daily summary empty data (load today+yesterday logs)
- Fix holdem DM-to-room reply routing
- Fix Robbie payout to 25% of item value
- Add fishing to leaderboard and TwinBee calculations
- Add Thom to morning menu
- Persist mortgage rate across restarts via db.CacheGet/Set

Infrastructure:
- Daily database backup via VACUUM INTO with 7-day retention
- Admin DM notification on backup failure (async)
- Daily NPC house balance reset for holdem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-04-10 16:50:49 -07:00
parent 90865d1c51
commit 76110f61ca
24 changed files with 3298 additions and 166 deletions

View File

@@ -4,6 +4,8 @@ import (
"fmt"
"log/slog"
"math/rand/v2"
"os"
"strings"
"time"
"gogobee/internal/db"
@@ -102,6 +104,19 @@ func (p *AdventurePlugin) sendMorningDMs() {
continue
}
// Pet arrival check (fires before normal morning DM)
if petShouldArrive(&char) {
p.petArrivalDM(char.UserID)
continue
}
// Morning pet event
petEvent := petMorningEvent(&char)
if petEvent != "" {
char.PetMorningDefense = true
_ = saveAdvCharacter(&char)
}
// Send morning DM with choices
equip, err := loadAdvEquipment(char.UserID)
if err != nil {
@@ -119,6 +134,9 @@ func (p *AdventurePlugin) sendMorningDMs() {
holidayLabel = holName
}
text := renderAdvMorningDM(&char, equip, balance, bonuses, holidayLabel)
if petEvent != "" {
text = fmt.Sprintf("🐾 *%s*\n\n%s", petEvent, text)
}
p.advMarkMenuSent(char.UserID)
if err := p.SendDM(char.UserID, text); err != nil {
slog.Error("adventure: failed to send morning DM", "user", char.UserID, "err", err)
@@ -171,7 +189,11 @@ func (p *AdventurePlugin) postDailySummary() {
return
}
todayLogs, _ := loadAdvTodayLogs()
// Load logs for today and yesterday — players may act across the UTC boundary
now := time.Now().UTC()
todayLogs, _ := loadAdvLogsForDate(now.Format("2006-01-02"))
yesterdayLogs, _ := loadAdvLogsForDate(now.AddDate(0, 0, -1).Format("2006-01-02"))
todayLogs = append(todayLogs, yesterdayLogs...)
// Group logs per user — holiday days may produce 2 entries per user
logsPerUser := make(map[id.UserID][]*AdvDayLog)
for i := range todayLogs {
@@ -394,9 +416,39 @@ func (p *AdventurePlugin) midnightReset() error {
// Check babysitting service expirations
p.checkBabysitExpiry(chars)
// Pet supply shop unlock check
p.petMidnightCheck()
// Reset holdem NPC house balance
resetNPCHouseBalance()
// Daily database backup (async to avoid blocking reset if DM sends are slow)
go func() {
if err := db.Backup(); err != nil {
slog.Error("adventure: daily backup failed", "err", err)
p.notifyAdmins(fmt.Sprintf("⚠️ **Daily backup failed:** %v", err))
}
}()
return nil
}
func (p *AdventurePlugin) notifyAdmins(msg string) {
admins := os.Getenv("ADMIN_USERS")
if admins == "" {
return
}
for _, a := range strings.Split(admins, ",") {
uid := id.UserID(strings.TrimSpace(a))
if uid == "" {
continue
}
if err := p.SendDM(uid, msg); err != nil {
slog.Error("adventure: failed to notify admin", "user", uid, "err", err)
}
}
}
// ── Helper ───────────────────────────────────────────────────────────────────
func (p *AdventurePlugin) registerDMRoom(userID id.UserID) {