Buffs System
1. Summary
Goal: Механизм временных бонусов, активируемых из инвентаря. Игрок получает предметы-баффы как награды, активирует их и получает усиленные награды или защиту стрика.
User Value: Возможность усилить прогресс в нужный момент (+25-100% к XP или Scrap) или застраховать стрик от пропуска дня.
Источники получения BUFF:
- Кейсы (Cases)
- Рулетка (Daily Spins)
- Квесты (Quests)
- Достижения (Achievements)
- Промокоды (Promo Codes)
Путь: Награда → Предмет BUFF в инвентаре → Активация → Временный бонус.
2. Business Logic
Types of Buffs
- XP Catalyst
- Scrap Catalyst
- Streak Shield
Эффект: Множитель получаемого XP с кейсов и рулетки
Тиры и множители:
| Tier | Множитель | Эффект |
|---|---|---|
| TIER_1 | ×1.25 | +25% XP |
| TIER_2 | ×1.50 | +50% XP |
| TIER_3 | ×2.00 | +100% XP |
Длительность: 30 минут по умолчанию (настраивается админом при создании предмета)
Offline: Таймер тикает всегда (даже когда приложение закрыто)
Где работает: Только кейсы и рулетка (квесты, ачивки, рефералы — фиксированные награды)
Цель: Ускорение прогресса для активных игроков
Эффект: Множитель получаемого Scrap с кейсов и рулетки
Тиры и множители:
| Tier | Множитель | Эффект |
|---|---|---|
| TIER_1 | ×1.25 | +25% Scrap |
| TIER_2 | ×1.50 | +50% Scrap |
| TIER_3 | ×2.00 | +100% Scrap |
Длительность: 30 минут по умолчанию (настраивается админом при создании предмета)
Offline: Таймер тикает всегда (даже когда приложение закрыто)
Где работает: Только кейсы и рулетка (квесты, ачивки, рефералы — фиксированные награды)
Цель: Накопление валюты для открытия кейсов
Эффект: Защита стрика от сброса при пропуске дня
Механика: При пропуске дня система автоматически использует shield (1 shield = 1 день защиты)
Лимиты:
| Параметр | Значение | Константа |
|---|---|---|
| Max активных shields | 3 | STREAK_SHIELD_LIMITS.maxActiveShields |
| Защит на shield | 1 | STREAK_SHIELD_LIMITS.usesPerShield |
Длительность: Бессрочно (пока не использован)
Цель: Страховка для активных игроков с длинными стриками


