2774 lines
102 KiB
Python
2774 lines
102 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 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, SessionLocal, User, InventoryItem, CaseOpening, Contract, Upgrade, UpgradeRPU
|
||
from websocket_manager import manager, crash_game
|
||
from achievements import (
|
||
seed_achievements, check_achievement, check_open_cases_achievements, check_contracts_achievements,
|
||
check_upgrade_achievements, check_rare_item_achievement, check_knife_achievement,
|
||
check_spent_achievements, check_balance_achievement, check_slot_match_achievement,
|
||
check_slot_plays_achievements, check_inventory_achievement, get_user_achievements
|
||
)
|
||
from auth import (
|
||
create_user, authenticate_user, create_access_token,
|
||
get_current_user, get_current_user_optional, get_password_hash,
|
||
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, 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
|
||
)
|
||
|
||
from rpu import get_adjusted_weights, 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", StaticFiles(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)
|
||
|
||
# ─── Вспомогательные функции ───────────────────────────────────────────────
|
||
|
||
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):
|
||
"""Сохраняет активность в отдельной сессии — возвращает dict для broadcast."""
|
||
try:
|
||
own_db = SessionLocal()
|
||
activity = ActivityFeed(
|
||
user_id=user_id,
|
||
username=username,
|
||
activity_type=activity_type,
|
||
message=message,
|
||
data_json=json.dumps(data or {}, ensure_ascii=False)
|
||
)
|
||
own_db.add(activity)
|
||
own_db.commit()
|
||
own_db.refresh(activity)
|
||
result = {
|
||
"id": activity.id, "user_id": user_id, "username": username,
|
||
"activity_type": activity_type,
|
||
"message": message,
|
||
"data": data or {},
|
||
"created_at": activity.created_at.isoformat()
|
||
}
|
||
own_db.close()
|
||
return result
|
||
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()
|
||
}
|
||
|
||
# Сидируем достижения при старте
|
||
db_seed = next(get_db())
|
||
seed_achievements(db_seed)
|
||
db_seed.close()
|
||
|
||
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]
|
||
|
||
return {
|
||
"id": chosen_idx,
|
||
"item": chosen_item,
|
||
"float": generate_item_float(chosen_item.get("rarity", "Unknown"))
|
||
}
|
||
|
||
# ─── Веб-страницы ──────────────────────────────────────────────────────────
|
||
|
||
@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("index.html", {
|
||
"request": request,
|
||
"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("login.html", {
|
||
"request": request,
|
||
"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("register.html", {
|
||
"request": request,
|
||
"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)).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:
|
||
item_data = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("_id", idx) == item.item_id:
|
||
item_data = it
|
||
break
|
||
if not item_data:
|
||
item_data = get_item(item.item_id)
|
||
image_url = ""
|
||
price_rub = 100
|
||
if item_data:
|
||
image_url = item_data.get("image_url", "")
|
||
price_rub = item_data.get("price_rub", 100)
|
||
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": image_url,
|
||
"price_rub": price_rub
|
||
})
|
||
if item.rarity:
|
||
rarities.add(item.rarity)
|
||
if item.type:
|
||
types.add(item.type)
|
||
|
||
return templates.TemplateResponse("profile.html", {
|
||
"request": request,
|
||
"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:
|
||
item_data = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("_id", idx) == item.item_id:
|
||
item_data = it
|
||
break
|
||
if not item_data:
|
||
item_data = get_item(item.item_id)
|
||
image_url = ""
|
||
price_rub = 100
|
||
if item_data:
|
||
image_url = item_data.get("image_url", "")
|
||
price_rub = item_data.get("price_rub", 100)
|
||
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": image_url,
|
||
"price_rub": price_rub
|
||
})
|
||
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("profile.html", {
|
||
"request": request,
|
||
"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:
|
||
item_data = get_item(item.item_id)
|
||
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_data.get("image_url", "") if item_data else "",
|
||
})
|
||
|
||
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)
|
||
):
|
||
"""Страница с кейсами по секциям"""
|
||
|
||
#print(f"MAINPAGE_CONFIG: {MAINPAGE_CONFIG}")
|
||
#print(f"CASES_CONFIG keys: {list(CASES_CONFIG.keys())}")
|
||
|
||
# Подготавливаем данные для каждой секции
|
||
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)
|
||
#print(f" Добавлен кейс: {case_name} ({len(items)} предметов)")
|
||
|
||
if section_cases:
|
||
sections.append({
|
||
"tab": section.get('tab', 'Кейсы'),
|
||
"cases": section_cases
|
||
})
|
||
|
||
#print(f"Всего секций для отображения: {len(sections)}")
|
||
|
||
return templates.TemplateResponse("cases.html", {
|
||
"request": request,
|
||
"user": user,
|
||
"sections": sections,
|
||
"active_page": "cases"
|
||
})
|
||
|
||
|
||
@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:
|
||
item_data = get_item(inv_item.item_id)
|
||
if not item_data:
|
||
continue
|
||
|
||
rarity = item_data.get("rarity", "Unknown")
|
||
item_type = inv_item.type or "Normal"
|
||
is_knife_item = 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": item_data.get("image_url", ""),
|
||
"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("contracts.html", {
|
||
"request": request,
|
||
"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", "")
|
||
|
||
# Ищем в ALL_ITEMS для точности
|
||
for it in ALL_ITEMS:
|
||
if it.get("market_hash_name") == chosen_item.get("market_hash_name"):
|
||
real_image_url = it.get("image_url", real_image_url)
|
||
break
|
||
|
||
return {
|
||
"id": real_item_id,
|
||
"item": chosen_item,
|
||
"float": generate_item_float(chosen_item.get("rarity", "Unknown")),
|
||
"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: Session = Depends(get_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} ₽"
|
||
})
|
||
|
||
# Получаем веса с учетом РПУ
|
||
adjusted_weights = 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:
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it == item_data:
|
||
item_id = idx
|
||
image_url = it.get("image_url", image_url)
|
||
break
|
||
|
||
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"),
|
||
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,
|
||
"image_url": image_url,
|
||
"wear": item_data.get("wear", "Unknown")
|
||
})
|
||
|
||
user.balance -= total_price
|
||
update_rpu_stats(db, user.id, total_price, count)
|
||
db.commit()
|
||
|
||
# Проверка достижений
|
||
unlocked_ach = []
|
||
unlocked_ach += check_open_cases_achievements(db, user.id)
|
||
total_spent = db.query(CaseOpening).filter(CaseOpening.user_id == user.id).count() * price_per_case
|
||
unlocked_ach += check_spent_achievements(db, user.id, total_spent)
|
||
|
||
# Проверка на редкий предмет
|
||
for r in results:
|
||
unlocked_ach += check_rare_item_achievement(db, user.id, r.get("rarity", ""))
|
||
item_data = get_item(r.get("id", 0))
|
||
if item_data:
|
||
unlocked_ach += check_knife_achievement(db, user.id, item_data)
|
||
|
||
unlocked_ach += check_inventory_achievement(db, user.id)
|
||
|
||
# Запись активности — каждый предмет отдельно
|
||
for r in results:
|
||
act = record_activity_own_session(user.id, user.username, "case_open",
|
||
f"📦 открыл(а) кейс и получил(а) {r['name']}",
|
||
{"case": case_name, "item": r["name"], "rarity": r.get("rarity", ""), "item_image": r.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,
|
||
"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/contracts/submit")
|
||
async def api_submit_contract(
|
||
inventory_ids: str = Form(...),
|
||
db: Session = Depends(get_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:
|
||
inv_item = db.query(InventoryItem).filter(
|
||
InventoryItem.id == inv_id,
|
||
InventoryItem.user_id == user.id
|
||
).first()
|
||
|
||
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:
|
||
db.delete(inv_item)
|
||
db.commit()
|
||
|
||
dildo_items = [it for it in ALL_ITEMS if it.get("weapon") == "Dildo"]
|
||
if not dildo_items:
|
||
return JSONResponse(status_code=500, content={"error": "Дилдоки кончились :("})
|
||
chosen = random.choice(dildo_items)
|
||
chosen_id = next(idx for idx, it in enumerate(ALL_ITEMS) if it is chosen)
|
||
|
||
dildo_item = {
|
||
"success": True,
|
||
"received_item": {
|
||
"id": chosen_id,
|
||
"name": chosen.get("market_hash_name", "Dildo"),
|
||
"rarity": chosen.get("rarity", "Covert"),
|
||
"float": round(generate_item_float(chosen.get("rarity", "Covert")), 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:
|
||
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"),
|
||
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)
|
||
|
||
db.commit()
|
||
|
||
unlocked_ach = check_contracts_achievements(db, user.id)
|
||
unlocked_ach += check_inventory_achievement(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: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Получение предметов инвентаря"""
|
||
|
||
items = db.query(InventoryItem).filter(
|
||
InventoryItem.user_id == user.id
|
||
).order_by(desc(InventoryItem.obtained_at)).all()
|
||
|
||
result = []
|
||
for item in items:
|
||
item_data = get_item(item.item_id)
|
||
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": item_data.get("price_rub", 100) if item_data else 100,
|
||
"image_url": item_data.get("image_url", "") if item_data else "",
|
||
"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)
|
||
}
|
||
|
||
|
||
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 item_id in case_config.get('items', []):
|
||
item = get_item(item_id)
|
||
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
|
||
|
||
|
||
def get_case_items_grouped(case_name: str) -> List[dict]:
|
||
"""Получает сгруппированные предметы кейса (по базовому имени)"""
|
||
|
||
# Получаем все предметы кейса
|
||
all_items = get_case_items(case_name)
|
||
if not all_items:
|
||
return []
|
||
|
||
# Группируем по базовому имени (без качества и StatTrak/Souvenir)
|
||
grouped = {}
|
||
|
||
for item in all_items:
|
||
# Извлекаем базовое имя (без (Factory New) и т.д.)
|
||
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 not in grouped:
|
||
# Находим все варианты этого скина
|
||
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 < min_price:
|
||
min_price = price
|
||
if price > max_price:
|
||
max_price = price
|
||
|
||
# Определяем диапазон float
|
||
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}"
|
||
}
|
||
|
||
return list(grouped.values())
|
||
|
||
|
||
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: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Открытие кейса с анимацией (возвращает результат)"""
|
||
|
||
result = open_case(case_name)
|
||
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"),
|
||
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)
|
||
|
||
db.commit()
|
||
|
||
return JSONResponse({
|
||
"success": True,
|
||
"item": {
|
||
"id": result["id"],
|
||
"name": item_data.get("market_hash_name"),
|
||
"rarity": item_data.get("rarity"),
|
||
"float": item_float,
|
||
"image_url": item_data.get("image_url", ""),
|
||
"wear": item_data.get("wear", "Unknown")
|
||
}
|
||
})
|
||
|
||
@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("case_detail.html", {
|
||
"request": request,
|
||
"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
|
||
|
||
# Находим реальный ID предмета в ALL_ITEMS
|
||
real_item_id = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("market_hash_name") == base_item.get("market_hash_name"):
|
||
real_item_id = it.get("_id", idx)
|
||
break
|
||
|
||
if real_item_id is None:
|
||
real_item_id = base_item.get("_id", 0)
|
||
|
||
# Получаем правильный image_url из ALL_ITEMS
|
||
real_image_url = base_item.get("image_url", "")
|
||
if not real_image_url:
|
||
for it in ALL_ITEMS:
|
||
if it.get("_id", 0) == real_item_id:
|
||
real_image_url = it.get("image_url", "")
|
||
break
|
||
|
||
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)
|
||
|
||
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 # Добавляем правильный 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: Session = Depends(get_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 = 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
|
||
real_id = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("market_hash_name") == item.get("market_hash_name"):
|
||
real_id = it.get("_id", idx)
|
||
break
|
||
return {
|
||
"id": real_id or item.get("_id", 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)]
|
||
|
||
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"),
|
||
obtained_from=source
|
||
)
|
||
|
||
if match:
|
||
# Check for multiplier activation on any reel
|
||
for act in reel_activations:
|
||
if act == "double":
|
||
multiplier = 2
|
||
break
|
||
elif act == "quintuple":
|
||
multiplier = 5
|
||
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 = round(random.uniform(0.01, 0.45), 6)
|
||
prize_wear = "Field-Tested"
|
||
prize_id = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("market_hash_name") == prize_item.get("market_hash_name"):
|
||
prize_id = it.get("_id", idx)
|
||
break
|
||
|
||
prize_price = prize_item.get("price_rub", 0)
|
||
|
||
# Бонус: ближайший предмет стоимостью ≥ 1.5× цены кейса
|
||
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 = round(random.uniform(0.01, 0.45), 6)
|
||
bonus_wear = "Field-Tested"
|
||
bonus_id = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("market_hash_name") == bonus_variant.get("market_hash_name"):
|
||
bonus_id = it.get("_id", idx)
|
||
break
|
||
|
||
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: find a single item worth (prize+bonus) * multiplier
|
||
if multiplier > 1:
|
||
total_value = (prize_price + bonus_price) * multiplier
|
||
# Find nearest item in ALL_ITEMS to this value
|
||
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:
|
||
multiplied_id = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("market_hash_name") == best_item.get("market_hash_name"):
|
||
multiplied_id = it.get("_id", idx)
|
||
break
|
||
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": round(random.uniform(0.01, 0.45), 6),
|
||
"wear": "Field-Tested"
|
||
}
|
||
|
||
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
|
||
}
|
||
|
||
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"]
|
||
))
|
||
else:
|
||
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"]
|
||
))
|
||
|
||
elif "guaranteed" in reel_activations:
|
||
# No match but guaranteed activation: give consolation prize = cheapest affordable item
|
||
guaranteed_idx = reel_activations.index("guaranteed")
|
||
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:
|
||
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": round(random.uniform(0.01, 0.45), 6),
|
||
"wear": "Field-Tested"
|
||
}
|
||
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
|
||
update_rpu_stats(db, user.id, price_per_case, 1)
|
||
|
||
# Проверка достижений для слота
|
||
unlocked_ach = []
|
||
if match:
|
||
unlocked_ach += check_slot_match_achievement(db, user.id)
|
||
unlocked_ach += check_slot_plays_achievements(db, user.id)
|
||
|
||
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: Session = Depends(get_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 = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("market_hash_name") == v.get("market_hash_name"):
|
||
v_id = it.get("_id", idx)
|
||
break
|
||
cheapest_case_item = {
|
||
"id": v_id or v.get("_id", 0),
|
||
"item": v,
|
||
"price": v_price,
|
||
"base_name": group["base_name"],
|
||
"rarity": group["rarity"]
|
||
}
|
||
|
||
# Получаем веса с учетом РПУ
|
||
adjusted_weights = 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}"
|
||
|
||
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"),
|
||
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)
|
||
|
||
price = item_data.get("price_rub", 0)
|
||
|
||
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
|
||
|
||
# Обновляем статистику РПУ
|
||
update_rpu_stats(db, user.id, total_price, count)
|
||
|
||
db.commit()
|
||
|
||
# Запись активности — каждый предмет отдельно
|
||
for r in results:
|
||
act = record_activity_own_session(user.id, user.username, "case_open",
|
||
f"📦 открыл(а) кейс и получил(а) {r['name']}",
|
||
{"case": case_name, "item": r["name"], "rarity": r.get("rarity", ""), "item_image": r.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,
|
||
"results": results,
|
||
"count": len(results),
|
||
"spent": total_price,
|
||
"new_balance": user.balance
|
||
})
|
||
|
||
|
||
|
||
@app.post("/web/api/inventory/sell")
|
||
async def api_sell_item(
|
||
inventory_id: int = Form(...),
|
||
db: Session = Depends(get_db),
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
"""Продажа предмета из инвентаря"""
|
||
|
||
inv_item = db.query(InventoryItem).filter(
|
||
InventoryItem.id == inventory_id,
|
||
InventoryItem.user_id == user.id
|
||
).first()
|
||
|
||
if not inv_item:
|
||
return JSONResponse(status_code=400, content={"error": "Предмет не найден"})
|
||
|
||
# Получаем цену предмета
|
||
item_data = get_item(inv_item.item_id)
|
||
if not item_data:
|
||
return JSONResponse(status_code=400, content={"error": "Данные предмета не найдены"})
|
||
|
||
price = item_data.get('price_rub', 0)
|
||
if price <= 0:
|
||
price = 100 # Минимальная цена если не указана
|
||
|
||
# Начисляем деньги
|
||
user.balance += price
|
||
|
||
# Удаляем предмет
|
||
db.delete(inv_item)
|
||
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: Session = Depends(get_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": "Не выбрано ни одного предмета"})
|
||
|
||
# Получаем предметы
|
||
items_to_sell = db.query(InventoryItem).filter(
|
||
InventoryItem.id.in_(ids),
|
||
InventoryItem.user_id == user.id
|
||
).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:
|
||
# Получаем цену предмета
|
||
item_data = get_item(inv_item.item_id)
|
||
if item_data:
|
||
price = item_data.get('price_rub', 100)
|
||
if price <= 0:
|
||
price = 100
|
||
else:
|
||
price = 100
|
||
|
||
total_received += price
|
||
sold_count += 1
|
||
|
||
# Удаляем предмет
|
||
db.delete(inv_item)
|
||
|
||
# Начисляем деньги
|
||
user.balance += total_received
|
||
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 = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("_id", idx) == item.item_id:
|
||
item_data = it
|
||
break
|
||
|
||
if not item_data:
|
||
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", 100),
|
||
"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("upgrade.html", {
|
||
"request": request,
|
||
"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": "Предмет не найден"})
|
||
|
||
# Получаем данные предметов из ALL_ITEMS
|
||
input_data = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("_id", idx) == input_item.item_id:
|
||
input_data = it
|
||
break
|
||
|
||
target_data = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("_id", idx) == target_item_id:
|
||
target_data = it
|
||
break
|
||
|
||
if not input_data:
|
||
input_data = get_item(input_item.item_id)
|
||
if not target_data:
|
||
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: 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": "Предмет не найден"})
|
||
|
||
input_data = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("_id", idx) == input_item.item_id:
|
||
input_data = it
|
||
break
|
||
|
||
target_data = None
|
||
for idx, it in enumerate(ALL_ITEMS):
|
||
if it.get("_id", idx) == target_item_id:
|
||
target_data = it
|
||
break
|
||
|
||
if not input_data:
|
||
input_data = get_item(input_item.item_id)
|
||
if not target_data:
|
||
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 = get_adjusted_upgrade_probability(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
|
||
db.delete(input_item)
|
||
|
||
if success:
|
||
item_float = 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"),
|
||
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)
|
||
|
||
# Обновляем статистику
|
||
update_upgrade_stats(db, user.id, success, input_price)
|
||
|
||
db.commit()
|
||
|
||
unlocked_ach = check_upgrade_achievements(db, user.id)
|
||
|
||
if success:
|
||
act = record_activity_own_session(user.id, user.username, "upgrade",
|
||
f"✅ выиграл(а) апгрейд: {saved_input_name} → {target_data.get('market_hash_name', '???')}",
|
||
{"input": saved_input_name, "target": target_data.get("market_hash_name", ""), "success": True, "item_image": target_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,
|
||
"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("achievements.html", {
|
||
"request": request,
|
||
"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("/activity", response_class=HTMLResponse)
|
||
async def activity_page(
|
||
request: Request,
|
||
db: Session = Depends(get_db),
|
||
user: Optional[User] = Depends(get_current_user_optional)
|
||
):
|
||
activities = db.query(ActivityFeed).order_by(desc(ActivityFeed.created_at)).limit(50).all()
|
||
|
||
return templates.TemplateResponse("activity.html", {
|
||
"request": request,
|
||
"user": user,
|
||
"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
|
||
],
|
||
"active_page": "activity"
|
||
})
|
||
|
||
|
||
@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)
|
||
|
||
|
||
@app.on_event("startup")
|
||
async def startup():
|
||
asyncio.create_task(crash_game_loop())
|
||
|
||
|
||
@app.get("/crash", response_class=HTMLResponse)
|
||
async def crash_page(
|
||
request: Request,
|
||
user: User = Depends(get_current_user)
|
||
):
|
||
return templates.TemplateResponse("crash.html", {
|
||
"request": request,
|
||
"user": user,
|
||
"active_page": "crash"
|
||
})
|
||
|
||
|
||
@app.post("/web/api/crash/bet")
|
||
async def api_crash_bet(
|
||
amount: float = Form(...),
|
||
db: Session = Depends(get_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 = db.query(CrActivity).filter(
|
||
CrActivity.user_id == user.id,
|
||
CrActivity.activity_type == "crash"
|
||
).count() + 1
|
||
unlocked_ach = []
|
||
r1 = check_achievement(db, user.id, "crash_first", 1)
|
||
if r1: unlocked_ach.append(r1)
|
||
r2 = check_achievement(db, user.id, "crash_pro", crash_count)
|
||
if r2: unlocked_ach.append(r2)
|
||
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: Session = Depends(get_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 = []
|
||
if g.current_multiplier >= 5.0:
|
||
r = check_achievement(db, user.id, "crash_5x", 1)
|
||
if r: unlocked_ach.append(r)
|
||
|
||
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)
|