Skip to main content

Craft System

1. Summary

Goal: Система крафта скинов из материалов (FRAGMENT, BLUEPRINT, RESOURCE). Финальный шаг геймификации: собранные из кейсов компоненты конвертируются в реальный скин для вывода в Steam.

User Value: Возможность собрать желаемый скин, а не надеяться на RNG. Путь: Кейсы → Материалы → Крафт → Скин → Вывод в Steam.


2. Business Logic

Craft Materials

Тип: Осколок скина

Получение: Выпадает из кейсов (с буст-шансом в Luck Pool)

Tier: Наследуется от targetSkin

Цель: Основной материал для крафта

Craft Recipe

Каждый скин (Item с itemType: SKIN) имеет рецепт крафта в поле craftRecipe (JSON):

{
"materials": [
{ "itemId": "blueprint-ak47-redline", "quantity": 1 },
{ "itemId": "fragment-ak47-redline", "quantity": 5 },
{ "itemId": "resource-hq-metal", "quantity": 10 }
]
}

Стоимость: Дополнительно списывается craftScrapCost Scrap.

Craft Flow

1. Validation

  • Предмет существует и isActive: true
  • itemType: SKIN (только скины крафтятся)
  • Есть craftRecipe (не null)

2. Resource Check

Порядок проверки
  1. Достаточно Scrap (user.scrap >= craftScrapCost)
  2. Все материалы в инвентаре (сумма по всем sourceType)

3. Atomic Transaction

Всё выполняется в одной транзакции $transaction:

  • Списание Scrap с User
  • Удаление материалов из UserInventory (меньшие стеки первыми)
  • Добавление скрафченного скина в инвентарь (sourceType: CRAFTING)
  • Создание записи в CraftHistory
  • Обновление статистики Item (totalCrafted)
  • Создание FeedEvent (ITEM_CRAFTED)

4. Budget Control Integration

При наличии активного сезона

Если скин имеет priceRub, крафт записывается в CraftBudgetLog и влияет на Budget Control:

  • Expense записывается в текущий BudgetPeriod
  • Если игрок был в Luck Pool, происходит выход из пула
  • Скины с прогрессом ≥50% блокируются до конца месяца

5. Anti-Fraud Check

При >10 крафтов за 24ч отправляется уведомление админам (suspicious activity).

Protection

ДействиеRate LimitAuthValidation
Check craftgeneral (100/min)TelegramCheckCraftAbilitySchema
Craft itemmutations (5/min)Telegram + Active SeasonCraftItemSchema
Get craftablegeneral (100/min)TelegramGetCraftableItemsSchema
Детали реализации

См. Security Matrix для полного обзора защит.

Edge Cases

Что видит пользователь (UI):

СитуацияUI поведение
❌ Недостаточно ScrapКнопка disabled, tooltip "Недостаточно N Scrap"
❌ Недостаточно материаловПоказан прогресс сбора: "3/5 осколков"
⛔ Сезон не активенКнопка disabled, toast "Сезон завершён"
✅ Крафт успешенАнимация + скин в инвентаре
Backend Error Codes (для API/тестов)
КодHTTPСообщение пользователю
INSUFFICIENT_SCRAP400"Недостаточно Scrap для крафта"
MISSING_MATERIALS400"Недостаточно материалов для крафта"
ITEM_NOT_FOUND404"Предмет не найден"
NOT_CRAFTABLE400"Предмет нельзя скрафтить"
INVALID_RECIPE400"Некорректный рецепт крафта"
ITEM_NOT_ACTIVE400"Предмет неактивен"
USER_NOT_FOUND404"Пользователь не найден"
ITEM_NOT_SKIN400"Крафтить можно только скины"

3. ADR (Architectural Decisions)

Почему JSON рецепт, а не реляционная таблица?

Проблема: Нужна гибкая настройка рецептов крафта через админку.

Решение: Рецепт хранится как JSON в Item.craftRecipe.

Альтернативы (отклонены):

  • CraftRecipe + CraftIngredient таблицы — избыточная сложность, рецепт фиксирован на момент крафта
  • Hardcoded рецепты — нельзя менять без деплоя

Последствия: Простота изменения, но требуется валидация JSON при каждом использовании.

Почему атомарные транзакции?

Критично для консистентности

При крафте нужно одновременно списать материалы, списать Scrap И создать скин. Если падает между операциями — потеря данных.

Решение: Prisma $transaction для atomic operations.

Последствия: Гарантированная консистентность, блокировка на время транзакции.

Почему Seniority сбрасывается после крафта?

Проблема: Игрок с максимальным Seniority (×12.9 буст) может накопить материалы и крафтить подряд несколько скинов с бустом.

