- 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>
272 lines
12 KiB
Lua
272 lines
12 KiB
Lua
-- =====================================================================
|
||
-- Aether Jobs — Jobdefinitionen, Arbeitsorte und Auftragsgenerator
|
||
--
|
||
-- Jeder Arbeitsschritt hat einen ECHTEN Ort in der Welt. Der Spieler
|
||
-- muss hinfahren; der Server prueft die Entfernung, bevor er bezahlt.
|
||
-- =====================================================================
|
||
|
||
Jobs = Jobs or {}
|
||
|
||
Jobs.XP_PRO_LEVEL = 120 -- Erfahrung bis zum naechsten Level
|
||
Jobs.MAX_LEVEL = 20 -- Hoechstlevel je Job
|
||
|
||
-- Wie nah man am Arbeitsort stehen muss (Meter)
|
||
Jobs.ORT_RADIUS = 4.0
|
||
-- Reichweite zum Jobcenter (Meter)
|
||
Jobs.INTERAKTION_WEITE = 2.0
|
||
|
||
-- Standort des Jobcenters (Innenstadt)
|
||
Jobs.JOBCENTER = vector3(-265.0, -963.5, 31.2)
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Abgabeorte im Stadtgebiet
|
||
-- Werden fuer Zustellungen, Taxifahrten und Frachten zufaellig gewaehlt.
|
||
-- ---------------------------------------------------------------------
|
||
Jobs.Abgabeorte = {
|
||
{ name = 'Legion Square', pos = vector3(195.9, -933.2, 30.7) },
|
||
{ name = 'Vinewood Blvd', pos = vector3(297.4, 180.6, 104.4) },
|
||
{ name = 'Vespucci Beach', pos = vector3(-1233.6, -1490.3, 4.4) },
|
||
{ name = 'Grove Street', pos = vector3(105.8, -1940.0, 21.3) },
|
||
{ name = 'Sandy Shores', pos = vector3(1961.0, 3740.5, 32.3) },
|
||
{ name = 'Del Perro', pos = vector3(-1305.6, -394.2, 36.7) },
|
||
{ name = 'Mirror Park', pos = vector3(1136.5, -982.3, 46.0) },
|
||
{ name = 'Downtown', pos = vector3(-247.5, -883.4, 31.2) },
|
||
{ name = 'Rockford Hills', pos = vector3(-1108.5, -455.2, 36.2) },
|
||
{ name = 'Paleto Bay', pos = vector3(-127.0, 6386.0, 31.5) },
|
||
}
|
||
|
||
-- Zufallsdaten fuer Fang und Erz
|
||
Jobs.Fische = {
|
||
{ n = 'alter Stiefel', w = 5 }, { n = 'Hering', w = 55 }, { n = 'Makrele', w = 85 },
|
||
{ n = 'Wolfsbarsch', w = 130 }, { n = 'Lachs', w = 190 }, { n = 'Thunfisch', w = 340 },
|
||
}
|
||
Jobs.Erze = {
|
||
{ n = 'Kohle', w = 45 }, { n = 'Eisenerz', w = 80 }, { n = 'Kupfererz', w = 120 },
|
||
{ n = 'Silbererz', w = 220 }, { n = 'Goldnugget', w = 480 },
|
||
}
|
||
|
||
-- ---------------------------------------------------------------------
|
||
-- Jobs
|
||
--
|
||
-- Je Schritt:
|
||
-- text Aufgabenbeschreibung
|
||
-- btn Beschriftung der Aktion
|
||
-- ort fester vector3 ODER 'zufaellig' (aus Jobs.Abgabeorte)
|
||
-- ortName Anzeigename bei festem Ort
|
||
-- dauer Wie lange die Handlung dauert (ms)
|
||
-- szenario GTA-Szenario, das dabei gespielt wird (optional)
|
||
--
|
||
-- fahrzeug: Dienstfahrzeug, das beim Antreten gestellt wird
|
||
-- ---------------------------------------------------------------------
|
||
Jobs.Liste = {
|
||
{
|
||
id = 'liefer', name = 'Lieferdienst', icon = '📦', kat = 'Service', farbe = '#4aa8ff',
|
||
beschreibung = 'Pakete am Depot laden und im Stadtgebiet ausliefern.',
|
||
lohnVon = 120, lohnBis = 180, xp = 30,
|
||
fahrzeug = { modell = 'boxville2', spawn = vector4(-424.9, -2789.5, 6.0, 90.0) },
|
||
schritte = {
|
||
{ text = 'Fahre zum Depot und lade das Paket ein.', btn = '📦 Paket laden',
|
||
ort = vector3(-419.5, -2792.3, 6.0), ortName = 'Depot Terminal',
|
||
dauer = 5000, szenario = 'PROP_HUMAN_BUM_BIN' },
|
||
{ text = 'Liefere das Paket beim Kunden aus.', btn = '✅ Zustellen',
|
||
ort = 'zufaellig', dauer = 4000, szenario = 'WORLD_HUMAN_CLIPBOARD' },
|
||
},
|
||
},
|
||
{
|
||
id = 'muell', name = 'Müllabfuhr', icon = '🗑️', kat = 'Service', farbe = '#7bc96f',
|
||
beschreibung = 'Mülltonnen einsammeln und am Wertstoffhof entladen.',
|
||
lohnVon = 90, lohnBis = 140, xp = 26,
|
||
fahrzeug = { modell = 'trash', spawn = vector4(-343.1, -1560.2, 25.5, 270.0) },
|
||
schritte = {
|
||
{ text = 'Fahre die Route ab und sammle die Tonnen ein.', btn = '🗑️ Tonnen leeren',
|
||
ort = 'zufaellig', dauer = 6000, szenario = 'PROP_HUMAN_BUM_BIN' },
|
||
{ text = 'Entlade am Wertstoffhof.', btn = '♻️ Entladen',
|
||
ort = vector3(-351.6, -1546.1, 25.2), ortName = 'Wertstoffhof',
|
||
dauer = 5000, szenario = 'WORLD_HUMAN_CLIPBOARD' },
|
||
},
|
||
},
|
||
{
|
||
id = 'taxi', name = 'Taxi', icon = '🚕', kat = 'Transport', farbe = '#facc15',
|
||
beschreibung = 'Fahrgäste abholen und ans Ziel bringen.',
|
||
lohnVon = 80, lohnBis = 250, xp = 22,
|
||
fahrzeug = { modell = 'taxi', spawn = vector4(895.8, -179.1, 74.7, 240.0) },
|
||
schritte = {
|
||
{ text = 'Hole den Fahrgast ab.', btn = '🧍 Fahrgast aufnehmen',
|
||
ort = 'zufaellig', dauer = 3000 },
|
||
{ text = 'Bringe den Fahrgast ans Ziel.', btn = '🏁 Fahrgast absetzen',
|
||
ort = 'zufaellig', dauer = 3000 },
|
||
},
|
||
},
|
||
{
|
||
id = 'trucker', name = 'Trucker', icon = '🚛', kat = 'Transport', farbe = '#f0684a',
|
||
beschreibung = 'Fracht über Land transportieren — gut bezahlt.',
|
||
lohnVon = 420, lohnBis = 680, xp = 45,
|
||
fahrzeug = { modell = 'mule3', spawn = vector4(151.6, -3211.0, 5.9, 90.0) },
|
||
schritte = {
|
||
{ text = 'Beladung am Lagerhaus vorbereiten.', btn = '📦 Beladen',
|
||
ort = vector3(158.7, -3212.5, 5.9), ortName = 'Hafenlager',
|
||
dauer = 8000, szenario = 'WORLD_HUMAN_CLIPBOARD' },
|
||
{ text = 'Fahre die Fracht zum Zielort.', btn = '🛣️ Ausliefern',
|
||
ort = 'zufaellig', dauer = 6000, szenario = 'WORLD_HUMAN_CLIPBOARD' },
|
||
},
|
||
},
|
||
{
|
||
id = 'tankwart', name = 'Tankwart', icon = '⛽', kat = 'Service', farbe = '#f5a742',
|
||
beschreibung = 'Kunden an der Zapfsäule bedienen und abkassieren.',
|
||
lohnVon = 100, lohnBis = 160, xp = 24,
|
||
schritte = {
|
||
{ text = 'Bediene die Zapfsäule für den Kunden.', btn = '⛽ Betanken',
|
||
ort = vector3(265.1, -1261.3, 29.3), ortName = 'Tankstelle Strawberry',
|
||
dauer = 6000, szenario = 'WORLD_HUMAN_VEHICLE_MECHANIC' },
|
||
{ text = 'Kassiere an der Kasse.', btn = '💳 Kassieren',
|
||
ort = vector3(263.9, -1256.4, 29.6), ortName = 'Kasse',
|
||
dauer = 3000, szenario = 'WORLD_HUMAN_CLIPBOARD' },
|
||
},
|
||
},
|
||
{
|
||
id = 'angeln', name = 'Angeln', icon = '🎣', kat = 'Natur', farbe = '#3fc7e0',
|
||
beschreibung = 'Am Pier angeln und den Fang am Stand verkaufen.',
|
||
lohnVon = 5, lohnBis = 340, xp = 18,
|
||
schritte = {
|
||
{ text = 'Wirf die Angel aus und warte auf einen Biss.', btn = '🎣 Angeln',
|
||
ort = vector3(-1850.1, -1245.9, 8.6), ortName = 'Pier Vespucci',
|
||
dauer = 12000, szenario = 'WORLD_HUMAN_STAND_FISHING' },
|
||
{ text = 'Verkaufe den Fang am Fischstand.', btn = '💰 Verkaufen',
|
||
ort = vector3(-1817.5, -1193.6, 14.3), ortName = 'Fischstand',
|
||
dauer = 3000, szenario = 'WORLD_HUMAN_CLIPBOARD' },
|
||
},
|
||
},
|
||
{
|
||
id = 'holz', name = 'Holzfäller', icon = '🪓', kat = 'Natur', farbe = '#a3703f',
|
||
beschreibung = 'Bäume fällen und die Stämme zum Sägewerk bringen.',
|
||
lohnVon = 105, lohnBis = 280, xp = 28,
|
||
schritte = {
|
||
{ text = 'Fälle den markierten Baum.', btn = '🪓 Baum fällen',
|
||
ort = vector3(-552.6, 5324.9, 73.6), ortName = 'Wald bei Paleto',
|
||
dauer = 10000, szenario = 'WORLD_HUMAN_HAMMERING' },
|
||
{ text = 'Bringe die Stämme zum Sägewerk.', btn = '🪵 Abliefern',
|
||
ort = vector3(-501.3, 5305.5, 79.5), ortName = 'Sägewerk',
|
||
dauer = 5000, szenario = 'WORLD_HUMAN_CLIPBOARD' },
|
||
},
|
||
},
|
||
{
|
||
id = 'bergbau', name = 'Bergbau', icon = '⛏️', kat = 'Natur', farbe = '#9a9aa0',
|
||
beschreibung = 'Erz in der Mine abbauen, einschmelzen und verkaufen.',
|
||
lohnVon = 45, lohnBis = 480, xp = 30,
|
||
schritte = {
|
||
{ text = 'Baue das Erz in der Mine ab.', btn = '⛏️ Erz abbauen',
|
||
ort = vector3(2949.6, 2792.2, 40.9), ortName = 'Mine Sandy Shores',
|
||
dauer = 12000, szenario = 'WORLD_HUMAN_HAMMERING' },
|
||
{ text = 'Schmelze das Erz ein und verkaufe es.', btn = '🔥 Einschmelzen',
|
||
ort = vector3(1109.4, -2007.9, 31.0), ortName = 'Schmelze Cypress Flats',
|
||
dauer = 8000, szenario = 'WORLD_HUMAN_WELDING' },
|
||
},
|
||
},
|
||
}
|
||
|
||
--- Liefert die Definition eines Jobs
|
||
--- @param jobId string Job-Kennung
|
||
--- @return table|nil
|
||
function Jobs.Hole(jobId)
|
||
for _, job in ipairs(Jobs.Liste) do
|
||
if job.id == jobId then return job end
|
||
end
|
||
return nil
|
||
end
|
||
|
||
local function zufall(a, b) return math.random(a, b) end
|
||
local function waehle(liste) return liste[math.random(#liste)] end
|
||
|
||
--- Loest die Orte aller Schritte eines Auftrags zu echten Koordinaten auf.
|
||
--- Wird nur serverseitig aufgerufen.
|
||
--- @param job table Jobdefinition
|
||
--- @return table Liste { position = {x,y,z}, name = ... } je Schritt
|
||
local function LoeseOrteAuf(job)
|
||
local orte = {}
|
||
local bereitsGewaehlt = nil
|
||
|
||
for _, schritt in ipairs(job.schritte) do
|
||
if schritt.ort == 'zufaellig' then
|
||
-- Nicht zweimal denselben Ort im selben Auftrag verwenden
|
||
local ziel
|
||
repeat ziel = waehle(Jobs.Abgabeorte)
|
||
until not bereitsGewaehlt or ziel.name ~= bereitsGewaehlt or #Jobs.Abgabeorte < 2
|
||
bereitsGewaehlt = ziel.name
|
||
orte[#orte + 1] = {
|
||
position = { x = ziel.pos.x, y = ziel.pos.y, z = ziel.pos.z },
|
||
name = ziel.name,
|
||
}
|
||
else
|
||
orte[#orte + 1] = {
|
||
position = { x = schritt.ort.x, y = schritt.ort.y, z = schritt.ort.z },
|
||
name = schritt.ortName or 'Arbeitsort',
|
||
}
|
||
end
|
||
end
|
||
return orte
|
||
end
|
||
|
||
--- Erzeugt einen Auftrag fuer einen Job.
|
||
--- Wird ausschliesslich serverseitig aufgerufen — Lohn und Orte duerfen
|
||
--- nie vom Client kommen.
|
||
--- @param jobId string Job-Kennung
|
||
--- @param level number Aktuelles Level des Spielers
|
||
--- @return table|nil Auftrag { ziel, geld, xp, info?, fang?, orte }
|
||
function Jobs.ErzeugeAuftrag(jobId, level)
|
||
local job = Jobs.Hole(jobId)
|
||
if not job then return nil end
|
||
|
||
local bonus = 1 + (math.max(1, level) - 1) * 0.05
|
||
local orte = LoeseOrteAuf(job)
|
||
|
||
local auftrag
|
||
if jobId == 'taxi' then
|
||
auftrag = {
|
||
ziel = orte[1].name .. ' → ' .. orte[2].name,
|
||
geld = math.floor(zufall(80, 250) * bonus + 0.5),
|
||
xp = job.xp,
|
||
}
|
||
elseif jobId == 'angeln' then
|
||
local fisch = waehle(Jobs.Fische)
|
||
auftrag = {
|
||
ziel = 'Pier Vespucci',
|
||
geld = math.floor(fisch.w * (1 + (level - 1) * 0.06) + 0.5),
|
||
xp = job.xp,
|
||
fang = fisch.n,
|
||
}
|
||
elseif jobId == 'holz' then
|
||
local anzahl = zufall(3, 8)
|
||
auftrag = {
|
||
ziel = 'Wald bei Paleto',
|
||
geld = math.floor(anzahl * 35 * bonus + 0.5),
|
||
xp = job.xp,
|
||
fang = anzahl .. ' Stämme',
|
||
}
|
||
elseif jobId == 'bergbau' then
|
||
local erz = waehle(Jobs.Erze)
|
||
local anzahl = zufall(2, 6)
|
||
auftrag = {
|
||
ziel = 'Mine Sandy Shores',
|
||
geld = math.floor(erz.w * anzahl * 0.5 * bonus + 0.5),
|
||
xp = job.xp,
|
||
fang = anzahl .. '× ' .. erz.n,
|
||
}
|
||
else
|
||
local vorsatz = {
|
||
liefer = 'Zustellung: ',
|
||
muell = 'Route: ',
|
||
trucker = 'Fracht nach ',
|
||
tankwart = 'Schicht an der ',
|
||
}
|
||
-- Beim Standardauftrag benennt der letzte Ort das Ziel
|
||
auftrag = {
|
||
ziel = (vorsatz[jobId] or '') .. orte[#orte].name,
|
||
geld = math.floor(zufall(job.lohnVon, job.lohnBis) * bonus + 0.5),
|
||
xp = job.xp,
|
||
}
|
||
end
|
||
|
||
auftrag.orte = orte
|
||
return auftrag
|
||
end
|