diff --git a/app/api/bonuses.py b/app/api/bonuses.py new file mode 100644 index 0000000..6ee9387 --- /dev/null +++ b/app/api/bonuses.py @@ -0,0 +1,41 @@ +from fastapi import APIRouter, Query, Body +from fastapi import HTTPException +from datetime import datetime, timedelta +from app.models.bonus import PurchaseBonus +import uuid + +router = APIRouter( + prefix="/api/bonuses", + tags=["Bonuses"] +) + +@router.get("/effects") +async def get_user_effects(username: str): + """Получить активные эффекты пользователя для плагина""" + from app.services.bonus import BonusService + return await BonusService().get_user_active_effects(username) + +@router.get("/types") +async def get_bonus_types(): + """Получить доступные типы бонусов""" + from app.services.bonus import BonusService + return await BonusService().list_available_bonuses() + +@router.get("/user/{username}") +async def get_user_bonuses(username: str): + """Получить активные бонусы пользователя""" + from app.services.bonus import BonusService + return await BonusService().get_user_bonuses(username) + +@router.post("/purchase") +async def purchase_bonus(purchase_bonus: PurchaseBonus): + """Купить бонус""" + from app.services.bonus import BonusService + return await BonusService().purchase_bonus(purchase_bonus.username, purchase_bonus.bonus_type_id) + +@router.post("/upgrade") +async def upgrade_user_bonus(username: str = Body(...), bonus_id: str = Body(...)): + """Улучшить существующий бонус""" + from app.services.bonus import BonusService + return await BonusService().upgrade_bonus(username, bonus_id) + diff --git a/app/models/bonus.py b/app/models/bonus.py new file mode 100644 index 0000000..f4987e1 --- /dev/null +++ b/app/models/bonus.py @@ -0,0 +1,48 @@ +from pydantic import BaseModel +from typing import Optional, List +from datetime import datetime + +class PurchaseBonus(BaseModel): + username: str + bonus_type_id: str + +class BonusEffect(BaseModel): + effect_type: str + effect_value: float + expires_at: Optional[datetime] = None + +class BonusType(BaseModel): + id: str + name: str + description: str + effect_type: str # "experience", "strength", "speed", etc. + base_effect_value: float # Базовое значение эффекта (например, 1.0 для +100%) + effect_increment: float # Прирост эффекта за уровень (например, 0.1 для +10%) + price: int # Базовая цена + upgrade_price: int # Цена улучшения за уровень + duration: int # Длительность в секундах (0 для бесконечных) + max_level: int = 0 # 0 = без ограничения уровней + +class UserTypeBonus(BaseModel): + id: str + name: str + description: str + effect_type: str + effect_value: float + level: int + purchased_at: datetime + can_upgrade: bool + upgrade_price: int + expires_at: Optional[datetime] = None + is_active: bool = True + is_permanent: bool + +class UserBonus(BaseModel): + id: str + user_id: str + username: str + bonus_type_id: str + level: int + purchased_at: datetime + expires_at: Optional[datetime] = None + is_active: bool diff --git a/app/services/bonus.py b/app/services/bonus.py new file mode 100644 index 0000000..3d77899 --- /dev/null +++ b/app/services/bonus.py @@ -0,0 +1,226 @@ +import uuid +from datetime import datetime, timedelta +from fastapi import HTTPException +from app.db.database import db +from app.services.coins import CoinsService +from app.models.bonus import BonusType + +# Коллекции для бонусов +bonus_types_collection = db.bonus_types +user_bonuses_collection = db.user_bonuses + +class BonusService: + async def get_user_active_effects(self, username: str): + """Получить активные эффекты пользователя для плагина""" + from app.db.database import users_collection + + user = await users_collection.find_one({"username": username}) + if not user: + return {"effects": []} + + # Находим активные бонусы с учетом бесконечных (expires_at = null) или действующих + active_bonuses = await user_bonuses_collection.find({ + "user_id": str(user["_id"]), + "is_active": True, + }).to_list(50) + + effects = [] + for bonus in active_bonuses: + bonus_type = await bonus_types_collection.find_one({"id": bonus["bonus_type_id"]}) + if bonus_type: + # Рассчитываем итоговое значение эффекта с учетом уровня + level = bonus.get("level", 1) + effect_value = bonus_type["base_effect_value"] + (level - 1) * bonus_type["effect_increment"] + + effect = { + "effect_type": bonus_type["effect_type"], + "effect_value": effect_value + } + + # Для временных бонусов добавляем срок + if bonus.get("expires_at"): + effect["expires_at"] = bonus["expires_at"].isoformat() + + effects.append(effect) + + return {"effects": effects} + + async def list_available_bonuses(self): + """Получить список доступных типов бонусов""" + bonuses = await bonus_types_collection.find().to_list(50) + return {"bonuses": [BonusType(**bonus) for bonus in bonuses]} + + async def get_user_bonuses(self, username: str): + """Получить активные бонусы пользователя""" + from app.db.database import users_collection + + user = await users_collection.find_one({"username": username}) + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + # Находим активные бонусы с учетом бесконечных (expires_at = null) или действующих + active_bonuses = await user_bonuses_collection.find({ + "user_id": str(user["_id"]), + "is_active": True, + }).to_list(50) + + result = [] + for bonus in active_bonuses: + bonus_type = await bonus_types_collection.find_one({"id": bonus["bonus_type_id"]}) + if bonus_type: + # Рассчитываем итоговое значение эффекта с учетом уровня + level = bonus.get("level", 1) + effect_value = bonus_type["base_effect_value"] + (level - 1) * bonus_type["effect_increment"] + + bonus_data = { + "id": bonus["id"], + "name": bonus_type["name"], + "description": bonus_type["description"], + "effect_type": bonus_type["effect_type"], + "effect_value": effect_value, + "level": level, + "purchased_at": bonus["purchased_at"].isoformat(), + "can_upgrade": bonus_type["max_level"] == 0 or level < bonus_type["max_level"], + "upgrade_price": bonus_type["upgrade_price"] + } + + # Для временных бонусов добавляем срок + if bonus.get("expires_at"): + bonus_data["expires_at"] = bonus["expires_at"].isoformat() + bonus_data["time_left"] = (bonus["expires_at"] - datetime.utcnow()).total_seconds() + else: + bonus_data["is_permanent"] = True + + result.append(bonus_data) + + return {"bonuses": result} + + async def purchase_bonus(self, username: str, bonus_type_id: str): + """Покупка базового бонуса пользователем""" + from app.db.database import users_collection + + # Находим пользователя + user = await users_collection.find_one({"username": username}) + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + # Находим тип бонуса + bonus_type = await bonus_types_collection.find_one({"id": bonus_type_id}) + if not bonus_type: + raise HTTPException(status_code=404, detail="Бонус не найден") + + # Проверяем, есть ли уже такой бонус у пользователя + existing_bonus = await user_bonuses_collection.find_one({ + "user_id": str(user["_id"]), + "bonus_type_id": bonus_type_id, + "is_active": True + }) + + if existing_bonus: + raise HTTPException(status_code=400, detail="Этот бонус уже приобретен. Вы можете улучшить его.") + + # Проверяем достаточно ли монет + coins_service = CoinsService() + user_coins = await coins_service.get_balance(username) + + if user_coins < bonus_type["price"]: + raise HTTPException(status_code=400, + detail=f"Недостаточно монет. Требуется: {bonus_type['price']}, имеется: {user_coins}") + + # Создаем запись о бонусе для пользователя + bonus_id = str(uuid.uuid4()) + now = datetime.utcnow() + + # Если бонус имеет длительность + expires_at = None + if bonus_type["duration"] > 0: + expires_at = now + timedelta(seconds=bonus_type["duration"]) + + user_bonus = { + "id": bonus_id, + "user_id": str(user["_id"]), + "username": username, + "bonus_type_id": bonus_type_id, + "level": 1, # Начальный уровень + "purchased_at": now, + "expires_at": expires_at, + "is_active": True + } + + # Сохраняем бонус в БД + await user_bonuses_collection.insert_one(user_bonus) + + # Списываем монеты + await coins_service.decrease_balance(username, bonus_type["price"]) + + # Формируем текст сообщения + duration_text = "навсегда" if bonus_type["duration"] == 0 else f"на {bonus_type['duration'] // 60} мин." + message = f"Бонус '{bonus_type['name']}' успешно приобретен {duration_text}" + + return { + "status": "success", + "message": message, + "remaining_coins": user_coins - bonus_type["price"] + } + + async def upgrade_bonus(self, username: str, bonus_id: str): + """Улучшение уже купленного бонуса""" + from app.db.database import users_collection + + # Находим пользователя + user = await users_collection.find_one({"username": username}) + if not user: + raise HTTPException(status_code=404, detail="Пользователь не найден") + + # Находим бонус пользователя + user_bonus = await user_bonuses_collection.find_one({ + "id": bonus_id, + "user_id": str(user["_id"]), + "is_active": True + }) + + if not user_bonus: + raise HTTPException(status_code=404, detail="Бонус не найден или не принадлежит вам") + + # Находим тип бонуса + bonus_type = await bonus_types_collection.find_one({"id": user_bonus["bonus_type_id"]}) + if not bonus_type: + raise HTTPException(status_code=404, detail="Тип бонуса не найден") + + # Проверяем ограничение на максимальный уровень + current_level = user_bonus["level"] + if bonus_type["max_level"] > 0 and current_level >= bonus_type["max_level"]: + raise HTTPException(status_code=400, detail="Достигнут максимальный уровень бонуса") + + # Рассчитываем стоимость улучшения + upgrade_price = bonus_type["upgrade_price"] + + # Проверяем достаточно ли монет + coins_service = CoinsService() + user_coins = await coins_service.get_balance(username) + + if user_coins < upgrade_price: + raise HTTPException(status_code=400, + detail=f"Недостаточно монет. Требуется: {upgrade_price}, имеется: {user_coins}") + + # Обновляем уровень бонуса + new_level = current_level + 1 + + await user_bonuses_collection.update_one( + {"id": bonus_id}, + {"$set": {"level": new_level}} + ) + + # Списываем монеты + await coins_service.decrease_balance(username, upgrade_price) + + # Рассчитываем новое значение эффекта + new_effect_value = bonus_type["base_effect_value"] + (new_level - 1) * bonus_type["effect_increment"] + + return { + "status": "success", + "message": f"Бонус '{bonus_type['name']}' улучшен до уровня {new_level}", + "new_level": new_level, + "effect_value": new_effect_value, + "remaining_coins": user_coins - upgrade_price + } diff --git a/main.py b/main.py index 6297231..c0d7188 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,6 @@ from fastapi import FastAPI from fastapi.staticfiles import StaticFiles -from app.api import users, skins, capes, meta, server, store, pranks, marketplace +from app.api import users, skins, capes, meta, server, store, pranks, marketplace, bonuses from fastapi.middleware.cors import CORSMiddleware app = FastAPI() @@ -13,11 +13,12 @@ app.include_router(server.router) app.include_router(store.router) app.include_router(pranks.router) app.include_router(marketplace.router) +app.include_router(bonuses.router) # Монтируем статику -app.mount("/skins", StaticFiles(directory="/app/static/skins"), name="skins") -app.mount("/capes", StaticFiles(directory="/app/static/capes"), name="capes") -app.mount("/capes_store", StaticFiles(directory="/app/static/capes_store"), name="capes_store") +app.mount("/skins", StaticFiles(directory="app/static/skins"), name="skins") +app.mount("/capes", StaticFiles(directory="app/static/capes"), name="capes") +app.mount("/capes_store", StaticFiles(directory="app/static/capes_store"), name="capes_store") # CORS, middleware и т.д. app.add_middleware( diff --git a/scripts/add_test_bonuses.py b/scripts/add_test_bonuses.py new file mode 100644 index 0000000..a201234 --- /dev/null +++ b/scripts/add_test_bonuses.py @@ -0,0 +1,54 @@ +# scripts/add_test_bonuses.py +from app.db.database import db +import uuid +from datetime import datetime + +# Коллекция для бонусов +bonus_types_collection = db.bonus_types + +# Очищаем существующие записи +bonus_types_collection.delete_many({}) + +# Добавляем типы бонусов +bonus_types = [ + { + "id": str(uuid.uuid4()), + "name": "Бонус опыта", + "description": "Увеличивает получаемый опыт на 100% (+10% за уровень)", + "effect_type": "experience", + "base_effect_value": 1.0, # +100% + "effect_increment": 0.1, # +10% за уровень + "price": 100, + "upgrade_price": 50, + "duration": 0, # Бесконечный + "max_level": 0 # Без ограничения уровня + }, + { + "id": str(uuid.uuid4()), + "name": "Бонус силы", + "description": "Увеличивает силу атаки на 10% (+5% за уровень)", + "effect_type": "strength", + "base_effect_value": 0.1, + "effect_increment": 0.05, + "price": 75, + "upgrade_price": 40, + "duration": 0, # Бесконечный + "max_level": 10 # Максимум 10 уровней + }, + { + "id": str(uuid.uuid4()), + "name": "Бонус скорости", + "description": "Временно увеличивает скорость передвижения на 20%", + "effect_type": "speed", + "base_effect_value": 0.2, + "effect_increment": 0.05, + "price": 40, + "upgrade_price": 30, + "duration": 1800, # 30 минут + "max_level": 5 + } +] + +# Вставляем бонусы в БД +bonus_types_collection.insert_many(bonus_types) +print(f"Добавлено {len(bonus_types)} типов бонусов")