Wróć do bloga
Guide8 min czytania

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)

Gotowy, aby zacząć?

Kup skrypty w naszym sklepie lub dołącz do Discorda, aby uzyskać wsparcie, aktualizacje i sprawdzić nadchodzące nowości.