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: Из пула выбираются 2 квеста детерминированно (одинаковые для всех)
Цель: Retention — причина заходить каждый день
Срок: До понедельника 00:00 UTC
Reset: Автоматический сброс в начале недели (Lazy Reset)
Rotation: Из пула выбирается 1 квест детерминированно
Цель: Retention — награда за стабильную активность в течение недели
Срок: startAt → endAt (задается вручную)
Reset: Нет сброса
Примеры: "Открой 10 кейсов за время акции"
Цель: Spike активности в нужный период (маркетинг, праздники)
Quest Categories (Типы действий)
- QUIZ
- CASES
- SOCIAL
- SPECIAL
- STREAK
- COLLECTION
- RECYCLE
- ECONOMY
- PROGRESSION
- 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— достичь уровня N
Пример: "Достигни 10 уровня" → { minLevel: 10 }
SPECIAL квесты проверяются автоматически при обновлении уровня. Если текущий уровень >= условия — квест завершается мгновенно.
Действие: Поддержание ежедневного стрика
Conditions:
streakDays— удерживать стрик N дней подряд
Пример: "Достигни 7-дневного стрика" → { streakDays: 7 }
STREAK квесты проверяются автоматически при 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" }
Действие: Заработок и трата Scrap (централизованный tracking через ScrapService)
Conditions:
progressType— дискриминатор:scrap-earn|scrap-spendscrapSource— источник заработка:cases|quizzes|quests|achievements|referrals|spins(только для earn)scrapSpendSource— источник траты:cases|craft|spins(только для spend)
Примеры:
- "Заработай 500 Scrap" →
{ progressType: "scrap-earn" } - "Заработай 200 Scrap из кейсов" →
{ progressType: "scrap-earn", scrapSource: "cases" } - "Потрать 300 Scrap" →
{ progressType: "scrap-spend" }
progressType предотвращает cross-matching: scrap-earn квесты не засчитывают траты, и наоборот. Пустой scrapSource/scrapSpendSource = match all sources.
Действие: Заработок XP (централизованный tracking через XPService)
Conditions:
progressType— дискриминатор:xp-earnxpSource— источник XP:cases|quizzes|quests|achievements|spins|salvage(опционально)
Примеры:
- "Заработай 1000 XP" →
{ progressType: "xp-earn" } - "Заработай 500 XP из квизов" →
{ progressType: "xp-earn", xpSource: "quizzes" }
При генерации ~33% квестов получают конкретный source, остальные — "из любого источника". Это создаёт вариативность.
Действие: Активность в игре Rust (через плагин на сервере)
Требования: Привязанный Steam аккаунт, запуск квеста через /start
RUST квесты используют интеграцию с игровыми серверами через Webhook API: сессии игроков, 17 типов событий (TIME, COMMAND, GATHER, LOOT_CONTAINER, LOOT_ITEM, KILL_ANIMAL, KILL_SCIENTIST, CRAFT, RECYCLE, EXPLOSIVE_USED, SKILL_UPGRADED, FISH, SKILL_MAXED, TEA_BREWED, FARMING_HARVEST, PIE_COOKED, MISSION_COMPLETED).
Новое: EXPLOSIVE_USED поддерживает гранулярные типы взрывчатки — можно создавать квесты на конкретные ракеты (MLRS, HV, Fire, Basic) и гранаты (F1, Beancan).
Дополнительные подтипы:
- FISH — ловля рыбы (
rustFishType: ANY или конкретный shortname,rustFishCount) - TEA_BREWED — варка чая на MixingTable (
rustTeaType: ANY или shortname,rustTeaCount) - FARMING_HARVEST — сбор урожая (
rustHarvestType: ANY/BERRY/shortname,rustHarvestCount). 10 культур: овощи (corn, pumpkin, potato, hemp) и ягоды (red.berry, blue.berry, green.berry, white.berry, yellow.berry, black.berry). Группы: ANY (любой урожай), BERRY (любая ягода) - PIE_COOKED — выпечка пирогов в печи (
rustPieType: ANY или shortname,rustPieCount) - MISSION_COMPLETED — завершение NPC миссий (
missionShortname: ANY/exact/comma-list,missionCount). 17 миссий: охота (boar_hunt, deer_hunt, wildlife_cull, shark_hunt), рыбалка (go_fish), exploration (an_important_broadcast, outpost_validation), PvE (oil_rig_raid), и другие. Без батчинга — 1 миссия = 1 webhook
Полная документация: 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, увеличивает прогресс в каждом в рамках одной транзакции.
Quest progress hooks вызываются ПОСЛЕ завершения $transaction, не внутри неё. Это предотвращает deadlock-и и обеспечивает атомарность бизнес-операции.
// Правильно:
const result = await this.prisma.$transaction(async (tx) => { ... });
await this.questProgressService.incrementCategoryProgress(userId, 'ECONOMY', conditions, amount);
// Неправильно — внутри транзакции:
await this.prisma.$transaction(async (tx) => {
await this.questProgressService.incrementCategoryProgress(...); // ЗАПРЕЩЕНО
});
2. Lazy Reset (Batch)
Прогресс DAILY/WEEKLY квестов сбрасывается НЕ по cron, а при первом запросе списка квестов после истечения периода. Используется batch-оптимизация для снижения N+1 queries.
Пользователь запрашивает /quests
↓
shouldReset() — pure function проверяет каждый userQuest в памяти
↓
Собирает IDs для сброса → resetBatch() одним UPDATE
Если квест COMPLETED но награда не забрана — сброс НЕ происходит. Пользователь может забрать награду в следующую сессию.
3. Quest Rotation System
Из большого пула DAILY/WEEKLY/PERMANENT квестов автоматически выбираются фиксированные наборы для показа пользователям.
- Детерминированность: Все пользователи видят одинаковые квесты (seeded shuffle)
- Отдельный пул Rust: Rust квесты ротируются независимо от обычных
- Запрет повторов: Квесты не повторяются в течение сезона (пока пул не исчерпан)
- Персональные исключения: Выполненные isUnique квесты исключаются из ротации
Лимиты (хранятся в модели Season):
| Тип | Regular | Rust | Период |
|---|---|---|---|
| DAILY | dailyQuestLimit (default 2) | 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. Reward Pool (Ограниченный пул наград)
Квесты с rewardPool поддерживают конечное количество наград (first-come-first-served). Создаёт ощущение срочности и конкуренции.
- Любой тип награды: ITEM, SCRAP, XP, CASE, STREAK_POINTS
- Одна награда на юзера: Pool-квест автоматически
isUnique = true - Только PERMANENT и EVENT: DAILY/WEEKLY не поддерживаются (validation error)
- Счётчик: UI показывает "Осталось: N" (без total)
- Исчерпанный квест: Остаётся видимым, смещается вниз в списке
- Dismiss: Через
UserQuestExclusionс reasonpool_exhausted - Admin: Может увеличить пул, но не уменьшить ниже уже забранного (
rewardPoolClaimed)
Атомарность claim:
UPDATE quests SET reward_pool_claimed = reward_pool_claimed + 1
WHERE id = :questId AND reward_pool_claimed < reward_pool
Если rows = 0 → POOL_EXHAUSTED (409). PostgreSQL row-level locking гарантирует, что при конкурентных claim-ах только один получит последний слот.
При claim pool-квеста кэш активных квестов инвалидируется (CachePrefixes.activeQuests), чтобы poolRemaining обновился для других пользователей. Обычные claim-ы не трогают кэш. Thundering herd не проблема — cache-aside pattern: первый miss заполняет кэш.
5. Unique Quests
Квесты с isUnique: true можно получить награду только один раз.
- Двойная защита: По
telegramIdИ поsteamId(если привязан) - Запись в
ClaimedUniqueQuestпри claim - Запись в
UserQuestExclusion— исключает квест из ротации для пользователя - При повторном claim — статус синхронизируется (CLAIMED), но награда не выдается
Защита работает в два уровня:
- По
telegramId— основная проверка - По
steamId— дополнительная (если Steam привязан к нескольким Telegram аккаунтам)
6. RUST Achievements Integration
Прогресс RUST достижений обновляется непосредственно в webhook-обработчиках при получении игровых событий от Rust сервера. Это обеспечивает реальное отслеживание активности игрока.
Логика:
- Rust сервер отправляет webhook с игровым событием (например, KILL_ANIMAL)
- Webhook handler обрабатывает событие и обновляет прогресс квеста
- В том же обработчике обновляется прогресс RUST достижений
- Все RUST достижения с подходящими условиями получают прогресс
Пример:
Квест "Убей 100 медведей" (targetProgress=100) выполнен
Подходящие достижения получат +100:
→ "Выполни квесты на 500 убийств медведей" (rustKillAnimalType=BEAR)
→ "Выполни квесты на 1000 убийств животных" (rustEventType=KILL_ANIMAL)
→ "Выполни 100 любых Rust квестов" (conditions=null)
RUST достижения накапливают прогресс по значению quest.targetProgress, а не фиксированный +1 как другие категории.
Это позволяет создавать масштабные долгосрочные достижения: "Выполни квесты на 10000 убийств" выполнится после ~100 квестов "Убей 100 врагов".
Прогресс достижений обновляется непосредственно в webhook-обработчиках (см. webhook-handlers.service.ts — метод updateAchievementProgress). Если обновление достижения упадёт с ошибкой, основная обработка webhook события не блокируется.
Полная документация: 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 обёртки.
Admin UI: Harvest Picker
Модальное окно выбора культур для RUST квестов (FARMING_HARVEST).
Каталог культур (10 штук):
| Группа | Shortname | Название |
|---|---|---|
| Овощи | corn | Кукуруза |
| Овощи | pumpkin | Тыква |
| Овощи | potato | Картофель |
| Овощи | hemp | Конопля |
| Ягоды | red.berry | Красная ягода |
| Ягоды | blue.berry | Синяя ягода |
| Ягоды | green.berry | Зелёная ягода |
| Ягоды | white.berry | Белая ягода |
| Ягоды | yellow.berry | Жёлтая ягода |
| Ягоды | black.berry | Чёрная ягода |
Групповые цели:
ANY— любой урожай (засчитывается сбор любого)BERRY— любая ягода (любая из 6 видов)
UI Flow:
- Админ создаёт RUST квест с типом FARMING_HARVEST
- Клик на поле "Тип урожая" → открывается HarvestSelectModal
- Фильтрация по типу: Все | Овощи | Ягоды + поиск по названию
- Выбор групповой цели (ANY/BERRY) или конкретной культуры
- Подтверждение → shortname заполняется в форму квеста
Технические детали:
- Каталог:
admin/src/constants/rust-harvest.ts - Компонент:
admin/src/components/quests/HarvestSelectModal.tsx - CDN изображений:
${STATIC_URL}/images/items/rust/Food/{shortname}.png - Fallback: Placeholder при ошибке загрузки изображения
Smart Quest Generation (Blueprints)
Сезонные квесты генерируются автоматически из code-defined блюпринтов. Каждый блюпринт описывает шаблон генерации с creative русскоязычными названиями, Russian pluralization и условиями.
Блюпринты: quest-blueprints.ts (38 шт.)
Генератор: 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 | ⌈seasonDays × 0.5⌉ (динамически) | 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 (10 блюпринтов) — 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-tier-daily | DAILY | generic | 1–3 (×1) | Medium | itemTier: 2, rewardType: ITEM |
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-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 блюпринты анализируют реальный контент сезона (предметы в кейсах и рулетках). Если в сезоне нет ресурсов — collection-resource-* блюпринты не генерируются.
SOCIAL (2 блюпринта):
| ID | Type | Mode | Target | Diff |
|---|---|---|---|---|
social-referral-permanent | PERMANENT | generic | 1–5 (×1) | Medium |
social-subscription | PERMANENT | generic | 1 (fixed) | Easy |
social-subscription — одноразовый квест (isUnique). Проверяет подписку на Telegram канал через Bot API getChatMember. Канал определяется env var TELEGRAM_CHANNEL_USERNAME.
SPECIAL (1 блюпринт):
| ID | Type | Mode | Target | Diff |
|---|---|---|---|---|
special-level-permanent | PERMANENT | generic | [3, 5, 10] (explicit) | 1→2→3 |
STREAK (2 блюпринта):
| ID | Type | Mode | Target | Diff |
|---|---|---|---|---|
special-streak-weekly | WEEKLY | generic | 3–7 (×1) | Medium |
streak-milestone | PERMANENT | generic | 7/14/30 (explicit) | 1→3 |
PROGRESSION (3 блюпринта) — централизованный XP tracking:
| ID | Type | Mode | Target | Diff |
|---|---|---|---|---|
xp-earn-daily | DAILY | generic | 100–500 (×50) | Easy |
xp-earn-weekly | WEEKLY | generic | 500–2000 (×250) | Easy |
xp-earn-permanent | PERMANENT | generic | 5000–20000 (×2500) | Hard |
XP и Scrap квесты могут иметь опциональный фильтр по источнику (cases, quizzes, quests и т.д.). При генерации источник выбирается случайно — ~33% шанс что квест будет "из любого источника".
ECONOMY (6 блюпринтов) — централизованный Scrap tracking:
| ID | Type | Mode | Target | Diff |
|---|---|---|---|---|
scrap-earn-daily | DAILY | generic | 50–300 (×50) | Easy |
scrap-earn-weekly | WEEKLY | generic | 500–2000 (×250) | Easy |
scrap-earn-permanent | PERMANENT | generic | 5000–30000 (×5000) | Hard |
scrap-spend-daily | DAILY | generic | 50–300 (×50) | Easy |
scrap-spend-weekly | WEEKLY | generic | 300–1500 (×150) | Easy |
scrap-spend-permanent | PERMANENT | generic | 3000–15000 (×3000) | Hard |
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).
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 |
| Dismiss pool quest | general (100/min) | Telegram + Active Season | DismissQuestSchema |
См. Security Matrix для полного обзора защит.
Edge Cases
Что видит пользователь (UI):
| Ситуация | UI поведение |
|---|---|
| RUST квест без старта | Кнопка "Начать квест", статус AVAILABLE |
| Внутренний квест без прогресса | "Ожидает прогресса", статус PENDING |
| Квест с прогрессом | "Выполняется...", показан прогресс X/Y |
| Квест выполнен | Кнопка "Забрать" активна, анимация готовности |
| Награда получена | Статус CLAIMED, карточка скрыта |
| Claim награды (UI) | Fade-out анимация 300ms → карточка исчезает из списка |
| После claim | Квесты остаются на месте (не "прыгают" вниз) |
| Claim последнего квеста на странице | Автоматический переход на предыдущую страницу |
| Daily reset прошёл | Прогресс сброшен, квест снова IN_PROGRESS |
| Уникальный уже получен | При claim — статус синхронизируется без ошибки |
| Pool quest: "Осталось: N" | Badge рядом с timeLeft (amber, для remaining > 0) |
| Pool exhausted, юзер без прогресса | "Награды закончились" (серый текст, без кнопки) |
| Pool exhausted, юзер с прогрессом | Кнопка "Понятно" → dismiss (убирает квест) |
| Pool exhausted, юзер COMPLETED | Claim → POOL_EXHAUSTED (409), кнопка "Понятно" |
| 2 concurrent claims, 1 slot | Atomic SQL: один получает reward, второй → POOL_EXHAUSTED (409) |
| Admin увеличивает pool | Безопасно: remaining пересчитывается автоматически |
| Admin уменьшает ниже claimed | Validation error: "Нельзя ниже N" |
| Admin убирает pool (null) | Квест становится безлимитным, rewardPoolClaimed остаётся для аналитики |
| Pool quest + DAILY/WEEKLY | Validation error: "Pool quests cannot be DAILY or WEEKLY" |
| Cache staleness (120s TTL) | Допустимо для display. Claim проверяет real-time через atomic SQL |
| Квест с подсказкой | Иконка ⓘ рядом с описанием → модалка с hint при клике |
| Квест без подсказки | Иконка ⓘ не отображается |
| Квест с difficulty | DifficultyBadge рядом с заголовком (1=зелёный, 2=жёлтый, 3=красный) |
| Квест без difficulty | DifficultyBadge не отображается |
Frontend Polling (автообновление списка квестов):
| Состояние списка | Интервал | Причина |
|---|---|---|
| Пустой список (нет квестов) | 30 сек | Подхватить квесты при активации нового сезона |
| Есть IN_PROGRESS квесты | 15 сек | Быстрое обновление прогресса от webhook-ов |
| Все квесты COMPLETED / нет IN_PROGRESS | 60 сек | Экономия трафика при отсутствии активности |
Ранее при пустом списке квестов polling полностью останавливался. Это приводило к проблеме: при активации нового сезона пользователи с пустым списком никогда не получали новые квесты без ручного обновления страницы. Сейчас polling продолжается с интервалом 30 секунд, что позволяет автоматически подхватить квесты из нового сезона.
Optimistic Updates (мгновенный отклик UI):
Claim и Start используют optimistic updates через React Query — UI обновляется мгновенно, не дожидаясь ответа сервера:
| Действие | Optimistic Update | Rollback при ошибке |
|---|---|---|
| Claim награды | Квест удаляется из кеша, запускается fade-out | Квест восстанавливается из snapshot |
| Start Rust квеста | Статус AVAILABLE → IN_PROGRESS | Статус откатывается к snapshot |
Все hooks (useQuests, useQuestClaim, useQuestStart, useQuestManagement, bootstrapHydration) используют единый QUESTS_QUERY_KEY из useQuests.ts. Это критично для корректной работы optimistic updates — getQueryData и setQueryData требуют точное совпадение ключа (в отличие от invalidateQueries, который работает по partial match).
Отображение наград (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 | "Эта награда уже была получена ранее" |
POOL_EXHAUSTED | 409 | "Награды закончились" |
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 на основе даты — все видят одинаковые квесты в один день.
Последствия: Единый игровой опыт, возможность обсуждения "квеста дня", но меньше персонализации.
Почему централизованный ScrapService?
Проблема: Scrap начислялся/списывался в 13+ местах (cases, quizzes, quests, spins, referrals и т.д.) напрямую через Prisma. Невозможно было отслеживать экономику через квесты без единой точки входа.
Решение: Создан ScrapService (backend/src/domains/users/services/scrap.service.ts) — аналог XPService. Все операции со Scrap проходят через addScrap() / spendScrap() / addScrapWithBuff().
Альтернативы (отклонены):
- Event-based tracking — сложнее, чем прямой вызов; не оправдано для текущего масштаба
Последствия: Единая точка для статистики (scrapFrom{Source}, scrapSpentOn{Source}), сезонных метрик и quest progress hooks.
Почему "Особые" UI grouping для ECONOMY и PROGRESSION?
Проблема: Новые категории ECONOMY и PROGRESSION нужно показывать пользователю, но добавлять отдельные фильтры в UI — перегрузка интерфейса.
Решение: Frontend фильтр "Особые" объединяет SPECIAL + STREAK + ECONOMY + PROGRESSION. Пользователь не различает между "заработай XP" (PROGRESSION), "достигни уровня" (SPECIAL) и "держи стрик" (STREAK).
Альтернативы (отклонены):
- Отдельные фильтры для каждой категории — слишком много UI-элементов, пользователи путаются
Последствия: Чистый UI с минимальным количеством фильтров. Категории различаются только на уровне backend.
Почему Admin/Promo исключены из quest tracking?
Проблема: Admin grants (/admin/grant) и promo-коды начисляют XP/Scrap. Если засчитывать их в quest progress — админ может "выполнять" квесты пользователям.
Решение: Admin grants и promo-code rewards исключены из quest progress tracking. Только "органические" источники (cases, quizzes, spins, referrals, achievements, quests, salvage) засчитываются.
Последствия: Честная система прогресса; промо-акции не влияют на квесты.
Почему progressType discriminator?
Проблема: ECONOMY содержит два типа квестов: "заработай Scrap" и "потрать Scrap". Без дискриминатора один вызов incrementCategoryProgress('ECONOMY', ...) засчитал бы прогресс в оба типа одновременно.
Решение: Поле progressType в QuestConditions (xp-earn | scrap-earn | scrap-spend) обеспечивает точное matching. matchesConditions() использует exact match для progressType.
Альтернативы (отклонены):
- Отдельные категории ECONOMY_EARN / ECONOMY_SPEND — раздувает enum, усложняет UI фильтры
Последствия: Одна категория ECONOMY с подтипами, чистое разделение earn/spend без cross-matching.
Почему COLLECTION scrap перенесён в ECONOMY?
Проблема: В COLLECTION были 3 scrap-related блюпринта ("Набери N скрапа"), которые не имели отношения к коллекционированию предметов.
Решение: Scrap-related блюпринты удалены из COLLECTION (PLAN_03). COLLECTION остаётся чисто item-related (10 блюпринтов). Scrap tracking переехал в ECONOMY с более детальными conditions (по source).
Последствия: Чистое разделение ответственности: COLLECTION = предметы, ECONOMY = валюта. DRY — нет дублирования tracking логики.
Почему rewardPool + rewardPoolClaimed, а не remaining?
Проблема: Нужен счётчик оставшихся наград в pool-квестах. Один столбец remaining или два (rewardPool + rewardPoolClaimed)?
Решение: Два поля: rewardPool (конфигурация, устанавливает админ) + rewardPoolClaimed (атомарный счётчик). remaining = rewardPool - rewardPoolClaimed — вычисляемое.
Альтернативы (отклонены):
- Одно поле
remainingс декрементом — админ не может безопасно менять размер пула (нужно пересчитывать вручную)
Последствия: Админ может менять rewardPool без пересчёта claimed. remaining всегда актуален.
Почему Raw SQL для atomic pool decrement?
Проблема: При конкурентных claim-ах нужна атомарная проверка "claimed < pool" и инкремент.
Решение: Raw SQL UPDATE ... SET claimed = claimed + 1 WHERE claimed < pool внутри транзакции. PostgreSQL row-level locking гарантирует, что второй concurrent UPDATE ждёт commit первого и re-evaluates condition.
Альтернативы (отклонены):
- Prisma
updateMany— не поддерживаетWHERE column_a < column_b - Optimistic locking с version — дополнительная сложность, retry loop
Последствия: Гарантированная атомарность без race conditions. Один raw SQL call.
Почему pool-квесты не DAILY/WEEKLY?
Проблема: Pool-квесты с ограниченным количеством наград + DAILY/WEEKLY reset — конфликт логики.
Решение: Validation error при попытке создать pool-квест с типом DAILY или WEEKLY. Только PERMANENT и EVENT допустимы.
Альтернативы (отклонены):
- Разрешить DAILY/WEEKLY с пулом — непредсказуемое поведение при reset (прогресс сбрасывается, а пул уже исчерпан)
Последствия: Простая и предсказуемая модель: pool = конечный ресурс, без temporal reset.
Quiz-Quest Coordination: почему quiz-квесты только generic?
Проблема: Две независимые системы не координированы:
- Публикация квизов — случайный выбор 3 квизов/день из неопубликованного пула, отправка в Telegram
- Ротация квестов — детерминированный выбор дневных/недельных квестов из пула
Специфичные quiz-квесты (по категории, подкатегории, slug, entityType) были бы часто невыполнимы: если дневной квест требует квиз про оружие, а сегодня опубликованы квизы про еду — квест невыполним.
Решение: Quiz-квесты используют только generic режим ("ответь на любой квиз") с таргетами: daily=1 (fixed), weekly=5 (fixed), permanent=1–100 (dynamic). Специфичные quiz-задания (по категории/подкатегории/slug/entityType) будут реализованы как Achievement templates — долгосрочные цели на весь сезон, для которых проблема координации не критична.
Альтернативы (отклонены):
- Специфичные quiz-квесты как PERMANENT — по-прежнему создают неудобства (ждать нужный квиз) и загромождают пул квестов
- Координация публикации с ротацией квестов — сложная связность, breaking existing architecture
- Quiz Catalog в TMA (все квизы доступны без публикации) — меняет бизнес-модель, снижает ценность Telegram-канала
Последствия:
- Чистое разделение: квесты = широкие задания, достижения = точечные цели
- Нет проблемы с координацией — любой опубликованный квиз засчитывается
PERMANENT_POOL_MAXостаётся 40 (PERMANENT_POOL_MIN + 10, гдеMIN = max(20, blueprintCount × 3)— 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 ротация квестов |
| QuestAdminService | backend/src/domains/quests/services/quest-admin.service.ts | Admin CRUD операции с квестами |
| ScrapService | backend/src/domains/users/services/scrap.service.ts | Централизованный earn/spend Scrap с quest progress hooks |
| QuestImportService | backend/src/domains/quests/services/quest-import.service.ts | Массовый импорт квестов из JSON (dry run + create) |
| TelegramSubscriptionService | backend/src/domains/quests/services/telegram-subscription.service.ts | Проверка подписки через Telegram API |
| QuestController | backend/src/domains/quests/controllers/user-quest.controller.ts | User API контроллер |
| QuestAdminController | backend/src/domains/quests/controllers/admin-quest.controller.ts | Admin API контроллер |
| Routes | backend/src/domains/quests/routes/user-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-base.schemas.ts, user-quests.schemas.ts, admin-quests.schemas.ts | Валидация запросов |
| useQuests | frontend/src/hooks/useQuests.ts | React Query hook — загрузка квестов, polling, экспорт QUESTS_QUERY_KEY |
| useQuestClaim | frontend/src/hooks/useQuestClaim.ts | Mutation hook — claim награды с optimistic update |
| useQuestStart | frontend/src/hooks/useQuestStart.ts | Mutation hook — старт Rust квеста с optimistic update |
| useQuestManagement | frontend/src/components/screens/HomeScreen/hooks/useQuestManagement.ts | Композитный hook — фильтры, пагинация, derived state |
| HarvestSelectModal | admin/src/components/quests/HarvestSelectModal.tsx | Admin: визуальный picker урожая для FARMING_HARVEST |
| rust-harvest.ts | admin/src/constants/rust-harvest.ts | Каталог культур (10 шт), группы, хелперы |
5. Database Schema
Models
| Модель | Описание | Ключевые поля |
|---|---|---|
| Quest | Конфигурация квеста | title, description, hint, type, category, targetProgress, difficulty, rewardId, conditions, isUnique, isPriority, rewardPool, rewardPoolClaimed, rustServerId, rustEventType, rustParams |
| 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 |
| UserFreeCaseOpens | Счетчик бесплатных открытий кейсов (награда типа CASE) | userId, caseId, count, createdAt |
Rust квесты хранят конфигурацию (целевые ресурсы, типы животных, shortnames предметов и т.д.) в одном JSON поле rustParams вместо 37 отдельных колонок. Внутри rustParams поля без rust prefix: fishType, targetMinutes, craftItemShortname.
API response остаётся FLAT с rust prefix для совместимости с фронтендом. Подробнее: Rust Integration — ADR: rustParams
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 | "Забрать" (активна) |
POOL_EXHAUSTED | poolRemaining === 0 и статус !== CLAIMED | "Награды закончились" / "Понятно" (dismiss) |
CLAIMED | Награда получена | Не отображается |
RUST квесты требуют явного старта через "Начать квест" — это создаёт Steam сессию и проверяет привязку.
Внутренние квесты (QUIZ, CASES, SOCIAL, etc.) используют Shared Progress — прогресс засчитывается автоматически при первом действии. Статус PENDING показывает, что квест ждёт первого действия пользователя.
Квесты сортируются по UI статусу: AVAILABLE → COMPLETED → IN_PROGRESS → PENDING → POOL_EXHAUSTED → CLAIMED.
Это гарантирует, что:
- RUST квесты готовые к старту — первыми
- Квесты готовые к claim — вторыми
- Квесты с прогрессом — перед "ожидающими"
- Исчерпанные pool-квесты — внизу (выше claimed)
Relationships
Key Indexes
@@index([type]) -- Фильтрация по типу (DAILY, WEEKLY, etc.)
@@index([category]) -- Фильтрация по категории (QUIZ, CASES, etc.)
@@index([isActive]) -- Только активные квесты
@@index([isPriority]) -- Сортировка приоритетных первыми
@@index([startAt, endAt]) -- EVENT квесты в диапазоне дат
@@index([rustServerId]) -- Фильтрация Rust квестов по серверу
@@index([isActive, category]) -- Compound index для частой фильтрации активных квестов по категории
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 квест | → |
| POST | /api/quests/:id/dismiss | Dismiss исчерпанного pool-квеста | — |
| Метод | Эндпоинт | Описание | 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 | Массовые операции | → |
| POST | /admin/quests/import | Массовый импорт квестов из JSON | → |
7. Related
- Seasons — квесты привязаны к сезону через SeasonQuest, лимиты ротации в Season model
- Cases — квесты категории CASES отслеживают открытия кейсов
- Quizzes — квесты категории QUIZ отслеживают прохождение квизов
- Achievements — похожая механика с условиями и наградами
- Streaks — STREAK квесты с условием
streakDays - Referrals — SOCIAL квесты на приглашение друзей
- Rust Integration — квесты категории RUST
- Inventory — куда попадают ITEM награды