Add version system, tax tracking, lottery winner DMs, fmtEuro, audit fixes

- Version system: per-plugin versions via Versioned interface, crash_log and
  version_history DB tables, !version command, crash stats in !botinfo
- Tax tracking: tax_ledger table, trackTaxPaid across all communityTax and
  communityPotAdd sites, tax paid display in !stats and !superstatsexplusalpha
- Lottery: DM winners with winning ticket details and matched numbers bolded
- fmtEuro: generic currency formatter with thousand separators across all
  economy display paths
- Audit fixes: streak_decayed column for Streak Survivor achievement,
  combat_narrative empty-phase guard, crafting success rate integer division,
  RecordCrash nil-DB guard

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
prosolis
2026-04-18 01:07:25 -07:00
parent 42e6e23900
commit b15c13cde7
22 changed files with 600 additions and 75 deletions

View File

@@ -133,6 +133,8 @@ func runMigrations(d *sql.DB) error {
`ALTER TABLE adventure_characters ADD COLUMN pet_supply_shop_unlocked INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE adventure_characters ADD COLUMN pet_level10_date TEXT NOT NULL DEFAULT ''`,
`ALTER TABLE adventure_characters ADD COLUMN pet_morning_defense INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE adventure_characters ADD COLUMN auto_babysit INTEGER NOT NULL DEFAULT 0`,
`ALTER TABLE adventure_characters ADD COLUMN streak_decayed INTEGER NOT NULL DEFAULT 0`,
}
for _, stmt := range columnMigrations {
if _, err := d.Exec(stmt); err != nil {
@@ -165,6 +167,39 @@ func MarkJobCompleted(jobName, dateKey string) {
)
}
// RecordCrash logs a panic/crash event with version and plugin context.
func RecordCrash(botVersion, plugin, message string) {
if globalDB == nil {
return
}
Exec("crash log",
`INSERT INTO crash_log (bot_version, plugin, message) VALUES (?, ?, ?)`,
botVersion, plugin, message,
)
}
// RecordStartup logs a version startup event.
func RecordStartup(version, commit string) {
Exec("version history",
`INSERT INTO version_history (version, commit_sha) VALUES (?, ?)`,
version, commit,
)
}
// CrashCount returns the number of crashes for a given bot version.
func CrashCount(botVersion string) int {
var count int
_ = Get().QueryRow(`SELECT COUNT(*) FROM crash_log WHERE bot_version = ?`, botVersion).Scan(&count)
return count
}
// CrashCountAll returns the total number of recorded crashes.
func CrashCountAll() int {
var count int
_ = Get().QueryRow(`SELECT COUNT(*) FROM crash_log`).Scan(&count)
return count
}
// CacheGet returns cached data for the given key if it exists and is within ttlSeconds.
// Returns empty string if not cached or expired.
func CacheGet(key string, ttlSeconds int) string {
@@ -1282,6 +1317,27 @@ CREATE TABLE IF NOT EXISTS lottery_history (
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS crash_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bot_version TEXT NOT NULL,
plugin TEXT NOT NULL DEFAULT '',
message TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS version_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
version TEXT NOT NULL,
commit_sha TEXT NOT NULL DEFAULT '',
started_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS tax_ledger (
user_id TEXT PRIMARY KEY,
total_paid INTEGER NOT NULL DEFAULT 0,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`
// SeedSchedulerDefaults inserts default scheduler jobs if they don't exist.

View File

@@ -44,7 +44,8 @@ func NewAchievementsPlugin(client *mautrix.Client, registry AchievementRegistry)
return p
}
func (p *AchievementsPlugin) Name() string { return "achievements" }
func (p *AchievementsPlugin) Name() string { return "achievements" }
func (p *AchievementsPlugin) Version() string { return "1.2.0" }
func (p *AchievementsPlugin) Commands() []CommandDef {
return []CommandDef{
@@ -1015,6 +1016,127 @@ func (p *AchievementsPlugin) buildAchievements() []achievementDef {
return count > 0
},
},
// ── Streak & Babysit ────────────────────────────────────────────────
{
ID: "adv_streaker", Name: "Streaker", Description: "Maintained a 7-day streak. Running wild.",
Emoji: "🏃",
Check: func(d *sql.DB, u id.UserID) bool {
var streak int
_ = d.QueryRow(`SELECT best_streak FROM adventure_characters WHERE user_id = ?`, string(u)).Scan(&streak)
return streak >= 7
},
},
{
ID: "adv_streak_60", Name: "Obsessed", Description: "60 days. The game plays you now.",
Emoji: "🔥",
Check: func(d *sql.DB, u id.UserID) bool {
var streak int
_ = d.QueryRow(`SELECT best_streak FROM adventure_characters WHERE user_id = ?`, string(u)).Scan(&streak)
return streak >= 60
},
},
{
ID: "adv_streak_100", Name: "Centurion", Description: "100 days. This is your life now.",
Emoji: "💯",
Check: func(d *sql.DB, u id.UserID) bool {
var streak int
_ = d.QueryRow(`SELECT best_streak FROM adventure_characters WHERE user_id = ?`, string(u)).Scan(&streak)
return streak >= 100
},
},
{
ID: "adv_babysit_hired", Name: "Delegation", Description: "Hired someone to do the hard part.",
Emoji: "🍼",
Check: func(d *sql.DB, u id.UserID) bool {
var count int
_ = d.QueryRow(`SELECT COUNT(*) FROM babysit_log WHERE user_id = ?`, string(u)).Scan(&count)
return count > 0
},
},
{
ID: "adv_auto_babysit", Name: "Safety Net", Description: "Auto-babysit saved your streak. Worth every euro.",
Emoji: "🛡️",
Check: func(d *sql.DB, u id.UserID) bool {
// Granted by scheduler when auto-babysit fires
return false
},
},
{
ID: "adv_streak_survivor", Name: "Streak Survivor", Description: "Rebuilt your streak to 14 after losing it.",
Emoji: "🔄",
Check: func(d *sql.DB, u id.UserID) bool {
var current, best, decayed int
err := d.QueryRow(`SELECT current_streak, best_streak, streak_decayed FROM adventure_characters WHERE user_id = ?`, string(u)).Scan(&current, &best, &decayed)
return err == nil && current >= 14 && decayed > 0
},
},
// ── Combat Moments ──────────────────────────────────────────────────
{
ID: "combat_near_death", Name: "Clutch", Description: "Won with less than 15% HP. Barely.",
Emoji: "💔",
Check: func(d *sql.DB, u id.UserID) bool {
// Granted by combat bridge on near-death victory
return false
},
},
{
ID: "combat_pet_save", Name: "Good Boy", Description: "Your pet distracted the enemy at exactly the right moment.",
Emoji: "🐾",
Check: func(d *sql.DB, u id.UserID) bool {
// Granted by combat bridge on pet whiff event
return false
},
},
{
ID: "combat_sniper_kill", Name: "One Shot", Description: "Arina ended the fight before it started.",
Emoji: "🎯",
Check: func(d *sql.DB, u id.UserID) bool {
// Granted by combat bridge on sniper kill
return false
},
},
{
ID: "combat_death_save", Name: "Sovereign's Reprieve", Description: "The crown said not today.",
Emoji: "👑",
Check: func(d *sql.DB, u id.UserID) bool {
// Granted by combat bridge on death save event
return false
},
},
{
ID: "combat_misty_clutch", Name: "Misty's Touch", Description: "Healed mid-combat. She was watching.",
Emoji: "💚",
Check: func(d *sql.DB, u id.UserID) bool {
// Granted by combat bridge on Misty heal event
return false
},
},
{
ID: "combat_consumable_used", Name: "Prepared", Description: "Used a consumable in combat. Preparation pays off.",
Emoji: "🧪",
Check: func(d *sql.DB, u id.UserID) bool {
// Granted by combat bridge on consumable use event
return false
},
},
{
ID: "adv_first_craft", Name: "Apprentice Herbalist", Description: "Crafted your first consumable. The wilds provide.",
Emoji: "🌿",
Check: func(d *sql.DB, u id.UserID) bool {
// Granted by combat bridge on first successful craft
return false
},
},
{
ID: "adv_craft_t5", Name: "Master Alchemist", Description: "Crafted a Tier 5 consumable. Nature bends to your will.",
Emoji: "⚗️",
Check: func(d *sql.DB, u id.UserID) bool {
// Granted by combat bridge on T5 craft
return false
},
},
}
}

View File

@@ -72,15 +72,41 @@ func (p *AdventurePlugin) handleBabysitCmd(ctx MessageContext, args string) erro
return p.handleBabysitPurchase(ctx, 7)
case lower == "month":
return p.handleBabysitPurchase(ctx, 30)
case lower == "auto":
return p.handleBabysitAutoToggle(ctx)
default:
return p.SendDM(ctx.Sender, "🍼 **Adventurer Babysitting Service**\n\n"+
"`!adventure babysit week` — 7 days of service\n"+
"`!adventure babysit month` — 30 days of service\n"+
"`!adventure babysit auto` — toggle auto-babysit on missed days\n"+
"`!adventure babysit status` — check service status\n"+
"`!adventure babysit cancel` — cancel early (no refund)")
}
}
func (p *AdventurePlugin) handleBabysitAutoToggle(ctx MessageContext) error {
userMu := p.advUserLock(ctx.Sender)
userMu.Lock()
defer userMu.Unlock()
char, err := loadAdvCharacter(ctx.Sender)
if err != nil {
return p.SendDM(ctx.Sender, "No adventurer found. Type `!adventure` to create one.")
}
char.AutoBabysit = !char.AutoBabysit
if err := saveAdvCharacter(char); err != nil {
slog.Error("babysit: failed to save auto-babysit toggle", "user", ctx.Sender, "err", err)
return p.SendDM(ctx.Sender, "Something went wrong. Try again.")
}
daily := babysitDailyCost(char.CombatLevel)
if char.AutoBabysit {
return p.SendDM(ctx.Sender, fmt.Sprintf("🍼 **Auto-babysit: ON**\n\nIf you miss a day, the babysitter steps in automatically (€%d/day). Your streak stays alive. Disable anytime with `!adventure babysit auto`.", daily))
}
return p.SendDM(ctx.Sender, "🍼 **Auto-babysit: OFF**\n\nYou're on your own. Miss a day and your streak takes the hit.")
}
func (p *AdventurePlugin) handleBabysitPurchase(ctx MessageContext, days int) error {
userMu := p.advUserLock(ctx.Sender)
userMu.Lock()
@@ -103,7 +129,7 @@ func (p *AdventurePlugin) handleBabysitPurchase(ctx MessageContext, days int) er
totalCost := daily * days
balance := p.euro.GetBalance(char.UserID)
if balance < float64(totalCost) {
return p.SendDM(ctx.Sender, fmt.Sprintf("🍼 The babysitting service costs €%d for %d days. You have €%.0f. The service has standards. Not many, but some.", totalCost, days, balance))
return p.SendDM(ctx.Sender, fmt.Sprintf("🍼 The babysitting service costs %s for %d days. You have %s. The service has standards. Not many, but some.", fmtEuro(totalCost), days, fmtEuro(balance)))
}
// Debit gold
@@ -148,8 +174,13 @@ func (p *AdventurePlugin) handleBabysitStatus(ctx MessageContext) error {
return p.SendDM(ctx.Sender, "No adventurer found.")
}
autoLabel := "OFF"
if char.AutoBabysit {
autoLabel = "ON"
}
if !char.BabysitActive {
return p.SendDM(ctx.Sender, "🍼 No active babysitting service. Use `!adventure babysit week` or `!adventure babysit month` to start.")
return p.SendDM(ctx.Sender, fmt.Sprintf("🍼 No active babysitting service.\nAuto-babysit: **%s** (`!adventure babysit auto` to toggle)\n\nUse `!adventure babysit week` or `!adventure babysit month` to start.", autoLabel))
}
remaining := "unknown"
@@ -176,8 +207,9 @@ func (p *AdventurePlugin) handleBabysitStatus(ctx MessageContext) error {
"Gold earned: €%d\n"+
"XP gained: %d\n"+
"Items claimed by babysitter: %d\n"+
"Rivals declined: %d",
remaining, titleCase(char.BabysitSkillFocus), len(logs), totalGold, totalXP, itemsClaimed, rivalsRefused)
"Rivals declined: %d\n"+
"Auto-babysit: %s",
remaining, titleCase(char.BabysitSkillFocus), len(logs), totalGold, totalXP, itemsClaimed, rivalsRefused, autoLabel)
return p.SendDM(ctx.Sender, text)
}
@@ -302,7 +334,8 @@ func (p *AdventurePlugin) runBabysitAction(char *AdventureCharacter, equip map[E
// Credit gold
if result.TotalLootValue > 0 {
p.euro.Credit(char.UserID, float64(result.TotalLootValue), "babysit_haul")
net, _ := communityTax(char.UserID, float64(result.TotalLootValue), 0.05)
p.euro.Credit(char.UserID, net, "babysit_haul")
}
// No treasure drops during babysitting
@@ -315,6 +348,34 @@ func (p *AdventurePlugin) runBabysitAction(char *AdventureCharacter, equip map[E
return int(result.TotalLootValue), result.XPGained, items
}
// runAutoBabysitDay runs a single day of babysit actions for auto-babysit.
// Called by the scheduler when a player with auto-babysit enabled misses a day.
func (p *AdventurePlugin) runAutoBabysitDay(char *AdventureCharacter) {
equip, err := loadAdvEquipment(char.UserID)
if err != nil {
slog.Error("auto-babysit: failed to load equipment", "user", char.UserID, "err", err)
return
}
isHol, _ := isHolidayToday()
harvestMax := maxHarvestActions
if isHol {
harvestMax++
}
bonuses := &AdvBonusSummary{}
skill := babysitWeakestSkill(char)
activity := skillToActivity(skill)
for char.HarvestActionsUsed < harvestMax {
p.runBabysitAction(char, equip, activity, bonuses)
char.HarvestActionsUsed++
}
char.ActionTakenToday = true
char.LastActionDate = time.Now().UTC().Format("2006-01-02")
}
// ── Expiry Check ────────────────────────────────────────────────────────────
func (p *AdventurePlugin) checkBabysitExpiry(chars []AdventureCharacter) {

View File

@@ -93,6 +93,8 @@ type AdventureCharacter struct {
PetSupplyShopUnlocked bool
PetLevel10Date string
PetMorningDefense bool
AutoBabysit bool
StreakDecayed bool
}
type AdvEquipment struct {
@@ -417,6 +419,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
var houseFrozen, houseAutopay int
var petChasedAway, petReactivated, petArrived, thomAnimalLine, petSupplyUnlocked, petMorningDef int
var autoBabysit, streakDecayed int
err := d.QueryRow(`
SELECT user_id, display_name,
@@ -442,7 +445,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
pet_chased_away, pet_reactivated, pet_arrived,
misty_encounter_count, misty_donated_count,
thom_animal_line_fired, pet_supply_shop_unlocked, pet_level10_date,
pet_morning_defense
pet_morning_defense, auto_babysit, streak_decayed
FROM adventure_characters WHERE user_id = ?`, string(userID)).Scan(
&c.UserID, &c.DisplayName,
&c.CombatLevel, &c.MiningSkill, &c.ForagingSkill, &c.FishingSkill,
@@ -467,7 +470,7 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
&petChasedAway, &petReactivated, &petArrived,
&c.MistyEncounterCount, &c.MistyDonatedCount,
&thomAnimalLine, &petSupplyUnlocked, &c.PetLevel10Date,
&petMorningDef,
&petMorningDef, &autoBabysit, &streakDecayed,
)
if err != nil {
return nil, err
@@ -478,6 +481,8 @@ func loadAdvCharacter(userID id.UserID) (*AdventureCharacter, error) {
c.RivalUnlockedNotified = rivalUnlocked == 1
c.BabysitActive = babysitAct == 1
c.PetMorningDefense = petMorningDef == 1
c.AutoBabysit = autoBabysit == 1
c.StreakDecayed = streakDecayed == 1
if deadUntil.Valid {
c.DeadUntil = &deadUntil.Time
}
@@ -598,6 +603,14 @@ func saveAdvCharacter(char *AdventureCharacter) error {
if char.PetMorningDefense {
petMorningDef = 1
}
autoBabysit := 0
if char.AutoBabysit {
autoBabysit = 1
}
streakDecayed := 0
if char.StreakDecayed {
streakDecayed = 1
}
_, err := d.Exec(`
UPDATE adventure_characters SET
@@ -623,7 +636,9 @@ func saveAdvCharacter(char *AdventureCharacter) error {
pet_chased_away = ?, pet_reactivated = ?, pet_arrived = ?,
misty_encounter_count = ?, misty_donated_count = ?,
thom_animal_line_fired = ?, pet_supply_shop_unlocked = ?, pet_level10_date = ?,
pet_morning_defense = ?
pet_morning_defense = ?,
auto_babysit = ?,
streak_decayed = ?
WHERE user_id = ?`,
char.DisplayName, char.CombatLevel, char.MiningSkill, char.ForagingSkill, char.FishingSkill,
char.CombatXP, char.MiningXP, char.ForagingXP, char.FishingXP,
@@ -647,6 +662,8 @@ func saveAdvCharacter(char *AdventureCharacter) error {
char.MistyEncounterCount, char.MistyDonatedCount,
thomAnimalLine, petSupplyUnlocked, char.PetLevel10Date,
petMorningDef,
autoBabysit,
streakDecayed,
string(char.UserID),
)
return err
@@ -777,7 +794,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) {
pet_chased_away, pet_reactivated, pet_arrived,
misty_encounter_count, misty_donated_count,
thom_animal_line_fired, pet_supply_shop_unlocked, pet_level10_date,
pet_morning_defense
pet_morning_defense, auto_babysit, streak_decayed
FROM adventure_characters`)
if err != nil {
return nil, err
@@ -792,6 +809,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) {
var mistyLastSeen, arinaLastSeen, mistyBuffExp, mistyDebuffExp, arinaBuffExp sql.NullTime
var houseFrozen, houseAutopay int
var petChasedAway, petReactivated, petArrived, thomAnimalLine, petSupplyUnlocked, petMorningDef int
var autoBabysit, streakDecayed int
if err := rows.Scan(
&c.UserID, &c.DisplayName,
&c.CombatLevel, &c.MiningSkill, &c.ForagingSkill, &c.FishingSkill,
@@ -816,7 +834,7 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) {
&petChasedAway, &petReactivated, &petArrived,
&c.MistyEncounterCount, &c.MistyDonatedCount,
&thomAnimalLine, &petSupplyUnlocked, &c.PetLevel10Date,
&petMorningDef,
&petMorningDef, &autoBabysit, &streakDecayed,
); err != nil {
return nil, err
}
@@ -826,6 +844,8 @@ func loadAllAdvCharacters() ([]AdventureCharacter, error) {
c.RivalUnlockedNotified = rivalUnlocked == 1
c.BabysitActive = babysitAct == 1
c.PetMorningDefense = petMorningDef == 1
c.AutoBabysit = autoBabysit == 1
c.StreakDecayed = streakDecayed == 1
c.HouseLoanFrozen = houseFrozen == 1
c.HouseAutopay = houseAutopay == 1
c.PetChasedAway = petChasedAway == 1

View File

@@ -176,8 +176,9 @@ func (p *AdventurePlugin) handleEventRespond(ctx MessageContext) error {
gold += rand.Int64N(event.GoldMax - event.GoldMin + 1)
}
// Credit gold
p.euro.Credit(ctx.Sender, float64(gold), "adventure_event_"+event.Key)
// Credit gold (5% community tax)
net, _ := communityTax(ctx.Sender, float64(gold), 0.05)
p.euro.Credit(ctx.Sender, net, "adventure_event_"+event.Key)
// Apply XP if applicable
xpSkill := event.XPSkill

View File

@@ -3,6 +3,7 @@ package plugin
import (
"fmt"
"log/slog"
"math"
"strings"
"time"
@@ -96,7 +97,7 @@ func (p *AdventurePlugin) handleHospitalCmd(ctx MessageContext) error {
sb.WriteString("\n\n")
// Payment prompt
sb.WriteString(fmt.Sprintf("**St. Guildmore's Memorial Hospital**\nAmount due: **€%d**\n\n", afterInsurance))
sb.WriteString(fmt.Sprintf("**St. Guildmore's Memorial Hospital**\nAmount due: **%s**\n\n", fmtEuro(afterInsurance)))
sb.WriteString("Pay now? (yes / no)\n\n")
sb.WriteString("*Note: Declining payment will result in discharge to the natural respawn queue. " +
"You'll be back tomorrow. The chair in the waiting room is available in the meantime.*")
@@ -162,6 +163,12 @@ func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *ad
return p.SendDM(ctx.Sender, text)
}
// Community health fund: 10% of revival cost.
if potCut := int(math.Round(float64(cost) * 0.1)); potCut > 0 {
communityPotAdd(potCut)
trackTaxPaid(ctx.Sender, potCut)
}
// Revive
char.Alive = true
char.DeadUntil = nil
@@ -176,10 +183,10 @@ func (p *AdventurePlugin) resolveHospitalPay(ctx MessageContext, interaction *ad
// Discharge DM
p.SendDM(ctx.Sender, fmt.Sprintf(
"✅ **Discharged — you're alive!**\n\n"+
"€%d deducted. Nurse Joy wishes you the best. "+
"%s deducted. Nurse Joy wishes you the best. "+
"She means it. She always means it.\n\n"+
"Go get 'em. Type `!adventure` when you're ready.",
cost))
fmtEuro(cost)))
// Room announcement
gr := gamesRoom()
@@ -220,9 +227,9 @@ func (p *AdventurePlugin) sendHospitalAd(userID id.UserID, char *AdventureCharac
"🏥 **St. Guildmore's Memorial Hospital**\n\n"+
"Nurse Joy has been notified. A bed is being prepared.\n\n"+
"Type `!hospital` to check in for same-day revival.\n"+
"Estimated bill: €%d (Guild insurance covers €%d → you pay €%d)\n\n"+
"Estimated bill: %s (Guild insurance covers %s → you pay %s)\n\n"+
"Or rest up — natural respawn at %s UTC.",
beforeInsurance, beforeInsurance-afterInsurance, afterInsurance, respawnTime)
fmtEuro(beforeInsurance), fmtEuro(beforeInsurance-afterInsurance), fmtEuro(afterInsurance), respawnTime)
time.AfterFunc(10*time.Second, func() {
if err := p.SendDM(userID, text); err != nil {

View File

@@ -4,6 +4,7 @@ import (
"database/sql"
"fmt"
"log/slog"
"math"
"math/rand/v2"
"strings"
"time"
@@ -169,6 +170,29 @@ func communityPotAdd(amount int) {
)
}
func communityTax(userID id.UserID, gross float64, rate float64) (net float64, tax int) {
t := math.Round(gross * rate)
tax = int(t)
net = gross - t
if tax > 0 {
communityPotAdd(tax)
trackTaxPaid(userID, tax)
}
return net, tax
}
func trackTaxPaid(userID id.UserID, amount int) {
if amount <= 0 {
return
}
db.Exec("tax: track paid",
`INSERT INTO tax_ledger (user_id, total_paid, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(user_id) DO UPDATE SET total_paid = total_paid + ?, updated_at = CURRENT_TIMESTAMP`,
string(userID), amount, amount,
)
}
func communityPotBalance() int {
d := db.Get()
var balance int
@@ -556,13 +580,14 @@ func (p *AdventurePlugin) finalizeRivalMatch(challenge *advRivalChallenge, playe
}
}
winnerShare := stake / 2
winnerShare := int(math.Round(float64(stake) * 0.3))
potShare := stake - winnerShare
if winnerShare > 0 {
p.euro.Credit(winnerID, float64(winnerShare), "rival_duel_win")
}
if potShare > 0 {
communityPotAdd(potShare)
trackTaxPaid(loserID, potShare)
}
// Update rival records (both directions).
@@ -672,13 +697,14 @@ func (p *AdventurePlugin) expireRivalChallenges() {
}
}
winnerShare := stake / 2
winnerShare := int(math.Round(float64(stake) * 0.3))
potShare := stake - winnerShare
if winnerShare > 0 {
p.euro.Credit(challenge.ChallengerID, float64(winnerShare), "rival_forfeit_win")
}
if potShare > 0 {
communityPotAdd(potShare)
trackTaxPaid(challenge.ChallengedID, potShare)
}
upsertRivalRecord(challenge.ChallengedID, challenge.ChallengerID, false)

View File

@@ -345,6 +345,36 @@ func (p *AdventurePlugin) midnightReset() error {
continue
}
// Auto-babysit: if enabled, alive, and affordable, run a single babysit day instead of losing streak
if char.AutoBabysit && char.Alive && !char.BabysitActive {
daily := babysitDailyCost(char.CombatLevel)
if p.euro.GetBalance(char.UserID) >= float64(daily) {
if p.euro.Debit(char.UserID, float64(daily), "auto_babysit") {
p.runAutoBabysitDay(&char)
if char.CurrentStreak > 0 {
char.CurrentStreak++
if char.CurrentStreak > char.BestStreak {
char.BestStreak = char.CurrentStreak
}
} else {
char.CurrentStreak = 1
}
_ = saveAdvCharacter(&char)
if p.achievements != nil {
p.achievements.GrantAchievement(char.UserID, "adv_auto_babysit")
}
if dmsSent > 0 {
time.Sleep(time.Duration(1000+rand.IntN(2000)) * time.Millisecond)
}
dmsSent++
p.SendDM(char.UserID, fmt.Sprintf("🍼 **Auto-babysit activated** — €%d deducted. Your streak is safe at %d days.", daily, char.CurrentStreak))
continue
}
}
}
// Jitter between DMs to avoid Matrix rate limits
if dmsSent > 0 {
time.Sleep(time.Duration(1000+rand.IntN(2000)) * time.Millisecond)
@@ -353,15 +383,19 @@ func (p *AdventurePlugin) midnightReset() error {
// Idle shame DM
text := renderAdvIdleShameDM(&char)
if char.CurrentStreak > 0 {
oldStreak := char.CurrentStreak
char.CurrentStreak /= 2
char.StreakDecayed = true
text += fmt.Sprintf("\n\n🔥 Streak: %d → %d days", oldStreak, char.CurrentStreak)
if char.CurrentStreak > 0 {
text += " — not all is lost."
}
_ = saveAdvCharacter(&char)
}
if err := p.SendDM(char.UserID, text); err != nil {
slog.Error("adventure: failed to send idle shame DM", "user", char.UserID, "err", err)
}
// Reset streak
if char.CurrentStreak > 0 {
char.CurrentStreak = 0
_ = saveAdvCharacter(&char)
}
} else {
// Update streak — LastActionDate was set at action time
yesterday := time.Now().UTC().Add(-24 * time.Hour).Format("2006-01-02")

View File

@@ -289,7 +289,8 @@ func (p *AdventurePlugin) distributeTwinBeeRewards(result *TwinBeeResult) TwinBe
if share < 1 {
share = 1
}
p.euro.Credit(ep.uid, float64(share), "twinbee_daily_share")
net, _ := communityTax(ep.uid, float64(share), 0.05)
p.euro.Credit(ep.uid, net, "twinbee_daily_share")
totalDistributed += share
if rollTwinBeeGift(ep.uid) {
summary.GiftCount++

View File

@@ -179,7 +179,8 @@ func NewBlackjackPlugin(client *mautrix.Client, euro *EuroPlugin) *BlackjackPlug
}
}
func (p *BlackjackPlugin) Name() string { return "blackjack" }
func (p *BlackjackPlugin) Name() string { return "blackjack" }
func (p *BlackjackPlugin) Version() string { return "1.1.0" }
func (p *BlackjackPlugin) Commands() []CommandDef {
return []CommandDef{
@@ -727,8 +728,10 @@ func (p *BlackjackPlugin) collectResolveRound(roomID id.RoomID, table *bjTable)
case playerBJ:
result = "Blackjack!"
payout = pl.Bet + math.Floor(pl.Bet*1.5) // Return bet + 1.5x (rounded down)
p.euro.Credit(pl.UserID, payout, "blackjack_win")
payout = pl.Bet + math.Floor(pl.Bet*1.5)
net, _ := communityTax(pl.UserID, payout, 0.05)
p.euro.Credit(pl.UserID, net, "blackjack_win")
payout = net
case dealerBJ:
result = "Dealer Blackjack"
@@ -737,12 +740,16 @@ func (p *BlackjackPlugin) collectResolveRound(roomID id.RoomID, table *bjTable)
case dealerBust:
result = "Win (dealer bust)!"
payout = pl.Bet * 2
p.euro.Credit(pl.UserID, payout, "blackjack_win")
net, _ := communityTax(pl.UserID, payout, 0.05)
p.euro.Credit(pl.UserID, net, "blackjack_win")
payout = net
case playerValue > dealerValue:
result = fmt.Sprintf("Win! %d vs %d", playerValue, dealerValue)
payout = pl.Bet * 2
p.euro.Credit(pl.UserID, payout, "blackjack_win")
net, _ := communityTax(pl.UserID, payout, 0.05)
p.euro.Credit(pl.UserID, net, "blackjack_win")
payout = net
case playerValue == dealerValue:
result = "Push"

View File

@@ -12,6 +12,7 @@ import (
"time"
"gogobee/internal/db"
"gogobee/internal/version"
"maunium.net/go/mautrix"
)
@@ -43,6 +44,7 @@ func (p *BotInfoPlugin) Name() string { return "botinfo" }
func (p *BotInfoPlugin) Commands() []CommandDef {
return []CommandDef{
{Name: "botinfo", Description: "Show bot diagnostics (admin only)", Usage: "!botinfo", Category: "Admin", AdminOnly: true},
{Name: "version", Description: "Show bot version", Usage: "!version", Category: "Info"},
}
}
@@ -51,8 +53,10 @@ func (p *BotInfoPlugin) Init() error { return nil }
func (p *BotInfoPlugin) OnReaction(_ ReactionContext) error { return nil }
func (p *BotInfoPlugin) OnMessage(ctx MessageContext) error {
if p.IsCommand(ctx.Body, "version") {
return p.handleVersion(ctx)
}
if !p.IsCommand(ctx.Body, "botinfo") {
// Passively count messages
IncrementMessageCount()
return nil
}
@@ -64,9 +68,24 @@ func (p *BotInfoPlugin) OnMessage(ctx MessageContext) error {
return p.handleBotInfo(ctx)
}
func (p *BotInfoPlugin) handleVersion(ctx MessageContext) error {
uptime := time.Since(botStartTime)
days := int(uptime.Hours() / 24)
hours := int(uptime.Hours()) % 24
msg := fmt.Sprintf("**%s**\nUptime: %dd %dh", version.Full(), days, hours)
crashes := db.CrashCount(version.Short())
if crashes > 0 {
msg += fmt.Sprintf(" · %d crash(es) this version", crashes)
}
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
}
func (p *BotInfoPlugin) handleBotInfo(ctx MessageContext) error {
var sb strings.Builder
sb.WriteString("Bot Diagnostics\n\n")
sb.WriteString(fmt.Sprintf("**Bot Diagnostics** — %s\n\n", version.Short()))
// Uptime
uptime := time.Since(botStartTime)
@@ -110,6 +129,33 @@ func (p *BotInfoPlugin) handleBotInfo(ctx MessageContext) error {
sb.WriteString("LLM status: not configured\n")
}
// Crash stats
crashesThisVersion := db.CrashCount(version.Short())
crashesTotal := db.CrashCountAll()
sb.WriteString(fmt.Sprintf("\nCrashes: %d this version / %d total\n", crashesThisVersion, crashesTotal))
// Recent crashes (last 5)
d2 := db.Get()
rows, err := d2.Query(`SELECT plugin, message, created_at FROM crash_log ORDER BY id DESC LIMIT 5`)
if err == nil {
defer rows.Close()
var hasCrashes bool
for rows.Next() {
var plug, msg, ts string
if rows.Scan(&plug, &msg, &ts) != nil {
continue
}
if !hasCrashes {
sb.WriteString("Recent crashes:\n")
hasCrashes = true
}
if len(msg) > 80 {
msg = msg[:80] + "..."
}
sb.WriteString(fmt.Sprintf(" %s [%s] %s\n", ts, plug, msg))
}
}
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
}

View File

@@ -14,6 +14,26 @@ import (
"maunium.net/go/mautrix/id"
)
// fmtEuro formats a currency value with thousand separators, e.g. "€12,500".
func fmtEuro[T int | int64 | float64](v T) string {
n := int64(v)
if n < 0 {
return "-" + fmtEuro(-n)
}
s := fmt.Sprintf("%d", n)
if len(s) <= 3 {
return "€" + s
}
var out []byte
for i, c := range s {
if i > 0 && (len(s)-i)%3 == 0 {
out = append(out, ',')
}
out = append(out, byte(c))
}
return "€" + string(out)
}
// gamesRoom returns the configured GAMES_ROOM, or empty if unset.
func gamesRoom() id.RoomID {
return id.RoomID(os.Getenv("GAMES_ROOM"))
@@ -63,7 +83,8 @@ func NewEuroPlugin(client *mautrix.Client) *EuroPlugin {
}
}
func (p *EuroPlugin) Name() string { return "euro" }
func (p *EuroPlugin) Name() string { return "euro" }
func (p *EuroPlugin) Version() string { return "1.1.0" }
func (p *EuroPlugin) Commands() []CommandDef {
return []CommandDef{

View File

@@ -296,7 +296,8 @@ func NewHangmanPlugin(client *mautrix.Client, euro *EuroPlugin, dict *dreamclien
}
}
func (p *HangmanPlugin) Name() string { return "hangman" }
func (p *HangmanPlugin) Name() string { return "hangman" }
func (p *HangmanPlugin) Version() string { return "1.1.0" }
func (p *HangmanPlugin) Commands() []CommandDef {
return []CommandDef{
@@ -820,10 +821,11 @@ func (p *HangmanPlugin) endGame(roomID id.RoomID, game *hangmanGame) error {
payout = share * p.bonusMul
label = " (early solve bonus!)"
}
p.euro.Credit(uid, payout, "hangman_win")
p.recordHangmanScore(uid, payout)
net, _ := communityTax(uid, payout, 0.05)
p.euro.Credit(uid, net, "hangman_win")
p.recordHangmanScore(uid, net)
name := p.DisplayName(uid)
sb.WriteString(fmt.Sprintf(" **%s**: +€%d%s\n", name, int(payout), label))
sb.WriteString(fmt.Sprintf(" **%s**: +€%d%s\n", name, int(net), label))
}
}

View File

@@ -25,7 +25,8 @@ func NewLotteryPlugin(client *mautrix.Client, euro *EuroPlugin) *LotteryPlugin {
}
}
func (p *LotteryPlugin) Name() string { return "lottery" }
func (p *LotteryPlugin) Name() string { return "lottery" }
func (p *LotteryPlugin) Version() string { return "1.1.0" }
func (p *LotteryPlugin) Commands() []CommandDef {
return []CommandDef{
@@ -198,11 +199,9 @@ func (p *LotteryPlugin) handleLotteryOdds(ctx MessageContext) error {
| Match | Prize | Odds (approx.) |
|-------|-------|----------------|
| 5 of 5 | Jackpot (split among winners) | 1 in 142,506 |
| 4 of 5 | €1,000 (fixed) | 1 in 3,062 |
| 3 of 5 | €100 (fixed) | 1 in 141 |
| 2 of 5 | €10 (fixed) | 1 in 16 |
| 1 of 5 | €2 (fixed) | 1 in 4 |
| 0 of 5 | Nothing | — |
| 4 of 5 | €5,000 (fixed) | 1 in 3,062 |
| 3 of 5 | €500 (fixed) | 1 in 141 |
| 2 of 5 | €25 (fixed) | 1 in 16 |
Tickets: €1 each. Max 100 per week. 5 numbers from 130.
Minimum €500 pot required for jackpot payout.`
@@ -228,8 +227,8 @@ func (p *LotteryPlugin) handleLotteryHistory(ctx MessageContext) error {
if h.RolledOver > 0 {
sb.WriteString(fmt.Sprintf(" | Rolled over: €%d", h.RolledOver))
}
sb.WriteString(fmt.Sprintf("\n 4-match: %d | 3-match: %d | 2-match: %d | 1-match: %d\n\n",
h.Match4Winners, h.Match3Winners, h.Match2Winners, h.Match1Winners))
sb.WriteString(fmt.Sprintf("\n 4-match: %d | 3-match: %d | 2-match: %d\n\n",
h.Match4Winners, h.Match3Winners, h.Match2Winners))
}
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())

View File

@@ -14,10 +14,9 @@ import (
// ── Prize Tiers (fixed payouts) ─────────────────────────────────────────────
var lotteryFixedPrizes = map[int]int{
4: 1000,
3: 100,
2: 10,
1: 2,
4: 5000,
3: 500,
2: 25,
}
// ── Draw Ticker ─────────────────────────────────────────────────────────────
@@ -66,9 +65,9 @@ func (p *LotteryPlugin) reminderTicker() {
if gr != "" {
pot := communityPotBalance()
p.SendMessage(gr, fmt.Sprintf(
"🎟️ Lottery draw tomorrow. 23:59 UTC. Current pot: €%d. "+
"🎟️ Lottery draw tomorrow. 23:59 UTC. Current pot: %s. "+
"Tickets €1 each. Max 100 per player. `!lottery buy [N]` to enter.",
pot))
fmtEuro(pot)))
}
db.MarkJobCompleted(jobName, weekKey)
@@ -101,7 +100,7 @@ func (p *LotteryPlugin) executeDraw(weekStart string) {
tickets[i].MatchCount = &mc
prize := 0
if mc >= 1 && mc <= 4 {
if mc >= 2 && mc <= 4 {
prize = lotteryFixedPrizes[mc]
}
tickets[i].Prize = &prize
@@ -192,7 +191,7 @@ func (p *LotteryPlugin) executeDraw(weekStart string) {
Match4Winners: len(matchBuckets[4]),
Match3Winners: len(matchBuckets[3]),
Match2Winners: len(matchBuckets[2]),
Match1Winners: len(matchBuckets[1]),
Match1Winners: 0,
PotTotal: initialPot,
RolledOver: rolledOver,
}
@@ -205,6 +204,9 @@ func (p *LotteryPlugin) executeDraw(weekStart string) {
p.SendMessage(gr, announcement)
}
// DM each winner with their tickets and results.
p.dmWinners(winning, tickets)
// Cleanup old tickets.
lotteryCleanupOldTickets()
@@ -215,6 +217,58 @@ func (p *LotteryPlugin) executeDraw(weekStart string) {
"jackpot_winners", len(jackpotWinners))
}
func (p *LotteryPlugin) dmWinners(winning []int, tickets []lotteryTicket) {
// Group winning tickets by user.
byUser := make(map[id.UserID][]lotteryTicket)
for _, t := range tickets {
if t.MatchCount == nil || *t.MatchCount < 2 {
continue
}
byUser[t.UserID] = append(byUser[t.UserID], t)
}
for uid, userTickets := range byUser {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("🎟️ **Lottery Results** — Winning numbers: **%s**\n\n", formatLotteryNumbers(winning)))
totalWon := 0
for _, t := range userTickets {
matches := *t.MatchCount
prize := 0
if t.Prize != nil {
prize = *t.Prize
}
totalWon += prize
// Show ticket numbers with matches highlighted
sb.WriteString(fmt.Sprintf("🎫 %s — **%d match** → %s\n",
formatTicketWithMatches(t.Numbers, winning), matches, fmtEuro(prize)))
}
sb.WriteString(fmt.Sprintf("\n💰 **Total: %s**", fmtEuro(totalWon)))
if err := p.SendDM(uid, sb.String()); err != nil {
slog.Error("lottery: failed to DM winner", "user", uid, "err", err)
}
}
}
func formatTicketWithMatches(ticket, winning []int) string {
winSet := make(map[int]bool, len(winning))
for _, n := range winning {
winSet[n] = true
}
parts := make([]string, len(ticket))
for i, n := range ticket {
if winSet[n] {
parts[i] = fmt.Sprintf("**%d**", n)
} else {
parts[i] = fmt.Sprintf("%d", n)
}
}
return strings.Join(parts, " · ")
}
// ── Announcement Builder ────────────────────────────────────────────────────
func (p *LotteryPlugin) buildDrawAnnouncement(winning []int, h *lotteryHistoryRow, jackpotWinners []lotteryTicket) string {
@@ -227,10 +281,10 @@ func (p *LotteryPlugin) buildDrawAnnouncement(winning []int, h *lotteryHistoryRo
if len(jackpotWinners) > 0 && h.JackpotAmount > 0 {
names := p.resolveWinnerNames(jackpotWinners)
if len(jackpotWinners) == 1 {
sb.WriteString(fmt.Sprintf("Jackpot (5 match): **%s** — €%d 🎉\n", names[0], h.JackpotAmount))
sb.WriteString(fmt.Sprintf("Jackpot (5 match): **%s** — %s 🎉\n", names[0], fmtEuro(h.JackpotAmount)))
} else {
sb.WriteString(fmt.Sprintf("Jackpot (5 match): Split between %s. €%d each. 🎉\n",
joinNames(names), h.JackpotAmount))
sb.WriteString(fmt.Sprintf("Jackpot (5 match): Split between %s. %s each. 🎉\n",
joinNames(names), fmtEuro(h.JackpotAmount)))
}
} else if len(jackpotWinners) > 0 && h.JackpotAmount == 0 {
sb.WriteString("Jackpot withheld — pot insufficient. Rolls to next week. Fixed tiers paid as normal.\n")
@@ -239,13 +293,12 @@ func (p *LotteryPlugin) buildDrawAnnouncement(winning []int, h *lotteryHistoryRo
}
// Fixed tiers.
sb.WriteString(fmt.Sprintf("4 match: %d winner(s) — €1,000 each\n", h.Match4Winners))
sb.WriteString(fmt.Sprintf("3 match: %d winner(s) — €100 each\n", h.Match3Winners))
sb.WriteString(fmt.Sprintf("2 match: %d winner(s) — €10 each\n", h.Match2Winners))
sb.WriteString(fmt.Sprintf("1 match: %d winner(s) — €2 each\n", h.Match1Winners))
sb.WriteString(fmt.Sprintf("4 match: %d winner(s) — €5,000 each\n", h.Match4Winners))
sb.WriteString(fmt.Sprintf("3 match: %d winner(s) — €500 each\n", h.Match3Winners))
sb.WriteString(fmt.Sprintf("2 match: %d winner(s) — €25 each\n", h.Match2Winners))
distributed := h.PotTotal - h.RolledOver
sb.WriteString(fmt.Sprintf("\nPot distributed: €%d. Next draw: Friday. Tickets on sale now.", distributed))
sb.WriteString(fmt.Sprintf("\nPot distributed: %s. Next draw: Friday. Tickets on sale now.", fmtEuro(distributed)))
return sb.String()
}

View File

@@ -13,6 +13,7 @@ import (
"gogobee/internal/db"
"gogobee/internal/util"
"gogobee/internal/version"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/event"
@@ -57,6 +58,19 @@ type Plugin interface {
Init() error
}
// Versioned is an optional interface plugins can implement to declare their version.
type Versioned interface {
Version() string
}
// PluginVersion returns the version for a plugin, or "1.0.0" if it doesn't implement Versioned.
func PluginVersion(p Plugin) string {
if v, ok := p.(Versioned); ok {
return v.Version()
}
return "1.0.0"
}
// dmCache maps user IDs to their DM room IDs to avoid creating duplicate rooms.
var (
dmCache = make(map[id.UserID]id.RoomID)
@@ -764,6 +778,7 @@ func safeGo(label string, fn func()) {
defer func() {
if r := recover(); r != nil {
slog.Error("goroutine panic recovered", "label", label, "panic", r)
db.RecordCrash(version.Short(), label, fmt.Sprintf("%v", r))
}
}()
fn()

View File

@@ -30,7 +30,8 @@ func NewStatsPlugin(client *mautrix.Client) *StatsPlugin {
}
}
func (p *StatsPlugin) Name() string { return "stats" }
func (p *StatsPlugin) Name() string { return "stats" }
func (p *StatsPlugin) Version() string { return "1.1.0" }
func (p *StatsPlugin) Commands() []CommandDef {
return []CommandDef{
@@ -199,6 +200,9 @@ func (p *StatsPlugin) handleStats(ctx MessageContext) error {
avgWords = totalWords / totalMsg
}
var taxPaid int
_ = d.QueryRow(`SELECT total_paid FROM tax_ledger WHERE user_id = ?`, string(target)).Scan(&taxPaid)
msg := fmt.Sprintf(
"📊 Stats for %s\n"+
"Messages: %s | Words: %s | Chars: %s\n"+
@@ -213,6 +217,9 @@ func (p *StatsPlugin) handleStats(ctx MessageContext) error {
formatNumber(nightMsg), formatNumber(morningMsg),
avgWords,
)
if taxPaid > 0 {
msg += fmt.Sprintf("\n🏛 Community tax paid: %s", fmtEuro(taxPaid))
}
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
}
@@ -322,8 +329,15 @@ func (p *StatsPlugin) handleSuperStats(ctx MessageContext) error {
var achievementCount int
_ = d.QueryRow(`SELECT COUNT(*) FROM achievements WHERE user_id = ?`, uid).Scan(&achievementCount)
sb.WriteString(fmt.Sprintf("⭐ **Level %d** · %s XP · 💰 €%.0f · 🏅 %d achievements\n\n",
level, formatNumber(xp), balance, achievementCount))
var taxPaid int
_ = d.QueryRow(`SELECT total_paid FROM tax_ledger WHERE user_id = ?`, uid).Scan(&taxPaid)
taxLine := ""
if taxPaid > 0 {
taxLine = fmt.Sprintf(" · 🏛️ %s tax paid", fmtEuro(taxPaid))
}
sb.WriteString(fmt.Sprintf("⭐ **Level %d** · %s XP · 💰 €%.0f · 🏅 %d achievements%s\n\n",
level, formatNumber(xp), balance, achievementCount, taxLine))
// ── Chat Stats ──
var totalMsg, totalWords, totalLinks, totalEmojis, totalQuestions int

View File

@@ -411,7 +411,8 @@ func NewUnoPlugin(client *mautrix.Client, euro *EuroPlugin) *UnoPlugin {
}
}
func (p *UnoPlugin) Name() string { return "uno" }
func (p *UnoPlugin) Name() string { return "uno" }
func (p *UnoPlugin) Version() string { return "1.2.0" }
func (p *UnoPlugin) Commands() []CommandDef {
return []CommandDef{
@@ -1689,7 +1690,8 @@ func (p *UnoPlugin) suddenDeathWinner(game *unoGame) {
if playerWins {
payout := p.claimFromPot(game.wager)
totalPayout := game.wager + payout
p.euro.Credit(game.playerID, totalPayout, "uno_win")
net, _ := communityTax(game.playerID, totalPayout, 0.05)
p.euro.Credit(game.playerID, net, "uno_win")
newPot := p.getPot()
p.SendMessage(game.dmRoomID, "🎉 **You win on points!**")
@@ -1885,7 +1887,8 @@ func (p *UnoPlugin) playerWins(game *unoGame) error {
potBefore := p.getPot()
payout := p.claimFromPot(game.wager)
totalPayout := game.wager + payout
p.euro.Credit(game.playerID, totalPayout, "uno_win")
net, _ := communityTax(game.playerID, totalPayout, 0.05)
p.euro.Credit(game.playerID, net, "uno_win")
newPot := p.getPot()
playerName := p.DisplayName(game.playerID)

View File

@@ -2564,7 +2564,8 @@ func (p *UnoPlugin) multiPlayerWins2(game *unoMultiGame, winner *unoMultiPlayer)
}
}
totalPot := game.ante * float64(humanCount+game.botAntes)
p.euro.Credit(winner.userID, totalPot, "uno_multi_win")
net, _ := communityTax(winner.userID, totalPot, 0.05)
p.euro.Credit(winner.userID, net, "uno_multi_win")
p.recordMultiGame(game, winner.userID, "sudden_death_win")
p.cleanupMultiGame(game)
@@ -2614,9 +2615,10 @@ func (p *UnoPlugin) multiPlayerWins(game *unoMultiGame, winner *unoMultiPlayer)
}
}
totalPot := game.ante * float64(humanCount+game.botAntes)
net, _ := communityTax(winner.userID, totalPot, 0.05)
name := p.DisplayName(winner.userID)
p.euro.Credit(winner.userID, totalPot, "uno_multi_win")
p.euro.Credit(winner.userID, net, "uno_multi_win")
p.SendMessage(game.roomID, fmt.Sprintf(
"🎉 **%s wins Multiplayer UNO!**\nPot: €%d | Turns: %d\n\n%s",

View File

@@ -49,7 +49,8 @@ func NewWordlePlugin(client *mautrix.Client, euro *EuroPlugin, dict *dreamclient
}
}
func (p *WordlePlugin) Name() string { return "wordle" }
func (p *WordlePlugin) Name() string { return "wordle" }
func (p *WordlePlugin) Version() string { return "2.0.0" }
func (p *WordlePlugin) Commands() []CommandDef {
return []CommandDef{
@@ -695,8 +696,9 @@ func (p *WordlePlugin) awardPrize(puzzle *WordlePuzzle) []WordlePayout {
if c.solver {
amount += solverBonus
}
p.euro.Credit(uid, float64(amount), "wordle_win")
payouts = append(payouts, WordlePayout{UserID: uid, Name: c.name, Amount: amount, Solver: c.solver})
net, _ := communityTax(uid, float64(amount), 0.05)
p.euro.Credit(uid, net, "wordle_win")
payouts = append(payouts, WordlePayout{UserID: uid, Name: c.name, Amount: int(net), Solver: c.solver})
}
return payouts
}

View File

@@ -0,0 +1,28 @@
package version
import (
"fmt"
"runtime"
)
const Version = "1.0.0"
var (
Commit = "dev"
BuildDate = "unknown"
)
func Full() string {
return fmt.Sprintf("GogoBee v%s (%s, %s, %s)", Version, Commit, BuildDate, runtime.Version())
}
func Short() string {
return fmt.Sprintf("v%s-%s", Version, shortCommit())
}
func shortCommit() string {
if len(Commit) > 7 {
return Commit[:7]
}
return Commit
}