>
Guía 2026-01-22

Cómo crear sistemas de crafting y fabricación para FiveM

TDYSKY

TDYSKY

Fundador y desarrollador principal de Agency Scripts

Por qué los sistemas de crafting impulsan el enganche del jugador

Un sistema de crafting da a los jugadores motivos para recolectar recursos, explorar el mapa, interactuar con otros para comerciar e invertir tiempo en construir algo tangible. Sin crafting, los ítems salen de tiendas con stock infinito o de spawns de admin, lo que aplana la economía y quita agencia al jugador. Un sistema bien diseñado crea cadenas de suministro donde un jugador mina materias primas, otro las refina y un tercero ensambla el producto final. Esta interdependencia genera interacciones de rol orgánicas y actividad económica. El crafting de armas aporta riesgo y recompensa al submundo criminal, mientras que rutas legales como cocina, sastrería o reparación de electrónica ofrecen fuentes de ingresos legítimos. En esta guía construiremos un framework completo con recetas, mesas de trabajo, progresión de habilidad y descubrimiento de planos.

Diseñar el sistema de recetas

Las recetas son la estructura de datos central de cualquier sistema de crafting. Cada receta define qué ítems se requieren como entrada, qué se produce como salida, cuánto dura el proceso, qué nivel de habilidad hace falta y, opcionalmente, qué tipo de mesa de trabajo se necesita. Guarda las recetas en un archivo de configuración compartido al que accedan cliente y servidor, porque el cliente necesita los datos para pintar la UI y el servidor para validar los intentos. Usa una tabla plana indexada por un ID único para búsquedas rápidas. Incluye un campo category para organizar recetas en la UI y un successChance modificable por la habilidad del jugador, añadiendo un componente de riesgo al crafting de alto nivel.

-- shared/config.lua (accesible por cliente y servidor)
Config = {}

Config.Recipes = {
    -- Armas
    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 segundos
        skillReq    = 50,     -- nivel de habilidad de crafting
        workbench   = 'weapons_bench',
        successBase = 0.75,   -- 75% base de éxito
        xpReward    = 25,
    },
    -- Electrónica
    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,
    },
    -- Cocina
    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 crafting en servidor

Toda la validación debe ocurrir en el servidor para evitar abusos. El servidor comprueba que el jugador tenga los ingredientes en el inventario, cumpla el requisito de habilidad, esté cerca de la mesa de trabajo correcta y no esté ya en un proceso de crafting. Tras validar, retira los ingredientes, lanza el temporizador de duración, calcula el éxito en función del valor base modificado por la habilidad y concede el ítem fabricado o devuelve parte del material si falla. No confíes nunca en que el cliente reporte resultados. El cliente solo debe enviar una petición con el ID de receta y el servidor verifica de forma independiente cada condición. Así se evita la duplicación de ítems cuando un cliente modificado afirma haber crafteado sin consumir materiales.

-- server.lua
local craftingPlayers = {} -- controla quién está crafteando ahora
local playerSkills = {}    -- caché de XP de crafting por jugador

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

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

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

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

    -- Espera la duración del crafting
    SetTimeout(recipe.duration, function()
        craftingPlayers[src] = nil

        -- Calcula probabilidad de éxito (bonus de skill limitado a +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
            -- Devuelve el 50% del material al fallar
            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)

Progresión de habilidad y experiencia

Un sistema de progresión aporta metas a largo plazo y sensación de avance para los jugadores que invierten en crafting. Cada éxito otorga puntos de experiencia que se acumulan hacia niveles de habilidad. Los niveles altos desbloquean recetas avanzadas y mejoran la tasa de éxito en los crafteos difíciles. Guarda los datos de habilidad por personaje en la base de datos para que persistan y queden ligados al personaje, no a la cuenta. Define hitos claros: nivel 0-10 permite cocina básica y reparaciones, 11-30 abre electrónica y herramientas básicas, 31-60 habilita componentes de arma y fabricación avanzada, y 61-100 desbloquea recetas raras y legendarias. Muestra el nivel actual y la barra de progreso en la UI para que los jugadores vean siempre cuánto les queda para el próximo desbloqueo.

-- Sistema de habilidad (server.lua continuación)
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)
        -- Comprueba recetas recién desbloqueadas
        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)
    -- Cada nivel requiere progresivamente más XP
    -- Nivel 1 = 50xp, Nivel 2 = 150xp, Nivel 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

