Achievements
1. Summary
Goal: Система долгосрочных целей для мотивации активности пользователей. Достижения отслеживают прогресс в различных категориях (квизы, кейсы, Rust игра) и награждают за выполнение.
User Value: Долгосрочная мотивация, коллекционирование, статус. Пользователи получают награды (Scrap, XP, Items) за достижение целей, видят свой прогресс и могут планировать путь к сложным достижениям.
2. Business Logic
Achievement Categories
Достижения разделены на категории по типу отслеживаемой активности:
- QUIZ
- CASES
- COLLECTION
- RECYCLE
- SOCIAL
- SPECIAL
- RUST
- 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 }
Действие: Выполнение Rust квестов (игровая активность на серверах)
Условия фильтрации:
rustEventType— тип события (KILL_ANIMAL, KILL_SCIENTIST, CRAFT, FISH, etc.)rustKillAnimalType— тип животного (BEAR, WOLF, ANY, PREDATOR)rustKillScientistType— тип учёного (OIL_RIG, CARGO_SHIP, ANY)rustCraftItemShortname— shortname предмета для крафта (rifle.ak, ammo.rifle)rustFishType— тип рыбы (ANY, fish.salmon)rustExplosiveType— тип взрывчатки (C4, SATCHEL, ROCKET, ANY)rustServerId— конкретный сервер (опционально)
Примеры:
- "Выполни 50 любых Rust квестов" →
conditions: null - "Выполни 10 квестов на убийство медведей" →
{ rustEventType: "KILL_ANIMAL", rustKillAnimalType: "BEAR" } - "Выполни 5 квестов на крафт AK-47" →
{ rustEventType: "CRAFT", rustCraftItemShortname: "rifle.ak" }
RUST достижения накапливают прогресс по значению quest.targetProgress, а не фиксированный +1.
Пример: Квест "Убей 100 медведей" (targetProgress=100) при выполнении добавит +100 к RUST достижению "Выполни квесты на 1000 убийств животных".
RUST достижения обновляются автоматически при claim награды за Rust квест. Подробнее: Quests Integration
Действие: Поддержание ежедневного стрика
Условия фильтрации:
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 | Секретное достижение не раскрыто |
| 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 | Сложный | 🔴 Красный | 💀 |
- 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 |
| RUST | Claim награды за 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 достижения интегрированы с системой квестов:
- Пользователь выполняет Rust квест (например, "Убей 100 медведей")
- Вызывает
/quests/:id/claimдля получения награды - quest-reward.service.ts выдаёт награду квеста (Scrap, XP, Items)
- Автоматически (через
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 = прогресс квеста
);
});
}
Используется setImmediate() для асинхронного обновления достижений без блокировки транзакции выдачи награды квеста. Если обновление достижения упадёт — пользователь всё равно получит награду за квест.
Подробнее о 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 фаз):
- Analyze Content — собрать quiz categories + counts, season cases, Rust integration, 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-level |
| PROGRESSION | 2 | progression-xp-earned, progression-all-rounder |
Генерируемые достижения получают 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 новые категории:
STREAK— поддержание ежедневного стрика (7/14/30 дней)ECONOMY— заработок scrap, достижение уровняPROGRESSION— общий XP за сезон (cross-category)RUST— игровая активность на серверах (с переменным прогрессом)
Последствия: Более разнообразные достижения, лучшее покрытие game loop.
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, статистика |
| AchievementGeneratorService | backend/src/domains/achievements/services/achievement-generator.service.ts | Blueprint-based генерация для сезонов (5 фаз) |
| Achievement Blueprints | backend/src/domains/seasons/data/achievement-blueprints.ts | 17 code-defined блюпринтов для генерации |
| QuestRewardService | backend/src/domains/quests/services/quest-reward.service.ts | Интеграция: RUST квесты → достижения |
| Routes | backend/src/domains/achievements/routes/achievement.routes.ts | User API |
| Admin Routes | backend/src/domains/achievements/routes/admin-achievement.routes.ts | Admin API |
| ConditionsEditor | admin/src/components/achievements/ConditionsEditor.tsx | RUST fields: event type, kill types, craft |
| 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 |
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 с условиями фильтрации. Структура зависит от категории:
- QUIZ
- RUST
- CASES
{
"categoryId": "weapon",
"subcategory": "looted-from",
"slug": "mp5a4",
"entityType": "item"
}
{
"rustEventType": "KILL_ANIMAL",
"rustKillAnimalType": "BEAR",
"rustServerId": "rust-server-123"
}
Доступные поля:
rustEventType: KILL_ANIMAL, KILL_SCIENTIST, CRAFT, FISH, EXPLOSIVE, etc.rustKillAnimalType: BEAR, WOLF, BOAR, CHICKEN, HORSE, STAG, ANY, PREDATORrustKillScientistType: OIL_RIG, CARGO_SHIP, MILITARY_TUNNEL, ANYrustCraftItemShortname: rifle.ak, ammo.rifle, explosive.timedrustFishType: ANY, fish.salmon, fish.troutsmallrustExplosiveType: C4, SATCHEL, ROCKET, ANYrustServerId: опционально, для привязки к конкретному серверу
{
"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 | Список достижений | Тестировать → |
| POST | /api/achievements/:id/claim | Получить награду | Тестировать → |
Admin API
| Метод | Эндпоинт | Описание | Ссылка |
|---|---|---|---|
| GET | /admin/achievements | Все достижения | Тестировать → |
| POST | /admin/achievements | Создать достижение | Тестировать → |
Season Content Budget API
| Метод | Эндпоинт | Описание |
|---|---|---|
| POST | /admin/content-budget/:seasonId/achievements/add | Привязать достижение к сезону вручную |
Endpoint принимает { achievementId } и создаёт SeasonAchievement запись с isAutoGenerated: false и rewardStatus: CONFIGURED. Проверки: сезон существует, не COMPLETED, достижение существует, ещё не привязано.