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)
