Zurück zum Blog
Guide9 Min. Lesezeit

FiveM-Handwerkssystem: Rezepte, Werkbänke und Fortschritt

Erstelle ein vielseitiges Handwerkssystem für FiveM mit Rezepten, Werkbänken, Fähigkeiten und den besten Scripts für QBCore, ESX und ox_inventory.

Agency Scripts

Gründer und leitender Entwickler bei Agency Scripts

Warum Crafting-Systeme das Engagement von Spielern fördern

Ein Crafting-System gibt Spielern einen Grund, Ressourcen zu sammeln, die Karte zu erkunden, mit anderen Spielern zu handeln und Zeit in die Herstellung greifbarer Gegenstände zu investieren. Ohne Crafting kommen Gegenstände entweder aus Läden mit unbegrenztem Vorrat oder aus Admin-Spawns. Beides schwächt die Wirtschaft und nimmt Spielern den Einfluss auf das Spielgeschehen. Ein gut konzipiertes Crafting-System schafft Lieferketten, in denen ein Spieler Rohstoffe abbaut, ein anderer sie verarbeitet und ein dritter das Endprodukt herstellt. Diese gegenseitige Abhängigkeit sorgt für spontane Roleplay-Interaktionen und wirtschaftliche Aktivität. Das Herstellen von Waffen ergänzt die kriminelle Unterwelt um Risiko und Belohnung, während legale Crafting-Wege wie Kochen, Schneidern oder die Reparatur von Elektronik legitime Einnahmequellen bieten. In diesem Leitfaden erstellen wir ein vollständiges Crafting-Framework mit Rezepten, Werkbänken, Skill-Fortschritt und dem Entdecken von Bauplänen.

Das Rezeptsystem entwerfen

Rezepte sind die zentrale Datenstruktur jedes Crafting-Systems. Jedes Rezept legt fest, welche Gegenstände als Zutaten benötigt werden, welcher Gegenstand als Ergebnis entsteht, wie lange der Crafting-Vorgang dauert, welches Skill-Level erforderlich ist und optional, welcher Werkbanktyp benötigt wird. Speichere Rezepte in einer gemeinsamen Konfigurationsdatei, auf die Client und Server zugreifen können. Der Client benötigt Rezeptdaten, um die Crafting-UI anzuzeigen, während der Server sie zur Validierung von Crafting-Versuchen benötigt. Verwende für schnelle Suchvorgänge eine flache Tabelle, die nach einer eindeutigen Rezept-ID indiziert ist. Füge ein category -Feld hinzu, um Rezepte in der UI zu organisieren, und ein successChance -Feld, das sich anhand des Skill-Levels des Spielers ändern lässt und bei Crafting auf hohem Niveau ein Risikoelement hinzufügt.

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

Crafting-Logik auf dem Server

Die gesamte Crafting-Validierung muss auf dem Server erfolgen, um Exploits zu verhindern. Der Server prüft, ob der Spieler die erforderlichen Zutaten im Inventar hat, das nötige Skill-Level erreicht, sich in der Nähe des passenden Werkbanktyps befindet und nicht bereits einen Crafting-Vorgang ausführt. Nach der Validierung entfernt der Server die Zutaten, startet den Timer für die Dauer, berechnet die Erfolgschance anhand der Basiswahrscheinlichkeit, angepasst an das Skill-Level, und gibt entweder den hergestellten Gegenstand aus oder erstattet bei einem Fehlschlag einen Teil der Materialien zurück. Vertraue dem Client niemals die Meldung des Crafting-Ergebnisses an. Der Client sollte nur eine Anfrage zum Crafting einer bestimmten Rezept-ID senden. Der Server überprüft dann unabhängig alle Bedingungen. So werden Exploits zur Gegenstandsduplizierung verhindert, bei denen ein modifizierter Client behauptet, Gegenstände hergestellt zu haben, ohne Zutaten zu verbrauchen.

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

Skill-Fortschritt und Erfahrung

Ein Skill-Fortschrittssystem gibt Spielern, die Zeit ins Crafting investieren, langfristige Ziele und ein Gefühl des Vorankommens. Für jedes erfolgreiche Crafting erhalten sie Erfahrungspunkte, die sich bis zum nächsten Skill-Level summieren. Höhere Skill-Level schalten fortgeschrittene Rezepte frei und erhöhen die Erfolgschance bei anspruchsvollen Crafting-Vorgängen. Speichere Skill-Daten für jeden Charakter in der Datenbank, damit sie erhalten bleiben und an den Charakter statt an das Spielerkonto gebunden sind. Lege klare Meilensteine fest: Level 0-10 ermöglicht einfaches Kochen und Reparaturen, Level 11-30 schaltet Elektronik und einfache Werkzeuge frei, Level 31-60 ermöglicht Waffenkomponenten und fortgeschrittene Fertigung, und Level 61-100 schaltet seltene und legendäre Rezepte frei. Zeige das aktuelle Skill-Level und den Fortschrittsbalken in der Crafting-UI an, damit Spieler immer sehen, wie nah sie am nächsten freischaltbaren Inhalt sind.

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

