Skip to main content

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 с кейсов и рулетки

Тиры и множители:

TierМножительЭффект
TIER_1×1.25+25% XP
TIER_2×1.50+50% XP
TIER_3×2.00+100% XP

Длительность: 30 минут по умолчанию (настраивается админом при создании предмета)

Offline: Таймер тикает всегда (даже когда приложение закрыто)

Где работает: Только кейсы и рулетка (квесты, ачивки, рефералы — фиксированные награды)

Цель: Ускорение прогресса для активных игроков

Активные баффы в профиле
Активные баффы в профиле: XP Catalyst (+100%) и Scrap Catalyst (+50%) с таймерами
Streak Shield в модалке стрика
Streak Shield (3/3 щитов) отображается в модалке стрика

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
Предусловие

expiresAt активного баффа не должен быть null. Если expiresAt === null при попытке продления — сервис выбрасывает ошибку. На практике это невозможно: временные баффы (XP/SCRAP) всегда создаются с expiresAt, а null используется только для STREAK_SHIELD, который не продлевается по этому пути.

6. Event Logging

  • После успешной транзакции создаётся запись в BuffEvent с типом ACTIVATION или EXTENSION

Buff Application

Механика применения множителя при получении награды:

Момент проверки

Бафф проверяется в момент нажатия (открытие кейса / спин), а не в момент выдачи награды. Если бафф был активен при нажатии — бонус применяется, даже если истёк во время анимации.

XP_BUFF:

finalXP = baseXP × buffMultiplier
bonusXP = finalXP - baseXP

Применяется централизованно через addUserXPWithBuff() в backend/src/domains/users/services/xp.service.ts.

SCRAP_BUFF:

finalScrap = baseScrap × buffMultiplier
bonusScrap = finalScrap - baseScrap

Применяется централизованно через addScrapWithBuff() в backend/src/domains/users/services/scrap.service.ts. CaseOpeningService и UserSpinService вызывают addScrapWithBuff(), который внутри обращается к getBuffMultiplierWithName('SCRAP_BUFF').

НЕ влияет на
  • Salvage — переработка предметов (можно накопить скрап, пока бафф активен)
  • Квесты/ачивки — фиксированные награды
  • Рефералы — пассивный доход

STREAK_SHIELD:

shieldsToUse = min(daysSkipped, totalShieldUses, 3)

Применяется автоматически в StreakService при обнаружении пропуска.

Event Types

EventTypeОписаниеЗаписывается при
ACTIVATIONАктивация нового баффаBuffService.activateBuff()
EXTENSIONПродление существующего баффаBuffService.activateBuff() (если бафф того же тира активен)
APPLICATIONПрименение множителя к наградеCaseOpeningService, UserSpinService
SHIELD_USEАвтоматическое использование shieldStreakService.claimDaily()

Protection

ДействиеRate LimitAuthValidation
Get active buffsgeneral (100/min)TelegramGetActiveBuffsSchema
Get buff historygeneral (100/min)TelegramGetBuffHistorySchema
Activate buffmutations (5/min)TelegramActivateBuffSchema
Детали реализации

См. Security Matrix для полного обзора защит.

Edge Cases

Что видит пользователь (UI):

СитуацияUI поведение
Нет баффов в инвентареСекция баффов скрыта или пустая
Активен бафф другого тираВ модалке активации: кнопка disabled, жёлтый блок с таймером до окончания текущего баффа
Max shields (3 uses)Кнопка disabled, tooltip "Достигнут лимит активных щитов"
Shield автоматически использованPush-уведомление "Streak Shield защитил вашу серию!"
Бафф применён к наградеВ модале награды показывается бонус с цветом тира
Конфликт тиров баффа
Попытка активации баффа при активном баффе другого тира
Backend Error Codes (для API/тестов)
КодHTTPСообщение
ITEM_NOT_FOUND400"Item not found in inventory"
NOT_A_BUFF400"Item is not a BUFF"
NO_BUFF_TYPE500"Failed to activate buff" (internal: "Item has no buffType")
TIER_MISMATCH500"Failed to activate buff" (internal: "Активен бафф другого уровня...")
MAX_SHIELDS400"Maximum 3 active Streak Shields allowed"
FORBIDDEN403"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.

