نظام السمعة في FiveM: الإحصائيات، المهارات والتقدم
إضافة السمعة وتقدم المهارات إلى FiveM. صيغ الإحصائيات، الفتحات، أشرطة واجهة المستخدم وأفضل سكربتات السمعة للحفاظ على استمرار اللاعبين في اللعب لفترة أطول.
Agency Scripts
المؤسس والمطور الرئيسي في Agency Scripts
لماذا تحول أنظمة السمعة اللعب التمثيلي
تعامل معظم خوادم FiveM مع تقدم الشخصية كأمر ثنائي: إما أن يكون لديك وظيفة أو لا. لا يوجد إحساس بالنمو، الإتقان، أو الحالة المكتسبة. يغير نظام السمعة والمهارات هذا بشكل جذري من خلال منح اللاعبين تقدمًا قابلاً للقياس يكافئ الوقت المستثمر والأنشطة المكتملة. عندما يصل الميكانيكي إلى إصلاح مركبته المئة ويفتح ضبط المحرك المتقدم، يشعر هذا الإنجاز بأنه مكتسب. عندما يبني المجرم سمعة كافية في الشارع للوصول إلى معدات السطو عالية المستوى، هناك إحساس ملموس بالتقدم يدفع طريقة لعبه للأمام. في هذا الدليل، سنبني إطار عمل كامل للسمعة والمهارات من الصفر، يشمل حسابات XP، أشجار المهارات، مستويات السمعة، القدرات القابلة للفتح، ونظام التميز الذي يبقي اللاعبين في نهاية اللعبة متفاعلين.
مخطط قاعدة البيانات لمهارات اللاعبين
أساس أي نظام تقدم هو مخطط قاعدة بيانات مصمم جيدًا. تحتاج إلى جداول تتعقب نقاط خبرة المهارات الفردية، درجات السمعة لكل فصيل أو نشاط، والقدرات المفتوحة. يجب أن يكون المخطط منظمًا بما يكفي ليكون قابلًا للاستعلام لكنه غير منظم بشكل مفرط لتجنب الانضمامات المكلفة في كل فحص مهارة. نحن نستخدم جدولًا واحدًا player_skills جدول بمفتاح مركب من معرف المواطن واسم المهارة، بالإضافة إلى player_reputation جدول لحالة الفصيل. هذا الفصل يحافظ على المهارات (القدرة الشخصية) منفصلة عن السمعة (كيف يرى NPCs والفصائل اللاعب)، مما يتيح تفاعلات لعب أكثر تعقيداً.
-- SQL schema
CREATE TABLE IF NOT EXISTS player_skills (
citizenid VARCHAR(50) NOT NULL,
skill_name VARCHAR(50) NOT NULL,
xp INT DEFAULT 0,
level INT DEFAULT 1,
prestige INT DEFAULT 0,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (citizenid, skill_name)
);
CREATE TABLE IF NOT EXISTS player_reputation (
citizenid VARCHAR(50) NOT NULL,
faction VARCHAR(50) NOT NULL,
reputation INT DEFAULT 0,
tier VARCHAR(20) DEFAULT 'neutral',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (citizenid, faction)
);
حساب الخبرة ومنحنيات التقدم
منحنى XP ثابت حيث يتطلب كل مستوى نفس كمية الخبرة يشعر بعدم المكافأة لأن المستويات المبكرة تمر بسرعة كبيرة بينما المستويات المتأخرة تبدو متطابقة. النهج الأفضل يستخدم منحنى كثير الحدود حيث يتطلب كل مستوى المزيد من XP تدريجيًا، لكن الزيادة ليست شديدة بحيث يصبح المستوى الأقصى غير قابل للتحقيق. الصيغة الشائعة هي requiredXP = baseXP * (level ^ exponent) حيث قاعدة 100 وأُس 1.5 تخلق منحنى سلس. المستوى 1 يتطلب 100 XP، المستوى 10 يتطلب حوالي 3,162 XP، والمستوى 50 يتطلب تقريبًا 35,355 XP. هذا يحافظ على تقدم سريع في المراحل المبكرة بينما يجعل المستويات الأعلى تبدو كإنجازات حقيقية تتطلب لعبًا مكرسًا للوصول إليها.
-- shared/skills_config.lua
SkillsConfig = {}
SkillsConfig.BaseXP = 100
SkillsConfig.Exponent = 1.5
SkillsConfig.MaxLevel = 50
function SkillsConfig.GetRequiredXP(level)
return math.floor(SkillsConfig.BaseXP * (level ^ SkillsConfig.Exponent))
end
function SkillsConfig.GetTotalXPForLevel(targetLevel)
local total = 0
for i = 1, targetLevel - 1 do
total = total + SkillsConfig.GetRequiredXP(i)
end
return total
end
-- Skill definitions with XP sources
SkillsConfig.Skills = {
driving = {
label = 'Driving',
icon = 'fa-car',
xpSources = {
{ action = 'distance_driven', xpPer = 1, unit = 'per 500m' },
{ action = 'race_won', xpPer = 150, unit = 'per win' },
{ action = 'no_crash_streak', xpPer = 50, unit = 'per 5min' },
}
},
shooting = {
label = 'Marksmanship',
icon = 'fa-crosshairs',
xpSources = {
{ action = 'headshot', xpPer = 25, unit = 'per kill' },
{ action = 'bodyshot', xpPer = 10, unit = 'per kill' },
{ action = 'range_practice', xpPer = 5, unit = 'per target' },
}
},
mechanic = {
label = 'Mechanic',
icon = 'fa-wrench',
xpSources = {
{ action = 'vehicle_repaired', xpPer = 30, unit = 'per repair' },
{ action = 'engine_swap', xpPer = 100, unit = 'per swap' },
{ action = 'custom_tune', xpPer = 75, unit = 'per tune' },
}
},
cooking = {
label = 'Cooking',
icon = 'fa-utensils',
xpSources = {
{ action = 'meal_cooked', xpPer = 20, unit = 'per meal' },
{ action = 'recipe_discovered', xpPer = 50, unit = 'per recipe' },
}
},
stamina = {
label = 'Stamina',
icon = 'fa-running',
xpSources = {
{ action = 'distance_sprinted', xpPer = 1, unit = 'per 200m' },
{ action = 'swimming', xpPer = 2, unit = 'per 100m' },
}
},
}
إدارة الخبرة (XP) على جانب الخادم
يجب أن تحدث كل تعديلات XP على جانب السيرفر لمنع الاستغلالات. يتحقق السيرفر من كل طلب منح XP، يفحص تحديد المعدل لمنع السبام، يطبق أي مضاعفات نشطة، ويحفظ النتيجة في قاعدة البيانات. قرار تصميم حاسم هو ما إذا كان الحفظ يتم عند كل تغيير XP أو دفعات. الحفظ عند كل زيادة صغيرة يضغط على قاعدة البيانات، خصوصًا للمهارات السلبية مثل مسافة القيادة. الحل هو ذاكرة تخزين مؤقت في الذاكرة تُفرغ دوريًا إلى قاعدة البيانات، مع حفظ فوري للأحداث المهمة مثل الترقيات. هذا يمنحك دقة في الوقت الحقيقي للاعب مع الحفاظ على تحميل قاعدة البيانات قابل للإدارة.
-- server/skills_manager.lua
local playerCache = {}
local SAVE_INTERVAL = 60000 -- flush to DB every 60 seconds
-- Load player skills from DB on join
AddEventHandler('playerJoining', function()
local src = source
local citizenid = GetCitizenId(src)
if not citizenid then return end
local skills = MySQL.query.await('SELECT * FROM player_skills WHERE citizenid = ?', {citizenid})
playerCache[citizenid] = {}
for _, row in ipairs(skills or {}) do
playerCache[citizenid][row.skill_name] = {
xp = row.xp,
level = row.level,
prestige = row.prestige,
dirty = false
}
end
end)
function AddSkillXP(citizenid, skillName, amount)
if not SkillsConfig.Skills[skillName] then return end
if not playerCache[citizenid] then return end
local skill = playerCache[citizenid][skillName]
if not skill then
skill = { xp = 0, level = 1, prestige = 0, dirty = false }
playerCache[citizenid][skillName] = skill
end
-- Apply prestige multiplier (each prestige = +5% XP)
local multiplier = 1.0 + (skill.prestige * 0.05)
local finalXP = math.floor(amount * multiplier)
skill.xp = skill.xp + finalXP
skill.dirty = true
-- Check for level up
local required = SkillsConfig.GetRequiredXP(skill.level)
while skill.xp >= required and skill.level < SkillsConfig.MaxLevel do
skill.xp = skill.xp - required
skill.level = skill.level + 1
required = SkillsConfig.GetRequiredXP(skill.level)
-- Notify player of level up
local src = GetPlayerByCitizenId(citizenid)
if src then
TriggerClientEvent('skills:levelUp', src, skillName, skill.level)
end
-- Immediate save on level up
SaveSkill(citizenid, skillName, skill)
end
return skill
end
exports('AddSkillXP', AddSkillXP)
أشجار المهارات والقدرات القابلة للفتح
تضيف أشجار المهارات طبقة استراتيجية فوق الترقية الخام. بدلاً من أن يكون كل لاعب في المستوى 20 في الميكانيكا متطابقًا، تتيح أشجار المهارات التخصص. قد يستثمر ميكانيكي واحد نقاطًا في أداء المحرك، مما يفتح القدرة على إضافة مجموعات التيربو، بينما يتخصص آخر في الهيكل، ويحصل على وصول إلى طلاءات وتصاميم حصرية. يمنح كل مستوى نقطة مهارة واحدة يمكن إنفاقها في شجرة متفرعة. تستخدم بنية الشجرة علاقة أب-ابن بسيطة حيث يتطلب فتح عقدة الابن فتح الأب ومستوى مهارة أدنى. هذا يمنع اللاعبين من التسرع إلى أقوى القدرات دون بناء كفاءة أساسية.
-- shared/skill_trees.lua
SkillTrees = {
mechanic = {
{ id = 'basic_repair', label = 'Basic Repair', reqLevel = 1, parent = nil,
effect = { type = 'speed', value = 1.0 }, desc = 'Standard repair speed' },
{ id = 'fast_repair', label = 'Quick Hands', reqLevel = 5, parent = 'basic_repair',
effect = { type = 'speed', value = 1.3 }, desc = '30% faster repairs' },
{ id = 'engine_diag', label = 'Engine Diagnostics', reqLevel = 10, parent = 'basic_repair',
effect = { type = 'unlock', value = 'engine_scan' }, desc = 'Scan engine health remotely' },
{ id = 'turbo_kit', label = 'Turbo Installation', reqLevel = 20, parent = 'engine_diag',
effect = { type = 'unlock', value = 'install_turbo' }, desc = 'Install turbo kits on vehicles' },
{ id = 'master_tune', label = 'Master Tuner', reqLevel = 35, parent = 'turbo_kit',
effect = { type = 'unlock', value = 'advanced_tune' }, desc = 'Access to advanced ECU tuning' },
{ id = 'body_expert', label = 'Body Expert', reqLevel = 10, parent = 'basic_repair',
effect = { type = 'unlock', value = 'custom_paint' }, desc = 'Unlock exclusive paint options' },
{ id = 'livery_master', label = 'Livery Master', reqLevel = 25, parent = 'body_expert',
effect = { type = 'unlock', value = 'custom_livery' }, desc = 'Create and apply custom liveries' },
},
}
-- server: unlock a skill tree node
function UnlockNode(citizenid, skillName, nodeId)
local tree = SkillTrees[skillName]
if not tree then return false, 'No tree for skill' end
local node = nil
for _, n in ipairs(tree) do
if n.id == nodeId then node = n break end
end
if not node then return false, 'Node not found' end
local skill = GetPlayerSkill(citizenid, skillName)
if not skill or skill.level < node.reqLevel then
return false, 'Level too low'
end
if node.parent then
local parentUnlocked = IsNodeUnlocked(citizenid, skillName, node.parent)
if not parentUnlocked then return false, 'Parent node locked' end
end
MySQL.insert('INSERT INTO player_skill_nodes (citizenid, skill_name, node_id) VALUES (?, ?, ?)',
{citizenid, skillName, nodeId})
return true
end
مستويات السمعة ومكانة الفصيل
تتبع السمعة كيف ترى المنظمات والفصائل اللاعب، منفصلة عن المهارة الخام. قد يكون اللاعب سائقاً ماهراً لكنه ذو مكانة سيئة مع فصيل الشرطة بسبب أنشطته الإجرامية. تُنظم السمعة إلى مستويات تتراوح من عدائي إلى محايد إلى موقر، مع فتح خيارات حوار مختلفة، وصول إلى المهام، وأسعار البائعين لكل مستوى. كسب السمعة مع فصيل واحد يمكن أن يقللها مع الفصائل المعارضة، مما يخلق مقايضات ذات معنى. على سبيل المثال، إكمال توصيلات المخدرات يزيد سمعة الكارتل لكنه يقلل سمعة الشرطة. يشجع هذا النظام اللاعبين على اتخاذ قرارات تحدد شخصيتهم بدلاً من تعظيم كل فصيل في نفس الوقت.
-- shared/reputation_config.lua
ReputationConfig = {
tiers = {
{ name = 'hostile', minRep = -1000, color = '#ef4444' },
{ name = 'unfriendly', minRep = -500, color = '#f97316' },
{ name = 'neutral', minRep = 0, color = '#94a3b8' },
{ name = 'friendly', minRep = 500, color = '#22c55e' },
{ name = 'honored', minRep = 1500, color = '#3b82f6' },
{ name = 'revered', minRep = 3000, color = '#a855f7' },
},
factions = {
police = { label = 'LSPD', opposing = {'cartel', 'gang_ballas'} },
ems = { label = 'EMS', opposing = {} },
cartel = { label = 'Madrazo Cartel', opposing = {'police'} },
mechanic = { label = 'LS Customs', opposing = {} },
gang_ballas = { label = 'Ballas', opposing = {'police', 'gang_families'} },
gang_families = { label = 'Families', opposing = {'gang_ballas'} },
},
opposingPenalty = 0.5, -- lose 50% of gained rep from opposing factions
}
-- server: modify reputation with faction cascading
function ModifyReputation(citizenid, faction, amount)
local config = ReputationConfig.factions[faction]
if not config then return end
-- Apply to primary faction
AdjustRep(citizenid, faction, amount)
-- Penalize opposing factions
if amount > 0 then
for _, opposing in ipairs(config.opposing) do
local penalty = math.floor(amount * ReputationConfig.opposingPenalty)
AdjustRep(citizenid, opposing, -penalty)
end
end
end
The Prestige System
بمجرد أن يصل اللاعب إلى المستوى الأقصى في مهارة، يحتاج إلى سبب للاستمرار في الانخراط في هذا النشاط. يوفر نظام الهيبة ذلك من خلال السماح للاعبين بإعادة تعيين المهارة إلى المستوى 1 مقابل مكافآت دائمة: شارة هيبة تجميلية تُعرض بجانب اسمهم، زيادة 5% في نقاط الخبرة لكل مستوى هيبة في تلك المهارة، والوصول إلى محتويات حصرية للهيبة مثل تعديلات مركبات فريدة أو وصفات تصنيع نادرة. عداد الهيبة غير محدود، لكن كل هيبة متتالية تستغرق وقتاً أطول لأن زيادة نقاط الخبرة تجعل المستويات الأولى سهلة بينما المنحنى لا يزال يلحق بالمستويات الأعلى. عرض عداد الهيبة بالأرقام الرومانية أو النجوم بجانب اسم المهارة في واجهة المستخدم لإعطاء مؤشر بصري واضح على تفاني اللاعب.
-- server: prestige a maxed skill
function PrestigeSkill(citizenid, skillName)
local skill = playerCache[citizenid] and playerCache[citizenid][skillName]
if not skill then return false, 'Skill not found' end
if skill.level < SkillsConfig.MaxLevel then return false, 'Not max level' end
-- Reset level and XP, increment prestige
skill.level = 1
skill.xp = 0
skill.prestige = skill.prestige + 1
skill.dirty = true
-- Save immediately
SaveSkill(citizenid, skillName, skill)
-- Grant prestige reward
local src = GetPlayerByCitizenId(citizenid)
if src then
TriggerClientEvent('skills:prestige', src, skillName, skill.prestige)
-- Unlock prestige-specific items
if skill.prestige == 1 then
exports.ox_inventory:AddItem(src, 'prestige_badge_'..skillName, 1)
elseif skill.prestige == 5 then
exports.ox_inventory:AddItem(src, 'gold_tool_'..skillName, 1)
end
end
return true, skill.prestige
end
RegisterNetEvent('skills:requestPrestige', function(skillName)
local src = source
local citizenid = GetCitizenId(src)
local success, result = PrestigeSkill(citizenid, skillName)
if success then
lib.notify(src, { title = 'Prestige!', description = ('Prestige %d achieved for %s'):format(result, skillName), type = 'success' })
else
lib.notify(src, { title = 'Cannot Prestige', description = result, type = 'error' })
end
end)
واجهة المستخدم على جانب العميل وتتبع XP السلبي
يتولى العميل تتبع XP السلبي لأنشطة مثل مسافة القيادة والجري، ويرسل تحديثات دورية إلى الخادم. خطأ شائع هو إرسال حدث على كل متر يُقطع، مما يفيض الشبكة. بدلاً من ذلك، اجمع المسافة في متغير محلي وأرسل تحديثًا مجمعًا كل 30 ثانية. بالنسبة لواجهة المستخدم، أنشئ لوحة مهارات يمكن الوصول إليها عبر أمر أو مفتاح تعرض جميع المهارات مع مستواها الحالي، شريط تقدم XP، عدد التميز، وشجرة المهارات. استخدم NUI مع إطار عمل حديث مثل React أو Vue لتصور الشجرة، مع إظهار العقد المقفلة باللون الرمادي والعقد المفتوحة بتأثير توهج. يجب أن يتحرك شريط التقدم بسلاسة عند اكتساب XP، مما يمنح ردود فعل بصرية مرضية تعزز حلقة التقدم وتحافظ على تحفيز اللاعبين لمواصلة تطوير شخصياتهم.