local selectorOpen = false local selectorInitialized = false local previewPed = nil local previewCamera = nil local previewModel = nil local introRunning = false local introCamera = nil local introDriver = nil local introVehicle = nil local function notify(message) BeginTextCommandThefeedPost('STRING') AddTextComponentSubstringPlayerName(message) EndTextCommandThefeedPostTicker(false, false) end local function runNuiRpc(name, data, nuiCallback, afterResponse) local replied = false local function reply(result) if replied then return end replied = true result = result or { ok = false, error = 'Leere Antwort vom Clientscript.' } if afterResponse then afterResponse(result) end nuiCallback(result) end local ok, errorMessage = pcall(function() DevXTriggerCallback(name, data or {}, reply) end) if not ok then print(('[devx_characters] Client-RPC %s konnte nicht gestartet werden: %s'):format( name, tostring(errorMessage) )) reply({ ok = false, error = ('Clientscriptfehler bei %s. Öffne F8.'):format(name) }) return end SetTimeout(24000, function() reply({ ok = false, error = ('Keine Antwort auf %s. Prüfe F8 und txAdmin.'):format(name) }) end) end local function loadModel(model) local hash = type(model) == 'number' and model or GetHashKey(model) if not IsModelInCdimage(hash) or not IsModelValid(hash) then return nil end RequestModel(hash) local deadline = GetGameTimer() + 10000 while not HasModelLoaded(hash) and GetGameTimer() < deadline do Wait(0) end return HasModelLoaded(hash) and hash or nil end local function loadAnimDict(dict) RequestAnimDict(dict) local deadline = GetGameTimer() + 7000 while not HasAnimDictLoaded(dict) and GetGameTimer() < deadline do Wait(0) end return HasAnimDictLoaded(dict) end local function safeDeleteEntity(entity) if entity and DoesEntityExist(entity) then SetEntityAsMissionEntity(entity, true, true) DeleteEntity(entity) end end local function applyBasicAppearance(ped, modelName) if not ped or not DoesEntityExist(ped) then return end SetPedDefaultComponentVariation(ped) ClearAllPedProps(ped) if modelName == 'mp_m_freemode_01' or modelName == 'mp_f_freemode_01' then SetPedHeadBlendData(ped, 0, 0, 0, 0, 0, 0, 0.5, 0.5, 0.0, false) SetPedComponentVariation(ped, 0, 0, 0, 2) SetPedHairColor(ped, 1, 1) SetPedEyeColor(ped, 2) end end local function destroyPreviewScene() if previewCamera and DoesCamExist(previewCamera) then RenderScriptCams(false, true, 350, true, true) DestroyCam(previewCamera, false) end previewCamera = nil safeDeleteEntity(previewPed) previewPed = nil previewModel = nil ClearFocus() end local function createPreviewCharacter(modelName) if not selectorOpen then return end modelName = modelName or 'mp_m_freemode_01' if previewPed and DoesEntityExist(previewPed) and previewModel == modelName then return end local model = loadModel(modelName) if not model then modelName = 'mp_m_freemode_01' model = loadModel(modelName) end if not model then SendNUIMessage({ action = 'error', message = 'Vorschaumodell konnte nicht geladen werden.' }) return end safeDeleteEntity(previewPed) local position = DevXCharacterConfig.Preview.pedPosition SetFocusPosAndVel(position.x, position.y, position.z, 0.0, 0.0, 0.0) previewPed = CreatePed(4, model, position.x, position.y, position.z, position.heading, false, false) if not DoesEntityExist(previewPed) then SetModelAsNoLongerNeeded(model) SendNUIMessage({ action = 'error', message = 'Charaktervorschau konnte nicht erzeugt werden.' }) return end previewModel = modelName SetEntityInvincible(previewPed, true) SetEntityCollision(previewPed, false, false) FreezeEntityPosition(previewPed, true) SetBlockingOfNonTemporaryEvents(previewPed, true) SetPedCanRagdoll(previewPed, false) applyBasicAppearance(previewPed, modelName) if loadAnimDict('anim@amb@business@bgen@bgen_no_work@') then TaskPlayAnim(previewPed, 'anim@amb@business@bgen@bgen_no_work@', 'stand_phone_phoneputdown_idle_nowork', 3.0, -3.0, -1, 1, 0.0, false, false, false) end local cameraPosition = DevXCharacterConfig.Preview.cameraPosition if not previewCamera or not DoesCamExist(previewCamera) then previewCamera = CreateCam('DEFAULT_SCRIPTED_CAMERA', true) SetCamCoord(previewCamera, cameraPosition.x, cameraPosition.y, cameraPosition.z) SetCamFov(previewCamera, DevXCharacterConfig.Preview.cameraFov or 32.0) SetCamActive(previewCamera, true) RenderScriptCams(true, true, 450, true, true) end PointCamAtEntity(previewCamera, previewPed, 0.0, 0.0, 0.72, true) SetModelAsNoLongerNeeded(model) end local function prepareSelectorScene() local stream = DevXCharacterConfig.Preview.streamPosition local ped = PlayerPedId() SetEntityCoordsNoOffset(ped, stream.x, stream.y, stream.z, false, false, false) SetEntityHeading(ped, stream.heading) SetEntityVisible(ped, false, false) SetEntityCollision(ped, false, false) FreezeEntityPosition(ped, true) createPreviewCharacter('mp_m_freemode_01') end local function setSelector(open) selectorOpen = open SetNuiFocus(open, open) SendNUIMessage({ action = open and 'open' or 'close' }) local ped = PlayerPedId() if open then DisplayRadar(false) prepareSelectorScene() else destroyPreviewScene() if DoesEntityExist(ped) then SetEntityVisible(ped, true, false) SetEntityCollision(ped, true, true) FreezeEntityPosition(ped, false) end end end local function requestCharacters() DevXTriggerCallback('characters:list', {}, function(result) if not result.ok then SendNUIMessage({ action = 'error', message = result.error }) return end SendNUIMessage({ action = 'characters', characters = result.characters, maxCharacters = result.maxCharacters }) end) end local function showDialogue(speaker, text) SendNUIMessage({ action = 'dialogue', speaker = speaker, text = text }) end local function hideDialogue() SendNUIMessage({ action = 'dialogueHide' }) end local function showSkipHelp() BeginTextCommandDisplayHelp('STRING') AddTextComponentSubstringPlayerName('Begrüßung überspringen: ~INPUT_JUMP~') EndTextCommandDisplayHelp(0, false, true, -1) end local function introControls() showSkipHelp() DisableControlAction(0, 24, true) DisableControlAction(0, 25, true) DisableControlAction(0, 37, true) DisableControlAction(0, 75, true) return IsControlJustReleased(0, 22) end local function waitIntro(milliseconds) local deadline = GetGameTimer() + milliseconds while introRunning and GetGameTimer() < deadline do if introControls() then return false end Wait(0) end return introRunning end local function waitUntilIntro(predicate, timeoutMs) local deadline = GetGameTimer() + timeoutMs while introRunning and GetGameTimer() < deadline do if introControls() then return 'skip' end if predicate() then return 'ready' end Wait(0) end return 'timeout' end local function setIntroCamera(position, target, fov, transition) if introCamera and DoesCamExist(introCamera) then DestroyCam(introCamera, false) end introCamera = CreateCam('DEFAULT_SCRIPTED_CAMERA', true) SetCamCoord(introCamera, position.x, position.y, position.z) PointCamAtCoord(introCamera, target.x, target.y, target.z) SetCamFov(introCamera, fov or 48.0) SetCamActive(introCamera, true) RenderScriptCams(true, transition == true, transition and 700 or 0, true, true) end local function resolveRoadPosition(point) local ok, found, node, heading = pcall(function() local success, position, roadHeading = GetClosestVehicleNodeWithHeading( point.x, point.y, point.z, 1, 3.0, 0 ) return success, position, roadHeading end) if ok and found and node then return { x = node.x, y = node.y, z = node.z, heading = heading or point.heading } end return point end local function playGesture(ped, animation) local dict = 'gestures@m@standing@casual' if not loadAnimDict(dict) then return end TaskPlayAnim(ped, dict, animation, 4.0, -4.0, 2600, 48, 0.0, false, false, false) end local function walkTo(ped, target, timeoutMs) TaskFollowNavMeshToCoord(ped, target.x, target.y, target.z, 1.0, timeoutMs, 0.3, false, target.heading or 0.0) local result = waitUntilIntro(function() return #(GetEntityCoords(ped) - vector3(target.x, target.y, target.z)) < 1.4 end, timeoutMs) if result == 'timeout' and DoesEntityExist(ped) then SetEntityCoordsNoOffset(ped, target.x, target.y, target.z, false, false, false) SetEntityHeading(ped, target.heading or GetEntityHeading(ped)) end return result end local function finishWelcomeIntro(markCompleted) introRunning = false hideDialogue() DoScreenFadeOut(350) local fadeDeadline = GetGameTimer() + 1400 while not IsScreenFadedOut() and GetGameTimer() < fadeDeadline do Wait(0) end if introCamera and DoesCamExist(introCamera) then RenderScriptCams(false, false, 0, true, true) DestroyCam(introCamera, false) end introCamera = nil local playerPed = PlayerPedId() local finalSpawn = DevXCharacterConfig.Intro.finalSpawn SetEntityCoordsNoOffset(playerPed, finalSpawn.x, finalSpawn.y, finalSpawn.z, false, false, false) SetEntityHeading(playerPed, finalSpawn.heading) SetEntityVisible(playerPed, true, false) SetEntityCollision(playerPed, true, true) SetEntityInvincible(playerPed, false) FreezeEntityPosition(playerPed, false) ClearPedTasksImmediately(playerPed) safeDeleteEntity(introDriver) safeDeleteEntity(introVehicle) introDriver = nil introVehicle = nil DisplayRadar(true) ClearFocus() TriggerEvent('devx_hud:client:setVisible', true) TriggerServerEvent('devx_core:server:updatePosition', finalSpawn) if markCompleted then DevXTriggerCallback('characters:completeIntro', {}, function(result) if not result.ok then print(('[devx_characters] Introstatus konnte nicht gespeichert werden: %s'):format( tostring(result.error) )) end end) end DoScreenFadeIn(600) notify('Willkommen in DevX-City. Dein neues Leben beginnt jetzt.') end local function startFollowCamera(ped) local config = DevXCharacterConfig.Intro.followCamera if introCamera and DoesCamExist(introCamera) then DestroyCam(introCamera, false) end introCamera = CreateCam('DEFAULT_SCRIPTED_CAMERA', true) AttachCamToEntity( introCamera, ped, config.offset.x, config.offset.y, config.offset.z, true ) PointCamAtEntity( introCamera, ped, config.lookAtOffset.x, config.lookAtOffset.y, config.lookAtOffset.z, true ) SetCamFov(introCamera, config.fov or 48.0) SetCamActive(introCamera, true) RenderScriptCams(true, true, 600, true, true) end local function parkIntroVehicle(vehicle, position) SetEntityCoordsNoOffset(vehicle, position.x, position.y, position.z, false, false, false) SetEntityHeading(vehicle, position.heading) SetVehicleOnGroundProperly(vehicle) SetVehicleEngineOn(vehicle, false, true, false) SetVehicleHandbrake(vehicle, true) FreezeEntityPosition(vehicle, true) end local function startWelcomeIntro() if introRunning then return end introRunning = true local config = DevXCharacterConfig.Intro local playerPed = PlayerPedId() local driverModel = loadModel(config.driverModel) local vehicleModel = loadModel(config.vehicleModel) if not driverModel or not vehicleModel then finishWelcomeIntro(true) return end TriggerEvent('devx_hud:client:setVisible', false) SetNuiFocus(false, false) DisplayRadar(false) SetEntityInvincible(playerPed, true) FreezeEntityPosition(playerPed, false) local parked = config.vehicleParked introVehicle = CreateVehicle( vehicleModel, parked.x, parked.y, parked.z, parked.heading, false, false ) introDriver = CreatePed( 4, driverModel, config.driverWait.x, config.driverWait.y, config.driverWait.z, config.driverWait.heading, false, false ) SetModelAsNoLongerNeeded(driverModel) SetModelAsNoLongerNeeded(vehicleModel) if not DoesEntityExist(introVehicle) or not DoesEntityExist(introDriver) then finishWelcomeIntro(true) return end SetEntityAsMissionEntity(introVehicle, true, true) SetEntityAsMissionEntity(introDriver, true, true) SetEntityInvincible(introVehicle, true) SetEntityInvincible(introDriver, true) SetBlockingOfNonTemporaryEvents(introDriver, true) SetPedCanRagdoll(introDriver, false) SetVehicleDoorsLocked(introVehicle, 1) parkIntroVehicle(introVehicle, parked) startFollowCamera(playerPed) DoScreenFadeIn(700) if not waitIntro(900) then finishWelcomeIntro(true) return end TaskFollowNavMeshToCoord( playerPed, config.terminalExit.x, config.terminalExit.y, config.terminalExit.z, 1.0, 17000, 0.2, false, config.terminalExit.heading ) local walkResult = waitUntilIntro(function() return #(GetEntityCoords(playerPed) - vector3( config.terminalExit.x, config.terminalExit.y, config.terminalExit.z )) < 1.5 end, 17000) if walkResult == 'skip' then finishWelcomeIntro(true) return end if walkResult == 'timeout' then SetEntityCoordsNoOffset( playerPed, config.terminalExit.x, config.terminalExit.y, config.terminalExit.z, false, false, false ) SetEntityHeading(playerPed, config.terminalExit.heading) end local playerWalk = walkTo(playerPed, config.meetingPoint, 9000) if playerWalk == 'skip' then finishWelcomeIntro(true) return end TaskGoStraightToCoord( introDriver, config.meetingPoint.x + 1.30, config.meetingPoint.y - 0.10, config.meetingPoint.z, 1.0, 7000, config.meetingPoint.heading + 180.0, 0.15 ) local driverResult = waitUntilIntro(function() return #(GetEntityCoords(introDriver) - vector3( config.meetingPoint.x + 1.30, config.meetingPoint.y - 0.10, config.meetingPoint.z )) < 1.4 end, 7000) if driverResult == 'skip' then finishWelcomeIntro(true) return end if driverResult == 'timeout' then SetEntityCoordsNoOffset( introDriver, config.meetingPoint.x + 1.30, config.meetingPoint.y - 0.10, config.meetingPoint.z, false, false, false ) end local greetingCamera = config.greetingCamera setIntroCamera(greetingCamera, { x = config.meetingPoint.x, y = config.meetingPoint.y, z = config.meetingPoint.z + 0.90 }, 43.0, true) TaskTurnPedToFaceEntity(playerPed, introDriver, 1200) TaskTurnPedToFaceEntity(introDriver, playerPed, 1200) if not waitIntro(1200) then finishWelcomeIntro(true) return end PlayAmbientSpeech1(introDriver, 'GENERIC_HI', 'SPEECH_PARAMS_FORCE') playGesture(introDriver, 'gesture_hello') showDialogue(config.driverName, 'Hey! Du musst neu hier sein. Willkommen in DevX-City.') if not waitIntro(4300) then finishWelcomeIntro(true) return end playGesture(introDriver, 'gesture_hand_right') showDialogue(config.driverName, 'Ich bin Mason. Mein Wagen steht schon bereit – ich bringe dich direkt ins Zentrum.') if not waitIntro(5000) then finishWelcomeIntro(true) return end playGesture(playerPed, 'gesture_nod_yes_hard') showDialogue('Du', 'Danke. Ich kenne mich hier noch überhaupt nicht aus.') if not waitIntro(3700) then finishWelcomeIntro(true) return end playGesture(introDriver, 'gesture_point') showDialogue(config.driverName, 'Dann komm. Unterwegs erkläre ich dir die wichtigsten Dinge.') if not waitIntro(4100) then finishWelcomeIntro(true) return end hideDialogue() ClearPedTasks(playerPed) ClearPedTasks(introDriver) FreezeEntityPosition(introVehicle, false) SetVehicleHandbrake(introVehicle, false) SetVehicleEngineOn(introVehicle, true, true, false) SetVehicleDoorsLocked(introVehicle, 1) TaskEnterVehicle(introDriver, introVehicle, 12000, -1, 1.0, 1, 0) TaskEnterVehicle(playerPed, introVehicle, 12000, 0, 1.0, 1, 0) local enterResult = waitUntilIntro(function() return IsPedInVehicle(introDriver, introVehicle, false) and IsPedInVehicle(playerPed, introVehicle, false) end, 12000) if enterResult == 'skip' then finishWelcomeIntro(true) return end if enterResult ~= 'ready' then SetPedIntoVehicle(introDriver, introVehicle, -1) SetPedIntoVehicle(playerPed, introVehicle, 0) end if introCamera and DoesCamExist(introCamera) then RenderScriptCams(false, true, 900, true, true) DestroyCam(introCamera, false) introCamera = nil end TaskVehicleDriveToCoordLongrange( introDriver, introVehicle, config.destinationRoad.x, config.destinationRoad.y, config.destinationRoad.z, config.driveSpeed, config.drivingStyle, 10.0 ) showDialogue(config.driverName, 'Am Anfang startest du klein. Arbeit, Kontakte und ein eigenes Zuhause kommen Schritt für Schritt.') if not waitIntro(6200) then finishWelcomeIntro(true) return end showDialogue(config.driverName, 'Mit M öffnest du dein Handy. Dort findest du deine Telefonnummer und dein Bankkonto.') if not waitIntro(5600) then finishWelcomeIntro(true) return end hideDialogue() local driveDeadline = GetGameTimer() + config.maxDriveTimeMs while introRunning and GetGameTimer() < driveDeadline do if introControls() then finishWelcomeIntro(true) return end if not DoesEntityExist(introVehicle) or not DoesEntityExist(introDriver) then break end local coords = GetEntityCoords(introVehicle) if #(coords - vector3(config.destinationRoad.x, config.destinationRoad.y, config.destinationRoad.z)) < 28.0 then break end Wait(0) end if not introRunning then return end TaskVehicleTempAction(introDriver, introVehicle, 27, 2500) showDialogue(config.driverName, 'Da wären wir. Willkommen im Zentrum von DevX-City – und viel Erfolg!') if not waitIntro(4700) then finishWelcomeIntro(true) return end finishWelcomeIntro(true) end local function spawnCharacter(player) local shouldPlayIntro = DevXCharacterConfig.Intro.enabled and player.introCompleted ~= true local position = shouldPlayIntro and DevXCharacterConfig.Intro.terminalInside or player.position or DevXConfig.StreetSpawn local modelName = player.model or 'mp_m_freemode_01' local model = loadModel(modelName) if not model then SendNUIMessage({ action = 'error', message = 'Charaktermodell konnte nicht geladen werden.' }) return end DoScreenFadeOut(250) while not IsScreenFadedOut() do Wait(0) end exports.spawnmanager:spawnPlayer({ x = position.x, y = position.y, z = position.z, heading = position.heading or 0.0, model = model, skipFade = true }, function() SetModelAsNoLongerNeeded(model) setSelector(false) local ped = PlayerPedId() applyBasicAppearance(ped, modelName) SetEntityCollision(ped, true, true) SetEntityVisible(ped, true, false) FreezeEntityPosition(ped, false) TriggerEvent('devx_characters:client:spawned', player) if shouldPlayIntro then CreateThread(startWelcomeIntro) else DisplayRadar(true) DoScreenFadeIn(500) end end) end local function initializeSelector() if selectorInitialized then return end selectorInitialized = true exports.spawnmanager:setAutoSpawn(false) while not NetworkIsSessionStarted() do Wait(100) end local spawn = DevXCharacterConfig.Preview.streamPosition local model = loadModel('mp_m_freemode_01') or loadModel('a_m_y_business_01') if not model then return end DoScreenFadeOut(0) local initialSpawnFinished = false local function finishInitialization() if initialSpawnFinished then return end initialSpawnFinished = true SetModelAsNoLongerNeeded(model) setSelector(true) requestCharacters() ShutdownLoadingScreen() ShutdownLoadingScreenNui() DoScreenFadeIn(500) end exports.spawnmanager:spawnPlayer({ x = spawn.x, y = spawn.y, z = spawn.z, heading = spawn.heading, model = model, skipFade = true }, finishInitialization) SetTimeout(12000, finishInitialization) end RegisterNUICallback('ready', function(_, cb) requestCharacters() cb({ ok = true }) end) RegisterNUICallback('preview', function(data, cb) createPreviewCharacter(data.model or (data.sex == 'f' and 'mp_f_freemode_01' or 'mp_m_freemode_01')) cb({ ok = true }) end) RegisterNUICallback('create', function(data, cb) runNuiRpc('characters:create', data, cb, function(result) if result.ok then requestCharacters() end end) end) RegisterNUICallback('select', function(data, cb) runNuiRpc('characters:select', data, cb, function(result) if result.ok then spawnCharacter(result.player) end end) end) RegisterNUICallback('delete', function(data, cb) runNuiRpc('characters:delete', data, cb, function(result) if result.ok then requestCharacters() end end) end) CreateThread(initializeSelector) AddEventHandler('onClientResourceStart', function(resourceName) if resourceName ~= GetCurrentResourceName() then return end exports.spawnmanager:setAutoSpawn(false) CreateThread(initializeSelector) end) AddEventHandler('onResourceStop', function(resourceName) if resourceName ~= GetCurrentResourceName() then return end introRunning = false destroyPreviewScene() safeDeleteEntity(introDriver) safeDeleteEntity(introVehicle) SetNuiFocus(false, false) end)