Activation Mechanics
Алгоритм активации (реализован в BuffService.activateBuff()):
1. Validation
- Предмет в инвентаре (
UserInventory) принадлежит пользователю - Тип предмета:
BUFF(Item.itemType) - У предмета указан
buffType
2. Limit Check (только для STREAK_SHIELD)
- Проверка суммы
usesLeftвсех активных shields:totalShieldUses < 3
3. Stacking Strategy
- XP_BUFF / SCRAP_BUFF: Если активен бафф того же типа и того же тира → продлевается время
- Разные тиры: Ошибка "Активен бафф другого уровня. Дождитесь окончания текущего баффа."
- STREAK_SHIELD: Добавляет +1 use к существующей записи или создаёт новый shield
4. Atomic Transaction
- Уменьшение
quantityв инвентаре (или удаление записи еслиquantity <= 1) - Создание/продление записи в
UserActiveBuff - Всё в одной транзакции Prisma
5. Extension Logic
Для временных баффов (XP/SCRAP):
newExpiresAt = max(currentExpiresAt, now) + durationMinutes
6. Event Logging
- После успешной транзакции создаётся запись в
BuffEventс типомACTIVATIONилиEXTENSION
Buff Application
Механика применения множителя при получении награды:
Бафф проверяется в момент нажатия (открытие кейса / спин), а не в момент выдачи награды. Если бафф был активен при нажатии — бонус применяется, даже если истёк во время анимации.
XP_BUFF:
finalXP = baseXP × buffMultiplier
bonusXP = finalXP - baseXP
Применяется централизованно через addUserXPWithBuff() в common/utils/xp.service.ts.
SCRAP_BUFF:
finalScrap = baseScrap × buffMultiplier
bonusScrap = finalScrap - baseScrap
Применяется в CaseOpeningService и UserSpinService.
- Salvage — переработка предметов (можно накопить скрап, пока бафф активен)
- Квесты/ачивки — фиксированные награды
- Рефералы — пассивный доход
STREAK_SHIELD:
shieldsToUse = min(daysSkipped, totalShieldUses, 3)
Применяется автоматически в StreakService при обнаружении пропуска.
Event Types
| EventType | Описание | Записывается при |
|---|---|---|
ACTIVATION | Активация нового баффа | BuffService.activateBuff() |
EXTENSION | Продление существующего баффа | BuffService.activateBuff() (если бафф того же тира активен) |
APPLICATION | Применение множителя к награде | CaseOpeningService, UserSpinService |
SHIELD_USE | Автоматическое использование shield | StreakService.claimDaily() |
Protection
| Действие | Rate Limit | Auth | Validation |
|---|---|---|---|
| Get active buffs | general (100/min) | Telegram | GetActiveBuffsSchema |
| Get buff history | general (100/min) | Telegram | GetBuffHistorySchema |
| Activate buff | mutations (5/min) | Telegram | ActivateBuffSchema |
См. Security Matrix для полного обзора защит.
Edge Cases
Что видит пользователь (UI):
| Ситуация | UI поведение |
|---|---|
| Нет баффов в инвентаре | Секция баффов скрыта или пустая |
| Активен бафф другого тира | В модалке активации: кнопка disabled, жёлтый блок с таймером до окончания текущего баффа |
| Max shields (3 uses) | Кнопка disabled, tooltip "Достигнут лимит активных щитов" |
| Shield автоматически использован | Push-уведомление "Streak Shield защитил вашу серию!" |
| Бафф применён к награде | В модале награды показывается бонус с цветом тира |

