>>
Guide 2026-01-22

Système de craft pour FiveM : guide développeur

TDYSKY

TDYSKY

Fondateur et développeur principal chez Agency Scripts

Pourquoi les systèmes d'artisanat stimulent l'engagement des joueurs

Un système d'artisanat donne aux joueurs une raison de rassembler des ressources, d'explorer la carte, d'interagir avec d'autres joueurs pour faire du commerce et d'investir du temps dans la construction de quelque chose de tangible. Sans fabrication, les objets proviennent soit de magasins avec un stock infini, soit d'apparitions d'administrateurs, ce qui aplatit l'économie et supprime l'agence des joueurs. Un système de fabrication bien conçu crée des chaînes d'approvisionnement dans lesquelles un joueur extrait les matières premières, un autre les raffine et un troisième assemble le produit final. Cette interdépendance génère des interactions de jeu de rôle organiques et une activité économique. La fabrication d'armes ajoute une couche de risque et de récompense au monde criminel, tandis que les filières de fabrication légales comme la cuisine, la couture ou la réparation électronique fournissent des sources de revenus légitimes. Dans ce guide, nous allons construire un cadre d'artisanat complet avec des recettes, des établis, une progression des compétences et une découverte de plans.

Conception du système de recettes

Les recettes constituent la structure de données de base de tout système d’artisanat. Chaque recette définit quels éléments sont requis comme intrants, quel élément est produit comme sortie, combien de temps prend le processus de fabrication, quel niveau de compétence est nécessaire et éventuellement quel type d'établi est requis. Stockez les recettes dans un fichier de configuration partagé auquel le client et le serveur peuvent accéder, car le client a besoin des données de recette pour afficher l'interface utilisateur de création tandis que le serveur en a besoin pour valider les tentatives de création. Utilisez un tableau plat indexé par un ID de recette unique pour des recherches rapides. Inclure un category champ pour organiser les recettes dans l'interface utilisateur et un successChance champ qui peut être modifié en fonction du niveau de compétence du joueur, ajoutant un élément de risque à l'artisanat de haut niveau.

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

Logique de création côté serveur

Toute validation de création doit avoir lieu sur le serveur pour empêcher toute exploitation. Le serveur vérifie que le joueur dispose des ingrédients requis dans son inventaire, qu'il répond aux compétences requises, qu'il est proche du bon type d'établi et qu'il n'est pas déjà dans un processus de fabrication. Après validation, il supprime les ingrédients, exécute le chronomètre de durée, calcule le succès en fonction de la chance de base modifiée par le niveau de compétence et accorde l'objet fabriqué ou renvoie des matériaux partiels en cas d'échec. Ne faites jamais confiance au client pour rapporter les résultats de la fabrication. Le client doit uniquement envoyer une demande pour créer un ID de recette spécifique, et le serveur vérifie indépendamment chaque condition. Cela empêche les exploits de duplication d'objets lorsqu'un client modifié prétend avoir fabriqué des objets sans consommer d'ingrédients.

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

Progression des compétences et expérience

Un système de progression des compétences ajoute des objectifs à long terme et un sentiment d'avancement pour les joueurs qui investissent dans l'artisanat. Chaque artisanat réussi attribue des points d'expérience qui s'accumulent pour atteindre les niveaux de compétence. Des niveaux de compétence plus élevés débloquent des recettes avancées et augmentent le taux de réussite des métiers difficiles. Stockez les données de compétences par personnage dans la base de données afin qu'elles persistent et soient liées au personnage plutôt qu'au compte du joueur. Définissez des jalons clairs : le niveau 0 à 10 permet la cuisine et les réparations de base, le niveau 11 à 30 ouvre l'électronique et les outils de base, le niveau 31 à 60 permet les composants d'armes et la fabrication avancée, et le niveau 61 à 100 débloque des recettes rares et légendaires. Affichez le niveau de compétence actuel et la barre de progression dans l'interface utilisateur de fabrication afin que les joueurs voient toujours à quel point ils sont proches du prochain déverrouillage.

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

Interaction avec Workbench et interface utilisateur client

Les établis sont des emplacements physiques dans le monde du jeu où les joueurs se rendent pour fabriquer des objets. Chaque type d'établi prend en charge une catégorie spécifique de recettes : un banc d'armes pour les armes à feu, une cuisine pour la nourriture, une station électronique pour les gadgets. Côté client, générez un marqueur ou utilisez un système cible tel que ox_target pour créer un point d'interaction à chaque emplacement de l'atelier. Lorsque le joueur interagit, vérifiez sa proximité, filtrez les recettes disponibles en fonction du type d'établi et du niveau de compétence du joueur, puis ouvrez l'interface utilisateur de fabrication. L'interface utilisateur doit afficher le nom de chaque recette, les ingrédients requis avec le nombre actuel d'inventaires, la probabilité de réussite, le temps de fabrication et la récompense d'XP de compétence. Mettez en surbrillance les recettes pour lesquelles le joueur dispose de tous les matériaux requis et estompez celles pour lesquelles il manque du matériel. Pendant la fabrication, lancez une animation appropriée, affichez une barre de progression et désactivez les mouvements des joueurs pour empêcher les exploits où les joueurs s'éloignent en cours de création.

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

Chaînes de production et collecte de matériaux

Les chaînes de production créent de la profondeur en obligeant les joueurs à traiter les matières premières en plusieurs étapes avant de fabriquer le produit final. Le minerai de fer doit être fondu en lingots d'acier dans un four, qui sont ensuite pressés en plaques d'acier dans une usine métallurgique, qui sont finalement utilisées dans un banc d'armes pour fabriquer des armes à feu. Chaque étape nécessite un emplacement d'établi différent et potentiellement une compétence différente. Cette conception de chaîne encourage la spécialisation, où certains acteurs se concentrent sur l’exploitation minière et la fusion tandis que d’autres se spécialisent dans l’assemblage. Les points de collecte de matériaux doivent être répartis sur la carte en utilisant des zones où les joueurs peuvent effectuer des actions de collecte comme l'extraction de nœuds de roche, la récolte d'usines ou la récupération de composants électroniques dans les dépotoirs. Utilisez un minuteur de réapparition sur les nœuds de collecte afin que les ressources soient limitées et que les joueurs se disputent l'accès, provoquant des conflits de jeu de rôle basés sur le territoire.

Découverte de plans et recettes rares

Toutes les recettes ne devraient pas être disponibles dès le départ. La découverte de plans ajoute une incitation à l'exploration et à la progression en cachant les recettes avancées derrière les butins, les achèvements de quêtes, les achats de PNJ ou les événements mondiaux aléatoires. Lorsqu'un joueur découvre un plan, celui-ci est ajouté à sa liste de recettes personnelle stockée dans la base de données. Les plans peuvent être des objets échangeables, créant un marché secondaire où les recettes rares deviennent des biens précieux. Par exemple, un plan d'arme de qualité militaire pourrait n'être obtenu que grâce à certaines récompenses de braquage, tandis qu'une recette de cuisine légendaire pourrait être achetée auprès d'un PNJ caché qui n'apparaîtrait qu'à des moments précis. L'interface utilisateur d'artisanat vérifie les plans déverrouillés du joueur par rapport à la liste complète des recettes et affiche uniquement les recettes auxquelles il a accès, créant un sentiment de progression où chaque nouveau plan ressemble à un déverrouillage significatif qui étend ses capacités d'artisanat.

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

Partager cet article

Prêt à améliorer votre serveur ?

Découvrez nos scripts FiveM premium dans la boutique Agency Scripts ou rejoignez notre communauté Discord pour le support et les mises à jour.