Решение: После крафта activePeriods сбрасывается до 1, игрок выходит из пула на период.

Альтернативы (отклонены):

  • Не сбрасывать — злоупотребление бустом
  • Полный бан из пула — слишком жёстко

Последствия: Справедливое распределение буста между активными игроками.


4. Architecture

Services Overview

Key Components

КомпонентПутьОписание
CraftServicebackend/src/domains/craft/services/craft.service.tsОсновная бизнес-логика
CraftControllerbackend/src/domains/craft/controllers/user-craft.controller.tsHTTP handlers
Routesbackend/src/domains/craft/routes/user-crafts.routes.tsUser API endpoints
Schemasbackend/src/domains/craft/schemas/user-crafts.schemas.tsВалидация запросов/ответов
Typesbackend/src/domains/craft/types/craft.types.tsTypeScript типы и error codes

5. Database Schema

Models

МодельОписаниеКлючевые поля
ItemПредмет (скин или материал)craftRecipe, craftScrapCost, itemType, tier
CraftHistoryИстория крафтаuserId, itemId, spentScrap, materialsUsed
CraftBudgetLogЛог для Budget ControlskinId, costRub, hadBoost, totalBoostMultiplier

Relationships

Структура CraftHistory
ПолеТипОписание
idStringCUID
userIdStringКто скрафтил
itemIdStringКакой скин получен
spentScrapIntПотрачено Scrap
materialsUsedJSON{ itemId: quantity }
gainedXPIntXP за крафт (сейчас 0)
craftedAtDateTimeКогда
Структура CraftBudgetLog
ПолеТипОписание
idStringCUID
userIdStringКто скрафтил
skinIdStringID скина
periodIdStringID бюджетного периода
tierItemTierРедкость скина
costRubFloatСтоимость в рублях
activePeriodsIntСтаж в пуле на момент крафта
hadBoostBooleanБыл ли буст
seniorityMultiplierFloatМножитель стажа (1.0-4.3)
totalBoostMultiplierFloatИтоговый буст (3.0-12.9)
blockedSkinIdsString[]Скины ≥50% на момент крафта
craftedAtDateTimeКогда

6. API Endpoints

МетодЭндпоинтОписаниеDocs
GET/api/craft/check/:itemIdПроверка возможности крафта
POST/api/craft/:itemIdВыполнить крафт
GET/api/craft/availableСписок всех крафтовых предметов

Response Examples

GET /api/craft/check/:itemId
{
"success": true,
"data": {
"canCraft": false,
"reason": "Insufficient materials: AK-47 Fragment (need 5, have 3)",
"requirements": {
"scrapCost": 500,
"materials": [
{
"itemId": "fragment-ak47",
"itemName": "AK-47 Fragment",
"itemImageUrl": "https://...",
"itemType": "FRAGMENT",
"itemTier": "TIER_3",
"needed": 5,
"available": 3
}
]
},
"userResources": {
"scrap": 1500,
"materials": {
"fragment-ak47": 3,
"blueprint-ak47": 1
}
}
}
}
POST /api/craft/:itemId
{
"success": true,
"data": {
"craftedItem": {
"id": "inv-123",
"itemId": "skin-ak47-redline",
"quantity": 1,
"sourceType": "CRAFTING",
"item": {
"id": "skin-ak47-redline",
"name": "AK-47 | Redline",
"tier": "TIER_3",
"itemType": "SKIN"
}
},
"newScrapBalance": 1000,
"xpGained": 0
}
}
GET /api/craft/available
{
"success": true,
"data": [
{
"item": {
"id": "skin-ak47-redline",
"name": "AK-47 | Redline",
"tier": "TIER_3",
"craftScrapCost": 500
},
"canCraft": true,
"dropSources": ["Weapon Case", "AK-47 Collection"]
},
{
"item": {
"id": "skin-awp-dragon-lore",
"name": "AWP | Dragon Lore",
"tier": "TIER_5",
"craftScrapCost": 5000
},
"canCraft": false,
"missingRequirements": {
"scrap": 3500,
"materials": [
{ "itemId": "blueprint-awp-dl", "itemName": "AWP DL Blueprint", "missing": 1 }
]
},
"dropSources": ["Legendary Case"]
}
]
}

  • Cases — источник материалов (FRAGMENT, BLUEPRINT, RESOURCE)
  • Inventory — хранение материалов и скрафченных скинов
  • Luck Pool — буст вероятности материалов для активных игроков
  • Budget — контроль расходов на крафт
  • Seasons — крафт требует активного сезона
  • Live Feed — событие ITEM_CRAFTED в публичной ленте