Script de máquinas expendedoras para FiveM: comida, bebidas y más
Añade máquinas expendedoras funcionales a tu servidor de FiveM. Ofrece aperitivos, bebidas y artículos personalizados, y colócalas con facilidad.
Agency Scripts
Fundador y desarrollador principal en Agency Scripts
Por qué las máquinas expendedoras aportan profundidad a tu servidor
Las máquinas expendedoras y los sistemas de tiendas automáticas cumplen una función esencial en cualquier economía de rol. Ofrecen acceso las 24 horas a artículos esenciales como comida, agua, cigarrillos y suministros básicos sin que tenga que estar conectado un dependiente. Además de ser prácticas, crean puntos de encuentro naturales donde los jugadores se reúnen, lo que da lugar a encuentros espontáneos de rol en gasolineras, hospitales y edificios de oficinas. Un sistema de máquinas expendedoras bien implementado ofrece mucho más que un simple menú de compra. Incluye animaciones de compra realistas, gestión de inventario que crea escasez, precios dinámicos que responden a la oferta y la demanda, distintos tipos de máquinas que venden distintas categorías de productos, mecánicas de robo para el rol criminal y trabajos de reposición que ofrecen empleo significativo a los jugadores. Este tutorial explica cómo crear un sistema completo de máquinas expendedoras y tiendas, desde la detección de props hasta la integración con el inventario y la gestión de la economía.
Detección y configuración de props de máquinas expendedoras
El mundo de GTA V está lleno de props de máquinas expendedoras que puedes convertir en objetos interactivos. Los modelos clave de props incluyen prop_vend_soda_01 y prop_vend_soda_02 para las máquinas de refrescos, prop_vend_water_01 para los dispensadores de agua, prop_vend_coffe_01 para máquinas de café, y prop_vend_snak_01 para máquinas de snacks. En lugar de colocar manualmente puntos de interacción en cada máquina del mapa, utiliza un sistema de detección por radio que busque estos modelos de objetos cerca del jugador. Cuando un jugador se acerque a un objeto coincidente dentro del rango de interacción, el sistema activará un aviso o una zona de objetivo. Este método funciona automáticamente con todas las instancias de estos objetos en todo el mapa, sin necesidad de configuración manual, y también detecta interiores MLO personalizados que incluyan estos modelos. Define catálogos de productos distintos para cada tipo de objeto, para que las máquinas de refrescos solo vendan bebidas, las máquinas de snacks vendan comida y las máquinas de café vendan distintas variedades de café.
-- shared/config.lua
Config = {}
Config.InteractDistance = 1.5
Config.UseAnimation = true
Config.AnimDuration = 3000 -- ms
Config.Machines = {
soda = {
models = {
`prop_vend_soda_01`,
`prop_vend_soda_02`,
},
label = 'Soda Machine',
items = {
{name = 'cola', label = 'Cola', price = 5, stock = 20},
{name = 'sprunk', label = 'Sprunk', price = 5, stock = 20},
{name = 'ecola', label = 'E-Cola', price = 6, stock = 15},
{name = 'energydrink',label = 'Energy Drink', price = 8, stock = 10},
},
},
snack = {
models = {`prop_vend_snak_01`},
label = 'Snack Machine',
items = {
{name = 'chips', label = 'Chips', price = 4, stock = 25},
{name = 'candy', label = 'Candy Bar', price = 3, stock = 30},
{name = 'sandwich', label = 'Sandwich', price = 7, stock = 15},
{name = 'granola', label = 'Granola Bar', price = 5, stock = 20},
},
},
coffee = {
models = {`prop_vend_coffe_01`},
label = 'Coffee Machine',
items = {
{name = 'coffee', label = 'Black Coffee', price = 4, stock = 30},
{name = 'latte', label = 'Latte', price = 6, stock = 20},
{name = 'espresso', label = 'Espresso', price = 5, stock = 25},
},
},
water = {
models = {`prop_vend_water_01`},
label = 'Water Cooler',
items = {
{name = 'water', label = 'Water Bottle', price = 3, stock = 40},
},
},
}
Sistema de interacción y animación del lado del cliente
Cuando un jugador se acerca a una máquina expendedora y pulsa la tecla de interacción, ocurren varias cosas en secuencia para crear una experiencia de compra inmersiva. Primero, se restringe el movimiento del jugador para evitar que se aleje a mitad de la animación. Luego, el personaje del jugador gira para mirar hacia la máquina usando TaskTurnPedToFaceEntity. Se reproduce una animación de compra usando el MINI@SPRUNK diccionario de animaciones para máquinas de refrescos, o ANIM@AM_HOLD_UP@MALE para interacciones generales. La máquina reproduce un efecto de sonido usando PlaySoundFromEntity para simular el ruido mecánico del dispensador. Cuando termina la animación, el cliente envía una solicitud de compra al servidor, que valida la transacción y añade el objeto al inventario del jugador. Si la máquina tiene una interacción con ranura para monedas, añade un pequeño objeto a la mano del jugador durante la animación usando AttachEntityToEntity para ofrecer mayor fidelidad visual. Toda la secuencia debería durar unos 3 segundos, lo suficiente para sentirse realista, pero sin frustrar a los jugadores.
-- client/interaction.lua
local isBuying = false
function PurchaseFromMachine(machineEntity, machineType, itemIndex)
if isBuying then return end
isBuying = true
local ped = PlayerPedId()
local machineCoords = GetEntityCoords(machineEntity)
-- Face the machine
TaskTurnPedToFaceEntity(ped, machineEntity, 1000)
Wait(1000)
-- Play animation
if Config.UseAnimation then
RequestAnimDict('mini@sprunk')
while not HasAnimDictLoaded('mini@sprunk') do Wait(10) end
TaskPlayAnim(ped, 'mini@sprunk', 'plyr_buy_drink_pt1',
8.0, -8.0, 2000, 0, 0, false, false, false)
Wait(1500)
TaskPlayAnim(ped, 'mini@sprunk', 'plyr_buy_drink_pt2',
8.0, -8.0, 1500, 0, 0, false, false, false)
-- Machine sound effect
PlaySoundFromEntity(-1, 'vending_machine_purchase',
machineEntity, 'dlc_vw_table_games', false, 0)
Wait(1500)
end
-- Request purchase from server
TriggerServerEvent('vendingmachine:purchase', machineType, itemIndex)
ClearPedTasks(ped)
isBuying = false
end
Gestión de existencias y economía del lado del servidor
La gestión de existencias convierte las máquinas expendedoras de dispensadores ilimitados de objetos en elementos económicos dinámicos. Cada ubicación de máquina controla su propio inventario de forma independiente. Cuando se agota un producto, los jugadores lo ven marcado como agotado en el menú de compra. Los niveles de existencias persisten en la base de datos tras reiniciar el servidor. Un trabajo de reposición permite que los jugadores trabajen como repartidores, recojan suministros en un almacén y los lleven a las máquinas vacías por la ciudad. Esto crea un ciclo económico completo: los proveedores reponen las máquinas, las máquinas venden a los consumidores, los ingresos vuelven al propietario de la máquina y el repartidor cobra un sueldo. Los precios dinámicos añaden otra capa: los productos populares aumentan de precio gradualmente a medida que bajan las existencias, y los precios se restablecen cuando se repone la máquina. Esto equilibra de forma natural los patrones de consumo y genera urgencia cuando queda poco de un producto muy solicitado.
-- server/stock.lua
local machineStock = {}
function GetMachineKey(machineType, coords)
return machineType .. ':' ..
math.floor(coords.x) .. ':' ..
math.floor(coords.y) .. ':' ..
math.floor(coords.z)
end
function GetStock(machineKey, itemIndex)
if not machineStock[machineKey] then
-- Load from database or initialize defaults
machineStock[machineKey] = {}
end
return machineStock[machineKey][itemIndex] or
Config.Machines[machineKey:match('^(%w+):')].items[itemIndex].stock
end
RegisterNetEvent('vendingmachine:purchase', function(machineType, itemIndex)
local src = source
local config = Config.Machines[machineType]
if not config or not config.items[itemIndex] then return end
local item = config.items[itemIndex]
local machineKey = machineType -- simplified; use coords in production
-- Check stock
local stock = GetStock(machineKey, itemIndex)
if stock <= 0 then
TriggerClientEvent('ox_lib:notify', src, {
title = 'Sold Out',
description = item.label .. ' is out of stock',
type = 'error'
})
return
end
-- Calculate dynamic price
local basePrice = item.price
local stockRatio = stock / item.stock
local dynamicPrice = math.ceil(basePrice * (1 + (1 - stockRatio) * 0.5))
-- Deduct money
local paid = exports['framework']:RemoveMoney(src, dynamicPrice, 'cash')
if not paid then
TriggerClientEvent('ox_lib:notify', src, {
title = 'No Cash',
description = 'You need $' .. dynamicPrice,
type = 'error'
})
return
end
-- Add item to inventory
local added = exports['ox_inventory']:AddItem(src, item.name, 1)
if not added then
exports['framework']:AddMoney(src, dynamicPrice, 'cash')
return
end
-- Decrease stock
machineStock[machineKey] = machineStock[machineKey] or {}
machineStock[machineKey][itemIndex] = stock - 1
TriggerClientEvent('ox_lib:notify', src, {
title = 'Purchased',
description = item.label .. ' - $' .. dynamicPrice,
type = 'success'
})
end)
Mecánicas de robo para el roleplay criminal
Las máquinas expendedoras son un objetivo de robo natural para los personajes criminales. Implementa una mecánica con ganzúa o palanca que permita a los criminales forzar las máquinas y robar el dinero que hay dentro. El robo debería llevar tiempo, requerir un minijuego como un puzle de ganzúas, generar ruido que alerte a los jugadores cercanos y pueda activar un aviso policial, y dar una recompensa variable según las compras que haya procesado la máquina desde su última reposición o robo. Registra la acumulación de dinero de cada máquina según las compras reales de los jugadores, para que la recompensa del robo sea realista y esté vinculada a la actividad económica efectiva. Tras un robo con éxito, la máquina queda dañada y no puede procesar compras hasta que la repare un mecánico o termine un periodo de espera automático. Esto genera consecuencias para la actividad criminal que afectan a toda la comunidad y crea trabajo de reparación para los mecánicos, enriqueciendo aún más el ecosistema económico.
Tiendas de jugadores y locales personalizados
Amplía el sistema de máquinas expendedoras hasta convertirlo en un sistema completo de punto de venta para negocios de jugadores. Los propietarios de negocios pueden colocar mostradores personalizados en sus propiedades, configurar los productos que venden, fijar sus propios precios y gestionar sus existencias mediante una NUI de administración. El sistema de tiendas utiliza las mismas mecánicas de compra y existencias que las máquinas expendedoras, pero añade una capa de gestión para el propietario con seguimiento de ingresos, gestión de gastos, permisos para empleados e informes de ganancias. Los empleados que hayan fichado en el negocio pueden usar la caja para procesar ventas manualmente como parte del roleplay, mientras que el sistema automático gestiona las ventas cuando no hay ningún empleado presente. Los ingresos de las ventas automáticas se depositan en la cuenta bancaria del negocio, menos un porcentaje de impuestos configurable que se destina a las arcas del gobierno del servidor. Esto crea una economía minorista completa en la que los jugadores fabrican o consiguen productos, abastecen sus tiendas, fijan precios competitivos frente a otros negocios y obtienen ingresos pasivos de su inversión.
Optimización del rendimiento e integración con sistemas de objetivos
Buscar modelos de objetos en cada fotograma destrozaría el rendimiento del servidor. En su lugar, utiliza un sistema de detección por niveles. Ejecuta una búsqueda general cada 2 segundos en un radio de 15 metros para encontrar máquinas expendedoras cercanas. Guarda en caché las ubicaciones de las máquinas detectadas y, en los fotogramas siguientes, comprueba con precisión la distancia solo con respecto a la caché. Cuando el jugador se aleje más de 20 metros de cualquier máquina guardada en caché, borra la caché y activa una nueva búsqueda. En los servidores que usan sistemas de objetivos como ox_target o qb-target, registra zonas de objetivo en los objetos detectados en lugar de usar avisos por proximidad. Las zonas de objetivo ofrecen mejor rendimiento porque solo se activan cuando el jugador apunta al objeto, lo que elimina por completo la necesidad de comprobar continuamente la distancia. Este método también ofrece una interfaz más clara, con opciones de interacción contextuales que aparecen directamente sobre el modelo de la máquina en lugar de mostrarse como avisos de texto flotantes.
-- client/target.lua (ox_target integration)
local registeredMachines = {}
CreateThread(function()
for machineType, config in pairs(Config.Machines) do
for _, model in ipairs(config.models) do
exports.ox_target:addModel(model, {
{
name = 'vending_' .. machineType,
icon = 'fas fa-shopping-cart',
label = 'Use ' .. config.label,
onSelect = function(data)
OpenMachineMenu(machineType, data.entity)
end,
distance = Config.InteractDistance,
},
})
end
end
end)