The economy had many coin faucets but almost no sinks: the /shop is
one-time ownership, and gambling/rob fines route to the house (which
players drain back via jackpots and heists), so they recirculate rather
than destroy coins. Result: steady inflation.
Add a consumables shop as a true recurring sink - buying destroys the
coins and grants a temporary boost, so there's always something to spend
on after gear is maxed:
- Energiajook XL (500) - 1h of 2x earnings on /work, /beg, /crime
- XP jook (500) - 1h of 2x EXP
- Kohv (300) - instantly clears all cooldowns
Timed buffs live in a new active_buffs field ({kind: expiry_iso}), pruned
on read; rebuying extends the timer. Effects hook where they belong:
earn_mult in income.do_work/do_beg/do_crime, exp_buff_mult in
levels.award_exp; kohv is self-contained. New /consumables command browses
the menu (with active buffs) or buys a boost. Covered by 9 tests.
Note: active_buffs is a new PocketBase field - run
scripts/sync_pb_schema.py before deploying or buffs won't persist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
2.5 KiB
Python
82 lines
2.5 KiB
Python
"""EXP, levels and vanity role thresholds."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import math
|
|
|
|
from .store import _locked_by, _prestige_mult, get_user, _commit
|
|
from .consumables import exp_buff_mult
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# EXP / Level system
|
|
# ---------------------------------------------------------------------------
|
|
# EXP awarded per successful action
|
|
EXP_REWARDS: dict[str, int] = {
|
|
"daily": 50,
|
|
"work": 25,
|
|
"beg": 5,
|
|
"crime_win": 15,
|
|
"rob_win": 15,
|
|
"gamble_win": 10,
|
|
"heist_win": 25,
|
|
}
|
|
|
|
def gamble_exp(bet: int) -> int:
|
|
"""Scale EXP for a gambling win by bet size.
|
|
Returns 0 for bets < 10 coins to close micro-bet EXP farming.
|
|
10-99 → 5, 100-999 → 10, 1 000-9 999 → 15, 10 000+ → 20, 100 000+ → 25 (cap).
|
|
"""
|
|
return min(25, max(0, int(math.log10(max(1, bet))) * 5))
|
|
|
|
|
|
ECONOMY_ROLE = "ECONOMY"
|
|
|
|
# Vanity role milestones: (min_level, role_name) - highest first
|
|
LEVEL_ROLES: list[tuple[int, str]] = [
|
|
(30, "TipiLEGEND"),
|
|
(20, "TipiCHAD"),
|
|
(10, "TipiHUSTLER"),
|
|
(5, "TipiGRINDER"),
|
|
(1, "TipiNOOB"),
|
|
]
|
|
|
|
|
|
def get_level(exp: int) -> int:
|
|
"""Level = max(1, floor(sqrt(exp/10))).
|
|
Level 5 @ 250 EXP, 10 @ 1000, 20 @ 4000, 30 @ 9000."""
|
|
return max(1, int(math.sqrt(max(0, exp) / 10)))
|
|
|
|
|
|
def exp_for_level(level: int) -> int:
|
|
"""Minimum cumulative EXP to reach this level.
|
|
Recurrence: exp_for_level(L) = L*20 - 10 + exp_for_level(L-1), base 0.
|
|
Closed form: 10*level^2."""
|
|
if level <= 1:
|
|
return 0
|
|
return 10 * level * level
|
|
|
|
|
|
def level_role_name(level: int) -> str:
|
|
"""Return the vanity role name for a given level."""
|
|
for threshold, name in LEVEL_ROLES:
|
|
if level >= threshold:
|
|
return name
|
|
return LEVEL_ROLES[-1][1]
|
|
|
|
|
|
@_locked_by(0)
|
|
async def award_exp(user_id: int, amount: int) -> dict:
|
|
"""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 * exp_buff_mult(user)))
|
|
old_exp = user.get("exp", 0)
|
|
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, "gained": gained}
|