mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 00:52:40 +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"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -11,6 +12,7 @@ import (
|
|||||||
"gogobee/internal/db"
|
"gogobee/internal/db"
|
||||||
|
|
||||||
"maunium.net/go/mautrix"
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/event"
|
||||||
"maunium.net/go/mautrix/id"
|
"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
|
// Euro Plugin
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
@@ -72,6 +102,7 @@ type EuroPlugin struct {
|
|||||||
Base
|
Base
|
||||||
cfg euroConfig
|
cfg euroConfig
|
||||||
cooldowns map[id.UserID]time.Time
|
cooldowns map[id.UserID]time.Time
|
||||||
|
combos map[id.UserID]map[string]*comboState // category → state
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +111,7 @@ func NewEuroPlugin(client *mautrix.Client) *EuroPlugin {
|
|||||||
Base: NewBase(client),
|
Base: NewBase(client),
|
||||||
cfg: loadEuroConfig(),
|
cfg: loadEuroConfig(),
|
||||||
cooldowns: make(map[id.UserID]time.Time),
|
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: "balance", Description: "Check your euro balance", Usage: "!balance", Category: "Economy"},
|
||||||
{Name: "baltop", Description: "Euro leaderboard", Usage: "!baltop", 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: "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)
|
return p.handleBaltop(ctx)
|
||||||
case p.IsCommand(ctx.Body, "baltransfer"):
|
case p.IsCommand(ctx.Body, "baltransfer"):
|
||||||
return p.handleTransfer(ctx)
|
return p.handleTransfer(ctx)
|
||||||
|
case p.IsCommand(ctx.Body, "combo"):
|
||||||
|
return p.handleCombo(ctx)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -128,7 +163,6 @@ func (p *EuroPlugin) awardPassiveEuros(ctx MessageContext) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
p.cooldowns[ctx.Sender] = now
|
p.cooldowns[ctx.Sender] = now
|
||||||
// Periodic cleanup
|
|
||||||
if len(p.cooldowns) > 1000 {
|
if len(p.cooldowns) > 1000 {
|
||||||
for uid, t := range p.cooldowns {
|
for uid, t := range p.cooldowns {
|
||||||
if now.Sub(t) > time.Duration(p.cfg.CooldownSeconds)*time.Second {
|
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.mu.Unlock()
|
||||||
|
|
||||||
|
p.ensureBalance(ctx.Sender)
|
||||||
|
|
||||||
|
// Message combo — base amount from word count, multiplied by combo.
|
||||||
words := len(strings.Fields(ctx.Body))
|
words := len(strings.Fields(ctx.Body))
|
||||||
var amount float64
|
var baseMsg float64
|
||||||
switch {
|
switch {
|
||||||
case words >= 51:
|
case words >= 51:
|
||||||
amount = 20.00
|
baseMsg = 20.00
|
||||||
case words >= 26:
|
case words >= 26:
|
||||||
amount = 10.00
|
baseMsg = 10.00
|
||||||
case words >= 11:
|
case words >= 11:
|
||||||
amount = 5.00
|
baseMsg = 5.00
|
||||||
case words >= 4:
|
case words >= 4:
|
||||||
amount = 2.50
|
baseMsg = 2.50
|
||||||
default:
|
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)
|
// Image combo — bonus for image/video/file attachments.
|
||||||
p.credit(ctx.Sender, amount, "message")
|
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