Add rival duels, babysitting service, multilingual !define, economy tuning

Rival System:
- RPS-based duels between combat level 5+ players (best of 3, ties re-prompt)
- Stakes scale with level, split 50/50 between winner and community pot
- 3-4 day randomized challenge interval, 7-day same-pair cooldown
- 24h response window with auto-forfeit
- Full flavor text pools, character sheet integration, !adventure rivals

Babysitting Service:
- Weekly/monthly auto-play service targeting weakest skill
- Babysitter rerolls death, claims items, credits gold and XP
- Rivals automatically declined during service
- End-of-service summary with diaper report

Revive fixes:
- On-demand revive in handleMenu, parseAndResolveChoice, handleStatus
- Morning DM respawn check now runs before babysit interception
- Players no longer stuck dead after DeadUntil expires mid-day

Other changes:
- Multilingual !define searches all DreamDict languages by default
- TwinBee empty haul now shows "jackshit" instead of blank
- Wordle solve payouts increased (e.g. 1-guess: €100 -> €500)
- Per-message euro payouts doubled across all tiers
- Audit fixes: user locks on RPS/babysit handlers, TOCTOU on gold
  transfers, dead code removal, deprecated strings.Title replaced,
  error logging on silent failures

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-04-03 20:11:51 -07:00
parent 2d99af5310
commit ad6e652755
14 changed files with 1769 additions and 112 deletions

View File

@@ -117,6 +117,7 @@ func (p *AdventurePlugin) Init() error {
go p.midnightTicker()
go p.eventTicker()
go p.arenaAutoCashoutTicker()
go p.rivalChallengeTicker()
// Auto-cashout any arena runs left in 'awaiting' from a prior restart
p.arenaCleanupStaleRuns()
@@ -199,6 +200,10 @@ func (p *AdventurePlugin) dispatchCommand(ctx MessageContext) error {
return p.handleEventRespond(ctx)
case lower == "help":
return p.SendDM(ctx.Sender, advHelpText)
case lower == "rivals":
return p.handleRivalsCmd(ctx)
case lower == "babysit" || strings.HasPrefix(lower, "babysit "):
return p.handleBabysitCmd(ctx, strings.TrimSpace(strings.TrimPrefix(lower, "babysit")))
}
return p.SendDM(ctx.Sender, "Unknown command. Type `!adventure help` to see available commands.")
@@ -216,6 +221,8 @@ const advHelpText = `**Adventure Commands**
` + "`!adventure inventory`" + ` — View your inventory
` + "`!adventure leaderboard`" + ` — View the leaderboard
` + "`!adventure respond`" + ` — Respond to a mid-day event
` + "`!adventure rivals`" + ` — View rival duel records
` + "`!adventure babysit`" + ` — Adventurer Babysitting Service
` + "`!adventure help`" + ` — This message
**Arena:**
@@ -238,8 +245,22 @@ func (p *AdventurePlugin) handleMenu(ctx MessageContext) error {
}
if !char.Alive {
text := renderAdvDeathStatusDM(char)
return p.SendDM(ctx.Sender, text)
// On-demand revive if death timer has expired
if char.DeadUntil != nil && time.Now().UTC().After(*char.DeadUntil) {
char.Alive = true
char.DeadUntil = nil
if err := saveAdvCharacter(char); err != nil {
slog.Error("adventure: on-demand revive failed", "user", char.UserID, "err", err)
} else {
text := renderAdvRespawnDM(char)
p.SendDM(ctx.Sender, text)
// Fall through to show menu
}
}
if !char.Alive {
text := renderAdvDeathStatusDM(char)
return p.SendDM(ctx.Sender, text)
}
}
if char.ActionTakenToday {
@@ -284,9 +305,29 @@ func (p *AdventurePlugin) handleStatus(ctx MessageContext) error {
return p.SendReply(ctx.RoomID, ctx.EventID, "No adventurer found. Type `!adventure` to create one.")
}
equip, _ := loadAdvEquipment(ctx.Sender)
items, _ := loadAdvInventory(ctx.Sender)
treasures, _ := loadAdvTreasureBonuses(ctx.Sender)
// On-demand revive if death timer has expired
if !char.Alive && char.DeadUntil != nil && time.Now().UTC().After(*char.DeadUntil) {
char.Alive = true
char.DeadUntil = nil
if err := saveAdvCharacter(char); err != nil {
slog.Error("adventure: on-demand revive failed", "user", char.UserID, "err", err)
} else {
p.SendDM(ctx.Sender, renderAdvRespawnDM(char))
}
}
equip, err := loadAdvEquipment(ctx.Sender)
if err != nil {
slog.Error("adventure: failed to load equipment for status", "user", ctx.Sender, "err", err)
}
items, err := loadAdvInventory(ctx.Sender)
if err != nil {
slog.Error("adventure: failed to load inventory for status", "user", ctx.Sender, "err", err)
}
treasures, err := loadAdvTreasureBonuses(ctx.Sender)
if err != nil {
slog.Error("adventure: failed to load treasures for status", "user", ctx.Sender, "err", err)
}
balance := p.euro.GetBalance(ctx.Sender)
text := renderAdvCharacterSheet(char, equip, items, treasures, balance)
@@ -453,6 +494,8 @@ func (p *AdventurePlugin) resolvePendingInteraction(ctx MessageContext, interact
return p.handleMasterworkEquipReply(ctx, interaction)
case "masterwork_equip_confirm":
return p.handleMasterworkEquipConfirm(ctx, interaction)
case "rival_rps":
return p.resolveRivalRPSRound(ctx, interaction)
}
return nil
}
@@ -499,7 +542,19 @@ func (p *AdventurePlugin) parseAndResolveChoice(ctx MessageContext, body string)
}
if !char.Alive {
return p.SendDM(ctx.Sender, renderAdvDeathStatusDM(char))
// On-demand revive if death timer has expired
if char.DeadUntil != nil && time.Now().UTC().After(*char.DeadUntil) {
char.Alive = true
char.DeadUntil = nil
if err := saveAdvCharacter(char); err != nil {
slog.Error("adventure: on-demand revive failed", "user", char.UserID, "err", err)
} else {
p.SendDM(ctx.Sender, renderAdvRespawnDM(char))
}
}
if !char.Alive {
return p.SendDM(ctx.Sender, renderAdvDeathStatusDM(char))
}
}
if char.ActionTakenToday {
@@ -665,6 +720,9 @@ func (p *AdventurePlugin) resolveActivity(ctx MessageContext, char *AdventureCha
// Check level up
result.LeveledUp, result.NewLevel = checkAdvLevelUp(char, result.XPSkill)
if result.LeveledUp && result.XPSkill == "combat" {
p.checkRivalPoolUnlock(char)
}
// Handle death
deathReprieved := false