>
Tutoriel 2026-04-11

Système d'armurerie pour FiveM

OntelMonke

OntelMonke

Administrateur et développeur chez Agency Scripts

Présentation de l'architecture du magasin d'armes

Un système de magasin d'armes sur un serveur de jeu de rôle FiveM doit équilibrer l'accessibilité pour les citoyens respectueux des lois avec des restrictions réalistes qui créent de la profondeur de jeu. L’architecture se divise en deux branches distinctes : les magasins légaux qui appliquent les licences d’armes, la vérification des antécédents et les délais d’achat, et les revendeurs illégaux du marché noir qui vendent des armes intraçables à des prix plus élevés sans paperasse. Les deux systèmes partagent la même logique sous-jacente d’inventaire et de transaction, mais diffèrent par leurs exigences d’accès et les métadonnées attachées à chaque arme. Les armes légales reçoivent des numéros de série liés à la carte d'identité de l'acheteur, ce qui les rend traçables par la police, tandis que les armes du marché noir ont des numéros de série rayés qui ne permettent pas de remonter à un joueur spécifique. Cette conception à double voie crée une tension naturelle dans le jeu de rôle, car les joueurs doivent peser la commodité et le moindre coût des achats légaux par rapport à l'anonymat du marché noir.

Schéma de base de données pour les armes et les licences

Ton base de données doit suivre l'inventaire des armes par magasin, les numéros de série des armes individuelles, les licences des joueurs et l'historique des achats. Le système de numéro de série est essentiel car il fait le lien entre le magasin d'armes et les systèmes d'enquête de la police. Chaque arme achetée légalement reçoit une série unique que la police peut consulter lors des contrôles routiers ou des enquêtes sur les lieux du crime. Concevez le schéma pour gérer à la fois la gestion des stocks du magasin et le suivi des armes individuelles :

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)
);

Le weapon_serials Le tableau suit chaque arme individuellement tout au long de son cycle de vie. Lorsqu'un joueur achète une arme légalement, un numéro de série est généré et lié à sa carte d'identité de citoyen. S'ils effacent la série en utilisant un service du marché noir, le is_scratched le drapeau devient vrai et le owner_citizenid est annulée. Le stock champ dans weapon_inventory utilise -1 pour indiquer un stock illimité, tandis que des valeurs positives permettent des mécanismes d'approvisionnement limités qui créent une pénurie et stimulent la demande du marché noir.

Implémentation d'un magasin d'armes légal

Les magasins d'armes légaux sont le principal moyen par lequel la plupart des joueurs acquièrent des armes à feu sur un serveur de jeu de rôle. Ils appliquent les exigences en matière de licences, appliquent des délais d'achat pour empêcher les achats groupés et génèrent des numéros de série traçables pour chaque arme vendue. L'interface utilisateur de la boutique doit afficher les armes regroupées par catégorie avec des indicateurs clairs indiquant le niveau de licence requis par chaque arme. Les joueurs sans licence appropriée voient l'arme mais ne peuvent pas l'acheter, ce qui les encourage à jouer un rôle dans le processus d'attribution de licence avec des représentants du gouvernement. Implémentez un système de temps de recharge qui empêche les joueurs d'acheter plus d'un nombre défini d'armes par jour en temps réel afin d'éviter de les stocker pour les revendre sur le marché noir :

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

Munitions et système de fixation

La gestion des munitions ajoute une fuite de ressources qui maintient les joueurs engagés dans l'économie et empêche une utilisation infinie des munitions. Au lieu de vendre des munitions en tant qu'article générique, associez-les à des types de calibres spécifiques qui correspondent aux catégories d'armes. Un pistolet utilise des cartouches de 9 mm, un SMG peut partager 9 mm ou utiliser 0,45 ACP, et les fusils utilisent 5,56 ou 7,62 selon le modèle. Ce système de calibre crée une expérience plus réaliste et donne aux propriétaires de magasins une flexibilité dans la tarification de différents types de munitions. Les munitions stockées comptent sur les métadonnées de l'arme plutôt que sur des éléments d'inventaire distincts afin que chaque arme suive ses propres cartouches chargées. Les pièces jointes suivent le même modèle de métadonnées où chaque élément d'arme stocke une liste de composants installés tels que des lunettes, des suppresseurs, des chargeurs étendus et des lampes de poche :

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',
    },
}

Lorsqu'un joueur achète une pièce jointe, vérifiez que son arme actuelle est dans le compatible liste, vérifiez leur niveau de licence, puis ajoutez le hachage du composant à la table de métadonnées de l'arme. Sur l'équipement d'arme, parcourez les composants stockés et appliquez-les avec GiveWeaponComponentToPed. Cette approche garantit que les pièces jointes persistent d'une session à l'autre et ne peuvent pas être dupliquées en manipulant le client.

Mécanismes du marché noir

