>
Tutorial2026-05-18

Script de Máquinas de Vending e Lojinhas em FiveM

TDYSKY

TDYSKY

Fundador & Lead Developer na Agency Scripts

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
end

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

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.