Files
tipibot/core/economy/fishing.py
2026-07-27 00:49:34 +03:00

195 lines
7.5 KiB
Python

"""Fishing minigame: catalogue, rolls, catch/sell flows."""
from __future__ import annotations
import random
from datetime import timedelta
from ..pb_client import DatabaseError
from .store import (
COOLDOWNS, _cooldown_remaining, _commit, _is_jailed, _locked_by, _now,
_prestige_mult, _txn, get_user,
)
# ---------------------------------------------------------------------------
# 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)
# ---------------------------------------------------------------------------
# /fish
# ---------------------------------------------------------------------------
@_locked_by(0)
async def do_fish_start(user_id: int) -> dict:
"""Check cooldown + jail, set cooldown. Call before starting the fishing minigame."""
try:
user = await get_user(user_id)
except DatabaseError:
return {"ok": False, "reason": "db_error"}
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}
@_locked_by(0)
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],
}
@_locked_by(0)
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:
sell_idx = {
(i if i >= 0 else len(inv) + i)
for i in indices
}
sell_idx = {i for i in sell_idx if 0 <= i < len(inv)}
to_sell = [inv[i] for i in sorted(sell_idx)]
keep_idx = set(range(len(inv))) - sell_idx
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),
}