1007 lines
35 KiB
Lua
1007 lines
35 KiB
Lua
-- ============================================================
|
|
-- ADVANCED GARAGES - Client Script (Refactored)
|
|
-- ============================================================
|
|
|
|
-- ============================================================
|
|
-- ALIASES LOCAUX (raccourcis vers les natives fréquemment utilisées)
|
|
-- ============================================================
|
|
|
|
local IsDisabledControlJustPressed = IsDisabledControlJustPressed
|
|
local IsDisabledControlPressed = IsDisabledControlPressed
|
|
local IsControlJustPressed = IsControlJustPressed
|
|
local HandleFlyCam = Utils.HandleFlyCam
|
|
local DrawScaleform = Utils.DrawScaleform
|
|
local GetEntityCoords = GetEntityCoords
|
|
|
|
-- ============================================================
|
|
-- VARIABLES GLOBALES
|
|
-- ============================================================
|
|
|
|
PolyZones = {}
|
|
ClosestGarage = nil
|
|
IsGarageOwner = false
|
|
nearbyGarageType = nil
|
|
selectingShell = nil
|
|
IsKeyHolder = false
|
|
DefaultPlayerCoords = nil
|
|
preGarageCoords = nil
|
|
|
|
local blipsList = {}
|
|
local isInitialized = false
|
|
local nearbyVehicleNetId = nil -- véhicule ciblé par la mise en fourrière
|
|
|
|
-- ============================================================
|
|
-- INITIALISATION NUI
|
|
-- ============================================================
|
|
|
|
-- Construit le payload de langue pour le front-end React
|
|
local function buildLocalePayload()
|
|
local localeName = Config.Locale
|
|
local resources = { [localeName] = { translation = _T } }
|
|
return localeName, resources
|
|
end
|
|
|
|
RegisterNUICallback("initialized", function(data, cb)
|
|
-- Attend que les traductions soient chargées
|
|
while not _T do
|
|
Wait(200)
|
|
end
|
|
|
|
if isInitialized then
|
|
Debug("Already initialized")
|
|
return cb("ok")
|
|
end
|
|
|
|
local localeName, resources = buildLocalePayload()
|
|
|
|
local resourceName = GetCurrentResourceName()
|
|
local configPayload = {
|
|
debug = Config.Debug,
|
|
version = GetResourceMetadata(resourceName, "version", 0),
|
|
intl = Config.Intl,
|
|
transferPrice = Config.TransferGaragePrice,
|
|
impoundPrice = Config.ImpoundPrice,
|
|
soundFiles = Config.SoundFiles,
|
|
tags = Config.Tags,
|
|
idleDuration = Config.IdleDuration,
|
|
soundPath = Config.Path .. "sounds/",
|
|
imagePath = Config.ImagePath,
|
|
music = Config.Music,
|
|
musicVolume = Config.MusicVolume,
|
|
enabledButtons = Config.EnabledButtons,
|
|
sellObjectCommision = Config.SellObjectCommision,
|
|
}
|
|
|
|
SendReactMessage("onUiReady", {
|
|
languageName = localeName,
|
|
resources = resources,
|
|
config = configPayload,
|
|
})
|
|
|
|
isInitialized = true
|
|
TriggerServerEvent("garages:playerConnected")
|
|
cb("ok")
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- UTILITAIRE : Envoie un message au front-end React (NUI)
|
|
-- ============================================================
|
|
|
|
function SendReactMessage(action, data)
|
|
SendNUIMessage({ action = action, data = data })
|
|
end
|
|
|
|
-- ============================================================
|
|
-- CALLBACKS NUI SIMPLES
|
|
-- ============================================================
|
|
|
|
RegisterNUICallback("notification", function(data, cb)
|
|
Notification(data.message, data.type)
|
|
cb("ok")
|
|
end)
|
|
|
|
RegisterNUICallback("close", function(data, cb)
|
|
SetNuiFocus(false, false)
|
|
decorate:close()
|
|
creator:close()
|
|
furnitureCreator:close()
|
|
cb(true)
|
|
end)
|
|
|
|
RegisterNUICallback("existMoney", function(data, cb)
|
|
local result = lib.callback.await("advancedgarages:existMoney", false, data)
|
|
cb(result)
|
|
end)
|
|
|
|
RegisterNUICallback("payVehiclePrice", function(data, cb)
|
|
TriggerServerEvent("advancedgarages:server:payVehiclePrice", data)
|
|
cb(true)
|
|
end)
|
|
|
|
RegisterNUICallback("impound", function(data, cb)
|
|
TriggerServerEvent("advancedgarages:impound", nearbyVehicleNetId, data)
|
|
SetNuiFocus(false, false)
|
|
cb(true)
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- SONS NUI
|
|
-- ============================================================
|
|
|
|
local soundMap = {
|
|
category_down = { "NAV_UP_DOWN", "HUD_FRONTEND_DEFAULT_SOUNDSET" },
|
|
item_down = { "Object_Collect_Remote", "GTAO_FM_Events_Soundset" },
|
|
finish = { "Menu_Accept", "Phone_SoundSet_Default" },
|
|
cancel = { "MP_IDLE_KICK", "HUD_FRONTEND_DEFAULT_SOUNDSET" },
|
|
admin_active = { "Hack_Success", "DLC_HEIST_BIOLAB_PREP_HACKING_SOUNDS" },
|
|
admin_disable = { "Hack_Failed", "DLC_HEIST_BIOLAB_PREP_HACKING_SOUNDS" },
|
|
hover_down = { "Highlight_Accept", "DLC_HEIST_PLANNING_BOARD_SOUNDS" },
|
|
hover_up = { "Highlight_Error", "DLC_HEIST_PLANNING_BOARD_SOUNDS" },
|
|
}
|
|
|
|
RegisterNUICallback("play_sound", function(soundKey, cb)
|
|
if Config.DisableSounds then return cb("ok") end
|
|
|
|
local sound = soundMap[soundKey]
|
|
if sound then
|
|
PlaySoundFrontend(-1, sound[1], sound[2], 0, 0, 1)
|
|
else
|
|
Error("Unknown sound:", soundKey)
|
|
end
|
|
|
|
cb("ok")
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- BLIPS
|
|
-- ============================================================
|
|
|
|
-- Supprime tous les blips de garage créés localement
|
|
local function clearBlips()
|
|
for _, blip in pairs(blipsList) do
|
|
RemoveBlip(blip)
|
|
end
|
|
blipsList = {}
|
|
end
|
|
|
|
-- Recrée les blips pour tous les garages visibles
|
|
function RefreshBlips()
|
|
if Config.DisableBlips then return end
|
|
|
|
clearBlips()
|
|
|
|
local playerIdentifier = GetPlayerIdentifier()
|
|
|
|
for garageId, garageData in pairs(Config.Garages) do
|
|
local isOwned = garageData.owner == playerIdentifier or garageData.owner
|
|
|
|
-- N'affiche pas les garages appartenant à quelqu'un d'autre
|
|
if not isOwned and garageData.owner then
|
|
-- Skip
|
|
else
|
|
local coords = garageData.coords.menuCoords
|
|
local blip = AddBlipForCoord(coords.x, coords.y, coords.z)
|
|
|
|
-- Type par défaut
|
|
if not garageData.type then garageData.type = "vehicle" end
|
|
|
|
-- Choix du sprite selon propriétaire ou non
|
|
local spriteConfig = Config.BlipSprites[garageData.type]
|
|
local sprite = (isOwned and spriteConfig.owned) or spriteConfig.notOwned
|
|
|
|
SetBlipSprite(blip, sprite)
|
|
SetBlipDisplay(blip, 4)
|
|
|
|
local blipSize = spriteConfig.size or 0.5
|
|
SetBlipScale(blip, blipSize)
|
|
|
|
-- Couleur
|
|
local blipColor = (isOwned and spriteConfig.color) or 37
|
|
|
|
-- Cas fourrière
|
|
if garageData.isImpound then
|
|
blipColor = Config.BlipSprites.impound.color
|
|
if Config.ShortNames then
|
|
garageId = garageId .. " " .. i18n.t("blip.impound")
|
|
end
|
|
end
|
|
|
|
-- Label selon le type si non possédé
|
|
local blipLabel = garageId
|
|
if not isOwned then
|
|
if garageData.type == "vehicle" then
|
|
blipLabel = i18n.t("blip.garage")
|
|
elseif garageData.type == "plane" then
|
|
blipLabel = i18n.t("blip.plane")
|
|
elseif garageData.type == "boat" then
|
|
blipLabel = i18n.t("blip.boat")
|
|
end
|
|
end
|
|
|
|
SetBlipColour(blip, blipColor)
|
|
SetBlipAsShortRange(blip, true)
|
|
|
|
BeginTextCommandSetBlipName("STRING")
|
|
AddTextComponentString(blipLabel or garageId)
|
|
EndTextCommandSetBlipName(blip)
|
|
|
|
if Config.ShortNames then
|
|
SetBlipCategory(blip, 10)
|
|
end
|
|
|
|
table.insert(blipsList, blip)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- ============================================================
|
|
-- BLIPS RÉCUPÉRATION (zone de récupération de véhicule)
|
|
-- ============================================================
|
|
|
|
CreateThread(function()
|
|
if not Config.Recovery.blip.active then return end
|
|
|
|
for _, coord in pairs(Config.Recovery.coords) do
|
|
local blip = AddBlipForCoord(coord.x, coord.y, coord.z)
|
|
local blipCfg = Config.Recovery.blip
|
|
|
|
SetBlipSprite(blip, blipCfg.sprite)
|
|
SetBlipDisplay(blip, 4)
|
|
SetBlipScale(blip, blipCfg.scale)
|
|
SetBlipColour(blip, blipCfg.color)
|
|
SetBlipAsShortRange(blip, true)
|
|
|
|
BeginTextCommandSetBlipName("STRING")
|
|
AddTextComponentString(blipCfg.name)
|
|
EndTextCommandSetBlipName(blip)
|
|
end
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- ZONES DE DÉTECTION (PolyZones / Sphères)
|
|
-- ============================================================
|
|
|
|
-- Détruit toutes les zones actives
|
|
local function clearPolyZones()
|
|
if not next(PolyZones) then return end
|
|
for _, zone in pairs(PolyZones) do
|
|
zone:remove()
|
|
end
|
|
PolyZones = {}
|
|
end
|
|
|
|
-- Recrée les zones de détection pour chaque garage
|
|
function RefreshPolyZones(debugMode)
|
|
clearPolyZones()
|
|
|
|
for garageId, garageData in pairs(Config.Garages) do
|
|
local zone = nil
|
|
|
|
local function onEnter()
|
|
ClosestGarage = garageId
|
|
IsGarageOwner = GetPlayerIdentifier() == garageData.owner
|
|
IsKeyHolder = HasKey()
|
|
nearbyGarageType = garageData.type
|
|
end
|
|
|
|
local function onExit()
|
|
if ClosestGarage == garageId then
|
|
ClosestGarage = nil
|
|
IsGarageOwner = false
|
|
end
|
|
end
|
|
|
|
local hasPolyPoints = garageData.zone and garageData.zone.points
|
|
|
|
if hasPolyPoints then
|
|
zone = lib.zones.poly({
|
|
points = garageData.zone.points,
|
|
thickness = garageData.zone.thickness,
|
|
debug = debugMode,
|
|
onEnter = onEnter,
|
|
onExit = onExit,
|
|
})
|
|
else
|
|
local radius = garageData.radius or Config.ZoneRadius
|
|
zone = lib.zones.sphere({
|
|
coords = garageData.coords.menuCoords,
|
|
radius = radius,
|
|
debug = debugMode,
|
|
onEnter = onEnter,
|
|
onExit = onExit,
|
|
})
|
|
end
|
|
|
|
PolyZones[garageId] = zone
|
|
end
|
|
end
|
|
|
|
-- ============================================================
|
|
-- FONCTIONS UTILITAIRES GARAGES
|
|
-- ============================================================
|
|
|
|
-- Vérifie si le joueur est près d'un JobGarage (distance < 25)
|
|
function IsNearbyJobGarage()
|
|
local playerPos = GetEntityCoords(cache.ped)
|
|
|
|
for _, garageData in pairs(Config.JobGarages) do
|
|
local spawnCoord = garageData.coords.spawnCoords
|
|
local dist = #(playerPos - vec3(spawnCoord.x, spawnCoord.y, spawnCoord.z))
|
|
if dist < 25.0 then
|
|
return garageData
|
|
end
|
|
end
|
|
|
|
return false
|
|
end
|
|
|
|
-- Arrondit un nombre à N décimales (ou à l'unité si N non fourni)
|
|
function MathRound(value, decimals)
|
|
if decimals then
|
|
local factor = 10 ^ decimals
|
|
return math.floor(value * factor + 0.5) / factor
|
|
else
|
|
return math.floor(value + 0.5)
|
|
end
|
|
end
|
|
|
|
-- Vérifie si le joueur est propriétaire ou détenteur de clé du garage donné
|
|
function HasKey(garageId)
|
|
garageId = garageId or ClosestGarage
|
|
local playerIdentifier = GetPlayerIdentifier()
|
|
|
|
if not garageId then return false end
|
|
|
|
local garageData = Config.Garages[garageId]
|
|
if not garageData then return false end
|
|
|
|
if garageData.owner == playerIdentifier then return true end
|
|
|
|
if not garageData.holders then return false end
|
|
|
|
return table.includes(garageData.holders, playerIdentifier)
|
|
end
|
|
|
|
-- ============================================================
|
|
-- REFRESH GLOBAL
|
|
-- ============================================================
|
|
|
|
-- Rafraîchit blips, zones et état du joueur vis-à-vis des garages
|
|
function RefreshGarages(resetPlayerState)
|
|
RefreshBlips()
|
|
RefreshPolyZones(Config.ZoneDebug)
|
|
|
|
local useTarget = Config.UseTarget
|
|
if useTarget and useTarget ~= "none" and useTarget ~= "qb-radialmenu" then
|
|
InitZones()
|
|
end
|
|
|
|
if resetPlayerState then
|
|
IsGarageOwner = false
|
|
IsKeyHolder = false
|
|
ClosestGarage = nil
|
|
return
|
|
end
|
|
|
|
IsKeyHolder = HasKey()
|
|
|
|
if IsGarageOwner then
|
|
local garageData = Config.Garages[ClosestGarage]
|
|
local playerIdent = GetPlayerIdentifier()
|
|
IsGarageOwner = garageData.owner == playerIdent
|
|
end
|
|
end
|
|
|
|
-- ============================================================
|
|
-- ÉVÉNEMENTS RÉSEAU : synchronisation des garages
|
|
-- ============================================================
|
|
|
|
RegisterNetEvent("garages:notification")
|
|
AddEventHandler("garages:notification", function(message, notifType)
|
|
Notification(message, notifType)
|
|
end)
|
|
|
|
RegisterNetEvent("garages:setGarages")
|
|
AddEventHandler("garages:setGarages", function(garagesData)
|
|
Debug("garages:setGarages", garagesData)
|
|
Config.Garages = garagesData
|
|
RefreshGarages()
|
|
end)
|
|
|
|
RegisterNetEvent("garages:createGarage")
|
|
AddEventHandler("garages:createGarage", function(garageData)
|
|
Debug("garages:createGarage", garageData)
|
|
Config.Garages[garageData.name] = garageData
|
|
RefreshGarages()
|
|
creator:updateUI()
|
|
end)
|
|
|
|
RegisterNetEvent("garages:updateGarage")
|
|
AddEventHandler("garages:updateGarage", function(garageData)
|
|
Debug("garages:updateGarage", garageData)
|
|
for garageId, existingData in pairs(Config.Garages) do
|
|
if existingData.name == garageData.name then
|
|
Config.Garages[garageId] = garageData
|
|
break
|
|
end
|
|
end
|
|
RefreshGarages()
|
|
creator:updateUI()
|
|
end)
|
|
|
|
RegisterNetEvent("garages:updateGaragePartial")
|
|
AddEventHandler("garages:updateGaragePartial", function(partialData)
|
|
Debug("garages:updateGaragePartial", partialData)
|
|
for garageId, existingData in pairs(Config.Garages) do
|
|
if garageId == partialData.name then
|
|
for key, value in pairs(partialData) do
|
|
existingData[key] = value
|
|
end
|
|
break
|
|
end
|
|
end
|
|
RefreshGarages()
|
|
creator:updateUI()
|
|
end)
|
|
|
|
RegisterNetEvent("garages:removeGarage")
|
|
AddEventHandler("garages:removeGarage", function(garageId)
|
|
Debug("garages:removeGarage", garageId)
|
|
Config.Garages[garageId] = nil
|
|
RefreshGarages(true)
|
|
creator:updateUI()
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- VÉHICULES : spawn, sauvegarde, propriétés
|
|
-- ============================================================
|
|
|
|
-- Spawne un véhicule réseau avec ses propriétés
|
|
function SpawnVehicle(vehicleData, spawnCoords)
|
|
lib.requestModel(vehicleData.model)
|
|
|
|
local vehicle = CreateVehicle(
|
|
vehicleData.model,
|
|
spawnCoords.x, spawnCoords.y, spawnCoords.z,
|
|
spawnCoords.w, 1, 0)
|
|
|
|
SetVehicleProperties(vehicle, vehicleData)
|
|
SpawnVehicleAlpha(vehicle)
|
|
return vehicle
|
|
end
|
|
|
|
-- Spawne un véhicule local (non réseau), supprime l'ancien si fourni
|
|
function LocalSpawnVehicle(vehicleData, spawnCoords, oldVehicle)
|
|
Debug("LocalSpawnVehicle:", vehicleData, spawnCoords, oldVehicle)
|
|
Wait(0)
|
|
|
|
if oldVehicle then
|
|
RemoveVehicle(oldVehicle)
|
|
Wait(125)
|
|
end
|
|
|
|
lib.requestModel(vehicleData.model)
|
|
|
|
local vehicle = CreateVehicle(
|
|
vehicleData.model,
|
|
spawnCoords.x, spawnCoords.y, spawnCoords.z,
|
|
spawnCoords.w, 0, 0)
|
|
|
|
if not vehicleData.plate then return vehicle end
|
|
|
|
SetVehicleProperties(vehicle, vehicleData, true)
|
|
SpawnVehicleAlpha(vehicle)
|
|
SpawnVehicleEvents(vehicle, vehicleData.plate)
|
|
return vehicle
|
|
end
|
|
|
|
-- Sauvegarde le véhicule dans le garage et le supprime du monde
|
|
function SaveVehicle(garageId, skipFadeIn, targetVehicle)
|
|
-- Vérifie les places disponibles dans le garage
|
|
local currentSlotCount = lib.callback.await("advancedgarages:getGarageSlots", false, garageId)
|
|
local maxSlots = 13
|
|
|
|
local garageData = Config.Garages[garageId]
|
|
local shellGarage = ShellGarages and ShellGarages[garageId]
|
|
|
|
if garageData then
|
|
local showRoom = Config.VehicleShowRooms[nearbyGarageType][garageData.shell.shell]
|
|
maxSlots = #showRoom.vehicleCoords
|
|
|
|
-- Gestion du prix de stockage
|
|
if garageData.storePrice and not garageData.isImpound then
|
|
local hasMoney = lib.callback.await("advancedgarages:existMoney", false, garageData.storePrice)
|
|
if not hasMoney then
|
|
Notification("You need $" .. garageData.storePrice .. " to store the vehicle", "error")
|
|
return
|
|
end
|
|
lib.callback.await("advancedgarages:removeMoney", false, garageData.storePrice)
|
|
Notification("You have been charged $" .. garageData.storePrice .. " to store the vehicle", "info")
|
|
end
|
|
elseif shellGarage then
|
|
local showRoom = Config.VehicleShowRooms[nearbyGarageType][shellGarage.shell.shell]
|
|
maxSlots = #showRoom.vehicleCoords
|
|
end
|
|
|
|
if currentSlotCount + 1 > maxSlots then
|
|
Notification(i18n.t("no_garage_slots"), "error")
|
|
return false
|
|
end
|
|
|
|
local vehicle = targetVehicle or cache.vehicle
|
|
|
|
-- Récupère les propriétés du véhicule
|
|
local vehicleProps = GetVehicleProperties(vehicle)
|
|
local deformations = {}
|
|
|
|
if GetResourceState("VehicleDeformation") == "started" then
|
|
deformations = exports.VehicleDeformation:GetVehicleDeformation(vehicle)
|
|
end
|
|
|
|
local plate = GetPlate(vehicle)
|
|
|
|
-- Vérifie la propriété du véhicule
|
|
local isOwned = lib.callback.await("advancedgarages:isVehicleOwned", false, plate)
|
|
if not isOwned then
|
|
return Notification(i18n.t("no_vehicle_owner"), "error")
|
|
end
|
|
|
|
SaveVehicleAlpha(vehicle)
|
|
|
|
-- Supprime les clés si configuré
|
|
if Config.Vehiclekeys and Config.Vehiclekeys ~= "none" then
|
|
local modelName = GetDisplayNameFromVehicleModel(GetEntityModel(vehicle))
|
|
RemoveVehiclekeys(modelName, plate)
|
|
Wait(150)
|
|
end
|
|
|
|
-- Supprime via AdvancedParking si actif
|
|
if GetResourceState("AdvancedParking") == "started" then
|
|
exports.AdvancedParking:DeleteVehicle(vehicle)
|
|
Debug("The [AdvancedParking] export was enabled to remove the vehicle in [SaveVehicle]")
|
|
end
|
|
|
|
-- Mise à jour de l'état véhicule (framework QB uniquement)
|
|
if Config.Framework == "qb" then
|
|
Debug("Vehicle goes to state 1 (SAVED)")
|
|
TriggerServerEvent("advancedgarages:server:updateVehicleState", 1, plate)
|
|
end
|
|
|
|
-- Sauvegarde en base
|
|
lib.callback.await("advancedgarages:saveVehicle", false, garageId, vehicleProps, deformations)
|
|
|
|
-- Supprime le véhicule du monde
|
|
RemoveVehicle(vehicle)
|
|
Wait(0)
|
|
|
|
-- Téléporte le joueur si c'était un bateau
|
|
local activeGarage = garageData or shellGarage
|
|
if activeGarage and activeGarage.type == "boat" then
|
|
local menuCoords = activeGarage.coords.menuCoords
|
|
SetEntityCoords(cache.ped,
|
|
menuCoords.x, menuCoords.y, menuCoords.z,
|
|
false, false, false, false)
|
|
end
|
|
|
|
if not skipFadeIn then
|
|
DoScreenFadeIn(950)
|
|
end
|
|
|
|
return true
|
|
end
|
|
|
|
-- ============================================================
|
|
-- PROPRIÉTÉS VÉHICULE
|
|
-- ============================================================
|
|
|
|
-- Applique les propriétés sauvegardées à un véhicule
|
|
function SetVehicleProperties(vehicle, props, skipNetworkWait)
|
|
if not DoesEntityExist(vehicle) then return end
|
|
|
|
if not skipNetworkWait then
|
|
local timeout = 5000
|
|
local startTime = GetGameTimer()
|
|
while not NetworkGetEntityIsNetworked(vehicle) do
|
|
if GetGameTimer() - startTime > timeout then
|
|
Debug("SetVehicleProperties timeout waiting for network entity")
|
|
break
|
|
end
|
|
Wait(50)
|
|
end
|
|
end
|
|
|
|
if not DoesEntityExist(vehicle) then return end
|
|
|
|
SetVehiclePropertiesFramework(vehicle, props)
|
|
SetVehicleEngineHealth(vehicle, (props.engineHealth and props.engineHealth + 0.0) or 1000.0)
|
|
SetVehicleBodyHealth(vehicle, (props.bodyHealth and props.bodyHealth + 0.0) or 1000.0)
|
|
SetVehicleDoorsLocked(vehicle, props.locked)
|
|
|
|
local modelName = GetDisplayNameFromVehicleModel(GetEntityModel(vehicle))
|
|
Debug("The specified vehicle is model: " ..
|
|
json.encode(modelName) ..
|
|
", use this model if you need to add addon name in config/vehicles.lua")
|
|
|
|
-- Carburant
|
|
if Config.Fuel and Config.Fuel ~= "none" then
|
|
SetFuel(vehicle, (props.fuelLevel and props.fuelLevel + 0.0) or 100.0)
|
|
else
|
|
props.fuelLevel = 100.0
|
|
Debug("You did not configure your fuel system and it is set to 100.0 in fuel, check in Config.Fuel")
|
|
end
|
|
|
|
-- Stancers (suspension réglable)
|
|
if props.stancers and GetResourceState("nfs-customs") == "started" then
|
|
return exports["nfs-customs"]:setVehicleStancers(vehicle, props.stancers)
|
|
end
|
|
|
|
-- Déformations
|
|
if props.deformations and GetResourceState("VehicleDeformation") == "started" then
|
|
return exports.VehicleDeformation:SetVehicleDeformation(vehicle, props.deformations)
|
|
end
|
|
end
|
|
|
|
-- Récupère les propriétés actuelles d'un véhicule
|
|
function GetVehicleProperties(vehicle)
|
|
if not DoesEntityExist(vehicle) then return end
|
|
|
|
local props = GetVehiclePropertiesFramework(vehicle)
|
|
props.engineHealth = GetVehicleEngineHealth(vehicle)
|
|
props.bodyHealth = GetVehicleBodyHealth(vehicle)
|
|
|
|
if Config.Fuel ~= "none" then
|
|
props.fuel = GetFuel(vehicle) or 100
|
|
end
|
|
|
|
-- Stancers (pcall car l'export peut ne pas exister)
|
|
local ok, stancers = pcall(function()
|
|
return exports["nfs-customs"]:getVehicleStancers(vehicle)
|
|
end)
|
|
if ok then props.stancers = stancers end
|
|
|
|
return props
|
|
end
|
|
|
|
-- ============================================================
|
|
-- INFORMATIONS VÉHICULE
|
|
-- ============================================================
|
|
|
|
-- Retourne le nom de classe du véhicule (string lisible)
|
|
function GetVehicleClassName(vehicle, fromModelName)
|
|
local classId = GetVehicleClass(vehicle)
|
|
if fromModelName then
|
|
classId = GetVehicleClassFromName(vehicle)
|
|
end
|
|
return Config.ClassList[classId] or "car"
|
|
end
|
|
|
|
-- Retourne l'image et le nom de marque d'un modèle de véhicule
|
|
function GetVehicleBrand(modelHash)
|
|
local defaultImage = Config.VehicleBrands.images.Default.image
|
|
local brandEntry = Config.VehicleBrands.brands[modelHash]
|
|
local brandName = brandEntry and brandEntry.brand
|
|
|
|
if not brandName then
|
|
return defaultImage, nil
|
|
end
|
|
|
|
local brandImage = Config.VehicleBrands.images[brandName]
|
|
brandImage = brandImage and brandImage.image
|
|
|
|
return brandImage or defaultImage, brandName
|
|
end
|
|
|
|
-- Retourne les statistiques complètes d'un véhicule
|
|
function GetVehicleStats(vehicle)
|
|
local modelHash = GetEntityModel(vehicle)
|
|
local acceleration = GetVehicleModelAcceleration(modelHash)
|
|
local brakeForce = GetVehicleHandlingFloat(vehicle, "CHandlingData", "fBrakeForce")
|
|
local maxFlatVel = GetVehicleHandlingFloat(vehicle, "CHandlingData", "fInitialDriveMaxFlatVel")
|
|
local topSpeedKmh = math.ceil(maxFlatVel * 1.3)
|
|
local tractionFront = GetVehicleHandlingFloat(vehicle, "CHandlingData", "fTractionBiasFront")
|
|
local tractionMax = GetVehicleHandlingFloat(vehicle, "CHandlingData", "fTractionCurveMax")
|
|
local tractionMin = GetVehicleHandlingFloat(vehicle, "CHandlingData", "fTractionCurveMin")
|
|
local engineHealth = GetVehicleEngineHealth(vehicle)
|
|
local bodyHealth = GetVehicleBodyHealth(vehicle)
|
|
local fuelLevel = GetVehicleFuelLevel(vehicle)
|
|
|
|
local traction = tractionFront + (tractionMax * tractionMin)
|
|
local name = GetVehicleName(vehicle)
|
|
local className = GetVehicleClassName(vehicle)
|
|
local plate = GetPlate(vehicle)
|
|
local logo, brand = GetVehicleBrand(modelHash)
|
|
|
|
return {
|
|
topSpeed = (topSpeedKmh / 300) * 100,
|
|
acceleration = acceleration * 150,
|
|
engineHealth = (engineHealth / 1000) * 100,
|
|
bodyHealth = (bodyHealth / 1000) * 100,
|
|
fuelLevel = fuelLevel,
|
|
brakes = brakeForce * 80,
|
|
traction = traction * 10,
|
|
name = name,
|
|
brand = brand or className,
|
|
logo = logo,
|
|
plate = plate,
|
|
}
|
|
end
|
|
|
|
-- Retourne la plaque d'immatriculation nettoyée (sans espaces)
|
|
function GetPlate(vehicle)
|
|
return tostring(MathTrim(GetVehicleNumberPlateText(vehicle)))
|
|
end
|
|
|
|
-- Retourne le nom lisible d'un véhicule (via son entité)
|
|
function GetVehicleName(vehicle)
|
|
local modelHash = GetEntityModel(vehicle)
|
|
local internalName = GetDisplayNameFromVehicleModel(modelHash)
|
|
return GetLabelText(internalName)
|
|
end
|
|
|
|
-- Retourne le nom lisible d'un véhicule depuis son modèle (hash ou string)
|
|
function GetVehicleNameFromModel(modelHash)
|
|
local internalName = GetDisplayNameFromVehicleModel(modelHash)
|
|
return GetLabelText(internalName)
|
|
end
|
|
|
|
-- ============================================================
|
|
-- SUPPRESSION VÉHICULE
|
|
-- ============================================================
|
|
|
|
-- Supprime proprement une entité véhicule (réseau ou locale)
|
|
function RemoveVehicle(vehicle)
|
|
if NetworkDoesNetworkIdExist(vehicle) then
|
|
local entity = NetworkGetEntityFromNetworkId(vehicle)
|
|
SetEntityAsMissionEntity(entity, false, true)
|
|
DeleteEntity(entity)
|
|
else
|
|
SetEntityAsMissionEntity(vehicle, false, true)
|
|
DeleteEntity(vehicle)
|
|
end
|
|
end
|
|
|
|
-- ============================================================
|
|
-- LABELS VÉHICULES ADDITIONNELS
|
|
-- ============================================================
|
|
|
|
CreateThread(function()
|
|
for label, name in pairs(Config.AddonVehiclesLabelList) do
|
|
AddTextEntry(label, name)
|
|
end
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- MISE EN FOURRIÈRE
|
|
-- ============================================================
|
|
|
|
-- Retourne la liste des IDs de garages-fourrière du type donné
|
|
local function getImpoundGaragesByType(vehicleType)
|
|
local result = {}
|
|
for garageId, garageData in pairs(Config.Garages) do
|
|
if garageData.isImpound and garageData.type == vehicleType then
|
|
table.insert(result, garageId)
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
-- Ouvre le menu de mise en fourrière pour le véhicule à proximité
|
|
function ImpoundVehicle()
|
|
local netId, vehicleType = lib.callback.await("advancedgarages:getNearbyVehicle", false)
|
|
|
|
if not netId then
|
|
return Notification(i18n.t("no_vehicle_nearby"), "error")
|
|
end
|
|
|
|
nearbyVehicleNetId = netId
|
|
local impoundGarages = getImpoundGaragesByType(vehicleType)
|
|
|
|
SendReactMessage("toggle_impound", {
|
|
visible = true,
|
|
impoundGarages = impoundGarages,
|
|
})
|
|
SetNuiFocus(true, true)
|
|
end
|
|
|
|
RegisterCommand("impound", function(source, args)
|
|
local jobName = GetJobName()
|
|
if not IsJobAllowed(jobName, "impound") then
|
|
return Notification(i18n.t("you_are_not_police"), "error")
|
|
end
|
|
ImpoundVehicle()
|
|
end, false)
|
|
|
|
exports("ImpoundVehicle", ImpoundVehicle)
|
|
|
|
-- ============================================================
|
|
-- EXPORTS
|
|
-- ============================================================
|
|
|
|
exports("GetGarages", function()
|
|
local garages = lib.callback.await("advancedgarages:getGarages", false)
|
|
return garages or {}
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- ACHAT DE GARAGE (mode sans target / qb-radialmenu uniquement)
|
|
-- ============================================================
|
|
|
|
local useTarget = Config.UseTarget
|
|
if not useTarget or useTarget == "none" or useTarget == "qb-radialmenu" then
|
|
CreateThread(function()
|
|
local function getBuyableGarageNearby()
|
|
if not ClosestGarage then return nil end
|
|
|
|
local garageData = Config.Garages[ClosestGarage]
|
|
if garageData.available then return nil end
|
|
if garageData.owner and garageData.owner ~= "" then return nil end
|
|
if not garageData.price then return nil end
|
|
|
|
local playerPos = GetEntityCoords(cache.ped)
|
|
local garagePos = vec3(
|
|
garageData.coords.menuCoords.x,
|
|
garageData.coords.menuCoords.y,
|
|
garageData.coords.menuCoords.z)
|
|
local dist = #(playerPos - garagePos)
|
|
|
|
if dist > 20.0 then return nil end
|
|
return garageData, dist
|
|
end
|
|
|
|
while true do
|
|
local garageData, dist = getBuyableGarageNearby()
|
|
|
|
if not garageData then
|
|
Wait(500)
|
|
else
|
|
if dist <= 2.0 then
|
|
local menuCoords = garageData.coords.menuCoords
|
|
DrawText3D(
|
|
menuCoords.x, menuCoords.y, menuCoords.z + 0.3,
|
|
i18n.t("buy_garage", { price = garageData.price }),
|
|
"buy_garage", "E")
|
|
|
|
if IsControlJustPressed(0, Keys.E) then
|
|
TriggerServerEvent("advancedgarages:buyGarage", ClosestGarage, garageData.price)
|
|
end
|
|
end
|
|
Wait(0)
|
|
end
|
|
end
|
|
end)
|
|
end
|
|
|
|
-- ============================================================
|
|
-- SHOWCASE DES SHELLS DE GARAGE (sélection de place de parking)
|
|
-- ============================================================
|
|
|
|
-- Affiche un environnement de prévisualisation pour choisir un shell de garage
|
|
function ShowcaseOfGarageShell(shellIndex, garageType)
|
|
selectingShell = true
|
|
shellIndex = shellIndex or 1
|
|
|
|
local showRooms = Config.VehicleShowRooms[garageType]
|
|
local shellData = showRooms[shellIndex]
|
|
local shellCoords = shellData.coords
|
|
|
|
-- Fondu au noir et téléportation
|
|
DoScreenFadeOut(300)
|
|
Wait(500)
|
|
|
|
if not shellCoords then return end
|
|
|
|
Wait(300)
|
|
SetEntityVisible(cache.ped, false, false)
|
|
SetEntityCoords(cache.ped,
|
|
shellCoords.x, shellCoords.y, shellCoords.z + 1.0,
|
|
false, false, false, false)
|
|
|
|
local shellRot = shellData.rotation or vec3(0.0, 0.0, 0.0)
|
|
local camCoords = vec3(shellCoords.x, shellCoords.y, shellCoords.z + 1.0)
|
|
|
|
local cam = Utils.CreateCamera("DEFAULT_SCRIPTED_CAMERA", camCoords, shellRot, true, nil, 0)
|
|
|
|
DoScreenFadeIn(200)
|
|
|
|
-- Retour aux coordonnées d'origine
|
|
local function exitShowcase()
|
|
DoScreenFadeOut(300)
|
|
Wait(500)
|
|
SetEntityCoords(cache.ped,
|
|
DefaultPlayerCoords.x, DefaultPlayerCoords.y, DefaultPlayerCoords.z,
|
|
false, false, false, false)
|
|
SetEntityVisible(cache.ped, true, false)
|
|
EnableAllControlActions(0)
|
|
Utils.DestroyFlyCam(cam)
|
|
Wait(1000)
|
|
DoScreenFadeIn(300)
|
|
selectingShell = false
|
|
return shellIndex
|
|
end
|
|
|
|
-- Pré-calcul des indices prev/next avec wrapping
|
|
local totalShells = #showRooms
|
|
local nextIndex = shellIndex < totalShells and shellIndex + 1 or 1
|
|
local prevIndex = shellIndex > 1 and shellIndex - 1 or totalShells
|
|
|
|
-- Construction de l'instructional (touches affichées à l'écran)
|
|
local controls = Utils.GetControls({ "arrow_left", "arrow_right", "done", "cancel" })
|
|
local slotsLabel = {
|
|
label = i18n.t("creator.available_slots", { name = shellData.name, slots = #shellData.vehicleCoords }),
|
|
codes = {},
|
|
}
|
|
table.insert(controls, slotsLabel)
|
|
local instructional = Utils.CreateInstructional(controls)
|
|
|
|
-- Boucle de sélection
|
|
while cam do
|
|
HandleFlyCam(cam, { mouse = true, keyboard = false, updatePlayerCoords = true })
|
|
|
|
-- Confirmer
|
|
if IsDisabledControlJustPressed(0, ActionControls.done.codes[1]) then
|
|
return exitShowcase()
|
|
end
|
|
|
|
-- Annuler
|
|
if IsDisabledControlPressed(0, ActionControls.cancel.codes[1]) then
|
|
DoScreenFadeOut(300)
|
|
Wait(1000)
|
|
Utils.DestroyFlyCam(cam)
|
|
SetEntityCoords(cache.ped,
|
|
DefaultPlayerCoords.x, DefaultPlayerCoords.y, DefaultPlayerCoords.z,
|
|
false, false, false, false)
|
|
SetEntityVisible(cache.ped, true, false)
|
|
Wait(500)
|
|
DoScreenFadeIn(300)
|
|
return nil
|
|
end
|
|
|
|
-- Shell précédent
|
|
if IsDisabledControlPressed(0, Keys.LEFT) then
|
|
DoScreenFadeOut(300)
|
|
Wait(500)
|
|
Utils.DestroyFlyCam(cam)
|
|
return ShowcaseOfGarageShell(prevIndex, garageType)
|
|
end
|
|
|
|
-- Shell suivant
|
|
if IsDisabledControlPressed(0, Keys.RIGHT) then
|
|
DoScreenFadeOut(300)
|
|
Wait(500)
|
|
Utils.DestroyFlyCam(cam)
|
|
return ShowcaseOfGarageShell(nextIndex, garageType)
|
|
end
|
|
|
|
DrawScaleform(instructional)
|
|
Wait(0)
|
|
end
|
|
end
|
|
|
|
-- Export : démarre le showcase depuis l'extérieur (sauvegarde les coords du joueur)
|
|
exports("ShowCaseOfGarageShell", function(shellIndex, garageType)
|
|
DefaultPlayerCoords = GetEntityCoords(cache.ped)
|
|
return ShowcaseOfGarageShell(shellIndex, garageType)
|
|
end)
|
|
|
|
-- ============================================================
|
|
-- DESSIN TEXTE 3D
|
|
-- ============================================================
|
|
|
|
-- Affiche un texte centré dans le monde 3D avec un fond semi-transparent
|
|
-- DrawText3Ds est conservé comme fallback simple si nécessaire
|
|
function DrawText3Ds(x, y, z, text)
|
|
SetTextScale(0.35, 0.35)
|
|
SetTextFont(4)
|
|
SetTextProportional(1)
|
|
SetTextColour(255, 255, 255, 215)
|
|
SetTextEntry("STRING")
|
|
SetTextCentre(true)
|
|
AddTextComponentString(text)
|
|
SetDrawOrigin(x, y, z, 0)
|
|
DrawText(0.0, 0.0)
|
|
|
|
local textWidth = #text / 370
|
|
DrawRect(0.0, 0.0125, 0.017 + textWidth, 0.03, 0, 0, 0, 75)
|
|
ClearDrawOrigin()
|
|
end
|
|
|