Skip to main content

Achievements

1. Summary

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

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Секретное достижение не раскрыто
IN_PROGRESSПрогресс начат, но не завершён
COMPLETEDУсловие выполнено, награда готова к получению
CLAIMEDНаграда получена

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

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

3. Награды

  • RewardType: SCRAP, XP, ITEM, CASE, STREAK_POINTS
  • Награда выдаётся при 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
RUSTClaim награды за Rust квестAchievementProgressService.incrementCategoryProgress()+quest.targetProgress
Переменный прогресс (amount parameter)

Реализация:

async incrementCategoryProgress(
userId: string,
category: AchievementCategory,
conditions?: AchievementConditions,
amount: number = 1 // Значение по умолчанию для большинства категорий
): Promise<ProgressUpdateResult[]>

Логика:

  • По умолчанию: amount = 1 (для QUIZ, CASES, COLLECTION, RECYCLE, SOCIAL, SPECIAL)
  • Для RUST: amount = quest.targetProgress (накопление по прогрессу квеста)

Пример:

  • Квест "Убей 100 медведей" (targetProgress=100) выполнен
  • При claim награды → RUST достижение получает +100 прогресса
  • Достижение "Выполни квесты на 1000 убийств" за один раз получает 10% прогресса

2. Shared Progress

Одно действие засчитывается во ВСЕ подходящие достижения одновременно.

Пользователь выполнил Rust квест "Убей 50 медведей" (targetProgress=50)
→ +50 к достижению "Выполни квесты на 500 убийств медведей" (rustKillAnimalType=BEAR)
→ +50 к достижению "Выполни квесты на 1000 убийств животных" (rustEventType=KILL_ANIMAL)
→ +50 к достижению "Выполни 100 любых Rust квестов" (conditions=null)

3. Condition Matching

Алгоритм фильтрации в matchesGenericConditions():

  • Если achievement.conditions = null → подходит ВСЕ в категории
  • Иначе: все указанные поля должны совпадать (точное сравнение для RUST)
// Пример проверки RUST условий
if (achCond.rustEventType && achCond.rustEventType !== providedConditions.rustEventType) {
return false; // Не совпало → не засчитывается
}

Quests Integration

RUST достижения интегрированы с системой квестов:

  1. Пользователь выполняет Rust квест (например, "Убей 100 медведей")
  2. Вызывает /quests/:id/claim для получения награды
  3. quest-reward.service.ts выдаёт награду квеста (Scrap, XP, Items)
  4. Автоматически (через setImmediate) обновляет прогресс RUST достижений:
// quest-reward.service.ts (Step 6.8)
if (quest.category === 'RUST' && quest.rustEventType) {
setImmediate(async () => {
const rustConditions = {
rustEventType: quest.rustEventType,
rustKillAnimalType: quest.rustKillAnimalType ?? undefined,
// ... остальные условия
};

await achievementProgressService.incrementCategoryProgress(
userId,
'RUST',
rustConditions,
quest.targetProgress // amount = прогресс квеста
);
});
}
Async без блокировки

Используется setImmediate() для асинхронного обновления достижений без блокировки транзакции выдачи награды квеста. Если обновление достижения упадёт — пользователь всё равно получит награду за квест.

Полная документация Rust Integration

Подробнее о Rust квестах, событиях и webhook API: Rust Integration

Edge Cases

СитуацияПоведениеКод
✅ Достижение уже COMPLETED/CLAIMEDПропускается, прогресс не увеличивается
✅ Один квест → несколько достиженийВсе подходящие получают прогресс одновременноShared Progress
⚡ RUST: Ошибка обновления достиженияЛогируется, награда квеста выдаётсяsetImmediate + try/catch
🔒 isUnique=true, награда уже полученаПроверка через ClaimedUniqueReward, повторная выдача запрещенаREWARD_ALREADY_CLAIMED
🎯 RUST: условия не совпадаютДостижение не получает прогресс (фильтрация)matchesGenericConditions()
📊 RUST: quest.targetProgress = 0Прогресс +0, достижение не обновляется

Smart Achievement Generation (Blueprint System)

Достижения для сезона генерируются автоматически из code-defined блюпринтов (17 шт.) с учётом контента сезона.

Алгоритм (5 фаз):

  1. Analyze Content — собрать quiz categories + counts, season cases, Rust integration, 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-level
PROGRESSION2progression-xp-earned, progression-all-rounder
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 новые категории:

  • STREAK — поддержание ежедневного стрика (7/14/30 дней)
  • ECONOMY — заработок scrap, достижение уровня
  • PROGRESSION — общий XP за сезон (cross-category)
  • RUST — игровая активность на серверах (с переменным прогрессом)

Последствия: Более разнообразные достижения, лучшее покрытие game loop.


4. Architecture

Key Components

КомпонентПутьОписание
AchievementProgressServicebackend/src/domains/achievements/services/achievement-progress.service.ts⭐ Автоматическое обновление прогресса
AchievementServicebackend/src/domains/achievements/services/achievement.service.tsCRUD, claim, статистика
AchievementGeneratorServicebackend/src/domains/achievements/services/achievement-generator.service.tsBlueprint-based генерация для сезонов (5 фаз)
Achievement Blueprintsbackend/src/domains/seasons/data/achievement-blueprints.ts17 code-defined блюпринтов для генерации
QuestRewardServicebackend/src/domains/quests/services/quest-reward.service.tsИнтеграция: RUST квесты → достижения
Routesbackend/src/domains/achievements/routes/achievement.routes.tsUser API
Admin Routesbackend/src/domains/achievements/routes/admin-achievement.routes.tsAdmin API
ConditionsEditoradmin/src/components/achievements/ConditionsEditor.tsxRUST fields: event type, kill types, craft
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

RUST Achievements Flow


5. Database Schema

Models

МодельОписаниеКлючевые поля
AchievementОпределение достиженияcategory, conditions, reward, targetProgress, difficulty
UserAchievementСтатус у пользователяuserId, achievementId, status, currentProgress, rewardSnapshot
ClaimedUniqueRewardПолученные уникальные наградыuserId, rewardType
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Список достиженийТестировать →
POST/api/achievements/:id/claimПолучить наградуТестировать →

Admin API

МетодЭндпоинтОписаниеСсылка
GET/admin/achievementsВсе достиженияТестировать →
POST/admin/achievementsСоздать достижениеТестировать →

Season Content Budget API

МетодЭндпоинтОписание
POST/admin/content-budget/:seasonId/achievements/addПривязать достижение к сезону вручную
Manual Achievement Linking

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


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