>
Guia 2026-05-01

Desenvolvimento de Concessionários e Lojas de Veículos para FiveM

OntelMonke

OntelMonke

Admin & Developer na Agency Scripts

Arquitetura de concessionária de veículos

Um sistema de concessionária de automóveis é um dos principais impulsionadores econômicos em qualquer servidor de roleplay FiveM, servindo como porta de entrada através da qual os jogadores adquirem seus veículos. Uma concessionária bem projetada vai além de um simples menu de compra, oferecendo recursos como prévias de veículos em um showroom, test drives, opções de financiamento, trocas e acompanhamento de vendas para funcionários da concessionária. A arquitetura se divide em um sistema de catálogo que define os veículos disponíveis com preços e categorias, um showroom que permite aos jogadores inspecionar os veículos antes de comprar, um mecanismo de transação que lida com compras e financiamento e uma camada de gerenciamento de funcionários para concessionárias administradas por jogadores. Cada camada se comunica por meio de eventos validados pelo servidor para evitar manipulação de preços e geração não autorizada de veículos.

Catálogo de veículos e sistema de categorias

O catálogo de veículos define todos os carros disponíveis para compra, organizados em categorias para facilitar a navegação. Cada entrada inclui o nome do spawn, rótulo de exibição, preço, categoria e metadados opcionais, como velocidade máxima e contagem de assentos para exibição no showroom. Armazene o catálogo em um arquivo de configuração compartilhado que tanto o cliente quanto o servidor fazem referência, garantindo que a validação do preço aconteça no lado do servidor enquanto o cliente usa os mesmos dados para renderização. Aqui está uma definição de catálogo estruturado:

Config.VehicleCatalog = {
    categories = {
        {id = 'sedan', label = 'Sedans', icon = 'fa-car'},
        {id = 'sport', label = 'Sports Cars', icon = 'fa-flag-checkered'},
        {id = 'suv', label = 'SUVs & Trucks', icon = 'fa-truck'},
        {id = 'muscle', label = 'Muscle Cars', icon = 'fa-bolt'},
        {id = 'motorcycle', label = 'Motorcycles', icon = 'fa-motorcycle'},
    },
    vehicles = {
        -- Sedans
        {model = 'sultan', label = 'Karin Sultan', price = 24500,
         category = 'sedan', seats = 4, testDrive = true},
        {model = 'schafter2', label = 'Benefactor Schafter', price = 38000,
         category = 'sedan', seats = 4, testDrive = true},
        -- Sports
        {model = 'elegy2', label = 'Annis Elegy RH8', price = 95000,
         category = 'sport', seats = 2, testDrive = true},
        {model = 'comet2', label = 'Pfister Comet', price = 110000,
         category = 'sport', seats = 2, testDrive = true},
        -- SUVs
        {model = 'baller', label = 'Gallivanter Baller', price = 55000,
         category = 'suv', seats = 4, testDrive = true},
        -- Muscle
        {model = 'dominator', label = 'Vapid Dominator', price = 42000,
         category = 'muscle', seats = 2, testDrive = true},
        -- Motorcycles
        {model = 'bati', label = 'Pegassi Bati 801', price = 18000,
         category = 'motorcycle', seats = 2, testDrive = true},
    },
}

-- Build lookup table for fast server-side price validation
Config.VehiclePrices = {}
for _, v in ipairs(Config.VehicleCatalog.vehicles) do
    Config.VehiclePrices[v.model] = v.price
end

A tabela de consulta de preços Config.VehiclePrices permite a validação de preços O(1) no servidor, evitando que os clientes enviem preços manipulados. Sempre valide o nome do modelo nesta tabela antes de processar uma compra. Considere carregar os preços dos veículos a partir de uma tabela de banco de dados em vez de um arquivo de configuração se desejar que os administradores do servidor ajustem os preços por meio de um painel de administração sem reiniciar o servidor.

Sistema de visualização de showroom

