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

نظام متجر الأسلحة FiveM: سكريبتات متجر الأسلحة المرخصة

ابنِ متجر أسلحة مرخص لسيرفرك في FiveM. تصاريح، فحوصات خلفية، ذخيرة، ملحقات وأفضل سكريبتات متاجر الأسلحة للعب التمثيلي.

Agency Scripts

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

نظرة عامة على هيكل متجر الأسلحة

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

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

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

CREATE TABLE IF NOT EXISTS weapon_shops (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(100) NOT NULL,
    type ENUM('legal', 'blackmarket') DEFAULT 'legal',
    coords_x FLOAT NOT NULL,
    coords_y FLOAT NOT NULL,
    coords_z FLOAT NOT NULL,
    is_active BOOLEAN DEFAULT TRUE
);

CREATE TABLE IF NOT EXISTS weapon_inventory (
    id INT AUTO_INCREMENT PRIMARY KEY,
    shop_id INT NOT NULL,
    weapon_name VARCHAR(50) NOT NULL,
    label VARCHAR(100) NOT NULL,
    price INT NOT NULL,
    ammo_price INT DEFAULT 0,
    category ENUM('handguns', 'smgs', 'rifles', 'shotguns', 'melee', 'throwables') NOT NULL,
    license_required ENUM('none', 'basic', 'advanced', 'military') DEFAULT 'none',
    stock INT DEFAULT -1,
    INDEX idx_shop (shop_id),
    FOREIGN KEY (shop_id) REFERENCES weapon_shops(id)
);

CREATE TABLE IF NOT EXISTS weapon_serials (
    serial VARCHAR(20) PRIMARY KEY,
    weapon_name VARCHAR(50) NOT NULL,
    owner_citizenid VARCHAR(50) DEFAULT NULL,
    is_scratched BOOLEAN DEFAULT FALSE,
    purchased_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    shop_id INT DEFAULT NULL,
    INDEX idx_owner (owner_citizenid)
);

CREATE TABLE IF NOT EXISTS weapon_licenses (
    id INT AUTO_INCREMENT PRIMARY KEY,
    citizenid VARCHAR(50) NOT NULL,
    license_type ENUM('basic', 'advanced', 'military') NOT NULL,
    issued_by VARCHAR(50) DEFAULT NULL,
    issued_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    expires_at TIMESTAMP NULL,
    revoked BOOLEAN DEFAULT FALSE,
    UNIQUE KEY unique_license (citizenid, license_type)
);

ال weapon_serials جدول يتتبع كل سلاح بشكل فردي خلال دورة حياته. عندما يشتري اللاعب سلاحاً بشكل قانوني، يتم إنشاء رقم تسلسلي مرتبط بمعرف المواطن الخاص به. إذا قام بخدش الرقم التسلسلي باستخدام خدمة السوق السوداء، is_scratched يتحول العلم إلى true و owner_citizenid يتم إلغاؤه. ال stock الحقل في weapon_inventory يستخدم -1 للدلالة على مخزون غير محدود، بينما القيم الإيجابية تُمكّن آليات العرض المحدود التي تخلق ندرة وتدفع الطلب في السوق السوداء.

تنفيذ متجر الأسلحة القانونية

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

