>
Guia 2026-01-22

Como Criar Sistemas de Crafting e Fabrico para FiveM

TDYSKY

TDYSKY

Fundador & Lead Developer na Agency Scripts

Por que os sistemas de criação impulsionam o envolvimento do jogador

Um sistema de criação dá aos jogadores um motivo para reunir recursos, explorar o mapa, interagir com outros jogadores para negociar e investir tempo na construção de algo tangível. Sem a elaboração, os itens vêm de lojas com estoque infinito ou de spawns administrativos, o que achata a economia e remove a agência do jogador. Um sistema de elaboração bem projetado cria cadeias de suprimentos onde um jogador extrai matérias-primas, outro as refina e um terceiro monta o produto final. Essa interdependência gera interações orgânicas de roleplay e atividade econômica. A fabricação de armas adiciona uma camada de risco e recompensa ao submundo do crime, enquanto caminhos legais de fabricação, como culinária, alfaiataria ou conserto de eletrônicos, fornecem fluxos de renda legítimos. Neste guia, construiremos uma estrutura de elaboração completa com receitas, bancadas de trabalho, progressão de habilidades e descoberta de projetos.

Projetando o sistema de receitas

As receitas são a estrutura de dados central de qualquer sistema de elaboração. Cada receita define quais itens são necessários como entradas, quais itens são produzidos como saída, quanto tempo leva o processo de elaboração, qual nível de habilidade é necessário e, opcionalmente, qual tipo de bancada é necessária. Armazene receitas em um arquivo de configuração compartilhado que tanto o cliente quanto o servidor possam acessar, pois o cliente precisa dos dados da receita para exibir a UI de criação, enquanto o servidor precisa deles para validar as tentativas de criação. Use uma tabela plana indexada por um ID de receita exclusivo para pesquisas rápidas. Inclui um campo category para organizar receitas na UI e um campo successChance que pode ser modificado pelo nível de habilidade do jogador, adicionando um elemento de risco à elaboração de alto nível.

-- 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},
        },
    },
}

Lógica de criação do lado do servidor

Toda validação de elaboração deve acontecer no servidor para evitar exploração. O servidor verifica se o jogador possui os ingredientes necessários em seu inventário, atende aos requisitos de habilidade, está próximo do tipo de bancada correto e ainda não está em processo de elaboração. Após a validação, ele remove os ingredientes, executa o cronômetro de duração, calcula o sucesso com base na chance básica modificada pelo nível de habilidade e concede o item criado ou devolve materiais parciais em caso de falha. Nunca confie no cliente para relatar os resultados da elaboração. O cliente deve apenas enviar uma solicitação para criar um ID de receita específico, e o servidor verifica cada condição de forma independente. Isso evita explorações de duplicação de itens em que um cliente modificado afirma ter criado itens sem consumir ingredientes.

-- 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)

Progressão de habilidades e experiência

Um sistema de progressão de habilidades adiciona objetivos de longo prazo e uma sensação de avanço para jogadores que investem em artesanato. Cada artesanato bem-sucedido concede pontos de experiência que se acumulam em níveis de habilidade. Níveis de habilidade mais altos desbloqueiam receitas avançadas e aumentam a taxa de sucesso de trabalhos manuais difíceis. Armazene dados de habilidade por personagem no banco de dados para que persistam e estejam vinculados ao personagem e não à conta do jogador. Defina marcos claros: o nível 0-10 permite cozinhar e reparos básicos, o nível 11-30 abre eletrônicos e ferramentas básicas, o nível 31-60 permite componentes de armas e fabricação avançada, e o nível 61-100 desbloqueia receitas raras e lendárias. Exiba o nível de habilidade atual e a barra de progresso na interface de criação para que os jogadores sempre vejam o quão perto estão do próximo desbloqueio.

-- 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

Interação do ambiente de trabalho e UI do cliente

Bancadas de trabalho são locais físicos no mundo do jogo onde os jogadores vão para fabricar itens. Cada tipo de bancada suporta uma categoria específica de receitas: uma bancada para armas de fogo, uma cozinha para alimentos, uma estação eletrônica para gadgets. No lado do cliente, gere um marcador ou use um sistema de destino como ox_target para criar um ponto de interação em cada local do ambiente de trabalho. Quando o jogador interage, verifique sua proximidade, filtre as receitas disponíveis com base no tipo de bancada e no nível de habilidade do jogador e abra a IU de criação. A IU deve exibir o nome de cada receita, os ingredientes necessários com a contagem atual do inventário, a probabilidade de sucesso, o tempo de elaboração e a recompensa de XP de habilidade. Destaque as receitas onde o jogador possui todos os materiais necessários e escureça aquelas onde faltam materiais. Durante a elaboração, reproduza uma animação apropriada, mostre uma barra de progresso e desative o movimento do jogador para evitar explorações que façam com que os jogadores se afastem no meio da criação.

-- 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)

Cadeias Produtivas e Captação de Materiais

As cadeias de produção criam profundidade ao exigir que os jogadores processem matérias-primas em vários estágios antes de elaborar o produto final. O minério de ferro deve ser fundido em lingotes de aço em uma fornalha, que são então prensados ​​em placas de aço em uma metalúrgica, que são finalmente usadas em uma bancada de armas para fabricar armas de fogo. Cada etapa requer um local de bancada diferente e, potencialmente, uma habilidade diferente. Este desenho de cadeia incentiva a especialização, onde alguns jogadores se concentram na mineração e fundição, enquanto outros se especializam na montagem. Os pontos de coleta de materiais devem ser espalhados pelo mapa usando zonas onde os jogadores podem realizar ações de coleta, como minerar nós de rocha, colher plantas ou retirar componentes eletrônicos de ferros-velhos. Use um temporizador de respawn na coleta de nós para que os recursos sejam limitados e os jogadores compitam pelo acesso, gerando conflitos de roleplay baseados em território.

Descoberta de projetos e receitas raras

Nem todas as receitas devem estar disponíveis desde o início. A descoberta do Blueprint adiciona um incentivo à exploração e progressão, ocultando receitas avançadas atrás de saques, conclusões de missões, compras de NPCs ou eventos mundiais aleatórios. Quando um jogador descobre um projeto, ele é adicionado à sua lista de receitas pessoais armazenada no banco de dados. Os projetos podem ser itens negociáveis, criando um mercado secundário onde receitas raras se tornam mercadorias valiosas. Por exemplo, um projeto de arma de nível militar só pode ser obtido com certas recompensas de assalto, enquanto uma receita culinária lendária pode ser comprada de um NPC oculto que só aparece em momentos específicos. A IU de criação compara os projetos desbloqueados do jogador com a lista completa de receitas e mostra apenas as receitas às quais eles têm acesso, criando uma sensação de progressão onde cada novo projeto parece um desbloqueio significativo que expande suas capacidades de criação.

-- 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)

Partilhar este artigo

Pronto para melhorar o teu servidor?

Explora os nossos scripts FiveM premium na loja Agency Scripts ou junta-te à nossa comunidade no Discord para suporte e atualizações.