----------------------------------------- -- FD DRONE SCRIPT - CLIENT ----------------------------------------- local DroneState = { active = false, drone = nil, baseVeh = nil, basePos = nil, batteryEnd = 0 } -- Global single-drone state (from server) local GlobalDroneActive = false local GlobalDroneNetId = 0 local GlobalTruckNetId = 0 -- Framework / job local ESX, QBCore local PlayerJob = nil -- Spotlight local spotlightEnabled = false local spotlightPitch = Config.Spotlight.DefaultPitch local spotlightYaw = Config.Spotlight.DefaultYaw local spotlightDirty = false local lastSpotlightState = false local remoteSpotlights = {} -- ANPR local anprAutoEnabled = false local anprCurrent = { hasTarget = false, plate = "", model = "", distance = 0.0 } -- Viewer local feedActive = false local feedCam = nil local feedDrone = nil ------------------------------------------------- -- UTIL ------------------------------------------------- local function hash(model) return joaat(model) end local function RequestModelSync(modelHash) if not HasModelLoaded(modelHash) then RequestModel(modelHash) local timeout = GetGameTimer() + 5000 while not HasModelLoaded(modelHash) do if GetGameTimer() > timeout then return false end Wait(0) end end return true end local ALLOWED_HASHES = {} for _, name in ipairs(Config.AllowedCars) do table.insert(ALLOWED_HASHES, hash(name)) end local DRONE_HASH = hash(Config.DroneModel) local function IsAllowedVehicle(veh) if veh == 0 or not DoesEntityExist(veh) then return false end local model = GetEntityModel(veh) for _, h in ipairs(ALLOWED_HASHES) do if model == h then return true end end return false end local function FormatVehicleName(modelHash) local label = GetDisplayNameFromVehicleModel(modelHash) if label and label ~= "" then local text = GetLabelText(label) if text and text ~= "NULL" then return text end return label end return "Unknown" end local function RotationToDirection(rot) local radX = math.rad(rot.x) local radZ = math.rad(rot.z) local cosX = math.cos(radX) return vector3( -math.sin(radZ) * cosX, math.cos(radZ) * cosX, math.sin(radX) ) end local function GetVehicleInCamera(maxDistance) local camPos = GetGameplayCamCoord() local rot = GetGameplayCamRot(2) local dir = RotationToDirection(rot) local dest = camPos + dir * maxDistance local rayHandle = StartShapeTestRay( camPos.x, camPos.y, camPos.z, dest.x, dest.y, dest.z, 10, -- vehicles only PlayerPedId(), 7 ) local _, hit, _, _, ent = GetShapeTestResult(rayHandle) if hit == 1 and ent ~= 0 and IsEntityAVehicle(ent) then local vpos = GetEntityCoords(ent) local dpos = DroneState.drone and GetEntityCoords(DroneState.drone) or camPos local dist = #(vpos - dpos) return ent, dist end return nil, 0.0 end local function LoadAnimDict(dict) if HasAnimDictLoaded(dict) then return true end RequestAnimDict(dict) local timeout = GetGameTimer() + 5000 while not HasAnimDictLoaded(dict) do if GetGameTimer() > timeout then return false end Wait(0) end return true end ------------------------------------------------- -- FRAMEWORK / JOB DETECTION (ESX -> QB) ------------------------------------------------- local function RefreshJob() PlayerJob = nil if ESX and ESX.PlayerData and ESX.PlayerData.job then PlayerJob = ESX.PlayerData.job.name return end if QBCore and QBCore.Functions and QBCore.Functions.GetPlayerData then local pdata = QBCore.Functions.GetPlayerData() if pdata and pdata.job then PlayerJob = pdata.job.name end end end CreateThread(function() -- Try ESX pcall(function() TriggerEvent("esx:getSharedObject", function(obj) ESX = obj end) end) -- Try QBCore pcall(function() if not QBCore and exports["qb-core"] then QBCore = exports["qb-core"]:GetCoreObject() end end) Wait(2000) RefreshJob() end) -- ESX events RegisterNetEvent("esx:playerLoaded", function(xPlayer) ESX = ESX or {} ESX.PlayerData = xPlayer RefreshJob() end) RegisterNetEvent("esx:setJob", function(job) if ESX and ESX.PlayerData then ESX.PlayerData.job = job end RefreshJob() end) -- QB events RegisterNetEvent("QBCore:Client:OnPlayerLoaded", function() RefreshJob() end) RegisterNetEvent("QBCore:Client:OnJobUpdate", function(job) if job and job.name then PlayerJob = job.name else RefreshJob() end end) local function IsPlayerLEO() if not Config.Viewer.EnableJobCheck then return true end if not PlayerJob then return false end for _, j in ipairs(Config.Viewer.AllowedJobs or {}) do if PlayerJob == j then return true end end return false end ------------------------------------------------- -- GLOBAL ACTIVE DRONE STATE (FROM SERVER) ------------------------------------------------- RegisterNetEvent("fd_drone:updateActiveState", function(isActive, truckNet, droneNet) GlobalDroneActive = isActive or false GlobalTruckNetId = truckNet or 0 GlobalDroneNetId = droneNet or 0 if not GlobalDroneActive and feedActive then RenderScriptCams(false, true, 300, true, true) if feedCam then DestroyCam(feedCam, false) end feedActive = false feedCam = nil feedDrone = nil end end) CreateThread(function() Wait(1500) TriggerServerEvent("fd_drone:requestActiveState") end) ------------------------------------------------- -- SPOTLIGHT HELPERS ------------------------------------------------- local function GetSpotlightDirection(drone, pitch, yaw) local heading = GetEntityHeading(drone) + yaw local rh = math.rad(heading) local rp = math.rad(pitch) return vector3( -math.sin(rh) * math.cos(rp), math.cos(rh) * math.cos(rp), math.sin(rp) ) end ------------------------------------------------- -- DRONE STATE RESET ------------------------------------------------- local function ResetDroneState() DroneState.active = false DroneState.drone = nil DroneState.baseVeh = nil DroneState.basePos = nil DroneState.batteryEnd = 0 spotlightEnabled = false spotlightDirty = false lastSpotlightState = false remoteSpotlights = {} anprAutoEnabled = false anprCurrent.hasTarget = false anprCurrent.plate = "" anprCurrent.model = "" anprCurrent.distance = 0.0 end ------------------------------------------------- -- FIND TRUCK BEHIND PLAYER ------------------------------------------------- local function GetPlayerVehicleBehind() local ped = PlayerPedId() local pedPos = GetEntityCoords(ped) local handle, veh = FindFirstVehicle() local success repeat if IsAllowedVehicle(veh) then local trunkPos = GetOffsetFromEntityInWorldCoords( veh, Config.TrunkOffset.x, Config.TrunkOffset.y, Config.TrunkOffset.z ) if #(pedPos - trunkPos) <= Config.TrunkRadius then EndFindVehicle(handle) return veh, trunkPos end end success, veh = FindNextVehicle(handle) until not success EndFindVehicle(handle) return nil, nil end ------------------------------------------------- -- ANIMATION SEQUENCES (FAST DEPLOY / RETRIEVE) ------------------------------------------------- -- Auto-step TO 1m behind the trunk offset (AS2) local function DoTrunkApproachAndFace(baseVeh) local ped = PlayerPedId() -- trunk offset point local trunkPos = GetOffsetFromEntityInWorldCoords( baseVeh, Config.TrunkOffset.x, Config.TrunkOffset.y, Config.TrunkOffset.z ) -- 1 meter further back along vehicle rear (AS2) local behindPos = GetOffsetFromEntityInWorldCoords( baseVeh, Config.TrunkOffset.x, Config.TrunkOffset.y - 1.0, Config.TrunkOffset.z ) -- Walk toward behindPos TaskGoStraightToCoord(ped, behindPos.x, behindPos.y, behindPos.z, 1.0, -1, GetEntityHeading(baseVeh), 0.1) local timeout = GetGameTimer() + 4000 while #(GetEntityCoords(ped) - behindPos) > 0.7 and GetGameTimer() < timeout do Wait(0) end ClearPedTasksImmediately(ped) SetEntityCoordsNoOffset(ped, behindPos.x, behindPos.y, behindPos.z, false, false, false) SetEntityHeading(ped, GetEntityHeading(baseVeh)) end -- FAST DEPLOY local function PlayDeployAnimationSequence(baseVeh) local ped = PlayerPedId() DoTrunkApproachAndFace(baseVeh) -- Open trunk & show prop SetVehicleDoorOpen(baseVeh, 5, false, false) SetVehicleExtra(baseVeh, 7, false) -- show extra_7 LoadAnimDict("random@domestic") TaskPlayAnim(ped, "random@domestic", "pickup_low", 4.0, -4.0, 1200, 0, 0.0, false, false, false) -- NUI deploy sound SendNUIMessage({ action = "playSound", sound = "deploy", volume = 0.9 }) Wait(750) -- Hide prop (drone taken) SetVehicleExtra(baseVeh, 7, true) end -- FAST RETRIEVE local function PlayRetrieveAnimationSequence(baseVeh) if not DoesEntityExist(baseVeh) then return end local ped = PlayerPedId() DoTrunkApproachAndFace(baseVeh) SetVehicleDoorOpen(baseVeh, 5, false, false) SetVehicleExtra(baseVeh, 7, true) -- ensure hidden at start LoadAnimDict("random@domestic") TaskPlayAnim(ped, "random@domestic", "pickup_low", 4.0, -4.0, 1400, 0, 0.0, false, false, false) -- NUI pickup sound SendNUIMessage({ action = "playSound", sound = "pickup", volume = 0.9 }) Wait(900) -- Show prop back in trunk SetVehicleExtra(baseVeh, 7, false) Wait(200) ClearPedTasks(ped) SetVehicleDoorShut(baseVeh, 5, false) end ------------------------------------------------- -- SPAWN DRONE ------------------------------------------------- local function SpawnDroneFromVehicle(baseVeh) if DroneState.active then Config.Notify("~y~Drone already active.") return end if GlobalDroneActive then Config.Notify("~r~Another drone is already deployed.") return end if not RequestModelSync(DRONE_HASH) then Config.Notify("~r~Failed to load drone model.") return end local spawnPos = GetOffsetFromEntityInWorldCoords( baseVeh, Config.DroneSpawnOffset.x, Config.DroneSpawnOffset.y, Config.DroneSpawnOffset.z ) local drone = CreateVehicle( DRONE_HASH, spawnPos.x, spawnPos.y, spawnPos.z, GetEntityHeading(baseVeh), true, false ) if drone == 0 then Config.Notify("~r~Drone spawn failed.") return end SetVehicleOnGroundProperly(drone) local droneNet = NetworkGetNetworkIdFromEntity(drone) local truckNet = NetworkGetNetworkIdFromEntity(baseVeh) if droneNet ~= 0 then SetNetworkIdCanMigrate(droneNet, true) SetNetworkIdExistsOnAllMachines(droneNet, true) end -- Close trunk after deploy SetVehicleDoorShut(baseVeh, 5, false) TaskWarpPedIntoVehicle(PlayerPedId(), drone, -1) Wait(200) DroneState.active = true DroneState.drone = drone DroneState.baseVeh = baseVeh DroneState.basePos = GetEntityCoords(baseVeh) DroneState.batteryEnd = GetGameTimer() + Config.BatterySeconds * 1000 spotlightEnabled = false spotlightDirty = true lastSpotlightState = false remoteSpotlights = {} anprAutoEnabled = false anprCurrent.hasTarget = false TriggerServerEvent("fd_drone:setActiveState", true, truckNet, droneNet) Config.Notify("~g~Drone deployed.") end local function StartDroneDeployFlow(baseVeh) if DroneState.active then Config.Notify("~y~Drone already active.") return end if GlobalDroneActive then Config.Notify("~r~Another drone is already deployed.") return end PlayDeployAnimationSequence(baseVeh) SpawnDroneFromVehicle(baseVeh) end ------------------------------------------------- -- RETURN DRONE (SAFE) ------------------------------------------------- local function ReturnDrone(toBase) if not DroneState.active or not DoesEntityExist(DroneState.drone) then ResetDroneState() TriggerServerEvent("fd_drone:setActiveState", false, 0, 0) return end local ped = PlayerPedId() local drone = DroneState.drone local base = DroneState.baseVeh local destPos if toBase and base ~= nil and DoesEntityExist(base) then destPos = GetOffsetFromEntityInWorldCoords( base, Config.TrunkOffset.x, Config.TrunkOffset.y - 0.2, Config.TrunkOffset.z ) else destPos = GetEntityCoords(drone) + vector3(0.0, 0.0, 1.0) end -- Turn off spotlight network-wide if DoesEntityExist(drone) then local id = NetworkGetNetworkIdFromEntity(drone) TriggerServerEvent("fd_drone:syncSpotlight", id, { enabled = false }) end spotlightEnabled = false remoteSpotlights = {} anprAutoEnabled = false anprCurrent.hasTarget = false SetPedCoordsKeepVehicle(ped, destPos.x, destPos.y, destPos.z) ClearPedTasksImmediately(ped) DeleteVehicle(drone) ResetDroneState() TriggerServerEvent("fd_drone:setActiveState", false, 0, 0) if toBase and base ~= nil and DoesEntityExist(base) then PlayRetrieveAnimationSequence(base) else Config.Notify("~g~Drone recovered.") end end ------------------------------------------------- -- TRUNK E PROMPT ------------------------------------------------- CreateThread(function() while true do Wait(0) if not DroneState.active and not GlobalDroneActive then local veh, pos = GetPlayerVehicleBehind() if veh then SetTextFont(0) SetTextScale(0.35,0.35) SetTextColour(255,255,255,220) SetTextCentre(true) SetDrawOrigin(pos.x,pos.y,pos.z+0.3,0) BeginTextCommandDisplayText("STRING") AddTextComponentSubstringPlayerName("~g~E~w~ - Deploy Drone") EndTextCommandDisplayText(0.0,0.0) ClearDrawOrigin() if IsControlJustPressed(0, Config.KeyLaunch) then StartDroneDeployFlow(veh) end else Wait(250) end elseif DroneState.active then if IsControlJustPressed(0, Config.KeyReturn) then ReturnDrone(true) end else Wait(250) end end end) ------------------------------------------------- -- PREVENT F EXIT ------------------------------------------------- CreateThread(function() while true do if DroneState.active and DoesEntityExist(DroneState.drone) then DisableControlAction(0, 75, true) DisableControlAction(27, 75, true) if IsDisabledControlJustPressed(0, 75) then Config.Notify("~y~Drone manually exited.") ReturnDrone(true) end Wait(0) else Wait(300) end end end) ------------------------------------------------- -- BATTERY & RANGE CHECK ------------------------------------------------- CreateThread(function() while true do if DroneState.active and DoesEntityExist(DroneState.drone) then local now = GetGameTimer() if now >= DroneState.batteryEnd then Config.Notify("~r~Battery dead — returning.") ReturnDrone(true) end local pos = GetEntityCoords(DroneState.drone) local dist = #(pos - DroneState.basePos) if dist >= Config.MaxRange * Config.RangeHardKick then Config.Notify("~r~Drone lost (range).") ReturnDrone(true) elseif dist >= Config.MaxRange * Config.RangeSoftWarn then Config.Notify("~y~Drone nearing range limit.") end Wait(500) else Wait(800) end end end) ------------------------------------------------- -- VISION MODES (L) ------------------------------------------------- local visionMode = 0 CreateThread(function() while true do if DroneState.active and DoesEntityExist(DroneState.drone) then if IsControlJustPressed(0, 182) then -- L visionMode = (visionMode + 1) % 3 if visionMode == 0 then SetNightvision(false) SetSeethrough(false) Config.Notify("Vision: Normal") elseif visionMode == 1 then SetNightvision(true) SetSeethrough(false) Config.Notify("Vision: Night") elseif visionMode == 2 then SetNightvision(false) SetSeethrough(true) Config.Notify("Vision: Thermal") end end Wait(0) else Wait(400) end end end) ------------------------------------------------- -- HUD (BATTERY / ALT / SIGNAL / SPOTLIGHT / ANPR) ------------------------------------------------- CreateThread(function() while true do if DroneState.active and DoesEntityExist(DroneState.drone) then local drone = DroneState.drone local pos = GetEntityCoords(drone) local now = GetGameTimer() local secs = math.floor((DroneState.batteryEnd - now)/1000) if secs < 0 then secs = 0 end local _, ground = GetGroundZFor_3dCoord(pos.x,pos.y,pos.z,0) local alt = math.floor(pos.z - (ground or pos.z)) local dist = #(pos - DroneState.basePos) local bars = (dist > Config.MaxRange*0.9 and 1) or (dist > Config.MaxRange*0.7 and 2) or (dist > Config.MaxRange*0.4 and 3) or 4 local x = 0.015 local y = 0.63 -- ANPR (above battery) if anprAutoEnabled then SetTextFont(4) SetTextScale(0.40,0.40) SetTextOutline() SetTextColour(0,200,255,255) BeginTextCommandDisplayText("STRING") if anprCurrent.hasTarget then AddTextComponentString( string.format( "ANPR: ~w~%s (%s) [%.1fm]", anprCurrent.plate, anprCurrent.model, anprCurrent.distance ) ) else AddTextComponentString("ANPR: ~w~Scanning...") end EndTextCommandDisplayText(x, y - 0.030) end -- Battery SetTextFont(4) SetTextScale(0.40,0.40) SetTextOutline() SetTextColour(secs < 120 and 255 or 0, secs < 120 and 0 or 255, 0,255) BeginTextCommandDisplayText("STRING") AddTextComponentString("Battery: ~w~"..secs.."s") EndTextCommandDisplayText(x,y) -- Altitude SetTextFont(4) SetTextScale(0.55,0.55) SetTextOutline() SetTextColour(0,255,255,255) BeginTextCommandDisplayText("STRING") AddTextComponentString("Alt: ~w~"..alt.."m") EndTextCommandDisplayText(x,y+0.035) -- Spotlight indicator if spotlightEnabled then SetTextFont(4) SetTextScale(0.40,0.40) SetTextOutline() SetTextColour(0,255,0,255) BeginTextCommandDisplayText("STRING") AddTextComponentString("Spotlight: ~g~ON") EndTextCommandDisplayText(x,y+0.070) end -- Signal bars for i=1,4 do local px = x + (i*0.010) local py = y + 0.145 - (i*0.006) local ph = 0.018 + (i*0.006) if i <= bars then DrawRect(px,py,0.006,ph,0,255,0,255) else DrawRect(px,py,0.006,ph,80,80,80,150) end end Wait(0) else Wait(600) end end end) ------------------------------------------------- -- SPOTLIGHT CONTROLS (H + ARROWS + PAGEUP/PAGEDOWN) ------------------------------------------------- -- Toggle spotlight CreateThread(function() while true do if DroneState.active then if IsControlJustPressed(0, Config.Spotlight.EnabledKey) then spotlightEnabled = not spotlightEnabled spotlightDirty = true Config.Notify("Spotlight: "..(spotlightEnabled and "~g~ON" or "~r~OFF")) end Wait(0) else Wait(300) end end end) -- Rotate spotlight (vehicle control group = 2) CreateThread(function() while true do if DroneState.active and spotlightEnabled and DoesEntityExist(DroneState.drone) then local changed = false local spd = Config.Spotlight.RotationSpeed if IsControlPressed(2, Config.Spotlight.RotateLeft) then spotlightYaw = math.min(Config.Spotlight.MaxYawRight, spotlightYaw + spd) changed = true end if IsControlPressed(2, Config.Spotlight.RotateRight) then spotlightYaw = math.max(Config.Spotlight.MaxYawLeft, spotlightYaw - spd) changed = true end if IsControlPressed(2, Config.Spotlight.RotateUp) then spotlightPitch = math.min(Config.Spotlight.MaxPitchUp, spotlightPitch + spd) changed = true end if IsControlPressed(2, Config.Spotlight.RotateDown) then spotlightPitch = math.max(Config.Spotlight.MaxPitchDown, spotlightPitch - spd) changed = true end if changed then spotlightDirty = true end Wait(0) else Wait(300) end end end) -- Adjust cone width (PageUp/PageDown) CreateThread(function() while true do if DroneState.active and spotlightEnabled then local changed = false if IsControlPressed(0, Config.Spotlight.AdjustWider) then Config.Spotlight.LightRadius = math.min(25.0, Config.Spotlight.LightRadius + Config.Spotlight.AdjustStep) Config.Spotlight.LightIntensity = math.max(5.0, Config.Spotlight.LightIntensity - 0.3) changed = true end if IsControlPressed(0, Config.Spotlight.AdjustNarrow) then Config.Spotlight.LightRadius = math.max(1.0, Config.Spotlight.LightRadius - Config.Spotlight.AdjustStep) Config.Spotlight.LightIntensity = math.min(60.0, Config.Spotlight.LightIntensity + 0.3) changed = true end if changed then spotlightDirty = true end Wait(0) else Wait(300) end end end) -- Local spotlight draw CreateThread(function() while true do if DroneState.active and spotlightEnabled and DoesEntityExist(DroneState.drone) then local drone = DroneState.drone local pos = GetEntityCoords(drone) local dir = GetSpotlightDirection(drone, spotlightPitch, spotlightYaw) DrawSpotLight( pos.x,pos.y,pos.z, dir.x,dir.y,dir.z, 255,255,255, Config.Spotlight.LightRange, Config.Spotlight.LightIntensity, Config.Spotlight.LightFalloff, Config.Spotlight.LightRadius, 1.0 ) Wait(0) else Wait(400) end end end) -- Network sync sender CreateThread(function() while true do if DroneState.active and DoesEntityExist(DroneState.drone) then if spotlightDirty or (spotlightEnabled ~= lastSpotlightState) then local id = NetworkGetNetworkIdFromEntity(DroneState.drone) TriggerServerEvent("fd_drone:syncSpotlight", id, { enabled = spotlightEnabled, pitch = spotlightPitch, yaw = spotlightYaw, radius = Config.Spotlight.LightRadius, intensity = Config.Spotlight.LightIntensity }) spotlightDirty = false lastSpotlightState = spotlightEnabled end Wait(80) else spotlightDirty = false Wait(400) end end end) -- Network sync receiver (remote spotlights) RegisterNetEvent("fd_drone:applySpotlight", function(src, netId, data) if type(netId) ~= "number" then return end if DroneState.active and DoesEntityExist(DroneState.drone) then local my = NetworkGetNetworkIdFromEntity(DroneState.drone) if my == netId then return end end if not data.enabled then remoteSpotlights[netId] = nil return end remoteSpotlights[netId] = { pitch = data.pitch, yaw = data.yaw, radius = data.radius, intensity = data.intensity } end) -- Draw remote spotlights CreateThread(function() while true do if next(remoteSpotlights) ~= nil then for netId, s in pairs(remoteSpotlights) do local veh = NetToVeh(netId) if veh ~= 0 and DoesEntityExist(veh) then local pos = GetEntityCoords(veh) local dir = GetSpotlightDirection(veh, s.pitch, s.yaw) DrawSpotLight( pos.x,pos.y,pos.z, dir.x,dir.y,dir.z, 255,255,255, Config.Spotlight.LightRange, s.intensity, Config.Spotlight.LightFalloff, s.radius, 1.0 ) else remoteSpotlights[netId] = nil end end Wait(0) else Wait(500) end end end) ------------------------------------------------- -- ANPR COMMAND (/droneplateread) ------------------------------------------------- RegisterCommand(Config.ANPR.CommandName, function() if not DroneState.active then Config.Notify("~r~No active drone.") return end local veh, dist = GetVehicleInCamera(Config.ANPR.MaxDistance) if not veh then Config.Notify("~y~No vehicle in sight.") return end local rawPlate = GetVehicleNumberPlateText(veh) or "UNKNOWN" local plate = rawPlate:gsub("^%s*(.-)%s*$", "%1") local modelName = FormatVehicleName(GetEntityModel(veh)) Config.Notify(string.format( "[DRONE ANPR] Plate: ~g~%s~w~ | Model: ~g~%s~w~ | Dist: ~g~%.1fm", plate, modelName, dist )) end) ------------------------------------------------- -- AUTO ANPR TOGGLE (O) + LOOP ------------------------------------------------- local function ToggleAutoANPR() if not DroneState.active then Config.Notify("~r~No active drone.") return end anprAutoEnabled = not anprAutoEnabled if anprAutoEnabled then Config.Notify("~g~Auto ANPR: ON") else Config.Notify("~r~Auto ANPR: OFF") anprCurrent.hasTarget = false anprCurrent.plate = "" anprCurrent.model = "" anprCurrent.distance = 0.0 end end RegisterCommand("fd_drone_toggle_auto_anpr", function() ToggleAutoANPR() end, false) RegisterKeyMapping( "fd_drone_toggle_auto_anpr", "Drone Auto ANPR Toggle", "keyboard", "o" ) CreateThread(function() while true do if DroneState.active and anprAutoEnabled then local veh, dist = GetVehicleInCamera(Config.ANPR.MaxDistance) if veh then local raw = GetVehicleNumberPlateText(veh) or "UNKNOWN" local plate = raw:gsub("^%s*(.-)%s*$","%1") anprCurrent.hasTarget = true anprCurrent.plate = plate anprCurrent.model = FormatVehicleName(GetEntityModel(veh)) anprCurrent.distance = dist else anprCurrent.hasTarget = false end Wait(Config.ANPR.ScanIntervalMs) else Wait(500) end end end) ------------------------------------------------- -- DRONE FEED VIEWER (K) - LEO + NEAR TRUCK ONLY ------------------------------------------------- local function StopFeedViewer() if feedActive then RenderScriptCams(false, true, 300, true, true) if feedCam then DestroyCam(feedCam, false) end feedActive = false feedCam = nil feedDrone = nil Config.Notify("~r~Drone feed closed.") end end local function StartFeedViewer() -- Operator cannot view their own feed if DroneState.active then Config.Notify("~y~Viewer disabled for drone operator.") return end if not GlobalDroneActive or GlobalDroneNetId == 0 then Config.Notify("~y~No active drone to view.") return end if not IsPlayerLEO() then Config.Notify("~r~Access restricted to LEO.") return end local truck = NetToVeh(GlobalTruckNetId) if truck == 0 or not DoesEntityExist(truck) then Config.Notify("~r~Drone vehicle not available.") return end local pedPos = GetEntityCoords(PlayerPedId()) local truckPos = GetEntityCoords(truck) local dist = #(pedPos - truckPos) local maxDist = Config.Viewer.MaxDistance or 8.0 if dist > maxDist then Config.Notify(string.format("~y~Move closer to the drone vehicle (~g~%.0fm~w~ max).", maxDist)) return end local droneVeh = NetToVeh(GlobalDroneNetId) if droneVeh == 0 or not DoesEntityExist(droneVeh) then Config.Notify("~r~Drone not streamed in.") return end feedDrone = droneVeh feedCam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true) AttachCamToEntity(feedCam, feedDrone, 0.0, 0.0, 0.5, true) local rot = GetEntityRotation(feedDrone, 2) SetCamRot(feedCam, rot.x, rot.y, rot.z, 2) SetCamFov(feedCam, 60.0) RenderScriptCams(true, false, 0, true, true) feedActive = true Config.Notify("~g~Drone feed viewer: LIVE") end RegisterCommand("fd_drone_feed", function() if feedActive then StopFeedViewer() else StartFeedViewer() end end, false) RegisterKeyMapping( "fd_drone_feed", "Drone Feed Viewer", "keyboard", "k" ) CreateThread(function() while true do if feedActive and feedCam and DoesCamExist(feedCam) and feedDrone and DoesEntityExist(feedDrone) then local rot = GetEntityRotation(feedDrone, 2) SetCamRot(feedCam, rot.x, rot.y, rot.z, 2) SetTextFont(4) SetTextScale(0.35,0.35) SetTextOutline() SetTextColour(0,255,0,255) BeginTextCommandDisplayText("STRING") AddTextComponentString("DRONE FEED ~r~LIVE") EndTextCommandDisplayText(0.78,0.06) SetTextFont(0) SetTextScale(0.30,0.30) SetTextOutline() SetTextColour(255,255,255,200) BeginTextCommandDisplayText("STRING") AddTextComponentString("Press ~g~K~w~ to close") EndTextCommandDisplayText(0.78,0.09) Wait(0) else if feedActive then StopFeedViewer() end Wait(500) end end end) ------------------------------------------------- -- LED SYSTEM (DYNAMIC "FAKE" LED LIGHTS) -- Single belly LED, tactical white / blue / red -- Distance + day/night scaled ------------------------------------------------- -- Helper: get the current drone entity for LED drawing local function GetLEDDroneEntity() -- Local operator has priority if DroneState.active and DoesEntityExist(DroneState.drone) then return DroneState.drone, true end -- Otherwise use global (for viewers / bystanders) if GlobalDroneActive and GlobalDroneNetId ~= 0 then local veh = NetToVeh(GlobalDroneNetId) if veh ~= 0 and DoesEntityExist(veh) then return veh, false end end return nil, false end local function IsNightTime() local h = GetClockHours() return (h >= 20 or h < 6) -- 8pm–6am = night end -- Simple blink helper (0 or 1) local function BlinkPhase(nowMs, period, duty) local phase = nowMs % period return (phase / period) < duty and 1.0 or 0.0 end CreateThread(function() -- Single belly LED local ledOffset = vector3(0.0, 0.0, -0.08) while true do local drone, isLocal = GetLEDDroneEntity() if drone then local pedPos = GetEntityCoords(PlayerPedId()) local dPos = GetEntityCoords(drone) local dist = #(pedPos - dPos) -- Distance-based intensity (mode C) local distFactor if dist <= 50.0 then distFactor = 1.0 elseif dist <= 150.0 then distFactor = 1.0 - ((dist - 50.0) / 100.0) else distFactor = 0.0 end if distFactor > 0.0 then local nowMs = GetGameTimer() local isNight = IsNightTime() -- Base intensity: much lower now local baseNight = 4.0 -- decent at night, not overpowering local baseDay = 0.35 -- almost invisible in full daylight local baseIntensity = isNight and baseNight or baseDay local r,g,b = 255,255,255 local blinkFactor = 0.0 local showLED = false if isLocal and DroneState.active and DoesEntityExist(DroneState.drone) then showLED = true -- Battery state local secs = 0 if DroneState.batteryEnd > 0 then secs = math.floor((DroneState.batteryEnd - nowMs)/1000) if secs < 0 then secs = 0 end end local lowThreshold = math.floor(Config.BatterySeconds * 0.20) local criticalThreshold = math.floor(Config.BatterySeconds * 0.10) local isCritical = (secs > 0 and secs <= criticalThreshold) local isLow = (secs > criticalThreshold and secs <= lowThreshold) if isCritical then -- Critical: fast red blink r,g,b = 255, 0, 0 blinkFactor = BlinkPhase(nowMs, 350, 0.45) -- quick elseif isLow then -- Low battery: slower red blink r,g,b = 255, 0, 0 blinkFactor = BlinkPhase(nowMs, 900, 0.50) else -- Normal ops: tactical logic if spotlightEnabled then -- Spotlight priority: steady bright white r,g,b = 255,255,255 blinkFactor = 1.0 elseif anprAutoEnabled then -- ANPR: steady soft blue r,g,b = 0, 160, 255 blinkFactor = 0.35 -- dimmer blue else -- Normal flight: white double-strobe (FAA-style) -- flash (100ms) → gap (80ms) → flash (100ms) → pause (~800ms) local period = 1200 local phase = nowMs % period if phase < 100 then blinkFactor = 1.0 elseif phase < 180 then blinkFactor = 0.0 elseif phase < 280 then blinkFactor = 1.0 else blinkFactor = 0.0 end r,g,b = 255,255,255 end end else -- Non-local clients: simple, dim white strobe when drone active if GlobalDroneActive then showLED = true r,g,b = 230, 230, 255 baseIntensity = isNight and 3.0 or 0.75 -- slightly dimmer for remote local period = 1200 local phase = nowMs % period if phase < 100 or (phase > 180 and phase < 280) then blinkFactor = 1.0 else blinkFactor = 0.0 end end end if showLED and blinkFactor > 0.0 and baseIntensity > 0.0 then local tinyRadius = 0.6 -- very small LED radius local finalIntensity = (baseIntensity * 0.35) * distFactor * blinkFactor local wPos = GetOffsetFromEntityInWorldCoords( drone, ledOffset.x, ledOffset.y, ledOffset.z ) DrawLightWithRange( wPos.x, wPos.y, wPos.z, r, g, b, tinyRadius, -- small LED pool finalIntensity -- scaled brightness ) end end Wait(0) else Wait(600) end end end)