العودة إلى المدونة
Guide8 دقيقة قراءة

نظام الحرف اليدوية في FiveM: الوصفات، المحطات والتقدم

ابنِ نظام تصنيع عميق لـ FiveM. منطق الوصفات، ورش العمل، تقدم المهارات وأفضل سكريبتات التصنيع لـ QBCore، ESX و ox_inventory.

Agency Scripts

المؤسس والمطور الرئيسي في Agency Scripts

لماذا تدفع أنظمة الحرف المشاركة اللاعبين

نظام الحرف يمنح اللاعبين سببًا لجمع الموارد، استكشاف الخريطة، التفاعل مع لاعبين آخرين للتجارة، واستثمار الوقت في بناء شيء ملموس. بدون الحرف، تأتي العناصر إما من متاجر ذات مخزون لا نهائي أو من إنشاءات المسؤول، وكلاهما يسطح الاقتصاد ويزيل وكالة اللاعب. يخلق نظام الحرف المصمم جيدًا سلاسل توريد حيث يقوم لاعب واحد بتعدين المواد الخام، وآخر يصقلها، وثالث يجمع المنتج النهائي. تولد هذه الاعتمادية المتبادلة تفاعلات دورية عضوية ونشاطًا اقتصاديًا. تضيف صناعة الأسلحة طبقة من المخاطرة والمكافأة للعالم الإجرامي، بينما توفر مسارات الحرف القانونية مثل الطهي، الخياطة، أو إصلاح الإلكترونيات مصادر دخل شرعية. في هذا الدليل، سنبني إطار عمل حرف كامل مع وصفات، ورش عمل، تقدم المهارات، واكتشاف المخططات.

تصميم نظام الوصفات

الوصفات هي الهيكل الأساسي لأي نظام تصنيع. كل وصفة تحدد العناصر المطلوبة كمدخلات، والعنصر المنتج كمخرج، ومدة عملية التصنيع، ومستوى المهارة المطلوب، وخيارياً نوع ورشة العمل المطلوبة. خزّن الوصفات في ملف تكوين مشترك يمكن لكل من العميل والخادم الوصول إليه، لأن العميل يحتاج بيانات الوصفة لعرض واجهة التصنيع بينما يحتاج الخادم للتحقق من محاولات التصنيع. استخدم جدولًا مسطحًا مفهرسًا بمعرف وصفة فريد للبحث السريع. تضمّن category الحقل لتنظيم الوصفات في واجهة المستخدم و successChance الحقل الذي يمكن تعديله بمستوى مهارة اللاعب، مضيفًا عنصر مخاطرة للحرف عالية المستوى.

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

منطق الصنع على جانب الخادم

يجب أن تحدث جميع عمليات التحقق من الصنع على الخادم لمنع الاستغلال. يتحقق الخادم من أن اللاعب لديه المكونات المطلوبة في مخزونه، ويستوفي متطلبات المهارة، ويكون بالقرب من نوع طاولة العمل الصحيح، وليس في عملية صنع قائمة بالفعل. بعد التحقق، يزيل المكونات، ويشغل مؤقت المدة، ويحسب النجاح بناءً على الاحتمال الأساسي المعدل بمستوى المهارة، ويمنح العنصر المصنوع أو يعيد المواد الجزئية عند الفشل. لا تثق أبدًا بالعميل للإبلاغ عن نتائج الصنع. يجب على العميل فقط إرسال طلب لصنع وصفة محددة بالمعرف، ويتحقق الخادم بشكل مستقل من كل شرط. هذا يمنع استغلال تكرار العناصر حيث يدعي عميل معدل أنه صنع عناصر دون استهلاك المكونات.

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

تقدم المهارات والخبرة

نظام تقدم المهارات يضيف أهدافًا طويلة الأمد وشعورًا بالتقدم للاعبين الذين يستثمرون في الحرف. كل صناعة ناجحة تمنح نقاط خبرة تتراكم نحو مستويات المهارة. المستويات الأعلى تفتح وصفات متقدمة وتزيد من معدل نجاح الصناعات الصعبة. خزّن بيانات المهارة لكل شخصية في قاعدة البيانات بحيث تبقى مرتبطة بالشخصية وليس بحساب اللاعب. حدد معالم واضحة: المستوى 0-10 يسمح بالطهي والإصلاحات الأساسية، المستوى 11-30 يفتح الإلكترونيات والأدوات الأساسية، المستوى 31-60 يمكن مكونات الأسلحة والتصنيع المتقدم، والمستوى 61-100 يفتح وصفات نادرة وأساطير. عرض مستوى المهارة الحالي وشريط التقدم في واجهة الحرف حتى يرى اللاعبون دائمًا مدى قربهم من الفتح التالي.

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