RegisterNetEvent('weaponshop:server:purchase', function(shopId, weaponName)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local citizenid = Player.PlayerData.citizenid
    local item = GetShopItem(shopId, weaponName)

    if not item then
        TriggerClientEvent('QBCore:Notify', src, 'Item not available', 'error')
        return
    end

    -- Check license requirement
    if item.license_required ~= 'none' then
        local hasLicense = HasValidLicense(citizenid, item.license_required)
        if not hasLicense then
            TriggerClientEvent('QBCore:Notify', src, 'You need a ' .. item.license_required .. ' weapons license', 'error')
            return
        end
    end

    -- Check purchase cooldown
    local recentPurchases = GetRecentPurchaseCount(citizenid, 86400) -- last 24h
    if recentPurchases >= Config.DailyPurchaseLimit then
        TriggerClientEvent('QBCore:Notify', src, 'Daily purchase limit reached', 'error')
        return
    end

    -- Check stock
    if item.stock ~= -1 then
        if item.stock <= 0 then
            TriggerClientEvent('QBCore:Notify', src, 'Out of stock', 'error')
            return
        end
    end

    -- Check funds
    if Player.PlayerData.money.bank < item.price then
        TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
        return
    end

    -- Generate serial number
    local serial = GenerateWeaponSerial()

    -- Process purchase
    Player.Functions.RemoveMoney('bank', item.price, 'weapon-purchase')
    Player.Functions.AddItem(weaponName, 1, false, { serial = serial })

    -- Record serial
    MySQL.insert('INSERT INTO weapon_serials (serial, weapon_name, owner_citizenid, shop_id) VALUES (?, ?, ?, ?)',
        { serial, weaponName, citizenid, shopId })

    -- Update stock
    if item.stock ~= -1 then
        MySQL.update('UPDATE weapon_inventory SET stock = stock - 1 WHERE shop_id = ? AND weapon_name = ?',
            { shopId, weaponName })
    end

    TriggerClientEvent('QBCore:Notify', src, 'Purchased ' .. item.label .. ' (S/N: ' .. serial .. ')', 'success')
end)