O showroom permite que os jogadores inspecionem os veículos em um ambiente controlado antes de fazer a compra. Gere um veículo de visualização em uma posição designada no showroom, aplique uma câmera que o jogador possa girar ao redor do veículo e exiba estatísticas ao lado do modelo. O veículo de visualização não deve ser interativo e desaparecerá quando o jogador fechar o menu ou selecionar um veículo diferente. Aqui está a lógica de visualização do lado do cliente:

-- client/showroom.lua
local previewVehicle = nil
local previewCam = nil
local camAngle = 0.0

function ShowVehiclePreview(modelName)
    -- Clean up previous preview
    DestroyPreview()

    local model = GetHashKey(modelName)
    RequestModel(model)
    while not HasModelLoaded(model) do Wait(10) end

    local showroomPos = Config.ShowroomPosition -- vector4
    previewVehicle = CreateVehicle(model, showroomPos.x, showroomPos.y,
        showroomPos.z, showroomPos.w, false, false)

    SetEntityInvincible(previewVehicle, true)
    SetVehicleDoorsLocked(previewVehicle, 2)
    FreezeEntityPosition(previewVehicle, true)
    SetVehicleOnGroundProperly(previewVehicle)
    SetModelAsNoLongerNeeded(model)

    -- Create orbiting camera
    previewCam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
    UpdateCameraPosition()
    SetCamActive(previewCam, true)
    RenderScriptCams(true, true, 500, true, true)
end

function UpdateCameraPosition()
    if not previewCam or not previewVehicle then return end
    local vehPos = GetEntityCoords(previewVehicle)
    local radius = 6.0
    local height = 2.0
    local rad = math.rad(camAngle)
    local camX = vehPos.x + radius * math.cos(rad)
    local camY = vehPos.y + radius * math.sin(rad)
    SetCamCoord(previewCam, camX, camY, vehPos.z + height)
    PointCamAtEntity(previewCam, previewVehicle, 0.0, 0.0, 0.0, true)
end

function DestroyPreview()
    if previewVehicle then
        DeleteEntity(previewVehicle)
        previewVehicle = nil
    end
    if previewCam then
        SetCamActive(previewCam, false)
        RenderScriptCams(false, true, 500, true, true)
        DestroyCam(previewCam, false)
        previewCam = nil
    end
end

Permita a rotação da câmera através do movimento do mouse ou dos controles do teclado enquanto o showroom estiver aberto. A abordagem da câmera orbital permite que os jogadores vejam o veículo de todos os ângulos sem a necessidade de contorná-lo. Adicione personalização de cores do veículo à visualização para que os jogadores possam ver a pintura desejada antes de comprar, o que reduz o remorso do comprador e as solicitações de suporte.

Processamento de compras e transações

As compras de veículos devem ser processadas inteiramente no lado do servidor para evitar exploração. O servidor valida se o jogador pode comprar o veículo, deduz o pagamento, gera uma placa única, cria o registro do veículo no banco de dados e notifica o cliente para gerar o veículo adquirido. Suporta compras em dinheiro e transferências bancárias e, opcionalmente, integre-se ao seu sistema bancário para transferências eletrônicas:

