from __future__ import annotations import asyncio import datetime import random import time from collections.abc import Awaitable, Callable, MutableSet import discord from discord import app_commands from core import economy import strings as S def register_economy_extra_commands( tree: app_commands.CommandTree, bot: discord.Client, coin: Callable[[int], str], cd_ts: Callable[[datetime.timedelta], str], parse_amount: Callable[[str, int], tuple[int | None, str | None]], ensure_level_role: Callable[[discord.Member, int], Awaitable[None]], active_games: MutableSet[int], ) -> None: active_heist = None # ----------------------------------------------------------------------- # /heist - multiplayer group robbery of the house # ----------------------------------------------------------------------- _HEIST_JOIN_WINDOW = 300 # seconds players have to join _HEIST_MIN_PLAYERS = 2 _HEIST_GLOBAL_CD = 14400 # seconds between heist events server-wide (4h) _HEIST_MAX_PLAYERS = 8 _HEIST_BASE_CHANCE = 0.35 # 35% solo _HEIST_CHANCE_STEP = 0.05 # +5% per extra player _HEIST_MAX_CHANCE = 0.65 # cap at 65% def _build_heist_story(participants: list[discord.Member], success: bool) -> list[str]: """Return a list of story lines for the heist narrative reveal.""" story = S.HEIST_STORY leader = participants[0].display_name if len(participants) == 1: names = f"**{leader}**" elif len(participants) == 2: names = S.HEIST_UI["names_duo"].format( a=participants[0].display_name, b=participants[1].display_name, ) elif len(participants) <= 4: names = S.HEIST_UI["names_sep"].join(f"**{p.display_name}**" for p in participants) else: names = S.HEIST_UI["names_crew"].format(leader=participants[0].display_name) vehicle = random.choice(story["vehicles"]) approach = random.choice(["sneaky", "loud"]) non_leaders = participants[1:] if len(participants) > 1 else participants def fill(tmpl: str) -> str: picked = random.choice(non_leaders).display_name return tmpl.format( leader=f"**{leader}**", member=f"**{picked}**", names=names, vehicle=vehicle, ) getaway_pool = "getaway_success" if success else "getaway_fail" return [ fill(random.choice(story["arrival"])), fill(random.choice(story[f"entry_{approach}"])), fill(random.choice(story["inside"])), fill(random.choice(story["vault"])), fill(random.choice(story["vault_open"])), fill(random.choice(story["police_inbound"])), fill(random.choice(story[getaway_pool])), fill(random.choice(story["escape_success" if success else "escape_fail"])), ] class HeistLobbyView(discord.ui.View): def __init__(self, organizer: discord.Member, organizer_has_jellyfin: bool = False): super().__init__(timeout=_HEIST_JOIN_WINDOW) self.organizer = organizer self.participants: list[discord.Member] = [organizer] self.message: discord.Message | None = None self.resolved = False self.jellyfin_holders: int = 1 if organizer_has_jellyfin else 0 def _chance(self) -> float: n = len(self.participants) base = min(_HEIST_BASE_CHANCE + _HEIST_CHANCE_STEP * (n - 1), _HEIST_MAX_CHANCE) jelly_bonus = 0.05 if self.jellyfin_holders > 0 else 0.0 return min(base + jelly_bonus, _HEIST_MAX_CHANCE) def _lobby_embed(self) -> discord.Embed: names = "\n".join(f"• {p.display_name}" for p in self.participants) desc = S.HEIST_UI["lobby_desc"].format( n=len(self.participants), max=_HEIST_MAX_PLAYERS, names=names, chance=int(self._chance() * 100), ts=int(self._timeout_expiry()), ) return discord.Embed(title=S.TITLE["heist_lobby"], description=desc, color=0xE67E22) def _timeout_expiry(self) -> float: return time.time() + (self.timeout or 0) @discord.ui.button(label=S.HEIST_UI["btn_join"], style=discord.ButtonStyle.danger) async def join(self, interaction: discord.Interaction, _: discord.ui.Button): if any(p.id == interaction.user.id for p in self.participants): await interaction.response.send_message(S.HEIST_UI["already_joined"], ephemeral=True) return if len(self.participants) >= _HEIST_MAX_PLAYERS: await interaction.response.send_message(S.ERR["heist_full"], ephemeral=True) return if interaction.user.id in active_games: await interaction.response.send_message(S.ERR["already_in_game"], ephemeral=True) return res = await economy.do_heist_check(interaction.user.id) if not res["ok"]: if res["reason"] == "banned": await interaction.response.send_message(S.MSG_BANNED, ephemeral=True) elif res["reason"] == "jailed": await interaction.response.send_message( S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])), ephemeral=True ) else: await interaction.response.send_message( S.CD_MSG["heist"].format(ts=cd_ts(res["remaining"])), ephemeral=True ) return self.participants.append(interaction.user) active_games.add(interaction.user.id) joiner_data = await economy.get_user(interaction.user.id) if "jellyfin" in joiner_data.get("items", []): self.jellyfin_holders += 1 await interaction.response.edit_message(embed=self._lobby_embed()) @discord.ui.button(label=S.HEIST_UI["btn_start"], style=discord.ButtonStyle.success) async def start_now(self, interaction: discord.Interaction, _: discord.ui.Button): if interaction.user.id != self.organizer.id: await interaction.response.send_message(S.HEIST_UI["only_organizer"], ephemeral=True) return if len(self.participants) < _HEIST_MIN_PLAYERS: await interaction.response.send_message( S.ERR["heist_min_players"].format(min=_HEIST_MIN_PLAYERS), ephemeral=True ) return await self._resolve(interaction) async def _resolve(self, interaction: discord.Interaction | None = None) -> None: nonlocal active_heist if self.resolved: return self.resolved = True active_heist = None self.stop() self.clear_items() for p in self.participants: active_games.discard(p.id) n = len(self.participants) channel = interaction.channel if interaction else self.message.channel if self.message else None if n < _HEIST_MIN_PLAYERS: embed = discord.Embed( title=S.TITLE["heist_cancel"], description=S.HEIST_UI["cancel_desc"].format(min=_HEIST_MIN_PLAYERS), color=0x99AAB5, ) if interaction and not interaction.response.is_done(): await interaction.response.edit_message(embed=embed, view=self) elif self.message: try: await self.message.edit(embed=embed, view=self) except discord.HTTPException: pass return success = random.random() < self._chance() story_lines = _build_heist_story(self.participants, success) lobby_done = discord.Embed( title=S.HEIST_UI["started_title"], description=S.HEIST_UI["started_desc"].format(n=n), color=0x99AAB5, ) if interaction and not interaction.response.is_done(): await interaction.response.edit_message(embed=lobby_done, view=self) elif self.message: try: await self.message.edit(embed=lobby_done, view=self) except discord.HTTPException: pass if channel: story_embed = discord.Embed(title=S.HEIST_UI["story_title"], description="", color=0xE67E22) story_msg = await channel.send(embed=story_embed) accumulated = "" for i, line in enumerate(story_lines): await asyncio.sleep(random.uniform(3.0, 4.5)) accumulated += ("\n\n" if i > 0 else "") + line story_embed.description = accumulated try: await story_msg.edit(embed=story_embed) except discord.HTTPException: pass await asyncio.sleep(2.0) res = await economy.do_heist_resolve([p.id for p in self.participants], success) payout_each = res["payout_each"] names_str = "\n".join(f"• {p.display_name}" for p in self.participants) guild = interaction.guild if interaction else self.message.guild if self.message else None if success: result_desc = S.HEIST_UI["win_desc"].format(names=names_str, payout=coin(payout_each)) result_embed = discord.Embed( title=S.TITLE["heist_win"], description=result_desc, color=0x57F287, ) for p in self.participants: exp_res = await economy.award_exp(p.id, economy.EXP_REWARDS["heist_win"]) if exp_res["old_level"] != exp_res["new_level"] and guild: gm = guild.get_member(p.id) if gm: asyncio.create_task(ensure_level_role(gm, exp_res["new_level"])) else: result_desc = S.HEIST_UI["fail_desc"].format(names=names_str) result_embed = discord.Embed( title=S.TITLE["heist_fail"], description=result_desc, color=0xED4245, ) await economy.set_heist_global_cd(time.time() + _HEIST_GLOBAL_CD) if channel: await channel.send(embed=result_embed) elif self.message: try: await self.message.channel.send(embed=result_embed) except discord.HTTPException: pass async def on_timeout(self) -> None: await self._resolve() @tree.command(name="heist", description=S.CMD["heist"]) @app_commands.guild_only() async def cmd_heist(interaction: discord.Interaction): nonlocal active_heist if active_heist is not None: await interaction.response.send_message(S.ERR["heist_active"], ephemeral=True) return heist_cd = await economy.get_heist_global_cd() if time.time() < heist_cd: await interaction.response.send_message( S.CD_MSG["heist_global"].format( ts=cd_ts(datetime.timedelta(seconds=heist_cd - time.time())) ), ephemeral=True, ) return if interaction.user.id in active_games: await interaction.response.send_message(S.ERR["already_in_game"], ephemeral=True) return res = await economy.do_heist_check(interaction.user.id) if not res["ok"]: if res["reason"] == "banned": await interaction.response.send_message(S.MSG_BANNED, ephemeral=True) elif res["reason"] == "jailed": await interaction.response.send_message( S.CD_MSG["jailed"].format(ts=cd_ts(res["remaining"])), ephemeral=True ) return organizer_data = await economy.get_user(interaction.user.id) view = HeistLobbyView(interaction.user, "jellyfin" in organizer_data.get("items", [])) active_heist = view active_games.add(interaction.user.id) await interaction.response.send_message(embed=view._lobby_embed(), view=view) view.message = await interaction.original_response() # ----------------------------------------------------------------------- # /jailbreak - Monopoly-style dice escape # ----------------------------------------------------------------------- _DICE_EMOJI = [ "<:TipiYKS:1483103190491856916>", "<:TipiKAKS:1483103215841972404>", "<:TipiKOLM:1483103217846980781>", "<:TipiNELI:1483103237585240114>", "<:TipiVIIS:1483103239036469289>", "<:TipiKUUS:1483103253163020348>", ] class JailbreakView(discord.ui.View): MAX_TRIES = 3 def __init__(self, user_id: int): super().__init__(timeout=120) self.user_id = user_id self.tries = 0 self._rolling = False self._add_roll_btn() def _add_roll_btn(self): self.clear_items() btn = discord.ui.Button( label=S.JAILBREAK_UI["btn_roll"].format(try_=self.tries + 1, max=self.MAX_TRIES), style=discord.ButtonStyle.primary, ) btn.callback = self._on_roll self.add_item(btn) async def _on_roll(self, interaction: discord.Interaction): if interaction.user.id != self.user_id: await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True) return if self._rolling: await interaction.response.defer() return self._rolling = True self.clear_items() rolling_embed = discord.Embed( title=S.TITLE["jailbreak"], description=S.JAILBREAK_UI["rolling_desc"], color=0xF4C430, ) await interaction.response.edit_message(embed=rolling_embed, view=self) d1 = random.randint(1, 6) d2 = random.randint(1, 6) e1, e2 = _DICE_EMOJI[d1 - 1], _DICE_EMOJI[d2 - 1] double = d1 == d2 self.tries += 1 tries_left = self.MAX_TRIES - self.tries await asyncio.sleep(1.5) self._rolling = False if double: await economy.do_jail_free(self.user_id) self.stop() embed = discord.Embed( title=S.TITLE["jailbreak_free"], description=S.JAILBREAK_UI["free_desc"].format(d1=e1, d2=e2), color=0x57F287, ) await interaction.edit_original_response(embed=embed, view=self) elif tries_left == 0: self.stop() user_data = await economy.get_user(self.user_id) bal = user_data["balance"] if bal >= economy.MIN_BAIL: min_fine = max(economy.MIN_BAIL, int(bal * 0.20)) max_fine = max(economy.MIN_BAIL, int(bal * 0.30)) desc = S.JAILBREAK_UI["fail_bail_offer"].format( d1=e1, d2=e2, min=coin(min_fine), max=coin(max_fine), bal=coin(bal), ) embed = discord.Embed( title=S.TITLE["jailbreak_fail"], description=desc, color=0xED4245, ) await interaction.edit_original_response(embed=embed, view=BailView(self.user_id)) else: embed = discord.Embed( title=S.TITLE["jailbreak_fail"], description=S.JAILBREAK_UI["fail_broke_desc"].format( d1=e1, d2=e2, balance=coin(bal), ), color=0xED4245, ) await interaction.edit_original_response(embed=embed, view=None) else: self._add_roll_btn() embed = discord.Embed( title=S.TITLE["jailbreak_miss"].format(tries=self.tries, max=self.MAX_TRIES), description=S.JAILBREAK_UI["miss_desc"].format(d1=e1, d2=e2, left=tries_left), color=0xF4C430, ) await interaction.edit_original_response(embed=embed, view=self) class BailView(discord.ui.View): def __init__(self, user_id: int): super().__init__(timeout=60) self.user_id = user_id @discord.ui.button(label=S.JAILBREAK_UI["bail_btn"], style=discord.ButtonStyle.danger) async def pay_bail(self, interaction: discord.Interaction, _: discord.ui.Button): if interaction.user.id != self.user_id: await interaction.response.send_message(S.ERR["not_your_game"], ephemeral=True) return res = await economy.do_bail(self.user_id) self.clear_items() self.stop() if not res["ok"] and res.get("reason") == "broke": embed = discord.Embed( title=S.TITLE["jailbreak_bail"], description=S.JAILBREAK_UI["bail_broke_desc"].format( min=coin(economy.MIN_BAIL), balance=coin(res["balance"]), ), color=0xED4245, ) else: embed = discord.Embed( title=S.TITLE["jailbreak_bail"], description=S.JAILBREAK_UI["bail_paid_desc"].format( fine=coin(res["fine"]), balance=coin(res["balance"]), ), color=0x57F287, ) await interaction.response.edit_message(embed=embed, view=self) @tree.command(name="jailbreak", description=S.CMD["jailbreak"]) async def cmd_jailbreak(interaction: discord.Interaction): user_data = await economy.get_user(interaction.user.id) remaining = economy._is_jailed(user_data) if not remaining: await interaction.response.send_message(S.ERR["not_jailed"], ephemeral=True) return if user_data.get("jailbreak_used", False): bal = user_data["balance"] min_fine = max(economy.MIN_BAIL, int(bal * 0.20)) max_fine = max(economy.MIN_BAIL, int(bal * 0.30)) if bal >= economy.MIN_BAIL: desc = S.JAILBREAK_UI["already_bail"].format( min=coin(min_fine), max=coin(max_fine), bal=coin(bal), ts=cd_ts(remaining), ) await interaction.response.send_message( embed=discord.Embed( title=S.TITLE["jailbreak_bail"], description=desc, color=0xED4245, ), view=BailView(interaction.user.id), ephemeral=True, ) else: desc = S.JAILBREAK_UI["already_broke"].format( min=coin(economy.MIN_BAIL), bal=coin(bal), ts=cd_ts(remaining), ) await interaction.response.send_message( embed=discord.Embed( title=S.TITLE["jailbreak_bail"], description=desc, color=0xED4245, ), ephemeral=True, ) return await economy.set_jailbreak_used(interaction.user.id) embed = discord.Embed( title=S.TITLE["jailbreak"], description=S.JAILBREAK_UI["intro_desc"].format( ts=cd_ts(remaining), tries=JailbreakView.MAX_TRIES, ), color=0xF4C430, ) await interaction.response.send_message( embed=embed, view=JailbreakView(interaction.user.id), ephemeral=True, ) @tree.command(name="give", description=S.CMD["give"]) @app_commands.describe(kasutaja=S.OPT["give_kasutaja"], summa=S.OPT["give_summa"]) async def cmd_give(interaction: discord.Interaction, kasutaja: discord.Member, summa: str): data = await economy.get_user(interaction.user.id) summa_int, err = parse_amount(summa, data["balance"]) if err: await interaction.response.send_message(err, ephemeral=True) return if summa_int is None or summa_int <= 0: await interaction.response.send_message(S.ERR["positive_amount"], ephemeral=True) return if kasutaja.id == interaction.user.id: await interaction.response.send_message(S.ERR["give_self"], ephemeral=True) return if kasutaja.bot: await interaction.response.send_message(S.ERR["give_bot"], ephemeral=True) return res = await economy.do_give(interaction.user.id, kasutaja.id, summa_int) if not res["ok"]: if res["reason"] == "banned": await interaction.response.send_message(S.MSG_BANNED, ephemeral=True) elif res["reason"] == "jailed": await interaction.response.send_message( S.ERR["give_jailed"].format(ts=cd_ts(res["remaining"])), ephemeral=True, ) else: await interaction.response.send_message( S.ERR["broke"].format(bal=coin(data["balance"])), ephemeral=True, ) return embed = discord.Embed( title=f"{economy.COIN} {S.TITLE['give']}", description=S.GIVE_UI["desc"].format( giver=interaction.user.display_name, amount=coin(summa_int), receiver=kasutaja.display_name, ), color=0xF4C430, ) await interaction.response.send_message(embed=embed) class LeaderboardView(discord.ui.View): PER_PAGE = 10 def __init__(self, data: dict, guild: discord.Guild | None, bot_user: discord.ClientUser | None): super().__init__(timeout=120) self.data = data self.guild = guild self.bot_user = bot_user self.page = 0 self.mode = "coins" self.max_page = 0 self._update_buttons() def _current_list(self) -> list: return self.data.get(self.mode, []) def _update_buttons(self): current = self._current_list() self.max_page = max(0, (len(current) - 1) // self.PER_PAGE) if current else 0 self.prev_btn.disabled = self.page == 0 self.next_btn.disabled = self.page >= self.max_page for m, btn in [ ("coins", self.coins_btn), ("exp", self.exp_btn), ("season", self.season_btn), ("prestige", self.prestige_btn), ("wagered", self.wagered_btn), ("fish", self.fish_btn), ]: btn.style = discord.ButtonStyle.primary if m == self.mode else discord.ButtonStyle.secondary def _name(self, uid: str, highlight_uid: int | None = None) -> str: if self.guild: member = self.guild.get_member(int(uid)) name = member.display_name if member else S.LEADERBOARD_UI["unknown_user"].format(uid=uid) else: name = S.LEADERBOARD_UI["unknown_user"].format(uid=uid) if highlight_uid and int(uid) == highlight_uid: name = f"**› {name} ‹**" return name def _make_embed(self, highlight_uid: int | None = None) -> discord.Embed: title_map = { "coins": f"{economy.COIN} {S.TITLE['leaderboard_coins']}", "exp": S.TITLE["leaderboard_exp"], "season": S.TITLE["leaderboard_season"], "prestige": S.TITLE["leaderboard_prestige"], "wagered": S.TITLE["leaderboard_wagered"], "fish": S.TITLE["leaderboard_fish"], } color_map = {"coins": 0xF4C430, "wagered": 0xED4245, "fish": 0x57F287} embed = discord.Embed( title=title_map.get(self.mode, "Edetabel"), color=color_map.get(self.mode, 0x5865F2), ) lines = [] if self.mode == "coins" and self.page == 0 and self.data.get("house_entry"): _, bal = self.data["house_entry"] house_name = self.bot_user.display_name if self.bot_user else S.LEADERBOARD_UI["house_default_name"] lines.append(S.LEADERBOARD_UI["house_entry"].format(name=house_name, balance=coin(bal))) lines.append("") start = self.page * self.PER_PAGE medals = ["🥇", "🥈", "🥉"] current = self._current_list() slice_ = current[start : start + self.PER_PAGE] if not slice_: lines.append(S.LEADERBOARD_UI["no_entries"]) else: for i, entry in enumerate(slice_): rank = start + i uid = entry[0] prefix = medals[rank] if rank < 3 else f"**{rank + 1}.**" name = self._name(uid, highlight_uid) if self.mode == "coins": lines.append(f"{prefix} {name} - {coin(entry[1])}") elif self.mode == "exp": lines.append( S.LEADERBOARD_UI["exp_entry"].format( prefix=prefix, name=name, exp=entry[1], level=entry[2], ) ) elif self.mode == "season": lines.append( S.LEADERBOARD_UI["season_entry"].format( prefix=prefix, name=name, exp=entry[1], prestige=entry[2], ) ) elif self.mode == "prestige": lines.append( S.LEADERBOARD_UI["prestige_entry"].format( prefix=prefix, name=name, prestige=entry[1], pp=entry[2], ) ) elif self.mode == "wagered": lines.append( S.LEADERBOARD_UI["wagered_entry"].format( prefix=prefix, name=name, wagered=coin(entry[1]), ) ) elif self.mode == "fish": lines.append( S.LEADERBOARD_UI["fish_entry"].format( prefix=prefix, name=name, caught=entry[1], ) ) total = self.max_page + 1 embed.description = "\n".join(lines) embed.set_footer( text=S.LEADERBOARD_UI["footer"].format( page=self.page + 1, total=total, count=len(current), ) ) return embed @discord.ui.button(label="◄", style=discord.ButtonStyle.secondary, row=0) async def prev_btn(self, interaction: discord.Interaction, button: discord.ui.Button): self.page -= 1 self._update_buttons() await interaction.response.edit_message(embed=self._make_embed(), view=self) @discord.ui.button(label="►", style=discord.ButtonStyle.secondary, row=0) async def next_btn(self, interaction: discord.Interaction, button: discord.ui.Button): self.page += 1 self._update_buttons() await interaction.response.edit_message(embed=self._make_embed(), view=self) @discord.ui.button(label=S.LEADERBOARD_UI["btn_find_me"], style=discord.ButtonStyle.secondary, row=0) async def find_me_btn(self, interaction: discord.Interaction, button: discord.ui.Button): uid = interaction.user.id for i, entry in enumerate(self._current_list()): if int(entry[0]) == uid: self.page = i // self.PER_PAGE self._update_buttons() await interaction.response.edit_message( embed=self._make_embed(highlight_uid=uid), view=self, ) return await interaction.response.send_message(S.ERR["not_in_leaderboard"], ephemeral=True) @discord.ui.button(label=S.LEADERBOARD_UI["btn_coins"], style=discord.ButtonStyle.primary, row=1) async def coins_btn(self, interaction: discord.Interaction, button: discord.ui.Button): self.mode = "coins" self.page = 0 self._update_buttons() await interaction.response.edit_message(embed=self._make_embed(), view=self) @discord.ui.button(label=S.LEADERBOARD_UI["btn_exp"], style=discord.ButtonStyle.secondary, row=1) async def exp_btn(self, interaction: discord.Interaction, button: discord.ui.Button): self.mode = "exp" self.page = 0 self._update_buttons() await interaction.response.edit_message(embed=self._make_embed(), view=self) @discord.ui.button(label=S.LEADERBOARD_UI["btn_season"], style=discord.ButtonStyle.secondary, row=1) async def season_btn(self, interaction: discord.Interaction, button: discord.ui.Button): self.mode = "season" self.page = 0 self._update_buttons() await interaction.response.edit_message(embed=self._make_embed(), view=self) @discord.ui.button(label=S.LEADERBOARD_UI["btn_prestige"], style=discord.ButtonStyle.secondary, row=1) async def prestige_btn(self, interaction: discord.Interaction, button: discord.ui.Button): self.mode = "prestige" self.page = 0 self._update_buttons() await interaction.response.edit_message(embed=self._make_embed(), view=self) @discord.ui.button(label=S.LEADERBOARD_UI["btn_wagered"], style=discord.ButtonStyle.secondary, row=1) async def wagered_btn(self, interaction: discord.Interaction, button: discord.ui.Button): self.mode = "wagered" self.page = 0 self._update_buttons() await interaction.response.edit_message(embed=self._make_embed(), view=self) @discord.ui.button(label=S.LEADERBOARD_UI["btn_fish"], style=discord.ButtonStyle.secondary, row=2) async def fish_btn(self, interaction: discord.Interaction, button: discord.ui.Button): self.mode = "fish" self.page = 0 self._update_buttons() await interaction.response.edit_message(embed=self._make_embed(), view=self) async def on_timeout(self): for child in self.children: child.disabled = True @tree.command(name="leaderboard", description=S.CMD["leaderboard"]) async def cmd_leaderboard(interaction: discord.Interaction): await interaction.response.defer() coins_raw, exp_raw, season_raw, prestige_raw, wagered_raw, fish_raw = await asyncio.gather( economy.get_leaderboard(top_n=None), economy.get_leaderboard_exp(top_n=None), economy.get_leaderboard_season_exp(top_n=None), economy.get_leaderboard_prestige(top_n=None), economy.get_leaderboard_wagered(top_n=None), economy.get_leaderboard_fish(top_n=None), ) house_entry = None regular = [] bot_id = bot.user.id if bot.user else None for uid, bal in coins_raw: if bot_id and int(uid) == bot_id: house_entry = (uid, bal) else: regular.append((uid, bal)) def _no_bot(entries: list) -> list: return [e for e in entries if not (bot_id and int(e[0]) == bot_id)] data = { "coins": regular, "exp": _no_bot(exp_raw), "season": _no_bot(season_raw), "prestige": _no_bot(prestige_raw), "wagered": _no_bot(wagered_raw), "fish": _no_bot(fish_raw), "house_entry": house_entry, } view = LeaderboardView(data, interaction.guild, bot.user) await interaction.followup.send(embed=view._make_embed(), view=view) def _shop_embed(tier: int, user_data: dict) -> discord.Embed: owned = set(user_data.get("items", [])) item_uses = user_data.get("item_uses", {}) tier_names = {1: S.SHOP_UI["tier_1"], 2: S.SHOP_UI["tier_2"], 3: S.SHOP_UI["tier_3"]} embed = discord.Embed( title=f"{economy.COIN} TipiBOTi pood · {tier_names[tier]}", description=S.SHOP_UI["desc"].format(bal=coin(user_data["balance"])), color=[0x57F287, 0xF4C430, 0xED4245][tier - 1], ) for item_id in sorted(economy.SHOP_TIERS[tier], key=lambda k: economy.SHOP[k]["cost"]): item = economy.SHOP[item_id] anticheat_uses = item_uses.get("anticheat", 0) if item_id == "anticheat" else 0 min_lvl = economy.SHOP_LEVEL_REQ.get(item_id, 0) user_lvl = economy.get_level(user_data.get("exp", 0)) if item_id in owned and not (item_id == "anticheat" and anticheat_uses <= 0): if item_id == "anticheat": key = "owned_uses_1" if anticheat_uses == 1 else "owned_uses_n" status = S.SHOP_UI[key].format(uses=anticheat_uses) else: status = S.SHOP_UI["owned"] elif min_lvl > 0 and user_lvl < min_lvl: status = S.SHOP_UI["locked"].format(min_lvl=min_lvl, user_lvl=user_lvl) else: status = f"{item['cost']} {economy.COIN}" embed.add_field( name=f"{item['emoji']} {item['name']} · {status}", value=item["description"], inline=False, ) return embed class ShopView(discord.ui.View): def __init__(self, user_data: dict, tier: int = 1): super().__init__(timeout=120) self._user_data = user_data self._tier = tier self._update_buttons() def _update_buttons(self): self.clear_items() for t, label in [(1, S.SHOP_BTN[1]), (2, S.SHOP_BTN[2]), (3, S.SHOP_BTN[3])]: btn = discord.ui.Button( label=label, style=discord.ButtonStyle.primary if t == self._tier else discord.ButtonStyle.secondary, custom_id=f"shop_tier_{t}", ) btn.callback = self._make_callback(t) self.add_item(btn) def _make_callback(self, tier: int): async def callback(interaction: discord.Interaction): self._tier = tier self._update_buttons() self._user_data = await economy.get_user(interaction.user.id) await interaction.response.edit_message( embed=_shop_embed(self._tier, self._user_data), view=self, ) return callback @tree.command(name="shop", description=S.CMD["shop"]) async def cmd_shop(interaction: discord.Interaction): data = await economy.get_user(interaction.user.id) await interaction.response.send_message( embed=_shop_embed(1, data), view=ShopView(data, tier=1), ephemeral=True, ) @tree.command(name="buy", description=S.CMD["buy"]) @app_commands.describe(ese=S.OPT["buy_ese"]) @app_commands.choices( ese=[ app_commands.Choice(name=f"{v['name']} ({v['cost']} TipiCOINi)", value=k) for k, v in economy.SHOP.items() ] ) async def cmd_buy(interaction: discord.Interaction, ese: app_commands.Choice[str]): res = await economy.do_buy(interaction.user.id, ese.value) if not res["ok"]: if res["reason"] == "banned": await interaction.response.send_message(S.MSG_BANNED, ephemeral=True) elif res["reason"] == "owned": await interaction.response.send_message(S.ERR["item_owned"], ephemeral=True) elif res["reason"] == "level_required": await interaction.response.send_message( S.ERR["item_level_req"].format( min_level=res["min_level"], user_level=res["user_level"], ), ephemeral=True, ) elif res["reason"] == "insufficient": await interaction.response.send_message( S.ERR["broke_need"].format(need=coin(res["need"])), ephemeral=True, ) else: await interaction.response.send_message(S.ERR["item_not_found"], ephemeral=True) return item = res["item"] embed = discord.Embed( title=S.BUY_UI["title"].format(emoji=item["emoji"], name=item["name"]), description=S.BUY_UI["desc"].format( description=item["description"], balance=coin(res["balance"]), ), color=0x57F287, ) await interaction.response.send_message(embed=embed)