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}
)
endInteraçã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)