Backend Error Codes (для API/тестов)
| Код | HTTP | Сообщение |
|---|---|---|
ITEM_NOT_FOUND | 400 | "Item not found in inventory" |
NOT_A_BUFF | 400 | "Item is not a BUFF" |
NO_BUFF_TYPE | 400 | "Item has no buffType" |
TIER_MISMATCH | 400 | "Активен бафф другого уровня. Дождитесь окончания текущего баффа." |
MAX_SHIELDS | 400 | "Maximum 3 active Streak Shields allowed" |
FORBIDDEN | 403 | "Item does not belong to user" |
Сценарии использования Streak Shield
Сценарий 1: Обычное продление стрика
День 1: Вход, streak = 1
День 2: Вход, streak = 2
→ Shields не используются
Сценарий 2: Пропуск 1 дня с shield
День 1: Вход, streak = 5, shields = 1
День 3: Вход (пропущен день 2)
→ 1 shield использован
→ Streak = 6 (продолжен)
→ Shields = 0
Сценарий 3: Пропуск 3 дней, 2 shields
День 1: Вход, streak = 10, shields = 2
День 5: Вход (пропущены дни 2,3,4)
→ Нужно защитить 3 пропуска
→ Есть только 2 shields
→ Shields использованы: 2
→ Streak СБРОШЕН (shields недостаточно)
Сценарий 4: Shield не защищает от Luck Pool AFK
День 1: Вход, в Luck Pool, shields = 3
День 15: Вход (пропущены дни 2-14)
→ 3 shields использованы (защитили 3 из 13 пропусков)
→ Streak СБРОШЕН (недостаточно shields)
→ Luck Pool: игрок УДАЛЁН (14 дней AFK)
3. ADR (Architectural Decisions)
Почему баффы одного тира стакаются, а разных — нет?
Проблема: Как обрабатывать активацию второго баффа того же типа?
Решение: Баффы одного типа и тира продлевают время, разных тиров — ошибка.
Альтернативы (отклонены):
- Стакать любые баффы (множители складываются) — слишком мощно, ломает экономику
- Заменять текущий бафф новым — плохой UX, теряется оставшееся время
- Очередь баффов — сложная логика, неинтуитивный UI
Последствия: Простая и понятная механика. Пользователь сам решает, когда активировать бафф. Trade-off: нельзя "копить" баффы разных тиров.
Почему STREAK_SHIELD не имеет времени истечения?
Проблема: Streak Shield должен защитить от случайного пропуска, но время пропуска непредсказуемо.
Решение: Shield не истекает по времени, а тратится по событию (при обнаружении пропуска в StreakService).
Shield расходуется автоматически в StreakService при проверке стрика. Пользователь не выбирает, использовать ли shield — это происходит мгновенно при первом запросе после пропуска.
Последствия: Справедливая защита от непредвиденных ситуаций. Лимит в 3 shields (сумма usesLeft) предотвращает накопление "вечной" защиты.
Почему активация потребляет предмет атомарно?
Проблема: Race condition — два параллельных запроса могут оба "увидеть" предмет и попытаться его использовать.
Решение: Prisma $transaction для atomic operations: декремент quantity + создание buff.
Последствия: Гарантированная консистентность. Невозможно активировать больше баффов, чем есть предметов.
Почему STREAK_SHIELD не защищает от Luck Pool AFK?
Проблема: Игрок с 3 shields может пропустить 3 дня и сохранить стрик. Должен ли он сохранить место в Luck Pool?
Решение: Shield защищает только streak, но НЕ влияет на AFK-статус в Luck Pool.
Независимо от количества shields, если игрок не заходил 14 дней — он удаляется из Luck Pool. Это справедливо: пул для активных игроков.
Альтернативы (отклонены):
- Shield также защищает от AFK — даёт нечестное преимущество накопителям shields
Последствия: Shields полезны для сохранения стрика при коротких пропусках (1-3 дня), но не дают преимущества AFK-ерам в Luck Pool.
Почему BuffEvent хранит историю отдельно?
Проблема: Нужна история использования баффов, но UserActiveBuff — это текущее состояние.
Решение: Отдельная таблица BuffEvent для immutable логов событий.
Альтернативы (отклонены):
- Хранить историю в
UserActiveBuffс soft delete — засоряет таблицу, сложные запросы - Логировать только в analytics — нет возможности показать в UI
Последствия: Чистое разделение: UserActiveBuff для текущего состояния, BuffEvent для истории. Возможность показать историю в профиле.
4. Architecture
Services Overview
Buff Application Flow
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| BuffService | backend/src/domains/buffs/services/buff.service.ts | Оркестратор активации и применения |
| BuffRepository | backend/src/domains/buffs/repositories/buff.repository.ts | CRUD операции с UserActiveBuff и BuffEvent |
| BuffController | backend/src/domains/buffs/controllers/buff.controller.ts | HTTP handlers |
| Routes | backend/src/domains/buffs/routes/buff.routes.ts | User API endpoints |
| Schemas | backend/src/domains/buffs/schemas/buff.schemas.ts | Валидация и Swagger |
| Constants | shared/src/constants/buff.constants.ts | BUFF_DURATION_MINUTES, STREAK_SHIELD_LIMITS |
Integration Points
| Сервис-потребитель | Метод | Описание |
|---|---|---|
| XPService | getBuffMultiplierWithName('XP_BUFF') | Применение множителя XP с получением названия баффа |
| CaseOpeningService | getBuffMultiplierWithName('SCRAP_BUFF') | Применение множителя Scrap |
| UserSpinService | getBuffMultiplierWithName('SCRAP_BUFF') | Применение множителя Scrap |
| StreakService | useStreakShields(daysSkipped) | Автоматическое использование shields |
XP_BUFF применяется через addUserXPWithBuff() в common/utils/xp.service.ts, а не напрямую в domain services.
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| UserActiveBuff | Активные баффы пользователя | userId, buffType, multiplier, expiresAt, usesLeft, sourceItemId |
| BuffEvent | История событий баффов | userId, buffType, eventType, multiplier, baseAmount, bonusAmount |
| Item | Предмет-источник баффа | itemType: BUFF, buffType, buffMultiplier, buffDurationMinutes |
| UserInventory | Инвентарь пользователя | userId, itemId, quantity |
UserActiveBuff Fields
| Поле | Тип | Описание |
|---|---|---|
buffType | BuffType | XP_BUFF, SCRAP_BUFF, STREAK_SHIELD |
multiplier | Float? | 1.25 / 1.5 / 2.0 (null для STREAK_SHIELD) |
expiresAt | DateTime? | Время истечения (null для STREAK_SHIELD) |
usesLeft | Int? | Оставшиеся использования (только STREAK_SHIELD) |
sourceItemId | String? | ID Item для аналитики и UI |
BuffEvent Fields
| Поле | Тип | Описание |
|---|---|---|
buffType | BuffType | Тип баффа |
eventType | BuffEventType | ACTIVATION, EXTENSION, APPLICATION, SHIELD_USE |
activeBuffId | String? | Ссылка на UserActiveBuff |
multiplier | Float? | Множитель (для ACTIVATION/EXTENSION) |
expiresAt | DateTime? | Время истечения (для ACTIVATION/EXTENSION) |
sourceType | String? | 'case' или 'spin' (для APPLICATION) |
sourceId | String? | ID caseOpening или spinResult (для APPLICATION) |
baseAmount | Int? | Базовая награда до баффа (для APPLICATION) |
bonusAmount | Int? | Бонус от баффа (для APPLICATION) |
daysProtected | Int? | Сколько дней защитил shield (для SHIELD_USE) |
streakBefore | Int? | Стрик до использования shield (для SHIELD_USE) |
Relationships
6. API Endpoints
- User API
- Internal (Service-to-Service)
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /api/buffs/active | Список активных баффов | → |
| GET | /api/buffs/history | История событий баффов с пагинацией | → |
| POST | /api/buffs/activate | Активировать бафф из инвентаря | → |
Внутренние методы BuffService вызываются другими сервисами напрямую:
| Метод | Вызывается из | Описание |
|---|---|---|
getBuffMultiplier(userId, buffType) | XPService | Получить множитель (только число) |
getBuffMultiplierWithName(userId, buffType) | CaseOpeningService, UserSpinService | Получить множитель + название + tier |
useStreakShields(userId, daysSkipped) | StreakService | Автоматическое использование shields |
getTotalShieldUses(userId) | StreakController | Количество доступных shield uses |
getShieldUsageHistory(userId, streakStartDate) | StreakController | История использования shields в текущем стрике |
Response Examples
GET /api/buffs/active
{
"success": true,
"data": [
{
"id": "clx...",
"buffType": "XP_BUFF",
"multiplier": 1.5,
"activatedAt": "2024-01-15T10:00:00.000Z",
"expiresAt": "2024-01-15T10:30:00.000Z",
"usesLeft": null,
"remainingSeconds": 1234
},
{
"id": "cly...",
"buffType": "STREAK_SHIELD",
"multiplier": null,
"activatedAt": "2024-01-14T08:00:00.000Z",
"expiresAt": null,
"usesLeft": 2,
"remainingSeconds": null
}
]
}
GET /api/buffs/history?page=1&limit=20&buffType=XP_BUFF
{
"success": true,
"data": {
"events": [
{
"id": "evt...",
"buffType": "XP_BUFF",
"eventType": "APPLICATION",
"multiplier": 1.5,
"sourceType": "case",
"sourceId": "case123",
"baseAmount": 100,
"bonusAmount": 50,
"createdAt": "2024-01-15T10:15:00.000Z"
}
],
"totalCount": 45,
"page": 1,
"limit": 20,
"totalPages": 3
}
}
Data Lifecycle
- Активные баффы: Фильтруются по
expiresAt > nowилиusesLeft > 0вgetActiveBuffs() - Истёкшие баффы: Остаются в
UserActiveBuffкак история текущего сезона - BuffEvent: Immutable log, не удаляется (кроме сезонного reset)
- Очистка: Все записи удаляются при смене сезона в
SeasonResetService
Баффы очищаются вместе с остальными данными (inventory, quests и т.д.) при сезонном reset.
7. Related
- Streaks — STREAK_SHIELD защищает стрик от сброса
- Cases — Баффы выпадают из кейсов как награды
- Daily Spins — Множители XP/SCRAP применяются к наградам
- Inventory — Баффы хранятся как предметы до активации
- Security Matrix — Матрица защит для buffs endpoints