Quests System
1. Summary
Goal: Система заданий для направления активности пользователей. Квесты стимулируют выполнение целевых действий (квизы, кейсы, подписки) и награждают за достижение целей.
User Value: Понятные цели + гарантированные награды. Пользователь видит конкретные задачи с прогрессом и может планировать свой путь к наградам.
2. Business Logic
Quest Types (Временные периоды)
- Permanent
- Daily
- Weekly
- Event
Срок: Бессрочный
Reset: Нет сброса прогресса
Примеры: "Пройди 100 квизов", "Открой 50 кейсов"
Цель: Квесты без дедлайна — подписки на спонсоров, разовые акции
Срок: До 00:00 UTC
Reset: Автоматический сброс прогресса в полночь (Lazy Reset)
Rotation: Из пула выбираются 3 квеста детерминированно (одинаковые для всех)
Цель: Retention — причина заходить каждый день
Срок: До понедельника 00:00 UTC
Reset: Автоматический сброс в начале недели (Lazy Reset)
Rotation: Из пула выбирается 1 квест детерминированно
Цель: Retention — награда за стабильную активность в течение недели
Срок: startAt → endAt (задается вручную)
Reset: Нет сброса
Примеры: "Открой 10 кейсов за время акции"
Цель: Spike активности в нужный период (маркетинг, праздники)
Quest Categories (Типы действий)
- QUIZ
- CASES
- SOCIAL
- SPECIAL
- COLLECTION
- RECYCLE
- RUST
- SKILL_UPGRADED
Действие: Прохождение квизов
Conditions:
categoryId— только квизы из категорииsubcategoryId— только из подкатегорииslug— конкретный квиз (например "mp5a4")entityType— тип сущности (item, monument, mechanic, npc, vehicle)
Пример: "Пройди 5 квизов про оружие" → { categoryId: "weapon" }
Действие: Открытие кейсов
Conditions:
caseTypeId— тип кейса (daily, paid, special)caseId— конкретный кейс
Пример: "Открой 3 ежедневных кейса" → { caseTypeId: "daily" }
Действие: Социальные действия
Conditions:
socialActionType— тип (subscription|referral)telegramChannelId— ID канала для подпискиtelegramChatId— ID чата для подпискиreferralCount— количество рефералов
Проверка: Через Telegram Bot API (getChatMember)
Пример: "Подпишись на наш канал" → { telegramChannelId: "@goloot_channel" }
Действие: Достижение уровня/стрика
Conditions:
minLevel— достичь уровня NstreakDays— удерживать стрик N дней
Пример: "Достигни 7-дневного стрика" → { streakDays: 7 }
SPECIAL квесты с streakDays проверяются автоматически при claim daily reward. Если текущий стрик >= условия — квест завершается.
Действие: Получение предметов или скрапа из кейсов и рулеток
Conditions:
itemId— конкретный предметitemTier— предмет тира >= NitemType— тип предмета:SKIN,BLUEPRINT,FRAGMENT,RESOURCE,BUFFrewardType— тип награды:ITEM(предмет) илиSCRAP(скрап)
Источники прогресса: Кейсы (CaseOpeningService) + рулетки (UserSpinService)
Примеры:
- "Собери 5 ресурсов" →
{ itemType: "RESOURCE", rewardType: "ITEM" } - "Набери 200 скрапа" →
{ rewardType: "SCRAP" } - "Выбей предмет Tier 3+" →
{ itemTier: 3, rewardType: "ITEM" }
CASES считает действия (сколько раз открыл кейс), COLLECTION считает результаты (что получил — предмет или скрап, из кейса или рулетки). Один открытый кейс может дать +1 к CASES квесту и +1 к COLLECTION квесту одновременно.
Действие: Salvage предметов
Conditions:
recycleItemType— тип предмета (BLUEPRINT|FRAGMENT)itemTier— тир предмета >= NitemId— конкретный предмет
Пример: "Разбери 10 чертежей" → { recycleItemType: "BLUEPRINT" }
Действие: Активность в игре Rust (через плагин на сервере)
Требования: Привязанный Steam аккаунт, запуск квеста через /start
RUST квесты используют интеграцию с игровыми серверами через Webhook API: сессии игроков, 11 типов событий (TIME, COMMAND, GATHER, LOOT_CONTAINER, LOOT_ITEM, KILL_ANIMAL, KILL_SCIENTIST, CRAFT, RECYCLE, EXPLOSIVE_USED, SKILL_UPGRADED).
Новое: EXPLOSIVE_USED поддерживает гранулярные типы взрывчатки — можно создавать квесты на конкретные ракеты (MLRS, HV, Fire, Basic) и гранаты (F1, Beancan).
Полная документация: Rust Integration
При claim награды за Rust квест автоматически обновляются RUST достижения. Прогресс накапливается по значению quest.targetProgress.
Подробнее: Achievements - RUST Category
Действие: Прокачка скиллов в SkillTree плагине
Режимы выбора:
- ANY — любой скилл в любом дереве
- TREE — любой скилл в конкретном дереве (Mining, Combat, etc.)
- SKILL — конкретный скилл (с уровнем или max level)
Conditions:
rustSkillTree— дерево скиллов (null= ANY)rustSkillName— buffKey скилла (null= любой в дереве)rustSkillTargetLevel— целевой уровень (1-5)rustSkillRequireMax— требовать максимальный уровеньrustSkillUpgradeCount— сколько прокачек нужно
Admin UI (разделение ответственности):
SkillTreeSelectModal (WHAT — что и до какого уровня):
- Fullscreen модальное окно с 13 деревьями скиллов (Mining ⛏️, Combat ⚔️, Woodcutting 🪓, Skinning 🔪, Harvesting 🌿, Medical ⚕️, Build_Craft 🔨, Scavenging 🔍, Vehicles 🚁, Cooking 🍖, Underwater 🌊, Raiding 💣, Team 👥)
- 136 скиллов, сгруппированных по тирам (Tier 1, 2, 3, Ultimate)
- Поиск по названиям, lazy loading изображений
- Выбор режима: ANY (любой скилл) / TREE (любой в дереве) / SKILL (конкретный)
- Для SKILL mode: кнопки уровня 1-5 и "Любой" (выбор целевого уровня прямо в модалке)
Quest Form (HOW MANY — сколько раз):
- Требуется максимальный уровень (checkbox): засчитываются только прокачки до max level
- Количество прокачек (input): сколько успешных прокачек нужно
- SKILL mode: выбираешь конкретный скилл + уровень в модалке → upgradeCount обычно = 1 (один скилл можно прокачать до уровня только раз)
- TREE mode: выбираешь дерево в модалке → upgradeCount = сколько РАЗНЫХ скиллов в дереве нужно прокачать
- ANY mode: выбираешь "любой скилл" → upgradeCount = сколько любых прокачек нужно
Примеры квестов:
- Прокачай 10 любых скиллов:
tree=null, skillName=null, upgradeCount=10 - Прокачай 3 скилла Mining до max:
tree="Mining", skillName=null, requireMax=true, upgradeCount=3→ засчитываются РАЗНЫЕ скиллы (Mining_Yield, XP_Catalyst, Expert_Miner) - Прокачай Duelist до уровня 3:
tree="Combat", skillName="Duelist", targetLevel=3, upgradeCount=1 - Прокачай Mining_Yield до max:
tree="Mining", skillName="Mining_Yield", requireMax=true, upgradeCount=1
Требуется установленный плагин SkillTree на Rust сервере. GoLootTracker перехватывает hook STOnNodeLevelUp и отправляет SKILL_UPGRADED webhook при каждой прокачке.
Core Mechanics
1. Shared Progress
Одно действие засчитывается во ВСЕ подходящие квесты одновременно.
Игрок прошёл квиз из категории "Оружие"
→ +1 к квесту "Пройди 5 квизов про оружие"
→ +1 к квесту "Пройди 10 любых квизов"
→ +1 к ежедневному "Пройди 3 квиза"
QuestProgressService.incrementCategoryProgress() ищет ВСЕ активные квесты нужной категории, фильтрует по conditions, увеличивает прогресс в каждом в рамках одной транзакции.
2. Lazy Reset
Прогресс DAILY/WEEKLY квестов сбрасывается НЕ по cron, а при первом запросе списка квестов после истечения периода.
Пользователь запрашивает /quests
↓
Для каждого userQuest: lastResetAt в том же дне/неделе?
↓
Если нет → сбросить currentProgress = 0, status = IN_PROGRESS
Если квест COMPLETED но награда не забрана — сброс НЕ происходит. Пользователь может забрать награду в следующую сессию.
3. Quest Rotation System
Из большого пула DAILY/WEEKLY/PERMANENT квестов автоматически выбираются фиксированные наборы для показа пользователям.
- Детерминированность: Все пользователи видят одинаковые квесты (seeded shuffle)
- Отдельный пул Rust: Rust квесты ротируются независимо от обычных
- Запрет повторов: Квесты не повторяются в течение сезона (пока пул не исчерпан)
- Персональные исключения: Выполненные isUnique квесты исключаются из ротации
Лимиты (хранятся в модели Season):
| Тип | Regular | Rust | Период |
|---|---|---|---|
| DAILY | dailyQuestLimit (default 3) | dailyRustQuestLimit (default 2) | Каждый день |
| WEEKLY | weeklyQuestLimit (default 1) | weeklyRustQuestLimit (default 1) | Каждую неделю |
| PERMANENT | Все | Все | Весь сезон |
Алгоритм выбора (stateless, deterministic):
1. Загрузить лимиты из Season model
2. Загрузить активные квесты сезона (через SeasonQuest)
3. Получить персональные исключения пользователя (isUnique)
4. Разделить квесты на regular и rust пулы
5. Применить исключения
6. Для DAILY/WEEKLY:
- Seeded shuffle пула (seed = type + seasonId)
- Pool cycling: startIdx = (offset * limit) % poolSize
- offset = дней/недель с начала сезона
7. PERMANENT: показать все
8. EVENT: фильтр по startAt/endAt
Seed для детерминированности:
| Тип | Формат seed | Пример |
|---|---|---|
| DAILY regular | daily-{seasonId} | daily-season-1 |
| DAILY rust | daily-{seasonId}-rust | daily-season-1-rust |
| WEEKLY regular | weekly-{seasonId} | weekly-season-1 |
| WEEKLY rust | weekly-{seasonId}-rust | weekly-season-1-rust |
4. Unique Quests
Квесты с isUnique: true можно получить награду только один раз.
- Двойная защита: По
telegramIdИ поsteamId(если привязан) - Запись в
ClaimedUniqueQuestпри claim - Запись в
UserQuestExclusion— исключает квест из ротации для пользователя - При повторном claim — статус синхронизируется (CLAIMED), но награда не выдается
Защита работает в два уровня:
- По
telegramId— основная проверка - По
steamId— дополнительная (если Steam привязан к нескольким Telegram аккаунтам)
5. RUST Achievements Integration
При claim награды за RUST квест автоматически обновляется прогресс RUST достижений.
Логика:
- Пользователь выполнил Rust квест (например, "Убей 100 медведей", targetProgress=100)
- Вызывает
/quests/:id/claim→ получает награду квеста (Scrap, XP, Item) - Автоматически (через
setImmediate) → прогресс RUST достижений +100 - Все RUST достижения с подходящими условиями получают прогресс
Пример:
Квест "Убей 100 медведей" (targetProgress=100) выполнен
Подходящие достижения получат +100:
→ "Выполни квесты на 500 убийств медведей" (rustKillAnimalType=BEAR)
→ "Выполни квесты на 1000 убийств животных" (rustEventType=KILL_ANIMAL)
→ "Выполни 100 любых Rust квестов" (conditions=null)
RUST достижения накапливают прогресс по значению quest.targetProgress, а не фиксированный +1 как другие категории.
Это позволяет создавать масштабные долгосрочные достижения: "Выполни квесты на 10000 убийств" выполнится после ~100 квестов "Убей 100 врагов".
Используется setImmediate() — если обновление достижения упадёт с ошибкой, пользователь всё равно получит награду за квест. Ошибки логируются, но не блокируют выдачу награды.
Полная документация: Achievements - RUST Integration
Reward Types
| Тип | Описание | Поле |
|---|---|---|
SCRAP | Основная валюта | amount |
XP | Опыт для прокачки уровня | amount |
STREAK_POINTS | Валюта лояльности (SP) | amount |
ITEM | Предмет в инвентарь | itemId |
CASE | Купон на бесплатное открытие кейса | caseId |
Reward API Response
API возвращает расширенную информацию о награде для визуального отображения:
| Поле | Тип | Описание |
|---|---|---|
itemImageUrl | string? | URL изображения предмета |
itemType | enum? | Тип предмета: SKIN, BLUEPRINT, FRAGMENT, RESOURCE, BUFF |
itemTier | enum? | Тир предмета: TIER_0 — TIER_5 |
targetSkinImageUrl | string? | URL изображения базового скина (для BLUEPRINT/FRAGMENT) |
caseImageUrl | string? | URL изображения кейса |
Для типов BLUEPRINT и FRAGMENT фронтенд отображает композитные иконки: базовое изображение скина (targetSkinImageUrl) с оверлеем значка типа (blueprintbase.webp или blueprint_{tier}.webp).
Difficulty Levels (Сложность)
Опциональное поле difficulty (1-3) позволяет указать уровень сложности квеста:
| Значение | Уровень | Цвет badge |
|---|---|---|
| 1 | Легкий | Зелёный |
| 2 | Средний | Жёлтый |
| 3 | Сложный | Красный |
UI отображение:
- TMA (QuestCard):
DifficultyBadgeрядом с заголовком квеста - Admin (QuestTable): Колонка "Сложность" с цветным badge
- Admin (QuestForm): Select "Уровень сложности" в секции "Сложность"
DifficultyBadge используется как в квестах, так и в достижениях. Компонент поддерживает размеры sm и md.
Quest Hints (Подсказки)
Опциональное поле hint (до 300 символов) позволяет добавить подсказку к квесту:
- В админке: Textarea в форме создания/редактирования квеста
- В TMA: Иконка ⓘ рядом с описанием (показывается только если hint заполнен)
- При клике: Модалка в glass morphism стиле с текстом подсказки и кнопкой "Понятно"
Подсказки полезны для квестов со сложными условиями или неочевидными способами выполнения. Например: "Откройте раздел 'Рулетка' в меню" для квеста на spin.
При создании нового Rust квеста в админке поле hint автоматически заполняется текстом: "💡 Напиши /goloot на сервере чтобы начать трекинг прогресса".
Почему: Игрок, который взял Rust квест будучи уже на сервере, не получит прогресс до следующего TIME_UPDATE webhook (0-5 минут). Команда /goloot заставляет плагин синхронизироваться немедленно.
Admin workflow: Подсказка заполняется автоматически при выборе категории RUST, но админ может отредактировать или очистить текст по необходимости.
Admin UI: Rust Item Picker
Модальное окно выбора предметов для RUST квестов (LOOT_ITEM, ITEM_CRAFTED).
Возможности:
- 933 предмета из rustclash.com вместо ранних 258
- 14 категорий в виде горизонтальных табов: Weapons, Construction, Items, Resources, Attire, Tools, Medical, Food, Ammo, Traps, Misc, Components, Electrical, Fun
- Hybrid filtering: Глобальный поиск по всем предметам или фильтрация по активной категории
- English names: Все названия на английском (из официальной документации Rust)
- Lazy loading: Изображения загружаются по мере прокрутки
- Custom shortname: Возможность ввести shortname вручную (для редких предметов)
UI Flow:
- Админ открывает форму создания RUST квеста (LOOT_ITEM или CRAFT)
- Клик на поле "Item Shortname" → открывается ItemSelectModal
- Выбор категории из табов или использование глобального поиска
- Клик на предмет → превью с названием и shortname
- Подтверждение → shortname заполняется в форму квеста
Технические детали:
- Каталог:
admin/src/constants/rust-item-catalog.ts - Компонент:
admin/src/components/quests/ItemSelectModal.tsx - CDN изображений:
https://wiki.rustclash.com/img/items40/{shortname}.png - Fallback: Placeholder "?" при ошибке загрузки изображения
Старый каталог (258 items) полностью заменён новым (933 items). Legacy функции в rust-loot.ts обеспечивают совместимость с существующим кодом через @deprecated обёртки.
Smart Quest Generation (Blueprints)
Сезонные квесты генерируются автоматически из code-defined блюпринтов. Каждый блюпринт описывает шаблон генерации с creative русскоязычными названиями, Russian pluralization и условиями.
Блюпринты: quest-blueprints.ts (29 шт.)
Генератор: season-quest-generator.service.ts
Blueprint Matrix
CASES (3 блюпринта) — действия открытия:
| ID | Type | Mode | Target | Diff |
|---|---|---|---|---|
cases-open-any-daily | DAILY | generic | 5–15 (×5) | Easy |
cases-open-specific-daily | DAILY | per-case | 1–3 (×1) | Medium |
cases-open-specific-weekly | WEEKLY | per-case | 3–10 (×1) | Medium |
QUIZ (3 блюпринта):
| ID | Type | Mode | Target | Diff |
|---|---|---|---|---|
quiz-any-daily | DAILY | generic | 1 (fixed) | Easy |
quiz-any-weekly | WEEKLY | generic | 5 (fixed) | Easy |
quiz-any-permanent | PERMANENT | generic | 15 (fixed) | Medium |
Quiz-квесты используют только generic режим ("ответь на любой квиз"). Специфичные задания по категориям/предметам/подкатегориям будут реализованы как Achievement templates — долгосрочные цели, для которых не нужна координация с публикацией квизов.
RECYCLE (8 блюпринтов):
| ID | Type | Mode | Target | Diff |
|---|---|---|---|---|
recycle-any-daily | DAILY | generic | 1–5 (×1) | Easy |
recycle-blueprint-daily | DAILY | generic | 1–3 (×1) | Easy |
recycle-fragment-daily | DAILY | generic | 1–3 (×1) | Easy |
recycle-tier-daily | DAILY | generic | 1–2 (×1) | Medium |
recycle-tier-weekly | WEEKLY | generic | 1–5 (×1) | Medium |
recycle-blueprint-tier-weekly | WEEKLY | generic | 2–5 (×1) | Medium |
recycle-fragment-tier-weekly | WEEKLY | generic | 2–5 (×1) | Medium |
recycle-item-weekly | WEEKLY | per-item | 1 (fixed) | Hard |
COLLECTION (12 блюпринтов) — content-aware результаты из кейсов + рулеток:
| ID | Type | Mode | Target | Diff | Условия |
|---|---|---|---|---|---|
collection-resource-daily | DAILY | generic | 3–10 (×1) | Easy | itemType: RESOURCE |
collection-fragment-daily | DAILY | generic | 1–3 (×1) | Medium | itemType: FRAGMENT |
collection-scrap-daily | DAILY | generic | 50–300 (×50) | Easy | rewardType: SCRAP |
collection-item-daily | DAILY | per-item | 1 (fixed) | Easy | itemId: {id} |
collection-tier-weekly | WEEKLY | generic | 1–5 (×1) | Medium | itemTier: 2 |
collection-blueprint-weekly | WEEKLY | generic | 1–3 (×1) | Medium | itemType: BLUEPRINT |
collection-scrap-weekly | WEEKLY | generic | 300–1500 (×150) | Easy | rewardType: SCRAP |
collection-item-weekly | WEEKLY | per-item | 1 (fixed) | Medium | itemId: {id} |
collection-resource-weekly | WEEKLY | generic | 15–50 (×5) | Easy | itemType: RESOURCE |
collection-tier-permanent | PERMANENT | generic | 1–5 (×1) | 1→3 | itemTier: 3 |
collection-item-permanent | PERMANENT | per-item | 1 (fixed) | Hard | itemId: {id}, isUnique |
collection-scrap-permanent | PERMANENT | generic | 2000–10000 (×1000) | Hard | rewardType: SCRAP |
COLLECTION блюпринты анализируют реальный контент сезона (предметы в кейсах и рулетках). Если в сезоне нет ресурсов — collection-resource-* блюпринты не генерируются. Скрап-квесты динамически капируются: avgScrapPerDrop × 3 drops/day × periodMultiplier.
SOCIAL (1), SPECIAL (2):
| ID | Category | Type | Mode | Target | Diff |
|---|---|---|---|---|---|
social-referral-permanent | SOCIAL | PERMANENT | generic | 1–5 (×1) | Medium |
special-level-permanent | SPECIAL | PERMANENT | generic | 5–20 (×5) | Medium |
special-streak-weekly | SPECIAL | WEEKLY | generic | 3–7 (×1) | Medium |
Generation Modes
| Режим | Описание | Варианты |
|---|---|---|
generic | Без привязки к контенту | До 3 (DAILY/WEEKLY), 1 (PERMANENT) |
per-case | По 1 квесту на каждый кейс сезона | {caseName} в названии |
per-item | По 1 квесту на предмет из пула (по difficulty) | {itemName} в названии |
Когда блюпринт генерирует >1 варианта (generic mode), к названию добавляются римские цифры: "Эрудит I", "Эрудит II", "Эрудит III".
Content-Aware Generation
Генератор анализирует реальный контент сезона через analyzeSeasonContent():
- Cases + Spins — все предметы и награды из привязанных кейсов и рулеток
- Item deduplication — один предмет может быть в нескольких кейсах; берётся лучший dropChance
- Difficulty mapping — dropChance → difficulty: ≥5% = Easy(1), 1-5% = Medium(2), <1% = Hard(3)
- Items by type — группировка по
itemType(SKIN, BLUEPRINT, FRAGMENT, RESOURCE, BUFF) - Scrap analysis — минимальный/средний/максимальный скрап из наград
COLLECTION per-item блюпринты получают пул предметов по difficulty (DAILY → Easy, WEEKLY → Medium, PERMANENT → Hard).
Скрап-квесты динамически капируются на основе avgScrapPerDrop × ESTIMATED_DROPS_PER_DAY (3) × periodMultiplier.
Placeholder Rewards
Генерируемые квесты получают SCRAP награды по difficulty:
| Difficulty | Reward |
|---|---|
| Easy (1) | 50 Scrap |
| Medium (2) | 100 Scrap |
| Hard (3) | 150 Scrap |
Админ настраивает финальные награды в Step 6 Setup Wizard после генерации.
Protection
| Действие | Rate Limit | Auth | Validation |
|---|---|---|---|
| Get quests | general (100/min) | Telegram | GetUserQuestsSchema |
| Get quest by ID | general (100/min) | Telegram | GetUserQuestByIdSchema |
| Claim reward | achievementClaim (15/min) | Telegram + Active Season | ClaimQuestRewardSchema |
| Check quest | general (100/min) | Telegram + Active Season | CheckQuestSchema |
| Start Rust quest | general (100/min) | Telegram + Active Season | StartRustQuestSchema |
См. Security Matrix для полного обзора защит.
Edge Cases
Что видит пользователь (UI):
| Ситуация | UI поведение |
|---|---|
| RUST квест без старта | Кнопка "Начать квест", статус AVAILABLE |
| Внутренний квест без прогресса | "Ожидает прогресса", статус PENDING |
| Квест с прогрессом | "Выполняется...", показан прогресс X/Y |
| Квест выполнен | Кнопка "Забрать" активна, анимация готовности |
| Награда получена | Статус CLAIMED, карточка скрыта |
| Claim награды (UI) | Fade-out анимация 300ms → карточка исчезает из списка |
| После claim | Квесты остаются на месте (не "прыгают" вниз) |
| Claim последнего квеста на странице | Автоматический переход на предыдущую страницу |
| Daily reset прошёл | Прогресс сброшен, квест снова IN_PROGRESS |
| Уникальный уже получен | При claim — статус синхронизируется без ошибки |
| Квест с подсказкой | Иконка ⓘ рядом с описанием → модалка с hint при клике |
| Квест без подсказки | Иконка ⓘ не отображается |
| Квест с difficulty | DifficultyBadge рядом с заголовком (1=зелёный, 2=жёлтый, 3=красный) |
| Квест без difficulty | DifficultyBadge не отображается |
Отображение наград (RewardIcon):
| Тип награды | Отображение |
|---|---|
SCRAP / XP / SP | Компонент Currency с иконкой и суммой |
ITEM (SKIN/RESOURCE) | Изображение предмета 48×48px |
ITEM (BLUEPRINT) | Изображение targetSkin + оверлей blueprintbase.webp |
ITEM (FRAGMENT) | Изображение targetSkin + оверлей blueprint_{tier}.webp |
ITEM (BUFF) | Изображение или иконка Zap (fallback) |
CASE | Изображение кейса 48×48px |
| Количество > 1 | Badge "×N" в правом нижнем углу |
| Нет изображения | Иконка Package (fallback) |
Backend Error Codes (для API/тестов)
| Код | HTTP | Сообщение пользователю |
|---|---|---|
QUEST_NOT_FOUND | 404 | "Квест не найден" |
NOT_COMPLETED | 400 | "Квест еще не выполнен" |
ALREADY_CLAIMED | 400 | "Награда уже получена" |
UNIQUE_ALREADY_CLAIMED | 400 | "Эта награда уже была получена ранее" |
REWARD_NOT_FOUND | 500 | "Награда не найдена" |
ITEM_NOT_FOUND | 500 | "Предмет награды не найден" |
CASE_NOT_FOUND | 500 | "Кейс награды не найден" |
3. ADR (Architectural Decisions)
Почему Shared Progress, а не отдельный трекинг?
Проблема: При каждом действии нужно засчитывать прогресс в нескольких квестах одновременно.
Решение: Паттерн Shared Progress — incrementCategoryProgress() находит ВСЕ подходящие квесты и увеличивает прогресс в рамках одной транзакции.
Альтернативы (отклонены):
- Event sourcing с отложенной обработкой — сложно, задержки в UI
- Отдельные счетчики на каждый квест — дублирование логики, race conditions
Последствия: Простота интеграции (один вызов из любого домена), атомарность, но нужна транзакция на запись.
Почему Lazy Reset, а не cron?
Проблема: Нужно сбрасывать прогресс DAILY/WEEKLY квестов без блокировки на всю базу.
Решение: Lazy Reset — сброс при первом обращении пользователя. Проверка lastResetAt vs текущего периода.
Альтернативы (отклонены):
- Cron job с массовым UPDATE — блокировка таблицы, пиковая нагрузка в полночь
- TTL в Redis — сложность консистентности с PostgreSQL
Последствия: Равномерная нагрузка, но первый запрос дня чуть медленнее.
Почему детерминированная ротация?
Проблема: При случайном выборе квестов разные пользователи видят разные задания — нельзя обсуждать в сообществе.
Решение: Seeded shuffle на основе даты — все видят одинаковые квесты в один день.
Последствия: Единый игровой опыт, возможность обсуждения "квеста дня", но меньше персонализации.
Quiz-Quest Coordination: почему quiz-квесты только generic?
Проблема: Две независимые системы не координированы:
- Публикация квизов — случайный выбор 3 квизов/день из неопубликованного пула, отправка в Telegram
- Ротация квестов — детерминированный выбор дневных/недельных квестов из пула
Специфичные quiz-квесты (по категории, подкатегории, slug, entityType) были бы часто невыполнимы: если дневной квест требует квиз про оружие, а сегодня опубликованы квизы про еду — квест невыполним.
Решение: Quiz-квесты используют только generic режим ("ответь на любой квиз") с фиксированными таргетами: daily=1, weekly=5, permanent=15. Специфичные quiz-задания (по категории/подкатегории/slug/entityType) будут реализованы как Achievement templates — долгосрочные цели на весь сезон, для которых проблема координации не критична.
Альтернативы (отклонены):
- Специфичные quiz-квесты как PERMANENT — по-прежнему создают неудобства (ждать нужный квиз) и загромождают пул квестов
- Координация публикации с ротацией квестов — сложная связность, breaking existing architecture
- Quiz Catalog в TMA (все квизы доступны без публикации) — меняет бизнес-модель, снижает ценность Telegram-канала
Последствия:
- Чистое разделение: квесты = широкие задания, достижения = точечные цели
- Нет проблемы с координацией — любой опубликованный квиз засчитывается
PERMANENT_POOL_MAXостаётся 10 (quiz permanent не раздувает пул)
4. Architecture
Flow: Прохождение квиза → Прогресс квеста
Flow: Claim награды
Key Components
| Компонент | Путь | Описание |
|---|---|---|
| QuestService | backend/src/domains/quests/services/quest.service.ts | Получение квестов с прогрессом, ротация |
| QuestProgressService | backend/src/domains/quests/services/quest-progress.service.ts | Shared Progress — увеличение прогресса |
| QuestRewardService | backend/src/domains/quests/services/quest-reward.service.ts | Выдача наград |
| QuestResetService | backend/src/domains/quests/services/quest-reset.service.ts | Lazy Reset для DAILY/WEEKLY |
| QuestRotationService | backend/src/domains/quests/services/quest-rotation.service.ts | Stateless deterministic ротация квестов |
| TelegramSubscriptionService | backend/src/domains/quests/services/telegram-subscription.service.ts | Проверка подписки через Telegram API |
| QuestController | backend/src/domains/quests/controllers/quest.controller.ts | User API контроллер |
| QuestAdminController | backend/src/domains/quests/controllers/quest-admin.controller.ts | Admin API контроллер |
| Routes | backend/src/domains/quests/routes/quests.routes.ts | User API роуты |
| Admin Routes | backend/src/domains/quests/routes/admin-quests.routes.ts | Admin API роуты |
| Schemas | backend/src/domains/quests/schemas/quest.schemas.ts | Валидация запросов |
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| Quest | Конфигурация квеста | title, description, hint, type, category, targetProgress, difficulty, rewardId, conditions, isUnique, isPriority |
| UserQuest | Прогресс пользователя | userId, questId, currentProgress, status, lastResetAt, rewardSnapshot |
| ClaimedUniqueQuest | Уникальные награды по telegramId/steamId | telegramId, steamId, questId |
| Reward | Награда (общая модель) | type, amount, amountMax, itemId, caseId |
| UserQuestExclusion | Персональные исключения (isUnique) | telegramId, steamId, questId, seasonId, reason |
Reward Audit (rewardSnapshot)
При claim награды в UserQuest.rewardSnapshot сохраняется JSON снимок:
{
"type": "ITEM",
"amount": 1,
"itemId": "item-123",
"itemName": "AK-47 Blueprint",
"itemTier": "TIER_3"
}
Snapshot фиксирует выданную награду на момент claim. Если квест или награда изменится — аудит сохранит оригинальные значения для истории операций пользователя.
Quest Statuses (Backend)
| Статус | Описание |
|---|---|
IN_PROGRESS | Активен, прогресс засчитывается |
COMPLETED | Выполнен, ожидает claim |
CLAIMED | Награда получена |
Quest UI Statuses (Frontend)
Frontend расширяет backend статусы дополнительными состояниями для UX:
| UI Статус | Условие | Кнопка / Отображение |
|---|---|---|
AVAILABLE | RUST квест без startedAt | "Начать квест" |
PENDING | Не-RUST квест с currentProgress = 0 | "Ожидает прогресса" |
IN_PROGRESS | currentProgress > 0 и < targetProgress | "Выполняется..." |
COMPLETED | currentProgress >= targetProgress, не claimed | "Забрать" (активна) |
CLAIMED | Награда получена | Не отображается |
RUST квесты требуют явного старта через "Начать квест" — это создаёт Steam сессию и проверяет привязку.
Внутренние квесты (QUIZ, CASES, SOCIAL, etc.) используют Shared Progress — прогресс засчитывается автоматически при первом действии. Статус PENDING показывает, что квест ждёт первого действия пользователя.
Квесты сортируются по UI статусу: AVAILABLE → COMPLETED → IN_PROGRESS → PENDING → CLAIMED.
Это гарантирует, что:
- RUST квесты готовые к старту — первыми
- Квесты готовые к claim — вторыми
- Квесты с прогрессом — перед "ожидающими"
Relationships
Key Indexes
@@index([type]) -- Фильтрация по типу (DAILY, WEEKLY, etc.)
@@index([category]) -- Фильтрация по категории (QUIZ, CASES, etc.)
@@index([isActive]) -- Только активные квесты
@@index([isPriority]) -- Сортировка приоритетных первыми
@@index([startAt, endAt]) -- EVENT квесты в диапазоне дат
6. API Endpoints
- User API
- Admin: Management
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /api/quests | Список квестов с прогрессом | → |
| GET | /api/quests/:id | Детали квеста | → |
| POST | /api/quests/:id/claim | Получение награды | → |
| POST | /api/quests/:id/check | Проверка выполнения (SOCIAL) | → |
| POST | /api/quests/:id/start | Начать Rust квест | → |
| Метод | Эндпоинт | Описание | Docs |
|---|---|---|---|
| GET | /admin/quests | Все квесты с пагинацией | → |
| GET | /admin/quests/:id | Детали квеста | → |
| GET | /admin/quests/:id/stats | Статистика квеста | → |
| POST | /admin/quests | Создание квеста | → |
| PUT | /admin/quests/:id | Обновление квеста | → |
| DELETE | /admin/quests/:id | Удаление квеста | → |
| PUT | /admin/quests/:id/toggle-active | Вкл/выкл квест | → |
| POST | /admin/quests/bulk-action | Массовые операции | → |
7. Related
- Seasons — квесты привязаны к сезону через SeasonQuest, лимиты ротации в Season model
- Cases — квесты категории CASES отслеживают открытия кейсов
- Quizzes — квесты категории QUIZ отслеживают прохождение квизов
- Achievements — похожая механика с условиями и наградами
- Streaks — SPECIAL квесты с условием
streakDays - Referrals — SOCIAL квесты на приглашение друзей
- Rust Integration — квесты категории RUST
- Inventory — куда попадают ITEM награды