RegisterNetEvent('dealer:server:purchaseVehicle', function(modelName, paymentType)
    local src = source
    local Player = QBCore.Functions.GetPlayer(src)
    if not Player then return end

    -- Validate model exists and get price
    local price = Config.VehiclePrices[modelName]
    if not price then
        TriggerClientEvent('QBCore:Notify', src, 'Vehicle not available', 'error')
        return
    end

    -- Check payment
    local moneyType = paymentType == 'bank' and 'bank' or 'cash'
    if Player.PlayerData.money[moneyType] < price then
        TriggerClientEvent('QBCore:Notify', src, 'Insufficient funds', 'error')
        return
    end

    -- Generate unique plate
    local plate = GenerateUniquePlate()

    -- Process payment
    Player.Functions.RemoveMoney(moneyType, price, 'vehicle-purchase-' .. modelName)

    -- Create vehicle record
    local vehicleHash = GetHashKey(modelName)
    MySQL.insert([[
        INSERT INTO player_vehicles
        (citizenid, vehicle, hash, plate, garage, state, fuel, engine, body)
        VALUES (?, ?, ?, ?, ?, 1, 100, 1000.0, 1000.0)
    ]], {
        Player.PlayerData.citizenid,
        modelName,
        tostring(vehicleHash),
        plate,
        'pillboxgarage',
    })

    -- Log transaction
    MySQL.insert([[
        INSERT INTO vehicle_sales (citizenid, vehicle, plate, price, sold_at)
        VALUES (?, ?, ?, ?, NOW())
    ]], {Player.PlayerData.citizenid, modelName, plate, price})

    TriggerClientEvent('QBCore:Notify', src,
        'Vehicle purchased! Plate: ' .. plate, 'success')
    TriggerClientEvent('dealer:client:vehiclePurchased', src, modelName, plate)
end)

