feat(economy): add /consumables shop of repeatable, expiring boosts

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>
This commit is contained in:
Rene Arumetsa
2026-08-19 20:32:36 +03:00
parent b9b4c7c4c7
commit a2e1601d0e
11 changed files with 371 additions and 5 deletions

View File

@@ -14,6 +14,7 @@ from .store import (
_is_jailed, _locked_by, _log, _now, _prestige_mult, _txn, get_user,
)
from .house import _credit_house
from .consumables import earn_mult
# ---------------------------------------------------------------------------
@@ -126,7 +127,7 @@ async def do_work(user_id: int) -> dict:
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)
earned = int(base * job_mult * worker_mult * desk_mult * (3.0 if lucky else 1.0) * work_plus_mult * coin_mult * earn_mult(user))
user["balance"] += earned
user["last_work"] = _now().isoformat()
user["work_count"] = user.get("work_count", 0) + 1
@@ -169,7 +170,7 @@ async def do_beg(user_id: int) -> dict:
jailed = bool(_is_jailed(user))
beg_mult = 2 if "klaviatuur" in user["items"] else 1
coin_mult, _ = _prestige_mult(user)
earned = int(random.randint(10, 40) * beg_mult * coin_mult)
earned = int(random.randint(10, 40) * beg_mult * coin_mult * earn_mult(user))
user["balance"] += earned
user["last_beg"] = _now().isoformat()
user["beg_count"] = user.get("beg_count", 0) + 1
@@ -217,6 +218,7 @@ async def do_crime(user_id: int) -> dict:
earned = random.randint(200, 500)
if "mikrofon" in user["items"]:
earned = int(earned * 1.3)
earned = int(earned * earn_mult(user))
user["balance"] += earned
user["crimes_succeeded"] = user.get("crimes_succeeded", 0) + 1
user["lifetime_earned"] = user.get("lifetime_earned", 0) + earned