Назад к блогу
Guide8 мин чтения

FiveM Система крафта: рецепты, станции и прогресс

Создайте глубокую систему крафта для FiveM. Логика рецептов, верстаки, прогресс навыков и лучшие скрипты крафта для QBCore, ESX и ox_inventory.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Почему системы крафта повышают вовлеченность игроков

Система крафта даёт игрокам повод собирать ресурсы, исследовать карту, взаимодействовать с другими игроками для торговли и вкладывать время в создание чего-то осязаемого. Без крафта предметы либо поступают из магазинов с бесконечным запасом, либо появляются админскими командами, что упрощает экономику и лишает игроков влияния. Хорошо продуманная система крафта создаёт цепочки поставок, где один игрок добывает сырьё, другой его перерабатывает, а третий собирает конечный продукт. Эта взаимозависимость порождает органичные ролевые взаимодействия и экономическую активность. Крафт оружия добавляет элемент риска и награды в криминальный мир, а легальные пути крафта, такие как кулинария, шитьё или ремонт электроники, обеспечивают законный доход. В этом руководстве мы создадим полноценный фреймворк крафта с рецептами, верстаками, прогрессом навыков и открытием чертежей.

Проектирование системы рецептов

Рецепты , это основная структура данных любой системы крафта. Каждый рецепт определяет, какие предметы требуются на входе, какой предмет получается на выходе, сколько времени занимает процесс крафта, какой уровень навыка необходим и, опционально, какой тип верстака требуется. Храните рецепты в общем конфигурационном файле, доступном как клиенту, так и серверу, поскольку клиенту нужны данные рецептов для отображения UI крафта, а серверу , для проверки попыток крафта. Используйте плоскую таблицу с индексом по уникальному ID рецепта для быстрого поиска. Включите category поле для организации рецептов в UI и successChance поле, которое может изменяться в зависимости от уровня навыка игрока, добавляя элемент риска в высокоуровневое крафтинг.

-- shared/config.lua (accessible by both client and server)
Config = {}

Config.Recipes = {
    -- Weapons
    pistol_craft = {
        label      = 'Craft Pistol',
        category   = 'weapons',
        result     = {item = 'weapon_pistol', count = 1},
        ingredients = {
            {item = 'steel_plate',    count = 3},
            {item = 'weapon_spring',  count = 2},
            {item = 'rubber_grip',    count = 1},
            {item = 'gun_oil',        count = 1},
        },
        duration    = 15000,  -- 15 seconds
        skillReq    = 50,     -- crafting skill level
        workbench   = 'weapons_bench',
        successBase = 0.75,   -- 75% base success
        xpReward    = 25,
    },
    -- Electronics
    radio_craft = {
        label      = 'Assemble Radio',
        category   = 'electronics',
        result     = {item = 'radio', count = 1},
        ingredients = {
            {item = 'electronic_parts', count = 2},
            {item = 'copper_wire',      count = 3},
            {item = 'plastic_casing',   count = 1},
        },
        duration    = 8000,
        skillReq    = 10,
        workbench   = 'electronics_bench',
        successBase = 0.90,
        xpReward    = 10,
    },
    -- Cooking
    sandwich_craft = {
        label      = 'Make Sandwich',
        category   = 'cooking',
        result     = {item = 'sandwich', count = 2},
        ingredients = {
            {item = 'bread',   count = 2},
            {item = 'cheese',  count = 1},
            {item = 'lettuce', count = 1},
        },
        duration    = 3000,
        skillReq    = 0,
        workbench   = 'kitchen',
        successBase = 1.0,
        xpReward    = 3,
    },
}

Config.Workbenches = {
    weapons_bench = {
        label  = 'Weapons Workbench',
        model  = 'prop_tool_bench02',
        coords = {
            {x = 1083.5, y = -1975.3, z = 31.5, h = 140.0},
        },
    },
    electronics_bench = {
        label  = 'Electronics Station',
        model  = 'prop_cs_server_drive',
        coords = {
            {x = 732.1, y = -1073.8, z = 22.2, h = 90.0},
        },
    },
    kitchen = {
        label  = 'Kitchen',
        model  = 'prop_cooker_03',
        coords = {
            {x = -1192.3, y = -896.1, z = 13.9, h = 35.0},
        },
    },
}

