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

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

ابنِ تاجر سيارات واقعي لـ FiveM. تجارب القيادة، إعداد صالة العرض، خيارات NPC وتشغيل اللاعبين، بالإضافة إلى أفضل سكريبتات الوكالات لسيرفرات QBCore و ESX.

Agency Scripts

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

هيكلية وكالة بيع المركبات

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

نظام كتالوج وتصنيف المركبات

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

Config.VehicleCatalog = {
    categories = {
        {id = 'sedan', label = 'Sedans', icon = 'fa-car'},
        {id = 'sport', label = 'Sports Cars', icon = 'fa-flag-checkered'},
        {id = 'suv', label = 'SUVs & Trucks', icon = 'fa-truck'},
        {id = 'muscle', label = 'Muscle Cars', icon = 'fa-bolt'},
        {id = 'motorcycle', label = 'Motorcycles', icon = 'fa-motorcycle'},
    },
    vehicles = {
        -- Sedans
        {model = 'sultan', label = 'Karin Sultan', price = 24500,
         category = 'sedan', seats = 4, testDrive = true},
        {model = 'schafter2', label = 'Benefactor Schafter', price = 38000,
         category = 'sedan', seats = 4, testDrive = true},
        -- Sports
        {model = 'elegy2', label = 'Annis Elegy RH8', price = 95000,
         category = 'sport', seats = 2, testDrive = true},
        {model = 'comet2', label = 'Pfister Comet', price = 110000,
         category = 'sport', seats = 2, testDrive = true},
        -- SUVs
        {model = 'baller', label = 'Gallivanter Baller', price = 55000,
         category = 'suv', seats = 4, testDrive = true},
        -- Muscle
        {model = 'dominator', label = 'Vapid Dominator', price = 42000,
         category = 'muscle', seats = 2, testDrive = true},
        -- Motorcycles
        {model = 'bati', label = 'Pegassi Bati 801', price = 18000,
         category = 'motorcycle', seats = 2, testDrive = true},
    },
}

-- Build lookup table for fast server-side price validation
Config.VehiclePrices = {}
for _, v in ipairs(Config.VehicleCatalog.vehicles) do
    Config.VehiclePrices[v.model] = v.price
end

جدول البحث عن الأسعار Config.VehiclePrices يُمكّن التحقق من السعر بوقت ثابت O(1) على الخادم، مما يمنع العملاء من إرسال أسعار معدلة. تحقق دائمًا من اسم النموذج مقابل هذا الجدول قبل معالجة الشراء. فكر في تحميل أسعار المركبات من جدول قاعدة بيانات بدلاً من ملف التكوين إذا كنت تريد لمسؤولي الخادم تعديل الأسعار عبر لوحة الإدارة دون إعادة تشغيل الخادم.

نظام معاينة صالة العرض

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

-- client/showroom.lua
local previewVehicle = nil
local previewCam = nil
local camAngle = 0.0

function ShowVehiclePreview(modelName)
    -- Clean up previous preview
    DestroyPreview()

    local model = GetHashKey(modelName)
    RequestModel(model)
    while not HasModelLoaded(model) do Wait(10) end

    local showroomPos = Config.ShowroomPosition -- vector4
    previewVehicle = CreateVehicle(model, showroomPos.x, showroomPos.y,
        showroomPos.z, showroomPos.w, false, false)

    SetEntityInvincible(previewVehicle, true)
    SetVehicleDoorsLocked(previewVehicle, 2)
    FreezeEntityPosition(previewVehicle, true)
    SetVehicleOnGroundProperly(previewVehicle)
    SetModelAsNoLongerNeeded(model)

    -- Create orbiting camera
    previewCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
    UpdateCameraPosition()
    SetCamActive(previewCam, true)
    RenderScriptCams(true, true, 500, true, true)
end

function UpdateCameraPosition()
    if not previewCam or not previewVehicle then return end
    local vehPos = GetEntityCoords(previewVehicle)
    local radius = 6.0
    local height = 2.0
    local rad = math.rad(camAngle)
    local camX = vehPos.x + radius * math.cos(rad)
    local camY = vehPos.y + radius * math.sin(rad)
    SetCamCoord(previewCam, camX, camY, vehPos.z + height)
    PointCamAtEntity(previewCam, previewVehicle, 0.0, 0.0, 0.0, true)
end

