aether-framework/resources/[aether]/aether-jobs/client/main.lua
Jerrit Fritzsche e4b82dd685 Aether-Framework: Fachmodule, Admin-Panel und Docker-Datenbank
- 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>
2026-07-27 18:55:32 +02:00

345 lines
12 KiB
Lua

-- =====================================================================
-- Aether Jobs — Clientlogik
-- Die Arbeit findet in der Welt statt: Marker, Route, Animation.
-- Der Server prueft Ort und Dauer, bevor er bezahlt.
-- =====================================================================
local istOffen = false
local istGeladen = false
-- Aktuelle Schicht (vom Server gespiegelt)
local schicht = nil -- { jobId, schrittIdx, auftrag, schritte, farbe, name }
local arbeitetGerade = false -- laeuft gerade eine Handlung?
local zielBlip = nil -- Markierung des aktuellen Arbeitsorts
local TASTE_BENUTZEN = 38 -- E
local function SendeAnNui(nachricht)
SendNuiMessage(json.encode(nachricht))
end
local function ZeigeSpielmeldung(text)
if not text or text == '' then return end
BeginTextCommandThefeedPost('STRING')
AddTextComponentSubstringPlayerName(text)
EndTextCommandThefeedPostTicker(false, true)
end
local function Schliessen()
if not istOffen then return end
istOffen = false
SetNuiFocus(false, false)
SendeAnNui({ aktion = 'schliessen' })
TriggerServerEvent('aether:jobs:schliessen')
pcall(function() exports['aether-hud']:SetzeSichtbar(true) end)
end
-- ---------------------------------------------------------------------
-- Wegpunkt zum aktuellen Arbeitsort
-- ---------------------------------------------------------------------
--- Liefert den Ort des aktuellen Arbeitsschritts
--- @return table|nil { position, name }
local function AktuellerOrt()
if not schicht or not schicht.auftrag or not schicht.auftrag.orte then return nil end
return schicht.auftrag.orte[schicht.schrittIdx + 1]
end
--- Setzt die Kartenmarkierung auf den aktuellen Arbeitsort
local function AktualisiereZielBlip()
if zielBlip then
RemoveBlip(zielBlip)
zielBlip = nil
end
local ort = AktuellerOrt()
if not ort then return end
zielBlip = AddBlipForCoord(ort.position.x, ort.position.y, ort.position.z)
SetBlipSprite(zielBlip, 1)
SetBlipColour(zielBlip, 5)
SetBlipScale(zielBlip, 0.9)
SetBlipRoute(zielBlip, true) -- Route auf der Karte anzeigen
SetBlipRouteColour(zielBlip, 5)
BeginTextCommandSetBlipName('STRING')
AddTextComponentString('Arbeitsort: ' .. (ort.name or ''))
EndTextCommandSetBlipName(zielBlip)
end
--- Entfernt die Zielmarkierung
local function EntferneZielBlip()
if zielBlip then
RemoveBlip(zielBlip)
zielBlip = nil
end
end
-- ---------------------------------------------------------------------
-- Arbeitshandlung ausfuehren
-- ---------------------------------------------------------------------
--- Spielt die Handlung ab und meldet sie danach dem Server
--- @param schrittDef table Schrittdefinition
local function FuehreHandlungAus(schrittDef)
if arbeitetGerade then return end
arbeitetGerade = true
local ped = PlayerPedId()
local dauer = schrittDef.dauer or 4000
-- Passendes Szenario abspielen, damit die Handlung sichtbar ist
if schrittDef.szenario then
TaskStartScenarioInPlace(ped, schrittDef.szenario, 0, true)
end
FreezeEntityPosition(ped, true)
-- Fortschrittsbalken anzeigen
local start = GetGameTimer()
CreateThread(function()
while arbeitetGerade do
local vergangen = GetGameTimer() - start
local anteil = math.min(1.0, vergangen / dauer)
-- Balken am unteren Bildschirmrand
DrawRect(0.5, 0.86, 0.24, 0.028, 0, 0, 0, 170)
DrawRect(0.38 + 0.12 * anteil, 0.86, 0.24 * anteil, 0.028, 90, 200, 120, 220)
SetTextFont(4)
SetTextScale(0.34, 0.34)
SetTextColour(255, 255, 255, 230)
SetTextCentre(true)
SetTextEntry('STRING')
AddTextComponentString(schrittDef.text or 'Arbeite …')
SetTextOutline()
DrawText(0.5, 0.895)
Wait(0)
end
end)
Wait(dauer)
ClearPedTasks(ped)
FreezeEntityPosition(ped, false)
arbeitetGerade = false
-- Erst jetzt meldet der Client den Schritt; der Server prueft Ort und Zeit
TriggerServerEvent('aether:jobs:schritt')
end
-- ---------------------------------------------------------------------
-- Ereignisse vom Server
-- ---------------------------------------------------------------------
AddEventHandler('aether:client:bereit', function()
istGeladen = true
end)
RegisterNetEvent('aether:jobs:zeige', function(zustand)
istOffen = true
SetNuiFocus(true, true)
pcall(function() exports['aether-hud']:SetzeSichtbar(false) end)
zustand.aktion = 'oeffnen'
SendeAnNui(zustand)
end)
RegisterNetEvent('aether:jobs:zustand', function(zustand)
-- Schicht lokal spiegeln, damit Marker und Anzeige stimmen
if zustand.aktiv then
local job = nil
for _, j in ipairs(zustand.jobs or {}) do
if j.id == zustand.aktiv.jobId then job = j break end
end
schicht = {
jobId = zustand.aktiv.jobId,
schrittIdx = zustand.aktiv.schrittIdx,
auftrag = zustand.aktiv.aktuell,
schritte = job and job.schritte or {},
name = job and job.name or '',
}
AktualisiereZielBlip()
else
schicht = nil
EntferneZielBlip()
end
if istOffen then
zustand.aktion = 'zustand'
SendeAnNui(zustand)
elseif zustand.hinweis then
ZeigeSpielmeldung(zustand.hinweis)
end
end)
RegisterNetEvent('aether:jobs:fehler', function(text)
if istOffen then
SendeAnNui({ aktion = 'fehler', text = text })
else
ZeigeSpielmeldung(text)
end
end)
--- Dienstfahrzeug steht bereit
RegisterNetEvent('aether:jobs:fahrzeugBereit', function(netId, spawn)
Schliessen()
local versuche = 0
while not NetworkDoesEntityExistWithNetworkId(netId) and versuche < 100 do
Wait(50)
versuche = versuche + 1
end
if not NetworkDoesEntityExistWithNetworkId(netId) then return end
local fahrzeug = NetToVeh(netId)
if fahrzeug and DoesEntityExist(fahrzeug) then
SetVehicleNumberPlateText(fahrzeug, 'DIENST')
ZeigeSpielmeldung('~g~Dienstfahrzeug~s~ steht bereit — folge der Markierung.')
-- Wegpunkt zum Fahrzeug setzen, damit man es findet
SetNewWaypoint(spawn.x, spawn.y)
end
end)
-- ---------------------------------------------------------------------
-- Jobcenter in der Naehe erkennen
-- ---------------------------------------------------------------------
CreateThread(function()
local blip = AddBlipForCoord(Jobs.JOBCENTER.x, Jobs.JOBCENTER.y, Jobs.JOBCENTER.z)
SetBlipSprite(blip, 407)
SetBlipColour(blip, 5)
SetBlipScale(blip, 0.8)
SetBlipAsShortRange(blip, true)
BeginTextCommandSetBlipName('STRING')
AddTextComponentString('Jobcenter')
EndTextCommandSetBlipName(blip)
while true do
local pause = 800
if istGeladen and not istOffen and not schicht then
local position = GetEntityCoords(PlayerPedId())
if #(position - Jobs.JOBCENTER) <= Jobs.INTERAKTION_WEITE then
pause = 0
SetTextFont(4)
SetTextScale(0.34, 0.34)
SetTextColour(255, 255, 255, 215)
SetTextCentre(true)
SetTextOutline()
SetTextEntry('STRING')
AddTextComponentString('[E] Jobcenter')
SetDrawOrigin(Jobs.JOBCENTER.x, Jobs.JOBCENTER.y, Jobs.JOBCENTER.z + 0.9, 0)
DrawText(0.0, 0.0)
ClearDrawOrigin()
if IsControlJustReleased(0, TASTE_BENUTZEN) then
TriggerServerEvent('aether:jobs:oeffnen')
end
end
end
Wait(pause)
end
end)
-- ---------------------------------------------------------------------
-- Arbeitsort: Marker, Aufgabenanzeige und Aktion
-- ---------------------------------------------------------------------
CreateThread(function()
while true do
local pause = 500
if schicht and not istOffen and not arbeitetGerade then
local ort = AktuellerOrt()
local schrittDef = schicht.schritte[schicht.schrittIdx + 1]
if ort and schrittDef then
pause = 0
local position = GetEntityCoords(PlayerPedId())
local ziel = vector3(ort.position.x, ort.position.y, ort.position.z)
local abstand = #(position - ziel)
-- Aufgabenanzeige oben links
SetTextFont(4)
SetTextScale(0.36, 0.36)
SetTextColour(255, 255, 255, 220)
SetTextEntry('STRING')
AddTextComponentString(('~y~%s~s~ %s'):format(schicht.name, schrittDef.text))
SetTextOutline()
DrawText(0.012, 0.62)
SetTextFont(4)
SetTextScale(0.31, 0.31)
SetTextColour(200, 200, 210, 190)
SetTextEntry('STRING')
AddTextComponentString(('Ziel: %s · %d m'):format(ort.name or '', math.floor(abstand)))
SetTextOutline()
DrawText(0.012, 0.655)
-- Marker am Arbeitsort, sobald er in Sichtweite ist
if abstand < 60.0 then
DrawMarker(1, ziel.x, ziel.y, ziel.z - 0.95, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0,
1.6, 1.6, 0.7, 90, 200, 120, 120, false, false, 2, false, nil, nil, false)
end
-- In Reichweite: Aktion anbieten
if abstand <= Jobs.ORT_RADIUS then
SetTextFont(4)
SetTextScale(0.34, 0.34)
SetTextColour(255, 255, 255, 220)
SetTextCentre(true)
SetTextOutline()
SetTextEntry('STRING')
AddTextComponentString('[E] ' .. (schrittDef.btn or 'Arbeiten'))
SetDrawOrigin(ziel.x, ziel.y, ziel.z + 0.9, 0)
DrawText(0.0, 0.0)
ClearDrawOrigin()
if IsControlJustReleased(0, TASTE_BENUTZEN) then
FuehreHandlungAus(schrittDef)
end
end
end
end
Wait(pause)
end
end)
--- Taste J: Arbeitsansicht oeffnen
RegisterCommand('job', function()
if istGeladen and not istOffen then
TriggerServerEvent('aether:jobs:oeffnen')
end
end, false)
RegisterKeyMapping('job', 'Jobcenter / Arbeitsansicht öffnen', 'keyboard', 'J')
-- ---------------------------------------------------------------------
-- NUI-Callbacks
-- ---------------------------------------------------------------------
RegisterNUICallback('schliessen', function(_, rueckgabe)
Schliessen()
rueckgabe({ ok = true })
end)
RegisterNUICallback('antreten', function(daten, rueckgabe)
TriggerServerEvent('aether:jobs:antreten', tostring(daten.jobId or ''))
Schliessen() -- gearbeitet wird in der Welt, nicht im Menue
rueckgabe({ ok = true })
end)
RegisterNUICallback('feierabend', function(_, rueckgabe)
TriggerServerEvent('aether:jobs:feierabend')
rueckgabe({ ok = true })
end)
-- ---------------------------------------------------------------------
-- Aufraeumen
-- ---------------------------------------------------------------------
AddEventHandler('onResourceStop', function(ressource)
if ressource ~= GetCurrentResourceName() then return end
SetNuiFocus(false, false)
EntferneZielBlip()
ClearPedTasks(PlayerPedId())
FreezeEntityPosition(PlayerPedId(), false)
end)