-- ============================================================ -- 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