Main/resources/[jobs]/[medic]/visn_are/script/helpers/c_functions.lua
2025-06-07 08:51:21 +02:00

634 lines
No EOL
21 KiB
Lua

--[[
-- Author: Tim Plate
-- Project: Advanced Roleplay Environment
-- Copyright (c) 2022 Tim Plate Solutions
--]]
--- Client extension module for Advanced Roleplay Environment.
local progressBarPool = {}
-- Notification
---Shows a notification
---@param text any
function ShowNotification(text)
LogDebug(true, "ShowNotification: ", text)
BeginTextCommandThefeedPost("STRING")
AddTextComponentString(text)
EndTextCommandThefeedPostTicker(false, false)
end
-- ClosestPlayer
---Gets all players
---@return table
function GetPlayers()
local players = {}
for _, player in ipairs(GetActivePlayers()) do
local ped = GetPlayerPed(player)
if DoesEntityExist(ped) then
table.insert(players, player)
end
end
return players
end
---Gets the closest player
---@return integer
---@return number
function GetClosestPlayer()
local players = GetPlayers()
local closestDistance = -1
local closestPlayer = -1
local playerId = PlayerId()
for i=1, #players, 1 do
local target = GetPlayerPed(players[i])
if players[i] ~= playerId then
local targetCoords = GetEntityCoords(target)
local distance = GetDistanceBetweenCoords(targetCoords, ClientData.coords, true)
if closestDistance == -1 or closestDistance > distance then
closestPlayer = players[i]
closestDistance = distance
end
end
end
return closestPlayer, closestDistance
end
-- Animations
-- Source: https://github.com/zaphosting/esx/blob/master/es_extended/client/modules/streaming.lua#L59
---Requests an animation dictionary
---@param animDict any
---@param cb any
function RequestAnimDictCustom(animDict, cb)
if not HasAnimDictLoaded(animDict) then
RequestAnimDict(animDict)
while not HasAnimDictLoaded(animDict) do
Citizen.Wait(1)
end
end
if cb ~= nil then
cb()
end
end
---Runs an animation
---@param animation any
---@param duration any
function RunAnimation(animation, duration)
RequestAnimDictCustom(animation.lib, function()
TaskPlayAnim(ClientData.ped, animation.lib, animation.name, 8.0, -8.0, -1, 9, 0, false, false, false)
local gameTimer = GetGameTimer()
while true do
if GetGameTimer() - gameTimer < duration then
Citizen.Wait(0)
else
BroadcastStop3DSound()
ClearPedTasks(ClientData.ped)
return
end
Citizen.Wait(0)
end
end)
end
-- ProgressBar
---Creates a progress bar
---@param text any
---@param duration any
---@param xFactorParam any
function CreateProgressBar(text, duration, xFactorParam)
if #progressBarPool > 0 then LogWarning("Can't create a progressbar. A progressbar is already running.") return end
local gameTimer = GetGameTimer()
table.insert(progressBarPool, {
text = TranslateText("ACTION_" .. text),
duration = duration,
xFactorParam = xFactorParam,
creationTimestamp = gameTimer,
endTimestamp = gameTimer + duration
})
progressBarPool[#progressBarPool].position = #progressBarPool
end
---Destroys a progress bar
---@param data any
function DestroyProgressBar(data)
table.remove(progressBarPool, data.position)
end
---Returns true if a progress bar is active
---@return boolean
function IsProgressBarActive()
return #progressBarPool > 0
end
Citizen.CreateThread(function()
local safeZoneSize = GetSafeZoneSize()
if not HasStreamedTextureDictLoaded('timerbars') then
RequestStreamedTextureDict('timerbars', true)
while not HasStreamedTextureDictLoaded('timerbars') do Citizen.Wait(1) end
end
while true do
Citizen.Wait(0)
local gameTimer = GetGameTimer()
for _, v in ipairs(progressBarPool) do
local width, xValue, yValue
if v.endTimestamp > gameTimer then
local timeDifference = gameTimer - v.creationTimestamp
if timeDifference < v.duration then width = timeDifference * (0.085 / v.duration) end
local correctionValue = ((1.0 - RoundValue(safeZoneSize, 2)) * 100) * 0.005
xValue, yValue = 0.9350 - correctionValue, 0.99 - correctionValue
SetScriptGfxDrawOrder(0)
DrawSprite('timerbars', 'all_black_bg', xValue, yValue, 0.15, 0.0325, 0.0, 255, 255, 255, 180)
SetScriptGfxDrawOrder(1)
DrawRect(xValue + 0.0275, yValue, 0.085, 0.0095, 143, 95, 13, 180)
SetScriptGfxDrawOrder(2)
DrawRect(xValue - 0.015 + (width / 2), yValue, width, 0.0095, 255, 162, 0, 180)
SetTextColour(255, 255, 255, 180)
SetTextFont(0)
SetTextScale(0.3, 0.3)
SetTextCentre(true)
SetTextEntry('STRING')
AddTextComponentString(v.text)
SetScriptGfxDrawOrder(3)
DrawText(xValue - v.xFactorParam, yValue - 0.012)
else
DestroyProgressBar(v)
end
end
end
end)
-- Carry player
local carrying = false
local carryingData = {}
---Carrys a player
---@param targetData any
---@param targetHealthBuffer any
function CarryPlayer(targetData, targetHealthBuffer)
if carrying then StopCarry() return end
if not targetHealthBuffer.unconscious and not targetHealthBuffer.anesthesia then return end
if targetData.ped == -1 or not DoesEntityExist(targetData.ped) or targetData.distance > 3.0 then return end
carrying = true
carryingData = {
ped = targetData.ped,
source = targetData.source,
distance = targetData.distance
}
TriggerServerEvent(ENUM_EVENT_TYPES.EVENT_START_CARRY, TempAuthToken, TargetData.source)
ClosePlayerMenu(false)
end
---Stop active carry
function StopCarry()
if not carrying then return end
local distance = GetDistanceBetweenCoords(ClientData.coords, GetEntityCoords(carryingData.ped), true)
if carryingData.ped == -1 or distance > 3.0 then return end
TriggerServerEvent(ENUM_EVENT_TYPES.EVENT_STOP_CARRY, TempAuthToken, carryingData.source)
carrying = false
ClearPedTasks(ClientData.ped)
DetachEntity(ClientData.ped, true, true)
end
---Plays the local animation loop
function PlayLocalAnimationLoop()
while not IsEntityPlayingAnim(ClientData.ped, "mini@cpr@char_b@cpr_def", "cpr_pumpchest_idle", 3) and (ClientHealthBuffer.unconscious or ClientHealthBuffer.anesthesia) and CarryAnimationData == nil and not IsPedInAnyVehicle(ClientData.ped, true) do
if UnconsciousAnimationAfter or 0 < GetGameTimer() then
TaskPlayAnim(ClientData.ped, "mini@cpr@char_b@cpr_def", "cpr_pumpchest_idle", 8.0, -8.0, 100000, 1, 0, false, false, false)
end
Citizen.Wait(0)
end
if ClientData.inVehicle and IsEntityPlayingAnim(ClientData.ped, "mini@cpr@char_b@cpr_def", "cpr_pumpchest_idle", 3) then
ClearPedTasks(ClientData.ped)
end
while CarryAnimationData ~= nil and not IsEntityPlayingAnim(ClientData.ped, CarryAnimationData.lib, CarryAnimationData.name, 3) do
TaskPlayAnim(ClientData.ped, CarryAnimationData.lib, CarryAnimationData.name, 8.0, 8.0, -1, CarryAnimationData.flag, 0, false, false, false)
Citizen.Wait(0)
end
end
-- Player menu
---Callbacks if the player is allowed to use the menu and gets active items from player
function GetMenuInfo()
local p = promise.new()
TriggerServerCallback {
eventName = ENUM_EVENT_TYPES.PROCEDURE_GET_MENU_INFO,
args = { TempAuthToken },
callback = function(result)
if not result then p:reject("No value returned")end
p:resolve(result)
end
}
return Citizen.Await(p)
end
-- Framework functions
---Respawns the player
---@param coords any
---@param authToken any
function RespawnPlayer(coords, authToken)
_TriggerEvent(ENUM_HOOKABLE_EVENTS.PLAYER_RESPAWNED, nil)
TriggerServerEvent(ENUM_EVENT_TYPES.EVENT_RESPAWN_PLAYER, json.encode(coords), authToken)
end
-- https://github.com/zaphosting/esx_12/blob/master/esx_ambulancejob/client/main.lua#L331
---Respawns the ped
---@param ped any
---@param coords any
---@param heading any
function RespawnPed(ped, coords, heading)
coords = { x = coords.x, y = coords.y, z = coords.z }
local inWater, waterHeight = GetWaterHeight(coords.x, coords.y, coords.z, 0)
if inWater then
coords.z = waterHeight + 0.6
end
--SetEntityCoords(ped, coords.x, coords.y, coords.z - 0.8, false, false, false, false)
--SetEntityHeading(ped, heading)
NetworkResurrectLocalPlayer(coords.x, coords.y, coords.z - 0.8, heading, true, false)
SetEntityHealth(ped, 200)
SetPlayerInvincible(ped, false)
ClearPedBloodDamage(ped)
-- Causes bad spawning?
-- TriggerEvent('playerSpawned')
end
---Emergency dispatch to configured receiver
---@param playerPed any
---@param coords any
function EmergencyDispatch(playerPed, coords)
if not ClientHealthBuffer.unconscious then return end
local emergencyDispatch = ClientConfig.m_emergencyDispatch
if not emergencyDispatch.enabled then return end
if emergencyDispatch.phoneConfiguration == "gcphone" then
for _, v in pairs(emergencyDispatch.receivers) do
TriggerServerEvent('esx_addons_gcphone:startCall', v, TranslateText("DISTRESS_MESSAGE"), coords, { PlayerCoords = { x = coords.x, y = coords.y, z = coords.z }, })
end
elseif emergencyDispatch.phoneConfiguration == "esx_phone" then
for _, v in pairs(emergencyDispatch.receivers) do
TriggerServerEvent('esx_phone:send', v, TranslateText("DISTRESS_MESSAGE"), false, { x = coords.x, y = coords.y, z = coords.z })
end
elseif emergencyDispatch.phoneConfiguration == "visn_phone" then
for _, v in pairs(emergencyDispatch.receivers) do
TriggerServerEvent('visn_phone:postService', v, TranslateText("DISTRESS_MESSAGE"), coords)
end
elseif emergencyDispatch.phoneConfiguration == "dphone" then
for _, v in pairs(emergencyDispatch.receivers) do
TriggerEvent("d-phone:client:message:senddispatch", TranslateText("DISTRESS_MESSAGE"), v, 0, 1, coords, "5")
end
elseif emergencyDispatch.phoneConfiguration == "roadphone" then
for _, v in pairs(emergencyDispatch.receivers) do
TriggerServerEvent('roadphone:sendDispatch', GetPlayerServerId(ClientData.id), TranslateText("DISTRESS_MESSAGE"), v, coords, false)
end
elseif emergencyDispatch.phoneConfiguration == "gksphone" then
TriggerServerEvent('gksphone:gkcs:jbmessage', "", "", TranslateText("DISTRESS_MESSAGE"), '', 'GPS: ' .. coords.x .. ', ' .. coords.y, json.encode(emergencyDispatch.receivers), true)
elseif emergencyDispatch.phoneConfiguration == "qs-smartphone" then
for _, v in pairs(emergencyDispatch.receivers) do
TriggerServerEvent('qs-smartphone:server:sendJobAlert', { message = TranslateText("DISTRESS_MESSAGE"), location = coords }, v)
TriggerServerEvent('qs-smartphone:server:AddNotifies', {
head = "EMS",
msg = TranslateText("DISTRESS_MESSAGE"),
app = 'business'
})
end
elseif emergencyDispatch.phoneConfiguration == "emergencydispatch" then
for _, v in pairs(emergencyDispatch.receivers) do
TriggerEvent('emergencydispatch:emergencycall:new', v, TranslateText("DISTRESS_MESSAGE"), false)
end
else
-- Implement your phone event here
end
end
---Loads the player
function LoadPlayer()
while not NetworkIsPlayerActive(PlayerId()) do
Citizen.Wait(0)
end
while not TempAuthToken or TempAuthToken == "" do
Citizen.Wait(0)
end
TriggerServerEvent(ENUM_EVENT_TYPES.EVENT_LOAD_PLAYER, TempAuthToken)
end
---Spawns a game object
---@param name any
---@param x any
---@param y any
---@param z any
---@return any
function SpawnGameObject(name, x, y, z)
if not ClientConfig.m_spawnGameObjects.enabled then return 0 end
local model = GetHashKey(name)
RequestModel(model)
local timeout = 0
while not HasModelLoaded(model) and timeout < 500 do timeout = timeout + 1 Citizen.Wait(0) end
if timeout >= 500 then return end
if #SpawnedGameObjects > (ClientConfig.m_spawnGameObjects.limit or 50) then DeleteObject(SpawnedGameObjects[1].object) table.remove(SpawnedGameObjects, 1) end
local object = CreateObject(model, x, y, z, true, true, false)
SetModelAsNoLongerNeeded(model)
table.insert(SpawnedGameObjects, {
object = object,
timestamp = GetGameTimer()
})
return object
end
---Cleans up spawned game objects
function CleanUpGameObjects()
for i, v in ipairs(SpawnedGameObjects) do
if (GetGameTimer() - v.timestamp) > ClientConfig.m_spawnGameObjects.lifetime then
DeleteObject(v.object)
table.remove(SpawnedGameObjects, i)
end
end
end
-- Command functions
---Function for the emergency dispatch command
function CallEmergencyDispatch()
if not ClientHealthBuffer then return end
if not ClientHealthBuffer.unconscious then return end
if not ClientConfig.m_emergencyDispatch.enabled then return end
if (GetGameTimer() - LastDispatch) < ClientConfig.m_emergencyDispatch.cooldown * 1000 then return end
if IsPauseMenuActive() then return end
EmergencyDispatch(ClientData.ped, ClientData.coords)
LastDispatch = GetGameTimer()
SendNUIMessage({
payload = "toggleDispatchInfo",
payloadData = {
value = false
}
})
LastDispatchHidden = true
end
---Function for the self menu command
function OpenSelfMenu()
if not ClientHealthBuffer then return end
if ClientHealthBuffer.unconscious then return end
if IsPauseMenuActive() then return end
ShowPlayerMenu()
end
---Function for the interaction menu command
function OpenInteractionMenu()
if not ClientHealthBuffer then return end
if ClientHealthBuffer.unconscious then return end
if IsPauseMenuActive() then return end
local closestPlayer, closestDistance = GetClosestPlayer()
if closestPlayer ~= -1 and closestDistance <= 3.0 then ShowPlayerMenu(GetPlayerServerId(closestPlayer)) end
end
---Function for the manual respawn command
function ManualRespawn()
if not ClientHealthBuffer then return end
if not ClientHealthBuffer.unconscious and not ClientHealthBuffer.bodybag then return end
if not ClientConfig.m_allowManualRespawnWhileBeingUnconscious and not AllowManualRespawn then return end
if not AllowManualRespawn and GetRemainingUnconsciousSeconds(ClientHealthBuffer) > 0 then return end
if IsPauseMenuActive() then return end
if AllowManualRespawn then AllowManualRespawn = false end
RespawnPlayer(ClientData.coords, TempAuthToken)
end
---Function for the cancel interaction command
function CancelInteraction()
if not ClientHealthBuffer then return end
if CarryAnimationData ~= nil then StopCarry() end
if ClientHealthBuffer.unconscious then return end
if not IsProgressBarActive() then return end
if IsPauseMenuActive() then return end
BroadcastStop3DSound()
DestroyProgressBar({ position = 1 })
ClearPedTasks(ClientData.ped)
TargetData.canceled = true
end
-- Non-Input-Mapper Key function handler
---Handles the key interactions
---@param keyConfig any
function HandleKeyInteractions(keyConfig)
if IsControlJustPressed(0, keyConfig.keys.CANCEL_INTERACTION.control_id) then CancelInteraction() end
if IsControlJustPressed(0, keyConfig.keys.OPEN_SELF_MENU.control_id) then OpenSelfMenu() end
if IsControlJustPressed(0, keyConfig.keys.OPEN_OTHER_MENU.control_id) then OpenInteractionMenu() end
if IsControlJustPressed(0, keyConfig.keys.MANUAL_RESPAWN.control_id) then ManualRespawn() end
if IsControlJustPressed(0, keyConfig.keys.EMERGENCY_DISPATCH.control_id) then CallEmergencyDispatch() end
end
-- Deathscreen function
---Handles the deathscreen
---@param bloodVolume any
function HandleUnconsciousScreen(bloodVolume)
local remainingSeconds = GetRemainingUnconsciousSeconds(ClientHealthBuffer)
SendNUIMessage({
payload = "updateUnconsciousTime",
payloadData = {
seconds = remainingSeconds,
max_seconds = ClientHealthBuffer.unconsciousTimeUntilDeath
}
})
if GetGameTimer() - LastDispatch > ClientConfig.m_emergencyDispatch.cooldown * 1000 and LastDispatchHidden then
SendNUIMessage({
payload = "toggleDispatchInfo",
payloadData = {
value = (ClientConfig.m_emergencyDispatch.enabled and ClientConfig.m_configurableKeys.keys.EMERGENCY_DISPATCH.enabled) and not ClientHealthBuffer.bodybag,
key = ClientConfig.m_configurableKeys.m_useInputMapper
and GetControlStringFromKeyMapping("emergency_dispatch")
or GetControlStringFromKeyMappingNoInputMapper(ClientConfig.m_configurableKeys.keys.EMERGENCY_DISPATCH.control_id)
}
})
LastDispatchHidden = false
end
if not CheckForCriticalVitals(bloodVolume) and math.random(0, 100) > 75 and
(
ClientHealthBuffer.heartRate > 50 and
ClientHealthBuffer.bloodVolume > ENUM_BLOOD_VOLUME_CLASSES.CLASS_4_HERMORRHAGE) then
SetUnconsciousState(false)
end
if ClientHealthBuffer.unconscious and remainingSeconds == 0 then
if ClientConfig.m_allowManualRespawnWhileBeingUnconscious then
SendNUIMessage({
payload = "showManualRespawnText",
payloadData = {
value = ClientConfig.m_configurableKeys.keys.MANUAL_RESPAWN.enabled,
key = ClientConfig.m_configurableKeys.m_useInputMapper
and GetControlStringFromKeyMapping("manual_respawn")
or GetControlStringFromKeyMappingNoInputMapper(ClientConfig.m_configurableKeys.keys.MANUAL_RESPAWN.control_id)
}
})
else
RespawnPlayer(ClientData.coords, TempAuthToken)
end
end
end
-- ShowAnesthesiaScreen function
---Handles the anesthesia screen
function ShowAnesthesiaScreen(state)
SendNUIMessage({
payload = "toggleAnesthesiaScreen",
payloadData = {
value = state
}
})
end
-- Enumerator
local entityEnumerator = {
__gc = function(enum)
if enum.destructor and enum.handle then
enum.destructor(enum.handle)
end
enum.destructor = nil
enum.handle = nil
end
}
local function EnumerateEntities(initFunc, moveFunc, disposeFunc)
return coroutine.wrap(function()
local iter, id = initFunc()
if not id or id == 0 then
disposeFunc(iter)
return
end
local enum = {handle = iter, destructor = disposeFunc}
setmetatable(enum, entityEnumerator)
local next = true
repeat
coroutine.yield(id)
next, id = moveFunc(iter)
until not next
enum.destructor, enum.handle = nil, nil
disposeFunc(iter)
end)
end
function EnumerateObjects()
return EnumerateEntities(FindFirstObject, FindNextObject, EndFindObject)
end
function EnumeratePeds()
return EnumerateEntities(FindFirstPed, FindNextPed, EndFindPed)
end
function EnumerateVehicles()
return EnumerateEntities(FindFirstVehicle, FindNextVehicle, EndFindVehicle)
end
function EnumeratePickups()
return EnumerateEntities(FindFirstPickup, FindNextPickup, EndFindPickup)
end
function GetCustomClosestVehicle(coords)
local oldDistance = 99999
local vehicle = nil
for v in EnumerateVehicles() do
local checkDistance = GetDistanceBetweenCoords(coords, GetEntityCoords(v), true)
if checkDistance <= oldDistance and v ~= vehicle then
vehicle = v
oldDistance = checkDistance
end
end
return vehicle, oldDistance
end
-- Function
---Returns if the player damage is disabled
---@return boolean
function IsPlayerDamageDisabled()
if GetResourceState("ws_ffa") == "started" and exports["ws_ffa"]:isInZone() then return true end
return PlayerDamageDisabled
end
-- ox_target
if ClientConfig.m_ox_target_support then
Citizen.CreateThread(function()
Citizen.Wait(1000)
local ox_menu_options = {
{
name = 'visn_are:openMenu',
icon = 'fa-solid fa-diagnoses',
label = TranslateText("OPEN_PLAYER_MENU"),
distance = 1.5,
onSelect = function(data)
ShowPlayerMenu(GetPlayerServerId(NetworkGetPlayerIndexFromPed(data.entity)))
end
}
}
exports["ox_target"]:addGlobalPlayer(ox_menu_options)
end)
end
-- State saving
Citizen.CreateThread(function()
while not NetworkIsPlayerActive(PlayerId()) do
Citizen.Wait(0)
end
while not TempAuthToken or TempAuthToken == "" do
Citizen.Wait(0)
end
Citizen.Wait(2500)
TriggerServerEvent(ENUM_EVENT_TYPES.EVENT_LOAD_PLAYER_STANDALONE, TempAuthToken)
end)
RegisterNetEvent('esx:playerLoaded', LoadPlayer)
RegisterNetEvent('QBCore:Client:OnPlayerLoaded', LoadPlayer)