feat(economy): add /achievements milestone badges

15 one-time achievements over the lifetime stat counters (work/wealth/gambling/
crime/heists/fishing/streaks/prestige), each paying a modest one-time coin reward
when unlocked. Detection is lazy - opening /achievements claims any newly earned
(like quests roll on view) - so no per-command hook is needed, and rewards are a
bounded, one-time coin source.

- New achievements.py: ACHIEVEMENTS table, pure newly_earned/achievements_view,
  and locked do_check_achievements. New achievements_earned schema field.
- /achievements command (own view claims; viewing others is read-only) with
  progress bars, db_error handling, and an "unlocked" banner.

8 tests: threshold detection, one-time claim (no double pay), multi-unlock,
view progress capping.

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:
Rene Arumetsa
2026-09-04 02:57:34 +03:00
parent 42bbdbd93d
commit 570d8b7cbd
9 changed files with 261 additions and 2 deletions

View File

@@ -10,6 +10,8 @@ from discord import app_commands
from core import economy
import strings as S
from ._replies import reply_db_error
def register_economy_profile_commands(
tree: app_commands.CommandTree,
@@ -342,6 +344,53 @@ def register_economy_profile_commands(
data = await economy.get_user(target.id)
await interaction.response.send_message(embed=_balance_embed(target, data))
@tree.command(name="achievements", description=S.CMD["achievements"])
async def cmd_achievements(
interaction: discord.Interaction, kasutaja: discord.Member | None = None
):
target = kasutaja or interaction.user
note = ""
# Only the invoker's own view claims newly-earned achievements.
if target.id == interaction.user.id:
res = await economy.do_check_achievements(interaction.user.id)
if not res["ok"]:
await reply_db_error(interaction)
return
if res["new"]:
names = ", ".join(
f"{economy.ACHIEVEMENTS[a]['emoji']} {economy.ACHIEVEMENTS[a]['name']}"
for a in res["new"]
)
note = S.ACHIEVEMENTS_UI["unlocked_note"].format(
names=names, reward=coin(res["reward"])
) + "\n\n"
data = await economy.get_user(target.id)
rows = economy.achievements_view(data)
earned_count = sum(1 for r in rows if r["earned"])
lines = [
S.ACHIEVEMENTS_UI["row_earned"].format(
emoji=r["emoji"], name=r["name"], reward=coin(r["reward"])
)
if r["earned"]
else S.ACHIEVEMENTS_UI["row_locked"].format(
emoji=r["emoji"], name=r["name"], progress=r["progress"],
goal=r["goal"], reward=coin(r["reward"]),
)
for r in rows
]
title = S.ACHIEVEMENTS_UI["title"]
if target.id != interaction.user.id:
title += f" · {target.display_name}"
embed = discord.Embed(
title=title,
description=note
+ S.ACHIEVEMENTS_UI["desc"].format(earned=earned_count, total=len(rows))
+ "\n\n" + "\n".join(lines),
color=0xF4C430,
)
await interaction.response.send_message(embed=embed)
@tree.command(name="cooldowns", description=S.CMD["cooldowns"])
async def cmd_cooldowns(interaction: discord.Interaction):
data = await economy.get_user(interaction.user.id)