نظام الحرف اليدوية في 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)