Sistema de armería para FiveM: scripts para tiendas de armas con licencia
Crea una armería con licencia en tu servidor FiveM. Permisos, verificaciones de antecedentes, munición, accesorios y los mejores scripts para roleplay.
Agency Scripts
Fundador y desarrollador principal en Agency Scripts
Descripción general de la arquitectura de una tienda de armas
Un sistema de tiendas de armas en un servidor de roleplay de FiveM debe equilibrar la accesibilidad para los ciudadanos respetuosos de la ley con restricciones realistas que aporten profundidad al juego. La arquitectura se divide en dos ramas distintas: las tiendas legales, que exigen licencias de armas, verificaciones de antecedentes y tiempos de espera entre compras, y los vendedores ilegales del mercado negro, que venden armas imposibles de rastrear a precios más altos y sin papeleo. Ambos sistemas comparten la misma lógica subyacente de inventario y transacciones, pero difieren en sus requisitos de acceso y en los metadatos asociados a cada arma. Las armas legales reciben números de serie vinculados al citizen ID del comprador, lo que permite que la policía las rastree, mientras que las armas del mercado negro tienen números de serie limados que no pueden rastrearse hasta un jugador específico. Este diseño de dos vías genera tensión natural en el roleplay, porque los jugadores deben sopesar la comodidad y el menor coste de las compras legales frente al anonimato del mercado negro.
Esquema de base de datos para armas y licencias
Tu base de datos debe registrar el inventario de armas de cada tienda, los números de serie individuales de las armas, las licencias de los jugadores y el historial de compras. El sistema de números de serie es fundamental porque conecta las tiendas de armas con los sistemas de investigación policial. Cada arma comprada legalmente recibe un número de serie único que la policía puede consultar durante controles de tráfico o investigaciones de escenas del crimen. Diseña el esquema para gestionar tanto el inventario de las tiendas como el seguimiento de armas individuales:
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)
);
El weapon_serials la tabla registra cada arma individualmente a lo largo de su ciclo de vida. Cuando un jugador compra un arma legalmente, se genera un número de serie y se vincula a su citizen ID. Si lima el número de serie mediante un servicio del mercado negro, el is_scratched indicador cambia a true y el owner_citizenid se anula. El stock campo en weapon_inventory usa -1 para indicar inventario ilimitado, mientras que los valores positivos habilitan mecánicas de suministro limitado que generan escasez e impulsan la demanda del mercado negro.
Implementación de una tienda legal de armas
Las tiendas legales de armas son la principal forma en que la mayoría de los jugadores consiguen armas de fuego en un servidor de roleplay. Exigen licencias, aplican tiempos de espera entre compras para evitar las compras al por mayor y generan números de serie rastreables para cada arma vendida. La interfaz de la tienda debe mostrar las armas agrupadas por categoría, con indicadores claros del nivel de licencia que requiere cada una. Los jugadores sin la licencia adecuada pueden ver el arma, pero no comprarla, lo que los anima a interpretar el proceso de obtención de licencias con funcionarios del gobierno. Implementa un sistema de tiempo de espera que impida a los jugadores comprar más de una cantidad determinada de armas por día en tiempo real, para evitar que acumulen existencias con el fin de revenderlas en el 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 munición y accesorios
La gestión de la munición añade un consumo de recursos que mantiene a los jugadores involucrados en la economía y evita exploits de munición infinita. En lugar de vender munición como un objeto genérico, vincúlala a tipos de calibre específicos que correspondan a las categorías de armas. Una pistola usa cartuchos de 9mm, un SMG puede usar los mismos de 9mm o .45 ACP, y los rifles usan 5.56 o 7.62, según el modelo. Este sistema de calibres crea una experiencia más realista y da flexibilidad a los dueños de las tiendas para fijar precios diferentes según el tipo de munición. Guarda la cantidad de munición en los metadatos del arma en lugar de usar objetos de inventario separados, para que cada arma lleve el registro de sus propios cartuchos cargados. Los accesorios siguen el mismo patrón de metadatos, donde cada objeto de arma guarda una lista de componentes instalados, como miras, silenciadores, cargadores extendidos y linternas:
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',
},
}
Cuando un jugador compra un accesorio, valida que su arma actual esté en la lista de compatible comprueba su nivel de licencia y luego añade el hash del componente a la tabla de metadatos del arma. Al equipar el arma, recorre los componentes guardados y aplícalos con GiveWeaponComponentToPed. Este enfoque garantiza que los accesorios persistan entre sesiones y no puedan duplicarse manipulando el cliente.
Mecánicas del mercado negro
El mercado negro es donde realmente cobra vida la profundidad del roleplay. A diferencia de las tiendas legales estáticas, los vendedores del mercado negro deben parecer peligrosos y exclusivos. Implementa ubicaciones rotativas para los vendedores, que cambien cada pocas horas, para que los jugadores necesiten información privilegiada o contactos para encontrarlos. Exige un sistema de reputación en el que los jugadores nuevos deban ganarse la confianza del vendedor con compras pequeñas antes de acceder a armas de nivel superior. El mercado negro vende armas sin números de serie, equipo militar restringido que las tiendas legales no pueden ofrecer y servicios como limar números de serie para que las armas compradas legalmente no puedan rastrearse. Fija precios 2-3x más altos que los de las tiendas legales para reflejar el riesgo añadido y generar una verdadera tensión económica entre las vías legales e ilegales:
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 licencias de armas
El sistema de licencias crea una importante capa de roleplay burocrático que conecta las tiendas de armas con las facciones gubernamentales y policiales. Los jugadores deben solicitar una licencia de armas en el ayuntamiento o en una comisaría, pasar una verificación de antecedentes que examine sus antecedentes penales y, posiblemente, realizar una prueba en un campo de tiro administrada por un agente. Implementa tres niveles de licencia: básica para pistolas y escopetas, avanzada para SMG y rifles, y militar para armas pesadas que rara vez se entregan a civiles. Las licencias deben tener fecha de vencimiento, normalmente 30 días en tiempo real, y requerir renovación. Los agentes de policía necesitan un comando para revocar las licencias cuando los jugadores sean condenados por delitos violentos. Esto les impedirá de inmediato comprar en tiendas legales y generará demanda en el mercado negro. Guarda el estado de las licencias en la base de datos y compruébalo en el servidor cada vez que alguien intente comprar. Nunca confíes en las comprobaciones de licencias del cliente, porque pueden saltarse fácilmente.
Interfaz de la tienda en el cliente
La NUI de la tienda de armas debe parecer un escaparate profesional, con las armas organizadas en pestañas por categoría. Muestra cada arma con su nombre, precio, licencia necesaria, existencias actuales y una pequeña imagen o icono de vista previa. Incluye una sección aparte para la munición, donde los jugadores seleccionen primero su arma y luego vean solo los tipos de munición compatibles. La sección de accesorios funciona de forma similar y muestra solo los componentes compatibles con el arma que lleva equipada el jugador. Implementa un carrito que permita a los jugadores añadir varios artículos y pagar en una sola transacción, en lugar de comprar cada artículo por separado. Para la interfaz del mercado negro, usa una estética más oscura y una barra de progreso de reputación que muestre cuánto le falta al jugador para llegar al siguiente nivel. Muestra los artículos bloqueados como siluetas con una etiqueta que indique la reputación necesaria, para que los jugadores sepan qué objetivo tienen. Ambos tipos de tienda deben activar una llamada de servidor al abrirse para obtener los precios y las existencias en tiempo real, en lugar de usar valores de configuración almacenados en caché, ya que los administradores pueden ajustar los precios dinámicamente según las condiciones de la economía del servidor.
Integración policial y medidas contra exploits
Las tiendas de armas son un objetivo frecuente para quienes intentan duplicar armas caras o generar objetos sin pagar. Valida cada compra en el servidor comprobando el dinero del jugador, el estado de su licencia, su proximidad a la tienda y el tiempo de espera entre compras antes de crear cualquier objeto. Registra cada transacción de armas con los identificadores del comprador, el número de serie del arma, el ID de la tienda y la marca de tiempo, para que los administradores puedan rastrear el origen de cualquier arma del servidor. Integra el sistema con el MDT policial para que los agentes puedan consultar un número de serie durante una investigación y ver el propietario registrado, la fecha de compra y si el número de serie ha sido limado. Implementa un export del registro de armas que genere un informe de todas las armas registradas a nombre de un ciudadano específico, útil para controles de libertad condicional. Limita la frecuencia de los eventos de compra para evitar solicitudes rápidas que puedan saltarse los controles de tiempo de espera mediante condiciones de carrera. Envía alertas mediante webhook de Discord cuando se vendan armas militares de nivel superior en el mercado negro, para que el equipo de moderación pueda vigilar posibles abusos. La combinación de validaciones adecuadas en el servidor, registros exhaustivos e integración policial crea una economía de armas realista y resistente a los exploits.