Daily Spins
1. Summary
Goal: Retention и engagement через систему рулеток. Бесплатный ежедневный спин — причина заходить каждый день. Платные спины — sink для внутренней валюты.
User Value: Получение наград (Scrap, XP, предметы для крафта) с гарантированным бесплатным ежедневным входом. Путь: Активность → Daily Spin → Награды → Крафт.
2. Business Logic
Types of Spins
- Daily Free
- Paid (Scrap)
- Paid (Streak Points)
- Special
Доступ: Бесплатный, один раз в сутки
Cooldown: Регулируется DailySpin.cooldownHours (по умолчанию 24ч)
Tracking: Per-spin cooldown через SpinResult.spunAt
Цель: Retention — причина заходить каждый день
Доступ: За валюту Scrap (priceScrap > 0)
Cooldown: Обычно без ограничений, пока есть баланс
Цель: Sink — drain внутренней экономики
Доступ: За Streak Points (currencyType: STREAK_POINTS)
Mechanic: Поощряет удержание серии ежедневных входов
Цель: Дополнительная ценность для лояльных игроков
Доступ: Ограничен периодом (availableFrom / availableTo)
Mechanic: Event-рулетки на праздники, спецакции
Цель: Создание срочности и excitement
Spinning Mechanics
Алгоритм спина (реализован в UserSpinService):
1. Validation
- Спин должен быть активен (
isActive: true) - Проверка периода доступности (для Special)
- Проверка per-spin cooldown
2. Payment Strategy
- Если
priceScrap = 0иpricePoints = null→ бесплатный спин - Если
currencyType = SCRAP→ проверкаuser.scrap >= priceScrap - Если
currencyType = STREAK_POINTS→ проверкаuser.streakPoints >= pricePoints - Всё в одной atomic transaction
3. RNG & Boost
Weighted Random выборка: P(item) = weight / sum(weights)
Luck Pool увеличивает шансы FRAGMENT/BLUEPRINT для активных игроков (× 3-13).
4. Reward Distribution
- SCRAP: Начисляется на баланс + применяется
SCRAP_BUFFесли активен - XP: Начисляется + применяется
XP_BUFFесли активен - ITEM: Добавляется в инвентарь с
sourceType: DAILY_SPIN
5. Side Effects
- Запись в
SpinResultс snapshot награды - Публикация в Live Feed (если награда значительная)
- Passive Income для рефереров (если Scrap)
- Обновление Season Stats
Edge Cases
Что видит пользователь (UI):
| Ситуация | UI поведение |
|---|---|
| ❌ Баланс < цены | Кнопка disabled, показ недостающей суммы |
| ⏱️ Cooldown активен | Таймер обратного отсчёта |
| 📅 Вне периода | Спин скрыт или показана дата начала |
| 🎰 Несколько спинов | Карусель/список активных спинов |
Backend Error Codes (для API/тестов)
| Код | HTTP | Сообщение |
|---|---|---|
SPIN_NOT_FOUND | 404 | "Рулетка не найдена" |
SPIN_NOT_AVAILABLE | 400 | "Рулетка недоступна в данный период" |
INSUFFICIENT_BALANCE | 400 | "Недостаточно Scrap" |
INSUFFICIENT_STREAK_POINTS | 400 | "Недостаточно Streak Points" |
COOLDOWN_ACTIVE | 400 | "Спин на кулдауне. Попробуйте через X минут" |
3. ADR (Architectural Decisions)
Почему cooldownHours на DailySpin, а не SpinType?
Проблема: Несколько спинов могут иметь разные кулдауны. Daily = 24ч, Paid = 0ч.
Решение: Кулдаун управляется на уровне DailySpin.cooldownHours, не SpinType.
Альтернативы (отклонены):
- SpinType.cooldownHours — не поддерживает разные кулдауны для спинов одного типа
- Глобальный User.lastDailySpin — не поддерживает multi-spin
Последствия: Гибкость настройки, но требует проверки кулдауна per-spin.
Почему Per-Spin Cooldown Tracking?
Пользователь может крутить разные рулетки параллельно. Нельзя блокировать все спины одним таймером.
Решение: Кулдаун проверяется через SpinResult.spunAt для конкретной пары (userId, spinId).
14:00 — пользователь крутит Рулетка #1
14:05 — пользователь крутит Рулетка #2 ✅ (разные спины)
14:25 — пользователь пытается крутить Рулетка #1 ❌ (кулдаун)
Почему RewardSnapshot в SpinResult?
Проблема: Награды могут быть удалены/изменены, но история должна показывать что выиграл пользователь.
Решение: JSON snapshot с полными данными награды на момент спина.
Формат RewardSnapshot
{
"id": "reward-id",
"type": "SCRAP",
"name": "500 Scrap",
"amount": 500,
"itemId": null,
"itemName": null,
"itemImageUrl": null,
"itemTier": null,
"buffBonus": {
"type": "SCRAP_BUFF",
"baseAmount": 500,
"bonusAmount": 150,
"multiplier": 1.3
}
}
4. Architecture
Services Overview
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| UserSpinService | backend/src/domains/cases/services/user-spin.service.ts | Основная логика спина |
| AdminSpinService | backend/src/domains/cases/services/admin-spin.service.ts | CRUD + статистика |
| DailySpinRepository | backend/src/domains/cases/repositories/daily-spin.repository.ts | Data Access |
| User Routes | backend/src/domains/cases/routes/user-spin.routes.ts | User API |
| Admin Routes | backend/src/domains/cases/routes/admin-daily-spins.routes.ts | Admin API |
| Schemas | backend/src/domains/cases/schemas/user-spin.schemas.ts | Валидация |
Код daily-spin находится в domains/cases вместе с кейсами — общая RNG механика.
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| SpinType | Группировка спинов | slug, isDailyFree, cooldownHours |
| DailySpin | Конфигурация рулетки | spinTypeId, priceScrap, currencyType, cooldownHours |
| SpinItem | Сектор рулетки | spinId, rewardId, weight, displayDropChance |
| SpinResult | История спинов | userId, spinId, rewardSnapshot, spunAt |
Relationships
6. API Endpoints
- User API
- Admin: Management
- Admin: Items
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /api/daily-spin/list | Все активные спины (карусель) | → |
| GET | /api/daily-spin/:spinId/check-cooldown | Проверка кулдауна и баланса | → |
| POST | /api/daily-spin/:spinId/spin | Выполнить спин | → |
| GET | /api/daily-spin/history | История спинов пользователя | → |