Interaktion mit Werkbänken und Client-UI

Werkbänke sind Orte in der Spielwelt, an denen Spieler Gegenstände herstellen können. Jeder Werkbanktyp unterstützt eine bestimmte Rezeptkategorie: eine Waffenwerkbank für Schusswaffen, eine Küche für Lebensmittel, eine Elektronikstation für Gadgets. Erstelle clientseitig an jedem Werkbankstandort einen Marker oder verwende ein Target-System wie ox_target, um einen Interaktionspunkt einzurichten. Prüfe bei der Interaktion, ob sich der Spieler in der Nähe befindet, filtere die verfügbaren Rezepte nach Werkbanktyp und Skill-Level des Spielers und öffne die Crafting-UI. Die UI sollte für jedes Rezept den Namen, die benötigten Zutaten mit den aktuellen Inventarbeständen, die Erfolgschance, die Crafting-Dauer und die Skill-XP-Belohnung anzeigen. Hebe Rezepte hervor, für die der Spieler alle benötigten Materialien besitzt, und blende Rezepte ab, bei denen Materialien fehlen. Spiele während des Craftings eine passende Animation ab, zeige einen Fortschrittsbalken an und deaktiviere die Spielerbewegung, um Exploits zu verhindern, bei denen Spieler sich mitten im Crafting-Vorgang entfernen.

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

Produktionsketten und Materialbeschaffung

Produktionsketten sorgen für mehr Tiefe, indem Spieler Rohstoffe in mehreren Schritten verarbeiten müssen, bevor sie das Endprodukt herstellen können. Eisenerz muss in einem Schmelzofen zu Stahlbarren geschmolzen werden. Anschließend werden diese in einem Metallwerk zu Stahlplatten gepresst, die schließlich an einer Waffenwerkbank zur Herstellung von Schusswaffen verwendet werden. Jeder Schritt erfordert einen anderen Werkbankstandort und möglicherweise eine andere Fähigkeit. Dieses Kettendesign fördert die Spezialisierung: Einige Spieler konzentrieren sich auf den Abbau und das Schmelzen, während andere sich auf die Montage spezialisieren. Sammelstellen für Materialien sollten über die Karte verteilt sein und Zonen nutzen, in denen Spieler Rohstoffe sammeln können, zum Beispiel durch den Abbau von Gesteinsvorkommen, das Ernten von Pflanzen oder das Durchsuchen von Schrottplätzen nach elektronischen Bauteilen. Nutze einen Respawn-Timer für Sammelstellen, damit Ressourcen begrenzt sind und Spieler um den Zugang konkurrieren, was territoriale Rollenspielkonflikte fördert.

Bauplanentdeckung und seltene Rezepte

Nicht alle Rezepte sollten von Anfang an verfügbar sein. Die Entdeckung von Bauplänen sorgt für einen Anreiz zur Erkundung und zum Spielfortschritt, indem fortgeschrittene Rezepte hinter Beute, abgeschlossenen Quests, Käufen bei NPCs oder zufälligen Weltereignissen verborgen werden. Entdeckt ein Spieler einen Bauplan, wird er seiner persönlichen, in der Datenbank gespeicherten Rezeptliste hinzugefügt. Baupläne können handelbare Gegenstände sein und so einen Sekundärmarkt schaffen, auf dem seltene Rezepte zu wertvollen Handelsgütern werden. So könnte ein Bauplan für eine Waffe in Militärqualität nur als Belohnung für bestimmte Raubüberfälle erhältlich sein, während ein legendäres Kochrezept bei einem versteckten NPC gekauft werden kann, der nur zu bestimmten Zeiten auftaucht. Die Crafting-UI gleicht die freigeschalteten Baupläne des Spielers mit der vollständigen Rezeptliste ab und zeigt nur Rezepte an, auf die der Spieler Zugriff hat. So entsteht ein Gefühl des Fortschritts, bei dem jeder neue Bauplan eine bedeutsame Freischaltung darstellt und die Crafting-Möglichkeiten erweitert.

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

Bereit loszulegen?

Hol dir die Scripts in unserem Shop, oder komm auf den Discord für Support, Updates und einen Blick auf das, was kommt.