Логика крафта на стороне сервера

Вся проверка крафта должна происходить на сервере для предотвращения эксплойтов. Сервер проверяет наличие необходимых ингредиентов в инвентаре, соответствие требованиям навыка, нахождение рядом с нужным типом верстака и отсутствие текущего процесса крафта. После проверки ингредиенты удаляются, запускается таймер, рассчитывается успех на основе базового шанса с поправкой на уровень навыка, и либо выдается созданный предмет, либо при неудаче возвращаются частичные материалы. Никогда не доверяйте клиенту сообщать результаты крафта. Клиент должен только отправлять запрос на создание рецепта по ID, а сервер самостоятельно проверяет все условия. Это предотвращает эксплойты с дублированием предметов, когда модифицированный клиент заявляет о создании предметов без расхода ингредиентов.

-- server.lua
local craftingPlayers = {} -- track who is currently crafting
local playerSkills = {}    -- cache of player crafting XP

RegisterNetEvent('crafting:attempt', function(recipeId)
    local src = source
    if craftingPlayers[src] then
        TriggerClientEvent('notifications:show', src, 'Error', 'Already crafting.', 'error')
        return
    end

    local recipe = Config.Recipes[recipeId]
    if not recipe then return end

    -- Check skill requirement
    local skill = GetPlayerCraftingSkill(src)
    if skill < recipe.skillReq then
        TriggerClientEvent('notifications:show', src,
            'Error', 'Requires crafting level ' .. recipe.skillReq .. '.', 'error')
        return
    end

    -- Check ingredients
    for _, ing in ipairs(recipe.ingredients) do
        local count = exports.ox_inventory:GetItemCount(src, ing.item)
        if count < ing.count then
            TriggerClientEvent('notifications:show', src,
                'Error', 'Missing materials.', 'error')
            return
        end
    end

    -- Remove ingredients
    for _, ing in ipairs(recipe.ingredients) do
        exports.ox_inventory:RemoveItem(src, ing.item, ing.count)
    end

    craftingPlayers[src] = true
    TriggerClientEvent('crafting:startProgress', src, recipe.duration, recipe.label)

    -- Wait for crafting duration
    SetTimeout(recipe.duration, function()
        craftingPlayers[src] = nil

        -- Calculate success chance (skill bonus caps at +20%)
        local skillBonus = math.min((skill - recipe.skillReq) * 0.005, 0.20)
        local finalChance = math.min(recipe.successBase + skillBonus, 1.0)

        if math.random() <= finalChance then
            exports.ox_inventory:AddItem(src, recipe.result.item, recipe.result.count)
            AddCraftingXP(src, recipe.xpReward)
            TriggerClientEvent('notifications:show', src,
                'Success', 'Crafted: ' .. recipe.label, 'success')
        else
            -- Return 50% of materials on failure
            for _, ing in ipairs(recipe.ingredients) do
                local refund = math.floor(ing.count * 0.5)
                if refund > 0 then
                    exports.ox_inventory:AddItem(src, ing.item, refund)
                end
            end
            TriggerClientEvent('notifications:show', src,
                'Failed', 'Crafting failed. Some materials recovered.', 'error')
        end
    end)
end)

Прогресс навыков и опыт

Система прогрессии навыков добавляет долгосрочные цели и ощущение развития для игроков, вкладывающихся в крафт. Каждый успешный крафт дает очки опыта, которые накапливаются для повышения уровня навыка. Более высокие уровни открывают продвинутые рецепты и увеличивают шанс успеха сложных крафтов. Данные навыков хранятся по персонажу в базе, чтобы сохраняться и быть привязанными к персонажу, а не к аккаунту игрока. Определите четкие этапы: уровни 0-10 позволяют базовую готовку и ремонт, 11-30 открывают электронику и базовые инструменты, 31-60 дают компоненты оружия и продвинутую сборку, 61-100 открывают редкие и легендарные рецепты. Показывайте текущий уровень навыка и индикатор прогресса в UI крафта, чтобы игроки всегда видели, насколько близки к следующему открытию.

-- Skill system (server.lua continued)
local skillCache = {}

function GetPlayerCraftingSkill(src)
    if skillCache[src] then return skillCache[src].level end
    return 0
