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
endA 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
endPermita 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
endA 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')
endA 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.

