>
Tutorial 2026-04-11

Desenvolvimento de Lojas de Armas e Mercado Negro em FiveM

OntelMonke

OntelMonke

Admin & Developer na Agency Scripts

Visão geral da arquitetura da loja de armas

Um sistema de loja de armas em um servidor de RPG FiveM precisa equilibrar a acessibilidade para cidadãos cumpridores da lei com restrições realistas que criem profundidade de jogo. A arquitetura se divide em dois ramos distintos: lojas legais que impõem licenciamento de armas, verificações de antecedentes e cooldowns de compra, e negociantes ilegais do mercado negro que vendem armas não rastreáveis ​​a preços mais altos e sem documentação. Ambos os sistemas compartilham o mesmo inventário subjacente e lógica de transação, mas diferem em seus requisitos de acesso e nos metadados anexados a cada arma. As armas legais têm números de série vinculados à identificação de cidadão do comprador, tornando-as rastreáveis ​​pela polícia, enquanto as armas do mercado negro têm números de série riscados que não podem ser rastreados até um jogador específico. Este design de pista dupla cria uma tensão natural no roleplay porque os jogadores devem pesar a conveniência e o custo mais baixo das compras legais em relação ao anonimato do mercado negro.

Esquema de banco de dados para armas e licenças

Seu banco de dados precisa rastrear o inventário de armas por loja, números de série de armas individuais, licenças de jogadores e histórico de compras. O sistema de números de série é fundamental porque une a loja de armas e os sistemas de investigação policial. Cada arma comprada legalmente recebe uma série exclusiva que a polícia pode consultar durante paradas de trânsito ou investigações da cena do crime. Projete o esquema para lidar com o gerenciamento de estoque da loja e o rastreamento de armas individuais:

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

A tabela weapon_serials rastreia cada arma individualmente durante seu ciclo de vida. Quando um jogador compra uma arma legalmente, uma série é gerada e vinculada ao seu ID de cidadão. Se eles riscarem a série usando um serviço do mercado negro, o sinalizador is_scratched muda para verdadeiro e owner_citizenid é anulado. O campo stock em weapon_inventory usa -1 para indicar estoque ilimitado, enquanto valores positivos permitem mecânica de oferta limitada que cria escassez e impulsiona a demanda no mercado negro.

Implementação de loja de armas legais

Lojas de armas legais são a principal forma pela qual a maioria dos jogadores adquirem armas de fogo em um servidor de RPG. Eles impõem requisitos de licenciamento, aplicam intervalos de compra para evitar compras em massa e geram números de série rastreáveis ​​para cada arma vendida. A interface da loja deve exibir armas agrupadas por categoria com indicadores claros mostrando qual nível de licença cada arma exige. Jogadores sem a licença adequada veem a arma, mas não podem comprá-la, incentivando-os a interpretar o processo de licenciamento com autoridades governamentais. Implemente um sistema de resfriamento que evite que os jogadores comprem mais do que um determinado número de armas por dia em tempo real para evitar o armazenamento para revenda no mercado negro:

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

Sistema de munição e acessórios

O gerenciamento de munição adiciona um dreno de recursos que mantém os jogadores envolvidos com a economia e evita explorações infinitas de munição. Em vez de vender munição como um item genérico, vincule-a a tipos de calibres específicos que correspondam às categorias de armas. Uma pistola usa cartuchos de 9 mm, uma SMG pode compartilhar 9 mm ou usar .45 ACP e rifles usam 5,56 ou 7,62 dependendo do modelo. Este sistema de calibre cria uma experiência mais realista e dá aos lojistas flexibilidade na definição de preços de diferentes tipos de munição. A munição armazenada conta com os metadados da arma, e não como itens de inventário separados, para que cada arma rastreie suas próprias munições carregadas. Os anexos seguem o mesmo padrão de metadados, onde cada item de arma armazena uma lista de componentes instalados, como miras telescópicas, supressores, carregadores estendidos e lanternas:

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

Quando um jogador compra um anexo, valide se sua arma atual está na lista compatible, verifique seu nível de licença e adicione o hash do componente à tabela de metadados da arma. Ao equipar a arma, percorra os componentes armazenados e aplique-os com GiveWeaponComponentToPed. Essa abordagem garante que os anexos persistam entre as sessões e não possam ser duplicados pela manipulação do cliente.

Mecânica do Mercado Negro