function GenerateUniquePlate()
    local plate
    repeat
        plate = ''
        local chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
        for i = 1, 8 do
            local idx = math.random(1, #chars)
            plate = plate .. chars:sub(idx, idx)
        end
        local exists = MySQL.scalar.await(
            'SELECT 1 FROM player_vehicles WHERE plate = ?', {plate}
        )
    until not exists
    return plate
end

A função GenerateUniquePlate usa um loop de repetição para garantir a exclusividade da placa em todo o banco de dados. Embora as colisões sejam estatisticamente raras com 8 caracteres alfanuméricos, a verificação garante segurança absoluta. Registre cada venda em uma tabela separada para análise administrativa e análise da economia do servidor, rastreando quais veículos são mais populares e o fluxo de caixa total da concessionária.

Sistema de test drive

Os test drives permitem que os jogadores experimentem um veículo antes de comprá-lo, o que é especialmente importante para veículos caros. Gera um veículo temporário com cronômetro e limite geográfico, retornando automaticamente o jogador à concessionária quando o tempo expirar ou ele sair da área permitida. Marque o veículo de test drive para que não possa ser armazenado em garagem ou modificado, evitando que os jogadores explorem o sistema para obter veículos gratuitos:

-- Client: test drive logic
local testDriveVehicle = nil
local testDriveTimer = 0
local testDriveActive = false

function StartTestDrive(modelName, duration)
    local model = GetHashKey(modelName)
    RequestModel(model)
    while not HasModelLoaded(model) do Wait(10) end

    local spawnPos = Config.TestDriveSpawn
    testDriveVehicle = CreateVehicle(model, spawnPos.x, spawnPos.y,
        spawnPos.z, spawnPos.w, true, false)

    SetVehicleNumberPlateText(testDriveVehicle, 'TESTDRVE')
    TaskWarpPedIntoVehicle(PlayerPedId(), testDriveVehicle, -1)
    SetModelAsNoLongerNeeded(model)

    testDriveTimer = duration
    testDriveActive = true

    -- Timer and boundary check thread
    CreateThread(function()
        while testDriveActive and testDriveTimer > 0 do
            Wait(1000)
            testDriveTimer = testDriveTimer - 1

            -- Show remaining time
            SendNUIMessage({
                action = 'updateTestDrive',
                timeLeft = testDriveTimer
            })

            -- Check boundary
            local playerPos = GetEntityCoords(PlayerPedId())
            local dealerPos = Config.DealerLocation
            if #(playerPos - dealerPos) > Config.TestDriveRadius then
                QBCore.Functions.Notify('Too far from dealer, returning...', 'error')
                EndTestDrive()
                return
            end
        end

        if testDriveActive then
            EndTestDrive()
        end
    end)
end

function EndTestDrive()
    testDriveActive = false
    if testDriveVehicle and DoesEntityExist(testDriveVehicle) then
        DeleteEntity(testDriveVehicle)
        testDriveVehicle = nil
    end

    local returnPos = Config.DealerLocation
    SetEntityCoords(PlayerPedId(), returnPos.x, returnPos.y, returnPos.z)
    QBCore.Functions.Notify('Test drive ended', 'info')
end

A fronteira geográfica impede que os jogadores dirijam o veículo de teste pelo mapa e o abandonem. Defina o raio para uma distância razoável que permita uma condução significativa pelas ruas próximas, mantendo o veículo recuperável. A placa "TESTDRVE" serve como um indicador visual para outros jogadores de que o veículo é temporário.

Planos de Financiamento e Pagamento

Para veículos caros, ofereça uma opção de financiamento onde os jogadores fazem um pagamento inicial e depois pagam parcelas ao longo do tempo. Acompanhe o empréstimo em uma tabela de banco de dados com saldo restante, cronograma de pagamento e taxa de juros. Se um jogador perder o pagamento, o veículo pode ser sinalizado para reintegração de posse. Esse recurso adiciona profundidade econômica e torna os veículos de última geração acessíveis aos jogadores que não acumularam dinheiro suficiente para uma compra completa. Implemente a verificação de parcelamento como um trabalho recorrente do lado do servidor que é executado diariamente durante o jogo, deduzindo os pagamentos da conta bancária do jogador e notificando-o de cada cobrança. Se o saldo bancário for insuficiente, incremente um contador de pagamentos perdidos e emita um aviso. Após um número configurável de pagamentos perdidos, marque o veículo para reintegração de posse, onde ele será retirado da garagem do jogador e devolvido ao inventário da concessionária.

Vendas de funcionários e acompanhamento de comissões

As concessionárias administradas por jogadores precisam de ferramentas para gerenciar a equipe de vendas, monitorar o desempenho e distribuir comissões. Quando um funcionário da concessionária facilita uma venda, ele ganha uma porcentagem configurável do preço do veículo como comissão. Acompanhe a contagem de vendas de cada funcionário, a receita total gerada e as comissões recebidas em uma tabela de banco de dados. Crie um painel de funcionários acessível por meio do NUI da concessionária que mostra estatísticas pessoais, histórico de vendas recentes e um placar comparando o desempenho de toda a equipe de vendas. O nível de proprietário ou gerente da concessionária deve ter acesso a um painel de administração para ajustar taxas de comissão, adicionar ou remover funcionários e visualizar relatórios de vendas agregadas. Isso transforma a concessionária de uma interação estática de NPCs em um negócio dinâmico de jogadores com responsabilidades reais de gerenciamento e incentivos competitivos.

Medidas de otimização e anti-exploração

Os scripts das concessionárias enfrentam vetores de exploração comuns que precisam de mitigação proativa. O mais crítico é a manipulação de preços, onde um cliente modificado envia uma solicitação de compra com preço inferior. Sempre valide os preços no servidor em relação ao catálogo e nunca confie nos valores informados pelo cliente. Eventos de compra com limite de taxa para evitar compras rápidas que poderiam duplicar veículos ou criar condições de corrida no banco de dados. Para a visualização do showroom, certifique-se de que o veículo de visualização seja criado com false para o parâmetro de rede, para que exista apenas localmente e não possa ser invadido ou roubado por outros jogadores. Limpe os veículos de visualização no manipulador onResourceStop para evitar entidades órfãs se o recurso for reiniciado. Para servidores com várias concessionárias, armazene em cache o catálogo de veículos na memória e recarregue-o somente quando um administrador acionar um comando de atualização, evitando leituras repetidas do arquivo de configuração em cada NUI aberto. Monitore os registros de compras em busca de anomalias, como o mesmo jogador comprando dezenas de veículos em rápida sucessão, o que pode indicar uma exploração ou duplicação de dinheiro que merece investigaçã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.