Last changes before full rewrite
This commit is contained in:
513
economy.py
513
economy.py
@@ -30,7 +30,10 @@ def _txn(event: str, **fields) -> None:
|
||||
# To use your custom Discord emoji replace COIN with the full tag, e.g.:
|
||||
# COIN = "<:tipicoin:1234567890123456789>"
|
||||
# ---------------------------------------------------------------------------
|
||||
COIN = "<:TipiCOIN:1483000209188589628>"
|
||||
COIN = "<:TipiCOIN:1483000209188589628>"
|
||||
PP_EMOJI = "<:TipiFIRE:1483431381668335687>"
|
||||
PRESTIGE_ROLE = "TipiPRESTIGE"
|
||||
PRESTIGE_MIN_LEVEL = 30 # minimum level required to prestige
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shop catalogue
|
||||
@@ -141,13 +144,32 @@ SHOP: dict[str, ShopItem] = {
|
||||
"cost": 9000,
|
||||
"description": strings.ITEM_DESCRIPTIONS["gaming_tool"],
|
||||
},
|
||||
# ----- Fishing items -----
|
||||
"ussipurk": {
|
||||
"name": "Ussipurk",
|
||||
"emoji": "🪣",
|
||||
"cost": 3500,
|
||||
"description": strings.ITEM_DESCRIPTIONS["ussipurk"],
|
||||
},
|
||||
"kalavork": {
|
||||
"name": "Kalavõrk",
|
||||
"emoji": "🪝",
|
||||
"cost": 5000,
|
||||
"description": strings.ITEM_DESCRIPTIONS["kalavork"],
|
||||
},
|
||||
"echolood": {
|
||||
"name": "Echolood",
|
||||
"emoji": "📡",
|
||||
"cost": 8000,
|
||||
"description": strings.ITEM_DESCRIPTIONS["echolood"],
|
||||
},
|
||||
}
|
||||
|
||||
# Tier grouping (used by /shop pagination)
|
||||
SHOP_TIERS: dict[int, list[str]] = {
|
||||
1: ["gaming_hiir", "hiirematt", "korvaklapid", "lan_pass", "energiajook", "anticheat", "gaming_laptop"],
|
||||
2: ["reguleeritav_laud", "jellyfin", "mikrofon", "klaviatuur", "monitor", "cat6"],
|
||||
3: ["monitor_360", "karikas", "gaming_tool"],
|
||||
2: ["reguleeritav_laud", "jellyfin", "mikrofon", "klaviatuur", "monitor", "cat6", "ussipurk"],
|
||||
3: ["monitor_360", "karikas", "gaming_tool", "kalavork", "echolood"],
|
||||
}
|
||||
|
||||
# Minimum level required to purchase Tier 2 / Tier 3 shop items
|
||||
@@ -158,11 +180,104 @@ SHOP_LEVEL_REQ: dict[str, int] = {
|
||||
"klaviatuur": 10,
|
||||
"monitor": 10,
|
||||
"cat6": 10,
|
||||
"ussipurk": 10,
|
||||
"monitor_360": 20,
|
||||
"karikas": 20,
|
||||
"gaming_tool": 20,
|
||||
"kalavork": 20,
|
||||
"echolood": 20,
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Prestige shop catalogue
|
||||
# ---------------------------------------------------------------------------
|
||||
class PrestigeItem(TypedDict):
|
||||
emoji: str
|
||||
max_level: int
|
||||
pp_cost: int
|
||||
effect: float
|
||||
|
||||
|
||||
PRESTIGE_SHOP: dict[str, PrestigeItem] = {
|
||||
"coin_mult": {
|
||||
"emoji": "<:TipiCOIN:1483000209188589628>",
|
||||
"max_level": 5,
|
||||
"pp_cost": 5,
|
||||
"effect": 0.08,
|
||||
},
|
||||
"exp_mult": {
|
||||
"emoji": "✨",
|
||||
"max_level": 5,
|
||||
"pp_cost": 5,
|
||||
"effect": 0.08,
|
||||
},
|
||||
"daily_plus": {
|
||||
"emoji": "📅",
|
||||
"max_level": 3,
|
||||
"pp_cost": 7,
|
||||
"effect": 0.20,
|
||||
},
|
||||
"work_plus": {
|
||||
"emoji": "💼",
|
||||
"max_level": 3,
|
||||
"pp_cost": 7,
|
||||
"effect": 0.20,
|
||||
},
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fish catalogue
|
||||
# ---------------------------------------------------------------------------
|
||||
FISH_CATALOGUE: dict[str, dict] = {
|
||||
# id: { rarity, weight=(min_g, max_g), coins=(min, max), exp }
|
||||
"sarj": {"rarity": "common", "weight": (50, 500), "coins": (3, 18), "exp": 3},
|
||||
"ahven": {"rarity": "common", "weight": (80, 700), "coins": (5, 22), "exp": 3},
|
||||
"koger": {"rarity": "common", "weight": (100, 800), "coins": (5, 20), "exp": 3},
|
||||
"viidikas": {"rarity": "common", "weight": (10, 120), "coins": (2, 8), "exp": 2},
|
||||
"latikas": {"rarity": "uncommon", "weight": (300, 2500), "coins": (20, 70), "exp": 6},
|
||||
"karpkala": {"rarity": "uncommon", "weight": (500, 4000), "coins": (25, 80), "exp": 7},
|
||||
"linask": {"rarity": "uncommon", "weight": (200, 2000), "coins": (18, 60), "exp": 6},
|
||||
"haug": {"rarity": "rare", "weight": (500, 6000), "coins": (50, 180), "exp": 10},
|
||||
"angerjas": {"rarity": "rare", "weight": (200, 1800), "coins": (40, 120), "exp": 10},
|
||||
"siig": {"rarity": "rare", "weight": (200, 2000), "coins": (45, 130), "exp": 10},
|
||||
"forell": {"rarity": "epic", "weight": (400, 4500), "coins": (100, 280), "exp": 15},
|
||||
"koha": {"rarity": "epic", "weight": (600, 7000), "coins": (120, 300), "exp": 15},
|
||||
"tougjas": {"rarity": "epic", "weight": (400, 4000), "coins": (90, 250), "exp": 14},
|
||||
"lohe": {"rarity": "legendary","weight": (1500, 12000), "coins": (250, 700), "exp": 25},
|
||||
"vimb": {"rarity": "legendary","weight": (200, 1200), "coins": (200, 600), "exp": 25},
|
||||
}
|
||||
|
||||
FISH_RARITY_WEIGHTS: dict[str, int] = {
|
||||
"junk": 15,
|
||||
"common": 45,
|
||||
"uncommon": 22,
|
||||
"rare": 12,
|
||||
"epic": 5,
|
||||
"legendary": 1,
|
||||
}
|
||||
|
||||
|
||||
def roll_fish(rarity_bump: bool = False) -> tuple[str, int]:
|
||||
"""Roll a random fish. Returns (fish_id, weight_grams) or ('junk', 0).
|
||||
rarity_bump=True (kalavork item) shifts each catch one tier up.
|
||||
"""
|
||||
rarity_pool = list(FISH_RARITY_WEIGHTS.keys())
|
||||
weights = list(FISH_RARITY_WEIGHTS.values())
|
||||
chosen_rarity = random.choices(rarity_pool, weights=weights)[0]
|
||||
if chosen_rarity == "junk":
|
||||
return ("junk", 0)
|
||||
if rarity_bump:
|
||||
order = ["common", "uncommon", "rare", "epic", "legendary"]
|
||||
idx = order.index(chosen_rarity) if chosen_rarity in order else 0
|
||||
chosen_rarity = order[min(idx + 1, len(order) - 1)]
|
||||
fish_of_rarity = [k for k, v in FISH_CATALOGUE.items() if v["rarity"] == chosen_rarity]
|
||||
if not fish_of_rarity:
|
||||
return ("junk", 0)
|
||||
fish_id = random.choice(fish_of_rarity)
|
||||
fish = FISH_CATALOGUE[fish_id]
|
||||
weight = random.randint(fish["weight"][0], fish["weight"][1])
|
||||
return (fish_id, weight)
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# EXP / Level system
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -227,6 +342,7 @@ COOLDOWNS: dict[str, timedelta] = {
|
||||
"beg": timedelta(minutes=5),
|
||||
"crime": timedelta(hours=2),
|
||||
"rob": timedelta(hours=2),
|
||||
"fish": timedelta(minutes=2),
|
||||
}
|
||||
|
||||
JAIL_DURATION = timedelta(minutes=30)
|
||||
@@ -272,6 +388,16 @@ class UserData(TypedDict, total=False):
|
||||
total_received: int
|
||||
best_daily_streak: int
|
||||
heist_global_cd_until: float
|
||||
# Prestige system
|
||||
prestige_level: int
|
||||
prestige_points: int
|
||||
season_total_exp: int # cumulative EXP this season (survives prestige resets)
|
||||
prestige_upgrades: dict # {upgrade_id: level}
|
||||
# Fishing system
|
||||
last_fish: str | None
|
||||
fish_book: dict # {fish_id: times_caught}
|
||||
total_fish_caught: int
|
||||
fish_inventory: list # [{fish_id, weight, value}] - survives prestige
|
||||
|
||||
|
||||
def _default_user() -> UserData:
|
||||
@@ -312,6 +438,16 @@ def _default_user() -> UserData:
|
||||
"total_received": 0,
|
||||
"best_daily_streak": 0,
|
||||
"heist_global_cd_until": 0.0,
|
||||
# ── Prestige ─────────────────────────────────────────────────────────
|
||||
"prestige_level": 0,
|
||||
"prestige_points": 0,
|
||||
"season_total_exp": 0,
|
||||
"prestige_upgrades": {},
|
||||
# ── Fishing ──────────────────────────────────────────────────────────
|
||||
"last_fish": None,
|
||||
"fish_book": {},
|
||||
"total_fish_caught": 0,
|
||||
"fish_inventory": [],
|
||||
}
|
||||
|
||||
|
||||
@@ -388,33 +524,6 @@ async def get_all_users_raw() -> dict[str, "UserData"]:
|
||||
return result
|
||||
|
||||
|
||||
async def migrate_anticheat_uses() -> int:
|
||||
"""One-time migration: users who own anticheat but have no item_uses entry get 2 uses."""
|
||||
records = await pb_client.list_all_records()
|
||||
changed = 0
|
||||
for record in records:
|
||||
items = record.get("items") or []
|
||||
item_uses = record.get("item_uses") or {}
|
||||
if "anticheat" in items and "anticheat" not in item_uses:
|
||||
item_uses["anticheat"] = 2
|
||||
await pb_client.update_record(record["id"], {"item_uses": item_uses})
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
async def migrate_reminders_default() -> int:
|
||||
"""One-time migration: enable all reminders for users who have an empty list."""
|
||||
_ALL_REMINDERS = ["daily", "work", "beg", "crime", "rob"]
|
||||
records = await pb_client.list_all_records()
|
||||
changed = 0
|
||||
for record in records:
|
||||
reminders = record.get("reminders")
|
||||
if not reminders:
|
||||
await pb_client.update_record(record["id"], {"reminders": _ALL_REMINDERS})
|
||||
changed += 1
|
||||
return changed
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Internal helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -498,6 +607,17 @@ async def get_leaderboard(top_n: int | None = 10) -> list[tuple[str, int]]:
|
||||
return result if top_n is None else result[:top_n]
|
||||
|
||||
|
||||
def _prestige_mult(user: UserData) -> tuple[float, float]:
|
||||
"""Return (coin_mult, exp_mult) based on prestige upgrades. Both ≥1.0."""
|
||||
upgrades: dict = user.get("prestige_upgrades") or {} # type: ignore[assignment]
|
||||
coin_level = upgrades.get("coin_mult", 0)
|
||||
exp_level = upgrades.get("exp_mult", 0)
|
||||
return (
|
||||
1.0 + coin_level * PRESTIGE_SHOP["coin_mult"]["effect"],
|
||||
1.0 + exp_level * PRESTIGE_SHOP["exp_mult"]["effect"],
|
||||
)
|
||||
|
||||
|
||||
async def get_leaderboard_exp(top_n: int | None = 10) -> list[tuple[str, int, int]]:
|
||||
"""Return top_n (user_id_str, exp, level) sorted by EXP descending."""
|
||||
records = await pb_client.list_all_records()
|
||||
@@ -511,15 +631,18 @@ async def get_leaderboard_exp(top_n: int | None = 10) -> list[tuple[str, int, in
|
||||
|
||||
|
||||
async def award_exp(user_id: int, amount: int) -> dict:
|
||||
"""Add EXP to a user. Returns old_level, new_level, total exp."""
|
||||
"""Add EXP to a user. Applies prestige exp_mult. Returns old_level, new_level, total exp."""
|
||||
user = await get_user(user_id)
|
||||
_, exp_mult = _prestige_mult(user)
|
||||
gained = max(1, int(amount * exp_mult))
|
||||
old_exp = user.get("exp", 0)
|
||||
new_exp = old_exp + amount
|
||||
new_exp = old_exp + gained
|
||||
old_level = get_level(old_exp)
|
||||
new_level = get_level(new_exp)
|
||||
user["exp"] = new_exp
|
||||
user["season_total_exp"] = user.get("season_total_exp", 0) + gained
|
||||
await _commit(user_id, user)
|
||||
return {"old_level": old_level, "new_level": new_level, "exp": new_exp}
|
||||
return {"old_level": old_level, "new_level": new_level, "exp": new_exp, "gained": gained}
|
||||
|
||||
|
||||
async def do_season_reset(top_n: int = 10) -> list[tuple[str, int, int]]:
|
||||
@@ -541,8 +664,10 @@ async def do_season_reset(top_n: int = 10) -> list[tuple[str, int, int]]:
|
||||
"last_beg": None,
|
||||
"last_crime": None,
|
||||
"last_rob": None,
|
||||
"last_fish": None,
|
||||
"daily_streak": 0,
|
||||
"last_streak_date": None,
|
||||
"season_total_exp": 0,
|
||||
}
|
||||
for record in records:
|
||||
await pb_client.update_record(record["id"], reset_fields)
|
||||
@@ -605,8 +730,13 @@ async def do_daily(user_id: int) -> dict:
|
||||
vip = "lan_pass" in user["items"]
|
||||
vip_mult = 2.0 if vip else 1.0
|
||||
|
||||
base = 150
|
||||
daily_plus_level = (user.get("prestige_upgrades") or {}).get("daily_plus", 0)
|
||||
base = int(150 * (1.0 + daily_plus_level * PRESTIGE_SHOP["daily_plus"]["effect"]))
|
||||
earned = int(base * streak_mult * vip_mult)
|
||||
if "korvaklapid" in user["items"]:
|
||||
earned += 25
|
||||
coin_mult, _ = _prestige_mult(user)
|
||||
earned = int(earned * coin_mult)
|
||||
|
||||
# Investor interest (capped at 500/day to prevent runaway wealth)
|
||||
interest = 0
|
||||
@@ -661,7 +791,10 @@ async def do_work(user_id: int) -> dict:
|
||||
if "energiajook" in user["items"] and random.random() < 0.30:
|
||||
lucky = True
|
||||
|
||||
earned = int(base * job_mult * worker_mult * desk_mult * (3.0 if lucky else 1.0))
|
||||
work_plus_level = (user.get("prestige_upgrades") or {}).get("work_plus", 0)
|
||||
work_plus_mult = 1.0 + work_plus_level * PRESTIGE_SHOP["work_plus"]["effect"]
|
||||
coin_mult, _ = _prestige_mult(user)
|
||||
earned = int(base * job_mult * worker_mult * desk_mult * (3.0 if lucky else 1.0) * work_plus_mult * coin_mult)
|
||||
user["balance"] += earned
|
||||
user["last_work"] = _now().isoformat()
|
||||
user["work_count"] = user.get("work_count", 0) + 1
|
||||
@@ -699,7 +832,8 @@ async def do_beg(user_id: int) -> dict:
|
||||
|
||||
jailed = bool(_is_jailed(user))
|
||||
beg_mult = 2 if "klaviatuur" in user["items"] else 1
|
||||
earned = random.randint(10, 40) * beg_mult
|
||||
coin_mult, _ = _prestige_mult(user)
|
||||
earned = int(random.randint(10, 40) * beg_mult * coin_mult)
|
||||
user["balance"] += earned
|
||||
user["last_beg"] = _now().isoformat()
|
||||
user["beg_count"] = user.get("beg_count", 0) + 1
|
||||
@@ -718,6 +852,262 @@ async def do_beg(user_id: int) -> dict:
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /fish
|
||||
# ---------------------------------------------------------------------------
|
||||
async def do_fish_start(user_id: int) -> dict:
|
||||
"""Check cooldown + jail, set cooldown. Call before starting the fishing minigame."""
|
||||
user = await get_user(user_id)
|
||||
if user.get("eco_banned"):
|
||||
return {"ok": False, "reason": "banned"}
|
||||
if jail := _is_jailed(user):
|
||||
return {"ok": False, "reason": "jailed", "remaining": jail}
|
||||
|
||||
fish_cd = timedelta(seconds=90) if "ussipurk" in user["items"] else COOLDOWNS["fish"]
|
||||
if cd := _cooldown_remaining(user, "fish", override_cd=fish_cd):
|
||||
return {"ok": False, "reason": "cooldown", "remaining": cd}
|
||||
|
||||
user["last_fish"] = _now().isoformat()
|
||||
await _commit(user_id, user)
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
async def do_fish_resolve(user_id: int, fish_id: str, weight: int) -> dict:
|
||||
"""Add catch to inventory + update fish_book. Returns catch info incl. pre-calculated value."""
|
||||
user = await get_user(user_id)
|
||||
|
||||
if fish_id == "junk":
|
||||
_txn("FISH_JUNK", user=user_id)
|
||||
return {"ok": True, "type": "junk", "coins": 0, "exp": 0}
|
||||
|
||||
if fish_id not in FISH_CATALOGUE:
|
||||
return {"ok": False, "reason": "invalid_fish"}
|
||||
|
||||
fish = FISH_CATALOGUE[fish_id]
|
||||
min_c, max_c = fish["coins"]
|
||||
w_min, w_max = fish["weight"]
|
||||
weight_ratio = (weight - w_min) / max(1, w_max - w_min)
|
||||
base_coins = int(min_c + weight_ratio * (max_c - min_c))
|
||||
coin_mult, _ = _prestige_mult(user)
|
||||
value = int(base_coins * coin_mult)
|
||||
exp = fish["exp"]
|
||||
|
||||
book: dict = user.get("fish_book") or {}
|
||||
prev_count = book.get(fish_id, 0)
|
||||
book[fish_id] = prev_count + 1
|
||||
user["fish_book"] = book
|
||||
user["total_fish_caught"] = user.get("total_fish_caught", 0) + 1
|
||||
|
||||
inv: list = list(user.get("fish_inventory") or [])
|
||||
inv.append({"fish_id": fish_id, "weight": weight, "value": value})
|
||||
user["fish_inventory"] = inv
|
||||
|
||||
await _commit(user_id, user)
|
||||
_txn("FISH", user=user_id, fish=fish_id, weight=weight, value=value)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"type": "fish",
|
||||
"fish_id": fish_id,
|
||||
"weight": weight,
|
||||
"value": value,
|
||||
"exp": exp,
|
||||
"is_new": prev_count == 0,
|
||||
"total_caught": book[fish_id],
|
||||
}
|
||||
|
||||
|
||||
async def do_fish_sell(user_id: int, indices: list[int] | None = None) -> dict:
|
||||
"""Sell fish from inventory. indices=None sells all. Returns coins earned."""
|
||||
user = await get_user(user_id)
|
||||
inv: list = list(user.get("fish_inventory") or [])
|
||||
if not inv:
|
||||
return {"ok": False, "reason": "empty"}
|
||||
|
||||
if indices is None:
|
||||
to_sell = inv
|
||||
remaining = []
|
||||
else:
|
||||
to_sell = [inv[i] for i in sorted(set(indices)) if 0 <= i < len(inv)]
|
||||
keep_idx = set(range(len(inv))) - set(indices)
|
||||
remaining = [inv[i] for i in sorted(keep_idx)]
|
||||
|
||||
if not to_sell:
|
||||
return {"ok": False, "reason": "empty"}
|
||||
|
||||
total_coins = sum(entry["value"] for entry in to_sell)
|
||||
user["fish_inventory"] = remaining
|
||||
user["balance"] = user.get("balance", 0) + total_coins
|
||||
user["lifetime_earned"] = user.get("lifetime_earned", 0) + total_coins
|
||||
user["peak_balance"] = max(user.get("peak_balance", 0), user["balance"])
|
||||
await _commit(user_id, user)
|
||||
_txn("FISH_SELL", user=user_id, count=len(to_sell), coins=f"+{total_coins}", bal=user["balance"])
|
||||
return {
|
||||
"ok": True,
|
||||
"coins": total_coins,
|
||||
"count": len(to_sell),
|
||||
"balance": user["balance"],
|
||||
}
|
||||
|
||||
|
||||
async def do_fishbook(user_id: int) -> dict:
|
||||
"""Return the user's fish book data including per-species inventory counts."""
|
||||
user = await get_user(user_id)
|
||||
book: dict = user.get("fish_book") or {}
|
||||
inv: list = user.get("fish_inventory") or []
|
||||
inv_counts: dict[str, int] = {}
|
||||
for entry in inv:
|
||||
fid = entry.get("fish_id", "")
|
||||
inv_counts[fid] = inv_counts.get(fid, 0) + 1
|
||||
return {
|
||||
"ok": True,
|
||||
"book": book,
|
||||
"inv_counts": inv_counts,
|
||||
"total_fish_caught": user.get("total_fish_caught", 0),
|
||||
"unique_caught": len(book),
|
||||
"total_species": len(FISH_CATALOGUE),
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /prestige
|
||||
# ---------------------------------------------------------------------------
|
||||
async def do_prestige(user_id: int) -> dict:
|
||||
"""Prestige: requires level 30, earns PP, resets balance/exp/items/cooldowns."""
|
||||
user = await get_user(user_id)
|
||||
if user.get("eco_banned"):
|
||||
return {"ok": False, "reason": "banned"}
|
||||
|
||||
exp = user.get("exp", 0)
|
||||
level = get_level(exp)
|
||||
if level < PRESTIGE_MIN_LEVEL:
|
||||
return {"ok": False, "reason": "level_too_low", "level": level, "required": PRESTIGE_MIN_LEVEL}
|
||||
|
||||
pp_earned = max(1, exp // 1000)
|
||||
new_prestige_level = user.get("prestige_level", 0) + 1
|
||||
|
||||
# Preserve: fish_book, fish_inventory, lifetime stats, prestige_points, season_total_exp, prestige_upgrades
|
||||
user["balance"] = 0
|
||||
user["exp"] = 0
|
||||
user["items"] = []
|
||||
user["item_uses"] = {}
|
||||
user["last_daily"] = None
|
||||
user["last_work"] = None
|
||||
user["last_beg"] = None
|
||||
user["last_crime"] = None
|
||||
user["last_rob"] = None
|
||||
user["last_fish"] = None
|
||||
user["last_heist"] = None
|
||||
user["daily_streak"] = 0
|
||||
user["last_streak_date"] = None
|
||||
user["jailed_until"] = None
|
||||
user["jailbreak_used"] = False
|
||||
user["prestige_level"] = new_prestige_level
|
||||
user["prestige_points"] = user.get("prestige_points", 0) + pp_earned
|
||||
|
||||
await _commit(user_id, user)
|
||||
_txn("PRESTIGE", user=user_id, pp_earned=pp_earned, prestige=new_prestige_level, old_exp=exp)
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"pp_earned": pp_earned,
|
||||
"prestige_level": new_prestige_level,
|
||||
"prestige_points": user["prestige_points"],
|
||||
"old_exp": exp,
|
||||
}
|
||||
|
||||
|
||||
async def do_prestige_buy(user_id: int, upgrade_id: str) -> dict:
|
||||
"""Spend PP to buy a prestige upgrade level."""
|
||||
if upgrade_id not in PRESTIGE_SHOP:
|
||||
return {"ok": False, "reason": "not_found"}
|
||||
|
||||
user = await get_user(user_id)
|
||||
if user.get("eco_banned"):
|
||||
return {"ok": False, "reason": "banned"}
|
||||
|
||||
upgrade = PRESTIGE_SHOP[upgrade_id]
|
||||
upgrades: dict = user.get("prestige_upgrades") or {}
|
||||
current_level = upgrades.get(upgrade_id, 0)
|
||||
|
||||
if current_level >= upgrade["max_level"]:
|
||||
return {"ok": False, "reason": "maxed", "max": upgrade["max_level"]}
|
||||
|
||||
pp = user.get("prestige_points", 0)
|
||||
cost = upgrade["pp_cost"]
|
||||
if pp < cost:
|
||||
return {"ok": False, "reason": "insufficient_pp", "have": pp, "need": cost}
|
||||
|
||||
upgrades[upgrade_id] = current_level + 1
|
||||
user["prestige_upgrades"] = upgrades
|
||||
user["prestige_points"] = pp - cost
|
||||
|
||||
await _commit(user_id, user)
|
||||
_txn("PRESTIGE_BUY", user=user_id, upgrade=upgrade_id,
|
||||
new_level=upgrades[upgrade_id], pp_left=user["prestige_points"])
|
||||
|
||||
return {
|
||||
"ok": True,
|
||||
"upgrade_id": upgrade_id,
|
||||
"new_level": upgrades[upgrade_id],
|
||||
"max_level": upgrade["max_level"],
|
||||
"pp_remaining": user["prestige_points"],
|
||||
}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Extended leaderboards
|
||||
# ---------------------------------------------------------------------------
|
||||
async def get_leaderboard_season_exp(top_n: int | None = 10) -> list[tuple[str, int, int]]:
|
||||
"""Return (user_id, season_total_exp, prestige_level) sorted by season EXP."""
|
||||
records = await pb_client.list_all_records()
|
||||
result = sorted(
|
||||
(
|
||||
(r["user_id"], r.get("season_total_exp", 0), r.get("prestige_level", 0))
|
||||
for r in records if r.get("user_id")
|
||||
),
|
||||
key=lambda x: x[1],
|
||||
reverse=True,
|
||||
)
|
||||
return result if top_n is None else result[:top_n]
|
||||
|
||||
|
||||
async def get_leaderboard_prestige(top_n: int | None = 10) -> list[tuple[str, int, int]]:
|
||||
"""Return (user_id, prestige_level, prestige_points) sorted by prestige_level then PP."""
|
||||
records = await pb_client.list_all_records()
|
||||
result = sorted(
|
||||
(
|
||||
(r["user_id"], r.get("prestige_level", 0), r.get("prestige_points", 0))
|
||||
for r in records if r.get("user_id")
|
||||
),
|
||||
key=lambda x: (x[1], x[2]),
|
||||
reverse=True,
|
||||
)
|
||||
return result if top_n is None else result[:top_n]
|
||||
|
||||
|
||||
async def get_leaderboard_wagered(top_n: int | None = 10) -> list[tuple[str, int]]:
|
||||
"""Return (user_id, total_wagered) sorted descending."""
|
||||
records = await pb_client.list_all_records()
|
||||
result = sorted(
|
||||
((r["user_id"], r.get("total_wagered", 0)) for r in records if r.get("user_id")),
|
||||
key=lambda x: x[1],
|
||||
reverse=True,
|
||||
)
|
||||
return result if top_n is None else result[:top_n]
|
||||
|
||||
|
||||
async def get_leaderboard_fish(top_n: int | None = 10) -> list[tuple[str, int]]:
|
||||
"""Return (user_id, total_fish_caught) sorted descending."""
|
||||
records = await pb_client.list_all_records()
|
||||
result = sorted(
|
||||
((r["user_id"], r.get("total_fish_caught", 0)) for r in records if r.get("user_id")),
|
||||
key=lambda x: x[1],
|
||||
reverse=True,
|
||||
)
|
||||
return result if top_n is None else result[:top_n]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /crime
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -1171,6 +1561,57 @@ async def do_admin_inspect(target_id: int) -> dict:
|
||||
return {"ok": True, "data": dict(user)}
|
||||
|
||||
|
||||
async def do_admin_exp(target_id: int, amount: int, admin_id: int, reason: str) -> dict:
|
||||
"""Give (positive) or take (negative) EXP from a user. EXP is floored at 0."""
|
||||
user = await get_user(target_id)
|
||||
old_exp = user.get("exp", 0)
|
||||
old_level = get_level(old_exp)
|
||||
user["exp"] = max(0, old_exp + amount)
|
||||
user["season_total_exp"] = max(0, user.get("season_total_exp", 0) + amount)
|
||||
new_level = get_level(user["exp"])
|
||||
await _commit(target_id, user)
|
||||
verb = f"+{amount}" if amount >= 0 else str(amount)
|
||||
_txn("ADMIN_EXP", admin=admin_id, target=target_id, amount=verb, reason=reason, exp=user["exp"])
|
||||
return {
|
||||
"ok": True,
|
||||
"exp": user["exp"],
|
||||
"change": amount,
|
||||
"old_level": old_level,
|
||||
"new_level": new_level,
|
||||
"level_changed": new_level != old_level,
|
||||
}
|
||||
|
||||
|
||||
async def do_admin_item(target_id: int, item_id: str, action: str, admin_id: int) -> dict:
|
||||
"""Give or remove an item. action='give'|'remove'. Returns ok/reason."""
|
||||
if item_id not in SHOP:
|
||||
return {"ok": False, "reason": "invalid_item"}
|
||||
user = await get_user(target_id)
|
||||
items: list = list(user.get("items") or [])
|
||||
item_uses: dict = dict(user.get("item_uses") or {})
|
||||
if action == "give":
|
||||
if item_id not in items:
|
||||
items.append(item_id)
|
||||
if item_id == "anticheat":
|
||||
item_uses["anticheat"] = 2
|
||||
user["items"] = items
|
||||
user["item_uses"] = item_uses
|
||||
await _commit(target_id, user)
|
||||
_txn("ADMIN_ITEM_GIVE", admin=admin_id, target=target_id, item=item_id)
|
||||
return {"ok": True, "action": "given", "item_id": item_id}
|
||||
elif action == "remove":
|
||||
if item_id not in items:
|
||||
return {"ok": False, "reason": "not_owned"}
|
||||
items.remove(item_id)
|
||||
item_uses.pop(item_id, None)
|
||||
user["items"] = items
|
||||
user["item_uses"] = item_uses
|
||||
await _commit(target_id, user)
|
||||
_txn("ADMIN_ITEM_REMOVE", admin=admin_id, target=target_id, item=item_id)
|
||||
return {"ok": True, "action": "removed", "item_id": item_id}
|
||||
return {"ok": False, "reason": "invalid_action"}
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /reminders
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
Reference in New Issue
Block a user