end

function AddCraftingXP(src, amount)
    if not skillCache[src] then
        skillCache[src] = {xp = 0, level = 0}
    end

    skillCache[src].xp = skillCache[src].xp + amount
    local newLevel = CalculateLevel(skillCache[src].xp)

    if newLevel > skillCache[src].level then
        skillCache[src].level = newLevel
        TriggerClientEvent('notifications:show', src,
            'Level Up!', 'Crafting level: ' .. newLevel, 'success', 6000)
        -- Check for newly unlocked recipes
        local unlocked = GetNewlyUnlockedRecipes(newLevel, skillCache[src].level)
        for _, recipe in ipairs(unlocked) do
            TriggerClientEvent('notifications:show', src,
                'Recipe Unlocked', recipe.label .. ' is now available.', 'info', 5000)
        end
    end

    skillCache[src].level = newLevel
    SaveCraftingSkill(src)
end

function CalculateLevel(xp)
    -- Each level requires progressively more XP
    -- Level 1 = 50xp, Level 2 = 150xp, Level 10 = 2750xp, etc.
    local level = 0
    local required = 50
    local remaining = xp
    while remaining >= required and level < 100 do
        remaining = remaining - required
        level = level + 1
        required = math.floor(required * 1.15)
    end
    return level
end

function SaveCraftingSkill(src)
    local charId = exports['multichar']:GetCharacterId(src)
    if not charId or not skillCache[src] then return end
    MySQL.update(
        'INSERT INTO character_skills (character_id, skill, xp, level) VALUES (?, ?, ?, ?) ON DUPLICATE KEY UPDATE xp = ?, level = ?',
        {charId, 'crafting', skillCache[src].xp, skillCache[src].level, skillCache[src].xp, skillCache[src].level}
    )
end

Взаимодействие с верстаком и клиентский UI

Верстаки , это физические локации в игровом мире, куда игроки идут для создания предметов. Каждый тип верстака поддерживает определённую категорию рецептов: оружейный верстак для огнестрельного оружия, кухня для еды, электронная станция для гаджетов. На клиентской стороне создайте маркер или используйте систему таргетинга, например ox_target, чтобы создать точку взаимодействия на каждой локации верстака. При взаимодействии игрока проверьте его близость, отфильтруйте доступные рецепты по типу верстака и уровню навыка игрока и откройте UI крафта. UI должен отображать название каждого рецепта, необходимые ингредиенты с текущими запасами, вероятность успеха, время создания и награду опыта навыка. Выделяйте рецепты, для которых у игрока есть все материалы, и затемняйте те, где материалы отсутствуют. Во время крафта воспроизводите соответствующую анимацию, показывайте progress bar и блокируйте движение игрока, чтобы предотвратить эксплойты, когда игрок уходит во время создания.

-- client.lua
local isCrafting = false

-- Setup workbench interaction points
CreateThread(function()
    for benchType, bench in pairs(Config.Workbenches) do
        for _, loc in ipairs(bench.coords) do
            exports.ox_target:addSphereZone({
                coords = vector3(loc.x, loc.y, loc.z),
                radius = 1.5,
                options = {
                    {
                        label = 'Use ' .. bench.label,
                        icon  = 'fas fa-tools',
                        onSelect = function()
                            OpenCraftingMenu(benchType)
                        end,
                        canInteract = function()
                            return not isCrafting
                        end
                    }
                }
            })
        end
    end
end)

function OpenCraftingMenu(benchType)
    local recipes = {}
    for id, recipe in pairs(Config.Recipes) do
        if recipe.workbench == benchType then
            recipes[#recipes + 1] = {
                id          = id,
                label       = recipe.label,
                category    = recipe.category,
                ingredients = recipe.ingredients,
                duration    = recipe.duration,
                skillReq    = recipe.skillReq,
                successBase = recipe.successBase,
                xpReward    = recipe.xpReward,
                result      = recipe.result,
            }
        end
    end
    -- Send to NUI or use a menu library
    SendNUIMessage({action = 'openCrafting', recipes = recipes, benchType = benchType})
    SetNuiFocus(true, true)
end

