2026-04-14 17:41:39 +02:00

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