O mercado negro é onde reside a verdadeira profundidade do RPG. Ao contrário das lojas jurídicas estáticas, os negociantes do mercado negro devem sentir-se perigosos e exclusivos. Implemente locais rotativos de revendedores que mudam a cada poucas horas para que os jogadores precisem de conhecimento interno ou contatos para encontrá-los. Exigir um sistema de reputação onde os novos jogadores devam construir a confiança do revendedor por meio de compras menores antes de obter acesso a armas de alto nível. O mercado negro vende armas sem números de série, equipamentos restritos de nível militar que as lojas legais não podem estocar e serviços como a raspagem de números de série que tornam as armas compradas legalmente indetectáveis. Fixe o preço de tudo 2 a 3 vezes mais alto do que as lojas legais para reflectir o prémio de risco e criar uma tensão económica genuína entre os caminhos legais e ilegais:

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)

Sistema de licenciamento de armas

O sistema de licenciamento cria uma importante camada burocrática de roleplay que conecta lojas de armas ao governo e às facções policiais. Os jogadores devem solicitar uma licença de porte de arma na prefeitura ou em uma delegacia de polícia, passar por uma verificação de antecedentes que examina sua ficha criminal e, potencialmente, participar de um teste de campo de tiro administrado por um policial. Implementar três níveis de licença: básica para revólveres e espingardas, avançada para submetralhadoras e rifles, e militar para armas pesadas que raramente são emitidas para civis. As licenças devem ter uma data de validade, normalmente 30 dias em tempo real, exigindo renovação. Os policiais precisam de um comando para revogar licenças quando os jogadores são condenados por crimes violentos, o que os impede imediatamente de comprar em lojas legais e cria demanda para o mercado negro. Armazene o status da licença no banco de dados e verifique-o no servidor em cada tentativa de compra; nunca confie nas verificações de licença do cliente, pois elas podem ser contornadas trivialmente.

Interface da loja do lado do cliente

A loja de armas NUI deve parecer uma vitrine profissional com armas exibidas em guias categorizadas. Mostre cada arma com seu nome, preço, licença necessária, nível de estoque atual e uma pequena imagem ou ícone de visualização. Inclua uma seção separada para munição onde os jogadores selecionam sua arma primeiro e depois veem apenas os tipos de munição compatíveis. A seção de anexos funciona de forma semelhante, mostrando apenas componentes compatíveis com a arma atualmente equipada do jogador. Implemente um sistema de carrinho de compras que permita aos jogadores colocar vários itens na fila e finalizar a compra em uma única transação, em vez de comprar cada item individualmente. Para a interface do mercado negro, use uma estética mais sombria com uma barra de progresso de reputação mostrando o quão perto o jogador está do próximo nível. Exiba itens bloqueados como silhuetas com um rótulo de exigência de reputação para que os jogadores saibam no que estão trabalhando. Ambos os tipos de loja devem acionar um retorno de chamada do servidor ao abrir para buscar preços e estoque em tempo real, em vez de depender de valores de configuração em cache, porque os administradores podem ajustar os preços dinamicamente com base nas condições de economia do servidor.

Antiexploração e Integração Policial

As lojas de armas são o principal alvo dos exploradores que tentam duplicar armas caras ou gerar itens sem pagar. Valide cada compra no servidor verificando o dinheiro do jogador, o status da licença, a proximidade da loja e o tempo de espera da compra antes de criar qualquer item. Registre todas as transações de armas com os identificadores do comprador, o número de série da arma, o ID da loja e o carimbo de data e hora para que os administradores possam rastrear a origem de qualquer arma no servidor. Integre-se ao sistema MDT da polícia para que os policiais possam procurar a série da arma durante as investigações e ver o proprietário registrado, a data da compra e se a série foi arranhada. Implementar uma exportação de registro de armas que gere um relatório de todas as armas registradas para um cidadão específico, útil para verificações de liberdade condicional. Eventos de compra com limite de taxa para evitar solicitações rápidas que poderiam ignorar as verificações de resfriamento por meio de condições de corrida. Envie alertas de webhook do Discord quando armas militares de alto nível forem vendidas no mercado negro para que sua equipe de moderação possa monitorar abusos. A combinação de validação adequada do lado do servidor, registro abrangente e integração policial cria uma economia de armas que parece realista, ao mesmo tempo que permanece resistente à exploração.

Partilhar este artigo

Pronto para melhorar o teu servidor?

Explora os nossos scripts FiveM premium na loja Agency Scripts ou junta-te à nossa comunidade no Discord para suporte e atualizações.