Skip to content

Commit c9029f4

Browse files
thehaffkclaude
andcommitted
feat: add giftbox and prestige systems — two new money sinks
Gift boxes (/giftbox): - 3 tiers: small (50💎), medium (200💎), large (500💎) - Random rewards: diamonds, titles, or nothing - Already-owned titles refund 50% as diamonds - 2 min cooldown between boxes Prestige (/prestige): - Costs 50,000💎 — resets balance to 0 - Grants permanent +5% income per level (max 10 = +50%) - Prestige bonus applies to all /job earnings - Shows in /profile with star display Also: - Updated help text with new commands - Migration 011: prestige_level column Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent e3ebf5e commit c9029f4

File tree

8 files changed

+401
-2
lines changed

8 files changed

+401
-2
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"""Add prestige_level column to users.
2+
3+
Revision ID: 011
4+
Revises: 010
5+
"""
6+
7+
from alembic import op
8+
import sqlalchemy as sa
9+
10+
revision = "011"
11+
down_revision = "010"
12+
13+
14+
def upgrade():
15+
op.add_column("users", sa.Column("prestige_level", sa.Integer(), nullable=False, server_default="0"))
16+
17+
18+
def downgrade():
19+
op.drop_column("users", "prestige_level")

app/bot.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,15 @@
1717
from app.handlers.children import register_children_handlers
1818
from app.handlers.duel import register_duel_handlers
1919
from app.handlers.economy import register_economy_handlers
20+
from app.handlers.giftbox import register_giftbox_handlers
2021
from app.handlers.feedback import register_feedback_handlers
2122
from app.handlers.house import register_house_handlers
2223
from app.handlers.lottery import register_lottery_handlers
2324
from app.handlers.marriage import register_marriage_handlers
2425
from app.handlers.menu import register_menu_handlers
2526
from app.handlers.mine import register_mine_handlers
2627
from app.handlers.pet import register_pet_handlers
28+
from app.handlers.prestige import register_prestige_handlers
2729
from app.handlers.quest import initialize_quests, register_quest_handlers
2830
from app.handlers.scratch import register_scratch_handlers
2931
from app.handlers.shop import register_shop_handlers
@@ -99,6 +101,8 @@ def create_bot() -> Application:
99101
register_scratch_handlers(application)
100102
register_blackjack_handlers(application)
101103
register_shop_handlers(application)
104+
register_giftbox_handlers(application)
105+
register_prestige_handlers(application)
102106
register_feedback_handlers(application)
103107
register_admin_handlers(application)
104108

app/database/models.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ class User(Base):
3333
last_daily_at = Column(DateTime, nullable=True)
3434
active_title = Column(String(100), nullable=True)
3535
purchased_titles = Column(String(1000), default="", nullable=False)
36+
prestige_level = Column(Integer, default=0, nullable=False)
3637
created_at = Column(DateTime, default=func.now(), nullable=False)
3738
updated_at = Column(DateTime, default=func.now(), onupdate=func.now(), nullable=False)
3839

