Files
tipibot/economy_extra_commands.py
2026-04-20 12:09:39 +03:00

884 lines
38 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
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)