-- ===================================================================== -- Aether Jobs — Serverlogik -- Der Server vergibt die Auftraege, bestimmt den Lohn und prueft die -- Zeit je Arbeitsschritt. Der Client kann nichts davon beeinflussen. -- ===================================================================== local Core = exports['aether-core'] -- Laufende Schichten: [quelle] = { -- jobId, auftrag, schrittIdx, schrittSeit, dienstBeginn, verdient, auftraege, fahrzeug -- } local schichten = {} -- --------------------------------------------------------------------- -- Dienstfahrzeuge -- --------------------------------------------------------------------- --- Stellt das Dienstfahrzeug eines Jobs bereit --- @param quelle number Server-ID --- @param job table Jobdefinition --- @return number|nil Entity-Handle local function StelleFahrzeug(quelle, job) if not job.fahrzeug then return nil end local spawn = job.fahrzeug.spawn local fahrzeug = CreateVehicle(GetHashKey(job.fahrzeug.modell), spawn.x, spawn.y, spawn.z, spawn.w, true, true) local versuche = 0 while not DoesEntityExist(fahrzeug) and versuche < 50 do Wait(10) versuche = versuche + 1 end if not DoesEntityExist(fahrzeug) then return nil end -- Kennzeichen als Dienstfahrzeug markieren Entity(fahrzeug).state:set('aetherJob', { job = job.id, besitzer = quelle, }, true) return fahrzeug end --- Entfernt das Dienstfahrzeug einer Schicht local function EntferneFahrzeug(schicht) if schicht and schicht.fahrzeug and DoesEntityExist(schicht.fahrzeug) then DeleteEntity(schicht.fahrzeug) end end -- --------------------------------------------------------------------- -- Hilfsfunktionen -- --------------------------------------------------------------------- local function CharakterVon(quelle) return Core:HoleCharakterId(quelle) end --- Laedt den Fortschritt aller Jobs eines Charakters --- @param charakterId number Charakter-ID --- @return table [jobId] = { level, xp, verdient, auftraege } local function HoleFortschritt(charakterId) local zeilen = MySQL.query.await( 'SELECT job, level, xp, verdient, auftraege FROM aether_jobs WHERE `character` = ?', { charakterId }) or {} local fortschritt = {} -- Standardwerte fuer alle bekannten Jobs for _, job in ipairs(Jobs.Liste) do fortschritt[job.id] = { level = 1, xp = 0, verdient = 0, auftraege = 0 } end for _, zeile in ipairs(zeilen) do if fortschritt[zeile.job] then fortschritt[zeile.job] = { level = zeile.level, xp = zeile.xp, verdient = zeile.verdient, auftraege = zeile.auftraege, } end end return fortschritt end --- Schreibt den Fortschritt eines Jobs zurueck local function SpeichereFortschritt(charakterId, jobId, werte) MySQL.query([[ INSERT INTO aether_jobs (`character`, job, level, xp, verdient, auftraege) VALUES (?, ?, ?, ?, ?, ?) ON DUPLICATE KEY UPDATE level = VALUES(level), xp = VALUES(xp), verdient = VALUES(verdient), auftraege = VALUES(auftraege) ]], { charakterId, jobId, werte.level, werte.xp, werte.verdient, werte.auftraege }) end --- Baut den Zustand fuer die Oberflaeche local function BaueZustand(quelle) local charakterId = CharakterVon(quelle) if not charakterId then return nil end local schicht = schichten[quelle] local aktiv = nil if schicht then local job = Jobs.Hole(schicht.jobId) aktiv = { jobId = schicht.jobId, schrittIdx = schicht.schrittIdx, aktuell = schicht.auftrag, verdient = schicht.verdient, auftraege = schicht.auftraege, dienstzeit = math.floor((os.time() - schicht.dienstBeginn)), schritte = job and job.schritte or {}, } end local person = Core:HoleSpielerInfo(quelle) return { jobs = Jobs.Liste, fortschritt = HoleFortschritt(charakterId), aktiv = aktiv, xpProLevel = Jobs.XP_PRO_LEVEL, schrittDauer= Jobs.SCHRITT_DAUER, geld = { bargeld = person and person.bargeld or 0, konto = person and person.konto or 0, }, } end local function SendeZustand(quelle, hinweis) local zustand = BaueZustand(quelle) if not zustand then return end zustand.hinweis = hinweis TriggerClientEvent('aether:jobs:zustand', quelle, zustand) end local function Ablehnen(quelle, grund) TriggerClientEvent('aether:jobs:fehler', quelle, grund) end -- --------------------------------------------------------------------- -- Jobcenter oeffnen -- --------------------------------------------------------------------- RegisterNetEvent('aether:jobs:oeffnen', function() local quelle = source if not CharakterVon(quelle) then return end -- Am Jobcenter oder bereits im Dienst (dann von ueberall einsehbar) if not schichten[quelle] then local position = GetEntityCoords(GetPlayerPed(quelle)) if #(position - Jobs.JOBCENTER) > (Jobs.INTERAKTION_WEITE + 3.0) then Ablehnen(quelle, 'Du bist nicht am Jobcenter.') return end end local zustand = BaueZustand(quelle) if zustand then TriggerClientEvent('aether:jobs:zeige', quelle, zustand) end end) -- --------------------------------------------------------------------- -- Dienst antreten -- --------------------------------------------------------------------- RegisterNetEvent('aether:jobs:antreten', function(jobId) local quelle = source local charakterId = CharakterVon(quelle) if not charakterId or type(jobId) ~= 'string' then return end local job = Jobs.Hole(jobId) if not job then return end if schichten[quelle] then Ablehnen(quelle, 'Du bist bereits im Dienst. Mach erst Feierabend.') return end local fortschritt = HoleFortschritt(charakterId) local level = fortschritt[jobId].level schichten[quelle] = { jobId = jobId, auftrag = Jobs.ErzeugeAuftrag(jobId, level), schrittIdx = 0, schrittSeit = 0, dienstBeginn = os.time(), verdient = 0, auftraege = 0, fahrzeug = nil, } -- Dienstfahrzeug bereitstellen, sofern der Job eines vorsieht if job.fahrzeug then schichten[quelle].fahrzeug = StelleFahrzeug(quelle, job) if schichten[quelle].fahrzeug then TriggerClientEvent('aether:jobs:fahrzeugBereit', quelle, NetworkGetNetworkIdFromEntity(schichten[quelle].fahrzeug), job.fahrzeug.spawn) end end SendeZustand(quelle, 'Dienst angetreten: ' .. job.name) end) -- --------------------------------------------------------------------- -- Arbeitsschritt ausfuehren -- --------------------------------------------------------------------- RegisterNetEvent('aether:jobs:schritt', function() local quelle = source local charakterId = CharakterVon(quelle) if not charakterId then return end local schicht = schichten[quelle] if not schicht then Ablehnen(quelle, 'Du bist nicht im Dienst.') return end local job = Jobs.Hole(schicht.jobId) if not job then return end local schrittDef = job.schritte[schicht.schrittIdx + 1] if not schrittDef then return end -- ---- Ortspruefung: Der Spieler muss wirklich dort stehen ---- local ziel = schicht.auftrag.orte and schicht.auftrag.orte[schicht.schrittIdx + 1] if not ziel then return end local position = GetEntityCoords(GetPlayerPed(quelle)) local abstand = #(position - vector3(ziel.position.x, ziel.position.y, ziel.position.z)) if abstand > (Jobs.ORT_RADIUS + 2.0) then Ablehnen(quelle, ('Du bist nicht am Arbeitsort (%s).'):format(ziel.name)) return end -- ---- Zeitpruefung: Die Handlung braucht ihre vorgesehene Dauer ---- local dauer = schrittDef.dauer or 4000 local jetzt = GetGameTimer() if schicht.schrittSeit > 0 and (jetzt - schicht.schrittSeit) < (dauer - 500) then Ablehnen(quelle, 'Zu schnell — arbeite den Schritt ab.') return end schicht.schrittSeit = jetzt schicht.schrittIdx = schicht.schrittIdx + 1 -- Noch nicht der letzte Schritt? Dann nur weiterzaehlen. if schicht.schrittIdx < #job.schritte then SendeZustand(quelle) return end -- ---- Auftrag abgeschlossen: bezahlen und Erfahrung gutschreiben ---- local auftrag = schicht.auftrag local fortschritt = HoleFortschritt(charakterId) local werte = fortschritt[schicht.jobId] -- Lohn kommt aus dem serverseitig erzeugten Auftrag Core:FuegeGeldHinzu(quelle, 'bargeld', auftrag.geld, 'Lohn ' .. job.name) werte.xp = werte.xp + auftrag.xp werte.verdient = werte.verdient + auftrag.geld werte.auftraege = werte.auftraege + 1 local aufgestiegen = false while werte.xp >= Jobs.XP_PRO_LEVEL and werte.level < Jobs.MAX_LEVEL do werte.xp = werte.xp - Jobs.XP_PRO_LEVEL werte.level = werte.level + 1 aufgestiegen = true end -- Auf Hoechstlevel keine Erfahrung mehr ansammeln if werte.level >= Jobs.MAX_LEVEL then werte.xp = 0 end SpeichereFortschritt(charakterId, schicht.jobId, werte) schicht.verdient = schicht.verdient + auftrag.geld schicht.auftraege = schicht.auftraege + 1 schicht.schrittIdx = 0 schicht.auftrag = Jobs.ErzeugeAuftrag(schicht.jobId, werte.level) local hinweis = ('Auftrag erledigt · +%d €'):format(auftrag.geld) if aufgestiegen then hinweis = hinweis .. (' · Level %d erreicht!'):format(werte.level) end SendeZustand(quelle, hinweis) end) -- --------------------------------------------------------------------- -- Feierabend -- --------------------------------------------------------------------- RegisterNetEvent('aether:jobs:feierabend', function() local quelle = source local schicht = schichten[quelle] if not schicht then return end local job = Jobs.Hole(schicht.jobId) local dauer = math.floor((os.time() - schicht.dienstBeginn) / 60) EntferneFahrzeug(schicht) schichten[quelle] = nil SendeZustand(quelle, ('Feierabend · %d € in %d Minuten verdient.') :format(schicht.verdient, dauer)) if job then Aether.Debug(('%s hat den Dienst als %s beendet (%d €).') :format(GetPlayerName(quelle), job.name, schicht.verdient)) end end) -- --------------------------------------------------------------------- -- Aufraeumen -- --------------------------------------------------------------------- RegisterNetEvent('aether:jobs:schliessen', function() -- Die Schicht laeuft weiter, nur die Oberflaeche wird geschlossen end) AddEventHandler('playerDropped', function() EntferneFahrzeug(schichten[source]) schichten[source] = nil end) --- Beim Stoppen der Ressource alle Dienstfahrzeuge entfernen AddEventHandler('onResourceStop', function(ressource) if ressource ~= GetCurrentResourceName() then return end for _, schicht in pairs(schichten) do EntferneFahrzeug(schicht) end end) -- ===================================================================== -- EXPORTS -- ===================================================================== --- Liefert den aktuellen Job eines Spielers (oder nil) exports('HoleAktivenJob', function(quelle) local schicht = schichten[quelle] return schicht and schicht.jobId or nil end) --- Liefert das Level eines Spielers in einem Job exports('HoleLevel', function(quelle, jobId) local charakterId = CharakterVon(quelle) if not charakterId then return 0 end local fortschritt = HoleFortschritt(charakterId) return fortschritt[jobId] and fortschritt[jobId].level or 1 end)