forked from sass/tipibot
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:
@@ -1,5 +1,6 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable
|
||||
|
||||
import discord
|
||||
@@ -322,6 +323,46 @@ def register_economy_support_commands(
|
||||
msg = S.VANITY_UI["equipped"].format(emoji=v["emoji"], title=v["title"])
|
||||
await interaction.response.send_message(msg)
|
||||
|
||||
# -- /lootbox -----------------------------------------------------------
|
||||
@tree.command(name="lootbox", description=S.CMD["lootbox"])
|
||||
async def cmd_lootbox(interaction: discord.Interaction):
|
||||
res = await economy.do_open_lootbox(interaction.user.id)
|
||||
if not res["ok"]:
|
||||
if res["reason"] == "db_error":
|
||||
await reply_db_error(interaction)
|
||||
elif res["reason"] == "banned":
|
||||
await interaction.response.send_message(S.MSG_BANNED, ephemeral=True)
|
||||
else:
|
||||
await interaction.response.send_message(
|
||||
S.ERR["broke_need"].format(need=coin(res["need"])), ephemeral=True
|
||||
)
|
||||
return
|
||||
|
||||
# Suspense: show the box opening, then reveal the reward.
|
||||
await interaction.response.send_message(
|
||||
embed=discord.Embed(
|
||||
title=S.LOOTBOX_UI["title"], description=S.LOOTBOX_UI["opening"], color=0xF4C430
|
||||
)
|
||||
)
|
||||
msg = await interaction.original_response()
|
||||
await asyncio.sleep(1.2)
|
||||
|
||||
if res["buff_kind"]:
|
||||
line = S.LOOTBOX_UI["buff_" + res["buff_kind"]].format(min=res["buff_min"])
|
||||
foot = S.LOOTBOX_UI["foot_buff"].format(balance=coin(res["balance"]))
|
||||
color = 0x5865F2
|
||||
else:
|
||||
line = S.LOOTBOX_UI[res["outcome"]].format(coins=coin(res["reward_coins"]))
|
||||
if res["net"] >= 0:
|
||||
foot = S.LOOTBOX_UI["foot_win"].format(net=coin(res["net"]), balance=coin(res["balance"]))
|
||||
color = 0x57F287
|
||||
else:
|
||||
foot = S.LOOTBOX_UI["foot_loss"].format(net=coin(abs(res["net"])), balance=coin(res["balance"]))
|
||||
color = 0xF4C430 if res["outcome"] != "coins_small" else 0xED4245
|
||||
embed = discord.Embed(title=S.LOOTBOX_UI["title"], description=line, color=color)
|
||||
embed.set_footer(text=foot)
|
||||
await msg.edit(embed=embed)
|
||||
|
||||
class RemindersSelect(discord.ui.Select):
|
||||
def __init__(self, user_id: int, current: list[str]):
|
||||
self.user_id = user_id
|
||||
|
||||
Reference in New Issue
Block a user