"""Achievements: one-time milestone badges over the lifetime stat counters. Each achievement watches a monotonic stat the bot already tracks and unlocks once that stat crosses a threshold, paying a modest one-time coin reward. Detection is lazy: do_check_achievements is called when the player opens /achievements (like quests roll on view), so no hook is needed on every command. Rewards are bounded (each pays once) so this is a small, capped coin source. """ from __future__ import annotations from typing import TypedDict from ..pb_client import DatabaseError from .store import _commit, _locked_by, _txn, get_user __all__ = [ "Achievement", "ACHIEVEMENTS", "newly_earned", "achievements_view", "do_check_achievements", ] class Achievement(TypedDict): stat: str # UserData counter this milestone watches goal: int name: str emoji: str reward: int # one-time coin payout # Ordered roughly easy -> hard within each theme. Rewards scale with difficulty. ACHIEVEMENTS: dict[str, Achievement] = { # Grind "work_10": {"stat": "work_count", "goal": 10, "name": "Töömesilane", "emoji": "🐝", "reward": 250}, "work_100": {"stat": "work_count", "goal": 100, "name": "Töönarkomaan", "emoji": "🛠️", "reward": 1_500}, "beg_50": {"stat": "beg_count", "goal": 50, "name": "Elukutseline kerjus", "emoji": "🥺", "reward": 500}, # Wealth "earned_50k": {"stat": "lifetime_earned", "goal": 50_000, "name": "Jõukas", "emoji": "💰", "reward": 1_000}, "earned_500k": {"stat": "lifetime_earned", "goal": 500_000, "name": "TipiRIKAS", "emoji": "🤑", "reward": 5_000}, # Gambling "wager_10k": {"stat": "total_wagered", "goal": 10_000, "name": "Hasartmängur", "emoji": "🎲", "reward": 750}, "wager_100k": {"stat": "total_wagered", "goal": 100_000, "name": "Kõrgete panuste mängija", "emoji": "🃏", "reward": 3_000}, "jackpot_1": {"stat": "slots_jackpots", "goal": 1, "name": "Jackpot!", "emoji": "🎰", "reward": 1_000}, # Crime & heists "crime_25": {"stat": "crimes_succeeded", "goal": 25, "name": "Kurjategija", "emoji": "🦹", "reward": 1_000}, "heist_5": {"stat": "heists_won", "goal": 5, "name": "Pangaröövel", "emoji": "💣", "reward": 1_500}, # Fishing "fish_25": {"stat": "total_fish_caught", "goal": 25, "name": "Kalur", "emoji": "🎣", "reward": 500}, "fish_250": {"stat": "total_fish_caught", "goal": 250, "name": "Kalapüügimeister", "emoji": "🐟", "reward": 3_000}, # Dedication "streak_7": {"stat": "best_daily_streak", "goal": 7, "name": "Püsiv", "emoji": "🔥", "reward": 500}, "streak_30": {"stat": "best_daily_streak", "goal": 30, "name": "Pühendunud", "emoji": "🗓️", "reward": 2_500}, "prestige_1": {"stat": "prestige_level", "goal": 1, "name": "Taassünd", "emoji": "♻️", "reward": 2_000}, } def _earned_ids(user) -> set[str]: return set(user.get("achievements_earned") or []) def newly_earned(user) -> list[str]: """Achievement ids whose threshold is met but which are not yet claimed.""" earned = _earned_ids(user) return [ aid for aid, a in ACHIEVEMENTS.items() if aid not in earned and int(user.get(a["stat"], 0) or 0) >= a["goal"] ] def achievements_view(user) -> list[dict]: """Display rows for every achievement: progress + earned flag (ordered as defined, earned last so unfinished goals surface first).""" earned = _earned_ids(user) rows = [] for aid, a in ACHIEVEMENTS.items(): prog = int(user.get(a["stat"], 0) or 0) rows.append({ "id": aid, "name": a["name"], "emoji": a["emoji"], "goal": a["goal"], "reward": a["reward"], "progress": min(prog, a["goal"]), "earned": aid in earned, }) rows.sort(key=lambda r: r["earned"]) # unearned first return rows @_locked_by(0) async def do_check_achievements(user_id: int) -> dict: """Claim any newly-earned achievements and pay their one-time rewards.""" try: user = await get_user(user_id) except DatabaseError: return {"ok": False, "reason": "db_error"} new = newly_earned(user) if not new: return {"ok": True, "new": [], "reward": 0, "balance": user["balance"]} earned = list(user.get("achievements_earned") or []) total = 0 for aid in new: earned.append(aid) total += ACHIEVEMENTS[aid]["reward"] user["achievements_earned"] = earned user["balance"] += total user["lifetime_earned"] = user.get("lifetime_earned", 0) + total user["peak_balance"] = max(user.get("peak_balance", 0), user["balance"]) await _commit(user_id, user) _txn("ACHIEVEMENTS", user=user_id, unlocked=",".join(new), reward=f"+{total}", bal=user["balance"]) return {"ok": True, "new": new, "reward": total, "balance": user["balance"]}