Achievements
1. Summary
Goal: Система долгосрочных целей для мотивации активности пользователей. Достижения отслеживают прогресс в различных категориях (квизы, кейсы, стрики, экономика, прогрессия) и награждают за выполнение.
User Value: Долгосрочная мотивация, коллекционирование, статус. Пользователи получают награды (Scrap, XP, Items) за достижение целей, видят свой прогресс и могут планировать путь к сложным достижениям.
2. Business Logic
Achievement Categories
Достижения разделены на категории по типу отслеживаемой активности:
- QUIZ
- CASES
- COLLECTION
- RECYCLE
- SOCIAL
- SPECIAL
- STREAK
- ECONOMY
- PROGRESSION
Действие: Прохождение квизов
Условия фильтрации:
categoryId— категория квизов (weapon, monument, etc.)subcategory— подкатегория (looted-from, creation, raid)slug— конкретный квиз (mp5a4, abandoned-military-base)entityType— тип сущности (item, monument, mechanic, npc, vehicle)
Примеры:
- "Пройди 100 квизов" →
conditions: null(любые квизы) - "Пройди 50 квизов про оружие" →
{ categoryId: "weapon" } - "Пройди все вопросы про MP5" →
{ slug: "mp5a4" }
Действие: Открытие кейсов
Условия фильтрации:
caseTypeId— тип кейса (daily, paid, special)caseId— конкретный кейс
Примеры:
- "Открой 50 любых кейсов" →
conditions: null - "Открой 10 ежедневных кейсов" →
{ caseTypeId: "daily" }
Действие: Получение предметов в инвентарь
Условия фильтрации:
itemId— конкретный предметitemTier— предмет тира >= N (0-5)
Примеры:
- "Получи 10 предметов Tier 3+" →
{ itemTier: 3 } - "Получи конкретный предмет" →
{ itemId: "item-123" }
Действие: Salvage (переработка) предметов
Условия фильтрации:
recycleItemType— тип предмета (BLUEPRINT | FRAGMENT)itemTier— тир >= NitemId— конкретный предмет
Примеры:
- "Разбери 50 чертежей" →
{ recycleItemType: "BLUEPRINT" } - "Разбери 20 предметов Tier 4+" →
{ itemTier: 4 }
Действие: Социальные активности
Условия фильтрации:
socialActionType— тип (subscription | referral)telegramChannelId— ID канала для подпискиtelegramChatId— ID чатаreferralCount— количество рефералов
Примеры:
- "Пригласи 10 друзей" →
{ socialActionType: "referral", referralCount: 10 } - "Подпишись на канал" →
{ socialActionType: "subscription", telegramChannelId: "@goloot" }
Действие: Достижение уровня/стрика
Условия фильтрации:
minLevel— уровень пользователя >= NstreakDays— стрик >= N дней
Примеры:
- "Достигни 7-дневного стрика" →
{ streakDays: 7 } - "Достигни 30 уровня" →
{ minLevel: 30 }
Действие: Поддержание ежедневного стрика
Условия фильтрации:
streakDays— целевое количество дней стрика
Примеры:
- "Поддержи 7-дневный стрик" →
{ streakDays: 7 } - "Поддержи 30-дневный стрик" →
{ streakDays: 30 }
Действие: Экономические достижения (заработок scrap, уровень)
Условия фильтрации:
minLevel— достигнутый уровень пользователя
Примеры:
- "Заработай 1000 Scrap за сезон" →
conditions: null, targetProgress=1000 - "Достигни 30 уровня" →
{ minLevel: 30 }
Действие: Прогрессионные достижения (общая активность)
Условия фильтрации:
- Нет специфических — отслеживает общий XP заработанный за сезон
Примеры:
- "Заработай 5000 XP за сезон" →
conditions: null, targetProgress=5000
Rules & Mechanics
1. Статусы достижений
| Статус | Описание |
|---|---|
| LOCKED | Начальный статус по умолчанию для всех UserAchievement (прогресс ещё не начат) |
| IN_PROGRESS | Прогресс начат, но не завершён |
| COMPLETED | Условие выполнено, награда готова к получению |
| CLAIMED | Награда получена |
2. Условия разблокировки
- Если
conditions = null→ засчитываются ВСЕ действия категории - Если
conditionsуказаны → засчитываются только подходящие под фильтры
3. Награды
RewardType: SCRAP, XP, ITEM, CASE, STREAK_POINTS
Через Admin UI (форма создания/редактирования достижения) доступны только SCRAP, XP, ITEM — это ограничение RewardInput DTO. Типы CASE и STREAK_POINTS существуют в модели Reward в Prisma, но недоступны при создании достижений через интерфейс.
- Награда выдаётся при claim (переход COMPLETED → CLAIMED)
- Snapshot награды сохраняется в
UserAchievement.rewardSnapshotдля аудита
4. Уникальные награды
- Если
Achievement.isUnique = true→ награда выдаётся один раз на telegramId - Даже после
/stopпользователь не сможет получить награду повторно - Запись в
ClaimedUniqueRewardпредотвращает обход через пересоздание аккаунта
Difficulty (Сложность)
Каждое достижение может иметь опциональный уровень сложности (difficulty):
| Уровень | Название | Цвет | Иконка |
|---|---|---|---|
| 1 | Легкий | 🟢 Зелёный | 💀 |
| 2 | Средний | 🟡 Жёлтый | 💀 |
| 3 | Сложный | 🔴 Красный | 💀 |
- Admin панель: Колонка "Сложность" с цветным бейджем (череп + текст)
- Frontend TMA: Бейдж рядом с названием достижения на карточке
- Если сложность не указана (
difficulty = null) — бейдж не отображается
Achievement.difficulty — это сложность достижения (1-3), а не редкость предмета.
В условиях достижений (conditions.itemTier) используется tier предмета — это разные поля.
Core Mechanics: Progress Update
1. Автоматическое обновление прогресса
Прогресс достижений обновляется автоматически при выполнении действий:
| Категория | Триггер | Сервис | Amount |
|---|---|---|---|
| QUIZ | После правильного ответа | AchievementProgressService.incrementQuizProgress() | +1 |
| CASES | После открытия кейса | AchievementProgressService.incrementCategoryProgress() | +1 |
| COLLECTION | Получение предмета в инвентарь | AchievementProgressService.incrementCategoryProgress() | +1 |
| RECYCLE | Salvage предмета | AchievementProgressService.incrementCategoryProgress() | +1 |
| SOCIAL | Социальные действия (подписка, реферал) | AchievementProgressService.incrementCategoryProgress() | +1 |
| STREAK | Обновление стрика | AchievementProgressService.setCategoryProgress() | =streakDays |
| ECONOMY | Заработок scrap / уровень | AchievementProgressService.incrementCategoryProgress() | +amount |
| PROGRESSION | Заработок XP | AchievementProgressService.incrementCategoryProgress() | +amount |
Переменный прогресс (amount parameter)
Два метода обновления прогресса:
1. incrementCategoryProgress() — инкрементальный:
async incrementCategoryProgress(
userId: string,
category: AchievementCategory,
conditions?: AchievementConditions,
amount: number = 1 // Значение по умолчанию для большинства категорий
): Promise<ProgressUpdateResult[]>
2. setCategoryProgress() — абсолютный:
async setCategoryProgress(
userId: string,
category: AchievementCategory,
conditions?: AchievementConditions,
absoluteValue: number = 0
): Promise<ProgressUpdateResult[]>
Логика:
incrementCategoryProgress:amount = 1по умолчанию (QUIZ, CASES, COLLECTION, RECYCLE, SOCIAL, SPECIAL, ECONOMY, PROGRESSION)setCategoryProgress: устанавливает абсолютное значение прогресса (используется для STREAK — текущее количество дней стрика)
Примеры:
- Пользователь ответил на квиз →
incrementCategoryProgress(QUIZ, +1)→ достижение получает +1 - Стрик обновлён до 14 дней →
setCategoryProgress(STREAK, 14)→ прогресс устанавливается в 14
2. Shared Progress
Одно действие засчитывается во ВСЕ подходящие достижения одновременно.
Пользователь прошёл квиз про оружие (weapons):
→ +1 к достижению "Пройди 50 квизов про оружие" (categoryId="weapons")
→ +1 к достижению "Пройди 300 квизов" (conditions=null)
→ +1 к достижению "Эрудит I" (conditions=null)
3. Condition Matching
Алгоритм фильтрации в matchesGenericConditions():
- Если
achievement.conditions = null→ подходит ВСЕ в категории - Иначе: все указанные поля должны совпадать (AND-логика)
Проверяемые поля по категориям:
- QUIZ:
categoryId,subcategory,slug,entityType - CASES:
caseTypeId,caseId - COLLECTION/RECYCLE:
itemId,itemTier(>=),recycleItemType - SOCIAL:
socialActionType,telegramChannelId,telegramChatId(полеreferralCountопределено в типе, но не проверяется вmatchesGenericConditions) - SPECIAL:
minLevel(полеstreakDaysопределено в типе, но не проверяется вmatchesGenericConditions) - STREAK:
streakDays(устанавливается черезsetCategoryProgress, не черезmatchesGenericConditions) - ECONOMY:
minLevel - PROGRESSION: без специфических условий
Edge Cases
| Ситуация | Поведение | Код |
|---|---|---|
| ✅ Достижение уже COMPLETED/CLAIMED | Пропускается, прогресс не увеличивается | — |
| ✅ Одно событие → несколько достижений | Все подходящие получают прогресс одновременно | Shared Progress |
| 🔒 isUnique=true, награда уже получена | Проверка через ClaimedUniqueReward, повторная выдача запрещена | ALREADY_CLAIMED |
Smart Achievement Generation (Blueprint System)
Достижения для сезона генерируются автоматически из code-defined блюпринтов (18 шт.) с учётом контента сезона.
Алгоритм (5 фаз):
- Analyze Content — собрать quiz categories + counts, season cases, quiz budget
- Calculate Pool Size — целевое количество (15-30), масштабируется от объёма контента
- Instantiate Blueprints — создать кандидатов из блюпринтов × контент (content-aware)
- Balance Distribution — round-robin по категориям, cap до targetCount
- Save to Database — транзакция: soft-delete старых auto-generated + создать новые
Генерация по режимам:
| Режим | Описание | Пример |
|---|---|---|
generic | До 3 вариантов с эскалацией сложности I/II/III | "Знаток квизов I" (50), "Знаток квизов II" (150) |
per-quiz-category | 1 на каждую активную категорию квизов | "Мастер оружия" (20 квизов про weapons) |
per-case | 1 на каждый кейс сезона | "Фанат «Discharge»" (30 открытий) |
Матрица блюпринтов:
| Категория | Блюпринтов | Примеры |
|---|---|---|
| QUIZ | 4 | quiz-complete-total, quiz-perfect-score, quiz-category-mastery, quiz-streak |
| CASES | 3 | cases-open-total, cases-open-specific, cases-win-rare |
| COLLECTION | 2 | collection-total, collection-tier3 |
| RECYCLE | 2 | recycle-total, recycle-blueprints |
| SOCIAL | 1 | social-referrals |
| STREAK | 1 | streak-maintain |
| ECONOMY | 2 | economy-scrap-earned, economy-cases-spent |
| PROGRESSION | 2 | progression-level, progression-xp |
| SPECIAL | 1 | special-completionist |
Генерируемые достижения получают placeholder SCRAP награды (Easy=50, Medium=100, Hard=150). Админ настраивает финальные награды после генерации через EditAchievementModal.
Если у сезона настроен quiz budget (через SeasonContentBudget), targets quiz-достижений автоматически капируются — не будет "Пройди 300 квизов" если в бюджете только 100.
Season-Achievement Model
Связь достижения с сезоном через SeasonAchievement:
| Поле | Описание |
|---|---|
seasonId | Связь с сезоном |
achievementId | Связь с Achievement |
isAutoGenerated | true для сгенерированных, false для добавленных вручную |
rewardStatus | PLACEHOLDER (scrap by default) или CONFIGURED (админ настроил) |
3. ADR (Architectural Decisions)
Почему blueprint-based генерация вместо DB-шаблонов?
Проблема: Изначально достижения создавались через AchievementTemplate модель в БД. Это приводило к:
- 5-level data misalignment (DB → service → controller → schema → frontend)
- Невозможность выразить
conditionsFactory(JavaScript функции) в БД - Сложность поддержания content-aware логики (per-quiz-category, per-case)
Решение: Code-defined blueprints в achievement-blueprints.ts:
conditionsFactory— функция, генерирующая JSON conditions из контекстаgenerationMode— управляет как blueprint превращается в achievementsexplicitTargets+escalateDifficulty— multi-variant генерация (I, II, III)AchievementTemplateмодель удалена из Prisma schema
Альтернативы (отклонены):
- JSON конфигурация — не может хранить функции (conditionsFactory)
- Admin UI шаблоны — переусложнение, blueprints меняются редко
Последствия: Blueprints в коде (не в БД), изменения требуют deploy. Но полный контроль над логикой генерации.
Почему 10 категорий достижений?
Проблема: Исходно было 6 категорий (QUIZ, CASES, COLLECTION, RECYCLE, SOCIAL, SPECIAL). Не покрывали стрики, экономику и общий прогресс.
Решение: Добавлены 4 новые категории:
RUST— достижения за Rust активность (в Prisma enum, но документация/blueprints удалены в V2)STREAK— поддержание ежедневного стрика (7/14/30 дней)ECONOMY— заработок scrap, достижение уровняPROGRESSION— общий XP за сезон (cross-category)
Последствия: Более разнообразные достижения, лучшее покрытие game loop. RUST остаётся в Prisma enum для обратной совместимости, но активных blueprints нет.
4. Architecture
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| AchievementProgressService | backend/src/domains/achievements/services/achievement-progress.service.ts | Автоматическое обновление прогресса |
| AchievementService | backend/src/domains/achievements/services/achievement.service.ts | CRUD, claim |
| AchievementRewardService | backend/src/domains/achievements/services/achievement-reward.service.ts | Выдача наград за достижения |
| AchievementStatsService | backend/src/domains/achievements/services/achievement-stats.service.ts | Статистика достижений |
| AchievementAdminService | backend/src/domains/achievements/services/achievement-admin.service.ts | Admin CRUD операции |
| AchievementGeneratorService | backend/src/domains/achievements/services/achievement-generator.service.ts | Blueprint-based генерация для сезонов (5 фаз) |
| Achievement Blueprints | backend/src/domains/seasons/data/achievement-blueprints.ts | 18 code-defined блюпринтов для генерации |
| Routes | backend/src/domains/achievements/routes/user-achievements.routes.ts | User API |
| Admin Routes | backend/src/domains/achievements/routes/admin-achievements.routes.ts | Admin API |
| ConditionsEditor | admin/src/components/achievements/ConditionsEditor.tsx | Редактор условий достижений |
| AchievementForm | admin/src/components/achievements/AchievementForm.tsx | Форма создания/редактирования |
| DifficultyBadge (Admin) | admin/src/components/achievements/DifficultyBadge.tsx | Бейдж сложности для таблицы |
| DifficultyBadge (TMA) | frontend/src/components/screens/AchievementsScreen/components/DifficultyBadge.tsx | Бейдж сложности для карточки |
| AchievementCard | frontend/src/components/screens/AchievementsScreen/components/AchievementCard.tsx | Карточка достижения в TMA |
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| Achievement | Определение достижения | category, conditions, reward, targetProgress, difficulty |
| UserAchievement | Статус у пользователя | userId, achievementId, status, currentProgress, rewardSnapshot |
| ClaimedUniqueReward | Полученные уникальные награды | telegramId, achievementId, claimedAt |
| SeasonAchievement | Связь достижения с сезоном | seasonId, achievementId, isAutoGenerated, rewardStatus |
Achievement.conditions (JSONB)
Поле conditions хранит JSON с условиями фильтрации. Структура зависит от категории:
- QUIZ
- CASES
{
"categoryId": "weapon",
"subcategory": "looted-from",
"slug": "mp5a4",
"entityType": "item"
}
{
"caseTypeId": "daily",
"caseId": "case-123"
}
Reward Audit (rewardSnapshot)
При claim награды за достижение в UserAchievement.rewardSnapshot сохраняется JSON снимок:
{
"type": "ITEM",
"amount": 1,
"itemId": "item-456",
"itemName": "Golden Badge",
"itemTier": "TIER_4"
}
Snapshot фиксирует выданную награду на момент claim. Даже если достижение или награда изменится — аудит сохранит оригинальные значения для истории операций пользователя.
6. API Endpoints
User API
| Метод | Эндпоинт | Описание | Ссылка |
|---|---|---|---|
| GET | /api/achievements | Список достижений | Тестировать → |
| GET | /api/achievements/:id | Детальная информация о достижении | Тестировать → |
| POST | /api/achievements/:id/claim | Получить награду | Тестировать → |
Admin API
| Метод | Эндпоинт | Описание | Ссылка |
|---|---|---|---|
| GET | /admin/achievements | Все достижения | Тестировать → |
| GET | /admin/achievements/:id | Достижение по ID | Тестировать → |
| POST | /admin/achievements | Создать достижение | Тестировать → |
| PUT | /admin/achievements/:id | Обновить достижение | Тестировать → |
| DELETE | /admin/achievements/:id | Удалить достижение | Тестировать → |
| PUT | /admin/achievements/:id/toggle-active | Переключить активность | Тестировать → |
| GET | /admin/achievements/:id/stats | Статистика достижения | Тестировать → |
| POST | /admin/achievements/bulk-action | Массовые операции | Тестировать → |
| PUT | /admin/achievements/:achievementId/reward | Обновить награду достижения (Content Budget pipeline) | — |
Season Content Budget API
| Метод | Эндпоинт | Описание |
|---|---|---|
| POST | /admin/content-budget/:seasonId/achievements/generate | Сгенерировать достижения из блюпринтов для сезона |
| POST | /admin/content-budget/:seasonId/achievements/add | Привязать достижение к сезону вручную |
| GET | /admin/content-budget/:seasonId/achievements | Получить список достижений сезона |
| DELETE | /admin/content-budget/:seasonId/achievements/clear | Очистить достижения сезона (все или только auto-generated) |
| DELETE | /admin/content-budget/:seasonId/achievements/:achievementId | Удалить конкретное достижение из сезона |
Endpoint achievements/add принимает { achievementId } и создаёт SeasonAchievement запись с isAutoGenerated: false и rewardStatus: CONFIGURED. Проверки: сезон существует, не COMPLETED, достижение существует, ещё не привязано.