-- ===================================================================== -- 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)