- aether-medical: Stationen/Belegung, Intensivstation, Blutbank, Fuhrpark, Nachrichten/Aushänge, Berichte und Textbausteine (server + NUI) - aether-admin: vollständiges Server-Admin-Panel als echtes FiveM-Resource, an aether-core gekoppelt (Rechte über aether_users.admin_group), mit WebRTC-Live-Kameras (Bildschirmfreigabe) und separatem Media-Server (SFU) - Prototypen admin/ vervollständigt - SQL-Schema aether_admin.sql - Docker-Compose mit MariaDB und idempotenter Auto-Migration (schema_migrations) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
250 lines
8.7 KiB
Lua
250 lines
8.7 KiB
Lua
-- =====================================================================
|
|
-- Aether Death — Serverlogik
|
|
-- Der Server fuehrt den Verblutungs-Countdown und entscheidet ueber
|
|
-- Wiederbelebung, Aufgeben und Krankenhausaufenthalt.
|
|
-- =====================================================================
|
|
|
|
local Core = exports['aether-core']
|
|
|
|
-- Verletzte Spieler: [quelle] = { seit, restBleed, sperre, emsGerufen, position }
|
|
local verletzte = {}
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Hilfsfunktionen
|
|
-- ---------------------------------------------------------------------
|
|
|
|
local function CharakterVon(quelle)
|
|
return Core:HoleCharakterId(quelle)
|
|
end
|
|
|
|
--- Schickt den aktuellen Zustand an den verletzten Spieler
|
|
--- @param quelle number Server-ID
|
|
local function SendeZustand(quelle)
|
|
local eintrag = verletzte[quelle]
|
|
if not eintrag then return end
|
|
|
|
TriggerClientEvent('aether:death:zustand', quelle, {
|
|
restBleed = eintrag.restBleed,
|
|
respawnSperre = eintrag.sperre,
|
|
emsGerufen = eintrag.emsGerufen,
|
|
bleedoutMax = Tod.BLEEDOUT,
|
|
})
|
|
end
|
|
|
|
--- Beendet den Verletzt-Zustand eines Spielers
|
|
--- @param quelle number Server-ID
|
|
local function BeendeVerletzung(quelle)
|
|
verletzte[quelle] = nil
|
|
local charakterId = CharakterVon(quelle)
|
|
if charakterId then
|
|
MySQL.update('UPDATE aether_characters SET is_dead = 0 WHERE id = ?', { charakterId })
|
|
end
|
|
end
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Spieler geht zu Boden
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:death:gefallen', function()
|
|
local quelle = source
|
|
local charakterId = CharakterVon(quelle)
|
|
if not charakterId then return end
|
|
if verletzte[quelle] then return end -- schon verletzt
|
|
|
|
local position = GetEntityCoords(GetPlayerPed(quelle))
|
|
|
|
verletzte[quelle] = {
|
|
seit = os.time(),
|
|
restBleed = Tod.BLEEDOUT,
|
|
sperre = Tod.RESPAWN_SPERRE,
|
|
emsGerufen = false,
|
|
position = { x = position.x, y = position.y, z = position.z },
|
|
}
|
|
|
|
MySQL.update('UPDATE aether_characters SET is_dead = 1 WHERE id = ?', { charakterId })
|
|
|
|
TriggerClientEvent('aether:death:zeige', quelle, {
|
|
restBleed = Tod.BLEEDOUT,
|
|
respawnSperre = Tod.RESPAWN_SPERRE,
|
|
bleedoutMax = Tod.BLEEDOUT,
|
|
})
|
|
|
|
-- Andere Module informieren (z. B. Job beenden, HUD anpassen)
|
|
TriggerEvent('aether:death:spielerGefallen', quelle)
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Rettungsdienst rufen
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:death:emsRufen', function()
|
|
local quelle = source
|
|
local eintrag = verletzte[quelle]
|
|
if not eintrag or eintrag.emsGerufen then return end
|
|
|
|
eintrag.emsGerufen = true
|
|
|
|
local person = Core:HoleSpielerInfo(quelle)
|
|
local name = person and (person.vorname .. ' ' .. person.nachname) or 'Unbekannt'
|
|
|
|
-- Notruf weiterreichen. Ein Rettungsdienst-Modul kann diesen Event
|
|
-- abfangen und daraus einen Einsatz erzeugen.
|
|
TriggerEvent('aether:death:notruf', quelle, {
|
|
name = name,
|
|
position = eintrag.position,
|
|
})
|
|
|
|
SendeZustand(quelle)
|
|
TriggerClientEvent('aether:death:hinweis', quelle, 'Notruf abgesetzt. Warte auf Hilfe.')
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Aufgeben: im Krankenhaus aufwachen
|
|
-- ---------------------------------------------------------------------
|
|
RegisterNetEvent('aether:death:aufgeben', function()
|
|
local quelle = source
|
|
local eintrag = verletzte[quelle]
|
|
if not eintrag then return end
|
|
|
|
-- Sperrzeit pruefen: nicht sofort aufgeben koennen
|
|
if eintrag.sperre > 0 then
|
|
TriggerClientEvent('aether:death:hinweis', quelle,
|
|
('Noch %d Sekunden, bevor du aufgeben kannst.'):format(eintrag.sperre))
|
|
return
|
|
end
|
|
|
|
local klinik = Tod.NaechstesKrankenhaus(
|
|
vector3(eintrag.position.x, eintrag.position.y, eintrag.position.z))
|
|
|
|
-- Behandlungskosten abbuchen (erst Bargeld, dann Konto)
|
|
local bezahlt = Core:EntferneGeld(quelle, 'bargeld', Tod.KRANKENHAUS_KOSTEN, 'Behandlung Krankenhaus')
|
|
if not bezahlt then
|
|
bezahlt = Core:EntferneGeld(quelle, 'konto', Tod.KRANKENHAUS_KOSTEN, 'Behandlung Krankenhaus')
|
|
end
|
|
|
|
BeendeVerletzung(quelle)
|
|
|
|
TriggerClientEvent('aether:death:krankenhaus', quelle, {
|
|
klinik = klinik.name,
|
|
position = { x = klinik.pos.x, y = klinik.pos.y, z = klinik.pos.z, w = klinik.pos.w },
|
|
leben = Tod.LEBEN_NACH_KLINIK,
|
|
kosten = bezahlt and Tod.KRANKENHAUS_KOSTEN or 0,
|
|
bezahlt = bezahlt,
|
|
grund = 'aufgegeben',
|
|
})
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Countdown: Verbluten und Sperrzeit
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(1000)
|
|
|
|
for quelle, eintrag in pairs(verletzte) do
|
|
if eintrag.sperre > 0 then
|
|
eintrag.sperre = eintrag.sperre - 1
|
|
end
|
|
|
|
eintrag.restBleed = eintrag.restBleed - 1
|
|
|
|
-- Zeit abgelaufen: Der Spieler verblutet und wacht in der Klinik auf
|
|
if eintrag.restBleed <= 0 then
|
|
local klinik = Tod.NaechstesKrankenhaus(
|
|
vector3(eintrag.position.x, eintrag.position.y, eintrag.position.z))
|
|
|
|
local bezahlt = Core:EntferneGeld(quelle, 'bargeld', Tod.KRANKENHAUS_KOSTEN, 'Behandlung Krankenhaus')
|
|
if not bezahlt then
|
|
bezahlt = Core:EntferneGeld(quelle, 'konto', Tod.KRANKENHAUS_KOSTEN, 'Behandlung Krankenhaus')
|
|
end
|
|
|
|
BeendeVerletzung(quelle)
|
|
|
|
TriggerClientEvent('aether:death:krankenhaus', quelle, {
|
|
klinik = klinik.name,
|
|
position = { x = klinik.pos.x, y = klinik.pos.y, z = klinik.pos.z, w = klinik.pos.w },
|
|
leben = Tod.LEBEN_NACH_KLINIK,
|
|
kosten = bezahlt and Tod.KRANKENHAUS_KOSTEN or 0,
|
|
bezahlt = bezahlt,
|
|
grund = 'verblutet',
|
|
})
|
|
else
|
|
SendeZustand(quelle)
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Position des Verletzten aktuell halten (er kann noch kriechen)
|
|
-- ---------------------------------------------------------------------
|
|
CreateThread(function()
|
|
while true do
|
|
Wait(5000)
|
|
for quelle, eintrag in pairs(verletzte) do
|
|
local ped = GetPlayerPed(quelle)
|
|
if ped and ped ~= 0 then
|
|
local position = GetEntityCoords(ped)
|
|
eintrag.position = { x = position.x, y = position.y, z = position.z }
|
|
end
|
|
end
|
|
end
|
|
end)
|
|
|
|
-- ---------------------------------------------------------------------
|
|
-- Aufraeumen
|
|
-- ---------------------------------------------------------------------
|
|
AddEventHandler('playerDropped', function()
|
|
verletzte[source] = nil
|
|
end)
|
|
|
|
-- =====================================================================
|
|
-- EXPORTS — fuer den Rettungsdienst
|
|
-- =====================================================================
|
|
|
|
--- Belebt einen verletzten Spieler wieder.
|
|
--- Wird vom EMS aufgerufen; prueft die Entfernung des Helfers.
|
|
--- @param quelle number Server-ID des Verletzten
|
|
--- @param helfer number|nil Server-ID des Sanitaeters (nil = ohne Pruefung)
|
|
--- @return boolean Erfolg
|
|
exports('Wiederbeleben', function(quelle, helfer)
|
|
local eintrag = verletzte[quelle]
|
|
if not eintrag then return false end
|
|
|
|
-- Entfernungspruefung, wenn ein Helfer angegeben ist
|
|
if helfer then
|
|
local hier = GetEntityCoords(GetPlayerPed(helfer))
|
|
local dort = GetEntityCoords(GetPlayerPed(quelle))
|
|
if #(hier - dort) > Tod.WIEDERBELEBUNG_WEITE then
|
|
return false
|
|
end
|
|
end
|
|
|
|
BeendeVerletzung(quelle)
|
|
|
|
TriggerClientEvent('aether:death:wiederbelebt', quelle, {
|
|
leben = Tod.LEBEN_NACH_HILFE,
|
|
})
|
|
return true
|
|
end)
|
|
|
|
--- Ist ein Spieler gerade verletzt am Boden?
|
|
--- @param quelle number Server-ID
|
|
--- @return boolean
|
|
exports('IstVerletzt', function(quelle)
|
|
return verletzte[quelle] ~= nil
|
|
end)
|
|
|
|
--- Liefert alle verletzten Spieler mit Position (fuer die EMS-Karte)
|
|
--- @return table
|
|
exports('HoleVerletzte', function()
|
|
local liste = {}
|
|
for quelle, eintrag in pairs(verletzte) do
|
|
liste[#liste + 1] = {
|
|
quelle = quelle,
|
|
position = eintrag.position,
|
|
emsGerufen = eintrag.emsGerufen,
|
|
restBleed = eintrag.restBleed,
|
|
}
|
|
end
|
|
return liste
|
|
end)
|