Files
devx-rp/resources/[devx]/devx_characters/client/main.lua
T
2026-08-05 01:29:39 +02:00

679 lines
22 KiB
Lua
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)