function DestroyPreview()
    if previewVehicle then
        DeleteEntity(previewVehicle)
        previewVehicle = nil
    end
    if previewCam then
        SetCamActive(previewCam, false)
        RenderScriptCams(false, true, 500, true, true)
        DestroyCam(previewCam, false)
        previewCam = nil
    end
end

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

المشتريات ومعالجة المعاملات

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

RegisterNetEvent('dealer:server:purchaseVehicle', function(modelName, paymentType)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    -- Validate model exists and get price
    local price = Config.VehiclePrices[modelName]
    if not price then
        TriggerClientEvent('QBCore:Notify', src, 'Vehicle not available', 'error')
        return
    end

    -- Check payment
    local moneyType = paymentType == 'bank' and 'bank' or 'cash'
    if Player.PlayerData.money[moneyType] < price then
        TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
        return
    end

    -- Generate unique plate
    local plate = GenerateUniquePlate()

    -- Process payment
    Player.Functions.RemoveMoney(moneyType, price, 'vehicle-purchase-' .. modelName)

    -- Create vehicle record
    local vehicleHash = GetHashKey(modelName)
    MySQL.insert([[
        INSERT INTO player_vehicles
        (citizenid, vehicle, hash, plate, garage, state, fuel, engine, body)
        VALUES (?, ?, ?, ?, ?, 1, 100, 1000.0, 1000.0)
    ]], {
        Player.PlayerData.citizenid,
        modelName,
        tostring(vehicleHash),
        plate,
        'pillboxgarage',
    })

    -- Log transaction
    MySQL.insert([[
        INSERT INTO vehicle_sales (citizenid, vehicle, plate, price, sold_at)
        VALUES (?, ?, ?, ?, NOW())
    ]], {Player.PlayerData.citizenid, modelName, plate, price})

    TriggerClientEvent('QBCore:Notify', src,
        'Vehicle purchased! Plate: ' .. plate, 'success')
    TriggerClientEvent('dealer:client:vehiclePurchased', src, modelName, plate)
end)

function GenerateUniquePlate()
    local plate
    repeat
        plate = ''
        local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
        for i = 1, 8 do
            local idx = math.random(1, #chars)
            plate = plate .. chars:sub(idx, idx)
        end
        local exists = MySQL.scalar.await(
            'SELECT 1 FROM player_vehicles WHERE plate = ?', {plate}
        )
    until not exists
    return plate
end

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

نظام تجربة القيادة

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

-- Client: test drive logic
local testDriveVehicle = nil
local testDriveTimer = 0
local testDriveActive = false

function StartTestDrive(modelName, duration)
    local model = GetHashKey(modelName)
    RequestModel(model)
    while not HasModelLoaded(model) do Wait(10) end

    local spawnPos = Config.TestDriveSpawn
    testDriveVehicle = CreateVehicle(model, spawnPos.x, spawnPos.y,
        spawnPos.z, spawnPos.w, true, false)

    SetVehicleNumberPlateText(testDriveVehicle, 'TESTDRVE')
    TaskWarpPedIntoVehicle(PlayerPedId(), testDriveVehicle, -1)
    SetModelAsNoLongerNeeded(model)

    testDriveTimer = duration
    testDriveActive = true

    -- Timer and boundary check thread
    CreateThread(function()
        while testDriveActive and testDriveTimer > 0 do
            Wait(1000)
            testDriveTimer = testDriveTimer - 1

            -- Show remaining time
            SendNUIMessage({
                action = 'updateTestDrive',
                timeLeft = testDriveTimer
            })

            -- Check boundary
            local playerPos = GetEntityCoords(PlayerPedId())
            local dealerPos = Config.DealerLocation
            if #(playerPos - dealerPos) > Config.TestDriveRadius then
                QBCore.Functions.Notify('Too far from dealer, returning...', 'error')
                EndTestDrive()
                return
            end
        end

        if testDriveActive then
            EndTestDrive()
        end
    end)
end

function EndTestDrive()
    testDriveActive = false
    if testDriveVehicle and DoesEntityExist(testDriveVehicle) then
        DeleteEntity(testDriveVehicle)
        testDriveVehicle = nil
    end

    local returnPos = Config.DealerLocation
    SetEntityCoords(PlayerPedId(), returnPos.x, returnPos.y, returnPos.z)
    QBCore.Functions.Notify('Test drive ended', 'info')
end

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

التمويل وخطط الدفع

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

تتبع مبيعات الموظفين والعمولات

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

التحسين وتدابير مكافحة الاستغلال

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

جاهز للبدء؟

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