14 дней AFK = выход из пула

Независимо от количества 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

КомпонентПутьОписание
BuffServicebackend/src/domains/buffs/services/buff.service.tsОркестратор активации и применения
BuffRepositorybackend/src/domains/buffs/repositories/buff.repository.tsCRUD операции с UserActiveBuff и BuffEvent
BuffControllerbackend/src/domains/buffs/controllers/user-buff.controller.tsHTTP handlers
Routesbackend/src/domains/buffs/routes/user-buffs.routes.tsUser API endpoints
Schemasbackend/src/domains/buffs/schemas/user-buffs.schemas.tsВалидация и Swagger
Constantsshared/src/constants/buff.constants.tsBUFF_DURATION_MINUTES, STREAK_SHIELD_LIMITS

Integration Points

Сервис-потребительМетодОписание
XPServicegetBuffMultiplierWithName('XP_BUFF')Применение множителя XP с получением названия баффа
ScrapService (addScrapWithBuff)getBuffMultiplierWithName('SCRAP_BUFF')Централизованное применение множителя Scrap (вызывается из CaseOpeningService, UserSpinService)
StreakServiceuseStreakShields(daysSkipped)Автоматическое использование shields
Централизованное начисление XP и Scrap

XP_BUFF применяется через addUserXPWithBuff() в backend/src/domains/users/services/xp.service.ts, SCRAP_BUFF — через addScrapWithBuff() в backend/src/domains/users/services/scrap.service.ts. Domain services (CaseOpeningService, UserSpinService) не вызывают getBuffMultiplierWithName() напрямую.


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

ПолеТипОписание
buffTypeBuffTypeXP_BUFF, SCRAP_BUFF, STREAK_SHIELD
multiplierFloat?1.25 / 1.5 / 2.0 (null для STREAK_SHIELD)
expiresAtDateTime?Время истечения (null для STREAK_SHIELD)
usesLeftInt?Оставшиеся использования (только STREAK_SHIELD)
sourceItemIdString?ID Item для аналитики и UI

BuffEvent Fields

ПолеТипОписание
buffTypeBuffTypeТип баффа
eventTypeBuffEventTypeACTIVATION, EXTENSION, APPLICATION, SHIELD_USE
activeBuffIdString?Ссылка на UserActiveBuff
multiplierFloat?Множитель (для ACTIVATION/EXTENSION)
expiresAtDateTime?Время истечения (для ACTIVATION/EXTENSION)
sourceTypeString?'case' или 'spin' (для APPLICATION)
sourceIdString?ID caseOpening или spinResult (для APPLICATION)
baseAmountInt?Базовая награда до баффа (для APPLICATION)
bonusAmountInt?Бонус от баффа (для APPLICATION)
daysProtectedInt?Сколько дней защитил shield (для SHIELD_USE)
streakBeforeInt?Стрик до использования shield (для SHIELD_USE)

Relationships


6. API Endpoints

МетодЭндпоинтОписаниеDocs
GET/api/buffs/activeСписок активных баффов
GET/api/buffs/historyИстория событий баффов с пагинацией
POST/api/buffs/activateАктивировать бафф из инвентаря

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,
"tier": "TIER_2"
},
{
"id": "cly...",
"buffType": "STREAK_SHIELD",
"multiplier": null,
"activatedAt": "2024-01-14T08:00:00.000Z",
"expiresAt": null,
"usesLeft": 2,
"remainingSeconds": null,
"tier": 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
Нет отдельного cleanup job

Баффы очищаются вместе с остальными данными (inventory, quests и т.д.) при сезонном reset.


  • Streaks — STREAK_SHIELD защищает стрик от сброса
  • Cases — Баффы выпадают из кейсов как награды
  • Daily Spins — Множители XP/SCRAP применяются к наградам
  • Inventory — Баффы хранятся как предметы до активации
  • Security Matrix — Матрица защит для buffs endpoints