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

نظام شجرة المهارات في FiveM: تقدم المستوى بشكل صحيح

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

Agency Scripts

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

لماذا تحول أشجار المهارات خوادم اللعب التمثيلي

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

تصميم هيكل بيانات شجرة المهارات

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

-- shared/skill_trees.lua
SkillTrees = {
    mechanic = {
        label = 'Mechanic',
        icon  = 'fa-wrench',
        nodes = {
            quickFix = {
                label    = 'Quick Fix',
                desc     = 'Reduce vehicle repair time',
                maxLevel = 5,
                cost     = 1,
                prereqs  = {},
                effect   = {type = 'repair_speed', perLevel = 0.10},
            },
            engineExpert = {
                label    = 'Engine Expert',
                desc     = 'Repair engines to higher condition',
                maxLevel = 3,
                cost     = 2,
                prereqs  = {'quickFix'},
                effect   = {type = 'engine_quality', perLevel = 0.15},
            },
            bodyworkMaster = {
                label    = 'Bodywork Master',
                desc     = 'Repair body damage more effectively',
                maxLevel = 3,
                cost     = 2,
                prereqs  = {'quickFix'},
                effect   = {type = 'body_quality', perLevel = 0.15},
            },
            turboTuner = {
                label    = 'Turbo Tuner',
                desc     = 'Unlock performance tuning abilities',
                maxLevel = 1,
                cost     = 5,
                prereqs  = {'engineExpert'},
                effect   = {type = 'unlock_tuning', perLevel = 1},
            },
        },
    },
    criminal = {
        label = 'Street Smarts',
        icon  = 'fa-mask',
        nodes = {
            lockpicking = {
                label    = 'Lockpicking',
                desc     = 'Pick locks faster with fewer failures',
                maxLevel = 5,
                cost     = 1,
                prereqs  = {},
                effect   = {type = 'lockpick_speed', perLevel = 0.12},
            },
            silentStep = {
                label    = 'Silent Step',
                desc     = 'Reduce noise while crouching',
                maxLevel = 3,
                cost     = 2,
                prereqs  = {'lockpicking'},
                effect   = {type = 'noise_reduction', perLevel = 0.20},
            },
            safecracker = {
                label    = 'Safecracker',
                desc     = 'Attempt to crack safes',
                maxLevel = 1,
                cost     = 4,
                prereqs  = {'lockpicking'},
                effect   = {type = 'unlock_safecrack', perLevel = 1},
            },
        },
    },
}

تتبع الخبرة والترقية

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

-- server/experience.lua
local XP_THRESHOLDS = {0, 100, 300, 600, 1000, 1500, 2200, 3000, 4000, 5500}

local recentActions = {}

function GrantXP(playerId, tree, amount, actionType)
    local identifier = GetPlayerIdentifier(playerId, 0)
    local now = os.time()

    -- Diminishing returns for repeated actions
    local key = identifier .. ':' .. tree .. ':' .. actionType
    if not recentActions[key] then
        recentActions[key] = {count = 0, resetAt = now + 600}
    end
    local ra = recentActions[key]
    if now > ra.resetAt then
        ra.count = 0
        ra.resetAt = now + 600
    end
    ra.count = ra.count + 1

    local multiplier = math.max(0.1, 1.0 - (ra.count - 1) * 0.25)
    local finalXP = math.floor(amount * multiplier)

    -- Update database
    MySQL.update([[
        UPDATE character_skills SET xp = xp + ?
        WHERE identifier = ? AND tree = ?
    ]], {finalXP, identifier, tree})

    -- Check for level up
    local data = MySQL.single.await([[
        SELECT xp, level, points FROM character_skills
        WHERE identifier = ? AND tree = ?
    ]], {identifier, tree})

    if data then
        local nextThreshold = XP_THRESHOLDS[data.level + 2]
        if nextThreshold and data.xp >= nextThreshold then
            local newLevel = data.level + 1
            MySQL.update([[
                UPDATE character_skills
                SET level = ?, points = points + 1
                WHERE identifier = ? AND tree = ?
            ]], {newLevel, identifier, tree})
            TriggerClientEvent('skills:levelUp', playerId, tree, newLevel)
        end
    end

    TriggerClientEvent('skills:xpGained', playerId, tree, finalXP)
end

تطبيق تأثيرات المهارات على طريقة اللعب

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

-- server/effects.lua
local playerSkillCache = {}

function GetSkillBonus(playerId, effectType)
    local identifier = GetPlayerIdentifier(playerId, 0)
    local cache = playerSkillCache[identifier]
    if not cache then return 0.0 end

    local total = 0.0
    for treeName, treeDef in pairs(SkillTrees) do
        for nodeId, nodeDef in pairs(treeDef.nodes) do
            if nodeDef.effect.type == effectType then
                local unlocked = cache[treeName] and cache[treeName][nodeId] or 0
                total = total + (nodeDef.effect.perLevel * unlocked)
            end
        end
    end
    return total
end

function HasSkillUnlock(playerId, effectType)
    return GetSkillBonus(playerId, effectType) > 0
end

-- Export for other resources
exports('GetSkillBonus', GetSkillBonus)
exports('HasSkillUnlock', HasSkillUnlock)

-- Example: another resource checking repair speed
-- local bonus = exports['skillsystem']:GetSkillBonus(source, 'repair_speed')
-- local repairTime = baseTime * (1.0 - bonus)

بناء شجرة المهارات باستخدام NUI

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

الاعتبارات المتعلقة بالتوازن ومكافحة الاستغلال

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

مخطط قاعدة البيانات واستراتيجية الترحيل

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

-- SQL schema
CREATE TABLE IF NOT EXISTS character_skills (
    id          INT AUTO_INCREMENT PRIMARY KEY,
    identifier  VARCHAR(64) NOT NULL,
    char_slot   INT DEFAULT 1,
    tree        VARCHAR(32) NOT NULL,
    level       INT DEFAULT 0,
    xp          INT DEFAULT 0,
    points      INT DEFAULT 0,
    allocations JSON DEFAULT '{}',
    created_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at  TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY unique_char_tree (identifier, char_slot, tree),
    INDEX idx_identifier (identifier)
);

جاهز للبدء؟

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