Added quests

This commit is contained in:
Rene Arumetsa
2026-07-26 20:11:14 +03:00
parent 0cdd8dac63
commit cb18d9b882
6 changed files with 403 additions and 0 deletions

View File

@@ -401,6 +401,9 @@ class UserData(TypedDict, total=False):
fish_book: dict # {fish_id: times_caught}
total_fish_caught: int
fish_inventory: list # [{fish_id, weight, value}] - survives prestige
# Quest system
quest_daily: dict # {"date": "YYYY-MM-DD", "quests": {qid: {snap, claimed}}}
quest_weekly: dict # {"week": "YYYY-Www", "quests": {qid: {snap, claimed}}}
def _default_user() -> UserData:
@@ -451,6 +454,9 @@ def _default_user() -> UserData:
"fish_book": {},
"total_fish_caught": 0,
"fish_inventory": [],
# ── Quests ───────────────────────────────────────────────────────────
"quest_daily": {},
"quest_weekly": {},
}
@@ -700,6 +706,147 @@ async def _commit(user_id: int, user: UserData) -> None:
raise DatabaseError(f"Failed to persist user {user_id}: {exc}") from exc
# ---------------------------------------------------------------------------
# Quest system
# ---------------------------------------------------------------------------
# Quests reuse the monotonic lifetime counters already tracked on each user.
# Progress = (current counter value) - (snapshot taken when the quest was rolled).
# Reset is lazy & per-user: the active set is regenerated the first time a user
# interacts after the day / ISO-week rolls over (mirrors the streak-date logic).
# Rotation is seeded by the period key, so every player gets the same set.
class QuestDef(TypedDict):
stat: str # UserData counter field the quest tracks
goal: int
coins: int
exp: int
QUESTS_DAILY: dict[str, QuestDef] = {
"work3": {"stat": "work_count", "goal": 3, "coins": 150, "exp": 20},
"beg5": {"stat": "beg_count", "goal": 5, "coins": 100, "exp": 15},
"wager500": {"stat": "total_wagered", "goal": 500, "coins": 150, "exp": 20},
"fish2": {"stat": "total_fish_caught", "goal": 2, "coins": 150, "exp": 20},
"crime1": {"stat": "crimes_succeeded", "goal": 1, "coins": 200, "exp": 25},
"earn1000": {"stat": "lifetime_earned", "goal": 1000, "coins": 150, "exp": 20},
"give200": {"stat": "total_given", "goal": 200, "coins": 100, "exp": 15},
}
QUESTS_WEEKLY: dict[str, QuestDef] = {
"work20": {"stat": "work_count", "goal": 20, "coins": 1000, "exp": 100},
"fish15": {"stat": "total_fish_caught", "goal": 15, "coins": 1200, "exp": 100},
"wager5000": {"stat": "total_wagered", "goal": 5000, "coins": 1000, "exp": 100},
"crime5": {"stat": "crimes_succeeded", "goal": 5, "coins": 1200, "exp": 120},
"heist1": {"stat": "heists_joined", "goal": 1, "coins": 800, "exp": 80},
"earn10000": {"stat": "lifetime_earned", "goal": 10000, "coins": 1500, "exp": 150},
}
DAILY_QUEST_COUNT = 3
WEEKLY_QUEST_COUNT = 2
def _period_keys() -> tuple[str, str]:
"""Return (day_key, week_key) for the current UTC time."""
today = _now().date()
iso = today.isocalendar()
return today.isoformat(), f"{iso[0]}-W{iso[1]:02d}"
def _pick_quests(pool: dict[str, QuestDef], count: int, seed: str) -> list[str]:
"""Deterministically choose `count` quest ids from `pool` for a period."""
rng = random.Random(seed)
return rng.sample(sorted(pool.keys()), min(count, len(pool)))
def _new_quest_block(
user: UserData, pool: dict[str, QuestDef], count: int, period_val: str, period_field: str
) -> dict:
chosen = _pick_quests(pool, count, f"{period_field}:{period_val}")
return {
period_field: period_val,
"quests": {
qid: {"snap": int(user.get(pool[qid]["stat"], 0) or 0), "claimed": False}
for qid in chosen
},
}
def _ensure_quests(user: UserData) -> bool:
"""Roll fresh daily/weekly quest sets if their period elapsed.
Mutates `user` in place; returns True if anything changed (caller commits)."""
changed = False
day_key, week_key = _period_keys()
if (user.get("quest_daily") or {}).get("date") != day_key:
user["quest_daily"] = _new_quest_block(user, QUESTS_DAILY, DAILY_QUEST_COUNT, day_key, "date")
changed = True
if (user.get("quest_weekly") or {}).get("week") != week_key:
user["quest_weekly"] = _new_quest_block(user, QUESTS_WEEKLY, WEEKLY_QUEST_COUNT, week_key, "week")
changed = True
return changed
def _quest_progress(user: UserData, pool: dict[str, QuestDef], qid: str, state: dict) -> int:
cur = int(user.get(pool[qid]["stat"], 0) or 0)
return max(0, cur - int(state.get("snap", 0)))
def _quest_view(user: UserData) -> dict:
def build(pool: dict[str, QuestDef], block: dict) -> list[dict]:
out: list[dict] = []
for qid, state in (block.get("quests") or {}).items():
if qid not in pool:
continue
d = pool[qid]
prog = min(d["goal"], _quest_progress(user, pool, qid, state))
out.append({
"id": qid, "goal": d["goal"], "coins": d["coins"], "exp": d["exp"],
"progress": prog, "done": prog >= d["goal"], "claimed": bool(state.get("claimed")),
})
return out
return {
"daily": build(QUESTS_DAILY, user.get("quest_daily") or {}),
"weekly": build(QUESTS_WEEKLY, user.get("quest_weekly") or {}),
}
async def get_quests(user_id: int) -> dict:
"""Return the user's active quests, rolling new sets if the period elapsed."""
user = await get_user(user_id)
if _ensure_quests(user):
await _commit(user_id, user)
return _quest_view(user)
async def claim_quests(user_id: int) -> dict:
"""Grant coins for every completed-but-unclaimed quest and mark them claimed.
Coins (with prestige coin_mult) are paid here; EXP is returned raw for the
caller to award via the shared award_exp path (keeps level-up notices)."""
user = await get_user(user_id)
_ensure_quests(user)
coin_mult, _ = _prestige_mult(user)
total_coins = total_exp = claimed = 0
for pool, block in (
(QUESTS_DAILY, user.get("quest_daily") or {}),
(QUESTS_WEEKLY, user.get("quest_weekly") or {}),
):
for qid, state in (block.get("quests") or {}).items():
if qid not in pool or state.get("claimed"):
continue
if _quest_progress(user, pool, qid, state) < pool[qid]["goal"]:
continue
total_coins += pool[qid]["coins"]
total_exp += pool[qid]["exp"]
state["claimed"] = True
claimed += 1
if not claimed:
return {"ok": False, "reason": "nothing"}
coins_awarded = int(total_coins * coin_mult)
user["balance"] += coins_awarded
user["lifetime_earned"] = user.get("lifetime_earned", 0) + coins_awarded
user["peak_balance"] = max(user.get("peak_balance", 0), user["balance"])
await _commit(user_id, user)
return {"ok": True, "claimed": claimed, "coins": coins_awarded, "exp": total_exp, "balance": user["balance"]}
# ---------------------------------------------------------------------------
# /daily
# ---------------------------------------------------------------------------