System craftingu FiveM: receptury, stanowiska i postęp
Stwórz rozbudowany system craftingu w FiveM. Logika receptur, stoły rzemieślnicze, rozwój umiejętności i najlepsze skrypty craftingu dla QBCore, ESX i ox_inventory.
Agency Scripts
Założyciel i główny deweloper w Agency Scripts
Dlaczego systemy craftingu zwiększają zaangażowanie graczy
System craftingu daje graczom powód do zbierania surowców, eksploracji mapy, handlu z innymi i inwestowania czasu w tworzenie czegoś namacalnego. Bez craftingu przedmioty pochodzą ze sklepów o nieskończonych zasobach lub od administratorów, co spłaszcza ekonomię i odbiera graczom wpływ na świat. Dobrze zaprojektowany system craftingu tworzy łańcuchy dostaw, w których jeden gracz wydobywa surowce, drugi je przetwarza, a trzeci składa gotowy produkt. Ta współzależność generuje naturalne interakcje roleplay i ożywia gospodarkę. Wytwarzanie broni dodaje element ryzyka i zysku w świecie przestępczym, podczas gdy legalne ścieżki rzemiosła, takie jak gotowanie, krawiectwo czy naprawa elektroniki, zapewniają legalne źródła dochodu. W tym poradniku zbudujemy kompletny system craftingu z recepturami, stołami warsztatowymi, rozwojem umiejętności i odkrywaniem schematów.
Projektowanie systemu receptur
Receptury są podstawową strukturą danych w każdym systemie craftingu. Każda receptura określa, jakie przedmioty są wymagane jako składniki, jaki przedmiot powstaje w wyniku, ile trwa proces craftingu, jaki poziom umiejętności jest potrzebny i opcjonalnie jaki typ stołu rzemieślniczego jest wymagany. Przechowuj receptury we wspólnym pliku konfiguracyjnym, do którego dostęp mają zarówno klient, jak i serwer, ponieważ klient potrzebuje danych receptur do wyświetlania UI craftingu, podczas gdy serwer potrzebuje ich do weryfikacji prób craftingu. Użyj płaskiej tabeli indeksowanej unikalnym identyfikatorem receptury dla szybkiego wyszukiwania. Dołącz category pole do organizowania receptur w UI oraz successChance pole, które może być modyfikowane przez poziom umiejętności gracza, dodając element ryzyka do craftingu wyższego poziomu.
-- 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},
},
},
}
Logika craftingu po stronie serwera
Cała walidacja craftingu musi odbywać się na serwerze, aby zapobiec exploitom. Serwer sprawdza, czy gracz ma wymagane składniki w ekwipunku, spełnia wymagania dotyczące umiejętności, znajduje się w pobliżu odpowiedniego typu stołu rzemieślniczego i nie bierze już udziału w procesie craftingu. Po weryfikacji usuwa składniki, uruchamia timer trwania, oblicza sukces na podstawie bazowej szansy zmodyfikowanej przez poziom umiejętności i w zależności od wyniku przyznaje wytworzony przedmiot lub zwraca część materiałów w przypadku niepowodzenia. Nigdy nie ufaj klientowi w kwestii zgłaszania wyników craftingu. Klient powinien jedynie wysłać żądanie wytworzenia przedmiotu o określonym ID receptury, a serwer niezależnie weryfikuje każdy warunek. Zapobiega to exploitom duplikowania przedmiotów, w których zmodyfikowany klient twierdzi, że wytworzył przedmioty bez zużywania składników.
-- 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)
Rozwój umiejętności i doświadczenie
System rozwoju umiejętności dodaje długoterminowe cele i poczucie postępu graczom inwestującym w crafting. Każde udane wytworzenie przedmiotu przyznaje punkty doświadczenia, które kumulują się na poziomy umiejętności. Wyższe poziomy odblokowują zaawansowane receptury i zwiększają szansę na powodzenie trudnych wytworzeń. Zapisuj dane umiejętności w bazie danych dla każdej postaci, aby były trwałe i powiązane z postacią, a nie z kontem gracza. Zdefiniuj jasne kamienie milowe: poziomy 0-10 pozwalają na podstawowe gotowanie i naprawy, poziomy 11-30 otwierają elektronikę i podstawowe narzędzia, poziomy 31-60 umożliwiają produkcję komponentów broni i zaawansowane wytwarzanie, a poziomy 61-100 odblokowują rzadkie i legendarne receptury. Wyświetlaj aktualny poziom umiejętności i pasek postępu w UI craftingu, aby gracze zawsze widzieli, jak blisko są kolejnego odblokowania.
-- 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
Interakcja ze stołem rzemieślniczym i UI klienta
Stoły rzemieślnicze to fizyczne miejsca w świecie gry, do których gracze udają się, aby wytwarzać przedmioty. Każdy typ stołu obsługuje określoną kategorię receptur: stół rusznikarski dla broni palnej, kuchnia dla jedzenia, stacja elektroniczna dla gadżetów. Po stronie klienta stwórz marker lub użyj systemu targetowania, takiego jak ox_target, aby utworzyć punkt interakcji w każdej lokalizacji stołu. Gdy gracz wejdzie w interakcję, sprawdź odległość, przefiltruj dostępne receptury na podstawie typu stołu oraz poziomu umiejętności gracza i otwórz UI craftingu. Interfejs powinien wyświetlać nazwę każdej receptury, wymagane składniki wraz z aktualnym stanem w ekwipunku, szansę powodzenia, czas wytwarzania oraz nagrodę XP do umiejętności. Wyróżnij receptury, do których gracz posiada wszystkie wymagane materiały, i przyciemnij te, w których brakuje składników. Podczas craftingu odtwórz odpowiednią animację, wyświetl pasek postępu i zablokuj poruszanie się gracza, aby zapobiec exploitom polegającym na odchodzeniu w trakcie tworzenia.
-- 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)
Łańcuchy produkcyjne i zbieranie materiałów
Łańcuchy produkcyjne nadają rozgrywce głębi, wymagając od graczy przetwarzania surowców na wielu etapach przed wytworzeniem gotowego produktu. Ruda żelaza musi zostać przetopiona w piecu na sztabki stali, które następnie są prasowane na stalowe płyty w zakładzie obróbki metalu, a na końcu wykorzystywane przy stole rusznikarskim do wytwarzania broni palnej. Każdy krok wymaga innego stanowiska rzemieślniczego i potencjalnie innej umiejętności. Taki układ łańcucha zachęca do specjalizacji, w której część graczy skupia się na wydobyciu i wytopie, podczas gdy inni specjalizują się w montażu. Punkty zbierania materiałów powinny być rozmieszczone na całej mapie w postaci stref, w których gracze mogą wykonywać czynności zbierania, takie jak wydobywanie złóż rudy, zbieranie roślin czy przeszukiwanie złomowisk w poszukiwaniu komponentów elektronicznych. Zastosuj czas odnawiania (respawn timer) w punktach zbierania, aby zasoby były ograniczone, a gracze rywalizowali o dostęp, co napędza konflikty roleplay o terytorium.
Odkrywanie schematów i rzadkie receptury
Nie wszystkie receptury powinny być dostępne od początku. Odkrywanie blueprintów dodaje motywację do eksploracji i progresji, ukrywając zaawansowane receptury za łupami, ukończeniem questów, zakupami u NPC lub losowymi wydarzeniami w świecie gry. Gdy gracz odkryje blueprint, zostaje on dodany do jego osobistej listy receptur zapisanej w bazie danych. Blueprinty mogą być przedmiotami wymiennymi, tworząc rynek wtórny, na którym rzadkie receptury stają się cennym towarem. Na przykład blueprint wojskowej broni może wypadać tylko z określonych napadów, podczas gdy legendarną recepturę kulinarną można kupić od ukrytego NPC, który pojawia się tylko w określonych porach. Interfejs craftingu sprawdza odblokowane blueprinty gracza z pełną listą receptur i pokazuje tylko te, do których ma dostęp, dając poczucie progresji, w którym każdy nowy blueprint to istotne odblokowanie rozszerzające możliwości craftingu.
-- 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)