Files
KingMcDonalds c6bdd23cc8 adding vehicles
2025-11-30 16:24:12 -08:00

1283 lines
38 KiB
Lua
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
-----------------------------------------
-- FD DRONE SCRIPT - CLIENT <!-- FD Drone System Nov 2025 - Ferious Dev-->
-----------------------------------------
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) -- 8pm6am = 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)