Le marché noir est le lieu où réside la véritable profondeur du jeu de rôle. Contrairement aux magasins légaux statiques, les revendeurs du marché noir devraient se sentir dangereux et exclusifs. Mettez en place des emplacements de revendeurs rotatifs qui changent toutes les quelques heures afin que les joueurs aient besoin de connaissances privilégiées ou de contacts pour les trouver. Exiger un système de réputation dans lequel les nouveaux joueurs doivent établir la confiance avec le revendeur via des achats plus petits avant d'avoir accès à des armes de haut niveau. Le marché noir vend des armes sans numéro de série, des équipements de qualité militaire restreints que les magasins légaux ne peuvent pas stocker, et des services tels que le grattage des numéros de série qui rendent introuvables les armes achetées légalement. Prix ​​tout 2 à 3 fois plus élevé que celui des magasins légaux pour refléter la prime de risque et créer une véritable tension économique entre les voies légales et illégales :

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)

Système de licences d'armes

Le système de licences crée une couche de jeu de rôle bureaucratique importante qui relie les magasins d’armes aux factions gouvernementales et chargées de l’application de la loi. Les joueurs doivent demander un permis d'armes à la mairie ou à un poste de police, se soumettre à une vérification de leurs antécédents qui examine leur casier judiciaire et éventuellement assister à un test de tir administré par un officier. Mettez en œuvre trois niveaux de licence : de base pour les armes de poing et les fusils de chasse, avancé pour les SMG et les fusils, et militaire pour les armes lourdes rarement délivrées aux civils. Les licences doivent avoir une date d'expiration, généralement 30 jours en temps réel, nécessitant un renouvellement. Les policiers ont besoin d'un ordre pour révoquer les licences lorsque les joueurs sont reconnus coupables de crimes violents, ce qui les empêche immédiatement d'acheter dans des magasins légaux et crée une demande pour le marché noir. Stockez l'état de la licence dans la base de données et vérifiez-le côté serveur à chaque tentative d'achat. Ne faites jamais confiance aux vérifications de licence côté client, car elles peuvent être contournées de manière triviale.

Interface de boutique côté client

Le magasin d'armes NUI devrait ressembler à une vitrine professionnelle avec des armes affichées dans des onglets catégorisés. Affichez chaque arme avec son nom, son prix, la licence requise, le niveau de stock actuel et une petite image ou icône d'aperçu. Incluez une section distincte pour les munitions dans laquelle les joueurs sélectionnent d'abord leur arme, puis voient uniquement les types de munitions compatibles. La section des pièces jointes fonctionne de la même manière, affichant uniquement les composants compatibles avec l'arme actuellement équipée du joueur. Implémentez un système de panier d'achat qui permet aux joueurs de mettre plusieurs articles en file d'attente et de payer en une seule transaction plutôt que d'acheter chaque article individuellement. Pour l'interface utilisateur du marché noir, utilisez une esthétique plus sombre avec une barre de progression de réputation indiquant à quel point le joueur est proche du niveau suivant. Affichez les objets verrouillés sous forme de silhouettes avec une étiquette d'exigence de réputation afin que les joueurs sachent vers quoi ils travaillent. Les deux types de magasins doivent déclencher un rappel du serveur à l'ouverture pour récupérer les prix et les stocks en temps réel plutôt que de s'appuyer sur les valeurs de configuration mises en cache, car les administrateurs peuvent ajuster les prix de manière dynamique en fonction des conditions économiques du serveur.

Anti-Exploit et intégration policière

Les magasins d'armes sont une cible privilégiée pour les exploiteurs qui tentent de dupliquer des armes coûteuses ou de générer des objets sans payer. Validez chaque achat côté serveur en vérifiant l'argent du joueur, le statut de la licence, la proximité du magasin et le temps de recharge de l'achat avant de créer des objets. Enregistrez chaque transaction d'arme avec les identifiants de l'acheteur, le numéro de série de l'arme, l'identifiant du magasin et l'horodatage afin que les administrateurs puissent retracer l'origine de toute arme sur le serveur. Intégrez-le au système de police MDT afin que les agents puissent rechercher un numéro de série d'arme pendant les enquêtes et voir le propriétaire enregistré, la date d'achat et si le numéro de série a été rayé. Implémentez une exportation de registre d'armes qui génère un rapport de toutes les armes enregistrées pour un citoyen spécifique, utile pour les contrôles de probation. Événements d’achat à limite de débit pour empêcher les demandes rapides qui pourraient contourner les contrôles de temps de recharge en raison de conditions de concurrence. Envoyez des alertes webhook Discord lorsque des armes militaires de haut niveau sont vendues sur le marché noir afin que ton équipe de modération puisse surveiller les abus. La combinaison d’une validation appropriée côté serveur, d’une journalisation complète et de l’intégration de la police crée une économie d’armes qui semble réaliste tout en restant résistante à l’exploitation.

Partager cet article

Prêt à améliorer votre serveur ?

Découvrez nos scripts FiveM premium dans la boutique Agency Scripts ou rejoignez notre communauté Discord pour le support et les mises à jour.