Система автосалона FiveM: правильный скриптинг дилершипов
Создайте реалистичный автосалон для FiveM. Тест-драйвы, оформление шоурума, варианты с NPC и игроками, а также лучшие скрипты автосалонов для QBCore и ESX.
Agency Scripts
Основатель и ведущий разработчик Agency Scripts
Архитектура автосалона
Система автодилера , один из основных экономических драйверов любого ролевого сервера FiveM, служащий точкой входа для приобретения игроками транспортных средств. Хорошо продуманный автосалон выходит за рамки простого меню покупки, предлагая функции предпросмотра автомобилей в шоуруме, тест-драйвы, варианты финансирования, трейд-ины и учёт продаж для сотрудников дилера. Архитектура делится на каталог с определением доступных автомобилей, ценами и категориями, шоурум для осмотра машин перед покупкой, движок транзакций для обработки покупок и финансирования, а также слой управления сотрудниками для дилерских точек, управляемых игроками. Каждый слой взаимодействует через серверные валидированные события, чтобы предотвратить манипуляции с ценами и несанкционированное появление автомобилей.
Каталог транспортных средств и система категорий
Каталог транспортных средств определяет каждую машину, доступную для покупки, организованную по категориям для удобного просмотра. Каждая запись включает имя спавна, отображаемую метку, цену, категорию и необязательные метаданные, такие как максимальная скорость и количество мест для витрины. Храните каталог в общем конфигурационном файле, к которому обращаются и клиент, и сервер, обеспечивая проверку цены на сервере, а клиент использует те же данные для отображения. Вот структурированное определение каталога:
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
Таблица поиска цен Config.VehiclePrices включает проверку цен на сервере с O(1), предотвращая отправку клиентами изменённых цен. Всегда проверяйте название модели по этой таблице перед обработкой покупки. Рассмотрите возможность загрузки цен на транспорт из таблицы базы данных вместо конфигурационного файла, если хотите, чтобы администраторы сервера могли менять цены через панель управления без перезапуска сервера.
Система предпросмотра шоурума
Шоурум позволяет игрокам осмотреть транспорт в контролируемой среде перед покупкой. Спавните превью транспорт в назначенной точке шоурума, примените камеру, которую игрок может вращать вокруг транспорта, и отображайте характеристики рядом с моделью. Превью транспорт должен быть неинтерактивным и удаляться при закрытии меню или выборе другого транспорта. Вот клиентская логика превью:
-- 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
Разрешить вращение камеры с помощью мыши или клавиатуры при открытом шоуруме. Подход с орбитальной камерой позволяет игрокам осмотреть транспорт со всех сторон без необходимости обходить его. Добавить настройку цвета автомобиля в превью, чтобы игроки могли увидеть желаемую окраску перед покупкой, что снижает сожаления о покупке и обращения в поддержку.
Покупка и обработка транзакций
Покупки автомобилей должны полностью обрабатываться на стороне сервера, чтобы предотвратить эксплойты. Сервер проверяет, что игрок может позволить себе машину, списывает оплату, генерирует уникальный номерной знак, создаёт запись о транспортном средстве в базе данных и уведомляет клиент о спавне купленного автомобиля. Поддерживается как полная оплата наличными, так и банковские переводы, а также опциональная интеграция с вашей банковской системой для проводок:
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
Параметр GenerateUniquePlate функция использует цикл повторных попыток для гарантии уникальности номеров по всей базе данных. Хотя коллизии статистически редки при 8 алфавитно-цифровых символах, проверка обеспечивает абсолютную безопасность. Логируйте каждую продажу в отдельной таблице для административного контроля и аналитики экономики сервера, отслеживая самые популярные автомобили и общий денежный поток через дилерский центр.
Система тест-драйва
Тест-драйвы позволяют игрокам опробовать транспорт перед покупкой, что особенно важно для дорогих машин. Спавните временный транспорт с таймером и географическими границами, автоматически возвращая игрока в автосалон при истечении времени или выходе за пределы зоны. Отметьте тест-драйв транспорт так, чтобы его нельзя было хранить в гараже или модифицировать, предотвращая злоупотребления системой для получения бесплатных машин:
-- 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
Географическая граница предотвращает выезд игроков на тестовом транспортном средстве по всей карте и его оставление. Установите радиус на разумное расстояние, позволяющее значимую поездку по ближайшим улицам при сохранении возможности восстановления транспортного средства. Номерной знак "TESTDRVE" служит визуальным индикатором для других игроков, что транспортное средство временное.
Финансирование и планы оплаты
Для дорогих транспортных средств предложите опцию финансирования, где игроки вносят первоначальный взнос, а затем платят рассрочку. Отслеживайте кредит в таблице базы данных с остатком, графиком платежей и процентной ставкой. Если игрок пропускает платежи, транспортное средство может быть помечено для изъятия. Эта функция добавляет экономическую глубину и делает дорогие машины доступными для игроков, не накопивших достаточно денег для полной покупки. Реализуйте проверку рассрочки как серверную периодическую задачу, которая запускается ежедневно по игровому времени, списывая платежи с банковского счёта игрока и уведомляя его о каждом списании. Если баланс недостаточен, увеличьте счётчик пропущенных платежей и выдайте предупреждение. После настраиваемого количества пропущенных платежей отметьте транспорт для изъятия, удалив его из гаража игрока и вернув в инвентарь дилера.
Отслеживание продаж сотрудников и комиссий
Автосалоны, управляемые игроками, нуждаются в инструментах для управления торговым персоналом, отслеживания результатов и распределения комиссионных. Когда сотрудник дилера способствует продаже, он получает настраиваемый процент от цены автомобиля в качестве комиссии. Отслеживайте количество продаж каждого сотрудника, общий доход и заработанные комиссионные в таблице базы данных. Создайте панель сотрудника, доступную через NUI автосалона, которая показывает личную статистику, историю последних продаж и таблицу лидеров с сравнением результатов команды продаж. Владельцы или менеджеры автосалона должны иметь доступ к админ-панели для настройки ставок комиссионных, добавления или удаления сотрудников и просмотра сводных отчетов по продажам. Это превращает автосалон из статичного взаимодействия с NPC в динамичный бизнес игроков с реальными управленческими обязанностями и конкурентными стимулами.
Оптимизация и меры против эксплойтов
Скрипты автосалонов подвержены распространённым вектором эксплойтов, требующим проактивного предотвращения. Самый критичный , манипуляция ценами, когда модифицированный клиент отправляет запрос на покупку с заниженной ценой. Всегда проверяйте цены на стороне сервера по каталогу и никогда не доверяйте значениям, переданным клиентом. Ограничьте частоту событий покупки, чтобы предотвратить быструю покупку, которая может дублировать транспортные средства или создавать гонки в базе данных. Для предпросмотра в шоуруме убедитесь, что предпросматриваемое транспортное средство создано с false для сетевого параметра, чтобы он существовал только локально и не мог быть введен или украден другими игроками. Удаляйте предварительно просмотренные транспортные средства в onResourceStop обработчик для предотвращения оставшихся сущностей, если ресурс перезапускается. Для серверов с несколькими автосалонами кешируйте каталог транспортных средств в памяти и перезагружайте его только при команде обновления от администратора, избегая повторного чтения конфигурационных файлов при каждом открытии NUI. Отслеживайте логи покупок на предмет аномалий, таких как один и тот же игрок, покупающий десятки автомобилей подряд, что может указывать на эксплойт или дублирование денег, требующее расследования.