2026-04-15 22:00:01 +02:00

695 lines
23 KiB
Lua

local ESX = nil
CreateThread(function()
while not ESX do
Wait(10)
ESX = exports['es_extended']:getSharedObject()
end
end)
-- ============================================================
-- STATE
-- ============================================================
local deathState = 'ALIVE' -- ALIVE | UNCONSCIOUS | BLEEDING_OUT
local unconsciousTimer = 0
local bleedoutTimer = 0
local emsCallHoldTime = 0
local emsCalled = false
local isReviving = false
local isTreating = false
local canGiveUp = false
local giveUpTimer = 0
-- Tabelle mit bewusstlosen Spielern (vom Server synchronisiert, für ox_target)
local DownedPlayers = {}
-- ============================================================
-- HELPER: Ist der lokale Spieler EMS?
-- (Muss VOR ox_target Registrierung stehen!)
-- ============================================================
local function IsLocalPlayerEMS()
if not ESX or not ESX.PlayerData or not ESX.PlayerData.job then return false end
for _, jobName in ipairs(Config.EMSJobNames) do
if ESX.PlayerData.job.name == jobName then
return true
end
end
return false
end
-- ============================================================
-- UTILS
-- ============================================================
local function GetNearestHospital()
local playerCoords = GetEntityCoords(PlayerPedId())
local nearest = nil
local nearestDist = math.huge
for _, loc in ipairs(Config.RespawnLocations) do
local dist = #(playerCoords - vector3(loc.x, loc.y, loc.z))
if dist < nearestDist then
nearestDist = dist
nearest = loc
end
end
return nearest or Config.RespawnLocations[1]
end
local function SetHudVisible(visible)
DisplayRadar(visible)
pcall(function()
if visible then
exports['l2_hud']:ShowHUD()
else
exports['l2_hud']:HideHUD()
end
end)
end
local function LoadAnimDict(dict)
RequestAnimDict(dict)
local timeout = 0
while not HasAnimDictLoaded(dict) and timeout < 1000 do
Wait(10)
timeout = timeout + 10
end
end
local function PlayDeadAnimation(ped)
if not IsPedRagdoll(ped) then
SetPedToRagdoll(ped, -1, -1, 0, false, false, false)
end
end
local function PlayAnimation(dict, anim, duration, flag)
if isTreating then return false end
isTreating = true
local ped = PlayerPedId()
LoadAnimDict(dict)
TaskPlayAnim(ped, dict, anim, 8.0, -8.0, duration, flag or 1, 0, false, false, false)
local startTime = GetGameTimer()
while (GetGameTimer() - startTime) < duration do
Wait(100)
if not DoesEntityExist(ped) or IsPedDeadOrDying(ped, true) then
isTreating = false
return false
end
end
ClearPedTasks(ped)
RemoveAnimDict(dict)
isTreating = false
return true
end
-- Wiederbeleben: CPR Animation (auf dem Boden knien, ganzer Körper)
local function PlayReviveAnimation(duration)
return PlayAnimation('mini@cpr@char_a@cpr_str', 'cpr_pumpchest', duration, 1)
end
-- Heilen / Bandage: Verband-Animation (im Stehen, nur Oberkörper)
local function PlayHealAnimation(duration)
return PlayAnimation('anim@heists@narcotics@funding@gang_idle', 'gang_chatting_idle01', duration, 49)
end
-- GTA Health-System: 0=tot, 100=minimum sichtbar, 200=voll
-- Diese Funktion rechnet Prozent in GTA-HP um (Bereich 100-200)
local function PercentToHealth(percent)
local health = 100 + math.floor(100 * percent)
if health < 110 then health = 110 end -- Sicherheitspuffer über "dying" Grenze
if health > 200 then health = 200 end
return health
end
-- ============================================================
-- NUI COMMUNICATION
-- ============================================================
local function ShowDeathscreen(state, timer)
SendNUIMessage({
action = 'show',
state = state,
timer = timer,
locale = Config.Locale
})
end
local function UpdateNUI(state, timer)
SendNUIMessage({
action = 'update',
state = state,
timer = timer
})
end
local function HideDeathscreen()
SendNUIMessage({ action = 'hide' })
end
-- ============================================================
-- DEATH STATE MANAGEMENT
-- ============================================================
local function SetUnconsciousState()
if deathState ~= 'ALIVE' or isReviving then return end
deathState = 'UNCONSCIOUS'
unconsciousTimer = Config.UnconsciousTime
emsCalled = false
canGiveUp = false
giveUpTimer = 0
local ped = PlayerPedId()
if IsPedDeadOrDying(ped, true) then
-- Ped ist bereits tot - muss resurrectet werden
local coords = GetEntityCoords(ped)
NetworkResurrectLocalPlayer(coords.x, coords.y, coords.z, GetEntityHeading(ped), true, false)
Wait(0)
ped = PlayerPedId()
SetEntityAlpha(ped, 0, false) -- Unsichtbar während Übergang
end
SetEntityHealth(ped, 200)
SetEntityInvincible(ped, true)
SetPedToRagdoll(ped, -1, -1, 0, false, false, false)
-- Kurz warten dann wieder sichtbar (Ragdoll hat bereits gestartet)
CreateThread(function()
Wait(100)
SetEntityAlpha(PlayerPedId(), 255, false)
end)
SetHudVisible(false)
ShowDeathscreen('UNCONSCIOUS', unconsciousTimer)
TriggerServerEvent('mercyv-deathscreen:server:updateState', 'UNCONSCIOUS')
end
local function SetBleedingOutState()
deathState = 'BLEEDING_OUT'
bleedoutTimer = Config.BleedoutTime
UpdateNUI('BLEEDING_OUT', bleedoutTimer)
TriggerServerEvent('mercyv-deathscreen:server:updateState', 'BLEEDING_OUT')
end
local function ResetToAlive(skipResurrect, reviveHealthPercent)
isReviving = true
unconsciousTimer = 0
bleedoutTimer = 0
emsCallHoldTime = 0
emsCalled = false
canGiveUp = false
giveUpTimer = 0
local ped = PlayerPedId()
-- Ragdoll / Animation stoppen
SetPedCanRagdoll(ped, false)
ClearPedTasksImmediately(ped)
SetPedCanRagdoll(ped, true)
if not skipResurrect then
NetworkResurrectLocalPlayer(GetEntityCoords(ped), GetEntityHeading(ped), true, false)
local health = PercentToHealth(reviveHealthPercent or Config.ReviveHealthPercent or 0.5)
SetEntityHealth(ped, health)
ClearPedBloodDamage(ped)
end
SetEntityInvincible(ped, false)
HideDeathscreen()
deathState = 'ALIVE'
-- HUD-Restore in eigenem Thread damit es garantiert ausgeführt wird
CreateThread(function()
Wait(1500)
SetHudVisible(true)
isReviving = false
end)
end
local function DoRespawn()
TriggerServerEvent('mercyv-deathscreen:server:respawn')
end
-- ============================================================
-- SERVER CALLBACKS
-- ============================================================
RegisterNetEvent('mercyv-deathscreen:client:doRespawn')
AddEventHandler('mercyv-deathscreen:client:doRespawn', function()
isReviving = true
local hospital = GetNearestHospital()
-- Beim Respawn MUSS resurrectet werden (Teleport zum Krankenhaus)
NetworkResurrectLocalPlayer(hospital.x, hospital.y, hospital.z, hospital.heading, true, false)
Wait(300)
local ped = PlayerPedId()
SetEntityCoords(ped, hospital.x, hospital.y, hospital.z, false, false, false, true)
SetEntityHeading(ped, hospital.heading)
SetEntityHealth(ped, 200) -- Volle HP beim Respawn
ClearPedBloodDamage(ped)
SetPedCanRagdoll(ped, false)
SetEntityInvincible(ped, false)
ClearPedTasksImmediately(ped)
SetPedCanRagdoll(ped, true)
HideDeathscreen()
deathState = 'ALIVE'
unconsciousTimer = 0
bleedoutTimer = 0
emsCallHoldTime = 0
emsCalled = false
canGiveUp = false
giveUpTimer = 0
TriggerServerEvent('mercyv-deathscreen:server:updateState', 'ALIVE')
-- HUD-Restore in eigenem Thread damit es garantiert ausgeführt wird
CreateThread(function()
Wait(500)
SetHudVisible(true)
isReviving = false
end)
end)
-- ============================================================
-- DOWNED PLAYERS SYNC
-- ============================================================
RegisterNetEvent('mercyv-deathscreen:client:syncDownedPlayers')
AddEventHandler('mercyv-deathscreen:client:syncDownedPlayers', function(downedList)
DownedPlayers = downedList or {}
end)
-- ============================================================
-- ESX EVENT HANDLERS
-- ============================================================
AddEventHandler('esx:onPlayerDeath', function(data)
SetUnconsciousState()
end)
RegisterNetEvent('esx:playerLoaded')
AddEventHandler('esx:playerLoaded', function(xPlayer)
ESX.PlayerData = xPlayer
ResetToAlive()
end)
RegisterNetEvent('esx:setJob')
AddEventHandler('esx:setJob', function(job)
if ESX.PlayerData then
ESX.PlayerData.job = job
end
end)
-- ============================================================
-- EIGENE TOD-ERKENNUNG (ohne esx_ambulancejob)
-- ============================================================
CreateThread(function()
while true do
if isReviving then
Wait(500)
elseif deathState == 'ALIVE' then
local ped = PlayerPedId()
local health = GetEntityHealth(ped)
if IsPedDeadOrDying(ped, true) or health <= 101 then
SetUnconsciousState()
end
Wait(0)
else
-- Im Death-State: Falls GTA den Ped nochmal killt, erneut resurrecten
local ped = PlayerPedId()
if IsPedDeadOrDying(ped, true) then
local coords = GetEntityCoords(ped)
NetworkResurrectLocalPlayer(coords.x, coords.y, coords.z, GetEntityHeading(ped), true, false)
Wait(0)
ped = PlayerPedId()
SetEntityAlpha(ped, 0, false)
SetEntityHealth(ped, 200)
SetEntityInvincible(ped, true)
SetPedToRagdoll(ped, -1, -1, 0, false, false, false)
CreateThread(function()
Wait(100)
SetEntityAlpha(PlayerPedId(), 255, false)
end)
end
Wait(1000)
end
end
end)
-- ============================================================
-- EIGENES REVIVE SYSTEM
-- ============================================================
RegisterNetEvent('mercyv-deathscreen:client:revived')
AddEventHandler('mercyv-deathscreen:client:revived', function()
if deathState == 'ALIVE' then return end
ResetToAlive(false, Config.ReviveHealthPercent)
TriggerServerEvent('mercyv-deathscreen:server:updateState', 'ALIVE')
ESX.ShowNotification(Config.Locale.revived)
end)
-- ============================================================
-- OX_TARGET: Wundenbehandlung auf lebende Spieler (nur EMS)
-- ============================================================
CreateThread(function()
while not ESX do Wait(100) end
local ok, err = pcall(function()
exports.ox_target:addGlobalPlayer({
{
name = 'mercyv_revive',
icon = 'fas fa-heart-pulse',
label = Config.Locale.reviveTargetLabel or 'Wiederbeleben',
distance = Config.ReviveDistance or 3.0,
canInteract = function(entity)
if isTreating then return false end
local targetPlayer = NetworkGetPlayerIndexFromPed(entity)
if targetPlayer == -1 then return false end
local targetServerId = GetPlayerServerId(targetPlayer)
-- Nur bei am Boden liegenden Spielern anzeigen
if not DownedPlayers[targetServerId] then return false end
if IsLocalPlayerEMS() then return true end
return Config.CitizenRevive
end,
onSelect = function(data)
local targetPlayer = NetworkGetPlayerIndexFromPed(data.entity)
if targetPlayer == -1 then return end
local targetServerId = GetPlayerServerId(targetPlayer)
if not targetServerId or targetServerId <= 0 then return end
local success = PlayReviveAnimation(Config.ReviveAnimDuration or 5000)
if success then
TriggerServerEvent('mercyv-deathscreen:server:revivePlayer', targetServerId)
end
end,
},
{
name = 'mercyv_heal_big',
icon = 'fas fa-kit-medical',
label = Config.Locale.healBigLabel or 'Große Wunden behandeln',
distance = Config.ReviveDistance or 3.0,
canInteract = function(entity)
if isTreating then return false end
if not IsLocalPlayerEMS() then return false end
local targetPlayer = NetworkGetPlayerIndexFromPed(entity)
if targetPlayer == -1 then return false end
local targetServerId = GetPlayerServerId(targetPlayer)
if DownedPlayers[targetServerId] then return false end
return true
end,
onSelect = function(data)
local targetPlayer = NetworkGetPlayerIndexFromPed(data.entity)
if targetPlayer == -1 then return end
local targetServerId = GetPlayerServerId(targetPlayer)
if not targetServerId or targetServerId <= 0 then return end
local success = PlayHealAnimation(Config.HealBigDuration or 8000)
if success then
TriggerServerEvent('mercyv-deathscreen:server:healPlayer', targetServerId, 'big')
end
end,
},
{
name = 'mercyv_heal_small',
icon = 'fas fa-bandage',
label = Config.Locale.healSmallLabel or 'Kleine Wunden behandeln',
distance = Config.ReviveDistance or 3.0,
canInteract = function(entity)
if isTreating then return false end
if not IsLocalPlayerEMS() then return false end
local targetPlayer = NetworkGetPlayerIndexFromPed(entity)
if targetPlayer == -1 then return false end
local targetServerId = GetPlayerServerId(targetPlayer)
if DownedPlayers[targetServerId] then return false end
return true
end,
onSelect = function(data)
local targetPlayer = NetworkGetPlayerIndexFromPed(data.entity)
if targetPlayer == -1 then return end
local targetServerId = GetPlayerServerId(targetPlayer)
if not targetServerId or targetServerId <= 0 then return end
local success = PlayHealAnimation(Config.HealSmallDuration or 5000)
if success then
TriggerServerEvent('mercyv-deathscreen:server:healPlayer', targetServerId, 'small')
end
end,
},
})
end)
if not ok then
print('[mercyv-deathscreen] ox_target Heal fehlgeschlagen: ' .. tostring(err))
end
end)
-- Wundenbehandlung: Client bekommt HP-Update
RegisterNetEvent('mercyv-deathscreen:client:healed')
AddEventHandler('mercyv-deathscreen:client:healed', function(newHealth, healType)
local ped = PlayerPedId()
SetEntityHealth(ped, newHealth)
ClearPedBloodDamage(ped)
ESX.ShowNotification(Config.Locale.healedByEMS or 'Du wurdest behandelt')
end)
-- ============================================================
-- SELBSTHEILUNG (Bandage)
-- ============================================================
RegisterNetEvent('mercyv-deathscreen:client:selfHeal')
AddEventHandler('mercyv-deathscreen:client:selfHeal', function()
if deathState ~= 'ALIVE' then return end
if isTreating then return end
local success = PlayHealAnimation(Config.SelfHealDuration or 4000)
if success then
local ped = PlayerPedId()
local currentHealth = GetEntityHealth(ped)
local maxHealth = GetEntityMaxHealth(ped)
local newHealth = currentHealth + (Config.SelfHealAmount or 50)
if newHealth > maxHealth then newHealth = maxHealth end
SetEntityHealth(ped, newHealth)
ClearPedBloodDamage(ped)
ESX.ShowNotification(Config.Locale.selfHealUsed or 'Du hast dich mit einer Bandage verarztet')
end
end)
-- ============================================================
-- MAIN TIMER THREAD
-- ============================================================
CreateThread(function()
while true do
if deathState == 'UNCONSCIOUS' then
unconsciousTimer = unconsciousTimer - 1
giveUpTimer = giveUpTimer + 1
UpdateNUI('UNCONSCIOUS', unconsciousTimer)
if unconsciousTimer <= 0 then
SetBleedingOutState()
end
if not canGiveUp and giveUpTimer >= (Config.GiveUpTime or 60) then
canGiveUp = true
SendNUIMessage({ action = 'enableGiveUp' })
end
elseif deathState == 'BLEEDING_OUT' then
bleedoutTimer = bleedoutTimer - 1
giveUpTimer = giveUpTimer + 1
UpdateNUI('BLEEDING_OUT', bleedoutTimer)
if bleedoutTimer <= 0 then
DoRespawn()
end
if not canGiveUp and giveUpTimer >= (Config.GiveUpTime or 60) then
canGiveUp = true
SendNUIMessage({ action = 'enableGiveUp' })
end
end
Wait(1000)
end
end)
-- ============================================================
-- INPUT & ANIMATION THREAD
-- ============================================================
CreateThread(function()
while true do
if deathState ~= 'ALIVE' and not isReviving then
local ped = PlayerPedId()
DisableAllControlActions(0)
PlayDeadAnimation(ped)
-- Aufgeben (ausbluten) per E-Taste → direkt Respawn
if canGiveUp and (deathState == 'UNCONSCIOUS' or deathState == 'BLEEDING_OUT') then
if IsDisabledControlJustPressed(0, Config.RespawnKey) then
DoRespawn()
end
end
-- EMS rufen per G-Taste
if (deathState == 'UNCONSCIOUS' or deathState == 'BLEEDING_OUT') and not emsCalled then
if IsDisabledControlPressed(0, Config.EMSCallKey) then
emsCallHoldTime = emsCallHoldTime + 1
-- Balken oder Fortschritt könnte hier visualisiert werden (ca. 1-2 Sek gedrückt halten)
if emsCallHoldTime >= 60 then
emsCalled = true
emsCallHoldTime = 0
local coords = GetEntityCoords(ped)
-- Straßenname ermitteln
local streetHash, crossingHash = GetStreetNameAtCoord(coords.x, coords.y, coords.z, 0, 0)
local streetName = GetStreetNameFromHashKey(streetHash)
-- CD_DISPATCH INTEGRATION
TriggerServerEvent('cd_dispatch:AddNotification', {
job_table = {'ambulance'},
coords = coords,
title = '10-15 - Bewusstlose Person',
message = 'Notruf: Verletzte Person bei ' .. tostring(streetName),
flash = 0,
unique_id = tostring(math.random(1111, 9999)),
sound = 1,
blip = {
sprite = 153,
scale = 1.2,
colour = 1,
flashes = true,
text = 'Notruf: Bewusstlose Person',
time = 5,
radius = 0,
}
})
SendNUIMessage({ action = 'emsCalled' })
ESX.ShowNotification('Der Rettungsdienst wurde alarmiert.')
end
else
emsCallHoldTime = 0
end
end
Wait(0)
else
Wait(500)
end
end
end)
-- ============================================================
-- CLEANUP
-- ============================================================
AddEventHandler('onResourceStop', function(resourceName)
if resourceName ~= GetCurrentResourceName() then return end
if deathState ~= 'ALIVE' then
local ped = PlayerPedId()
SetPedCanRagdoll(ped, false)
SetEntityInvincible(ped, false)
ClearPedTasksImmediately(ped)
SetPedCanRagdoll(ped, true)
SetHudVisible(true)
HideDeathscreen()
end
end)
-- ============================================================
-- MEDICAL NPC SYSTEM (AUTO-MEDIC)
-- ============================================================
local MedicNPC = {
model = `s_m_m_doctor_01`, -- Der Skin des Arztes
coords = vector4(-327.2433, -588.2013, 31.7755, 211.5617), -- Z leicht gesenkt für Bodenhaftung
}
CreateThread(function()
-- NPC Erstellen
RequestModel(MedicNPC.model)
while not HasModelLoaded(MedicNPC.model) do Wait(10) end
local npcEntity = CreatePed(4, MedicNPC.model, MedicNPC.coords.x, MedicNPC.coords.y, MedicNPC.coords.z, MedicNPC.coords.w, false, true)
SetEntityHeading(npcEntity, MedicNPC.coords.w)
FreezeEntityPosition(npcEntity, true)
SetEntityInvincible(npcEntity, true)
SetBlockingOfNonTemporaryEvents(npcEntity, true)
-- ox_target Optionen für den NPC
exports.ox_target:addLocalEntity(npcEntity, {
{
name = 'medic_npc_heal',
icon = 'fas fa-kit-medical',
label = 'Sich selbst heilen (Kostenlos)',
canInteract = function()
return deathState == 'ALIVE' and GetEntityHealth(PlayerPedId()) < 200
end,
onSelect = function()
TriggerServerEvent('mercyv-deathscreen:server:npcHealSelf')
end
},
{
name = 'medic_npc_revive',
icon = 'fas fa-heart-pulse',
label = 'Person in der Nähe wiederbeleben',
canInteract = function()
-- Prüfen ob jemand am Boden liegt in der Nähe des Spielers
local coords = GetEntityCoords(PlayerPedId())
local players = ESX.Game.GetPlayersInArea(coords, 5.0)
for _, player in ipairs(players) do
local serverId = GetPlayerServerId(player)
if DownedPlayers[serverId] and player ~= PlayerId() then
return true
end
end
return false
end,
onSelect = function()
local coords = GetEntityCoords(PlayerPedId())
local players = ESX.Game.GetPlayersInArea(coords, 5.0)
local closestPlayer = nil
local shortestDist = 5.0
for _, player in ipairs(players) do
local serverId = GetPlayerServerId(player)
if DownedPlayers[serverId] and player ~= PlayerId() then
closestPlayer = serverId
break
end
end
if closestPlayer then
TriggerServerEvent('mercyv-deathscreen:server:npcReviveOther', closestPlayer)
else
ESX.ShowNotification('Keine bewusstlose Person in der Nähe!')
end
end
}
})
end)