RegisterNetEvent('crafting:startProgress', function(duration, label)
    isCrafting = true
    local ped = PlayerPedId()
    FreezeEntityPosition(ped, true)

    -- Play crafting animation
    RequestAnimDict('mini@repair')
    while not HasAnimDictLoaded('mini@repair') do Wait(10) end
    TaskPlayAnim(ped, 'mini@repair', 'fixing_a_player', 8.0, -8.0, -1, 1, 0, false, false, false)

    -- Progress bar via ox_lib or custom NUI
    exports.ox_lib:progressBar({
        duration = duration,
        label    = label,
        useWhileDead  = false,
        canCancel     = false,
        disable = {move = true, car = true, combat = true},
    })

    ClearPedTasks(ped)
    FreezeEntityPosition(ped, false)
    isCrafting = false
end)

Производственные цепочки и сбор материалов

Производственные цепочки создают глубину, требуя от игроков обработки сырья на нескольких этапах перед созданием конечного продукта. Железная руда должна быть переплавлена в стальные слитки в печи, которые затем прессуются в стальные пластины на металлообрабатывающем заводе, и наконец используются на оружейном верстаке для создания огнестрельного оружия. Каждый этап требует разного места работы и, возможно, разных навыков. Такая цепочка поощряет специализацию, когда одни игроки сосредотачиваются на добыче и переплавке, а другие , на сборке. Точки сбора материалов должны быть разбросаны по карте с использованием зон, где игроки могут выполнять действия по сбору, такие как добыча каменных узлов, сбор растений или поиск электронных компонентов на свалках. Используйте таймер возрождения на узлах сбора, чтобы ресурсы были ограничены и игроки конкурировали за доступ, стимулируя территориальные ролевые конфликты.

Обнаружение чертежей и редкие рецепты

Не все рецепты должны быть доступны с самого начала. Открытие чертежей добавляет стимул к исследованию и прогрессу, скрывая продвинутые рецепты за добычей, выполнением заданий, покупками у NPC или случайными событиями мира. Когда игрок находит чертёж, он добавляется в его личный список рецептов в базе данных. Чертежи могут быть предметами для торговли, создавая вторичный рынок, где редкие рецепты становятся ценными товарами. Например, чертёж военного оружия может выпадать только из определённых наград за ограбления, а легендарный рецепт кулинарии можно купить у скрытого NPC, появляющегося в определённое время. Интерфейс крафта проверяет разблокированные игроком чертежи и показывает только доступные рецепты, создавая ощущение прогресса, где каждый новый чертёж , значимое открытие, расширяющее возможности крафта.

-- Blueprint system (server.lua)
RegisterNetEvent('crafting:useBlueprint', function(blueprintItem)
    local src = source
    local charId = exports['multichar']:GetCharacterId(src)
    if not charId then return end

    -- Validate the player has the blueprint item
    local count = exports.ox_inventory:GetItemCount(src, blueprintItem)
    if count < 1 then return end

    -- Map blueprint items to recipe IDs
    local blueprintMap = {
        blueprint_pistol     = 'pistol_craft',
        blueprint_radio      = 'radio_craft',
        blueprint_armor      = 'armor_craft',
        blueprint_lockpick   = 'adv_lockpick_craft',
    }

    local recipeId = blueprintMap[blueprintItem]
    if not recipeId then return end

    -- Check if already unlocked
    local exists = MySQL.scalar.await(
        'SELECT 1 FROM character_blueprints WHERE character_id = ? AND recipe_id = ?',
        {charId, recipeId}
    )
    if exists then
        TriggerClientEvent('notifications:show', src,
            'Info', 'You already know this recipe.', 'info')
        return
    end

    -- Consume blueprint and unlock recipe
    exports.ox_inventory:RemoveItem(src, blueprintItem, 1)
    MySQL.insert(
        'INSERT INTO character_blueprints (character_id, recipe_id) VALUES (?, ?)',
        {charId, recipeId}
    )

    local recipe = Config.Recipes[recipeId]
    TriggerClientEvent('notifications:show', src,
        'Blueprint Learned', 'You can now craft: ' .. (recipe and recipe.label or recipeId), 'success', 6000)
end)

Готовы начать?

Возьмите скрипты в нашем магазине или заходите в Discord за поддержкой, обновлениями и анонсами.