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

نظام السمعة في 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، مما يمنح ردود فعل بصرية مرضية تعزز حلقة التقدم وتحافظ على تحفيز اللاعبين لمواصلة تطوير شخصياتهم.

جاهز للبدء؟

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