Por que as máquinas de venda automática adicionam profundidade ao seu servidor
Máquinas de venda automática e sistemas de lojas automatizadas desempenham um papel crucial em qualquer economia de RPG. Eles fornecem acesso 24 horas por dia, 7 dias por semana, a itens essenciais, como alimentos, água, cigarros e suprimentos básicos, sem a necessidade de um funcionário da loja estar online. Além da conveniência, eles criam pontos de encontro naturais onde os jogadores se reúnem, levando a encontros espontâneos de roleplay em postos de gasolina, hospitais e prédios de escritórios. Um sistema de máquina de venda automática bem implementado vai muito além de um simples menu de compra. Inclui animações de compra realistas, gerenciamento de estoque que cria escassez, preços dinâmicos que respondem à oferta e demanda, diferentes tipos de máquinas que vendem diferentes categorias de produtos, mecânica de roubo para dramatização criminosa e empregos de reabastecimento que dão aos jogadores empregos significativos. Este tutorial cobre a construção de um sistema completo de vendas e lojas, desde a detecção de acessórios até a integração de estoque e gerenciamento de economia.
Detectando e configurando acessórios de máquinas de venda automática
O mundo de GTA V está repleto de acessórios de máquinas de venda automática que você pode transformar em objetos interativos. Os principais modelos de suporte incluem prop_vend_soda_01 e prop_vend_soda_02 para máquinas de refrigerantes, prop_vend_water_01 para dispensadores de água, prop_vend_coffe_01 para máquinas de café e prop_vend_snak_01 para máquinas de salgadinhos. Em vez de colocar manualmente pontos de interação em cada local de máquina no mapa, use um sistema de detecção baseado em raio que procura esses modelos de objetos próximos ao jogador. Quando um jogador se aproxima de qualquer objeto correspondente dentro do alcance de interação, o sistema ativa um alerta ou zona alvo. Essa abordagem funciona automaticamente com todas as instâncias desses adereços em todo o mapa, sem configuração manual, e ainda seleciona interiores MLO personalizados que incluem esses modelos. Defina catálogos de produtos diferentes para cada tipo de acessório para que as máquinas de refrigerante vendam apenas bebidas, as máquinas de salgadinhos vendam alimentos e as máquinas de café vendam variantes 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 interação e animação do lado do cliente
Quando um jogador se aproxima de uma máquina de venda automática e pressiona a tecla de interação, várias coisas acontecem em sequência para criar uma experiência de compra envolvente. Primeiro, o movimento do jogador é restrito para evitar que ele se afaste no meio da animação. Então o personagem do jogador se vira para a máquina usando TaskTurnPedToFaceEntity. Uma animação de compra é reproduzida usando o dicionário de animação MINI@SPRUNK para máquinas de refrigerante ou ANIM@AM_HOLD_UP@MALE para interações gerais. A própria máquina reproduz um efeito sonoro usando PlaySoundFromEntity para simular o ruído mecânico de distribuição. Após o término da animação, o cliente envia uma solicitação de compra ao servidor, que valida a transação e adiciona o item ao inventário do jogador. Se a máquina tiver uma interação de slot de moeda, adicione um pequeno acessório à mão do jogador durante a animação usando AttachEntityToEntity para maior fidelidade visual. A sequência inteira deve durar cerca de 3 segundos, longa o suficiente para parecer realista, mas curta o suficiente para não frustrar os jogadores.
-- 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
endGerenciamento e economia de estoque no lado do servidor
A gestão de stocks transforma as máquinas de venda automática de distribuidores ilimitados de artigos em elementos económicos dinâmicos. Cada local de máquina rastreia seu próprio inventário de forma independente. Quando uma máquina fica sem um determinado item, os jogadores o veem marcado como esgotado no menu de compra. Os níveis de estoque persistem no banco de dados durante as reinicializações do servidor. Um trabalho de reabastecimento permite que os jogadores trabalhem como motoristas de entrega que pegam suprimentos em um depósito e os levam para máquinas vazias pela cidade. Isto cria todo um ciclo económico: os fornecedores reabastecem as máquinas, as máquinas vendem aos consumidores, a receita flui de volta para o proprietário da máquina e o entregador ganha salários. O preço dinâmico adiciona outra camada onde os itens populares aumentam gradualmente de preço à medida que o estoque diminui e os preços são redefinidos quando a máquina é reabastecida. Isso naturalmente equilibra os padrões de consumo e cria urgência quando uma máquina está com pouco produto desejável.
-- 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ânica de roubo para dramatização criminal
As máquinas de venda automática representam um alvo natural de roubo para personagens criminosos. Implemente uma mecânica de arrombamento ou alavanca que permita que os criminosos invadam as máquinas para roubar o dinheiro que elas contêm. O roubo deve levar tempo, exigir um minijogo como um quebra-cabeça de arrombamento, gerar ruído que alerte os jogadores próximos e potencialmente acione um despacho policial e renderá uma recompensa variável com base em quantas compras a máquina processou desde seu último reabastecimento ou roubo. Acompanhe o acúmulo de dinheiro por máquina com base nas compras reais dos jogadores para que a recompensa do roubo seja realista e vinculada à atividade econômica genuína. Após um roubo bem-sucedido, a máquina entra em um estado danificado, onde não pode processar as compras até ser reparada por um mecânico ou automaticamente após um período de espera. Isto cria consequências para a actividade criminosa que afecta a comunidade em geral e gera trabalhos de reparação para mecânicos, enriquecendo ainda mais o ecossistema económico.
Lojas de propriedade dos jogadores e vitrines personalizadas
Estenda a estrutura da máquina de venda automática para um sistema completo de ponto de venda para empresas de propriedade dos jogadores. Os proprietários de empresas podem colocar balcões de lojas personalizados em suas propriedades, configurar quais itens vendem, definir seus próprios preços e gerenciar seu estoque por meio de um NUI de gerenciamento. O sistema de loja usa a mesma mecânica subjacente de compra e estoque das máquinas de venda automática, mas adiciona uma camada de proprietário com rastreamento de receitas, gerenciamento de despesas, permissões de funcionários e relatórios de lucros. Os funcionários registrados no negócio podem acessar o registro para processar vendas manualmente para interações de roleplay, enquanto o sistema automatizado lida com as vendas quando nenhum funcionário está presente. A receita das vendas automatizadas vai para a conta bancária comercial menos uma porcentagem de imposto configurável que flui para o tesouro governamental do servidor. Isto cria uma economia de retalho completa, onde os jogadores fabricam ou adquirem bens, abastecem as suas lojas, estabelecem preços competitivos em relação a outras empresas e obtêm rendimentos passivos com o seu investimento.
Otimização de desempenho e integração de metas
A varredura de modelos de prop em cada quadro destruiria o desempenho do servidor. Em vez disso, use uma abordagem de detecção em camadas. Faça uma varredura ampla a cada 2 segundos em um raio de 15 metros para encontrar acessórios de máquinas de venda automática próximos. Cache localizações de máquinas descobertas e só faz verificações precisas de distância em relação ao cache em quadros subsequentes. Quando o jogador se mover mais de 20 metros de qualquer máquina em cache, limpe o cache e acione uma nova varredura. Para servidores que usam sistemas de destino como ox_target ou qb-target, registre zonas de destino em adereços descobertos em vez de usar prompts de proximidade. As zonas-alvo têm melhor desempenho porque só são ativadas quando o jogador aponta para o objeto, eliminando totalmente a necessidade de verificação contínua da distância. A abordagem de destino também fornece uma experiência de UI mais limpa com opções de interação sensíveis ao contexto que aparecem diretamente no modelo da máquina, em vez de prompts de texto flutuantes.
-- 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)
