diff --git a/resources/[freizeit]/nordi_ai_train/client.lua b/resources/[freizeit]/nordi_ai_train/client.lua new file mode 100644 index 000000000..0dda1b1e4 --- /dev/null +++ b/resources/[freizeit]/nordi_ai_train/client.lua @@ -0,0 +1,446 @@ +local QBCore = exports['qb-core']:GetCoreObject() +local lib = exports.ox_lib +local PlayerData = {} +local nearbyTrains = {} +local currentTrain = nil +local isRiding = false +local cinemaCam = nil + +RegisterNetEvent('QBCore:Client:OnPlayerLoaded', function() + PlayerData = QBCore.Functions.GetPlayerData() + CreateStationBlips() +end) + +-- Bahnhof Blips erstellen +function CreateStationBlips() + for _, station in pairs(Config.TrainStations) do + local blip = AddBlipForCoord(station.coords.x, station.coords.y, station.coords.z) + SetBlipSprite(blip, station.blip.sprite) + SetBlipDisplay(blip, 4) + SetBlipScale(blip, station.blip.scale) + SetBlipColour(blip, station.blip.color) + BeginTextCommandSetBlipName("STRING") + AddTextComponentString("🚂 " .. station.name) + EndTextCommandSetBlipName(blip) + end +end + +-- Zug spawnen +function SpawnTrainAtLocation(station) + local model = GetHashKey(Config.TrainCars.main) + + RequestModel(model) + while not HasModelLoaded(model) do + Wait(500) + end + + local train = CreateMissionTrain(24, station.coords.x, station.coords.y, station.coords.z, true) + + if DoesEntityExist(train) then + SetEntityHeading(train, station.coords.w) + SetTrainSpeed(train, 0.0) + SetTrainCruiseSpeed(train, 0.0) + + -- Waggons hinzufügen + Wait(1000) + for _, carModel in pairs(Config.TrainCars.cars) do + local carHash = GetHashKey(carModel) + RequestModel(carHash) + while not HasModelLoaded(carHash) do + Wait(500) + end + CreateMissionTrainCar(train, carHash, false, false, false) + end + + if Config.Debug then + print("Zug gespawnt bei: " .. station.name) + end + + return train + end + return nil +end + +-- Züge in der Nähe finden +function FindNearbyTrains() + local playerPed = PlayerPedId() + local playerCoords = GetEntityCoords(playerPed) + local trains = {} + + local vehicles = GetGamePool('CVehicle') + for _, vehicle in pairs(vehicles) do + if DoesEntityExist(vehicle) then + local model = GetEntityModel(vehicle) + + if IsThisModelATrain(model) then + local trainCoords = GetEntityCoords(vehicle) + local distance = #(playerCoords - trainCoords) + + if distance <= Config.InteractionDistance then + table.insert(trains, { + entity = vehicle, + coords = trainCoords, + distance = distance + }) + end + end + end + end + + return trains +end + +-- ox_lib Zielmenü öffnen +function OpenDestinationMenu(train) + local playerCoords = GetEntityCoords(PlayerPedId()) + local currentStation = GetNearestStation(playerCoords) + + local options = {} + + for _, station in pairs(Config.TrainStations) do + if station.name ~= currentStation then + local icon = Config.Menu.stationIcons.city -- Standard Icon + + -- Icon basierend auf Station wählen + if string.find(station.name:lower(), "depot") then + icon = Config.Menu.stationIcons.depot + elseif string.find(station.name:lower(), "island") or string.find(station.name:lower(), "terminal") then + icon = Config.Menu.stationIcons.port + elseif string.find(station.name:lower(), "industrial") then + icon = Config.Menu.stationIcons.industrial + elseif string.find(station.name:lower(), "bay") then + icon = Config.Menu.stationIcons.rural + end + + table.insert(options, { + title = icon .. " " .. station.name, + description = station.description .. " - " .. Config.Menu.texts.price .. ": $" .. station.price, + icon = 'train', + onSelect = function() + SelectDestination(train, station) + end, + metadata = { + {label = Config.Menu.texts.price, value = "$" .. station.price}, + {label = "Entfernung", value = math.floor(#(playerCoords - vector3(station.coords.x, station.coords.y, station.coords.z))) .. "m"} + } + }) + end + end + + table.insert(options, { + title = "❌ " .. Config.Menu.texts.cancel, + description = "Menü schließen", + icon = 'xmark' + }) + + lib:registerContext({ + id = 'train_destination_menu', + title = Config.Menu.title, + options = options, + position = Config.Menu.position + }) + + lib:showContext('train_destination_menu') +end + +-- Ziel auswählen +function SelectDestination(train, destination) + if not DoesEntityExist(train) then + lib:notify({ + title = 'Fehler', + description = Config.Menu.texts.trainNotAvailable, + type = Config.Notifications.types.error, + duration = Config.Notifications.duration.short + }) + return + end + + -- Geld prüfen + QBCore.Functions.TriggerCallback('train:server:canAfford', function(canAfford) + if canAfford then + StartTrainJourney(train, destination) + else + lib:notify({ + title = 'Nicht genug Geld', + description = Config.Menu.texts.notEnoughMoney .. " ($" .. destination.price .. ")", + type = Config.Notifications.types.error, + duration = Config.Notifications.duration.medium + }) + end + end, destination.price) +end + +-- Nächste Station finden +function GetNearestStation(coords) + local nearestStation = nil + local nearestDistance = math.huge + + for _, station in pairs(Config.TrainStations) do + local distance = #(coords - vector3(station.coords.x, station.coords.y, station.coords.z)) + if distance < nearestDistance then + nearestDistance = distance + nearestStation = station.name + end + end + + return nearestStation +end + +-- Zugfahrt starten +function StartTrainJourney(train, destination) + local playerPed = PlayerPedId() + currentTrain = train + isRiding = true + + -- Spieler in Zug setzen + SetPedIntoVehicle(playerPed, train, 1) + + -- Benachrichtigung + lib:notify({ + title = '🚂 ' .. Config.Menu.texts.journeyStarted, + description = "Fahrt nach " .. destination.name, + type = Config.Notifications.types.success, + duration = Config.Notifications.duration.medium + }) + + -- Cinema Kamera starten + if Config.CinemaCamera.enabled then + StartCinemaCamera(train) + end + + -- Automatische Fahrt + CreateThread(function() + Wait(3000) + + lib:notify({ + title = '🚂 ' .. Config.Menu.texts.trainDeparting, + description = "Nächster Halt: " .. destination.name, + type = Config.Notifications.types.info, + duration = Config.Notifications.duration.short + }) + + DriveTrainToDestination(train, destination) + end) + + -- Journey loggen + if Config.DebugOptions.logJourneys then + TriggerServerEvent('train:server:logJourney', GetNearestStation(GetEntityCoords(playerPed)), destination.name, destination.price) + end +end + +-- Cinema Kamera +function StartCinemaCamera(train) + if cinemaCam then + DestroyCam(cinemaCam, false) + end + + CreateThread(function() + while isRiding and DoesEntityExist(train) do + for _, camPos in pairs(Config.CinemaCamera.positions) do + if not isRiding then break end + + if cinemaCam then + DestroyCam(cinemaCam, false) + end + + local trainCoords = GetEntityCoords(train) + local trainHeading = GetEntityHeading(train) + local camCoords = trainCoords + camPos.offset + + cinemaCam = CreateCam("DEFAULT_SCRIPTED_CAMERA", true) + SetCamCoord(cinemaCam, camCoords.x, camCoords.y, camCoords.z) + SetCamRot(cinemaCam, camPos.rotation.x, camPos.rotation.y, camPos.rotation.z + trainHeading, 2) + SetCamActive(cinemaCam, true) + RenderScriptCams(true, true, 1000, true, true) + + local holdTime = math.random(Config.CinemaCamera.switchInterval.min, Config.CinemaCamera.switchInterval.max) + Wait(holdTime) + end + end + end) +end + +-- Zug zur Destination fahren +function DriveTrainToDestination(train, destination) + CreateThread(function() + local targetCoords = vector3(destination.coords.x, destination.coords.y, destination.coords.z) + local arrived = false + + -- Beschleunigung + for speed = 0, Config.TrainSpeed.max, Config.TrainSpeed.acceleration do + if not DoesEntityExist(train) then return end + SetTrainSpeed(train, speed) + SetTrainCruiseSpeed(train, speed) + Wait(500) + end + + -- Fahrt + while not arrived and DoesEntityExist(train) and isRiding do + local trainCoords = GetEntityCoords(train) + local distance = #(trainCoords - targetCoords) + + if distance < 200 then + local newSpeed = math.max(Config.TrainSpeed.min, distance / 20) + SetTrainSpeed(train, newSpeed) + SetTrainCruiseSpeed(train, newSpeed) + end + + if distance < 50 then + arrived = true + SetTrainSpeed(train, 0) + SetTrainCruiseSpeed(train, 0) + + Wait(2000) + + lib:notify({ + title = '🚂 ' .. Config.Menu.texts.arrived, + description = destination.name, + type = Config.Notifications.types.success, + duration = Config.Notifications.duration.medium + }) + + lib:notify({ + title = Config.Menu.texts.thankYou, + description = "Gute Weiterreise!", + type = Config.Notifications.types.info, + duration = Config.Notifications.duration.short + }) + + EndTrainJourney() + end + + Wait(1000) + end + end) +end + +-- Zugfahrt beenden +function EndTrainJourney() + isRiding = false + + if cinemaCam then + RenderScriptCams(false, true, 1000, true, true) + DestroyCam(cinemaCam, false) + cinemaCam = nil + end + + if currentTrain and DoesEntityExist(currentTrain) then + local playerPed = PlayerPedId() + TaskLeaveVehicle(playerPed, currentTrain, 0) + end + + currentTrain = nil + + Wait(3000) + lib:notify({ + title = Config.Menu.texts.canExit, + type = Config.Notifications.types.info, + duration = Config.Notifications.duration.short + }) +end + +-- 3D Text zeichnen +function DrawText3D(x, y, z, text) + local onScreen, _x, _y = World3dToScreen2d(x, y, z) + + if onScreen then + SetTextScale(Config.DrawText.scale, Config.DrawText.scale) + SetTextFont(Config.DrawText.font) + SetTextProportional(1) + SetTextColour(Config.DrawText.color.r, Config.DrawText.color.g, Config.DrawText.color.b, Config.DrawText.color.a) + SetTextEntry("STRING") + SetTextCentre(1) + AddTextComponentString(text) + DrawText(_x, _y) + + local factor = (string.len(text)) / 370 + DrawRect(_x, _y + 0.0125, 0.015 + factor, 0.03, + Config.DrawText.backgroundColor.r, + Config.DrawText.backgroundColor.g, + Config.DrawText.backgroundColor.b, + Config.DrawText.backgroundColor.a) + end +end + +-- Haupt-Loop +CreateThread(function() + while true do + local sleep = 1000 + local playerPed = PlayerPedId() + local playerCoords = GetEntityCoords(playerPed) + + if not isRiding and not IsPedInAnyVehicle(playerPed, false) then + nearbyTrains = FindNearbyTrains() + + for _, train in pairs(nearbyTrains) do + if train.distance <= 5.0 then + sleep = 0 + + DrawText3D(train.coords.x, train.coords.y, train.coords.z + 2.0, Config.DrawText.interactText) + + if IsControlJustPressed(0, 38) then -- E + OpenDestinationMenu(train.entity) + end + end + end + end + + if isRiding then + sleep = 0 + if IsControlJustPressed(0, 23) then -- F + lib:notify({ + title = Config.Menu.texts.emergencyExit, + type = Config.Notifications.types.warning, + duration = Config.Notifications.duration.short + }) + EndTrainJourney() + end + end + + Wait(sleep) + end +end) + +-- Commands +RegisterCommand('spawntrain', function(source, args) + local stationId = args[1] or Config.TrainStations[1].id + local station = nil + + for _, s in pairs(Config.TrainStations) do + if s.id == stationId then + station = s + break + end + end + + if station then + SpawnTrainAtLocation(station) + lib:notify({ + title = 'Zug gespawnt', + description = station.name, + type = Config.Notifications.types.success + }) + end +end) + +-- Auto-Spawn +if Config.AutoSpawn.enabled then + CreateThread(function() + Wait(Config.AutoSpawn.delay) + + for i = 1, Config.AutoSpawn.maxTrains do + local randomStation = Config.TrainStations[math.random(1, #Config.TrainStations)] + SpawnTrainAtLocation(randomStation) + Wait(Config.AutoSpawn.spawnInterval) + end + end) +end + +-- Cleanup +AddEventHandler('onResourceStop', function(resourceName) + if GetCurrentResourceName() == resourceName then + if isRiding then + EndTrainJourney() + end + end +end) diff --git a/resources/[freizeit]/nordi_ai_train/config.lua b/resources/[freizeit]/nordi_ai_train/config.lua new file mode 100644 index 000000000..cd663e059 --- /dev/null +++ b/resources/[freizeit]/nordi_ai_train/config.lua @@ -0,0 +1,201 @@ +Config = {} + +-- Allgemeine Einstellungen +Config.Debug = false +Config.DefaultCurrency = 'cash' -- 'cash' oder 'bank' +Config.InteractionDistance = 8.0 +Config.TrainSpeed = { + min = 5.0, + max = 25.0, + acceleration = 2.0 +} + +-- Cinema Kamera Einstellungen +Config.CinemaCamera = { + enabled = true, + switchInterval = {min = 8000, max = 12000}, -- Zeit zwischen Kamerawechseln + positions = { + { + offset = vector3(15.0, 5.0, 5.0), + rotation = vector3(-10.0, 0.0, 90.0) + }, + { + offset = vector3(10.0, 15.0, 8.0), + rotation = vector3(-15.0, 0.0, 45.0) + }, + { + offset = vector3(-10.0, -15.0, 6.0), + rotation = vector3(-5.0, 0.0, -135.0) + }, + { + offset = vector3(0.0, -20.0, 15.0), + rotation = vector3(-20.0, 0.0, 0.0) + } + } +} + +-- Zug Waggon Konfiguration +Config.TrainCars = { + main = "freight", -- Hauptlok + cars = {"freightcar", "freightcont1", "freightgrain", "tankercar"} +} + +-- Bahnhöfe / Ziele +Config.TrainStations = { + { + id = "sandy_depot", + coords = vector4(2533.0, 2833.0, 38.0, 0.0), + name = "Sandy Shores Depot", + description = "Hauptdepot in Sandy Shores", + price = 0, -- Startbahnhof kostenlos + blip = { + sprite = 795, + color = 2, + scale = 0.7 + } + }, + { + id = "sandy_north", + coords = vector4(2606.0, 2927.0, 40.0, 90.0), + name = "Sandy Shores Nord", + description = "Nördlicher Bahnhof von Sandy Shores", + price = 50, + blip = { + sprite = 795, + color = 3, + scale = 0.7 + } + }, + { + id = "ls_depot", + coords = vector4(1164.0, -3250.0, 7.0, 180.0), + name = "Los Santos Hauptbahnhof", + description = "Zentraler Bahnhof in Los Santos", + price = 150, + blip = { + sprite = 795, + color = 1, + scale = 0.8 + } + }, + { + id = "elysian", + coords = vector4(219.0, -2487.0, 6.0, 270.0), + name = "Elysian Island", + description = "Industriegebiet am Hafen", + price = 100, + blip = { + sprite = 795, + color = 4, + scale = 0.7 + } + }, + { + id = "terminal", + coords = vector4(-1100.0, -2724.0, 13.0, 0.0), + name = "Fracht Terminal", + description = "Großes Frachtterminal", + price = 200, + blip = { + sprite = 795, + color = 5, + scale = 0.7 + } + }, + { + id = "downtown", + coords = vector4(-500.0, -1500.0, 10.0, 45.0), + name = "Downtown Station", + description = "Bahnhof in der Innenstadt", + price = 75, + blip = { + sprite = 795, + color = 6, + scale = 0.7 + } + }, + { + id = "paleto", + coords = vector4(100.0, 6500.0, 32.0, 180.0), + name = "Paleto Bay", + description = "Kleiner Bahnhof in Paleto Bay", + price = 250, + blip = { + sprite = 795, + color = 7, + scale = 0.7 + } + } +} + +-- Menü Texte und Einstellungen +Config.Menu = { + title = "🚂 Zugfahrkarten", + subtitle = "Wählen Sie Ihr Reiseziel", + position = "top-right", -- ox_lib menu position + + -- Texte + texts = { + selectDestination = "Ziel auswählen", + price = "Preis", + description = "Beschreibung", + cancel = "Abbrechen", + notEnoughMoney = "Nicht genug Geld", + trainNotAvailable = "Zug nicht verfügbar", + journeyStarted = "Zugfahrt gestartet", + trainDeparting = "Der Zug fährt ab", + arrived = "Ankunft", + thankYou = "Vielen Dank für die Fahrt", + emergencyExit = "Notfall-Ausstieg", + canExit = "Sie können jetzt aussteigen" + }, + + -- Icons für verschiedene Stationstypen + stationIcons = { + depot = "🏭", + city = "🏢", + industrial = "🏗️", + port = "⚓", + rural = "🌾" + } +} + +-- Nachrichten Einstellungen +Config.Notifications = { + duration = { + short = 3000, + medium = 5000, + long = 8000 + }, + types = { + success = "success", + error = "error", + info = "info", + warning = "warning" + } +} + +-- Automatisches Spawnen +Config.AutoSpawn = { + enabled = true, + delay = 10000, -- Wartezeit nach Server-Start + maxTrains = 4, -- Maximale Anzahl gespawnter Züge + spawnInterval = 3000 -- Zeit zwischen Spawns +} + +-- DrawText Einstellungen +Config.DrawText = { + font = 4, + scale = 0.35, + color = {r = 255, g = 255, b = 255, a = 215}, + backgroundColor = {r = 41, g = 128, b = 185, a = 100}, + interactText = "[E] Zug besteigen", + emergencyText = "[F] Notfall-Ausstieg" +} + +-- Debug Einstellungen +Config.DebugOptions = { + showCoords = false, + showDistance = false, + logJourneys = true +} diff --git a/resources/[freizeit]/nordi_ai_train/fxmanifest.lua b/resources/[freizeit]/nordi_ai_train/fxmanifest.lua new file mode 100644 index 000000000..2d6c93692 --- /dev/null +++ b/resources/[freizeit]/nordi_ai_train/fxmanifest.lua @@ -0,0 +1,24 @@ +fx_version 'cerulean' +game 'gta5' + +description 'QBCore Train System with ox_lib' +version '2.0.0' + +shared_scripts { + 'config.lua' +} + +client_scripts { + 'client.lua' +} + +server_scripts { + 'server.lua' +} + +dependencies { + 'qb-core', + 'ox_lib' +} + +lua54 'yes' diff --git a/resources/[freizeit]/nordi_ai_train/server.lua b/resources/[freizeit]/nordi_ai_train/server.lua new file mode 100644 index 000000000..0b7b9cbd0 --- /dev/null +++ b/resources/[freizeit]/nordi_ai_train/server.lua @@ -0,0 +1,33 @@ +local QBCore = exports['qb-core']:GetCoreObject() + +-- Geld prüfen +QBCore.Functions.CreateCallback('train:server:canAfford', function(source, cb, price) + local Player = QBCore.Functions.GetPlayer(source) + if Player then + local money = Player.PlayerData.money[Config.DefaultCurrency] + if money >= price then + Player.Functions.RemoveMoney(Config.DefaultCurrency, price) + cb(true) + else + cb(false) + end + else + cb(false) + end +end) + +-- Journey Log +RegisterNetEvent('train:server:logJourney', function(from, to, price) + if not Config.DebugOptions.logJourneys then return end + + local src = source + local Player = QBCore.Functions.GetPlayer(src) + + if Player then + print(string.format("[TRAIN] %s (%s) - %s → %s ($%d)", + Player.PlayerData.charinfo.firstname .. " " .. Player.PlayerData.charinfo.lastname, + Player.PlayerData.citizenid, + from, to, price + )) + end +end)