539 lines
14 KiB
Lua
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)
|
|
|