function GenerateWeaponSerial()
    local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
    local serial = ''
    for i = 1, 3 do
        serial = serial .. chars:sub(math.random(#chars), math.random(#chars))
    end
    serial = serial .. '-'
    for i = 1, 5 do
        serial = serial .. tostring(math.random(0, 9))
    end
    return serial
end

نظام الذخيرة والملحقات

إدارة الذخيرة تضيف استنزافًا للموارد يبقي اللاعبين منخرطين في الاقتصاد ويمنع استغلال الذخيرة اللانهائية. بدلاً من بيع الذخيرة كعنصر عام، اربطها بأنواع عيارات محددة تتناسب مع فئات الأسلحة. تستخدم المسدسات ذخيرة 9mm، قد تشترك SMG في 9mm أو تستخدم .45 ACP، وتستخدم البنادق 5.56 أو 7.62 حسب الطراز. يخلق هذا النظام العياري تجربة أكثر واقعية ويمنح أصحاب المتاجر مرونة في تسعير أنواع الذخيرة المختلفة. خزّن عدد الذخيرة في بيانات تعريف السلاح بدلاً من كونه عناصر منفصلة في المخزون بحيث يتتبع كل سلاح طلقاته المحملة الخاصة. تتبع الملحقات نفس نمط بيانات التعريف حيث يخزن كل عنصر سلاح قائمة بالمكونات المثبتة مثل المناظير، كاتمات الصوت، المجلات الممتدة، والمصابيح اليدوية:

Config.AmmoTypes = {
    ['ammo_9mm']     = { label = '9mm Rounds',    price = 5,  amount = 24, weapons = {'WEAPON_PISTOL', 'WEAPON_COMBATPISTOL', 'WEAPON_SMG'} },
    ['ammo_45acp']   = { label = '.45 ACP Rounds', price = 7,  amount = 24, weapons = {'WEAPON_APPISTOL', 'WEAPON_MACHINEPISTOL'} },
    ['ammo_556']     = { label = '5.56 Rounds',    price = 12, amount = 30, weapons = {'WEAPON_CARBINERIFLE', 'WEAPON_ASSAULTRIFLE'} },
    ['ammo_762']     = { label = '7.62 Rounds',    price = 15, amount = 30, weapons = {'WEAPON_SNIPERRIFLE', 'WEAPON_MARKSMANRIFLE'} },
    ['ammo_12gauge'] = { label = '12ga Shells',    price = 10, amount = 8,  weapons = {'WEAPON_PUMPSHOTGUN', 'WEAPON_ASSAULTSHOTGUN'} },
}

Config.Attachments = {
    ['attachment_suppressor'] = {
        label = 'Suppressor',
        price = 8500,
        component = 'COMPONENT_AT_PI_SUPP_02',
        compatible = {'WEAPON_PISTOL', 'WEAPON_COMBATPISTOL', 'WEAPON_SMG', 'WEAPON_CARBINERIFLE'},
        license_required = 'advanced',
    },
    ['attachment_scope'] = {
        label = 'Holographic Scope',
        price = 3200,
        component = 'COMPONENT_AT_SCOPE_MACRO_02',
        compatible = {'WEAPON_SMG', 'WEAPON_CARBINERIFLE', 'WEAPON_ASSAULTRIFLE'},
        license_required = 'basic',
    },
    ['attachment_extmag'] = {
        label = 'Extended Magazine',
        price = 4500,
        component = 'COMPONENT_CARBINERIFLE_CLIP_02',
        compatible = {'WEAPON_CARBINERIFLE', 'WEAPON_ASSAULTRIFLE', 'WEAPON_SMG'},
        license_required = 'basic',
    },
    ['attachment_flashlight'] = {
        label = 'Flashlight',
        price = 1200,
        component = 'COMPONENT_AT_AR_FLSH',
        compatible = {'WEAPON_PISTOL', 'WEAPON_COMBATPISTOL', 'WEAPON_CARBINERIFLE', 'WEAPON_PUMPSHOTGUN'},
        license_required = 'none',
    },
}

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

ميكانيكيون السوق السوداء

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

Config.BlackMarket = {
    rotationInterval = 10800, -- 3 hours between location changes
    locations = {
        { coords = vector3(89.98, -1810.87, 24.98), heading = 230.0, label = 'Underground Parking' },
        { coords = vector3(1394.27, 1141.48, 114.33), heading = 90.0, label = 'Desert Warehouse' },
        { coords = vector3(-58.21, 6443.31, 31.43), heading = 45.0, label = 'Paleto Docks' },
        { coords = vector3(981.45, -1812.66, 31.14), heading = 180.0, label = 'Industrial Zone' },
    },
    reputationTiers = {
        [0] = { label = 'Unknown', items = {'WEAPON_KNIFE', 'WEAPON_BAT'} },
        [1] = { label = 'Associate', items = {'WEAPON_PISTOL', 'WEAPON_MICROSMG', 'ammo_9mm'} },
        [2] = { label = 'Trusted', items = {'WEAPON_SMG', 'WEAPON_PUMPSHOTGUN', 'ammo_45acp', 'ammo_12gauge', 'attachment_suppressor'} },
        [3] = { label = 'Inner Circle', items = {'WEAPON_ASSAULTRIFLE', 'WEAPON_CARBINERIFLE', 'ammo_556', 'service_scratch_serial'} },
        [4] = { label = 'Arms Dealer', items = {'WEAPON_SNIPERRIFLE', 'WEAPON_RPG', 'WEAPON_GRENADELAUNCHER', 'ammo_762', 'armor_heavy'} },
    },
    priceMultiplier = 2.5,
    reputationGainPerPurchase = 0.15,
}

-- Serial scratching service
RegisterNetEvent('blackmarket:server:scratchSerial', function(weaponSlot)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    local citizenid = Player.PlayerData.citizenid
    local reputation = GetBlackMarketReputation(citizenid)

    if reputation < 3 then
        TriggerClientEvent('QBCore:Notify', src, 'You don\'t have enough reputation', 'error')
        return
    end

    local item = Player.Functions.GetItemBySlot(weaponSlot)
    if not item or not item.info or not item.info.serial then
        TriggerClientEvent('QBCore:Notify', src, 'No weapon with serial found', 'error')
        return
    end

    local cost = Config.ScratchSerialPrice -- e.g., 15000
    if Player.PlayerData.money.cash < cost then
        TriggerClientEvent('QBCore:Notify', src, 'Not enough cash', 'error')
        return
    end

    Player.Functions.RemoveMoney('cash', cost, 'serial-scratch')

    -- Update database
    MySQL.update('UPDATE weapon_serials SET is_scratched = TRUE, owner_citizenid = NULL WHERE serial = ?',
        { item.info.serial })

    -- Update item metadata
    item.info.serial = 'SCRATCHED'
    Player.Functions.SetInventoryItem(item.name, item.amount, item.info, weaponSlot)

    TriggerClientEvent('QBCore:Notify', src, 'Serial number removed', 'success')
end)

نظام ترخيص الأسلحة

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

واجهة المتجر على جانب العميل

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

مكافحة الاستغلال وتكامل الشرطة

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

جاهز للبدء؟

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