Назад к блогу
Tutorial9 мин чтения

Система оружейного магазина FiveM: Скрипты лицензированных оружейных магазинов

Создайте лицензированный оружейный магазин для вашего сервера FiveM. Разрешения, проверка биографии, боеприпасы, аксессуары и лучшие скрипты оружейных магазинов для ролевых серверов.

Agency Scripts

Основатель и ведущий разработчик Agency Scripts

Обзор архитектуры оружейных магазинов

Система оружейного магазина на FiveM ролевом сервере должна балансировать доступность для законопослушных граждан с реалистичными ограничениями, создающими глубину геймплея. Архитектура делится на два направления: легальные магазины, которые требуют лицензии, проверки и перерывы между покупками, и нелегальных торговцев на чёрном рынке, продающих неотслеживаемое оружие по более высоким ценам без документов. Обе системы используют одну и ту же логику инвентаря и транзакций, но отличаются требованиями доступа и метаданными оружия. Легальное оружие имеет серийные номера, привязанные к ID гражданина покупателя, что позволяет полиции отслеживать, а чёрный рынок , царапанные номера, не позволяющие отследить владельца. Такая двойная система создаёт естественное 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 таблица отслеживает каждое оружие индивидуально на протяжении всего жизненного цикла. Когда игрок покупает оружие легально, генерируется серийный номер, связанный с его ID гражданина. Если он стирает серийник через черный рынок, is_scratched флаг переключается в true и owner_citizenid аннулируется. The 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

Система боеприпасов и аксессуаров

Управление боеприпасами добавляет расход ресурсов, что удерживает игроков вовлечёнными в экономику и предотвращает бесконечные эксплойты с патронами. Вместо продажи патронов как универсального предмета, привязывайте их к конкретным калибрам, соответствующим категориям оружия. Пистолет использует 9мм патроны, ПП может использовать 9мм или .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 оружейного магазина должен выглядеть как профессиональный магазин с оружием, отображаемым в категоризированных вкладках. Показывайте каждое оружие с названием, ценой, требуемой лицензией, текущим уровнем запасов и небольшим превью или иконкой. Включите отдельный раздел для боеприпасов, где игроки сначала выбирают оружие, а затем видят только совместимые типы патронов. Раздел с аксессуарами работает аналогично, показывая только компоненты, совместимые с текущим оружием игрока. Реализуйте систему корзины покупок, позволяющую игрокам добавлять несколько предметов и оплачивать их одной транзакцией, а не покупать по одному. Для интерфейса черного рынка используйте более темную эстетику с индикатором прогресса репутации, показывающим, насколько игрок близок к следующему уровню. Заблокированные предметы отображайте в виде силуэтов с меткой требования репутации, чтобы игроки знали, к чему стремиться. Оба типа магазинов должны вызывать серверный callback при открытии для получения актуальных цен и запасов, а не полагаться на кэшированные значения конфигурации, так как администраторы могут динамически корректировать цены в зависимости от состояния экономики сервера.

Анти-эксплойт и интеграция с полицией

Оружейные магазины являются основной целью злоумышленников, пытающихся дублировать дорогостоящее оружие или создавать предметы без оплаты. Проверяйте каждую покупку на стороне сервера, проверяя деньги игрока, статус лицензии, близость к магазину и время ожидания между покупками перед созданием предметов. Логируйте каждую транзакцию оружия с идентификаторами покупателя, серийным номером оружия, ID магазина и временной меткой, чтобы администраторы могли отследить происхождение любого оружия на сервере. Интегрируйте с полицейской системой MDT, чтобы офицеры могли искать серийный номер оружия во время расследований и видеть зарегистрированного владельца, дату покупки и информацию о том, был ли номер повреждён. Реализуйте экспорт реестра оружия, который генерирует отчёт обо всех зарегистрированных оружиях конкретного гражданина, полезный для проверок условно осуждённых. Ограничьте частоту событий покупки, чтобы предотвратить быстрые запросы, которые могут обойти проверки времени ожидания через состояния гонки. Отправляйте оповещения в Discord через webhook при продаже высококлассного военного оружия на чёрном рынке, чтобы ваша команда модерации могла контролировать злоупотребления. Сочетание правильной серверной валидации, комплексного логирования и интеграции с полицией создаёт оружейную экономику, которая ощущается реалистичной и устойчива к эксплуатации.

Готовы начать?

Возьмите скрипты в нашем магазине или заходите в Discord за поддержкой, обновлениями и анонсами.