Всё, что здесь лежит, написал ИИ-агент на живом сервере под запись — без ESX, без QBCore, без скачанных с форума скриптов. - resources/justrp — свой ресурс на Lua: регистрация, вход, создание персонажа, спавн - resources/justrp/html — экраны NUI: кинематографичная загрузка, вход, редактор персонажа - api/api.py — внутренний HTTP-API к MariaDB, пароли через scrypt - server.cfg.example — пример конфига Секреты (пароль БД, внутренний ключ API) вынесены в переменные окружения и convar. Ключ Cfx.re в репозиторий не попадает. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
344 lines
11 KiB
Lua
344 lines
11 KiB
Lua
-- JustRP — Client-side core
|
|
-- Manages NUI screens, cameras, spawning and position sync
|
|
|
|
local inSession = false
|
|
local charLoaded = false
|
|
local camHandle = nil
|
|
local saveTimer = 0
|
|
|
|
-- ─── NUI helper ─────────────────────────────────────────────────────────────
|
|
|
|
local function nuiMsg(action, data)
|
|
SendNUIMessage({ action = action, data = data or {} })
|
|
end
|
|
|
|
-- ─── On resource start (loading screen is already showing) ──────────────────
|
|
|
|
AddEventHandler("onClientMapStart", function()
|
|
Wait(1500) -- give the game a moment to settle
|
|
TriggerServerEvent("justrp:clientReady")
|
|
end)
|
|
|
|
-- ─── Session start from server ───────────────────────────────────────────────
|
|
|
|
RegisterNetEvent("justrp:startSession")
|
|
AddEventHandler("justrp:startSession", function()
|
|
-- Freeze world while auth is happening
|
|
SetEntityVisible(PlayerPedId(), false, false)
|
|
FreezeEntityPosition(PlayerPedId(), true)
|
|
DisplayHud(false)
|
|
DisplayRadar(false)
|
|
|
|
-- Shut loading screen, then show auth UI
|
|
Wait(800)
|
|
ShutdownLoadingScreenNui()
|
|
Wait(300)
|
|
|
|
SetNuiFocus(true, true)
|
|
nuiMsg("showAuth")
|
|
inSession = true
|
|
end)
|
|
|
|
-- ─── Auth results ────────────────────────────────────────────────────────────
|
|
|
|
RegisterNetEvent("justrp:auth:ok")
|
|
AddEventHandler("justrp:auth:ok", function(accountId, username)
|
|
nuiMsg("authOk", { accountId = accountId, username = username })
|
|
TriggerServerEvent("justrp:chars:load")
|
|
end)
|
|
|
|
RegisterNetEvent("justrp:auth:fail")
|
|
AddEventHandler("justrp:auth:fail", function(err)
|
|
nuiMsg("authFail", { error = err })
|
|
end)
|
|
|
|
-- ─── Character list from server ──────────────────────────────────────────────
|
|
|
|
RegisterNetEvent("justrp:chars:list")
|
|
AddEventHandler("justrp:chars:list", function(chars)
|
|
nuiMsg("showChars", { characters = chars })
|
|
end)
|
|
|
|
-- ─── Character creation result ───────────────────────────────────────────────
|
|
|
|
RegisterNetEvent("justrp:char:created")
|
|
AddEventHandler("justrp:char:created", function(charId)
|
|
-- Reload character list so the new one shows
|
|
TriggerServerEvent("justrp:chars:load")
|
|
end)
|
|
|
|
RegisterNetEvent("justrp:char:createFail")
|
|
AddEventHandler("justrp:char:createFail", function(err)
|
|
nuiMsg("charCreateFail", { error = err })
|
|
end)
|
|
|
|
-- ─── Character selected → load appearance ────────────────────────────────────
|
|
|
|
RegisterNetEvent("justrp:char:loaded")
|
|
AddEventHandler("justrp:char:loaded", function(ch)
|
|
-- Apply appearance to player ped
|
|
local ped = PlayerPedId()
|
|
local model = ch.gender == 1 and GetHashKey("mp_f_freemode_01") or GetHashKey("mp_m_freemode_01")
|
|
|
|
RequestModel(model)
|
|
while not HasModelLoaded(model) do Wait(10) end
|
|
SetPlayerModel(PlayerId(), model)
|
|
SetModelAsNoLongerNeeded(model)
|
|
|
|
ped = PlayerPedId()
|
|
|
|
-- Face blend
|
|
SetPedHeadBlendData(
|
|
ped,
|
|
math.floor(ch.face_blend * 45), math.floor(ch.face_blend * 45),
|
|
0,
|
|
math.floor(ch.face_blend * 45), math.floor(ch.face_blend * 45),
|
|
0,
|
|
ch.face_blend, ch.face_blend, 0.0, false
|
|
)
|
|
|
|
-- Skin overlay (skin tone)
|
|
SetPedMicroblendData(ped, ch.skin_tone, ch.skin_tone, ch.skin_tone)
|
|
|
|
-- Eyes
|
|
SetPedEyeColor(ped, ch.eye_color)
|
|
|
|
-- Hair
|
|
SetPedComponentVariation(ped, 2, ch.hair_style, 0, 2)
|
|
SetPedHairColor(ped, ch.hair_color, 0)
|
|
|
|
-- Body shape
|
|
SetPedFaceFeature(ped, 16, (ch.body_weight - 0.5) * 2) -- body weight feature
|
|
|
|
-- Outfit
|
|
SetPedComponentVariation(ped, 11, ch.outfit_top, 0, 2)
|
|
SetPedComponentVariation(ped, 4, ch.outfit_pants, 0, 2)
|
|
SetPedComponentVariation(ped, 6, ch.outfit_shoes, 0, 2)
|
|
|
|
-- Tell NUI to show spawn screen
|
|
nuiMsg("showSpawn", {
|
|
name = ch.firstname .. " " .. ch.lastname,
|
|
})
|
|
end)
|
|
|
|
-- ─── Spawn ───────────────────────────────────────────────────────────────────
|
|
|
|
RegisterNetEvent("justrp:spawn:go")
|
|
AddEventHandler("justrp:spawn:go", function(pos)
|
|
local ped = PlayerPedId()
|
|
|
|
-- Teleport to position and unfreeze
|
|
SetEntityCoords(ped, pos.x, pos.y, pos.z, false, false, false, true)
|
|
SetEntityHeading(ped, pos.h)
|
|
|
|
-- Cinematic camera fly-down
|
|
local camX = pos.x + math.sin(math.rad(pos.h)) * 200
|
|
local camY = pos.y + math.cos(math.rad(pos.h)) * 200
|
|
camHandle = CreateCameraWithParams(
|
|
"DEFAULT_SCRIPTED_CAMERA",
|
|
camX, camY, pos.z + 180,
|
|
0.0, 0.0, 0.0,
|
|
60.0, false, 0
|
|
)
|
|
SetCamActive(camHandle, true)
|
|
RenderScriptCams(true, true, 1000, true, true)
|
|
|
|
-- Animate camera flying down to player
|
|
local steps = 80
|
|
Citizen.CreateThread(function()
|
|
for i = 1, steps do
|
|
local t = i / steps
|
|
local ease = 1 - math.pow(1 - t, 3) -- ease out cubic
|
|
|
|
local cx = camX + (pos.x - camX) * ease
|
|
local cy = camY + (pos.y - camY) * ease
|
|
local cz = (pos.z + 180) + ((pos.z + 2) - (pos.z + 180)) * ease
|
|
|
|
SetCamCoord(camHandle, cx, cy, cz)
|
|
PointCamAtCoord(camHandle, pos.x, pos.y, pos.z)
|
|
Wait(16)
|
|
end
|
|
|
|
-- Transition back to player cam
|
|
RenderScriptCams(false, true, 1200, true, true)
|
|
Wait(1300)
|
|
DestroyCam(camHandle, false)
|
|
camHandle = nil
|
|
|
|
-- Reveal player
|
|
SetEntityVisible(ped, true, false)
|
|
FreezeEntityPosition(ped, false)
|
|
DisplayHud(true)
|
|
DisplayRadar(true)
|
|
SetNuiFocus(false, false)
|
|
nuiMsg("hide")
|
|
charLoaded = true
|
|
end)
|
|
end)
|
|
|
|
-- ─── NUI Callbacks ───────────────────────────────────────────────────────────
|
|
|
|
-- Auth
|
|
RegisterNUICallback("authRegister", function(data, cb)
|
|
TriggerServerEvent("justrp:auth:register", data.username, data.password)
|
|
cb("ok")
|
|
end)
|
|
|
|
RegisterNUICallback("authLogin", function(data, cb)
|
|
TriggerServerEvent("justrp:auth:login", data.username, data.password)
|
|
cb("ok")
|
|
end)
|
|
|
|
-- Character management
|
|
RegisterNUICallback("charSelect", function(data, cb)
|
|
TriggerServerEvent("justrp:char:select", tonumber(data.id))
|
|
cb("ok")
|
|
end)
|
|
|
|
RegisterNUICallback("charCreate", function(data, cb)
|
|
TriggerServerEvent("justrp:char:create", data)
|
|
cb("ok")
|
|
end)
|
|
|
|
-- Spawn selection
|
|
RegisterNUICallback("spawnRequest", function(data, cb)
|
|
TriggerServerEvent("justrp:spawn:request", data.location)
|
|
cb("ok")
|
|
end)
|
|
|
|
-- Character creation — camera rotation control
|
|
RegisterNUICallback("rotateCam", function(data, cb)
|
|
if camHandle then
|
|
-- data.delta is horizontal rotation amount
|
|
local heading = GetEntityHeading(PlayerPedId())
|
|
SetEntityHeading(PlayerPedId(), heading - (data.delta or 0) * 0.4)
|
|
end
|
|
cb("ok")
|
|
end)
|
|
|
|
-- ─── Character creation preview camera ───────────────────────────────────────
|
|
|
|
local charCamActive = false
|
|
local charCamAngle = 0.0
|
|
|
|
RegisterNUICallback("startCharCam", function(data, cb)
|
|
if charCamActive then
|
|
cb("ok")
|
|
return
|
|
end
|
|
charCamActive = true
|
|
local ped = PlayerPedId()
|
|
SetEntityVisible(ped, true, false)
|
|
|
|
camHandle = CreateCamera("DEFAULT_SCRIPTED_CAMERA", false)
|
|
SetCamActive(camHandle, true)
|
|
RenderScriptCams(true, false, 0, true, true)
|
|
|
|
Citizen.CreateThread(function()
|
|
while charCamActive do
|
|
local pedPos = GetEntityCoords(ped)
|
|
local angle = charCamAngle
|
|
local dist = 1.6
|
|
local cx = pedPos.x + math.sin(math.rad(angle)) * dist
|
|
local cy = pedPos.y - math.cos(math.rad(angle)) * dist
|
|
local cz = pedPos.z + 0.7
|
|
|
|
SetCamCoord(camHandle, cx, cy, cz)
|
|
PointCamAtCoord(camHandle, pedPos.x, pedPos.y, pedPos.z + 0.7)
|
|
charCamAngle = charCamAngle + 0.15 -- slow auto-orbit
|
|
|
|
Wait(16)
|
|
end
|
|
end)
|
|
|
|
cb("ok")
|
|
end)
|
|
|
|
RegisterNUICallback("stopCharCam", function(data, cb)
|
|
charCamActive = false
|
|
if camHandle then
|
|
RenderScriptCams(false, true, 600, true, true)
|
|
Wait(700)
|
|
DestroyCam(camHandle, false)
|
|
camHandle = nil
|
|
end
|
|
cb("ok")
|
|
end)
|
|
|
|
RegisterNUICallback("charCamDrag", function(data, cb)
|
|
charCamAngle = charCamAngle + (data.delta or 0) * 0.5
|
|
charCamActive = true -- stop auto-orbit briefly
|
|
cb("ok")
|
|
end)
|
|
|
|
-- Apply live appearance updates during char creation
|
|
RegisterNUICallback("applyAppearance", function(data, cb)
|
|
local ped = PlayerPedId()
|
|
|
|
if data.gender ~= nil then
|
|
local model = data.gender == 1
|
|
and GetHashKey("mp_f_freemode_01")
|
|
or GetHashKey("mp_m_freemode_01")
|
|
if not HasModelLoaded(model) then
|
|
RequestModel(model)
|
|
while not HasModelLoaded(model) do Wait(10) end
|
|
end
|
|
SetPlayerModel(PlayerId(), model)
|
|
SetModelAsNoLongerNeeded(model)
|
|
ped = PlayerPedId()
|
|
end
|
|
|
|
if data.face_blend ~= nil then
|
|
SetPedHeadBlendData(
|
|
ped,
|
|
math.floor(data.face_blend * 45),
|
|
math.floor(data.face_blend * 45),
|
|
0,
|
|
math.floor(data.face_blend * 45),
|
|
math.floor(data.face_blend * 45),
|
|
0,
|
|
data.face_blend, data.face_blend, 0.0, false
|
|
)
|
|
end
|
|
if data.skin_tone ~= nil then
|
|
SetPedMicroblendData(ped, data.skin_tone, data.skin_tone, data.skin_tone)
|
|
end
|
|
if data.eye_color ~= nil then
|
|
SetPedEyeColor(ped, data.eye_color)
|
|
end
|
|
if data.hair_style ~= nil or data.hair_color ~= nil then
|
|
SetPedComponentVariation(ped, 2, data.hair_style or 0, 0, 2)
|
|
SetPedHairColor(ped, data.hair_color or 0, 0)
|
|
end
|
|
if data.body_weight ~= nil then
|
|
SetPedFaceFeature(ped, 16, (data.body_weight - 0.5) * 2)
|
|
end
|
|
if data.outfit_top ~= nil then
|
|
SetPedComponentVariation(ped, 11, data.outfit_top, 0, 2)
|
|
end
|
|
if data.outfit_pants ~= nil then
|
|
SetPedComponentVariation(ped, 4, data.outfit_pants, 0, 2)
|
|
end
|
|
if data.outfit_shoes ~= nil then
|
|
SetPedComponentVariation(ped, 6, data.outfit_shoes, 0, 2)
|
|
end
|
|
|
|
cb("ok")
|
|
end)
|
|
|
|
-- ─── Position auto-save ───────────────────────────────────────────────────────
|
|
|
|
Citizen.CreateThread(function()
|
|
while true do
|
|
Wait(60000)
|
|
if charLoaded then
|
|
local ped = PlayerPedId()
|
|
local pos = GetEntityCoords(ped)
|
|
local hdg = GetEntityHeading(ped)
|
|
TriggerServerEvent("justrp:savePosition", pos.x, pos.y, pos.z, hdg)
|
|
end
|
|
end
|
|
end)
|
|
|
|
print("[JustRP] Client core loaded.")
|