app/handlers/giftbox.py

Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
"""Gift box (mystery box) command handlers — money sink."""
2+
3+
import random
4+
from datetime import datetime, timedelta
5+
6+
import structlog
7+
from telegram import Update
8+
from telegram.ext import CommandHandler, ContextTypes
9+
10+
from app.database.connection import get_db
11+
from app.database.models import Cooldown, User
12+
from app.handlers.quest import update_quest_progress
13+
from app.handlers.shop import SHOP_TITLES, add_user_title, get_user_titles
14+
from app.utils.decorators import require_registered
15+
from app.utils.formatters import format_diamonds
16+
17+
logger = structlog.get_logger()
18+
19+
GIFTBOX_COOLDOWN_SECONDS = 120 # 2 min between boxes
20+
21+
# Box tiers
22+
BOXES = {
23+
"small": {
24+
"name": "🎁 Маленький бокс",
25+
"price": 50,
26+
"rewards": [
27+
{"type": "diamonds", "amount": 10, "label": "10💎", "weight": 30},
28+
{"type": "diamonds", "amount": 25, "label": "25💎", "weight": 25},
29+
{"type": "diamonds", "amount": 50, "label": "50💎", "weight": 15},
30+
{"type": "diamonds", "amount": 100, "label": "100💎", "weight": 5},
31+
{"type": "nothing", "amount": 0, "label": "Пусто", "weight": 25},
32+
],
33+
# EV = 0.30*10 + 0.25*25 + 0.15*50 + 0.05*100 + 0.25*0 = 3+6.25+7.5+5 = 21.75
34+
# House edge = (50-21.75)/50 = 56.5%
35+
},
36+
"medium": {
37+
"name": "🎁 Средний бокс",
38+
"price": 200,
39+
"rewards": [
40+
{"type": "diamonds", "amount": 50, "label": "50💎", "weight": 25},
41+
{"type": "diamonds", "amount": 100, "label": "100💎", "weight": 20},
42+
{"type": "diamonds", "amount": 200, "label": "200💎", "weight": 12},
43+
{"type": "diamonds", "amount": 500, "label": "500💎", "weight": 3},
44+
{"type": "title", "title_id": "shadow", "label": "☠️ Титул «Тень»", "weight": 5},
45+
{"type": "title", "title_id": "fire", "label": "🔥 Титул «Огненный»", "weight": 5},
46+
{"type": "nothing", "amount": 0, "label": "Пусто", "weight": 30},
47+
],
48+
# EV diamonds = 0.25*50 + 0.20*100 + 0.12*200 + 0.03*500 = 12.5+20+24+15 = 71.5
49+
# Titles worth ~1000-1500 each, 10% chance = ~100-150 EV
50+
# Total EV ~170-220 vs 200 cost = ~10-15% house edge on average
51+
},
52+
"large": {
53+
"name": "🎁 Большой бокс",
54+
"price": 500,
55+
"rewards": [
56+
{"type": "diamonds", "amount": 100, "label": "100💎", "weight": 20},
57+
{"type": "diamonds", "amount": 250, "label": "250💎", "weight": 18},
58+
{"type": "diamonds", "amount": 500, "label": "500💎", "weight": 10},
59+
{"type": "diamonds", "amount": 1000, "label": "1000💎", "weight": 3},
60+
{"type": "diamonds", "amount": 2500, "label": "2500💎 ДЖЕКПОТ!", "weight": 1},
61+
{"type": "title", "title_id": "legend", "label": "⭐ Титул «Легенда»", "weight": 4},
62+
{"type": "title", "title_id": "angel", "label": "😇 Титул «Ангел»", "weight": 4},
63+
{"type": "title", "title_id": "devil", "label": "😈 Титул «Дьявол»", "weight": 4},
64+
{"type": "nothing", "amount": 0, "label": "Пусто", "weight": 36},
65+
],
66+
# EV diamonds = 0.20*100 + 0.18*250 + 0.10*500 + 0.03*1000 + 0.01*2500 = 20+45+50+30+25 = 170
67+
# Titles worth 2000-2500 each, 12% chance = ~240-300
68+
# Total EV ~410-470 vs 500 cost = ~6-18% house edge
69+
},
70+
}
71+
72+
73+
def roll_reward(box_type: str) -> dict:
74+
"""Roll a random reward from a box."""
75+
box = BOXES[box_type]
76+
rewards = box["rewards"]
77+
weights = [r["weight"] for r in rewards]
78+
return random.choices(rewards, weights=weights, k=1)[0]
79+
80+
81+
@require_registered
82+
async def giftbox_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
83+
"""Handle /giftbox [small|medium|large] command."""
84+
if not update.effective_user or not update.message:
85+
return
86+
87+
user_id = update.effective_user.id
88+
89+
# Parse box type
90+
if not context.args:
91+
text = (
92+
"🎁 <b>Гифт-боксы</b>\n\n"
93+
"Открой бокс и получи случайный приз!\n\n"
94+
f"🎁 /giftbox small — {format_diamonds(BOXES['small']['price'])}\n"
95+
" Алмазы 10-100 или пусто\n\n"
96+
f"🎁 /giftbox medium — {format_diamonds(BOXES['medium']['price'])}\n"
97+
" Алмазы 50-500 или титул\n\n"
98+
f"🎁 /giftbox large — {format_diamonds(BOXES['large']['price'])}\n"
99+
" Алмазы 100-2500 или редкий титул\n\n"
100+
"💡 Можно получить титул из магазина бесплатно!"
101+
)
102+
await update.message.reply_text(text, parse_mode="HTML")
103+
return
104+
105+
box_type = context.args[0].lower()
106+
if box_type not in BOXES:
107+
await update.message.reply_text("❌ Выбери: small, medium или large")
108+
return
109+
110+
box = BOXES[box_type]
111+
112+
with get_db() as db:
113+
user = db.query(User).filter(User.telegram_id == user_id).first()
114+
115+
# Check balance
116+
if user.balance < box["price"]:
117+
await update.message.reply_text(
118+
f"❌ Недостаточно алмазов\n\n"
119+
f"Цена: {format_diamonds(box['price'])}\n"
120+
f"У тебя: {format_diamonds(user.balance)}"
121+
)
122+
return
123+
124+
# Check cooldown
125+
cooldown = db.query(Cooldown).filter(Cooldown.user_id == user_id, Cooldown.action == "giftbox").first()
126+
if cooldown and cooldown.expires_at > datetime.utcnow():
127+
remaining = cooldown.expires_at - datetime.utcnow()
128+
seconds_left = int(remaining.total_seconds())
129+
await update.message.reply_text(f"⏰ Следующий бокс через {seconds_left}с")
130+
return
131+
132+
# Deduct payment
133+
user.balance -= box["price"]
134+
135+
# Set cooldown
136+
expires_at = datetime.utcnow() + timedelta(seconds=GIFTBOX_COOLDOWN_SECONDS)
137+
if cooldown:
138+
cooldown.expires_at = expires_at
139+
else:
140+
db.add(Cooldown(user_id=user_id, action="giftbox", expires_at=expires_at))
141+
142+
# Roll reward
143+
reward = roll_reward(box_type)
144+
145+
# Apply reward
146+
reward_text = ""
147+
if reward["type"] == "diamonds":
148+
amount = reward["amount"]
149+
user.balance += amount
150+
reward_text = f"💎 +{format_diamonds(amount)}"
151+
elif reward["type"] == "title":
152+
title_id = reward["title_id"]
153+
owned = get_user_titles(user)
154+
if title_id in owned:
155+
# Already has title — give diamond equivalent instead
156+
title_data = SHOP_TITLES.get(title_id, {})
157+
refund = title_data.get("price", 500) // 2
158+
user.balance += refund
159+
reward_text = f"🔄 Титул уже есть → +{format_diamonds(refund)}"
160+
else:
161+
add_user_title(user, title_id)
162+
user.active_title = title_id
163+
reward_text = f"🏆 {reward['label']} — установлен!"
164+
else:
165+
reward_text = "💨 Пусто... Повезёт в следующий раз!"
166+
167+
balance = user.balance
168+
169+
# Build message
170+
text = (
171+
f"{box['name']}\n\n"
172+
f"Потрачено: {format_diamonds(box['price'])}\n\n"
173+
f"<b>{reward_text}</b>\n\n"
174+
f"💰 Баланс: {format_diamonds(balance)}"
175+
)
176+
177+
await update.message.reply_text(text, parse_mode="HTML")
178+
179+
try:
180+
update_quest_progress(user_id, "casino")
181+
except Exception:
182+
pass
183+
184+
logger.info(
185+
"Giftbox opened",
186+
user_id=user_id,
187+
box_type=box_type,
188+
reward_type=reward["type"],
189+
reward_label=reward["label"],
190+
cost=box["price"],
191+
)
192+
193+
194+
def register_giftbox_handlers(application):
195+
"""Register giftbox handlers."""
196+
application.add_handler(CommandHandler("giftbox", giftbox_command))
197+
logger.info("Giftbox handlers registered")

0 commit comments

Comments
 (0)