from __future__ import annotations import asyncio import datetime from collections.abc import Awaitable, Callable import discord from discord import app_commands from core import economy import strings as S _BAR_SLOTS = 8 def _bar(progress: int, goal: int) -> str: """Return an 8-slot ▰/▱ progress bar for a quest.""" filled = 0 if goal <= 0 else min(_BAR_SLOTS, round(_BAR_SLOTS * progress / goal)) return "▰" * filled + "▱" * (_BAR_SLOTS - filled) def _has_claimable(view_data: dict) -> bool: return any( q["done"] and not q["claimed"] for q in view_data["daily"] + view_data["weekly"] ) def register_economy_quests_commands( tree: app_commands.CommandTree, bot: discord.Client, coin: Callable[[int], str], award_exp: Callable[[discord.Interaction, int], Awaitable[None]], ) -> None: def _render(view_data: dict) -> discord.Embed: embed = discord.Embed(title=S.TITLE["quests"], color=0xF4C430) sections = ( (S.QUEST_UI["daily_header"], view_data["daily"]), (S.QUEST_UI["weekly_header"], view_data["weekly"]), ) for header, quests in sections: if quests: blocks = [] for q in quests: desc = S.QUEST_DESCRIPTIONS.get(q["id"], q["id"]) if q["claimed"]: status = S.QUEST_UI["completed"] elif q["done"]: status = S.QUEST_UI["ready"] else: status = S.QUEST_UI["progress"].format(progress=q["progress"], max=q["goal"]) reward = S.QUEST_UI["reward"].format(coins=f"{q['coins']:,}", exp=q["exp"]) blocks.append( f"{_bar(q['progress'], q['goal'])} **{desc}**\n{status} · {reward}" ) value = "\n\n".join(blocks) else: value = S.QUEST_UI["empty"] embed.add_field(name=header, value=value, inline=False) return embed class QuestView(discord.ui.View): def __init__(self, invoker_id: int, view_data: dict): super().__init__(timeout=180) self.invoker_id = invoker_id btn = discord.ui.Button( label=S.QUEST_UI["claim_btn"], style=discord.ButtonStyle.success, disabled=not _has_claimable(view_data), ) btn.callback = self._claim self.add_item(btn) async def _claim(self, interaction: discord.Interaction): if interaction.user.id != self.invoker_id: await interaction.response.send_message(S.ERR["not_your_menu"], ephemeral=True) return try: res = await economy.claim_quests(self.invoker_id) except economy.DatabaseError: await interaction.response.send_message(S.QUEST_UI["error"], ephemeral=True) return if not res["ok"]: await interaction.response.send_message(S.QUEST_UI["nothing"], ephemeral=True) return new_data = await economy.get_quests(self.invoker_id) await interaction.response.edit_message( embed=_render(new_data), view=QuestView(self.invoker_id, new_data) ) await interaction.followup.send( S.QUEST_UI["claimed_msg"].format( count=res["claimed"], coins=coin(res["coins"]), exp=res["exp"] ), ephemeral=True, ) if res["exp"]: asyncio.create_task(award_exp(interaction, res["exp"])) @tree.command(name="quests", description=S.CMD["quests"]) async def cmd_quests(interaction: discord.Interaction): await interaction.response.defer() try: data = await economy.get_quests(interaction.user.id) except economy.DatabaseError: await interaction.followup.send(S.QUEST_UI["error"], ephemeral=True) return await interaction.followup.send(embed=_render(data), view=QuestView(interaction.user.id, data))