mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-15 08:32:41 +00:00
Add activity combo system with +200% per-step multiplier
Messages, links, and images each have independent combo streaks that multiply passive euro earning. Combo grows 3x per step (+200%), resets after 10 min idle, with daily caps (50 msg, 10 links, 15 images). !combo command shows current streaks. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"log/slog"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"gogobee/internal/db"
|
||||
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
)
|
||||
|
||||
@@ -64,6 +66,34 @@ func loadEuroConfig() euroConfig {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Combo system — activity streaks that multiply passive euro earning.
|
||||
//
|
||||
// Three independent combo categories: messages, links, images.
|
||||
// Each combo increments with qualifying activity and resets after 10 min idle.
|
||||
// Multiplier: 1 + (combo_step * 2.0) — so step 1 = 3x, step 5 = 11x, etc.
|
||||
// Daily cap per category prevents unbounded farming.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const (
|
||||
comboTimeout = 10 * time.Minute
|
||||
comboMsgCap = 50
|
||||
comboLinkCap = 10
|
||||
comboImageCap = 15
|
||||
comboMultPerStep = 2.0 // +200% per step
|
||||
comboLinkBase = 5.0
|
||||
comboImageBase = 3.0
|
||||
)
|
||||
|
||||
type comboState struct {
|
||||
Step int
|
||||
LastAt time.Time
|
||||
Today string
|
||||
DayHits int
|
||||
}
|
||||
|
||||
var urlPattern = regexp.MustCompile(`https?://\S+`)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Euro Plugin
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -72,6 +102,7 @@ type EuroPlugin struct {
|
||||
Base
|
||||
cfg euroConfig
|
||||
cooldowns map[id.UserID]time.Time
|
||||
combos map[id.UserID]map[string]*comboState // category → state
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
@@ -80,6 +111,7 @@ func NewEuroPlugin(client *mautrix.Client) *EuroPlugin {
|
||||
Base: NewBase(client),
|
||||
cfg: loadEuroConfig(),
|
||||
cooldowns: make(map[id.UserID]time.Time),
|
||||
combos: make(map[id.UserID]map[string]*comboState),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,6 +123,7 @@ func (p *EuroPlugin) Commands() []CommandDef {
|
||||
{Name: "balance", Description: "Check your euro balance", Usage: "!balance", Category: "Economy"},
|
||||
{Name: "baltop", Description: "Euro leaderboard", Usage: "!baltop", Category: "Economy"},
|
||||
{Name: "baltransfer", Description: "Send euros to another player", Usage: "!baltransfer @user €amount", Category: "Economy"},
|
||||
{Name: "combo", Description: "View your current activity combo streaks", Usage: "!combo", Category: "Economy"},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,6 +144,8 @@ func (p *EuroPlugin) OnMessage(ctx MessageContext) error {
|
||||
return p.handleBaltop(ctx)
|
||||
case p.IsCommand(ctx.Body, "baltransfer"):
|
||||
return p.handleTransfer(ctx)
|
||||
case p.IsCommand(ctx.Body, "combo"):
|
||||
return p.handleCombo(ctx)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -128,7 +163,6 @@ func (p *EuroPlugin) awardPassiveEuros(ctx MessageContext) {
|
||||
return
|
||||
}
|
||||
p.cooldowns[ctx.Sender] = now
|
||||
// Periodic cleanup
|
||||
if len(p.cooldowns) > 1000 {
|
||||
for uid, t := range p.cooldowns {
|
||||
if now.Sub(t) > time.Duration(p.cfg.CooldownSeconds)*time.Second {
|
||||
@@ -138,23 +172,138 @@ func (p *EuroPlugin) awardPassiveEuros(ctx MessageContext) {
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
p.ensureBalance(ctx.Sender)
|
||||
|
||||
// Message combo — base amount from word count, multiplied by combo.
|
||||
words := len(strings.Fields(ctx.Body))
|
||||
var amount float64
|
||||
var baseMsg float64
|
||||
switch {
|
||||
case words >= 51:
|
||||
amount = 20.00
|
||||
baseMsg = 20.00
|
||||
case words >= 26:
|
||||
amount = 10.00
|
||||
baseMsg = 10.00
|
||||
case words >= 11:
|
||||
amount = 5.00
|
||||
baseMsg = 5.00
|
||||
case words >= 4:
|
||||
amount = 2.50
|
||||
baseMsg = 2.50
|
||||
default:
|
||||
amount = 1.00
|
||||
baseMsg = 1.00
|
||||
}
|
||||
msgMult := p.advanceCombo(ctx.Sender, "message", comboMsgCap)
|
||||
p.credit(ctx.Sender, baseMsg*msgMult, "message")
|
||||
|
||||
// Link combo — bonus per URL in the message.
|
||||
if urls := urlPattern.FindAllString(ctx.Body, -1); len(urls) > 0 {
|
||||
linkMult := p.advanceCombo(ctx.Sender, "link", comboLinkCap)
|
||||
p.credit(ctx.Sender, comboLinkBase*linkMult*float64(len(urls)), "link_combo")
|
||||
}
|
||||
|
||||
p.ensureBalance(ctx.Sender)
|
||||
p.credit(ctx.Sender, amount, "message")
|
||||
// Image combo — bonus for image/video/file attachments.
|
||||
if mc := ctx.Event.Content.AsMessage(); mc != nil {
|
||||
if mc.MsgType == event.MsgImage || mc.MsgType == event.MsgVideo || mc.MsgType == event.MsgFile {
|
||||
imgMult := p.advanceCombo(ctx.Sender, "image", comboImageCap)
|
||||
p.credit(ctx.Sender, comboImageBase*imgMult, "image_combo")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// advanceCombo increments a user's combo for a category, resets if timed out
|
||||
// or daily cap reached, and returns the current multiplier.
|
||||
func (p *EuroPlugin) advanceCombo(userID id.UserID, category string, dailyCap int) float64 {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
now := time.Now()
|
||||
today := now.UTC().Format("2006-01-02")
|
||||
|
||||
userCombos, ok := p.combos[userID]
|
||||
if !ok {
|
||||
userCombos = make(map[string]*comboState)
|
||||
p.combos[userID] = userCombos
|
||||
}
|
||||
|
||||
cs, ok := userCombos[category]
|
||||
if !ok {
|
||||
cs = &comboState{}
|
||||
userCombos[category] = cs
|
||||
}
|
||||
|
||||
// Reset on new day.
|
||||
if cs.Today != today {
|
||||
cs.Step = 0
|
||||
cs.DayHits = 0
|
||||
cs.Today = today
|
||||
}
|
||||
|
||||
// Reset if combo timed out.
|
||||
if !cs.LastAt.IsZero() && now.Sub(cs.LastAt) > comboTimeout {
|
||||
cs.Step = 0
|
||||
}
|
||||
|
||||
// At daily cap, return base multiplier (no combo bonus).
|
||||
if cs.DayHits >= dailyCap {
|
||||
return 1.0
|
||||
}
|
||||
|
||||
cs.DayHits++
|
||||
cs.Step++
|
||||
cs.LastAt = now
|
||||
|
||||
return 1.0 + float64(cs.Step)*comboMultPerStep
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// !combo command
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (p *EuroPlugin) handleCombo(ctx MessageContext) error {
|
||||
p.mu.Lock()
|
||||
userCombos := p.combos[ctx.Sender]
|
||||
now := time.Now()
|
||||
today := now.UTC().Format("2006-01-02")
|
||||
|
||||
type info struct {
|
||||
name string
|
||||
step int
|
||||
dayHits int
|
||||
cap int
|
||||
active bool
|
||||
}
|
||||
cats := []info{
|
||||
{"Messages", 0, 0, comboMsgCap, false},
|
||||
{"Links", 0, 0, comboLinkCap, false},
|
||||
{"Images", 0, 0, comboImageCap, false},
|
||||
}
|
||||
keys := []string{"message", "link", "image"}
|
||||
|
||||
if userCombos != nil {
|
||||
for i, k := range keys {
|
||||
if cs, ok := userCombos[k]; ok && cs.Today == today {
|
||||
cats[i].step = cs.Step
|
||||
cats[i].dayHits = cs.DayHits
|
||||
cats[i].active = !cs.LastAt.IsZero() && now.Sub(cs.LastAt) < comboTimeout
|
||||
}
|
||||
}
|
||||
}
|
||||
p.mu.Unlock()
|
||||
|
||||
var sb strings.Builder
|
||||
sb.WriteString("🔥 **Activity Combo**\n\n")
|
||||
|
||||
for _, c := range cats {
|
||||
mult := 1.0 + float64(c.step)*comboMultPerStep
|
||||
status := "💤 inactive"
|
||||
if c.active {
|
||||
status = fmt.Sprintf("⚡ **%.0fx** multiplier", mult)
|
||||
} else if c.step > 0 {
|
||||
status = "⏳ expired (resets on next activity)"
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("**%s**: step %d/%d — %s\n", c.name, c.dayHits, c.cap, status))
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("\nCombo grows +%.0f%% per step. Resets after %d min idle.", comboMultPerStep*100, int(comboTimeout.Minutes())))
|
||||
|
||||
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user