Skip to main content

Achievements

1. Summary

Goal: Система долгосрочных целей для мотивации активности пользователей. Достижения отслеживают прогресс в различных категориях (квизы, кейсы, стрики, экономика, прогрессия) и награждают за выполнение.

User Value: Долгосрочная мотивация, коллекционирование, статус. Пользователи получают награды (Scrap, XP, Items) за достижение целей, видят свой прогресс и могут планировать путь к сложным достижениям.


2. Business Logic

Achievement Categories

Достижения разделены на категории по типу отслеживаемой активности:

Действие: Прохождение квизов

Условия фильтрации:

  • 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" }

Rules & Mechanics

1. Статусы достижений

СтатусОписание
LOCKEDНачальный статус по умолчанию для всех UserAchievement (прогресс ещё не начат)
IN_PROGRESSПрогресс начат, но не завершён
COMPLETEDУсловие выполнено, награда готова к получению
CLAIMEDНаграда получена

2. Условия разблокировки

  • Если conditions = null → засчитываются ВСЕ действия категории
  • Если conditions указаны → засчитываются только подходящие под фильтры

3. Награды

  • RewardType: SCRAP, XP, ITEM, CASE, STREAK_POINTS
Типы наград в Admin UI

Через 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Сложный🔴 Красный💀
UI отображение
  • Admin панель: Колонка "Сложность" с цветным бейджем (череп + текст)
  • Frontend TMA: Бейдж рядом с названием достижения на карточке
  • Если сложность не указана (difficulty = null) — бейдж не отображается
Не путать с Item Tier!

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
RECYCLESalvage предметаAchievementProgressService.incrementCategoryProgress()+1
SOCIALСоциальные действия (подписка, реферал)AchievementProgressService.incrementCategoryProgress()+1
STREAKОбновление стрикаAchievementProgressService.setCategoryProgress()=streakDays
ECONOMYЗаработок scrap / уровеньAchievementProgressService.incrementCategoryProgress()+amount
PROGRESSIONЗаработок XPAchievementProgressService.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 фаз):

  1. Analyze Content — собрать quiz categories + counts, season cases, quiz budget
  2. Calculate Pool Size — целевое количество (15-30), масштабируется от объёма контента
  3. Instantiate Blueprints — создать кандидатов из блюпринтов × контент (content-aware)
  4. Balance Distribution — round-robin по категориям, cap до targetCount
  5. Save to Database — транзакция: soft-delete старых auto-generated + создать новые

Генерация по режимам:

РежимОписаниеПример
genericДо 3 вариантов с эскалацией сложности I/II/III"Знаток квизов I" (50), "Знаток квизов II" (150)
per-quiz-category1 на каждую активную категорию квизов"Мастер оружия" (20 квизов про weapons)
per-case1 на каждый кейс сезона"Фанат «Discharge»" (30 открытий)

Матрица блюпринтов:

КатегорияБлюпринтовПримеры
QUIZ4quiz-complete-total, quiz-perfect-score, quiz-category-mastery, quiz-streak
CASES3cases-open-total, cases-open-specific, cases-win-rare
COLLECTION2collection-total, collection-tier3
RECYCLE2recycle-total, recycle-blueprints
SOCIAL1social-referrals
STREAK1streak-maintain
ECONOMY2economy-scrap-earned, economy-cases-spent
PROGRESSION2progression-level, progression-xp
SPECIAL1special-completionist
Placeholder rewards

Генерируемые достижения получают placeholder SCRAP награды (Easy=50, Medium=100, Hard=150). Админ настраивает финальные награды после генерации через EditAchievementModal.

Quiz Budget Capping

Если у сезона настроен quiz budget (через SeasonContentBudget), targets quiz-достижений автоматически капируются — не будет "Пройди 300 квизов" если в бюджете только 100.

Season-Achievement Model

Связь достижения с сезоном через SeasonAchievement:

ПолеОписание
seasonIdСвязь с сезоном
achievementIdСвязь с Achievement
isAutoGeneratedtrue для сгенерированных, false для добавленных вручную
rewardStatusPLACEHOLDER (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 превращается в achievements
  • explicitTargets + 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

КомпонентПутьОписание
AchievementProgressServicebackend/src/domains/achievements/services/achievement-progress.service.tsАвтоматическое обновление прогресса
AchievementServicebackend/src/domains/achievements/services/achievement.service.tsCRUD, claim
AchievementRewardServicebackend/src/domains/achievements/services/achievement-reward.service.tsВыдача наград за достижения
AchievementStatsServicebackend/src/domains/achievements/services/achievement-stats.service.tsСтатистика достижений
AchievementAdminServicebackend/src/domains/achievements/services/achievement-admin.service.tsAdmin CRUD операции
AchievementGeneratorServicebackend/src/domains/achievements/services/achievement-generator.service.tsBlueprint-based генерация для сезонов (5 фаз)
Achievement Blueprintsbackend/src/domains/seasons/data/achievement-blueprints.ts18 code-defined блюпринтов для генерации
Routesbackend/src/domains/achievements/routes/user-achievements.routes.tsUser API
Admin Routesbackend/src/domains/achievements/routes/admin-achievements.routes.tsAdmin API
ConditionsEditoradmin/src/components/achievements/ConditionsEditor.tsxРедактор условий достижений
AchievementFormadmin/src/components/achievements/AchievementForm.tsxФорма создания/редактирования
DifficultyBadge (Admin)admin/src/components/achievements/DifficultyBadge.tsxБейдж сложности для таблицы
DifficultyBadge (TMA)frontend/src/components/screens/AchievementsScreen/components/DifficultyBadge.tsxБейдж сложности для карточки
AchievementCardfrontend/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 с условиями фильтрации. Структура зависит от категории:

{
"categoryId": "weapon",
"subcategory": "looted-from",
"slug": "mp5a4",
"entityType": "item"
}

Reward Audit (rewardSnapshot)

При claim награды за достижение в UserAchievement.rewardSnapshot сохраняется JSON снимок:

{
"type": "ITEM",
"amount": 1,
"itemId": "item-456",
"itemName": "Golden Badge",
"itemTier": "TIER_4"
}
Для чего нужен rewardSnapshot

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Удалить конкретное достижение из сезона
Manual Achievement Linking

Endpoint achievements/add принимает { achievementId } и создаёт SeasonAchievement запись с isAutoGenerated: false и rewardStatus: CONFIGURED. Проверки: сезон существует, не COMPLETED, достижение существует, ещё не привязано.


  • Seasonsblueprint-based генерация достижений при активации сезона
  • Quests — похожая механика прогресса
  • Rust Integration — игровые события, webhook API (квесты, не достижения)
  • Streaks — достижения за стрики
  • Quizzes — достижения за правильные ответы
  • Glossary — термины и определения