298 lines
11 KiB
Lua
298 lines
11 KiB
Lua
|
|
local config = require 'config.client'
|
||
|
|
local FOV_MAX = 80.0
|
||
|
|
local FOV_MIN = 10.0 -- max zoom level (smaller fov is more zoom)
|
||
|
|
local ZOOM_SPEED = 2.0 -- camera zoom speed
|
||
|
|
local LR_SPEED = 3.0 -- speed by which the camera pans left-right
|
||
|
|
local UD_SPEED = 3.0 -- speed by which the camera pans up-down
|
||
|
|
local toggleHeliCam = 51 -- control id of the button by which to toggle the heliCam mode. Default: INPUT_CONTEXT (E)
|
||
|
|
local toggleVision = 25 -- control id to toggle vision mode. Default: INPUT_AIM (Right mouse btn)
|
||
|
|
local toggleRappel = 154 -- control id to rappel out of the heli. Default: INPUT_DUCK (X)
|
||
|
|
local toggleSpotlight = 74 -- control id to toggle the front spotlight Default: INPUT_VEH_HEADLIGHT (H)
|
||
|
|
local toggleLockOn = 22 -- control id to lock onto a vehicle with the camera. Default is INPUT_SPRINT (spacebar)
|
||
|
|
local spotlightState = false
|
||
|
|
local heliCam = false
|
||
|
|
local fov = (FOV_MAX + FOV_MIN) * 0.5
|
||
|
|
|
||
|
|
---@enum
|
||
|
|
local VISION_STATE = {
|
||
|
|
normal = 0,
|
||
|
|
nightmode = 1,
|
||
|
|
thermal = 2,
|
||
|
|
}
|
||
|
|
|
||
|
|
local visionState = VISION_STATE.normal
|
||
|
|
local scanValue = 0
|
||
|
|
|
||
|
|
---@enum
|
||
|
|
local VEHICLE_LOCK_STATE = {
|
||
|
|
dormant = 0,
|
||
|
|
scanning = 1,
|
||
|
|
locked = 2,
|
||
|
|
}
|
||
|
|
|
||
|
|
local vehicleLockState = VEHICLE_LOCK_STATE.dormant
|
||
|
|
local vehicleDetected = nil
|
||
|
|
local lockedOnVehicle = nil
|
||
|
|
|
||
|
|
local function isPlayerInPoliceHeli()
|
||
|
|
return GetEntityModel(cache.vehicle) == joaat(config.policeHelicopter)
|
||
|
|
end
|
||
|
|
|
||
|
|
local function isHeliHighEnough(heli)
|
||
|
|
return GetEntityHeightAboveGround(heli) > 1.5
|
||
|
|
end
|
||
|
|
|
||
|
|
local function changeVision()
|
||
|
|
PlaySoundFrontend(-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', false)
|
||
|
|
if visionState == VISION_STATE.normal then
|
||
|
|
SetNightvision(true)
|
||
|
|
elseif visionState == VISION_STATE.nightmode then
|
||
|
|
SetNightvision(false)
|
||
|
|
SetSeethrough(true)
|
||
|
|
elseif visionState == VISION_STATE.thermal then
|
||
|
|
SetSeethrough(false)
|
||
|
|
else
|
||
|
|
error('Unexpected visionState ' .. json.encode(visionState))
|
||
|
|
end
|
||
|
|
visionState = (visionState + 1) % 3
|
||
|
|
end
|
||
|
|
|
||
|
|
local function hideHudThisFrame()
|
||
|
|
HideHelpTextThisFrame()
|
||
|
|
HideHudAndRadarThisFrame()
|
||
|
|
|
||
|
|
local hudComponents = {1, 2, 3, 4, 13, 11, 12, 15, 18, 19}
|
||
|
|
for _, component in ipairs(hudComponents) do
|
||
|
|
HideHudComponentThisFrame(component)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
local function checkInputRotation(cam, zoomValue)
|
||
|
|
local rightAxisX = GetDisabledControlNormal(0, 220)
|
||
|
|
local rightAxisY = GetDisabledControlNormal(0, 221)
|
||
|
|
local rotation = GetCamRot(cam, 2)
|
||
|
|
if rightAxisX == 0.0 and rightAxisY == 0.0 then return end
|
||
|
|
|
||
|
|
local zoomFactor = zoomValue + 0.1
|
||
|
|
local newZ = rotation.z - rightAxisX * UD_SPEED * zoomFactor
|
||
|
|
local newY = rightAxisY * -1.0 * LR_SPEED * zoomFactor
|
||
|
|
local newX = math.max(math.min(20.0, rotation.x + newY), -89.5)
|
||
|
|
SetCamRot(cam, newX, 0.0, newZ, 2)
|
||
|
|
end
|
||
|
|
|
||
|
|
local function handleZoom(cam)
|
||
|
|
if IsControlJustPressed(0,241) then -- Scrollup
|
||
|
|
fov = math.max(fov - ZOOM_SPEED, FOV_MIN)
|
||
|
|
end
|
||
|
|
if IsControlJustPressed(0,242) then
|
||
|
|
fov = math.min(fov + ZOOM_SPEED, FOV_MAX) -- ScrollDown
|
||
|
|
end
|
||
|
|
local currentFov = GetCamFov(cam)
|
||
|
|
if math.abs(fov - currentFov) < 0.1 then -- the difference is too small, just set the value directly to avoid unneeded updates to FOV of order 10^-5
|
||
|
|
fov = currentFov
|
||
|
|
end
|
||
|
|
SetCamFov(cam, currentFov + (fov - currentFov) * 0.05) -- Smoothing of camera zoom
|
||
|
|
end
|
||
|
|
|
||
|
|
local function rotAnglesToVec(rot) -- input vector3
|
||
|
|
local z = math.rad(rot.z)
|
||
|
|
local x = math.rad(rot.x)
|
||
|
|
local num = math.abs(math.cos(x))
|
||
|
|
return vector3(-math.sin(z) * num, math.cos(z) * num, math.sin(x))
|
||
|
|
end
|
||
|
|
|
||
|
|
local function getVehicleInView(cam)
|
||
|
|
local coords = GetCamCoord(cam)
|
||
|
|
local forwardVector = coords + (rotAnglesToVec(GetCamRot(cam, 2)) * 400.0)
|
||
|
|
--DrawLine(coords, coords + (forward_vector * 100.0), 255, 0, 0, 255) -- debug line to show LOS of cam
|
||
|
|
local rayHandle = CastRayPointToPoint(coords.x, coords.y, coords.z, forwardVector.x, forwardVector.y, forwardVector.z, 10, cache.vehicle, 0)
|
||
|
|
local _, _, _, _, entityHit = GetRaycastResult(rayHandle)
|
||
|
|
return entityHit <= 0 and nil or IsEntityAVehicle(entityHit) and entityHit
|
||
|
|
end
|
||
|
|
|
||
|
|
local function renderVehicleInfo(vehicle)
|
||
|
|
local pos = GetEntityCoords(vehicle)
|
||
|
|
local model = GetEntityModel(vehicle)
|
||
|
|
local vehName = GetLabelText(GetDisplayNameFromVehicleModel(model))
|
||
|
|
local licensePlate = qbx.getVehiclePlate(vehicle)
|
||
|
|
local speed = math.ceil(GetEntitySpeed(vehicle) * 3.6)
|
||
|
|
local street1, street2 = GetStreetNameAtCoord(pos.x, pos.y, pos.z)
|
||
|
|
local streetLabel = GetStreetNameFromHashKey(street1)
|
||
|
|
if street2 ~= 0 then
|
||
|
|
streetLabel = streetLabel .. ' | ' .. GetStreetNameFromHashKey(street2)
|
||
|
|
end
|
||
|
|
SendNUIMessage({
|
||
|
|
type = 'heliupdateinfo',
|
||
|
|
model = vehName,
|
||
|
|
plate = licensePlate,
|
||
|
|
speed = speed,
|
||
|
|
street = streetLabel,
|
||
|
|
})
|
||
|
|
end
|
||
|
|
|
||
|
|
RegisterNetEvent('heli:spotlight', function(serverId, state)
|
||
|
|
SetVehicleSearchlight(GetVehiclePedIsIn(GetPlayerPed(GetPlayerFromServerId(serverId)), false), state, false)
|
||
|
|
end)
|
||
|
|
|
||
|
|
local function heliCamThread()
|
||
|
|
CreateThread(function()
|
||
|
|
local sleep
|
||
|
|
while heliCam do
|
||
|
|
sleep = 0
|
||
|
|
if vehicleLockState == VEHICLE_LOCK_STATE.scanning then
|
||
|
|
if scanValue < 100 then
|
||
|
|
scanValue += 1
|
||
|
|
SendNUIMessage({
|
||
|
|
type = 'heliscan',
|
||
|
|
scanvalue = scanValue,
|
||
|
|
})
|
||
|
|
if scanValue == 100 then
|
||
|
|
PlaySoundFrontend(-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', false)
|
||
|
|
lockedOnVehicle = vehicleDetected
|
||
|
|
vehicleLockState = VEHICLE_LOCK_STATE.locked
|
||
|
|
end
|
||
|
|
sleep = 10
|
||
|
|
end
|
||
|
|
elseif vehicleLockState == VEHICLE_LOCK_STATE.locked then
|
||
|
|
scanValue = 100
|
||
|
|
renderVehicleInfo(lockedOnVehicle)
|
||
|
|
sleep = 100
|
||
|
|
else
|
||
|
|
scanValue = 0
|
||
|
|
sleep = 500
|
||
|
|
end
|
||
|
|
Wait(sleep)
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
end
|
||
|
|
|
||
|
|
local function unlockCam(cam)
|
||
|
|
PlaySoundFrontend(-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', false)
|
||
|
|
lockedOnVehicle = nil
|
||
|
|
local rot = GetCamRot(cam, 2) -- All this because I can't seem to get the camera unlocked from the entity
|
||
|
|
fov = GetCamFov(cam)
|
||
|
|
local oldCam = cam
|
||
|
|
DestroyCam(oldCam, false)
|
||
|
|
local newCam = CreateCam('DEFAULT_SCRIPTED_FLY_CAMERA', true)
|
||
|
|
AttachCamToEntity(newCam, cache.vehicle, 0.0,0.0,-1.5, true)
|
||
|
|
SetCamRot(newCam, rot.x, rot.y, rot.z, 2)
|
||
|
|
SetCamFov(newCam, fov)
|
||
|
|
RenderScriptCams(true, false, 0, true, false)
|
||
|
|
vehicleLockState = VEHICLE_LOCK_STATE.dormant
|
||
|
|
scanValue = 0
|
||
|
|
SendNUIMessage({
|
||
|
|
type = 'disablescan',
|
||
|
|
})
|
||
|
|
return newCam
|
||
|
|
end
|
||
|
|
|
||
|
|
local function turnOffCam()
|
||
|
|
PlaySoundFrontend(-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', false)
|
||
|
|
heliCam = false
|
||
|
|
vehicleLockState = VEHICLE_LOCK_STATE.dormant
|
||
|
|
scanValue = 0
|
||
|
|
SendNUIMessage({
|
||
|
|
type = 'disablescan',
|
||
|
|
})
|
||
|
|
SendNUIMessage({
|
||
|
|
type = 'heliclose',
|
||
|
|
})
|
||
|
|
end
|
||
|
|
|
||
|
|
local function handleInVehicle()
|
||
|
|
if not LocalPlayer.state.isLoggedIn then return end
|
||
|
|
if QBX.PlayerData.job.type ~= 'leo' and not QBX.PlayerData.job.onduty then return end
|
||
|
|
if isHeliHighEnough(cache.vehicle) then
|
||
|
|
if IsControlJustPressed(0, toggleHeliCam) then -- Toggle Helicam
|
||
|
|
PlaySoundFrontend(-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', false)
|
||
|
|
heliCam = true
|
||
|
|
heliCamThread()
|
||
|
|
SendNUIMessage({
|
||
|
|
type = 'heliopen',
|
||
|
|
})
|
||
|
|
end
|
||
|
|
|
||
|
|
if IsControlJustPressed(0, toggleRappel) and (cache.seat == 1 or cache.seat == 2) then -- Initiate rappel
|
||
|
|
PlaySoundFrontend(-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', false)
|
||
|
|
TaskRappelFromHeli(cache.ped, 1)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
if IsControlJustPressed(0, toggleSpotlight) and (cache.seat == -1 or cache.seat == 0) then
|
||
|
|
spotlightState = not spotlightState
|
||
|
|
TriggerServerEvent('heli:spotlight', spotlightState)
|
||
|
|
PlaySoundFrontend(-1, 'SELECT', 'HUD_FRONTEND_DEFAULT_SOUNDSET', false)
|
||
|
|
end
|
||
|
|
|
||
|
|
if heliCam then
|
||
|
|
SetTimecycleModifier('heliGunCam')
|
||
|
|
SetTimecycleModifierStrength(0.3)
|
||
|
|
local scaleform = lib.requestScaleformMovie('HELI_CAM')
|
||
|
|
local cam = CreateCam('DEFAULT_SCRIPTED_FLY_CAMERA', true)
|
||
|
|
AttachCamToEntity(cam, cache.vehicle, 0.0,0.0,-1.5, true)
|
||
|
|
SetCamRot(cam, 0.0, 0.0, GetEntityHeading(cache.vehicle), 2)
|
||
|
|
SetCamFov(cam, fov)
|
||
|
|
RenderScriptCams(true, false, 0, true, false)
|
||
|
|
PushScaleformMovieFunction(scaleform, 'SET_CAM_LOGO')
|
||
|
|
PushScaleformMovieFunctionParameterInt(0) -- 0 for nothing, 1 for LSPD logo
|
||
|
|
PopScaleformMovieFunctionVoid()
|
||
|
|
lockedOnVehicle = nil
|
||
|
|
while heliCam and not IsEntityDead(cache.ped) and cache.vehicle and isHeliHighEnough(cache.vehicle) do
|
||
|
|
if IsControlJustPressed(0, toggleHeliCam) then -- Toggle Helicam
|
||
|
|
turnOffCam()
|
||
|
|
end
|
||
|
|
if IsControlJustPressed(0, toggleVision) then
|
||
|
|
changeVision()
|
||
|
|
end
|
||
|
|
local zoomValue = 0
|
||
|
|
if lockedOnVehicle then
|
||
|
|
if DoesEntityExist(lockedOnVehicle) then
|
||
|
|
|
||
|
|
PointCamAtEntity(cam, lockedOnVehicle, 0.0, 0.0, 0.0, true)
|
||
|
|
if IsControlJustPressed(0, toggleLockOn) then
|
||
|
|
cam = unlockCam(cam)
|
||
|
|
end
|
||
|
|
else
|
||
|
|
vehicleLockState = VEHICLE_LOCK_STATE.dormant
|
||
|
|
SendNUIMessage({
|
||
|
|
type = 'disablescan',
|
||
|
|
})
|
||
|
|
lockedOnVehicle = nil -- Cam will auto unlock when entity doesn't exist anyway
|
||
|
|
end
|
||
|
|
else
|
||
|
|
zoomValue = (1.0 / (FOV_MAX - FOV_MIN)) * (fov - FOV_MIN)
|
||
|
|
checkInputRotation(cam, zoomValue)
|
||
|
|
vehicleDetected = getVehicleInView(cam)
|
||
|
|
vehicleLockState = DoesEntityExist(vehicleDetected) and VEHICLE_LOCK_STATE.scanning or VEHICLE_LOCK_STATE.dormant
|
||
|
|
end
|
||
|
|
handleZoom(cam)
|
||
|
|
hideHudThisFrame()
|
||
|
|
PushScaleformMovieFunction(scaleform, 'SET_ALT_FOV_HEADING')
|
||
|
|
PushScaleformMovieFunctionParameterFloat(GetEntityCoords(cache.vehicle).z)
|
||
|
|
PushScaleformMovieFunctionParameterFloat(zoomValue)
|
||
|
|
PushScaleformMovieFunctionParameterFloat(GetCamRot(cam, 2).z)
|
||
|
|
PopScaleformMovieFunctionVoid()
|
||
|
|
DrawScaleformMovieFullscreen(scaleform, 255, 255, 255, 255, 0)
|
||
|
|
Wait(0)
|
||
|
|
end
|
||
|
|
heliCam = false
|
||
|
|
ClearTimecycleModifier()
|
||
|
|
fov = (FOV_MAX + FOV_MIN) * 0.5 -- reset to starting zoom level
|
||
|
|
RenderScriptCams(false, false, 0, true, false) -- Return to gameplay camera
|
||
|
|
SetScaleformMovieAsNoLongerNeeded(scaleform) -- Cleanly release the scaleform
|
||
|
|
DestroyCam(cam, false)
|
||
|
|
SetNightvision(false)
|
||
|
|
SetSeethrough(false)
|
||
|
|
end
|
||
|
|
end
|
||
|
|
|
||
|
|
AddEventHandler('ox_lib:cache:vehicle', function()
|
||
|
|
CreateThread(function()
|
||
|
|
if not isPlayerInPoliceHeli() then return end
|
||
|
|
while cache.vehicle do
|
||
|
|
handleInVehicle()
|
||
|
|
Wait(0)
|
||
|
|
end
|
||
|
|
end)
|
||
|
|
end)
|