feat(economy): add /lootbox mystery box (coin sink with random reward)

A pay-to-open box (1000 coins) with a weighted reward: coin tiers (usually a net
loss - the sink), a random 30-min earn/exp buff, or a rare jackpot. All rolling
lives in do_open_lootbox for testability; the command adds a short reveal.

- New core module lootbox.py; extracted consumables.grant_buff (reused by both
  consumables and lootbox) to avoid duplicating the buff-stacking logic.
- New lootboxes_opened stat; pending schema syncs as a number automatically.
- Added /consumables, /lootbox, /vanity to the help embed (the first two were
  previously missing from /help).

Tests cover charging, insufficient/banned, the coin and buff outcomes, the
net-vs-reward invariant, and that balance never goes negative.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013VbAVsrZuYesea99mPMmPT
This commit is contained in:
Rene Arumetsa
2026-09-04 02:44:46 +03:00
parent 8481003edf
commit b34c1e22da
9 changed files with 253 additions and 8 deletions

View File

@@ -59,6 +59,7 @@ from .economy import (
CONSUMABLES_UI,
CONSUMABLE_DESCRIPTIONS,
VANITY_UI,
LOOTBOX_UI,
JAILED_UI,
SHOP_BTN,
DAILY_UI,
@@ -152,6 +153,7 @@ __all__ = [
'CONSUMABLES_UI',
'CONSUMABLE_DESCRIPTIONS',
'VANITY_UI',
'LOOTBOX_UI',
'JAILED_UI',
'SHOP_BTN',
'DAILY_UI',

View File

@@ -75,6 +75,7 @@ CMD: dict[str, str] = {
"quests": "Vaata oma päeva- ja nädalaülesandeid ning nõua auhinnad",
"consumables": "Sirvi ja osta turgutusi (korduvostetavad boostid)",
"vanity": "Staatusepood - osta ja kanna kosmeetilisi tiitleid",
"lootbox": "Ava õnnekast - juhuslik auhind müntide või boonuse näol",
}
# ---------------------------------------------------------------------------
@@ -153,6 +154,9 @@ HELP_CATEGORIES: dict[str, dict] = {
("/leaderboard", "TipiBOTi edetabel - kes on kõige rikkam?"),
("/shop", "Sirvi TipiBOTi poodi"),
("/buy <item>", "Osta ese TipiBOTi poodist"),
("/consumables", "Osta korduvostetavaid turgutusi (ajutised boostid)."),
("/lootbox", "Ava õnnekast (1000 ⬡) - juhuslik auhind: mündid või ajutine boonus."),
("/vanity", "Staatusepood - osta ja kanna kosmeetilisi tiitleid (näha /profile-l)."),
("/request <amount> <reason> [target]", "Saada crowdfundingu taotlus. Keegi saab 'Toeta' nuppu vajutades raha kanda (taotlus kehtib 5 minutit)."),
("/reminders", "DM meeldetuletused on vaikimisi sees. Kasuta seda käsku, et lülitada sisse/välja, milliseid käsklusi meelde tuletada."),
],

View File

@@ -18,6 +18,7 @@ __all__ = [
'CONSUMABLES_UI',
'CONSUMABLE_DESCRIPTIONS',
'VANITY_UI',
'LOOTBOX_UI',
'JAILED_UI',
'SHOP_BTN',
'DAILY_UI',
@@ -257,6 +258,23 @@ VANITY_UI: dict[str, str] = {
"unequipped": "Märk eemaldatud - su profiil on jälle tavaline.",
}
# ---------------------------------------------------------------------------
# Lootbox (pay-to-open mystery box, a coin sink with a random reward)
# ---------------------------------------------------------------------------
LOOTBOX_UI: dict[str, str] = {
"title": "🎁 Õnnekast",
"opening": "🎁 Avan õnnekasti...",
"coins_small": "🪙 Leidsid põhjast paar münti: **+{coins}**",
"coins_medium": "💰 Korralik saak: **+{coins}**",
"coins_big": "💎 Suur õnn: **+{coins}**",
"jackpot": "🎉 **JACKPOT!** **+{coins}**",
"buff_earn": "⚡ Boonus: teenimine **×2** järgmiseks **{min} minutiks**!",
"buff_exp": "✨ Boonus: EXP **×2** järgmiseks **{min} minutiks**!",
"foot_win": "Netovõit: +{net} · Saldo: {balance}",
"foot_loss": "Netokahjum: {net} · Saldo: {balance}",
"foot_buff": "Saldo: {balance}",
}
JAILED_UI: dict[str, str] = {
"title": "🔒 Praegu vanglas",
"empty": "Kõik on vabad! Vanglas pole kedagi.",