9fed81082f
Convert all money-flow endpoints (api_open_case, api_submit_contract, api_execute_upgrade, api_sell_item, api_sell_bulk_items, api_open_multiple_cases, api_open_slot_case, api_open_case_animated, api_crash_bet, api_crash_cashout, api_get_inventory, api_item_detail, api_rpu_info) to use AsyncSession + await throughout. Critical bugfix: rpu.py and achievements.py functions (all async def) were being called without 'await' and with sync Session objects, which returned coroutine objects instead of actual data. This made RPU weight adjustment, achievement tracking, and stat updates non-functional. Also added AsyncSession to imports.
3197 lines
120 KiB
Python
3197 lines
120 KiB
Python
import asyncio
|
||
from fastapi import FastAPI, Request, Depends, HTTPException, Form, Response, WebSocket, WebSocketDisconnect
|
||
from fastapi.responses import FileResponse, HTMLResponse, RedirectResponse, JSONResponse
|
||
from fastapi.staticfiles import StaticFiles
|
||
from fastapi.templating import Jinja2Templates
|
||
from starlette.responses import FileResponse
|
||
from starlette.types import Scope
|
||
import os
|
||
import mimetypes
|
||
|
||
class CachedStaticFiles(StaticFiles):
|
||
async def get_response(self, path: str, scope: Scope) -> FileResponse:
|
||
response = await super().get_response(path, scope)
|
||
if response.status_code == 200:
|
||
ext = os.path.splitext(path)[1].lower()
|
||
if ext in ('.css', '.js', '.png', '.jpg', '.jpeg', '.gif', '.svg', '.ico', '.woff2', '.webp'):
|
||
response.headers['Cache-Control'] = 'public, max-age=86400'
|
||
elif ext in ('.html',):
|
||
response.headers['Cache-Control'] = 'no-cache'
|
||
return response
|
||
from sqlalchemy.orm import Session
|
||
from sqlalchemy import desc
|
||
from typing import Optional, List
|
||
import json
|
||
import random
|
||
import math
|
||
from datetime import datetime, timedelta
|
||
from pathlib import Path
|
||
import json
|
||
from pathlib import Path
|
||
|
||
from database import get_db, get_db_executor, get_async_db, AsyncSessionLocal, SessionLocal, AsyncSession, User, InventoryItem, CaseOpening, Contract, Upgrade, UpgradeRPU, PromoCode
|
||
from websocket_manager import manager, crash_game
|
||
from sqlalchemy import select, func as sa_func, desc
|
||
from achievements import (
|
||
seed_achievements, check_achievement,
|
||
check_open_cases_achievements, check_contracts_achievements,
|
||
check_upgrade_achievements, check_upgrade_risk_achievements,
|
||
check_upgrade_result_achievements, check_rare_item_achievement,
|
||
check_knife_achievement, check_spent_achievements,
|
||
check_balance_achievements, check_slot_match_achievements,
|
||
check_slot_plays_achievements, check_slot_match_count_achievements,
|
||
check_inventory_achievements, check_dildo_achievements,
|
||
check_dildo_contract_achievement, check_crash_achievements,
|
||
check_crash_cashout_achievements, check_items_unboxed_achievements,
|
||
check_total_profit_achievements, check_achievements_count_achievements,
|
||
check_rare_item_count_achievements, check_knife_count_achievements,
|
||
check_midnight_case_achievement, check_exact_cases_achievement,
|
||
check_collection_achievements, get_user_achievements
|
||
)
|
||
from auth import (
|
||
create_user, authenticate_user, create_access_token,
|
||
get_current_user, get_current_user_optional, get_password_hash,
|
||
get_current_user_sync, get_current_user_optional_sync,
|
||
ACCESS_TOKEN_EXPIRE_MINUTES, is_item_craftable
|
||
)
|
||
|
||
from admin import admin_router
|
||
|
||
import sys
|
||
sys.path.append(str(Path(__file__).parent.parent))
|
||
from backend import (
|
||
ALL_ITEMS, get_item, ITEM_BY_ID, NAME_LOWER_TO_IDS, COLLECTIONS_INDEX,
|
||
simulate_trade_up, calculate_average_float, RARITY_ORDER,
|
||
get_rarity_level, is_knife, is_gloves, extract_base_name,
|
||
get_possible_outcomes
|
||
)
|
||
|
||
from upgrade import (
|
||
get_upgrade_rpu, calculate_upgrade_probability,
|
||
get_adjusted_upgrade_probability, spin_upgrade_wheel,
|
||
update_upgrade_stats,
|
||
get_upgrade_rpu_async, get_adjusted_upgrade_probability_async,
|
||
update_upgrade_stats_async
|
||
)
|
||
from rpu import get_adjusted_weights, get_user_rpu, update_rpu_stats, RARITY_TO_FIELD
|
||
|
||
from auth import get_password_hash
|
||
|
||
# Создаем отдельное приложение для веб-интерфейса
|
||
app = FastAPI(title="CS2 Trade-Up Simulator Web")
|
||
|
||
app.include_router(admin_router)
|
||
|
||
# Статические файлы и шаблоны
|
||
app.mount("/static", CachedStaticFiles(directory="static"), name="static")
|
||
templates = Jinja2Templates(directory="templates")
|
||
|
||
def money_filter(value):
|
||
try:
|
||
return "{:,.0f}".format(float(value)).replace(",", " ")
|
||
except (ValueError, TypeError):
|
||
return "0"
|
||
|
||
templates.env.filters["money"] = money_filter
|
||
|
||
# Создаем отдельный роутер для API оригинального симулятора
|
||
from backend import app as api_app
|
||
|
||
# Монтируем оригинальное API на /api/simulator чтобы избежать конфликтов
|
||
app.mount("/api/simulator", api_app)
|
||
|
||
# ─── Pagination helper ─────────────────────────────────────────────────────
|
||
DEFAULT_PAGE_SIZE = 50
|
||
|
||
def paginate(query, page: int = 1, page_size: int = DEFAULT_PAGE_SIZE):
|
||
offset = (page - 1) * page_size
|
||
return query.offset(offset).limit(page_size)
|
||
|
||
def paginated_response(items, total: int, page: int, page_size: int = DEFAULT_PAGE_SIZE):
|
||
return {
|
||
"items": items,
|
||
"total": total,
|
||
"page": page,
|
||
"page_size": page_size,
|
||
"pages": max(1, (total + page_size - 1) // page_size),
|
||
}
|
||
|
||
# ─── Rate Limiting ──────────────────────────────────────────────────────────
|
||
from collections import defaultdict
|
||
import time
|
||
_rate_limit_store: dict = defaultdict(list)
|
||
|
||
RATE_LIMIT_WINDOW = 60 # seconds
|
||
RATE_LIMIT_MAX_CASES = 30 # max case opens per window
|
||
RATE_LIMIT_MAX_GENERAL = 120 # max general requests per window
|
||
|
||
async def rate_limit_check(user_id: int, action: str, max_req: int, window: int = RATE_LIMIT_WINDOW):
|
||
key = f"{user_id}:{action}"
|
||
now = time.time()
|
||
_rate_limit_store[key] = [t for t in _rate_limit_store.get(key, []) if now - t < window]
|
||
if len(_rate_limit_store[key]) >= max_req:
|
||
return False
|
||
_rate_limit_store[key].append(now)
|
||
return True
|
||
|
||
# ─── Вспомогательные функции ───────────────────────────────────────────────
|
||
|
||
# Кэш dildo-предметов (создаётся один раз)
|
||
DILDO_ITEMS_CACHE = None
|
||
|
||
def get_dildo_items():
|
||
global DILDO_ITEMS_CACHE
|
||
if DILDO_ITEMS_CACHE is None:
|
||
DILDO_ITEMS_CACHE = [it for it in ALL_ITEMS if it.get("weapon") == "Dildo"]
|
||
return DILDO_ITEMS_CACHE
|
||
|
||
def item_id_by_name(name: str) -> int:
|
||
"""O(1) поиск ID предмета по имени через NAME_LOWER_TO_IDS."""
|
||
key = (name or "").lower().strip()
|
||
ids = NAME_LOWER_TO_IDS.get(key)
|
||
return ids[0] if ids else 0
|
||
|
||
def reset_pidor_password():
|
||
db = next(get_db())
|
||
# Ищем пользователя с id=2 И username="pidor"
|
||
user = db.query(User).filter(User.id == 2, User.username == "pidor").first()
|
||
|
||
if user:
|
||
# Хешируем новый пароль
|
||
new_hashed_password = get_password_hash("adamkadirov123")
|
||
user.hashed_password = new_hashed_password
|
||
db.commit()
|
||
print(f"Пароль пользователя {user.username} (id={user.id}) сброшен на 'adamkadirov123'")
|
||
else:
|
||
print("Пользователь с id=2 и username='pidor' не найден")
|
||
|
||
def make_first_user_admin():
|
||
db = next(get_db())
|
||
user = db.query(User).first()
|
||
if user:
|
||
user.is_admin = True
|
||
db.commit()
|
||
print(f"Пользователь {user.username} стал администратором")
|
||
|
||
|
||
# Загрузка конфигурации кейсов
|
||
MAINPAGE_FILE = Path("mainpage.json")
|
||
CASES_FILE = Path("cases.json")
|
||
|
||
CASE_RARITY_WEIGHTS = {
|
||
"Consumer Grade": 1000,
|
||
"Industrial Grade": 500,
|
||
"Mil-Spec": 250,
|
||
"Mil-Spec Grade": 250,
|
||
"Restricted": 125,
|
||
"Classified": 25,
|
||
"Covert": 5,
|
||
"Rare Special Item": 1,
|
||
"Extraordinary": 1
|
||
}
|
||
|
||
def load_mainpage_config():
|
||
"""Загружает конфигурацию главной страницы кейсов"""
|
||
if MAINPAGE_FILE.exists():
|
||
try:
|
||
with open(MAINPAGE_FILE, 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
except Exception as e:
|
||
print(f"Ошибка загрузки mainpage.json: {e}")
|
||
return [
|
||
{
|
||
"tab": "Кейсы",
|
||
"cases": []
|
||
}
|
||
]
|
||
|
||
def load_cases_config():
|
||
"""Загружает конфигурацию кейсов"""
|
||
if CASES_FILE.exists():
|
||
try:
|
||
with open(CASES_FILE, 'r', encoding='utf-8') as f:
|
||
return json.load(f)
|
||
except Exception as e:
|
||
print(f"Ошибка загрузки cases.json: {e}")
|
||
return []
|
||
|
||
MAINPAGE_CONFIG = load_mainpage_config()
|
||
CASES_CONFIG = {c['name']: c for c in load_cases_config()}
|
||
|
||
print(f"Загружено секций: {len(MAINPAGE_CONFIG)}")
|
||
print(f"Загружено кейсов: {len(CASES_CONFIG)}")
|
||
|
||
from database import ActivityFeed
|
||
|
||
def record_activity_sync(db: Session, user_id: int, username: str, activity_type: str, message: str, data: dict = None) -> ActivityFeed:
|
||
activity = ActivityFeed(
|
||
user_id=user_id,
|
||
username=username,
|
||
activity_type=activity_type,
|
||
message=message,
|
||
data_json=json.dumps(data or {}, ensure_ascii=False)
|
||
)
|
||
db.add(activity)
|
||
try:
|
||
db.commit()
|
||
db.refresh(activity)
|
||
except Exception:
|
||
db.rollback()
|
||
return activity
|
||
|
||
def record_activity_own_session(user_id: int, username: str, activity_type: str, message: str, data: dict = None, db: Session = None):
|
||
"""Сохраняет активность. Если передан db — использует его (без commit), иначе своя сессия."""
|
||
try:
|
||
activity = ActivityFeed(
|
||
user_id=user_id,
|
||
username=username,
|
||
activity_type=activity_type,
|
||
message=message,
|
||
data_json=json.dumps(data or {}, ensure_ascii=False)
|
||
)
|
||
if db:
|
||
db.add(activity)
|
||
db.flush()
|
||
db.refresh(activity)
|
||
else:
|
||
own_db = SessionLocal()
|
||
own_db.add(activity)
|
||
own_db.commit()
|
||
own_db.refresh(activity)
|
||
own_db.close()
|
||
return {
|
||
"id": activity.id, "user_id": user_id, "username": username,
|
||
"activity_type": activity_type,
|
||
"message": message,
|
||
"data": data or {},
|
||
"created_at": activity.created_at.isoformat()
|
||
}
|
||
except Exception as e:
|
||
print(f"record_activity_own_session error: {e}")
|
||
return {
|
||
"id": 0, "user_id": user_id, "username": username,
|
||
"activity_type": activity_type,
|
||
"message": message,
|
||
"data": data or {},
|
||
"created_at": datetime.utcnow().isoformat()
|
||
}
|
||
|
||
# Сидируем достижения при старте
|
||
try:
|
||
from database import init_sync_db
|
||
init_sync_db()
|
||
db_seed = next(get_db())
|
||
from achievements import ACHIEVEMENT_DEFS
|
||
from database import Achievement
|
||
existing = db_seed.query(Achievement).count()
|
||
if existing == 0:
|
||
for defn in ACHIEVEMENT_DEFS:
|
||
db_seed.add(Achievement(**defn))
|
||
db_seed.commit()
|
||
print(f"[Seed] Добавлено {len(ACHIEVEMENT_DEFS)} достижений")
|
||
else:
|
||
existing_names = {a.name for a in db_seed.query(Achievement.name).all()}
|
||
added = 0
|
||
for defn in ACHIEVEMENT_DEFS:
|
||
if defn["name"] not in existing_names:
|
||
db_seed.add(Achievement(**defn))
|
||
added += 1
|
||
if added:
|
||
db_seed.commit()
|
||
print(f"[Seed] Добавлено {added} новых достижений")
|
||
db_seed.close()
|
||
except Exception as e:
|
||
print(f"[Seed] {e}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
|
||
async def broadcast_online_count():
|
||
"""Рассылает всем количество игроков онлайн"""
|
||
try:
|
||
await manager.broadcast({"type": "online_count", "online": manager.online_count})
|
||
except:
|
||
pass
|
||
|
||
@app.websocket("/ws")
|
||
async def websocket_endpoint(websocket: WebSocket):
|
||
"""WebSocket для реал-тайм обновлений"""
|
||
token = websocket.cookies.get("access_token", "")
|
||
if token.startswith("Bearer "):
|
||
token = token[7:]
|
||
|
||
from auth import decode_token, get_current_user_from_token
|
||
payload = decode_token(token)
|
||
user_id = 0
|
||
if payload:
|
||
username = payload.get("sub")
|
||
db = next(get_db())
|
||
user = db.query(User).filter(User.username == username).first()
|
||
if user:
|
||
user_id = user.id
|
||
db.close()
|
||
|
||
await manager.connect(websocket, user_id)
|
||
await broadcast_online_count()
|
||
try:
|
||
while True:
|
||
data = await websocket.receive_text()
|
||
try:
|
||
msg = json.loads(data)
|
||
if msg.get("type") == "ping":
|
||
await websocket.send_json({"type": "pong"})
|
||
except:
|
||
pass
|
||
except WebSocketDisconnect:
|
||
await manager.disconnect(websocket, user_id)
|
||
await broadcast_online_count()
|
||
|
||
def generate_item_float(rarity: str) -> float:
|
||
"""Генерирует float для предмета в зависимости от редкости"""
|
||
weights = CASE_RARITY_WEIGHTS
|
||
|
||
if rarity in ["Covert", "Rare Special Item", "Extraordinary"]:
|
||
# Для редких предметов float ближе к минимальному
|
||
return round(random.uniform(0.01, 0.15), 4)
|
||
elif rarity in ["Classified"]:
|
||
return round(random.uniform(0.01, 0.25), 4)
|
||
elif rarity in ["Restricted"]:
|
||
return round(random.uniform(0.01, 0.35), 4)
|
||
else:
|
||
return round(random.uniform(0.01, 0.45), 4)
|
||
|
||
def open_case(case_name: str) -> dict:
|
||
"""Открывает кейс и возвращает предмет"""
|
||
case_items = []
|
||
|
||
# Ищем предметы в кейсе
|
||
search_name = f"Case: {case_name}"
|
||
if search_name in COLLECTIONS_INDEX:
|
||
for idx in COLLECTIONS_INDEX[search_name]:
|
||
item = get_item(idx)
|
||
if item:
|
||
case_items.append((idx, item))
|
||
|
||
if not case_items:
|
||
return None
|
||
|
||
# Взвешенный выбор по редкости
|
||
items_with_weights = []
|
||
for idx, item in case_items:
|
||
rarity = item.get("rarity", "Unknown")
|
||
weight = CASE_RARITY_WEIGHTS.get(rarity, 100)
|
||
items_with_weights.append((idx, item, weight))
|
||
|
||
total_weight = sum(w for _, _, w in items_with_weights)
|
||
rand_val = random.uniform(0, total_weight)
|
||
|
||
cumsum = 0
|
||
chosen_item = None
|
||
chosen_idx = None
|
||
|
||
for idx, item, weight in items_with_weights:
|
||
cumsum += weight
|
||
if rand_val <= cumsum:
|
||
chosen_item = item
|
||
chosen_idx = idx
|
||
break
|
||
|
||
if not chosen_item:
|
||
chosen_idx, chosen_item = items_with_weights[-1][:2]
|
||
|
||
item_float = chosen_item.get("mid_float_used") or generate_item_float(chosen_item.get("rarity", "Unknown"))
|
||
return {
|
||
"id": chosen_idx,
|
||
"item": chosen_item,
|
||
"float": item_float
|
||
}
|
||
|
||
# ─── Веб-страницы ──────────────────────────────────────────────────────────
|
||
|
||
@app.get("/", response_class=HTMLResponse)
|
||
async def index(request: Request, user: Optional[User] = Depends(get_current_user_optional)):
|
||
"""Главная страница — редирект на кейсы"""
|
||
if user:
|
||
return RedirectResponse(url="/cases")
|
||
|
||
return templates.TemplateResponse(request, "index.html", {
|
||
"user": None,
|
||
"active_page": "home"
|
||
})
|
||
|
||
@app.get("/login", response_class=HTMLResponse)
|
||
async def login_page(request: Request, user: Optional[User] = Depends(get_current_user_optional)):
|
||
"""Страница входа"""
|
||
if user:
|
||
return RedirectResponse(url="/profile")
|
||
|
||
return templates.TemplateResponse(request, "login.html", {
|
||
"user": None,
|
||
"active_page": "login"
|
||
})
|
||
|
||
@app.get("/register", response_class=HTMLResponse)
|
||
async def register_page(request: Request, user: Optional[User] = Depends(get_current_user_optional)):
|
||
"""Страница регистрации"""
|
||
if user:
|
||
return RedirectResponse(url="/profile")
|
||
|
||
return templates.TemplateResponse(request, "register.html", {
|
||
"user": None,
|
||
"active_page": "register"
|
||
})
|
||
|
||
@app.get("/profile", response_class=HTMLResponse)
|
||
async def profile_page(
|
||
request: Request,
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
total_items = db.query(InventoryItem).filter(InventoryItem.user_id == user.id).count()
|
||
cases_opened = db.query(CaseOpening).filter(CaseOpening.user_id == user.id).count()
|
||
contracts_completed = db.query(Contract).filter(Contract.user_id == user.id).count()
|
||
|
||
recent_openings = db.query(CaseOpening).filter(
|
||
CaseOpening.user_id == user.id
|
||
).order_by(desc(CaseOpening.opened_at)).limit(10).all()
|
||
|
||
recent_contracts = db.query(Contract).filter(
|
||
Contract.user_id == user.id
|
||
).order_by(desc(Contract.created_at)).limit(10).all()
|
||
|
||
rarity_order = {k: v for k, v in RARITY_ORDER.items()}
|
||
|
||
inventory_items = db.query(InventoryItem).filter(
|
||
InventoryItem.user_id == user.id
|
||
).order_by(desc(InventoryItem.obtained_at)).limit(2000).all()
|
||
|
||
valuable_items = sorted(
|
||
inventory_items,
|
||
key=lambda x: rarity_order.get(x.rarity, 0),
|
||
reverse=True
|
||
)[:12]
|
||
|
||
rarities = set()
|
||
types = set()
|
||
enriched_items = []
|
||
for item in inventory_items:
|
||
enriched_items.append({
|
||
"id": item.id,
|
||
"item_id": item.item_id,
|
||
"name": item.market_hash_name,
|
||
"rarity": item.rarity,
|
||
"wear": item.wear,
|
||
"float": item.float_value,
|
||
"type": item.type,
|
||
"image_url": item.image_url or "",
|
||
"price_rub": item.price_rub or 0
|
||
})
|
||
if item.rarity:
|
||
rarities.add(item.rarity)
|
||
if item.type:
|
||
types.add(item.type)
|
||
|
||
return templates.TemplateResponse(request, "profile.html", {
|
||
"user": user,
|
||
"total_items": total_items,
|
||
"cases_opened": cases_opened,
|
||
"contracts_completed": contracts_completed,
|
||
"balance": user.balance,
|
||
"recent_openings": recent_openings,
|
||
"recent_contracts": recent_contracts,
|
||
"valuable_items": valuable_items,
|
||
"inventory_items": enriched_items,
|
||
"rarities": sorted(rarities),
|
||
"types": sorted(types),
|
||
"rarity_order": RARITY_ORDER,
|
||
"is_public": False,
|
||
"profile_user": user,
|
||
"active_page": "profile"
|
||
})
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 👤 Публичные профили
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
@app.get("/profiles/{profile_id}", response_class=HTMLResponse)
|
||
async def public_profile_page(
|
||
profile_id: int,
|
||
request: Request,
|
||
db: Session = Depends(get_db),
|
||
current_user: Optional[User] = Depends(get_current_user_optional)
|
||
):
|
||
"""Публичный профиль пользователя"""
|
||
profile_user = db.query(User).filter(User.id == profile_id).first()
|
||
if not profile_user:
|
||
raise HTTPException(404, detail="Пользователь не найден")
|
||
|
||
total_items = db.query(InventoryItem).filter(InventoryItem.user_id == profile_user.id).count()
|
||
cases_opened = db.query(CaseOpening).filter(CaseOpening.user_id == profile_user.id).count()
|
||
contracts_completed = db.query(Contract).filter(Contract.user_id == profile_user.id).count()
|
||
|
||
recent_openings = db.query(CaseOpening).filter(
|
||
CaseOpening.user_id == profile_user.id
|
||
).order_by(desc(CaseOpening.opened_at)).limit(10).all()
|
||
|
||
recent_contracts = db.query(Contract).filter(
|
||
Contract.user_id == profile_user.id
|
||
).order_by(desc(Contract.created_at)).limit(10).all()
|
||
|
||
inventory_items = db.query(InventoryItem).filter(
|
||
InventoryItem.user_id == profile_user.id
|
||
).order_by(desc(InventoryItem.obtained_at)).all()
|
||
|
||
rarity_order = {k: v for k, v in RARITY_ORDER.items()}
|
||
|
||
rarities = set()
|
||
types = set()
|
||
enriched_items = []
|
||
for item in inventory_items:
|
||
enriched_items.append({
|
||
"id": item.id,
|
||
"item_id": item.item_id,
|
||
"name": item.market_hash_name,
|
||
"rarity": item.rarity,
|
||
"wear": item.wear,
|
||
"float": item.float_value,
|
||
"type": item.type,
|
||
"image_url": item.image_url or "",
|
||
"price_rub": item.price_rub or 0
|
||
})
|
||
if item.rarity:
|
||
rarities.add(item.rarity)
|
||
if item.type:
|
||
types.add(item.type)
|
||
|
||
valuable_items = sorted(
|
||
inventory_items,
|
||
key=lambda x: rarity_order.get(x.rarity, 0),
|
||
reverse=True
|
||
)[:12]
|
||
|
||
return templates.TemplateResponse(request, "profile.html", {
|
||
"user": current_user,
|
||
"profile_user": profile_user,
|
||
"is_public": True,
|
||
"total_items": total_items,
|
||
"cases_opened": cases_opened,
|
||
"contracts_completed": contracts_completed,
|
||
"recent_openings": recent_openings,
|
||
"recent_contracts": recent_contracts,
|
||
"valuable_items": valuable_items,
|
||
"inventory_items": enriched_items,
|
||
"rarities": sorted(rarities),
|
||
"types": sorted(types),
|
||
"rarity_order": RARITY_ORDER,
|
||
"active_page": "profile"
|
||
})
|
||
|
||
|
||
@app.get("/web/api/profiles/{profile_id}/inventory")
|
||
async def api_public_profile_inventory(
|
||
profile_id: int,
|
||
db: Session = Depends(get_db),
|
||
current_user: Optional[User] = Depends(get_current_user_optional)
|
||
):
|
||
"""Публичный инвентарь пользователя (только для просмотра)"""
|
||
profile_user = db.query(User).filter(User.id == profile_id).first()
|
||
if not profile_user:
|
||
return JSONResponse(status_code=404, content={"error": "Пользователь не найден"})
|
||
|
||
items = db.query(InventoryItem).filter(
|
||
InventoryItem.user_id == profile_user.id
|
||
).order_by(desc(InventoryItem.obtained_at)).all()
|
||
|
||
result = []
|
||
for item in items:
|
||
result.append({
|
||
"id": item.id,
|
||
"item_id": item.item_id,
|
||
"name": item.market_hash_name,
|
||
"rarity": item.rarity,
|
||
"wear": item.wear,
|
||
"float": item.float_value,
|
||
"type": item.type,
|
||
"image_url": item.image_url or "",
|
||
})
|
||
|
||
return JSONResponse({"success": True, "items": result, "username": profile_user.username})
|
||
|
||
|
||
@app.get("/cases", response_class=HTMLResponse)
|
||
async def cases_page(
|
||
request: Request,
|
||
user: Optional[User] = Depends(get_current_user_optional),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""Страница с кейсами по секциям"""
|
||
|
||
# Авто-промокод для баннера
|
||
auto_promo = None
|
||
try:
|
||
from promo_service import get_current_auto_promo
|
||
auto_promo = get_current_auto_promo(db)
|
||
except Exception:
|
||
pass
|
||
|
||
# Подготавливаем данные для каждой секции
|
||
sections = []
|
||
for section in MAINPAGE_CONFIG:
|
||
section_cases = []
|
||
for case_name in section.get('cases', []):
|
||
case_config = CASES_CONFIG.get(case_name, {})
|
||
|
||
# Получаем предметы кейса
|
||
items = get_case_items(case_name)
|
||
|
||
# Считаем статистику
|
||
rarities = set()
|
||
for item in items:
|
||
rarities.add(item.get("rarity", "Unknown"))
|
||
|
||
case_data = {
|
||
"name": case_name,
|
||
"display_name": case_config.get('display_name', case_name),
|
||
"price": case_config.get('price_open', 250),
|
||
"items_count": len(items),
|
||
"rarities": list(rarities),
|
||
"image_url": case_config.get('image_url', ''),
|
||
"description": case_config.get('description', f'Содержит {len(items)} предметов')
|
||
}
|
||
section_cases.append(case_data)
|
||
|
||
if section_cases:
|
||
sections.append({
|
||
"tab": section.get('tab', 'Кейсы'),
|
||
"cases": section_cases
|
||
})
|
||
|
||
return templates.TemplateResponse(request, "cases.html", {
|
||
"user": user,
|
||
"sections": sections,
|
||
"active_page": "cases",
|
||
"auto_promo": auto_promo
|
||
})
|
||
|
||
|
||
@app.get("/contracts", response_class=HTMLResponse)
|
||
async def contracts_page(
|
||
request: Request,
|
||
db: Session = Depends(get_db),
|
||
user: Optional[User] = Depends(get_current_user_optional)
|
||
):
|
||
inventory_items = []
|
||
all_items = []
|
||
knife_items = []
|
||
has_contract = False
|
||
has_knife_contract = False
|
||
|
||
if user:
|
||
inventory_items = db.query(InventoryItem).filter(
|
||
InventoryItem.user_id == user.id
|
||
).all()
|
||
|
||
for inv_item in inventory_items:
|
||
rarity = inv_item.rarity or "Unknown"
|
||
item_type = inv_item.type or "Normal"
|
||
item_data = get_item(inv_item.item_id)
|
||
is_knife_item = item_data and is_knife(item_data)
|
||
|
||
entry = {
|
||
"inventory_id": inv_item.id,
|
||
"item_id": inv_item.item_id,
|
||
"name": inv_item.market_hash_name,
|
||
"float": inv_item.float_value,
|
||
"rarity": rarity,
|
||
"type": item_type,
|
||
"image_url": inv_item.image_url or "",
|
||
"is_knife": is_knife_item,
|
||
"craftable": is_item_craftable(inv_item.item_id) or is_knife_item
|
||
}
|
||
if is_knife_item:
|
||
knife_items.append(entry)
|
||
elif is_item_craftable(inv_item.item_id):
|
||
all_items.append(entry)
|
||
|
||
all_items.sort(key=lambda x: RARITY_ORDER.get(x["rarity"], 99))
|
||
|
||
# проверяем есть ли 10+ одинаковых по (rarity, type) в all_items
|
||
from collections import Counter
|
||
group_counts = Counter((i["rarity"], i["type"]) for i in all_items)
|
||
has_contract = any(c >= 10 for c in group_counts.values())
|
||
has_knife_contract = len(knife_items) >= 10
|
||
|
||
return templates.TemplateResponse(request, "contracts.html", {
|
||
"user": user,
|
||
"inventory_items": all_items + knife_items,
|
||
"has_contract": has_contract or has_knife_contract,
|
||
"knife_count": len(knife_items),
|
||
"active_page": "contracts"
|
||
})
|
||
|
||
@app.get("/inventory")
|
||
async def inventory_redirect():
|
||
return RedirectResponse(url="/profile")
|
||
|
||
# ─── API эндпоинты для веб-интерфейса ─────────────────────────────────────
|
||
|
||
@app.post("/web/api/auth/register")
|
||
async def api_register(
|
||
request: Request,
|
||
username: str = Form(...),
|
||
password: str = Form(...),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""Регистрация нового пользователя"""
|
||
|
||
# Проверяем существует ли пользователь
|
||
existing_user = db.query(User).filter(User.username == username).first()
|
||
if existing_user:
|
||
return JSONResponse(
|
||
status_code=400,
|
||
content={"error": "Username already exists"}
|
||
)
|
||
|
||
if len(username) < 3:
|
||
return JSONResponse(
|
||
status_code=400,
|
||
content={"error": "Username must be at least 3 characters"}
|
||
)
|
||
|
||
if len(password) < 4:
|
||
return JSONResponse(
|
||
status_code=400,
|
||
content={"error": "Password must be at least 4 characters"}
|
||
)
|
||
|
||
# Создаем пользователя
|
||
user = create_user(db, username, password)
|
||
|
||
# Создаем токен
|
||
access_token = create_access_token(data={"sub": user.username})
|
||
|
||
response = JSONResponse(content={"success": True, "redirect": "/profile"})
|
||
response.set_cookie(
|
||
key="access_token",
|
||
value=f"Bearer {access_token}",
|
||
httponly=True,
|
||
max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||
samesite="lax"
|
||
)
|
||
|
||
return response
|
||
|
||
@app.post("/web/api/auth/login")
|
||
async def api_login(
|
||
request: Request,
|
||
username: str = Form(...),
|
||
password: str = Form(...),
|
||
db: Session = Depends(get_db)
|
||
):
|
||
"""Вход в систему"""
|
||
|
||
user = authenticate_user(db, username, password)
|
||
if not user:
|
||
return JSONResponse(
|
||
status_code=401,
|
||
content={"error": "Invalid username or password"}
|
||
)
|
||
|
||
# Обновляем время последнего входа
|
||
user.last_login = datetime.utcnow()
|
||
db.commit()
|
||
|
||
# Создаем токен
|
||
access_token = create_access_token(data={"sub": user.username})
|
||
|
||
response = JSONResponse(content={"success": True, "redirect": "/profile"})
|
||
response.set_cookie(
|
||
key="access_token",
|
||
value=f"Bearer {access_token}",
|
||
httponly=True,
|
||
max_age=ACCESS_TOKEN_EXPIRE_MINUTES * 60,
|
||
samesite="lax"
|
||
)
|
||
|
||
return response
|
||
|
||
@app.post("/web/api/auth/logout")
|
||
async def api_logout():
|
||
"""Выход из системы"""
|
||
response = JSONResponse(content={"success": True, "redirect": "/"})
|
||
response.delete_cookie("access_token")
|
||
return response
|
||
|
||
|
||
def open_case_with_rpu(case_name: str, adjusted_weights: dict = None) -> dict:
|
||
"""Открывает кейс с учетом РПУ весов"""
|
||
case_items = []
|
||
|
||
search_name = f"Case: {case_name}"
|
||
if search_name in COLLECTIONS_INDEX:
|
||
for idx in COLLECTIONS_INDEX[search_name]:
|
||
item = get_item(idx)
|
||
if item:
|
||
case_items.append((idx, item))
|
||
|
||
if not case_items:
|
||
return None
|
||
|
||
items_with_weights = []
|
||
for idx, item in case_items:
|
||
rarity = item.get("rarity", "Unknown")
|
||
|
||
if adjusted_weights:
|
||
weight = adjusted_weights.get(rarity, CASE_RARITY_WEIGHTS.get(rarity, 100))
|
||
else:
|
||
weight = CASE_RARITY_WEIGHTS.get(rarity, 100)
|
||
|
||
items_with_weights.append((idx, item, weight))
|
||
|
||
total_weight = sum(w for _, _, w in items_with_weights)
|
||
rand_val = random.uniform(0, total_weight)
|
||
|
||
cumsum = 0
|
||
chosen_item = None
|
||
chosen_idx = None
|
||
|
||
for idx, item, weight in items_with_weights:
|
||
cumsum += weight
|
||
if rand_val <= cumsum:
|
||
chosen_item = item
|
||
chosen_idx = idx
|
||
break
|
||
|
||
if not chosen_item:
|
||
chosen_idx, chosen_item = items_with_weights[-1][:2]
|
||
|
||
# Находим правильный ID и image_url
|
||
real_item_id = chosen_idx
|
||
real_image_url = chosen_item.get("image_url", "")
|
||
|
||
# Ищем image_url через NAME_LOWER_TO_IDS (O(1) вместо O(n))
|
||
name_key = chosen_item.get("market_hash_name", "").lower().strip()
|
||
if name_key:
|
||
id_list = NAME_LOWER_TO_IDS.get(name_key)
|
||
if id_list:
|
||
first_id = id_list[0]
|
||
it = ITEM_BY_ID.get(first_id)
|
||
if it:
|
||
real_image_url = it.get("image_url", real_image_url)
|
||
|
||
item_float = chosen_item.get("mid_float_used") or generate_item_float(chosen_item.get("rarity", "Unknown"))
|
||
return {
|
||
"id": real_item_id,
|
||
"item": chosen_item,
|
||
"float": item_float,
|
||
"image_url": real_image_url
|
||
}
|
||
|
||
@app.post("/web/api/cases/open")
|
||
async def api_open_case(
|
||
case_name: str = Form(...),
|
||
count: int = Form(1),
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Открытие кейса с проверкой баланса и учетом РПУ"""
|
||
|
||
# Определяем максимальное количество в зависимости от типа кейса
|
||
is_farm = "фарм" in case_name.lower() or "farm" in case_name.lower()
|
||
max_allowed = 100 if is_farm else 10
|
||
|
||
if count < 1 or count > max_allowed:
|
||
return JSONResponse(status_code=400, content={"error": f"Количество должно быть от 1 до {max_allowed}"})
|
||
|
||
case_config = CASES_CONFIG.get(case_name, {})
|
||
price_per_case = case_config.get('price_open', 250)
|
||
total_price = price_per_case * count
|
||
|
||
if user.balance < total_price:
|
||
return JSONResponse(status_code=400, content={
|
||
"error": f"Недостаточно средств. Нужно {total_price:.2f} ₽, у вас {user.balance:.2f} ₽"
|
||
})
|
||
|
||
# Включаем batch-режим — все sub-функции делают flush вместо commit
|
||
import rpu as _rpu_mod
|
||
_rpu_mod._BATCH_MODE = True
|
||
try:
|
||
# Один раз считаем RPU на всю пачку (кэшируется внутри)
|
||
adjusted_weights = await get_adjusted_weights(db, user.id)
|
||
results = []
|
||
for _ in range(count):
|
||
result = open_case_with_rpu(case_name, adjusted_weights)
|
||
if result:
|
||
item_data = result["item"]
|
||
item_float = result["float"]
|
||
item_id = result["id"]
|
||
image_url = result.get("image_url", item_data.get("image_url", ""))
|
||
|
||
if item_id == 0:
|
||
name_key = item_data.get("market_hash_name", "").lower().strip()
|
||
id_list = NAME_LOWER_TO_IDS.get(name_key)
|
||
if id_list:
|
||
item_id = id_list[0]
|
||
it = ITEM_BY_ID.get(item_id)
|
||
if it:
|
||
image_url = it.get("image_url", image_url)
|
||
|
||
price = item_data.get("price_rub", 0) or 100
|
||
inventory_item = InventoryItem(
|
||
user_id=user.id,
|
||
item_id=item_id or 0,
|
||
market_hash_name=item_data.get("market_hash_name", "Unknown"),
|
||
rarity=item_data.get("rarity", "Unknown"),
|
||
wear=item_data.get("wear", "Unknown"),
|
||
float_value=item_float,
|
||
type=item_data.get("type", "Normal"),
|
||
image_url=item_data.get("image_url", ""),
|
||
price_rub=price,
|
||
obtained_from=f"case: {case_name}"
|
||
)
|
||
db.add(inventory_item)
|
||
|
||
opening = CaseOpening(
|
||
user_id=user.id,
|
||
case_name=case_name,
|
||
item_id=item_id or 0,
|
||
item_name=item_data.get("market_hash_name", "Unknown"),
|
||
rarity=item_data.get("rarity", "Unknown"),
|
||
float_value=item_float
|
||
)
|
||
db.add(opening)
|
||
|
||
results.append({
|
||
"id": item_id or 0,
|
||
"name": item_data.get("market_hash_name"),
|
||
"rarity": item_data.get("rarity"),
|
||
"float": item_float,
|
||
"price": price,
|
||
"image_url": image_url,
|
||
"wear": item_data.get("wear", "Unknown")
|
||
})
|
||
|
||
# Считаем общую стоимость полученного
|
||
total_received = sum(r.get("price", 0) for r in results)
|
||
# Определяем был ли редкий дроп
|
||
had_rare = any(r.get("rarity") in ("Covert", "Rare Special Item", "Extraordinary") for r in results)
|
||
|
||
await update_rpu_stats(db, user.id, total_price, count, total_received,
|
||
rarity=results[0]["rarity"] if results else None, is_rare=had_rare)
|
||
|
||
# Проверка достижений
|
||
unlocked_ach = []
|
||
open_count_result = await db.execute(select(sa_func.count(CaseOpening.id)).filter(CaseOpening.user_id == user.id))
|
||
open_count = open_count_result.scalar()
|
||
unlocked_ach += await check_open_cases_achievements(db, user.id)
|
||
user_rpu = await get_user_rpu(db, user.id)
|
||
total_spent = user_rpu.total_spent
|
||
unlocked_ach += await check_spent_achievements(db, user.id, total_spent)
|
||
|
||
for r in results:
|
||
unlocked_ach += await check_rare_item_achievement(db, user.id, r.get("rarity", ""))
|
||
item_data = get_item(r.get("id", 0))
|
||
if item_data:
|
||
unlocked_ach += await check_knife_achievement(db, user.id, item_data)
|
||
|
||
unlocked_ach += await check_inventory_achievements(db, user.id)
|
||
unlocked_ach += await check_dildo_achievements(db, user.id)
|
||
unlocked_ach += await check_items_unboxed_achievements(db, user.id)
|
||
unlocked_ach += await check_rare_item_count_achievements(db, user.id)
|
||
unlocked_ach += await check_knife_count_achievements(db, user.id)
|
||
unlocked_ach += await check_balance_achievements(db, user.id)
|
||
unlocked_ach += await check_achievements_count_achievements(db, user.id)
|
||
unlocked_ach += await check_midnight_case_achievement(db, user.id)
|
||
unlocked_ach += await check_exact_cases_achievement(db, user.id, open_count)
|
||
profit = user.balance + total_spent
|
||
unlocked_ach += await check_total_profit_achievements(db, user.id, profit)
|
||
|
||
# Запись активности — используем ту же db-сессию
|
||
for r in results:
|
||
item_price = r.get("price", 0)
|
||
mult = round(item_price / max(1, price_per_case), 1) if item_price > 0 else 0
|
||
effect = f"drop:{mult}x" if mult >= 3 else None
|
||
act_data = {
|
||
"case": case_name, "item": r["name"], "rarity": r.get("rarity", ""),
|
||
"item_image": r.get("image_url", ""), "mult": mult
|
||
}
|
||
if effect:
|
||
act_data["effect"] = effect
|
||
act = record_activity_own_session(user.id, user.username, "case_open",
|
||
f"📦 открыл(а) кейс и получил(а) {r['name']}", act_data, db=db)
|
||
try:
|
||
asyncio.ensure_future(manager.broadcast({
|
||
"type": "activity",
|
||
"activity": act
|
||
}))
|
||
except:
|
||
pass
|
||
finally:
|
||
_rpu_mod._BATCH_MODE = False
|
||
_rpu_mod._batch_cache_clear()
|
||
|
||
# Один большой commit вместо 40+ мелких
|
||
await db.commit()
|
||
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "inventory_update"}))
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "balance_update", "balance": user.balance}))
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"results": results,
|
||
"count": len(results),
|
||
"spent": total_price,
|
||
"new_balance": user.balance,
|
||
"achievements_unlocked": unlocked_ach if unlocked_ach else None
|
||
})
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════
|
||
# ДЕПОЗИТ, ВЫВОД, ПРОМОКОДЫ
|
||
# ═══════════════════════════════════════════════════════════════
|
||
|
||
@app.post("/web/api/deposit")
|
||
async def api_deposit(
|
||
amount: float = Form(...),
|
||
promo_code: str = Form(""),
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
from promo_service import direct_deposit
|
||
result = await direct_deposit(db, user.id, amount, promo_code.strip())
|
||
if not result["success"]:
|
||
return JSONResponse(status_code=400, content=result)
|
||
asyncio.ensure_future(manager.send_personal(user.id, {
|
||
"type": "balance_update", "balance": user.balance
|
||
}))
|
||
return JSONResponse(result)
|
||
|
||
|
||
@app.post("/web/api/promo/activate")
|
||
async def api_activate_promo(
|
||
code: str = Form(...),
|
||
amount: float = Form(0),
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
from promo_service import activate_promo_code
|
||
result = await activate_promo_code(db, user.id, code.strip(), amount)
|
||
if not result["success"]:
|
||
return JSONResponse(status_code=400, content=result)
|
||
asyncio.ensure_future(manager.send_personal(user.id, {
|
||
"type": "balance_update", "balance": user.balance
|
||
}))
|
||
return JSONResponse(result)
|
||
|
||
|
||
@app.post("/web/api/withdraw")
|
||
async def api_withdraw(
|
||
inventory_item_id: int = Form(...),
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
from promo_service import withdraw_item
|
||
result = await withdraw_item(db, user.id, inventory_item_id)
|
||
if not result["success"]:
|
||
return JSONResponse(status_code=400, content=result)
|
||
asyncio.ensure_future(manager.send_personal(user.id, {
|
||
"type": "inventory_update"
|
||
}))
|
||
return JSONResponse(result)
|
||
|
||
|
||
@app.get("/web/api/user/card-balance")
|
||
async def api_card_balance(
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
from bank_api import bank_client
|
||
import asyncio
|
||
balance = await bank_client.get_balance(user.id)
|
||
user.card_balance_cached = balance
|
||
db.commit()
|
||
return {"card_balance": balance}
|
||
|
||
|
||
@app.get("/web/api/online-count")
|
||
async def api_online_count():
|
||
try:
|
||
return {"online": manager.online_count}
|
||
except:
|
||
return {"online": 0}
|
||
|
||
@app.get("/web/api/promo/list")
|
||
async def api_promo_list(
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Возвращает активные промокоды + авто-промокод"""
|
||
from promo_service import get_current_auto_promo
|
||
|
||
now = datetime.utcnow()
|
||
promos = db.query(PromoCode).filter(
|
||
PromoCode.is_active == True,
|
||
PromoCode.start_date <= now,
|
||
(PromoCode.end_date >= now) | (PromoCode.end_date.is_(None)),
|
||
(PromoCode.max_uses > PromoCode.used_count) | (PromoCode.max_uses == 0)
|
||
).limit(20).all()
|
||
|
||
result = [{
|
||
"code": p.code,
|
||
"reward_type": p.reward_type,
|
||
"reward_amount": p.reward_amount,
|
||
"reward_data": p.reward_data,
|
||
"used_count": p.used_count,
|
||
"max_uses": p.max_uses,
|
||
} for p in promos]
|
||
|
||
# Добавляем авто-промокод
|
||
try:
|
||
auto = get_current_auto_promo(db)
|
||
if auto and not any(p["code"] == auto["code"] for p in result):
|
||
result.insert(0, {
|
||
"code": auto["code"],
|
||
"reward_type": "card_to_site",
|
||
"reward_amount": auto["reward_amount"],
|
||
"reward_data": "auto",
|
||
"used_count": auto["used_count"],
|
||
"max_uses": 999,
|
||
})
|
||
except Exception:
|
||
pass
|
||
|
||
return result
|
||
|
||
|
||
@app.get("/web/api/rpu-info")
|
||
async def api_rpu_info(
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
from rpu import get_rpu_v2_info
|
||
return await get_rpu_v2_info(db, user.id)
|
||
|
||
|
||
@app.post("/web/api/contracts/submit")
|
||
async def api_submit_contract(
|
||
inventory_ids: str = Form(...),
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
try:
|
||
ids = json.loads(inventory_ids)
|
||
except:
|
||
return JSONResponse(status_code=400, content={"error": "Invalid inventory IDs"})
|
||
|
||
if len(ids) != 10:
|
||
return JSONResponse(status_code=400, content={"error": "Exactly 10 items required"})
|
||
|
||
input_items_data = []
|
||
inventory_items = []
|
||
all_knives = True
|
||
|
||
for inv_id in ids:
|
||
result = await db.execute(
|
||
select(InventoryItem).filter(
|
||
InventoryItem.id == inv_id,
|
||
InventoryItem.user_id == user.id
|
||
)
|
||
)
|
||
inv_item = result.scalar_one_or_none()
|
||
|
||
if not inv_item:
|
||
return JSONResponse(status_code=400, content={"error": f"Item {inv_id} not found"})
|
||
|
||
item_data = get_item(inv_item.item_id)
|
||
if not item_data:
|
||
return JSONResponse(status_code=400, content={"error": f"Item data not found"})
|
||
|
||
if not is_knife(item_data):
|
||
all_knives = False
|
||
|
||
item_with_float = item_data.copy()
|
||
item_with_float["mid_float_used"] = inv_item.float_value
|
||
|
||
input_items_data.append(item_with_float)
|
||
inventory_items.append(inv_item)
|
||
|
||
# Проверка: если есть и ножи и не-ножи — ошибка
|
||
if not all_knives and any(is_knife(get_item(it.item_id)) for it in inventory_items):
|
||
return JSONResponse(status_code=400, content={"error": "Нельзя мешать ножи с обычными предметами"})
|
||
if not all_knives:
|
||
for inv_item in inventory_items:
|
||
if not is_item_craftable(inv_item.item_id):
|
||
return JSONResponse(status_code=400, content={"error": f"Предмет {inv_item.market_hash_name} нельзя использовать в контракте"})
|
||
|
||
# Крафт дилдоков из ножей
|
||
if all_knives and len(inventory_items) == 10:
|
||
for inv_item in inventory_items:
|
||
await db.delete(inv_item)
|
||
await db.commit()
|
||
|
||
dildo_items = get_dildo_items()
|
||
if not dildo_items:
|
||
return JSONResponse(status_code=500, content={"error": "Дилдоки кончились :("})
|
||
chosen = random.choice(dildo_items)
|
||
chosen_name = chosen.get("market_hash_name", "").lower().strip()
|
||
chosen_ids = NAME_LOWER_TO_IDS.get(chosen_name, [])
|
||
chosen_id = chosen_ids[0] if chosen_ids else ALL_ITEMS.index(chosen)
|
||
|
||
dildo_float = chosen.get("mid_float_used") or generate_item_float(chosen.get("rarity", "Covert"))
|
||
dildo_item = {
|
||
"success": True,
|
||
"received_item": {
|
||
"id": chosen_id,
|
||
"name": chosen.get("market_hash_name", "Dildo"),
|
||
"rarity": chosen.get("rarity", "Covert"),
|
||
"float": round(dildo_float, 6),
|
||
"probability": 100.0,
|
||
"image_url": chosen.get("image_url", "/static/placeholder.png")
|
||
},
|
||
"achievements_unlocked": None,
|
||
"is_knife_contract": True
|
||
}
|
||
|
||
act_data = {"item": chosen.get("market_hash_name", "Dildo"), "rarity": chosen.get("rarity", "Covert")}
|
||
act = record_activity_own_session(user.id, user.username, "contract",
|
||
f"🔞 скрафтил(а) {chosen.get('market_hash_name', 'дилдок')} из 10 ножей", act_data)
|
||
try:
|
||
asyncio.ensure_future(manager.broadcast({"type": "activity", "activity": act}))
|
||
except:
|
||
pass
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "inventory_update"}))
|
||
return JSONResponse(content=dildo_item)
|
||
|
||
result = simulate_trade_up(input_items_data)
|
||
|
||
if not result.success:
|
||
return JSONResponse(status_code=400, content={"error": result.message})
|
||
|
||
# Бан: принудительно выдаём самый дешёвый возможный предмет
|
||
if user.is_banned:
|
||
possible_outcomes, _ = get_possible_outcomes(input_items_data)
|
||
if possible_outcomes:
|
||
cheapest_idx = min(
|
||
possible_outcomes,
|
||
key=lambda x: (get_item(x[0]) or {}).get("price_rub", float('inf'))
|
||
)[0]
|
||
forced_item = get_item(cheapest_idx)
|
||
if forced_item:
|
||
from backend import ItemShort
|
||
result.received_item = ItemShort(
|
||
id=cheapest_idx,
|
||
market_hash_name=forced_item.get("market_hash_name", "Unknown"),
|
||
rarity=forced_item.get("rarity", "Unknown"),
|
||
wear=forced_item.get("wear", "Unknown"),
|
||
image_url=forced_item.get("image_url", ""),
|
||
price_rub=forced_item.get("price_rub", 0)
|
||
)
|
||
result.received_float = round(calculate_average_float(input_items_data), 4)
|
||
result.probability = 0.01
|
||
|
||
for inv_item in inventory_items:
|
||
await db.delete(inv_item)
|
||
|
||
received_item_data = get_item(result.received_item.id)
|
||
|
||
new_inventory_item = InventoryItem(
|
||
user_id=user.id,
|
||
item_id=result.received_item.id,
|
||
market_hash_name=result.received_item.market_hash_name,
|
||
rarity=result.received_item.rarity,
|
||
wear=received_item_data.get("wear", "Unknown"),
|
||
float_value=result.received_float,
|
||
type=received_item_data.get("type", "Normal"),
|
||
image_url=received_item_data.get("image_url", "") if received_item_data else "",
|
||
price_rub=result.received_item.price_rub or (received_item_data.get("price_rub", 0) if received_item_data else 0),
|
||
obtained_from="contract"
|
||
)
|
||
db.add(new_inventory_item)
|
||
|
||
contract = Contract(
|
||
user_id=user.id,
|
||
input_item_ids=json.dumps(ids),
|
||
output_item_id=result.received_item.id,
|
||
output_item_name=result.received_item.market_hash_name,
|
||
output_float=result.received_float,
|
||
probability=result.probability
|
||
)
|
||
db.add(contract)
|
||
|
||
await db.commit()
|
||
|
||
unlocked_ach = await check_contracts_achievements(db, user.id)
|
||
unlocked_ach += await check_inventory_achievements(db, user.id)
|
||
unlocked_ach += await check_balance_achievements(db, user.id)
|
||
unlocked_ach += await check_dildo_achievements(db, user.id)
|
||
unlocked_ach += await check_dildo_contract_achievement(db, user.id)
|
||
unlocked_ach += await check_collection_achievements(db, user.id)
|
||
unlocked_ach += await check_achievements_count_achievements(db, user.id)
|
||
|
||
act = record_activity_own_session(user.id, user.username, "contract",
|
||
f"🔄 выполнил(а) контракт и получил(а) {result.received_item.market_hash_name}",
|
||
{"item": result.received_item.market_hash_name, "rarity": result.received_item.rarity, "item_image": received_item_data.get("image_url", "")})
|
||
try:
|
||
asyncio.ensure_future(manager.broadcast({
|
||
"type": "activity",
|
||
"activity": act
|
||
}))
|
||
except:
|
||
pass
|
||
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "inventory_update"}))
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "balance_update", "balance": user.balance}))
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"received_item": {
|
||
"id": result.received_item.id,
|
||
"name": result.received_item.market_hash_name,
|
||
"rarity": result.received_item.rarity,
|
||
"float": result.received_float,
|
||
"probability": result.probability,
|
||
"image_url": received_item_data.get("image_url", "")
|
||
},
|
||
"achievements_unlocked": unlocked_ach if unlocked_ach else None
|
||
})
|
||
|
||
@app.get("/web/api/inventory/items")
|
||
async def api_get_inventory(
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Получение предметов инвентаря"""
|
||
|
||
result = await db.execute(
|
||
select(InventoryItem).filter(
|
||
InventoryItem.user_id == user.id
|
||
).order_by(desc(InventoryItem.obtained_at))
|
||
)
|
||
items = result.scalars().all()
|
||
|
||
result = []
|
||
for item in items:
|
||
price = item.price_rub
|
||
if not price:
|
||
item_data = get_item(item.item_id)
|
||
if item_data:
|
||
price = item_data.get("price_rub", 0) or 0
|
||
result.append({
|
||
"id": item.id,
|
||
"item_id": item.item_id,
|
||
"name": item.market_hash_name,
|
||
"rarity": item.rarity,
|
||
"wear": item.wear,
|
||
"float": item.float_value,
|
||
"type": item.type,
|
||
"price_rub": price,
|
||
"image_url": item.image_url or "",
|
||
"obtained_from": item.obtained_from,
|
||
"obtained_at": item.obtained_at.isoformat() if item.obtained_at else None
|
||
})
|
||
|
||
return result
|
||
|
||
|
||
@app.get("/web/api/item/{item_id}/image")
|
||
async def get_item_image(
|
||
item_id: int,
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Получение URL картинки предмета"""
|
||
item = get_item(item_id)
|
||
if not item:
|
||
return JSONResponse(
|
||
status_code=404,
|
||
content={"error": "Item not found"}
|
||
)
|
||
|
||
image_url = item.get("image_url", "")
|
||
return {
|
||
"item_id": item_id,
|
||
"image_url": image_url,
|
||
"has_image": bool(image_url)
|
||
}
|
||
|
||
|
||
@app.get("/web/api/inventory/item-detail/{inventory_id}")
|
||
async def api_item_detail(
|
||
inventory_id: int,
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Детальная информация о предмете с коллекцией"""
|
||
result = await db.execute(
|
||
select(InventoryItem).filter(
|
||
InventoryItem.id == inventory_id,
|
||
InventoryItem.user_id == user.id
|
||
)
|
||
)
|
||
inv_item = result.scalar_one_or_none()
|
||
if not inv_item:
|
||
# Also allow viewing if item exists (public profiles)
|
||
result = await db.execute(
|
||
select(InventoryItem).filter(
|
||
InventoryItem.id == inventory_id
|
||
)
|
||
)
|
||
inv_item = result.scalar_one_or_none()
|
||
if not inv_item:
|
||
return JSONResponse(status_code=404, content={"error": "Предмет не найден"})
|
||
|
||
item_data = get_item(inv_item.item_id)
|
||
if not item_data:
|
||
return JSONResponse(status_code=404, content={"error": "Данные предмета не найдены"})
|
||
|
||
collection = item_data.get("collection", "").strip()
|
||
# fallback to case name
|
||
if not collection or collection == "Без коллекции":
|
||
crates = item_data.get("crates_names", [])
|
||
if crates:
|
||
collection = f"Case: {crates[0]}"
|
||
|
||
item_resp = {
|
||
"id": inv_item.id,
|
||
"item_id": inv_item.item_id,
|
||
"name": inv_item.market_hash_name,
|
||
"rarity": inv_item.rarity,
|
||
"wear": inv_item.wear,
|
||
"float": inv_item.float_value,
|
||
"type": inv_item.type,
|
||
"price_rub": inv_item.price_rub or item_data.get("price_rub", 100),
|
||
"image_url": inv_item.image_url or item_data.get("image_url", ""),
|
||
"collection": collection,
|
||
"weapon": item_data.get("weapon", ""),
|
||
"pattern": item_data.get("pattern", ""),
|
||
}
|
||
|
||
# Собираем все предметы коллекции
|
||
coll_items = []
|
||
all_owned = True
|
||
owned_names = set()
|
||
|
||
# Получаем все item_id из коллекции
|
||
from backend import COLLECTIONS_INDEX, extract_base_name, RARITY_ORDER
|
||
collection_ids = COLLECTIONS_INDEX.get(collection, [])
|
||
|
||
# Группируем по базовому имени (без учёта вариантов)
|
||
seen_base = {}
|
||
for cid in collection_ids:
|
||
citem = get_item(cid)
|
||
if not citem:
|
||
continue
|
||
base = extract_base_name(citem.get("market_hash_name", ""))
|
||
if base and base not in seen_base:
|
||
seen_base[base] = citem
|
||
|
||
# Все предметы пользователя — для проверки владения
|
||
user_items_result = await db.execute(
|
||
select(InventoryItem.item_id).filter(
|
||
InventoryItem.user_id == user.id
|
||
)
|
||
)
|
||
user_item_ids = {row[0] for row in user_items_result.all()}
|
||
|
||
# Сортируем по редкости (низшая → высшая)
|
||
sorted_bases = sorted(seen_base.values(), key=lambda x: RARITY_ORDER.get(x.get("rarity", "Unknown"), -1))
|
||
|
||
for citem in sorted_bases:
|
||
base = extract_base_name(citem.get("market_hash_name", ""))
|
||
owned = False
|
||
for u_id in user_item_ids:
|
||
u_item = get_item(u_id)
|
||
if u_item and extract_base_name(u_item.get("market_hash_name", "")) == base:
|
||
owned = True
|
||
owned_names.add(base)
|
||
break
|
||
|
||
if not owned:
|
||
all_owned = False
|
||
|
||
coll_items.append({
|
||
"base_name": base,
|
||
"market_hash_name": citem.get("market_hash_name", ""),
|
||
"weapon": citem.get("weapon", ""),
|
||
"pattern": citem.get("pattern", ""),
|
||
"rarity": citem.get("rarity", ""),
|
||
"rarity_level": RARITY_ORDER.get(citem.get("rarity", "Unknown"), -1),
|
||
"owned": owned,
|
||
"image_url": citem.get("image_url", ""),
|
||
})
|
||
|
||
return {
|
||
"item": item_resp,
|
||
"collection_name": collection if collection else "",
|
||
"items": coll_items,
|
||
"all_owned": all_owned,
|
||
}
|
||
|
||
|
||
def get_case_items(case_name: str) -> List[dict]:
|
||
"""Получает предметы кейса по его имени"""
|
||
items = []
|
||
|
||
# Сначала проверяем cases.json
|
||
case_config = CASES_CONFIG.get(case_name)
|
||
if case_config and case_config.get('items'):
|
||
# Загружаем предметы по ID из конфига
|
||
for entry in case_config.get('items', []):
|
||
if isinstance(entry, dict):
|
||
item_id = entry.get('id') or entry.get('_id', 0)
|
||
else:
|
||
item_id = entry
|
||
item = get_item(int(item_id)) if item_id else None
|
||
if item:
|
||
items.append(item)
|
||
print(f"[DEBUG] Загружено {len(items)} предметов из cases.json для кейса {case_name}")
|
||
|
||
# Если предметов нет, ищем через COLLECTIONS_INDEX (старый метод)
|
||
if not items:
|
||
search_name = f"Case: {case_name}"
|
||
if search_name in COLLECTIONS_INDEX:
|
||
for idx in COLLECTIONS_INDEX[search_name]:
|
||
item = get_item(idx)
|
||
if item:
|
||
items.append(item)
|
||
print(f"[DEBUG] Загружено {len(items)} предметов из COLLECTIONS_INDEX для кейса {case_name}")
|
||
|
||
return items
|
||
|
||
|
||
_GROUPED_CACHE = {}
|
||
_GROUPED_CACHE_TTL = 300
|
||
|
||
def get_case_items_grouped(case_name: str) -> List[dict]:
|
||
"""Получает сгруппированные предметы кейса (по базовому имени), с кэшем"""
|
||
|
||
now = datetime.utcnow().timestamp()
|
||
cached = _GROUPED_CACHE.get(case_name)
|
||
if cached and (now - cached["ts"]) < _GROUPED_CACHE_TTL:
|
||
return cached["data"]
|
||
|
||
all_items = get_case_items(case_name)
|
||
if not all_items:
|
||
return []
|
||
|
||
grouped = {}
|
||
for item in all_items:
|
||
name = item.get("market_hash_name", "")
|
||
|
||
base_name = name
|
||
for prefix in ["StatTrak™ ", "Souvenir "]:
|
||
if base_name.startswith(prefix):
|
||
base_name = base_name[len(prefix):]
|
||
|
||
if " (" in base_name:
|
||
base_name = base_name.split(" (")[0]
|
||
|
||
item_type = item.get("type", "Normal")
|
||
rarity = item.get("rarity", "Unknown")
|
||
group_key = f"{base_name}|{item_type}|{rarity}"
|
||
|
||
if group_key in grouped:
|
||
continue
|
||
|
||
variants = []
|
||
min_price = float('inf')
|
||
max_price = 0
|
||
|
||
for variant in all_items:
|
||
variant_name = variant.get("market_hash_name", "")
|
||
variant_base = variant_name
|
||
for prefix in ["StatTrak™ ", "Souvenir "]:
|
||
if variant_base.startswith(prefix):
|
||
variant_base = variant_base[len(prefix):]
|
||
if " (" in variant_base:
|
||
variant_base = variant_base.split(" (")[0]
|
||
|
||
if variant_base == base_name and variant.get("type") == item_type:
|
||
variants.append(variant)
|
||
price = variant.get("price_rub", 0)
|
||
if price and price < min_price:
|
||
min_price = price
|
||
if price and price > max_price:
|
||
max_price = price
|
||
|
||
float_values = [v.get("mid_float_used", 0) for v in variants if v.get("mid_float_used")]
|
||
min_float = min(float_values) if float_values else 0.01
|
||
max_float = max(float_values) if float_values else 0.45
|
||
|
||
grouped[group_key] = {
|
||
"base_name": base_name,
|
||
"display_name": base_name,
|
||
"type": item_type,
|
||
"rarity": rarity,
|
||
"variants": variants,
|
||
"variants_count": len(variants),
|
||
"min_price": min_price if min_price != float('inf') else 0,
|
||
"max_price": max_price,
|
||
"min_float": min_float,
|
||
"max_float": max_float,
|
||
"image_url": item.get("image_url", ""),
|
||
"wear_range": f"{min_float:.2f} - {max_float:.2f}"
|
||
}
|
||
|
||
result = list(grouped.values())
|
||
_GROUPED_CACHE[case_name] = {"data": result, "ts": now}
|
||
return result
|
||
|
||
|
||
def open_case_with_variants(case_name: str) -> dict:
|
||
"""Открывает кейс и возвращает предмет с случайным float"""
|
||
|
||
grouped_items = get_case_items_grouped(case_name)
|
||
if not grouped_items:
|
||
return None
|
||
|
||
# Взвешенный выбор по редкости (шанс на группу)
|
||
items_with_weights = []
|
||
for group in grouped_items:
|
||
rarity = group["rarity"]
|
||
weight = CASE_RARITY_WEIGHTS.get(rarity, 100)
|
||
items_with_weights.append((group, weight))
|
||
|
||
total_weight = sum(w for _, w in items_with_weights)
|
||
rand_val = random.uniform(0, total_weight)
|
||
|
||
cumsum = 0
|
||
chosen_group = None
|
||
for group, weight in items_with_weights:
|
||
cumsum += weight
|
||
if rand_val <= cumsum:
|
||
chosen_group = group
|
||
break
|
||
|
||
if not chosen_group:
|
||
chosen_group = items_with_weights[-1][0]
|
||
|
||
# Выбираем случайный вариант из группы (или генерируем новый)
|
||
variants = chosen_group["variants"]
|
||
|
||
if variants:
|
||
# Выбираем случайный вариант как основу
|
||
base_item = random.choice(variants)
|
||
else:
|
||
return None
|
||
|
||
# Генерируем случайный float в диапазоне
|
||
min_float = chosen_group["min_float"]
|
||
max_float = chosen_group["max_float"]
|
||
item_float = round(random.uniform(min_float, max_float), 6)
|
||
|
||
# Определяем wear по float
|
||
wear = get_wear_from_float(item_float)
|
||
|
||
# Создаем итоговый предмет
|
||
result_item = base_item.copy()
|
||
result_item["float_value"] = item_float
|
||
result_item["wear"] = wear
|
||
result_item["mid_float_used"] = item_float
|
||
|
||
return {
|
||
"id": base_item.get("_id", 0),
|
||
"item": result_item,
|
||
"float": item_float,
|
||
"group": chosen_group
|
||
}
|
||
|
||
def get_wear_from_float(float_value: float) -> str:
|
||
"""Определяет качество по float"""
|
||
if float_value < 0.07:
|
||
return "Factory New"
|
||
elif float_value < 0.15:
|
||
return "Minimal Wear"
|
||
elif float_value < 0.38:
|
||
return "Field-Tested"
|
||
elif float_value < 0.45:
|
||
return "Well-Worn"
|
||
else:
|
||
return "Battle-Scarred"
|
||
|
||
|
||
@app.get("/web/api/user/balance")
|
||
async def get_user_balance(
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Получение текущего баланса пользователя"""
|
||
return JSONResponse({
|
||
"success": True,
|
||
"balance": user.balance
|
||
})
|
||
|
||
|
||
@app.post("/web/api/cases/open/animated")
|
||
async def api_open_case_animated(
|
||
case_name: str = Form(...),
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Открытие кейса с анимацией (возвращает результат)"""
|
||
|
||
case_config = CASES_CONFIG.get(case_name, {})
|
||
price_per_case = case_config.get('price_open', 250)
|
||
|
||
if user.balance < price_per_case:
|
||
return JSONResponse(status_code=400, content={
|
||
"error": f"Недостаточно средств. Нужно {price_per_case:.2f} ₽, у вас {user.balance:.2f} ₽"
|
||
})
|
||
|
||
adjusted_weights = await get_adjusted_weights(db, user.id)
|
||
|
||
result = open_case_with_rpu(case_name, adjusted_weights)
|
||
if not result:
|
||
return JSONResponse(
|
||
status_code=400,
|
||
content={"error": "Case not found or empty"}
|
||
)
|
||
|
||
item_data = result["item"]
|
||
item_float = result["float"]
|
||
|
||
# Добавляем предмет в инвентарь
|
||
inventory_item = InventoryItem(
|
||
user_id=user.id,
|
||
item_id=result["id"],
|
||
market_hash_name=item_data.get("market_hash_name", "Unknown"),
|
||
rarity=item_data.get("rarity", "Unknown"),
|
||
wear=item_data.get("wear", "Unknown"),
|
||
float_value=item_float,
|
||
type=item_data.get("type", "Normal"),
|
||
image_url=item_data.get("image_url", ""),
|
||
price_rub=item_data.get("price_rub", 0),
|
||
obtained_from=f"case: {case_name}"
|
||
)
|
||
db.add(inventory_item)
|
||
|
||
# Записываем открытие
|
||
opening = CaseOpening(
|
||
user_id=user.id,
|
||
case_name=case_name,
|
||
item_id=result["id"],
|
||
item_name=item_data.get("market_hash_name", "Unknown"),
|
||
rarity=item_data.get("rarity", "Unknown"),
|
||
float_value=item_float
|
||
)
|
||
db.add(opening)
|
||
|
||
user.balance -= price_per_case
|
||
|
||
# Обновляем РПУ
|
||
await update_rpu_stats(db, user.id, price_per_case, 1,
|
||
item_data.get("price_rub", 0),
|
||
rarity=item_data.get("rarity"),
|
||
is_rare=item_data.get("rarity") in ("Covert", "Rare Special Item", "Extraordinary"))
|
||
|
||
await db.commit()
|
||
|
||
# Проверка достижений
|
||
unlocked_ach = []
|
||
unlocked_ach += await check_open_cases_achievements(db, user.id)
|
||
user_rpu = await get_user_rpu(db, user.id)
|
||
unlocked_ach += await check_spent_achievements(db, user.id, user_rpu.total_spent)
|
||
unlocked_ach += await check_rare_item_achievement(db, user.id, item_data.get("rarity", ""))
|
||
item_full = get_item(result["id"])
|
||
if item_full:
|
||
unlocked_ach += await check_knife_achievement(db, user.id, item_full)
|
||
unlocked_ach += await check_inventory_achievements(db, user.id)
|
||
unlocked_ach += await check_dildo_achievements(db, user.id)
|
||
unlocked_ach += await check_items_unboxed_achievements(db, user.id)
|
||
unlocked_ach += await check_rare_item_count_achievements(db, user.id)
|
||
unlocked_ach += await check_knife_count_achievements(db, user.id)
|
||
unlocked_ach += await check_balance_achievements(db, user.id)
|
||
unlocked_ach += await check_achievements_count_achievements(db, user.id)
|
||
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "inventory_update"}))
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "balance_update", "balance": user.balance}))
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"item": {
|
||
"id": result["id"],
|
||
"name": item_data.get("market_hash_name"),
|
||
"rarity": item_data.get("rarity"),
|
||
"float": item_float,
|
||
"price": item_data.get("price_rub", 100),
|
||
"image_url": item_data.get("image_url", ""),
|
||
"wear": item_data.get("wear", "Unknown")
|
||
},
|
||
"achievements_unlocked": unlocked_ach if unlocked_ach else None
|
||
})
|
||
|
||
@app.get("/static/placeholder.png")
|
||
async def get_placeholder():
|
||
"""Возвращает placeholder изображение"""
|
||
placeholder_path = Path("static/placeholder.png")
|
||
if placeholder_path.exists():
|
||
return FileResponse(placeholder_path)
|
||
|
||
# Возвращаем SVG если файла нет
|
||
from fastapi.responses import Response
|
||
svg = '''<svg xmlns="http://www.w3.org/2000/svg" width="200" height="150" viewBox="0 0 200 150">
|
||
<rect width="200" height="150" fill="#334155"/>
|
||
<rect x="10" y="10" width="180" height="130" fill="#1e293b" rx="8"/>
|
||
<text x="100" y="80" text-anchor="middle" fill="#94a3b8" font-size="16" font-family="Arial">🎁</text>
|
||
<text x="100" y="105" text-anchor="middle" fill="#64748b" font-size="12" font-family="Arial">No image</text>
|
||
</svg>'''
|
||
return Response(content=svg, media_type="image/svg+xml")
|
||
|
||
|
||
@app.get("/case/{case_name}", response_class=HTMLResponse)
|
||
async def case_detail_page(
|
||
case_name: str,
|
||
request: Request,
|
||
user: Optional[User] = Depends(get_current_user_optional)
|
||
):
|
||
"""Страница конкретного кейса с группировкой скинов"""
|
||
|
||
case_config = CASES_CONFIG.get(case_name, {})
|
||
|
||
# Получаем сгруппированные предметы
|
||
grouped_items = get_case_items_grouped(case_name)
|
||
|
||
if not grouped_items:
|
||
raise HTTPException(404, detail="Кейс не найден или пуст")
|
||
|
||
# Группируем по редкости для отображения
|
||
items_by_rarity = {}
|
||
rarities_count = {}
|
||
total_weight = 0
|
||
|
||
for group in grouped_items:
|
||
rarity = group["rarity"]
|
||
weight = CASE_RARITY_WEIGHTS.get(rarity, 100)
|
||
total_weight += weight
|
||
|
||
if rarity not in items_by_rarity:
|
||
items_by_rarity[rarity] = []
|
||
|
||
item_data = {
|
||
"name": group["display_name"],
|
||
"rarity": rarity,
|
||
"type": group["type"],
|
||
"image_url": group["image_url"],
|
||
"min_price": group["min_price"],
|
||
"max_price": group["max_price"],
|
||
"min_float": group["min_float"],
|
||
"max_float": group["max_float"],
|
||
"wear_range": group["wear_range"],
|
||
"variants_count": group["variants_count"],
|
||
"weight": weight,
|
||
"probability": 0
|
||
}
|
||
items_by_rarity[rarity].append(item_data)
|
||
rarities_count[rarity] = rarities_count.get(rarity, 0) + 1
|
||
|
||
# Считаем вероятности
|
||
for rarity in items_by_rarity:
|
||
for item in items_by_rarity[rarity]:
|
||
item["probability"] = (item["weight"] / total_weight) * 100
|
||
|
||
sorted_rarities = sorted(items_by_rarity.keys(),
|
||
key=lambda x: RARITY_ORDER.get(x, 0))
|
||
|
||
# Для анимации - список всех возможных групп
|
||
case_items_json = []
|
||
for rarity in items_by_rarity:
|
||
for item in items_by_rarity[rarity]:
|
||
case_items_json.append({
|
||
"name": item["name"],
|
||
"rarity": item["rarity"],
|
||
"image_url": item["image_url"]
|
||
})
|
||
|
||
return templates.TemplateResponse(request, "case_detail.html", {
|
||
"user": user,
|
||
"case_name": case_name,
|
||
"case_display_name": case_config.get('display_name', case_name),
|
||
"case_price": case_config.get('price_open', 250),
|
||
"case_image": case_config.get('image_url', ''),
|
||
"case_description": case_config.get('description', ''),
|
||
"total_items": len(grouped_items),
|
||
"items_by_rarity": items_by_rarity,
|
||
"sorted_rarities": sorted_rarities,
|
||
"rarities_count": rarities_count,
|
||
"case_items": case_items_json,
|
||
"active_page": "cases"
|
||
})
|
||
|
||
|
||
def open_case_with_variants_rpu(case_name: str, adjusted_weights: dict = None) -> dict:
|
||
"""Открывает кейс с вариантами и учетом РПУ"""
|
||
|
||
grouped_items = get_case_items_grouped(case_name)
|
||
if not grouped_items:
|
||
return None
|
||
|
||
items_with_weights = []
|
||
for group in grouped_items:
|
||
rarity = group["rarity"]
|
||
if adjusted_weights:
|
||
weight = adjusted_weights.get(rarity, CASE_RARITY_WEIGHTS.get(rarity, 100))
|
||
else:
|
||
weight = CASE_RARITY_WEIGHTS.get(rarity, 100)
|
||
items_with_weights.append((group, weight))
|
||
|
||
total_weight = sum(w for _, w in items_with_weights)
|
||
rand_val = random.uniform(0, total_weight)
|
||
|
||
cumsum = 0
|
||
chosen_group = None
|
||
for group, weight in items_with_weights:
|
||
cumsum += weight
|
||
if rand_val <= cumsum:
|
||
chosen_group = group
|
||
break
|
||
|
||
if not chosen_group:
|
||
chosen_group = items_with_weights[-1][0]
|
||
|
||
variants = chosen_group["variants"]
|
||
|
||
if variants:
|
||
base_item = random.choice(variants)
|
||
else:
|
||
return None
|
||
|
||
# Используем mid_float_used если есть (для под-вариантов)
|
||
item_float = base_item.get("mid_float_used")
|
||
if item_float is None:
|
||
min_float = chosen_group["min_float"]
|
||
max_float = chosen_group["max_float"]
|
||
item_float = round(random.uniform(min_float, max_float), 6)
|
||
|
||
wear = get_wear_from_float(item_float)
|
||
|
||
# Находим реальный ID предмета через NAME_LOWER_TO_IDS (O(1) вместо O(n))
|
||
real_item_id = base_item.get("_id")
|
||
if real_item_id is None:
|
||
name_key = base_item.get("market_hash_name", "").lower().strip()
|
||
id_list = NAME_LOWER_TO_IDS.get(name_key)
|
||
real_item_id = id_list[0] if id_list else 0
|
||
|
||
# Получаем правильный image_url
|
||
real_image_url = base_item.get("image_url", "")
|
||
if not real_image_url:
|
||
it = get_item(int(real_item_id)) if real_item_id else None
|
||
if it:
|
||
real_image_url = it.get("image_url", "")
|
||
|
||
result_item = base_item.copy()
|
||
result_item["float_value"] = item_float
|
||
result_item["wear"] = wear
|
||
result_item["mid_float_used"] = item_float
|
||
result_item["image_url"] = real_image_url
|
||
|
||
return {
|
||
"id": real_item_id,
|
||
"item": result_item,
|
||
"float": item_float,
|
||
"group": chosen_group
|
||
}
|
||
|
||
|
||
def pick_slot_reel(grouped_items: list, adjusted_weights: dict, exclude_groups: set = None) -> tuple:
|
||
"""Выбирает один предмет для барабана слот-машины"""
|
||
items_with_weights = []
|
||
for group in grouped_items:
|
||
if exclude_groups and group["base_name"] in exclude_groups:
|
||
continue
|
||
rarity = group["rarity"]
|
||
weight = adjusted_weights.get(rarity, CASE_RARITY_WEIGHTS.get(rarity, 100))
|
||
items_with_weights.append((group, weight))
|
||
|
||
if not items_with_weights:
|
||
return None, None
|
||
|
||
total_weight = sum(w for _, w in items_with_weights)
|
||
rand_val = random.uniform(0, total_weight)
|
||
|
||
cumsum = 0
|
||
chosen_group = None
|
||
for group, weight in items_with_weights:
|
||
cumsum += weight
|
||
if rand_val <= cumsum:
|
||
chosen_group = group
|
||
break
|
||
|
||
if not chosen_group:
|
||
chosen_group = items_with_weights[-1][0]
|
||
|
||
variants = chosen_group["variants"]
|
||
base_item = random.choice(variants) if variants else None
|
||
return chosen_group, base_item
|
||
|
||
|
||
@app.post("/web/api/cases/open/slot")
|
||
async def api_open_slot_case(
|
||
case_name: str = Form(...),
|
||
free_spin: str = Form(""),
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Слот-машина: 3 барабана, совпадение = приз"""
|
||
|
||
case_config = CASES_CONFIG.get(case_name, {})
|
||
price_per_case = case_config.get('price_open', 250)
|
||
|
||
is_free = free_spin == "1"
|
||
|
||
if not is_free and user.balance < price_per_case:
|
||
return JSONResponse(status_code=400, content={
|
||
"error": f"Недостаточно средств. Нужно {price_per_case:.2f} ₽"
|
||
})
|
||
|
||
grouped_items = get_case_items_grouped(case_name)
|
||
if not grouped_items or len(grouped_items) < 2:
|
||
return JSONResponse(status_code=400, content={"error": "В кейсе недостаточно предметов"})
|
||
|
||
adjusted_weights = await get_adjusted_weights(db, user.id)
|
||
|
||
# Буст для слотов: чаще выпадают активки
|
||
SLOT_RARITY_BOOST = {
|
||
"Covert": 8,
|
||
"Classified": 4,
|
||
"Restricted": 2,
|
||
"Rare Special Item": 12,
|
||
"Extraordinary": 10
|
||
}
|
||
for rarity, boost in SLOT_RARITY_BOOST.items():
|
||
if rarity in adjusted_weights:
|
||
adjusted_weights[rarity] *= boost
|
||
|
||
# Баним: принудительно разные группы на всех барабанах
|
||
if user.is_banned:
|
||
group1, item1 = pick_slot_reel(grouped_items, adjusted_weights)
|
||
group2, item2 = pick_slot_reel(grouped_items, adjusted_weights, {group1["base_name"]})
|
||
group3, item3 = pick_slot_reel(grouped_items, adjusted_weights, {group1["base_name"], group2["base_name"]})
|
||
if not item3:
|
||
group3, item3 = pick_slot_reel(grouped_items, adjusted_weights, {group1["base_name"]})
|
||
else:
|
||
group1, item1 = pick_slot_reel(grouped_items, adjusted_weights)
|
||
group2, item2 = pick_slot_reel(grouped_items, adjusted_weights)
|
||
group3, item3 = pick_slot_reel(grouped_items, adjusted_weights)
|
||
|
||
def resolve_reel(group, item):
|
||
if not item or not group:
|
||
return None
|
||
name_key = item.get("market_hash_name", "").lower().strip()
|
||
id_list = NAME_LOWER_TO_IDS.get(name_key)
|
||
real_id = id_list[0] if id_list else None
|
||
return {
|
||
"id": real_id or 0,
|
||
"name": item.get("market_hash_name", "???"),
|
||
"image_url": item.get("image_url", ""),
|
||
"rarity": group["rarity"],
|
||
"base_name": group["base_name"]
|
||
}
|
||
|
||
reels = [resolve_reel(group1, item1), resolve_reel(group2, item2), resolve_reel(group3, item3)]
|
||
reels = [r for r in reels if r]
|
||
|
||
# Assign activations to each reel's target item
|
||
ACTIVATION_WEIGHTS = [
|
||
("extra_spin", 0.10),
|
||
("double", 0.04),
|
||
("guaranteed", 0.07),
|
||
("free_spin", 0.07),
|
||
("quintuple", 0.02),
|
||
]
|
||
def pick_activation():
|
||
r = random.random()
|
||
cum = 0
|
||
for name, w in ACTIVATION_WEIGHTS:
|
||
cum += w
|
||
if r < cum:
|
||
return name
|
||
return ""
|
||
|
||
reel_activations = [pick_activation() for _ in range(3)]
|
||
activations_str = ', '.join(a if a else 'none' for a in reel_activations)
|
||
print(f"[SLOT] activations: {activations_str}")
|
||
|
||
match = False
|
||
prize_data = None
|
||
bonus_data = None
|
||
prize_price = 0
|
||
bonus_price = 0
|
||
multiplier = 1 # 1 = normal, 2 = 2x, 5 = 5x
|
||
multiplied_prize = None
|
||
|
||
if len(reels) == 3:
|
||
base_names = [r["base_name"] for r in reels]
|
||
match = len(set(base_names)) == 1
|
||
|
||
def make_item(item_data, item_id, base_name, wear, item_float, source):
|
||
display_name = f"{base_name} ({wear})"
|
||
return InventoryItem(
|
||
user_id=user.id, item_id=item_id,
|
||
market_hash_name=display_name,
|
||
rarity=item_data.get("rarity", "Unknown"),
|
||
wear=wear, float_value=item_float,
|
||
type=item_data.get("type", "Normal"),
|
||
image_url=item_data.get("image_url", ""),
|
||
price_rub=item_data.get("price_rub", 0),
|
||
obtained_from=source
|
||
)
|
||
|
||
def generate_wear(item):
|
||
min_f = item.get("min_float", item.get("wear_range", [0, 1])[0])
|
||
max_f = item.get("max_float", item.get("wear_range", [0, 1])[1])
|
||
f = round(random.uniform(min_f, max_f), 6)
|
||
return f, get_wear_from_float(f)
|
||
|
||
if match:
|
||
for act in reel_activations:
|
||
if act == "quintuple":
|
||
multiplier = 5
|
||
break
|
||
elif act == "double":
|
||
multiplier = 2
|
||
break
|
||
|
||
matched_name = reels[0]["name"]
|
||
prize_item = None
|
||
for g in grouped_items:
|
||
if g["base_name"] == reels[0]["base_name"]:
|
||
variants = g["variants"]
|
||
prize_item = random.choice(variants) if variants else None
|
||
break
|
||
|
||
if prize_item:
|
||
prize_float, prize_wear = generate_wear(prize_item)
|
||
prize_id = item_id_by_name(prize_item.get("market_hash_name"))
|
||
prize_price = prize_item.get("price_rub", 0)
|
||
|
||
target_min_price = price_per_case * 1.5
|
||
candidates = []
|
||
for g in grouped_items:
|
||
for v in g["variants"]:
|
||
vp = v.get("price_rub", 0)
|
||
if vp >= target_min_price:
|
||
candidates.append((vp, v, g))
|
||
|
||
if candidates:
|
||
candidates.sort(key=lambda x: x[0])
|
||
bonus_price, bonus_variant, bonus_group = candidates[0]
|
||
bonus_float, bonus_wear = generate_wear(bonus_variant)
|
||
bonus_id = item_id_by_name(bonus_variant.get("market_hash_name"))
|
||
|
||
bonus_data = {
|
||
"id": bonus_id or bonus_variant.get("_id", 0),
|
||
"name": bonus_variant.get("market_hash_name", "???"),
|
||
"image_url": bonus_variant.get("image_url", ""),
|
||
"rarity": bonus_group["rarity"],
|
||
"price": bonus_price,
|
||
"float": bonus_float,
|
||
"wear": bonus_wear
|
||
}
|
||
|
||
if multiplier > 1:
|
||
total_value = (prize_price + bonus_price) * multiplier
|
||
best_item = None
|
||
best_diff = float('inf')
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
ip = it.get("price_rub", 0)
|
||
if ip <= 0:
|
||
continue
|
||
diff = abs(ip - total_value)
|
||
if diff < best_diff:
|
||
best_diff = diff
|
||
best_item = it
|
||
|
||
if best_item:
|
||
mult_float, mult_wear = generate_wear(best_item)
|
||
multiplied_id = item_id_by_name(best_item.get("market_hash_name"))
|
||
multiplied_prize = {
|
||
"id": multiplied_id or best_item.get("_id", 0),
|
||
"name": best_item.get("market_hash_name", "???"),
|
||
"image_url": best_item.get("image_url", ""),
|
||
"rarity": best_item.get("rarity", "Unknown"),
|
||
"price": best_item.get("price_rub", 0),
|
||
"float": mult_float,
|
||
"wear": mult_wear
|
||
}
|
||
|
||
prize_data = {
|
||
"id": prize_id or prize_item.get("_id", 0),
|
||
"name": prize_item.get("market_hash_name", "???"),
|
||
"image_url": prize_item.get("image_url", ""),
|
||
"rarity": prize_item.get("rarity", "Unknown"),
|
||
"price": prize_price,
|
||
"float": prize_float,
|
||
"wear": prize_wear
|
||
}
|
||
|
||
db.add(make_item(prize_item, prize_data["id"], reels[0]["base_name"],
|
||
prize_data["wear"], prize_data["float"], f"slot: {case_name}"))
|
||
db.add(CaseOpening(
|
||
user_id=user.id, case_name=case_name,
|
||
item_id=prize_data["id"],
|
||
item_name=prize_data["name"],
|
||
rarity=prize_data["rarity"],
|
||
float_value=prize_data["float"]
|
||
))
|
||
|
||
if bonus_data:
|
||
bonus_item_data = {"rarity": bonus_data["rarity"], "type": "Normal"}
|
||
db.add(make_item(bonus_item_data, bonus_data["id"],
|
||
bonus_data["name"].rsplit(" (", 1)[0] if " (" in bonus_data["name"] else bonus_data["name"],
|
||
bonus_data["wear"], bonus_data["float"], f"slot_bonus: {case_name}"))
|
||
db.add(CaseOpening(
|
||
user_id=user.id, case_name=case_name,
|
||
item_id=bonus_data["id"],
|
||
item_name=bonus_data["name"],
|
||
rarity=bonus_data["rarity"],
|
||
float_value=bonus_data["float"]
|
||
))
|
||
|
||
if multiplied_prize:
|
||
mult_item_data = {"rarity": multiplied_prize["rarity"], "type": "Normal"}
|
||
db.add(make_item(mult_item_data, multiplied_prize["id"],
|
||
multiplied_prize["name"].rsplit(" (", 1)[0] if " (" in multiplied_prize["name"] else multiplied_prize["name"],
|
||
multiplied_prize["wear"], multiplied_prize["float"],
|
||
f"slot_mult{multiplier}: {case_name}"))
|
||
db.add(CaseOpening(
|
||
user_id=user.id, case_name=case_name,
|
||
item_id=multiplied_prize["id"],
|
||
item_name=multiplied_prize["name"],
|
||
rarity=multiplied_prize["rarity"],
|
||
float_value=multiplied_prize["float"]
|
||
))
|
||
|
||
elif "guaranteed" in reel_activations:
|
||
guessed_group, guessed_item = pick_slot_reel(grouped_items, adjusted_weights)
|
||
if guessed_item and guessed_group:
|
||
g_price = guessed_item.get("price_rub", 0)
|
||
if g_price > 0:
|
||
g_float, g_wear = generate_wear(guessed_item)
|
||
prize_price = g_price
|
||
prize_data = {
|
||
"id": guessed_item.get("_id", 0),
|
||
"name": guessed_item.get("market_hash_name", "???"),
|
||
"image_url": guessed_item.get("image_url", ""),
|
||
"rarity": guessed_group["rarity"],
|
||
"price": prize_price,
|
||
"float": g_float,
|
||
"wear": g_wear
|
||
}
|
||
db.add(make_item(
|
||
{"rarity": guessed_group["rarity"], "type": "Normal"},
|
||
prize_data["id"],
|
||
guessed_group["base_name"],
|
||
prize_data["wear"], prize_data["float"],
|
||
f"slot_guaranteed: {case_name}"
|
||
))
|
||
db.add(CaseOpening(
|
||
user_id=user.id, case_name=case_name,
|
||
item_id=prize_data["id"],
|
||
item_name=prize_data["name"],
|
||
rarity=prize_data["rarity"],
|
||
float_value=prize_data["float"]
|
||
))
|
||
|
||
if not is_free:
|
||
user.balance -= price_per_case
|
||
await update_rpu_stats(db, user.id, price_per_case, 1)
|
||
|
||
# Проверка достижений для слота
|
||
unlocked_ach = []
|
||
if match:
|
||
unlocked_ach += await check_slot_match_achievements(db, user.id)
|
||
unlocked_ach += await check_slot_plays_achievements(db, user.id)
|
||
unlocked_ach += await check_slot_match_count_achievements(db, user.id)
|
||
unlocked_ach += await check_balance_achievements(db, user.id)
|
||
unlocked_ach += await check_achievements_count_achievements(db, user.id)
|
||
|
||
await db.commit()
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"achievements_unlocked": unlocked_ach if unlocked_ach else None,
|
||
"reels": reels,
|
||
"match": match,
|
||
"prize": prize_data,
|
||
"bonus": bonus_data,
|
||
"prize_value": prize_price,
|
||
"bonus_value": bonus_price,
|
||
"spent": 0 if is_free else price_per_case,
|
||
"activations": reel_activations,
|
||
"multiplier": multiplier if multiplier > 1 else 0,
|
||
"multiplied_prize": multiplied_prize
|
||
})
|
||
|
||
|
||
@app.post("/web/api/cases/open/multiple")
|
||
async def api_open_multiple_cases(
|
||
case_name: str = Form(...),
|
||
count: int = Form(...),
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Открытие нескольких кейсов с группировкой и РПУ"""
|
||
|
||
# Определяем максимальное количество в зависимости от типа кейса
|
||
is_slot = "slot" in case_name.lower() or "слот" in case_name.lower()
|
||
is_farm = "фарм" in case_name.lower() or "farm" in case_name.lower()
|
||
max_allowed = 1 if is_slot else (100 if is_farm else 10)
|
||
|
||
if count < 1 or count > max_allowed:
|
||
return JSONResponse(status_code=400, content={
|
||
"error": f"Количество должно быть от 1 до {max_allowed}"
|
||
})
|
||
|
||
case_config = CASES_CONFIG.get(case_name, {})
|
||
price_per_case = case_config.get('price_open', 250)
|
||
total_price = price_per_case * count
|
||
|
||
if user.balance < total_price:
|
||
return JSONResponse(status_code=400, content={
|
||
"error": f"Недостаточно средств. Нужно {total_price:.2f} ₽, у вас {user.balance:.2f} ₽"
|
||
})
|
||
|
||
# Подготовка для бана: ищем самый дешёвый предмет в кейсе
|
||
cheapest_case_item = None
|
||
if user.is_banned:
|
||
for group in get_case_items_grouped(case_name):
|
||
for v in group["variants"]:
|
||
v_price = v.get("price_rub", 0)
|
||
if cheapest_case_item is None or v_price < cheapest_case_item["price"]:
|
||
v_id = item_id_by_name(v.get("market_hash_name"))
|
||
cheapest_case_item = {
|
||
"id": v_id or v.get("_id", 0),
|
||
"item": v,
|
||
"price": v_price,
|
||
"base_name": group["base_name"],
|
||
"rarity": group["rarity"]
|
||
}
|
||
|
||
# Включаем batch-режим — все sub-функции делают flush вместо commit
|
||
import rpu as _rpu_mod
|
||
_rpu_mod._BATCH_MODE = True
|
||
try:
|
||
# Один раз считаем RPU на всю пачку (кэшируется внутри)
|
||
adjusted_weights = await get_adjusted_weights(db, user.id)
|
||
results = []
|
||
for _ in range(count):
|
||
result = open_case_with_variants_rpu(case_name, adjusted_weights)
|
||
if result:
|
||
item_data = result["item"]
|
||
item_float = result["float"]
|
||
item_id = result["id"]
|
||
base_name = result["group"]["base_name"]
|
||
|
||
# Бан: выдаём самый дешёвый предмет если выпал дороже 75% цены кейса
|
||
if user.is_banned and cheapest_case_item:
|
||
item_price = item_data.get("price_rub", 0)
|
||
max_allowed_price = price_per_case * 0.75
|
||
if item_price > max_allowed_price:
|
||
ci = cheapest_case_item
|
||
item_data = ci["item"]
|
||
item_id = ci["id"]
|
||
item_float = round(random.uniform(0.01, 0.45), 6)
|
||
base_name = ci["base_name"]
|
||
wear = "Field-Tested"
|
||
|
||
wear = item_data.get("wear", "Field-Tested")
|
||
display_name = f"{base_name} ({wear})"
|
||
if item_data.get("type") == "StatTrak":
|
||
display_name = f"StatTrak™ {display_name}"
|
||
elif item_data.get("type") == "Souvenir":
|
||
display_name = f"Souvenir {display_name}"
|
||
|
||
price = item_data.get("price_rub", 0) or 100
|
||
inventory_item = InventoryItem(
|
||
user_id=user.id,
|
||
item_id=item_id,
|
||
market_hash_name=display_name,
|
||
rarity=item_data.get("rarity", "Unknown"),
|
||
wear=wear,
|
||
float_value=item_float,
|
||
type=item_data.get("type", "Normal"),
|
||
image_url=item_data.get("image_url", ""),
|
||
price_rub=price,
|
||
obtained_from=f"case: {case_name}"
|
||
)
|
||
db.add(inventory_item)
|
||
|
||
opening = CaseOpening(
|
||
user_id=user.id,
|
||
case_name=case_name,
|
||
item_id=item_id,
|
||
item_name=display_name,
|
||
rarity=item_data.get("rarity", "Unknown"),
|
||
float_value=item_float
|
||
)
|
||
db.add(opening)
|
||
|
||
results.append({
|
||
"id": item_id,
|
||
"name": display_name,
|
||
"rarity": item_data.get("rarity"),
|
||
"float": item_float,
|
||
"image_url": item_data.get("image_url", ""),
|
||
"wear": wear,
|
||
"price": price
|
||
})
|
||
|
||
user.balance -= total_price
|
||
|
||
# Обновляем статистику РПУ
|
||
total_received = sum(r.get("price", 0) for r in results)
|
||
had_rare = any(r.get("rarity") in ("Covert", "Rare Special Item", "Extraordinary") for r in results)
|
||
await update_rpu_stats(db, user.id, total_price, count, total_received,
|
||
rarity=results[0]["rarity"] if results else None, is_rare=had_rare)
|
||
|
||
await db.commit()
|
||
finally:
|
||
_rpu_mod._BATCH_MODE = False
|
||
_rpu_mod._batch_cache_clear()
|
||
|
||
# Запись активности — каждый предмет отдельно
|
||
for r in results:
|
||
item_price = r.get("price", 0)
|
||
mult = round(item_price / max(1, price_per_case), 1) if item_price > 0 else 0
|
||
effect = f"drop:{mult}x" if mult >= 3 else None
|
||
act_data = {
|
||
"case": case_name, "item": r["name"], "rarity": r.get("rarity", ""),
|
||
"item_image": r.get("image_url", ""), "mult": mult
|
||
}
|
||
if effect:
|
||
act_data["effect"] = effect
|
||
act = record_activity_own_session(user.id, user.username, "case_open",
|
||
f"📦 открыл(а) кейс и получил(а) {r['name']}", act_data)
|
||
try:
|
||
asyncio.ensure_future(manager.broadcast({
|
||
"type": "activity",
|
||
"activity": act
|
||
}))
|
||
except:
|
||
pass
|
||
|
||
# Проверка достижений
|
||
unlocked_ach = []
|
||
unlocked_ach += await check_open_cases_achievements(db, user.id)
|
||
user_rpu = await get_user_rpu(db, user.id)
|
||
unlocked_ach += await check_spent_achievements(db, user.id, user_rpu.total_spent)
|
||
unlocked_ach += await check_inventory_achievements(db, user.id)
|
||
unlocked_ach += await check_dildo_achievements(db, user.id)
|
||
unlocked_ach += await check_items_unboxed_achievements(db, user.id)
|
||
unlocked_ach += await check_rare_item_count_achievements(db, user.id)
|
||
unlocked_ach += await check_knife_count_achievements(db, user.id)
|
||
unlocked_ach += await check_balance_achievements(db, user.id)
|
||
unlocked_ach += await check_achievements_count_achievements(db, user.id)
|
||
|
||
for r in results:
|
||
unlocked_ach += await check_rare_item_achievement(db, user.id, r.get("rarity", ""))
|
||
item_full = get_item(r.get("id", 0))
|
||
if item_full:
|
||
unlocked_ach += await check_knife_achievement(db, user.id, item_full)
|
||
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "inventory_update"}))
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "balance_update", "balance": user.balance}))
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"results": results,
|
||
"count": len(results),
|
||
"spent": total_price,
|
||
"new_balance": user.balance,
|
||
"achievements_unlocked": unlocked_ach if unlocked_ach else None
|
||
})
|
||
|
||
|
||
|
||
@app.post("/web/api/inventory/sell")
|
||
async def api_sell_item(
|
||
inventory_id: int = Form(...),
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Продажа предмета из инвентаря"""
|
||
|
||
result = await db.execute(
|
||
select(InventoryItem).filter(
|
||
InventoryItem.id == inventory_id,
|
||
InventoryItem.user_id == user.id
|
||
)
|
||
)
|
||
inv_item = result.scalar_one_or_none()
|
||
|
||
if not inv_item:
|
||
return JSONResponse(status_code=400, content={"error": "Предмет не найден"})
|
||
|
||
# Получаем цену предмета (используем сохранённую при получении, не из ALL_ITEMS)
|
||
price = inv_item.price_rub
|
||
if price <= 0:
|
||
item_data = get_item(inv_item.item_id)
|
||
if item_data:
|
||
price = item_data.get('price_rub', 0)
|
||
if price <= 0:
|
||
price = 100 # Минимальная цена если не указана
|
||
|
||
# Начисляем деньги
|
||
user.balance += price
|
||
|
||
# Удаляем предмет
|
||
await db.delete(inv_item)
|
||
await db.commit()
|
||
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "inventory_update"}))
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "balance_update", "balance": user.balance}))
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"sold_price": price,
|
||
"new_balance": user.balance,
|
||
"message": f"Предмет продан за {price:.2f} ₽"
|
||
})
|
||
|
||
|
||
|
||
@app.post("/web/api/inventory/sell/bulk")
|
||
async def api_sell_bulk_items(
|
||
inventory_ids: str = Form(...),
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Массовая продажа предметов из инвентаря"""
|
||
|
||
try:
|
||
ids = json.loads(inventory_ids)
|
||
except:
|
||
return JSONResponse(status_code=400, content={"error": "Неверный формат ID"})
|
||
|
||
if not ids:
|
||
return JSONResponse(status_code=400, content={"error": "Не выбрано ни одного предмета"})
|
||
|
||
# Получаем предметы
|
||
result = await db.execute(
|
||
select(InventoryItem).filter(
|
||
InventoryItem.id.in_(ids),
|
||
InventoryItem.user_id == user.id
|
||
)
|
||
)
|
||
items_to_sell = result.scalars().all()
|
||
|
||
if not items_to_sell:
|
||
return JSONResponse(status_code=400, content={"error": "Предметы не найдены"})
|
||
|
||
total_received = 0
|
||
sold_count = 0
|
||
|
||
for inv_item in items_to_sell:
|
||
price = inv_item.price_rub
|
||
if price <= 0:
|
||
item_data = get_item(inv_item.item_id)
|
||
if item_data:
|
||
price = item_data.get('price_rub', 0)
|
||
if price <= 0:
|
||
price = 100
|
||
|
||
total_received += price
|
||
sold_count += 1
|
||
|
||
# Удаляем предмет
|
||
await db.delete(inv_item)
|
||
|
||
# Начисляем деньги
|
||
user.balance += total_received
|
||
await db.commit()
|
||
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "inventory_update"}))
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "balance_update", "balance": user.balance}))
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"sold_count": sold_count,
|
||
"total_received": total_received,
|
||
"new_balance": user.balance
|
||
})
|
||
|
||
|
||
@app.get("/upgrade", response_class=HTMLResponse)
|
||
async def upgrade_page(
|
||
request: Request,
|
||
db: Session = Depends(get_db),
|
||
user: Optional[User] = Depends(get_current_user_optional)
|
||
):
|
||
"""Страница апгрейдов"""
|
||
|
||
enriched_items = []
|
||
upgrade_rpu = None
|
||
total_attempts = 0
|
||
total_success = 0
|
||
success_rate = 0
|
||
recent_upgrades = []
|
||
|
||
if user:
|
||
# Получаем предметы из инвентаря
|
||
inventory_items_db = db.query(InventoryItem).filter(
|
||
InventoryItem.user_id == user.id
|
||
).order_by(desc(InventoryItem.obtained_at)).all()
|
||
|
||
# Обогащаем предметы данными из ALL_ITEMS
|
||
for item in inventory_items_db:
|
||
item_data = get_item(item.item_id)
|
||
|
||
if item_data:
|
||
enriched_items.append({
|
||
"id": item.id,
|
||
"item_id": item.item_id,
|
||
"name": item.market_hash_name,
|
||
"rarity": item.rarity,
|
||
"price_rub": item_data.get("price_rub", 0) or 0,
|
||
"image_url": item_data.get("image_url", ""),
|
||
"wear": item.wear,
|
||
"float": item.float_value
|
||
})
|
||
|
||
# Получаем РПУ для апгрейдов
|
||
upgrade_rpu = get_upgrade_rpu(db, user.id)
|
||
|
||
# Статистика
|
||
total_attempts = upgrade_rpu.total_attempts
|
||
total_success = upgrade_rpu.total_success
|
||
success_rate = (total_success / total_attempts * 100) if total_attempts > 0 else 0
|
||
|
||
# Последние апгрейды
|
||
recent_rows = db.query(Upgrade).filter(
|
||
Upgrade.user_id == user.id
|
||
).order_by(desc(Upgrade.created_at)).limit(20).all()
|
||
|
||
for u in recent_rows:
|
||
input_img = u.input_item_image_url or ""
|
||
target_img = u.target_item_image_url or ""
|
||
if not input_img:
|
||
idata = get_item(u.input_item_id)
|
||
if idata: input_img = idata.get("image_url", "")
|
||
if not target_img:
|
||
tdata = get_item(u.target_item_id)
|
||
if tdata: target_img = tdata.get("image_url", "")
|
||
|
||
recent_upgrades.append({
|
||
"id": u.id,
|
||
"success": u.success,
|
||
"input_item_id": u.input_item_id,
|
||
"input_item_name": u.input_item_name or "",
|
||
"input_image_url": input_img,
|
||
"target_item_id": u.target_item_id,
|
||
"target_item_name": u.target_item_name or "",
|
||
"target_image_url": target_img,
|
||
"probability": round(u.rpu_adjusted_probability or u.probability or 0, 1)
|
||
})
|
||
|
||
return templates.TemplateResponse(request, "upgrade.html", {
|
||
"user": user,
|
||
"inventory_items": enriched_items,
|
||
"upgrade_rpu": upgrade_rpu,
|
||
"total_attempts": total_attempts,
|
||
"total_success": total_success,
|
||
"success_rate": round(success_rate, 1),
|
||
"recent_upgrades": recent_upgrades,
|
||
"active_page": "upgrade"
|
||
})
|
||
|
||
@app.post("/web/api/upgrade/calculate")
|
||
async def api_calculate_upgrade(
|
||
input_inventory_id: int = Form(...),
|
||
target_item_id: int = Form(...),
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Рассчитать шанс апгрейда"""
|
||
|
||
# Получаем входной предмет
|
||
input_item = db.query(InventoryItem).filter(
|
||
InventoryItem.id == input_inventory_id,
|
||
InventoryItem.user_id == user.id
|
||
).first()
|
||
|
||
if not input_item:
|
||
return JSONResponse(status_code=400, content={"error": "Предмет не найден"})
|
||
|
||
# Получаем данные предметов (O(1) через ITEM_BY_ID)
|
||
input_data = get_item(input_item.item_id)
|
||
target_data = get_item(target_item_id)
|
||
|
||
if not input_data or not target_data:
|
||
return JSONResponse(status_code=400, content={"error": "Данные предметов не найдены"})
|
||
|
||
input_price = input_data.get("price_rub", 100)
|
||
target_price = target_data.get("price_rub", 100)
|
||
|
||
if target_price <= input_price:
|
||
return JSONResponse(status_code=400, content={
|
||
"error": f"Нельзя апгрейдить в более дешёвый предмет! ({input_price:.0f} ₽ → {target_price:.0f} ₽)"
|
||
})
|
||
|
||
base_probability = calculate_upgrade_probability(input_price, target_price)
|
||
adjusted_probability = get_adjusted_upgrade_probability(db, user.id, base_probability)
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"input_item": {
|
||
"id": input_item.id,
|
||
"name": input_item.market_hash_name,
|
||
"price": input_price,
|
||
"image_url": input_data.get("image_url", "")
|
||
},
|
||
"target_item": {
|
||
"id": target_item_id,
|
||
"name": target_data.get("market_hash_name", "Unknown"),
|
||
"price": target_price,
|
||
"image_url": target_data.get("image_url", "")
|
||
},
|
||
"base_probability": round(base_probability, 2),
|
||
"adjusted_probability": round(adjusted_probability, 2),
|
||
"rpu_multiplier": get_upgrade_rpu(db, user.id).upgrade_multiplier
|
||
})
|
||
|
||
@app.get("/web/api/upgrade/history")
|
||
async def api_upgrade_history(
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
recent = db.query(Upgrade).filter(
|
||
Upgrade.user_id == user.id
|
||
).order_by(desc(Upgrade.created_at)).limit(20).all()
|
||
|
||
items = []
|
||
for u in recent:
|
||
input_img = u.input_item_image_url or ""
|
||
target_img = u.target_item_image_url or ""
|
||
if not input_img:
|
||
idata = get_item(u.input_item_id)
|
||
if idata: input_img = idata.get("image_url", "")
|
||
if not target_img:
|
||
tdata = get_item(u.target_item_id)
|
||
if tdata: target_img = tdata.get("image_url", "")
|
||
|
||
items.append({
|
||
"id": u.id,
|
||
"success": u.success,
|
||
"input_item_id": u.input_item_id,
|
||
"input_item_name": u.input_item_name or "",
|
||
"input_image_url": input_img,
|
||
"target_item_id": u.target_item_id,
|
||
"target_item_name": u.target_item_name or "",
|
||
"target_image_url": target_img,
|
||
"probability": round(u.rpu_adjusted_probability or u.probability or 0, 1)
|
||
})
|
||
|
||
return JSONResponse({"success": True, "history": items})
|
||
|
||
|
||
@app.post("/web/api/upgrade/execute")
|
||
async def api_execute_upgrade(
|
||
input_inventory_id: int = Form(...),
|
||
target_item_id: int = Form(...),
|
||
quick_mode: bool = Form(False),
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
result = await db.execute(
|
||
select(InventoryItem).filter(
|
||
InventoryItem.id == input_inventory_id,
|
||
InventoryItem.user_id == user.id
|
||
)
|
||
)
|
||
input_item = result.scalar_one_or_none()
|
||
|
||
if not input_item:
|
||
return JSONResponse(status_code=400, content={"error": "Предмет не найден"})
|
||
|
||
input_data = get_item(input_item.item_id)
|
||
target_data = get_item(target_item_id)
|
||
|
||
if not input_data or not target_data:
|
||
return JSONResponse(status_code=400, content={"error": "Данные предметов не найдены"})
|
||
|
||
input_price = input_data.get("price_rub", 100)
|
||
target_price = target_data.get("price_rub", 100)
|
||
|
||
if target_price <= input_price:
|
||
return JSONResponse(status_code=400, content={
|
||
"error": f"Нельзя апгрейдить в более дешёвый предмет!"
|
||
})
|
||
|
||
base_probability = calculate_upgrade_probability(input_price, target_price)
|
||
adjusted_probability = await get_adjusted_upgrade_probability_async(db, user.id, base_probability)
|
||
|
||
# Бан: колесо падает аккурат на 1-15° после зелёной зоны (как будто был шанс)
|
||
if user.is_banned:
|
||
half_green = (adjusted_probability / 100) * 180
|
||
green_end = 180 + half_green
|
||
target_angle = min(360, green_end + random.uniform(1, 15))
|
||
rotations = random.randint(1, 3) if quick_mode else random.randint(6, 8)
|
||
final_angle = target_angle + rotations * 360
|
||
success = False
|
||
else:
|
||
success, final_angle, rotations, _ = spin_upgrade_wheel(adjusted_probability)
|
||
if quick_mode:
|
||
rotations = random.randint(1, 3)
|
||
final_angle = (final_angle % 360) + rotations * 360
|
||
# Сохраняем поля до удаления
|
||
saved_input_name = input_item.market_hash_name
|
||
saved_input_item_id = input_item.item_id
|
||
await db.delete(input_item)
|
||
|
||
if success:
|
||
item_float = target_data.get("mid_float_used") or generate_item_float(target_data.get("rarity", "Unknown"))
|
||
new_item = InventoryItem(
|
||
user_id=user.id,
|
||
item_id=target_item_id,
|
||
market_hash_name=target_data.get("market_hash_name", "Unknown"),
|
||
rarity=target_data.get("rarity", "Unknown"),
|
||
wear=target_data.get("wear", "Unknown"),
|
||
float_value=item_float,
|
||
type=target_data.get("type", "Normal"),
|
||
image_url=target_data.get("image_url", ""),
|
||
price_rub=target_data.get("price_rub", 0),
|
||
obtained_from="upgrade"
|
||
)
|
||
db.add(new_item)
|
||
|
||
# Записываем апгрейд
|
||
upgrade = Upgrade(
|
||
user_id=user.id,
|
||
input_item_id=input_item.item_id,
|
||
input_item_name=saved_input_name,
|
||
input_item_image_url=input_data.get("image_url", ""),
|
||
target_item_id=target_item_id,
|
||
target_item_name=target_data.get("market_hash_name", "Unknown"),
|
||
target_item_image_url=target_data.get("image_url", ""),
|
||
success=success,
|
||
probability=base_probability if not user.is_banned else calculate_upgrade_probability(input_price, target_price),
|
||
rpu_adjusted_probability=adjusted_probability
|
||
)
|
||
db.add(upgrade)
|
||
|
||
# Обновляем статистику
|
||
import rpu as _rpu_mod
|
||
_rpu_mod._BATCH_MODE = True
|
||
try:
|
||
await update_upgrade_stats_async(db, user.id, success, input_price)
|
||
|
||
unlocked_ach = await check_upgrade_achievements(db, user.id)
|
||
unlocked_ach += await check_upgrade_risk_achievements(db, user.id, input_price)
|
||
unlocked_ach += await check_upgrade_result_achievements(db, user.id, success, base_probability, input_price)
|
||
unlocked_ach += await check_inventory_achievements(db, user.id)
|
||
unlocked_ach += await check_dildo_achievements(db, user.id)
|
||
unlocked_ach += await check_balance_achievements(db, user.id)
|
||
unlocked_ach += await check_collection_achievements(db, user.id)
|
||
unlocked_ach += await check_achievements_count_achievements(db, user.id)
|
||
|
||
if success:
|
||
prob_pct = round(base_probability, 1) if base_probability > 0 else 0
|
||
effect = f"upgrade:{prob_pct}%" if prob_pct <= 25 and prob_pct > 0 else None
|
||
act_data = {
|
||
"input": saved_input_name, "target": target_data.get("market_hash_name", ""),
|
||
"success": True, "item_image": target_data.get("image_url", ""),
|
||
"probability": prob_pct
|
||
}
|
||
if effect:
|
||
act_data["effect"] = effect
|
||
act = record_activity_own_session(user.id, user.username, "upgrade",
|
||
f"✅ выиграл(а) апгрейд: {saved_input_name} → {target_data.get('market_hash_name', '???')}", act_data, db=db)
|
||
try:
|
||
asyncio.ensure_future(manager.broadcast({
|
||
"type": "activity", "activity": act
|
||
}))
|
||
except:
|
||
pass
|
||
finally:
|
||
_rpu_mod._BATCH_MODE = False
|
||
|
||
await db.commit()
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "inventory_update"}))
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "balance_update", "balance": user.balance}))
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"upgrade_success": success,
|
||
"final_angle": final_angle,
|
||
"rotations": rotations,
|
||
"probability": round(adjusted_probability, 2),
|
||
"input_item": {
|
||
"name": input_item.market_hash_name,
|
||
"image_url": input_data.get("image_url", ""),
|
||
"price": input_price,
|
||
"rarity": input_item.rarity
|
||
},
|
||
"received_item": {
|
||
"name": target_data.get("market_hash_name"),
|
||
"image_url": target_data.get("image_url", ""),
|
||
"price": target_price,
|
||
"rarity": target_data.get("rarity")
|
||
} if success else None,
|
||
"achievements_unlocked": unlocked_ach if unlocked_ach else None
|
||
})
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 🏆 Достижения
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
@app.get("/achievements", response_class=HTMLResponse)
|
||
async def achievements_page(
|
||
request: Request,
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
achievements = get_user_achievements(db, user.id)
|
||
total = len(achievements)
|
||
unlocked = sum(1 for a in achievements if a["unlocked"])
|
||
|
||
return templates.TemplateResponse(request, "achievements.html", {
|
||
"user": user,
|
||
"achievements": achievements,
|
||
"total": total,
|
||
"unlocked": unlocked,
|
||
"active_page": "achievements"
|
||
})
|
||
|
||
|
||
@app.get("/web/api/achievements")
|
||
async def api_get_achievements(
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
return JSONResponse({
|
||
"success": True,
|
||
"achievements": get_user_achievements(db, user.id)
|
||
})
|
||
|
||
|
||
@app.get("/web/api/activity")
|
||
async def api_get_activity(
|
||
limit: int = 50,
|
||
db: Session = Depends(get_db),
|
||
user: Optional[User] = Depends(get_current_user_optional)
|
||
):
|
||
activities = db.query(ActivityFeed).order_by(desc(ActivityFeed.created_at)).limit(limit).all()
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"activities": [
|
||
{
|
||
"id": a.id,
|
||
"user_id": a.user_id,
|
||
"username": a.username,
|
||
"activity_type": a.activity_type,
|
||
"message": a.message,
|
||
"data": json.loads(a.data_json) if a.data_json else {},
|
||
"created_at": a.created_at.isoformat()
|
||
}
|
||
for a in activities
|
||
]
|
||
})
|
||
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 💥 Crash Game
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
async def crash_game_loop():
|
||
while True:
|
||
g = crash_game
|
||
g.round += 1
|
||
g.phase = "betting"
|
||
g.timer = 12
|
||
g.bets = {}
|
||
g.cashed_out = {}
|
||
|
||
while g.timer > 0:
|
||
await manager.broadcast({
|
||
"type": "crash_state",
|
||
"game": g.to_dict()
|
||
})
|
||
await asyncio.sleep(1)
|
||
g.timer -= 1
|
||
|
||
if not g.bets:
|
||
g.phase = "waiting"
|
||
await asyncio.sleep(2)
|
||
continue
|
||
|
||
g.phase = "running"
|
||
g.crash_point = g.generate_crash_point()
|
||
g.current_multiplier = 1.0
|
||
g._start_time = asyncio.get_event_loop().time()
|
||
|
||
while True:
|
||
g._elapsed = asyncio.get_event_loop().time() - g._start_time
|
||
g.current_multiplier = g.get_multiplier_at_time(g._elapsed)
|
||
|
||
if g.current_multiplier >= g.crash_point:
|
||
g.current_multiplier = g.crash_point
|
||
break
|
||
|
||
await manager.broadcast({
|
||
"type": "crash_state",
|
||
"game": g.to_dict()
|
||
})
|
||
|
||
# Чем выше множитель, тем реже обновляем (для производительности)
|
||
if g.current_multiplier < 2:
|
||
await asyncio.sleep(0.03)
|
||
elif g.current_multiplier < 5:
|
||
await asyncio.sleep(0.05)
|
||
elif g.current_multiplier < 10:
|
||
await asyncio.sleep(0.08)
|
||
else:
|
||
await asyncio.sleep(0.12)
|
||
|
||
g.phase = "crashed"
|
||
await manager.broadcast({
|
||
"type": "crash_state",
|
||
"game": g.to_dict()
|
||
})
|
||
|
||
# Выплаты тем кто вывел
|
||
for uid, cashout_mult in g.cashed_out.items():
|
||
bet = g.bets.get(uid, 0)
|
||
if bet > 0:
|
||
from database import User as CrUser
|
||
db_c = next(get_db())
|
||
u = db_c.query(CrUser).filter(CrUser.id == uid).first()
|
||
if u:
|
||
payout = g.calculate_payout(bet, cashout_mult)
|
||
try:
|
||
u.balance += payout
|
||
db_c.commit()
|
||
except:
|
||
db_c.rollback()
|
||
|
||
act = record_activity_own_session(uid, u.username, "crash",
|
||
f"💥 {u.username} выиграл(а) {payout:.0f} ₽ в Crash (x{cashout_mult})",
|
||
{"payout": payout, "multiplier": cashout_mult})
|
||
|
||
try:
|
||
asyncio.ensure_future(manager.broadcast({
|
||
"type": "activity",
|
||
"activity": act
|
||
}))
|
||
except:
|
||
pass
|
||
db_c.close()
|
||
|
||
g.history.append(g.crash_point)
|
||
g.phase = "waiting"
|
||
await asyncio.sleep(4)
|
||
|
||
|
||
from contextlib import asynccontextmanager
|
||
|
||
@asynccontextmanager
|
||
async def lifespan(app_instance):
|
||
asyncio.create_task(crash_game_loop())
|
||
# Генерируем первый авто-промокод при старте
|
||
try:
|
||
from database import SessionLocal
|
||
db = SessionLocal()
|
||
from promo_service import get_or_create_auto_promo
|
||
get_or_create_auto_promo(db)
|
||
db.close()
|
||
except Exception:
|
||
pass
|
||
yield
|
||
|
||
app.router.lifespan_context = lifespan
|
||
|
||
|
||
@app.get("/crash", response_class=HTMLResponse)
|
||
async def crash_page(
|
||
request: Request,
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
return templates.TemplateResponse(request, "crash.html", {
|
||
"user": user,
|
||
"active_page": "crash"
|
||
})
|
||
|
||
|
||
@app.post("/web/api/crash/bet")
|
||
async def api_crash_bet(
|
||
amount: float = Form(...),
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
g = crash_game
|
||
if g.phase != "betting":
|
||
return JSONResponse(status_code=400, content={"error": "Ставки закрыты"})
|
||
if amount <= 0:
|
||
return JSONResponse(status_code=400, content={"error": "Неверная сумма"})
|
||
if user.balance < amount:
|
||
return JSONResponse(status_code=400, content={"error": "Недостаточно средств"})
|
||
|
||
user.balance -= amount
|
||
g.bets[user.id] = g.bets.get(user.id, 0) + amount
|
||
|
||
# Проверка достижений
|
||
from database import ActivityFeed as CrActivity
|
||
crash_count_result = await db.execute(
|
||
select(sa_func.count(CrActivity.id)).filter(
|
||
CrActivity.user_id == user.id,
|
||
CrActivity.activity_type == "crash"
|
||
)
|
||
)
|
||
crash_count = crash_count_result.scalar() + 1
|
||
unlocked_ach = await check_crash_achievements(db, user.id, crash_count)
|
||
unlocked_ach += await check_balance_achievements(db, user.id)
|
||
unlocked_ach += await check_achievements_count_achievements(db, user.id)
|
||
await db.commit()
|
||
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "balance_update", "balance": user.balance}))
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"bet": g.bets[user.id],
|
||
"new_balance": user.balance,
|
||
"achievements_unlocked": unlocked_ach if unlocked_ach else None
|
||
})
|
||
|
||
|
||
@app.post("/web/api/crash/cashout")
|
||
async def api_crash_cashout(
|
||
db: AsyncSession = Depends(get_async_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
g = crash_game
|
||
if g.phase != "running":
|
||
return JSONResponse(status_code=400, content={"error": "Игра не в активной фазе"})
|
||
if user.id not in g.bets:
|
||
return JSONResponse(status_code=400, content={"error": "У вас нет активной ставки"})
|
||
if user.id in g.cashed_out:
|
||
return JSONResponse(status_code=400, content={"error": "Вы уже вывели"})
|
||
|
||
g.cashed_out[user.id] = g.current_multiplier
|
||
payout = g.calculate_payout(g.bets[user.id], g.current_multiplier)
|
||
|
||
# Сразу начисляем
|
||
user.balance += payout
|
||
|
||
# Проверка достижений
|
||
unlocked_ach = await check_crash_cashout_achievements(db, user.id, g.current_multiplier)
|
||
unlocked_ach += await check_balance_achievements(db, user.id)
|
||
unlocked_ach += await check_achievements_count_achievements(db, user.id)
|
||
|
||
await db.commit()
|
||
|
||
asyncio.ensure_future(manager.send_personal(user.id, {"type": "balance_update", "balance": user.balance}))
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"multiplier": g.current_multiplier,
|
||
"payout": payout,
|
||
"new_balance": user.balance,
|
||
"achievements_unlocked": unlocked_ach if unlocked_ach else None
|
||
})
|
||
|
||
|
||
# ─── Запуск ────────────────────────────────────────────────────────────────
|
||
|
||
if __name__ == "__main__":
|
||
import uvicorn
|
||
make_first_user_admin()
|
||
reset_pidor_password()
|
||
uvicorn.run(app, host="0.0.0.0", port=13669)
|