forked from sass/tipibot
884 lines
38 KiB
Python
884 lines
38 KiB
Python
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)
|