تفاعل ورشة العمل وواجهة المستخدم للعميل

ورش العمل هي مواقع فعلية في عالم اللعبة يذهب إليها اللاعبون لصنع العناصر. يدعم كل نوع من ورش العمل فئة محددة من الوصفات: ورشة أسلحة للأسلحة النارية، مطبخ للطعام، محطة إلكترونيات للأجهزة. على جانب العميل، أنشئ علامة أو استخدم نظام استهداف مثل ox_target لإنشاء نقطة تفاعل في كل موقع ورشة عمل. عند تفاعل اللاعب، تحقق من قربه، وفرز الوصفات المتاحة بناءً على نوع ورشة العمل ومستوى مهارة اللاعب، وافتح واجهة صنع العناصر. يجب أن تعرض الواجهة اسم كل وصفة، المكونات المطلوبة مع عددها الحالي في المخزون، احتمال النجاح، وقت الصنع، ومكافأة خبرة المهارة. أبرز الوصفات التي يمتلك اللاعب كل المواد المطلوبة وقلل وضوح تلك التي تفتقد مواد. أثناء الصنع، شغّل حركة مناسبة، أظهر شريط تقدم، وعطل حركة اللاعب لمنع الاستغلال حيث يمشي اللاعب بعيدًا أثناء الصنع.

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

سلاسل الإنتاج وجمع المواد

تخلق سلاسل الإنتاج عمقًا من خلال مطالبة اللاعبين بمعالجة المواد الخام عبر مراحل متعددة قبل صنع المنتج النهائي. يجب صهر خام الحديد إلى سبائك فولاذ في فرن، ثم تُضغط إلى صفائح فولاذ في مصنع معادن، والتي تُستخدم أخيرًا على منضدة الأسلحة لصنع الأسلحة النارية. يتطلب كل خطوة موقع منضدة عمل مختلفة وربما مهارة مختلفة. يشجع تصميم السلسلة هذا التخصص، حيث يركز بعض اللاعبين على التعدين والصهر بينما يتخصص آخرون في التجميع. يجب توزيع نقاط جمع المواد عبر الخريطة باستخدام مناطق يمكن للاعبين فيها أداء إجراءات جمع مثل تعدين عُقد الصخور، حصاد النباتات، أو جمع المكونات الإلكترونية من مكبات النفايات. استخدم مؤقت إعادة ظهور على نقاط الجمع بحيث تكون الموارد محدودة ويتنافس اللاعبون على الوصول، مما يدفع نزاعات تمثيل الأدوار القائمة على الأراضي.

اكتشاف المخططات والوصفات النادرة

ليس كل الوصفات يجب أن تكون متاحة من البداية. اكتشاف المخططات يضيف حافزًا للاستكشاف والتقدم عن طريق إخفاء الوصفات المتقدمة خلف إسقاطات الغنائم، إكمال المهام، شراء من NPC، أو أحداث عشوائية في العالم. عندما يكتشف اللاعب مخططًا، يُضاف إلى قائمة وصفاته الشخصية المخزنة في قاعدة البيانات. يمكن أن تكون المخططات عناصر قابلة للتداول، مما يخلق سوقًا ثانوية حيث تصبح الوصفات النادرة سلعًا ذات قيمة. على سبيل المثال، قد يسقط مخطط سلاح عسكري فقط من مكافآت سرقة معينة، في حين يمكن شراء وصفة طهي أسطورية من NPC مخفي يظهر فقط في أوقات محددة. تتحقق واجهة الصنع من المخططات المفتوحة للاعب مقابل قائمة الوصفات الكاملة وتعرض فقط الوصفات التي يمكنهم الوصول إليها، مما يخلق إحساسًا بالتقدم حيث يشعر كل مخطط جديد بأنه فتح مهم يوسع قدراتهم في الصنع.

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

جاهز للبدء؟

احصل على السكربتات من متجرنا، أو انضم إلى Discord للدعم والتحديثات ونظرة على ما هو قادم.