Luck Pool
1. Summary
Goal: Поощрение активных игроков через систему вероятностного буста. Игроки, активно собирающие материалы для крафта, получают увеличенные шансы на нужные компоненты.
User Value: Активные игроки (50%+ прогресс крафта) получают × 3-13 увеличение шансов на материалы для целевых скинов. Чем дольше в пуле — тем выше буст.
2. Business Logic
Pool Mechanics
- Вход в пул
- Seniority (Стаж)
- После крафта
- AFK выход
Условие: Прогресс крафта любого скина >= 50%
Базовый буст: × 3.0 к весам FRAGMENT/BLUEPRINT
Автоматически: Cron job проверяет каждые 12 часов
progress = collectedMaterials / totalMaterials
Где totalMaterials — сумма всех материалов в рецепте крафта скина.
Формула: seniorityMultiplier = 1.2^(activePeriods - 1)
За каждый активный период в пуле множитель растёт:
| Периоды | Seniority | Total Boost |
|---|---|---|
| 1 (новичок) | 1.0× | 3.0× |
| 2 | 1.2× | 3.6× |
| 3 | 1.44× | 4.32× |
| 4 | 1.73× | 5.18× |
| 5 | 2.07× | 6.22× |
| 6 | 2.49× | 7.46× |
| 7 | 2.99× | 8.96× |
| 8 | 3.58× | 10.75× |
| 9 (max) | 4.30× | 12.9× |
Условие роста: Нужно быть активным в течение периода (10 дней).
Что происходит при крафте скина:
- Выход из пула до конца текущего периода (
canReenterAfter) - Сброс seniority (
activePeriods = 1) - Блокировка скинов >= 50% до конца месяца (
blockedSkinIds)
Seniority сбрасывается чтобы предотвратить накопление буста без крафта. Блокировка скинов не даёт сразу начать собирать тот же скин с бустом.
14+ дней неактивности:
- Удаление из пула (
isActive = false) - Seniority сохраняется (можно вернуться с тем же стажем)
Определение активности: Открытие кейсов, спины, streak bonus
Boost Application
boostedWeight = originalWeight × baseMultiplier × seniorityMultiplier
Где baseMultiplier = 3.0, seniorityMultiplier = 1.2^(activePeriods - 1)
Что бустится:
- FRAGMENT для скинов в пуле
- BLUEPRINT для скинов в пуле
- НЕ бустится: SCRAP, XP, другие награды
Где применяется:
- Cases —
CaseOpeningService.applyBoostToWeights() - Daily Spins —
UserSpinService.applyBoostToWeights()
Пример применения
// Игрок в пуле: activePeriods = 3, скин "AWP Dragon Lore" >= 50%
// boostConfig.totalMultiplier = 3.0 × 1.44 = 4.32
// Награда: Fragment для AWP Dragon Lore
// originalWeight = 100
// boostedWeight = 100 × 4.32 = 432
// Шанс выпадения вырос в 4.32 раза!
Protection
| Действие | Rate Limit | Auth | Validation |
|---|---|---|---|
| GET /entries | general | Admin JWT | — |
| GET /stats | general | Admin JWT | — |
| GET /user/:userId | general | Admin JWT | userId param |
| POST /process | mutations | Admin JWT | — |
См. Security Matrix для полного обзора защит.
Edge Cases
| Ситуация | Поведение |
|---|---|
| Игрок достиг 50% | Автоматически добавляется в пул (cron 12ч) |
| Игрок скрафтил скин | Выход из пула, сброс seniority, блокировка скинов |
| 14 дней AFK | Выход из пула, seniority сохраняется |
| Все скины заблокированы | Игрок не может войти в пул до снятия блокировки |
| Конец месяца | blockedSkinIds очищаются |
| Конец периода | activePeriods++ для активных игроков |
| Вход после крафта | Только после canReenterAfter и при наличии незаблокированных скинов |
3. ADR (Architectural Decisions)
Почему буст только для FRAGMENT/BLUEPRINT?
Проблема: Буст всех наград инфлирует экономику. Игрок получает больше Scrap/XP просто за нахождение в пуле.
Решение: Бустить только материалы для целевых скинов.
Альтернативы (отклонены):
- Буст всех наград — инфляция, нарушает баланс
- Буст только Blueprint — слишком узко, не мотивирует
Последствия: Игрок в пуле быстрее собирает конкретный скин, но не получает экономических преимуществ.
Почему Seniority сбрасывается при крафте?
Проблема: Игрок может накопить × 12.9 буст и никогда не крафтить, только собирая материалы.
Решение: Сброс activePeriods = 1 после каждого крафта.
Альтернативы (отклонены):
- Не сбрасывать — приводит к бесконечному накоплению
- Частичный сброс (÷2) — усложняет логику без выгоды
Последствия: Мотивация крафтить регулярно, а не копить буст. Защита от злоупотреблений.
Почему AFK сохраняет seniority?
Проблема: Игрок уезжает в отпуск на 3 недели, теряет весь стаж.
Решение: AFK удаляет из пула, но сохраняет activePeriods.
Альтернативы (отклонены):
- Полный сброс — слишком жёстко, игроки уйдут
- Уменьшение за каждую неделю AFK — сложная логика
Последствия: Игроки могут вернуться без потери прогресса. Retention выше.
4. Architecture
Integration Overview
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| LuckPoolService | backend/src/domains/luck-pool/services/luck-pool.service.ts | Core business logic (535 lines) |
| LuckPoolRepository | backend/src/domains/luck-pool/repositories/luck-pool.repository.ts | Database operations |
| LuckPoolAdminController | backend/src/domains/luck-pool/controllers/luck-pool-admin.controller.ts | Admin HTTP handlers |
| Admin Routes | backend/src/domains/luck-pool/routes/admin-luck-pool.routes.ts | Route registration |
| Schemas | backend/src/domains/luck-pool/schemas/luck-pool.schemas.ts | Fastify JSON schemas |
| Types | backend/src/domains/luck-pool/types/luck-pool.types.ts | TypeScript interfaces |
| Constants | shared/src/constants/budgetControl.ts | All system constants |
Константы системы
BUDGET_CONTROL = {
// Pool Configuration
MIN_PROGRESS_FOR_POOL: 0.5, // 50%
INACTIVE_DAYS_THRESHOLD: 14, // Days for AFK detection
POOL_CHECK_INTERVAL_HOURS: 12, // Cron job interval
// Boost
BASE_BOOST_MULTIPLIER: 3.0,
// Seniority
SENIORITY: {
MULTIPLIER_PER_PERIOD: 1.2,
MAX_ACTIVE_PERIODS: 9,
},
// Blocked Skins
BLOCKED_SKINS: {
CLEAR_INTERVAL_PERIODS: 3, // Clear after 3 periods (1 month)
},
// Activity Definition
ACTIVITY_ACTIONS: [
'CASE_OPENED',
'SPIN_USED',
'STREAK_BONUS_CLAIMED',
],
}
Service Methods
| Метод | Описание | Вызывается из |
|---|---|---|
getUserPoolStatus() | Статус участия и буст | Admin API, Frontend |
getBoostConfig() | Конфиг буста для RNG | CaseOpeningService, UserSpinService |
isUserInPool() | Простая проверка | Internal |
addToPool() | Добавить в пул | processPoolUpdates cron |
handleCraft() | Обработка крафта | CraftService |
markUserInactive() | AFK удаление | processPoolUpdates cron |
updateActivity() | Обновить активность | CaseOpeningService, UserSpinService |
processPoolUpdates() | Cron: новые, AFK, reentry | SeasonLifecycleJob |
handlePeriodChange() | Cron: increment seniority | SeasonLifecycleJob |
clearExpiredBlockedSkins() | Cron: очистка блокировок | SeasonLifecycleJob |
Admin Notification (TG Alert)
После выполнения processPoolUpdates() отправляется уведомление администратору:
| Событие | Тип | Когда отправляется |
|---|---|---|
| Обновление пула | POOL_UPDATE | После каждой обработки пула (каждые 12 часов) |
Формат уведомления
📊 Обновление Luck Pool
Сезон: {seasonNumber}
Добавлено в пул: +{added}
Удалено (AFK): -{removedAFK}
Вернулись после крафта: +{reenteredAfterCraft}
Всего в пуле: {totalInPool}
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| LuckPoolEntry | Участник пула | activePeriods, boostMultiplier, isActive, blockedSkinIds |
LuckPoolEntry Fields
| Поле | Тип | Описание |
|---|---|---|
id | String | Primary key (CUID) |
userId | String | FK to User |
seasonId | String | FK to Season |
activePeriods | Int | Количество активных периодов (seniority) |
boostMultiplier | Float | Текущий множитель (BASE × seniority) |
enteredAt | DateTime | Когда впервые вошёл в пул |
lastActiveAt | DateTime | Последняя активность |
isActive | Boolean | Текущий статус в пуле |
lastCraftAt | DateTime? | Когда последний раз крафтил |
canReenterAfter | DateTime? | Когда можно вернуться после крафта |
blockedSkinIds | String[] | Заблокированные скины (≥50% при крафте) |
blockedUntil | DateTime? | Когда истекает блокировка |
Relationships
Indexes
@@unique([userId, seasonId])— один entry на user/season@@index([seasonId, isActive])— быстрый поиск активных@@index([canReenterAfter])— для reentry cron
6. API Endpoints
Luck Pool не имеет user-facing API. Все эндпоинты только для админ-панели.
| Метод | Эндпоинт | Описание |
|---|---|---|
| GET | /admin/luck-pool/entries | Все участники пула |
| GET | /admin/luck-pool/stats | Статистика пула |
| GET | /admin/luck-pool/user/:userId | Статус пользователя |
| POST | /admin/luck-pool/process | Ручной запуск обработки |
Response Examples
GET /admin/luck-pool/stats
{
"success": true,
"data": {
"total": 150,
"active": 120,
"avgSeniority": 3.4,
"avgBoost": 5.8
}
}
GET /admin/luck-pool/user/:userId
{
"success": true,
"data": {
"isInPool": true,
"entry": {
"id": "clxx...",
"userId": "user123",
"seasonId": "season456",
"activePeriods": 3,
"boostMultiplier": 4.32,
"enteredAt": "2024-01-15T10:00:00Z",
"lastActiveAt": "2024-01-20T15:30:00Z",
"isActive": true,
"blockedSkinIds": [],
"blockedUntil": null
},
"eligibleSkins": [
{
"skinId": "item123",
"skinName": "AWP Dragon Lore",
"progress": 0.65,
"isBlocked": false,
"totalMaterials": 100,
"collectedMaterials": 65
}
],
"boostMultiplier": 4.32,
"seniorityMultiplier": 1.44
}
}
7. Related
- Budget Control — общий контекст Budget Control System
- Cases — использует Luck Pool для буста материалов
- Daily Spins — использует Luck Pool для буста материалов
- Craft — триггерит выход из пула
- Seasons — контекст для периодов и пула
- Inventory — прогресс крафта из инвентаря