Main/resources/[jobs]/[police]/loaf_spikestrips/client/client.lua
2025-06-07 08:51:21 +02:00

539 lines
14 KiB
Lua

---@alias stinger { id: string, netId?: number, entity?: number, coords: vector3, rotation: vector3, minOffset: vector3, maxOffset: vector3, point?: table, blip?: number }
---@type {string: stinger }
local stingers = lib.callback.await("loaf_spikestrips:getSpikestrips")
---@type stinger[]
local nearbyStingers = {}
local nearbyCount = 0
---@type { number: boolean }
local targettableEntities = {}
local bones = Config.Bones
local placing
local stingersTick
local pickupKeyHelp
local currentStinger
local function placeStinger()
local playerPed = cache.ped
local offset = GetOffsetFromEntityInWorldCoords(playerPed, -0.2, 2.0, 0.0)
local heading = GetEntityHeading(playerPed)
local onFoot = IsPedOnFoot(playerPed)
local skipAnimation = false
local stinger
local netId
if not onFoot and cache.vehicle then
local model = GetEntityModel(cache.vehicle)
local min = model and GetModelDimensions(model) or { y = -2.5 }
offset = GetOffsetFromEntityInWorldCoords(cache.vehicle, 0.0, min.y, 0.0)
heading -= 90
skipAnimation = true
end
if not Config.AllowFromVehicle and not onFoot then
Notify(L("cant_vehicle"), "error")
return
end
if Config.OnlyRoads then
if not IsPointOnRoad(offset.x, offset.y, offset.z, 0) then
Notify(L("only_roads"), "error")
return
end
end
if Config.SpawnMethod == "server" then
netId = lib.callback.await("loaf_spikestrips:createSpikestrip", false, offset)
if not netId then
return
end
stinger = WaitForControlAndNetId(netId)
else
local allowedPlace = lib.callback.await("loaf_spikestrips:startPlacing")
if not allowedPlace then
return
end
end
local playerDict = LoadDict("amb@medic@standing@kneel@enter")
local stingerDict = LoadDict("p_ld_stinger_s")
local model = LoadModel(`p_ld_stinger_s`)
if Config.SpawnMethod == "local" then
stinger = CreateObject(model, offset.x, offset.y, offset.z, false, false, false)
placing = stinger
elseif Config.SpawnMethod == "networked" then
stinger = CreateObject(model, offset.x, offset.y, offset.z, true, true, false)
netId = NetworkGetNetworkIdFromEntity(stinger)
end
FreezeEntityPosition(stinger, true)
SetEntityVisible(stinger, false, false)
SetEntityCoordsNoOffset(stinger, offset.x, offset.y, offset.z, true, true, true)
SetEntityHeading(stinger, heading)
PlaceObjectOnGroundProperly(stinger)
local coords = GetEntityCoords(stinger)
local minOffset = GetOffsetFromEntityInWorldCoords(stinger, 0.0, -1.84, -0.1)
local maxOffset = GetOffsetFromEntityInWorldCoords(stinger, 0.0, 1.84, 0.1)
TriggerServerEvent("loaf_spikestrips:placedSpikestrip", coords, GetEntityRotation(stinger, 2), minOffset, maxOffset, netId)
if not skipAnimation then
-- Player deploying animation
TaskPlayAnim(playerPed, playerDict, "enter", 1000.0, -1.0, 200, 16, 0, false, false, false)
WaitForAnimation(playerPed, playerDict, "enter")
SetAnimRate(playerPed, 3.0, 1.0, false)
Wait(550)
end
-- Stinger animation
PlayEntityAnim(stinger, "p_stinger_s_deploy", stingerDict, 1000.0, false, true, false, 0.0, 0)
WaitForAnimation(stinger, stingerDict, "p_stinger_s_deploy")
SetEntityVisible(stinger, true, false)
PlayDeployAudio(stinger)
-- Cleanup
RemoveAnimDict(playerDict)
RemoveAnimDict(stingerDict)
SetModelAsNoLongerNeeded(model)
end
RegisterNetEvent("loaf_spikestrips:placeSpikestrip", placeStinger)
RegisterNetEvent("loaf_spikestrips:removeSpikestrip", function(data)
local entity = currentStinger or data?.entity
local stingerId
if not entity or not IsPedOnFoot(cache.ped) then
return
end
for i = 1, nearbyCount do
local stinger = nearbyStingers[i]
if stinger?.entity == entity then
stingerId = stinger.id
break
end
end
if not stingerId then
return
end
local dict = LoadDict("pickup_object")
TaskPlayAnim(cache.ped, dict, "pickup_low", 8.0, -8.0, -1, 48, 0, false, false, false)
Wait(500)
TriggerServerEvent("loaf_spikestrips:removeSpikestrip", stingerId)
RemoveAnimDict(dict)
end)
RegisterNetEvent("loaf_spikestrips:spikestripAdded", function(placer, id, coords, rotation, minOffset, maxOffset, netId)
local entity = netId and NetworkDoesNetworkIdExist(netId) and NetworkGetEntityFromNetworkId(netId) or nil
if Config.SpawnMethod == "local" and placer ~= cache.serverId and #(GetEntityCoords(cache.ped) - coords) < 150.0 then
Wait(550)
local stingerDict = LoadDict("p_ld_stinger_s")
local model = LoadModel(`p_ld_stinger_s`)
local stinger = CreateObject(model, coords.x, coords.y, coords.z, false, false, false)
FreezeEntityPosition(stinger, true)
PlaceObjectOnGroundProperly(stinger)
SetEntityRotation(stinger, rotation.x, rotation.y, rotation.z, 2, true)
PlayEntityAnim(stinger, "p_stinger_s_deploy", stingerDict, 1000.0, false, true, false, 0.0, 0)
WaitForAnimation(stinger, stingerDict, "p_stinger_s_deploy")
RemoveAnimDict(stingerDict)
SetModelAsNoLongerNeeded(model)
entity = stinger
elseif Config.SpawnMethod == "local" and placer == cache.serverId then
entity = placing
placing = nil
end
local point
if entity and Config.InteractStyle == "target" then
targettableEntities[entity] = true
elseif Config.InteractStyle == "native" then
point = lib.points.new({
coords = coords,
distance = 2
})
function point:onEnter()
currentStinger = stingers[id]?.entity
ShowHelpText("PICK_UP_STINGER")
end
function point:onExit()
currentStinger = nil
ClearHelpText()
end
end
stingers[id] = {
id = id,
netId = netId,
entity = entity,
coords = coords,
rotation = rotation,
minOffset = minOffset,
maxOffset = maxOffset,
point = point,
}
if Config.Blips and IsPolice() then
stingers[id].blip = CreateStingerBlip(coords)
end
if Config.RemoveDistance and placer == cache.serverId then
while #(GetEntityCoords(cache.ped) - coords) < Config.RemoveDistance and stingers[id] do
Wait(1000)
end
if stingers[id] then
debugprint("remove due to distance")
TriggerServerEvent("loaf_spikestrips:removeSpikestrip", id, true)
end
end
end)
RegisterNetEvent("loaf_spikestrips:updateNetId", function(id, netId)
if not stingers[id] then
return
end
stingers[id].netId = netId
debugprint("updated netId for stinger", id, "to", netId)
end)
RegisterNetEvent("loaf_spikestrips:spikestripRemoved", function(id)
local stinger = stingers[id]
if not stinger then
debugprint("can't remove: doesn't exist")
return
end
if stinger.point then
stinger.point:onExit()
stinger.point:remove()
end
if stinger.blip then
RemoveBlip(stinger.blip)
end
if Config.SpawnMethod == "local" and stinger.entity then
if Config.InteractStyle == "target" and targettableEntities[stinger.entity] then
targettableEntities[stinger.entity] = nil
end
DeleteEntity(stinger.entity)
end
stingers[id] = nil
end)
local function handleTouching(minOffset, maxOffset, vehicle)
for i = 1, #bones do
local bone = bones[i]
local boneIndex = GetEntityBoneIndexByName(vehicle, bone.bone)
if boneIndex == -1 or IsVehicleTyreBurst(vehicle, bone.index, false) then
goto nextBone
end
local boneCoords = GetWorldPositionOfEntityBone(vehicle, boneIndex)
local wheelTouching = IsPointInAngledArea(
boneCoords.x, boneCoords.y, boneCoords.z,
minOffset.x, minOffset.y, minOffset.z,
maxOffset.x, maxOffset.y, maxOffset.z,
0.45, false, false
)
if wheelTouching then
SetVehicleTyreBurst(vehicle, bone.index, wheelTouching, 1000.0)
end
::nextBone::
end
end
local function processStingers()
local vehicle = cache.vehicle
if not vehicle and not Config.BurstNPC then
return
end
local vehicleCoords = vehicle and GetEntityCoords(vehicle)
for i = 1, nearbyCount do
local stinger = nearbyStingers[i]
local stingerCoords = stinger?.coords
local stingerEntity = stinger?.entity
local minOffset = stinger?.minOffset
local maxOffset = stinger?.maxOffset
if not stingerCoords or not stingerEntity or not minOffset or not maxOffset then
goto continue
end
if vehicle and #(vehicleCoords - stingerCoords) < 10.0 and IsEntityTouchingEntity(stingerEntity, vehicle) then
handleTouching(minOffset, maxOffset, vehicle)
end
if not Config.BurstNPC then
goto continue
end
local closestVehicle = GetClosestVehicle(stingerCoords.x, stingerCoords.y, stingerCoords.z, 10.0, 0, 39)
if closestVehicle ~= 0 and closestVehicle ~= vehicle and NetworkHasControlOfEntity(closestVehicle) and IsEntityTouchingEntity(stingerEntity, closestVehicle) then
handleTouching(minOffset, maxOffset, closestVehicle)
end
::continue::
end
end
CreateThread(function()
while true do
if nearbyCount ~= 0 then
table.wipe(nearbyStingers)
nearbyCount = 0
end
local coords = GetEntityCoords(cache.ped)
for id, stinger in pairs(stingers) do
local distance = #(coords - stinger.coords)
if not Config.BurstNPC and distance > 100.0 then
goto continue
end
if not stinger.entity or not DoesEntityExist(stinger.entity) then
if Config.InteractStyle == "target" and stinger.entity then
targettableEntities[stinger.entity] = nil
end
if stinger.netId and NetworkDoesNetworkIdExist(stinger.netId) then
stinger.entity = NetworkGetEntityFromNetworkId(stinger.netId)
elseif Config.SpawnMethod == "local" and distance < 150.0 then
local model = LoadModel(`p_ld_stinger_s`)
stinger.entity = CreateObjectNoOffset(model, stinger.coords.x, stinger.coords.y, stinger.coords.z, false, false, false)
FreezeEntityPosition(stinger.entity, true)
PlaceObjectOnGroundProperly(stinger.entity)
SetEntityRotation(stinger.entity, stinger.rotation.x, stinger.rotation.y, stinger.rotation.z, 2, true)
SetModelAsNoLongerNeeded(model)
debugprint("created local entity for stinger", id)
end
if Config.InteractStyle == "target" and stinger.entity then
targettableEntities[stinger.entity] = true
end
end
if stinger.entity and DoesEntityExist(stinger.entity) then
if Config.SpawnMethod == "server" and #(GetEntityRotation(stinger.entity, 2) - stinger.rotation) > 1.0 then
debugprint("rotation out of sync, updating", id)
SetEntityRotation(stinger.entity, stinger.rotation.x, stinger.rotation.y, stinger.rotation.z, 2, true)
end
nearbyCount += 1
nearbyStingers[nearbyCount] = stinger
end
::continue::
end
if nearbyCount > 0 and (cache.seat == -1 or Config.BurstNPC) then
if not stingersTick then
stingersTick = SetInterval(processStingers)
end
elseif stingersTick then
stingersTick = ClearInterval(stingersTick)
end
Wait(250)
end
end)
if Config.InteractStyle == "target" then
exports.qtarget:AddTargetModel({ `p_ld_stinger_s` }, {
distance = 3.0,
options = {
{
event = "loaf_spikestrips:removeSpikestrip",
icon = "fa-solid fa-road-spikes",
label = L("remove_spikestrip"),
canInteract = function(entity)
if Config.Job.RequireRemove then
if not IsPolice then
infoprint("error", "IsPolice function not defined")
return
elseif not IsPolice() then
return
end
end
if not IsPedOnFoot(cache.ped) then
return
end
return targettableEntities[entity]
end
}
},
})
elseif Config.InteractStyle == "native" then
local keyBind = Config.PickupKey
local pickupKey = lib.addKeyBind({
name = "pickup_spikestirp",
description = L("keybind_description"),
defaultKey = keyBind.key,
defaultMapper = keyBind.mapper,
secondaryKey = keyBind.secondaryKey,
secondaryMapper = keyBind.secondaryMapper,
onReleased = function()
if currentStinger then
TriggerEvent("loaf_spikestrips:removeSpikestrip")
end
end
})
local hex = string.upper(string.format("%x", pickupKey.hash))
if pickupKey.hash < 0 then
hex = string.gsub(hex, string.rep("F", 8), "")
end
pickupKeyHelp = "~INPUT_" .. hex .. "~"
AddTextEntry("PICK_UP_STINGER", L("remove_spikestrip_help", {
key = pickupKeyHelp
}))
end
if Config.Command then
RegisterCommand(Config.Command, function()
placeStinger()
end, false)
TriggerEvent("chat:addSuggestion", "/" .. Config.Command, L("place_description"), {})
end
local function refreshBlips(isPolice)
for _, stinger in pairs(stingers) do
if Config.Blips and isPolice and not stinger.blip then
stinger.blip = CreateStingerBlip(stinger.coords)
elseif stinger.blip then
RemoveBlip(stinger.blip)
stinger.blip = nil
end
end
end
if Config.Blips and Config.BlipsCommand then
RegisterCommand(Config.BlipsCommand, function()
local isPolice = IsPolice()
if not isPolice then
return debugprint("not police")
end
Config.Blips = not Config.Blips
refreshBlips(isPolice)
end, false)
end
AddEventHandler("loaf_spikestrips:toggleIsPolice", function(isPolice)
if not Config.Blips then
return
end
refreshBlips(isPolice)
end)
AddTextEntry("SPIKESTRIP_BLIP", L("blip_name"))
AddEventHandler("onResourceStop", function(resource)
if resource ~= GetCurrentResourceName() then
return
end
for _, stinger in pairs(stingers) do
if stinger.blip then
RemoveBlip(stinger.blip)
end
if stinger.entity then
DeleteEntity(stinger.entity)
end
end
if Config.Command then
TriggerEvent("chat:removeSuggestion", "/" .. Config.Command)
end
end)
-- Blockiere Kamera- und Bewegungssteuerung vollständig während ox_target offen ist
CreateThread(function()
while true do
Wait(0)
-- Prüfe ob ALT gedrückt wird (Standard-Key für ox_target: INPUT_CHARACTER_WHEEL = 19)
local altPressed = IsControlPressed(0, 19)
local cursorVisible = IsControlPressed(0, 200) or IsControlPressed(0, 202) -- Beispiel: ESC oder Back
if altPressed then
-- Eingabe blockieren während ALT gehalten wird
SetNuiFocus(true, true)
DisableControlAction(0, 1, true) -- Look Left/Right (Mouse)
DisableControlAction(0, 2, true) -- Look Up/Down (Mouse)
DisableControlAction(0, 30, true) -- Move Left/Right (A/D)
DisableControlAction(0, 31, true) -- Move Forward/Back (W/S)
DisableControlAction(0, 25, true) -- Aim
DisableControlAction(0, 24, true) -- Attack
DisableControlAction(0, 140, true)
DisableControlAction(0, 141, true)
DisableControlAction(0, 142, true)
DisableControlAction(0, 257, true)
-- Mausposition ggf. sperren
SetCursorLocation(0.5, 0.5)
else
-- Fokus zurücksetzen, wenn ALT losgelassen wird
SetNuiFocus(false, false)
end
end
end)