mirror of
https://github.com/prosolis/gogobee.git
synced 2026-07-16 17:02:42 +00:00
Coop lockout fix, holidays full list, !fx convert, lottery cleanup
- Co-op: when a run wipes or completes, clear the 99-sentinel combat lock for participants and pin combat_actions_used to maxCombatActions. Previously the lock only released on the next midnight reset, so members were stuck unable to combat for the rest of the day after their run ended. - Holidays: !holidays now prints the full list. Previously it called the same featured-only formatter as TwinBee's morning auto-post, so users saw the same truncated message twice with a misleading "Use !holidays for the full list" hint that pointed at itself. - Forex: add currency conversion. Auto-detects when the first arg is numeric (`!fx 1500 USD EUR`) and also accepts an explicit `convert` subcommand. Tolerant parsing: "to"/"into"/"in"/"=" filters, comma and k/m suffix support, and any of `1500 USD EUR`, `1500 USD/EUR`, `USD/EUR 1500` orderings. Includes parser + comma-formatter tests. - Lottery: drop 2-match prize tier. At 1/16 odds × €25 it was +EV per ticket on its own and bled the community pot (~€775/wk recently), preventing jackpot rollover from accumulating. Removed from scoring, payout loops, draw announcement, !lottery odds, and !lottery history. Match2Winners DB column kept (history rows preserved); always written as 0 going forward. - Lottery: shift week boundary so Sat/Sun return next Monday. Friday's 23:59 draw used to leave players locked at their 100-ticket cap until next Monday rolled in; now the cap resets immediately after the draw. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -268,7 +268,9 @@ func (p *AdventurePlugin) resolveArina(ctx MessageContext, char *AdventureCharac
|
|||||||
if err := saveAdvCharacter(char); err != nil {
|
if err := saveAdvCharacter(char); err != nil {
|
||||||
slog.Error("npc: failed to save arina buff", "user", ctx.Sender, "err", err)
|
slog.Error("npc: failed to save arina buff", "user", ctx.Sender, "err", err)
|
||||||
}
|
}
|
||||||
return p.SendDM(ctx.Sender, fmt.Sprintf("_%s_", arinaAcceptLine))
|
return p.SendDM(ctx.Sender, fmt.Sprintf(
|
||||||
|
"_She snatches the money from you, almost rudely, then looks at you expectantly._\n\n\"%s\"\n\n_She walks away just as quickly as she came._",
|
||||||
|
arinaAcceptLine))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Declined — no mechanical effect, just insults
|
// Declined — no mechanical effect, just insults
|
||||||
|
|||||||
@@ -177,6 +177,21 @@ func completeCoopRun(runID int, status string, reward int) error {
|
|||||||
// reset (which would otherwise zero them) and at bot startup. Combat stays
|
// reset (which would otherwise zero them) and at bot startup. Combat stays
|
||||||
// locked for the full duration of the run — players in a co-op cannot also
|
// locked for the full duration of the run — players in a co-op cannot also
|
||||||
// solo-grind dungeons or arena.
|
// solo-grind dungeons or arena.
|
||||||
|
// unlockCoopCombatActionsForRun clears the 99-sentinel lock on the given run's
|
||||||
|
// members and pins combat_actions_used to the daily cap (= maxCombatActions),
|
||||||
|
// so they can't squeeze in extra combat today but are not blocked into
|
||||||
|
// tomorrow if the midnight reset is delayed. Called when a run ends (wipe or
|
||||||
|
// complete). Idempotent.
|
||||||
|
func unlockCoopCombatActionsForRun(runID int) error {
|
||||||
|
d := db.Get()
|
||||||
|
_, err := d.Exec(`UPDATE adventure_characters
|
||||||
|
SET combat_actions_used = ?
|
||||||
|
WHERE combat_actions_used > ?
|
||||||
|
AND user_id IN (SELECT user_id FROM coop_dungeon_members WHERE run_id = ?)`,
|
||||||
|
maxCombatActions, maxCombatActions, runID)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
func lockCoopCombatActions() error {
|
func lockCoopCombatActions() error {
|
||||||
d := db.Get()
|
d := db.Get()
|
||||||
_, err := d.Exec(`UPDATE adventure_characters
|
_, err := d.Exec(`UPDATE adventure_characters
|
||||||
|
|||||||
@@ -341,6 +341,7 @@ func (p *AdventurePlugin) coopResolveFloor(run *CoopRun) error {
|
|||||||
_ = p.SendMessage(gr, strings.Join(parts, "\n\n"))
|
_ = p.SendMessage(gr, strings.Join(parts, "\n\n"))
|
||||||
}
|
}
|
||||||
_ = completeCoopRun(run.ID, "wiped", 0)
|
_ = completeCoopRun(run.ID, "wiped", 0)
|
||||||
|
_ = unlockCoopCombatActionsForRun(run.ID)
|
||||||
_ = setCoopRunLastResolvedDay(run.ID, day)
|
_ = setCoopRunLastResolvedDay(run.ID, day)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -350,6 +351,7 @@ func (p *AdventurePlugin) coopResolveFloor(run *CoopRun) error {
|
|||||||
reward := def.rewardBase
|
reward := def.rewardBase
|
||||||
p.coopDistributeReward(run, reward, members)
|
p.coopDistributeReward(run, reward, members)
|
||||||
_ = completeCoopRun(run.ID, "complete", reward)
|
_ = completeCoopRun(run.ID, "complete", reward)
|
||||||
|
_ = unlockCoopCombatActionsForRun(run.ID)
|
||||||
_ = setCoopRunLastResolvedDay(run.ID, day)
|
_ = setCoopRunLastResolvedDay(run.ID, day)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ func (p *ForexPlugin) Name() string { return "forex" }
|
|||||||
|
|
||||||
func (p *ForexPlugin) Commands() []CommandDef {
|
func (p *ForexPlugin) Commands() []CommandDef {
|
||||||
return []CommandDef{
|
return []CommandDef{
|
||||||
{Name: "fx", Description: "Forex rates and analysis", Usage: "!fx rate [EUR|JPY] · !fx report [EUR|JPY] · !fx setalert <cur> <rate> · !fx alerts · !fx delalert <cur> <rate>", Category: "Entertainment"},
|
{Name: "fx", Description: "Forex rates, conversion, and analysis", Usage: "!fx rate [EUR|JPY] · !fx 1500 USD to EUR · !fx report [EUR|JPY] · !fx setalert <cur> <rate> · !fx alerts · !fx delalert <cur> <rate>", Category: "Entertainment"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +52,23 @@ func (p *ForexPlugin) OnMessage(ctx MessageContext) error {
|
|||||||
args := strings.TrimSpace(p.GetArgs(ctx.Body, "fx"))
|
args := strings.TrimSpace(p.GetArgs(ctx.Body, "fx"))
|
||||||
if args == "" {
|
if args == "" {
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID,
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||||
"Usage: `!fx rate [EUR|JPY]` · `!fx report [EUR|JPY]` · `!fx setalert <currency> <rate>` · `!fx alerts` · `!fx delalert <currency> <rate>`")
|
"Usage: `!fx rate [EUR|JPY]` · `!fx 1500 USD to EUR` · `!fx report [EUR|JPY]` · `!fx setalert <currency> <rate>` · `!fx alerts` · `!fx delalert <currency> <rate>`")
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Fields(args)
|
parts := strings.Fields(args)
|
||||||
sub := strings.ToLower(parts[0])
|
sub := strings.ToLower(parts[0])
|
||||||
|
|
||||||
|
// Auto-detect conversion: if the first token parses as a number,
|
||||||
|
// treat the whole arg list as a convert request (e.g. `!fx 1500 USD EUR`).
|
||||||
|
if _, err := fxParseAmount(parts[0]); err == nil {
|
||||||
|
safeGo("forex-handler", func() {
|
||||||
|
if err := p.cmdConvert(ctx, parts); err != nil {
|
||||||
|
slog.Error("forex: handler error", "err", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// DB-only and instant subcommands
|
// DB-only and instant subcommands
|
||||||
switch sub {
|
switch sub {
|
||||||
case "setalert":
|
case "setalert":
|
||||||
@@ -85,6 +96,13 @@ func (p *ForexPlugin) OnMessage(ctx MessageContext) error {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
return nil
|
return nil
|
||||||
|
case "convert":
|
||||||
|
safeGo("forex-handler", func() {
|
||||||
|
if err := p.cmdConvert(ctx, parts[1:]); err != nil {
|
||||||
|
slog.Error("forex: handler error", "err", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return nil
|
||||||
default:
|
default:
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Unknown subcommand `%s`. Try `!fx help`.", parts[0]))
|
return p.SendReply(ctx.RoomID, ctx.EventID, fmt.Sprintf("Unknown subcommand `%s`. Try `!fx help`.", parts[0]))
|
||||||
}
|
}
|
||||||
@@ -94,6 +112,7 @@ const fxHelpText = "**Forex Commands**\n\n" +
|
|||||||
"`!fx rate [EUR|JPY|CAD]` — current rate + quick signal\n" +
|
"`!fx rate [EUR|JPY|CAD]` — current rate + quick signal\n" +
|
||||||
"`!fx rate EUR/USD` — cross-pair rate from first currency's perspective\n" +
|
"`!fx rate EUR/USD` — cross-pair rate from first currency's perspective\n" +
|
||||||
"`!fx report [EUR|JPY|CAD]` — full analysis (averages, 52w range, buy score)\n" +
|
"`!fx report [EUR|JPY|CAD]` — full analysis (averages, 52w range, buy score)\n" +
|
||||||
|
"`!fx 1500 USD to EUR` — convert an amount (also: `!fx convert 1500 USD EUR`)\n" +
|
||||||
"`!fx setalert <currency> <rate>` — alert when USD/currency reaches threshold\n" +
|
"`!fx setalert <currency> <rate>` — alert when USD/currency reaches threshold\n" +
|
||||||
"`!fx alerts` — list active alerts in this room\n" +
|
"`!fx alerts` — list active alerts in this room\n" +
|
||||||
"`!fx delalert <currency> <rate>` — remove an alert\n" +
|
"`!fx delalert <currency> <rate>` — remove an alert\n" +
|
||||||
|
|||||||
193
internal/plugin/forex_convert.go
Normal file
193
internal/plugin/forex_convert.go
Normal file
@@ -0,0 +1,193 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log/slog"
|
||||||
|
"math"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fxParseAmount parses a numeric token, allowing thousands separators
|
||||||
|
// ("1,500", "1500", "1500.50", "1.5k"). Returns an error on a non-numeric
|
||||||
|
// token. Negative amounts are rejected.
|
||||||
|
func fxParseAmount(tok string) (float64, error) {
|
||||||
|
t := strings.TrimSpace(tok)
|
||||||
|
t = strings.ReplaceAll(t, ",", "")
|
||||||
|
t = strings.ReplaceAll(t, "_", "")
|
||||||
|
multiplier := 1.0
|
||||||
|
if len(t) > 0 {
|
||||||
|
switch strings.ToLower(t[len(t)-1:]) {
|
||||||
|
case "k":
|
||||||
|
multiplier = 1_000
|
||||||
|
t = t[:len(t)-1]
|
||||||
|
case "m":
|
||||||
|
multiplier = 1_000_000
|
||||||
|
t = t[:len(t)-1]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
v, err := strconv.ParseFloat(t, 64)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
if v < 0 || math.IsNaN(v) || math.IsInf(v, 0) {
|
||||||
|
return 0, fmt.Errorf("non-positive amount")
|
||||||
|
}
|
||||||
|
return v * multiplier, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fxParseConvert parses a conversion request from free-form args.
|
||||||
|
// Accepts: `1500 USD EUR`, `1500 USD to EUR`, `1500 USD/EUR`, `USD/EUR 1500`,
|
||||||
|
// `1500USD EUR` is NOT supported (keep parsing simple — require whitespace).
|
||||||
|
// "to" / "into" / "in" are filtered as noise tokens.
|
||||||
|
func fxParseConvert(args []string) (amount float64, base, quote string, err error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return 0, "", "", fmt.Errorf("no arguments")
|
||||||
|
}
|
||||||
|
tokens := make([]string, 0, len(args))
|
||||||
|
for _, a := range args {
|
||||||
|
switch strings.ToLower(a) {
|
||||||
|
case "to", "into", "in", "=":
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
tokens = append(tokens, a)
|
||||||
|
}
|
||||||
|
|
||||||
|
var amountSet bool
|
||||||
|
var currencies []string
|
||||||
|
for _, tok := range tokens {
|
||||||
|
// Pair form: USD/EUR
|
||||||
|
if strings.ContainsAny(tok, "/|-") {
|
||||||
|
pair := fxParsePair([]string{tok})
|
||||||
|
if pair != nil {
|
||||||
|
if base != "" {
|
||||||
|
return 0, "", "", fmt.Errorf("multiple currency pairs")
|
||||||
|
}
|
||||||
|
base, quote = pair.Base, pair.Quote
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Number?
|
||||||
|
if v, perr := fxParseAmount(tok); perr == nil {
|
||||||
|
if amountSet {
|
||||||
|
return 0, "", "", fmt.Errorf("multiple amounts")
|
||||||
|
}
|
||||||
|
amount = v
|
||||||
|
amountSet = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Currency code?
|
||||||
|
up := strings.ToUpper(tok)
|
||||||
|
if fxIsSupported(up) {
|
||||||
|
currencies = append(currencies, up)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
return 0, "", "", fmt.Errorf("unrecognized token: %s", tok)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !amountSet {
|
||||||
|
return 0, "", "", fmt.Errorf("missing amount")
|
||||||
|
}
|
||||||
|
if base == "" {
|
||||||
|
if len(currencies) != 2 {
|
||||||
|
return 0, "", "", fmt.Errorf("expected two currencies, got %d", len(currencies))
|
||||||
|
}
|
||||||
|
base, quote = currencies[0], currencies[1]
|
||||||
|
} else if len(currencies) != 0 {
|
||||||
|
return 0, "", "", fmt.Errorf("currency pair plus extra currency")
|
||||||
|
}
|
||||||
|
if base == quote {
|
||||||
|
return 0, "", "", fmt.Errorf("base and quote are the same")
|
||||||
|
}
|
||||||
|
return amount, base, quote, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *ForexPlugin) cmdConvert(ctx MessageContext, args []string) error {
|
||||||
|
amount, base, quote, err := fxParseConvert(args)
|
||||||
|
if err != nil {
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID,
|
||||||
|
"Usage: `!fx 1500 USD to EUR` (or `!fx convert 1500 USD EUR`, `!fx convert USD/EUR 1500`)")
|
||||||
|
}
|
||||||
|
|
||||||
|
pair := &fxPair{Base: base, Quote: quote}
|
||||||
|
rate, err := p.fxLivePairRate(pair)
|
||||||
|
if err != nil {
|
||||||
|
slog.Error("forex: fetch rates failed", "err", err)
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, "Could not fetch rates. Try again shortly.")
|
||||||
|
}
|
||||||
|
|
||||||
|
converted := amount * rate
|
||||||
|
baseEmoji := fxEmoji(base)
|
||||||
|
quoteEmoji := fxEmoji(quote)
|
||||||
|
|
||||||
|
msg := fmt.Sprintf("%s **%s %s** = %s **%s %s**\n_1 %s = %s %s_",
|
||||||
|
baseEmoji, fxFormatAmount(base, amount), base,
|
||||||
|
quoteEmoji, fxFormatAmount(quote, converted), quote,
|
||||||
|
base, fxFormatRate(quote, rate), quote)
|
||||||
|
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fxEmoji returns the flag emoji for a supported currency, falling back
|
||||||
|
// to USD's flag.
|
||||||
|
func fxEmoji(cur string) string {
|
||||||
|
if m, ok := fxMeta[cur]; ok && m.Emoji != "" {
|
||||||
|
return m.Emoji
|
||||||
|
}
|
||||||
|
if cur == "USD" {
|
||||||
|
return "🇺🇸"
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// fxFormatAmount formats a converted amount with thousands separators.
|
||||||
|
// JPY uses 0 decimal places (yen are not subdivided in practice for display),
|
||||||
|
// other currencies use 2.
|
||||||
|
func fxFormatAmount(currency string, amount float64) string {
|
||||||
|
decimals := 2
|
||||||
|
if currency == "JPY" {
|
||||||
|
decimals = 0
|
||||||
|
}
|
||||||
|
return fxAddCommas(amount, decimals)
|
||||||
|
}
|
||||||
|
|
||||||
|
// fxAddCommas formats a float with N decimal places and thousands separators.
|
||||||
|
func fxAddCommas(v float64, decimals int) string {
|
||||||
|
s := strconv.FormatFloat(v, 'f', decimals, 64)
|
||||||
|
intPart := s
|
||||||
|
fracPart := ""
|
||||||
|
if i := strings.IndexByte(s, '.'); i >= 0 {
|
||||||
|
intPart = s[:i]
|
||||||
|
fracPart = s[i:]
|
||||||
|
}
|
||||||
|
neg := false
|
||||||
|
if strings.HasPrefix(intPart, "-") {
|
||||||
|
neg = true
|
||||||
|
intPart = intPart[1:]
|
||||||
|
}
|
||||||
|
n := len(intPart)
|
||||||
|
if n <= 3 {
|
||||||
|
if neg {
|
||||||
|
return "-" + intPart + fracPart
|
||||||
|
}
|
||||||
|
return intPart + fracPart
|
||||||
|
}
|
||||||
|
var b strings.Builder
|
||||||
|
if neg {
|
||||||
|
b.WriteByte('-')
|
||||||
|
}
|
||||||
|
pre := n % 3
|
||||||
|
if pre > 0 {
|
||||||
|
b.WriteString(intPart[:pre])
|
||||||
|
if n > pre {
|
||||||
|
b.WriteByte(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := pre; i < n; i += 3 {
|
||||||
|
b.WriteString(intPart[i : i+3])
|
||||||
|
if i+3 < n {
|
||||||
|
b.WriteByte(',')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
b.WriteString(fracPart)
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
70
internal/plugin/forex_convert_test.go
Normal file
70
internal/plugin/forex_convert_test.go
Normal file
@@ -0,0 +1,70 @@
|
|||||||
|
package plugin
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestFxParseConvert(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
in []string
|
||||||
|
amt float64
|
||||||
|
base string
|
||||||
|
quote string
|
||||||
|
shouldErr bool
|
||||||
|
}{
|
||||||
|
{[]string{"1500", "USD", "EUR"}, 1500, "USD", "EUR", false},
|
||||||
|
{[]string{"1500", "USD", "to", "EUR"}, 1500, "USD", "EUR", false},
|
||||||
|
{[]string{"1500", "USD", "into", "EUR"}, 1500, "USD", "EUR", false},
|
||||||
|
{[]string{"1,500.50", "USD", "EUR"}, 1500.50, "USD", "EUR", false},
|
||||||
|
{[]string{"1.5k", "USD", "EUR"}, 1500, "USD", "EUR", false},
|
||||||
|
{[]string{"1500", "USD/EUR"}, 1500, "USD", "EUR", false},
|
||||||
|
{[]string{"USD/EUR", "1500"}, 1500, "USD", "EUR", false},
|
||||||
|
{[]string{"1500", "EUR/USD"}, 1500, "EUR", "USD", false},
|
||||||
|
{[]string{"1500", "JPY", "USD"}, 1500, "JPY", "USD", false},
|
||||||
|
|
||||||
|
{[]string{"USD", "EUR"}, 0, "", "", true}, // no amount
|
||||||
|
{[]string{"1500", "USD"}, 0, "", "", true}, // missing quote
|
||||||
|
{[]string{"1500", "USD", "USD"}, 0, "", "", true}, // same currency
|
||||||
|
{[]string{"1500", "ZZZ", "EUR"}, 0, "", "", true}, // unknown currency
|
||||||
|
{[]string{"1500", "USD", "EUR", "JPY"}, 0, "", "", true}, // too many currencies
|
||||||
|
{[]string{"1500", "100", "USD", "EUR"}, 0, "", "", true}, // multiple amounts
|
||||||
|
{[]string{"-100", "USD", "EUR"}, 0, "", "", true}, // negative
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
amt, base, quote, err := fxParseConvert(c.in)
|
||||||
|
if c.shouldErr {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("%v: expected error, got %v %s/%s", c.in, amt, base, quote)
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("%v: unexpected error: %v", c.in, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if amt != c.amt || base != c.base || quote != c.quote {
|
||||||
|
t.Errorf("%v: got %v %s/%s, want %v %s/%s", c.in, amt, base, quote, c.amt, c.base, c.quote)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFxAddCommas(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
v float64
|
||||||
|
decimals int
|
||||||
|
want string
|
||||||
|
}{
|
||||||
|
{1500, 2, "1,500.00"},
|
||||||
|
{1500.5, 2, "1,500.50"},
|
||||||
|
{1234567.89, 2, "1,234,567.89"},
|
||||||
|
{100, 2, "100.00"},
|
||||||
|
{0, 2, "0.00"},
|
||||||
|
{232500, 0, "232,500"},
|
||||||
|
{-1500, 2, "-1,500.00"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
got := fxAddCommas(c.v, c.decimals)
|
||||||
|
if got != c.want {
|
||||||
|
t.Errorf("fxAddCommas(%v, %d) = %q, want %q", c.v, c.decimals, got, c.want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -239,7 +239,7 @@ func (p *HolidaysPlugin) PostHolidays(roomID id.RoomID) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := p.formatHolidays(today, data.Holidays)
|
msg := p.formatHolidays(today, data.Holidays, false)
|
||||||
if err := p.SendMessage(roomID, msg); err != nil {
|
if err := p.SendMessage(roomID, msg); err != nil {
|
||||||
return fmt.Errorf("holidays: send: %w", err)
|
return fmt.Errorf("holidays: send: %w", err)
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,7 @@ func (p *HolidaysPlugin) handleHolidaysToday(ctx MessageContext) error {
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, "No holidays today.")
|
return p.SendReply(ctx.RoomID, ctx.EventID, "No holidays today.")
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := p.formatHolidays(today, data.Holidays)
|
msg := p.formatHolidays(today, data.Holidays, true)
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
return p.SendReply(ctx.RoomID, ctx.EventID, msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -340,13 +340,16 @@ func (p *HolidaysPlugin) handleHolidaysRange(ctx MessageContext, days int) error
|
|||||||
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *HolidaysPlugin) formatHolidays(date string, holidays []Holiday) string {
|
func (p *HolidaysPlugin) formatHolidays(date string, holidays []Holiday, full bool) string {
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
t, _ := time.Parse("2006-01-02", date)
|
t, _ := time.Parse("2006-01-02", date)
|
||||||
sb.WriteString(fmt.Sprintf("Today's Holidays — %s\n", t.Format("Monday, January 2, 2006")))
|
sb.WriteString(fmt.Sprintf("Today's Holidays — %s\n", t.Format("Monday, January 2, 2006")))
|
||||||
|
|
||||||
featured := p.selectFeatured(holidays)
|
shown := holidays
|
||||||
for _, h := range featured {
|
if !full {
|
||||||
|
shown = p.selectFeatured(holidays)
|
||||||
|
}
|
||||||
|
for _, h := range shown {
|
||||||
sb.WriteString(fmt.Sprintf("\n- %s", h.Name))
|
sb.WriteString(fmt.Sprintf("\n- %s", h.Name))
|
||||||
if h.Country != "" && h.Country != "International" {
|
if h.Country != "" && h.Country != "International" {
|
||||||
sb.WriteString(fmt.Sprintf(" [%s]", h.Country))
|
sb.WriteString(fmt.Sprintf(" [%s]", h.Country))
|
||||||
@@ -356,10 +359,12 @@ func (p *HolidaysPlugin) formatHolidays(date string, holidays []Holiday) string
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
remaining := len(holidays) - len(featured)
|
if !full {
|
||||||
|
remaining := len(holidays) - len(shown)
|
||||||
if remaining > 0 {
|
if remaining > 0 {
|
||||||
sb.WriteString(fmt.Sprintf("\n\n...and %d more. Use !holidays for the full list.", remaining))
|
sb.WriteString(fmt.Sprintf("\n\n...and %d more. Use !holidays for the full list.", remaining))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return sb.String()
|
return sb.String()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -201,7 +201,6 @@ func (p *LotteryPlugin) handleLotteryOdds(ctx MessageContext) error {
|
|||||||
| 5 of 5 | Jackpot (split among winners) | 1 in 142,506 |
|
| 5 of 5 | Jackpot (split among winners) | 1 in 142,506 |
|
||||||
| 4 of 5 | €5,000 (fixed) | 1 in 3,062 |
|
| 4 of 5 | €5,000 (fixed) | 1 in 3,062 |
|
||||||
| 3 of 5 | €500 (fixed) | 1 in 141 |
|
| 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 1–30.
|
Tickets: €1 each. Max 100 per week. 5 numbers from 1–30.
|
||||||
Minimum €500 pot required for jackpot payout.`
|
Minimum €500 pot required for jackpot payout.`
|
||||||
@@ -227,8 +226,8 @@ func (p *LotteryPlugin) handleLotteryHistory(ctx MessageContext) error {
|
|||||||
if h.RolledOver > 0 {
|
if h.RolledOver > 0 {
|
||||||
sb.WriteString(fmt.Sprintf(" | Rolled over: €%d", h.RolledOver))
|
sb.WriteString(fmt.Sprintf(" | Rolled over: €%d", h.RolledOver))
|
||||||
}
|
}
|
||||||
sb.WriteString(fmt.Sprintf("\n 4-match: %d | 3-match: %d | 2-match: %d\n\n",
|
sb.WriteString(fmt.Sprintf("\n 4-match: %d | 3-match: %d\n\n",
|
||||||
h.Match4Winners, h.Match3Winners, h.Match2Winners))
|
h.Match4Winners, h.Match3Winners))
|
||||||
}
|
}
|
||||||
|
|
||||||
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
return p.SendReply(ctx.RoomID, ctx.EventID, sb.String())
|
||||||
|
|||||||
@@ -37,14 +37,22 @@ type lotteryHistoryRow struct {
|
|||||||
|
|
||||||
// ── Week Helpers ────────────────────────────────────────────────────────────
|
// ── Week Helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
// lotteryCurrentWeekStart returns Monday of the current week as "2006-01-02".
|
// lotteryCurrentWeekStart returns the Monday of the lottery week currently
|
||||||
|
// accepting tickets. Mon–Fri returns this week's Monday; Sat–Sun returns
|
||||||
|
// next week's Monday — the Friday 23:59 UTC draw resets the week boundary,
|
||||||
|
// so Saturday onward, ticket purchases count toward the next draw.
|
||||||
func lotteryCurrentWeekStart() string {
|
func lotteryCurrentWeekStart() string {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
weekday := int(now.Weekday())
|
wd := now.Weekday() // Sunday=0, Monday=1, ..., Saturday=6
|
||||||
if weekday == 0 {
|
var monday time.Time
|
||||||
weekday = 7 // Sunday = 7
|
switch wd {
|
||||||
|
case time.Sunday:
|
||||||
|
monday = now.AddDate(0, 0, 1)
|
||||||
|
case time.Saturday:
|
||||||
|
monday = now.AddDate(0, 0, 2)
|
||||||
|
default:
|
||||||
|
monday = now.AddDate(0, 0, -(int(wd) - 1))
|
||||||
}
|
}
|
||||||
monday := now.AddDate(0, 0, -(weekday - 1))
|
|
||||||
return monday.Format("2006-01-02")
|
return monday.Format("2006-01-02")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ import (
|
|||||||
var lotteryFixedPrizes = map[int]int{
|
var lotteryFixedPrizes = map[int]int{
|
||||||
4: 5000,
|
4: 5000,
|
||||||
3: 500,
|
3: 500,
|
||||||
2: 25,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Draw Ticker ─────────────────────────────────────────────────────────────
|
// ── Draw Ticker ─────────────────────────────────────────────────────────────
|
||||||
@@ -100,7 +99,7 @@ func (p *LotteryPlugin) executeDraw(weekStart string) {
|
|||||||
tickets[i].MatchCount = &mc
|
tickets[i].MatchCount = &mc
|
||||||
|
|
||||||
prize := 0
|
prize := 0
|
||||||
if mc >= 2 && mc <= 4 {
|
if mc >= 3 && mc <= 4 {
|
||||||
prize = lotteryFixedPrizes[mc]
|
prize = lotteryFixedPrizes[mc]
|
||||||
}
|
}
|
||||||
tickets[i].Prize = &prize
|
tickets[i].Prize = &prize
|
||||||
@@ -114,7 +113,7 @@ func (p *LotteryPlugin) executeDraw(weekStart string) {
|
|||||||
|
|
||||||
// Calculate fixed tier payouts.
|
// Calculate fixed tier payouts.
|
||||||
fixedTotal := 0
|
fixedTotal := 0
|
||||||
for tier := 4; tier >= 1; tier-- {
|
for tier := 4; tier >= 3; tier-- {
|
||||||
count := len(matchBuckets[tier])
|
count := len(matchBuckets[tier])
|
||||||
fixedTotal += count * lotteryFixedPrizes[tier]
|
fixedTotal += count * lotteryFixedPrizes[tier]
|
||||||
}
|
}
|
||||||
@@ -141,7 +140,7 @@ func (p *LotteryPlugin) executeDraw(weekStart string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if actualFixed > 0 {
|
if actualFixed > 0 {
|
||||||
for tier := 4; tier >= 1; tier-- {
|
for tier := 4; tier >= 3; tier-- {
|
||||||
for _, t := range matchBuckets[tier] {
|
for _, t := range matchBuckets[tier] {
|
||||||
amount := int(float64(lotteryFixedPrizes[tier]) * prorateRatio)
|
amount := int(float64(lotteryFixedPrizes[tier]) * prorateRatio)
|
||||||
if amount > 0 {
|
if amount > 0 {
|
||||||
@@ -190,7 +189,7 @@ func (p *LotteryPlugin) executeDraw(weekStart string) {
|
|||||||
JackpotAmount: jackpotAmount,
|
JackpotAmount: jackpotAmount,
|
||||||
Match4Winners: len(matchBuckets[4]),
|
Match4Winners: len(matchBuckets[4]),
|
||||||
Match3Winners: len(matchBuckets[3]),
|
Match3Winners: len(matchBuckets[3]),
|
||||||
Match2Winners: len(matchBuckets[2]),
|
Match2Winners: 0,
|
||||||
Match1Winners: 0,
|
Match1Winners: 0,
|
||||||
PotTotal: initialPot,
|
PotTotal: initialPot,
|
||||||
RolledOver: rolledOver,
|
RolledOver: rolledOver,
|
||||||
@@ -221,7 +220,7 @@ func (p *LotteryPlugin) dmWinners(winning []int, tickets []lotteryTicket) {
|
|||||||
// Group winning tickets by user.
|
// Group winning tickets by user.
|
||||||
byUser := make(map[id.UserID][]lotteryTicket)
|
byUser := make(map[id.UserID][]lotteryTicket)
|
||||||
for _, t := range tickets {
|
for _, t := range tickets {
|
||||||
if t.MatchCount == nil || *t.MatchCount < 2 {
|
if t.MatchCount == nil || *t.MatchCount < 3 {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
byUser[t.UserID] = append(byUser[t.UserID], t)
|
byUser[t.UserID] = append(byUser[t.UserID], t)
|
||||||
@@ -295,7 +294,6 @@ func (p *LotteryPlugin) buildDrawAnnouncement(winning []int, h *lotteryHistoryRo
|
|||||||
// Fixed tiers.
|
// Fixed tiers.
|
||||||
sb.WriteString(fmt.Sprintf("4 match: %d winner(s) — €5,000 each\n", h.Match4Winners))
|
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("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
|
distributed := h.PotTotal - h.RolledOver
|
||||||
sb.WriteString(fmt.Sprintf("\nPot distributed: %s. Next draw: Friday. Tickets on sale now.", fmtEuro(distributed)))
|
sb.WriteString(fmt.Sprintf("\nPot distributed: %s. Next draw: Friday. Tickets on sale now.", fmtEuro(distributed)))
|
||||||
|
|||||||
Reference in New Issue
Block a user