Interacción con la mesa de trabajo y UI del cliente

Las mesas de trabajo son ubicaciones físicas en el mundo donde los jugadores van a fabricar. Cada tipo de mesa soporta una categoría concreta de recetas: mesa de armas para armas de fuego, cocina para comida, estación de electrónica para gadgets. En el cliente, spawnea un marker o usa un sistema de target como ox_target para crear un punto de interacción en cada ubicación. Cuando el jugador interactúe, comprueba proximidad, filtra las recetas disponibles según el tipo de mesa y el nivel de habilidad y abre la UI de crafting. La UI debería mostrar nombre, ingredientes con cantidad actual en inventario, probabilidad de éxito, tiempo de fabricación y recompensa en XP. Resalta las recetas cuyos materiales estén completos y atenúa las que falten. Durante el crafteo, reproduce una animación, muestra barra de progreso y desactiva el movimiento para evitar exploits de jugadores que se alejen a medio proceso.

-- client.lua
local isCrafting = false

-- Configura puntos de interacción en las mesas
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
    -- Envía a NUI o usa una librería de menús
    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)

    -- Reproduce animación de crafting
    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)

    -- Barra de progreso vía ox_lib o NUI propia
    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)

Cadenas de producción y recolección de materiales

Las cadenas de producción aportan profundidad exigiendo a los jugadores procesar materias primas en varias etapas antes de crear el producto final. El mineral de hierro hay que fundirlo en lingotes de acero en un horno, que luego se prensan en placas en una metalistería y, por fin, se usan en la mesa de armas para crear armas de fuego. Cada paso exige una mesa distinta y potencialmente otra habilidad. Este diseño en cadena anima a la especialización: algunos jugadores se centran en minería y fundición mientras otros se especializan en ensamblaje. Los puntos de recolección deberían distribuirse por el mapa usando zonas donde los jugadores realicen acciones como minar nodos de roca, cosechar plantas o recuperar componentes electrónicos de desguaces. Usa un temporizador de respawn en los nodos para que los recursos sean limitados y los jugadores compitan por el acceso, alimentando conflictos de rol territoriales.

Descubrimiento de planos y recetas raras

No todas las recetas deberían estar disponibles desde el principio. El descubrimiento de planos aporta un incentivo de exploración y progresión escondiendo recetas avanzadas tras drops de loot, completado de misiones, compras a NPC o eventos aleatorios del mundo. Cuando un jugador descubre un plano, se añade a su lista personal de recetas guardada en la base de datos. Los planos pueden ser ítems intercambiables, creando un mercado secundario donde las recetas raras se vuelven bienes valiosos. Por ejemplo, un plano de arma militar podría soltarse solo como recompensa de ciertos atracos, mientras que una receta de cocina legendaria se compra a un NPC oculto que solo aparece en momentos concretos. La UI de crafting contrasta los planos desbloqueados del jugador con la lista completa y muestra solo las recetas a las que tiene acceso, creando una sensación de progresión en la que cada nuevo plano es un desbloqueo significativo que amplía sus capacidades.

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

    -- Valida que el jugador tenga el plano
    local count = exports.ox_inventory:GetItemCount(src, blueprintItem)
    if count < 1 then return end

    -- Mapea planos a IDs de receta
    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

    -- Comprueba si ya está desbloqueado
    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 el plano y desbloquea la receta
    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)

Compartir este artículo

¿Listo para mejorar tu servidor?

Echa un vistazo a nuestros scripts premium de FiveM en la tienda de Agency Scripts o únete a nuestra comunidad de Discord para soporte y novedades.