Part 1 final build: Los Santos RP written on camera by the agent
rp_core (sessions, scrypt auth, self-written MySQL-wire driver rp_db), county-records NUI: residency-file auth, identity-record character intake, survey-grid spawn, procedural night-city loading screen. Secrets replaced with .example files. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Scripted cameras for the pre-spawn screens.
|
||||
--
|
||||
-- One camera object is reused throughout. Everything is driven from a single
|
||||
-- render thread that eases the live values towards target values, so a change
|
||||
-- requested by the UI never snaps.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
Cam = {}
|
||||
|
||||
local cam = nil
|
||||
local mode = 'off' -- 'off' | 'shots' | 'orbit' | 'fly'
|
||||
local renderThread = false
|
||||
|
||||
-- live and target orbit values
|
||||
local orbit = {
|
||||
angle = 180.0, targetAngle = 180.0,
|
||||
radius = 1.6, targetRadius = 1.6,
|
||||
height = 0.65, targetHeight = 0.65,
|
||||
focus = nil, -- entity to orbit
|
||||
pitch = 0.0, targetPitch = 0.0,
|
||||
}
|
||||
|
||||
local shots = { list = {}, index = 0, startedAt = 0, duration = 22000 }
|
||||
local fly = { from = nil, to = nil, lookFrom = nil, lookTo = nil, startedAt = 0, duration = 3000, done = nil }
|
||||
|
||||
local function ensureCam()
|
||||
if cam and DoesCamExist(cam) then return cam end
|
||||
cam = CreateCam('DEFAULT_SCRIPTED_CAMERA', true)
|
||||
SetCamActive(cam, true)
|
||||
RenderScriptCams(true, false, 0, true, true)
|
||||
return cam
|
||||
end
|
||||
|
||||
local function lerp(a, b, t) return a + (b - a) * t end
|
||||
|
||||
local function vlerp(a, b, t)
|
||||
return vector3(lerp(a.x, b.x, t), lerp(a.y, b.y, t), lerp(a.z, b.z, t))
|
||||
end
|
||||
|
||||
-- smoothstep, so fly-throughs start and end at rest
|
||||
local function ease(t)
|
||||
t = math.max(0.0, math.min(1.0, t))
|
||||
return t * t * (3.0 - 2.0 * t)
|
||||
end
|
||||
|
||||
local function startRenderThread()
|
||||
if renderThread then return end
|
||||
renderThread = true
|
||||
|
||||
CreateThread(function()
|
||||
while mode ~= 'off' do
|
||||
local c = ensureCam()
|
||||
|
||||
if mode == 'shots' then
|
||||
local shot = shots.list[shots.index]
|
||||
if shot then
|
||||
local t = (GetGameTimer() - shots.startedAt) / shots.duration
|
||||
if t >= 1.0 then
|
||||
shots.index = (shots.index % #shots.list) + 1
|
||||
shots.startedAt = GetGameTimer()
|
||||
shot = shots.list[shots.index]
|
||||
t = 0.0
|
||||
end
|
||||
-- a slow linear dolly reads as a helicopter, not a spline
|
||||
local pos = vlerp(shot.from, shot.to, t)
|
||||
SetCamCoord(c, pos.x, pos.y, pos.z)
|
||||
PointCamAtCoord(c, shot.look.x, shot.look.y, shot.look.z)
|
||||
end
|
||||
|
||||
elseif mode == 'orbit' then
|
||||
orbit.angle = lerp(orbit.angle, orbit.targetAngle, 0.14)
|
||||
orbit.radius = lerp(orbit.radius, orbit.targetRadius, 0.10)
|
||||
orbit.height = lerp(orbit.height, orbit.targetHeight, 0.10)
|
||||
orbit.pitch = lerp(orbit.pitch, orbit.targetPitch, 0.10)
|
||||
|
||||
local ent = orbit.focus
|
||||
if ent and DoesEntityExist(ent) then
|
||||
local base = GetEntityCoords(ent)
|
||||
local rad = math.rad(orbit.angle)
|
||||
local px = base.x + math.sin(rad) * orbit.radius
|
||||
local py = base.y + math.cos(rad) * orbit.radius
|
||||
local pz = base.z + orbit.height
|
||||
SetCamCoord(c, px, py, pz)
|
||||
PointCamAtCoord(c, base.x, base.y, base.z + orbit.height - orbit.pitch)
|
||||
end
|
||||
|
||||
elseif mode == 'fly' then
|
||||
local t = ease((GetGameTimer() - fly.startedAt) / fly.duration)
|
||||
local pos = vlerp(fly.from, fly.to, t)
|
||||
local look = vlerp(fly.lookFrom, fly.lookTo, t)
|
||||
SetCamCoord(c, pos.x, pos.y, pos.z)
|
||||
PointCamAtCoord(c, look.x, look.y, look.z)
|
||||
if t >= 1.0 then
|
||||
mode = 'idle'
|
||||
if fly.done then
|
||||
local fn = fly.done
|
||||
fly.done = nil
|
||||
fn()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Wait(0)
|
||||
end
|
||||
renderThread = false
|
||||
end)
|
||||
end
|
||||
|
||||
--- Slow establishing shots over the city, cycled forever.
|
||||
function Cam.Shots(list)
|
||||
shots.list = list
|
||||
shots.index = 1
|
||||
shots.startedAt = GetGameTimer()
|
||||
mode = 'shots'
|
||||
ensureCam()
|
||||
SetCamFov(cam, 42.0)
|
||||
startRenderThread()
|
||||
end
|
||||
|
||||
--- Orbit an entity. `snap` places the camera immediately instead of easing in.
|
||||
function Cam.Orbit(entity, radius, height, angle, snap)
|
||||
orbit.focus = entity
|
||||
orbit.targetRadius = radius or orbit.targetRadius
|
||||
orbit.targetHeight = height or orbit.targetHeight
|
||||
if angle then orbit.targetAngle = angle end
|
||||
if snap then
|
||||
orbit.radius = orbit.targetRadius
|
||||
orbit.height = orbit.targetHeight
|
||||
orbit.angle = orbit.targetAngle
|
||||
end
|
||||
mode = 'orbit'
|
||||
ensureCam()
|
||||
SetCamFov(cam, 34.0)
|
||||
startRenderThread()
|
||||
end
|
||||
|
||||
--- Framing presets used by the creator's section tabs.
|
||||
function Cam.OrbitFrame(radius, height, pitch)
|
||||
orbit.targetRadius = radius
|
||||
orbit.targetHeight = height
|
||||
orbit.targetPitch = pitch or 0.0
|
||||
end
|
||||
|
||||
function Cam.Nudge(deltaDegrees)
|
||||
orbit.targetAngle = orbit.targetAngle + deltaDegrees
|
||||
end
|
||||
|
||||
function Cam.SetAngle(deg)
|
||||
orbit.targetAngle = deg
|
||||
end
|
||||
|
||||
function Cam.Angle() return orbit.targetAngle end
|
||||
|
||||
--- Fly from wherever we are to a point looking at a target, then call `done`.
|
||||
function Cam.FlyTo(fromPos, fromLook, toPos, toLook, duration, done)
|
||||
fly.from = fromPos
|
||||
fly.to = toPos
|
||||
fly.lookFrom = fromLook
|
||||
fly.lookTo = toLook
|
||||
fly.duration = duration or 3000
|
||||
fly.startedAt = GetGameTimer()
|
||||
fly.done = done
|
||||
mode = 'fly'
|
||||
ensureCam()
|
||||
startRenderThread()
|
||||
end
|
||||
|
||||
function Cam.SetFov(fov)
|
||||
if cam and DoesCamExist(cam) then SetCamFov(cam, fov + 0.0) end
|
||||
end
|
||||
|
||||
function Cam.Position()
|
||||
if cam and DoesCamExist(cam) then return GetCamCoord(cam) end
|
||||
return GetEntityCoords(PlayerPedId())
|
||||
end
|
||||
|
||||
--- Hand control back to the gameplay camera. With a duration the engine
|
||||
--- interpolates between the two, which hides the cut entirely.
|
||||
function Cam.Release(duration)
|
||||
mode = 'off'
|
||||
duration = duration or 0
|
||||
RenderScriptCams(false, duration > 0, duration, true, true)
|
||||
if cam and DoesCamExist(cam) then
|
||||
DestroyCam(cam, true)
|
||||
end
|
||||
cam = nil
|
||||
end
|
||||
@@ -0,0 +1,508 @@
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- rp_ui - the pre-spawn experience.
|
||||
--
|
||||
-- Owns the stage machine (auth -> characters -> creator -> spawn -> live),
|
||||
-- the NUI bridge, and the transition into the world. The server decides
|
||||
-- everything that matters; this file only asks and then animates the answer.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local stage = 'boot'
|
||||
local account = nil
|
||||
local characters = {}
|
||||
local draft = nil -- appearance currently being edited
|
||||
local previewPed = nil
|
||||
local spawnData = nil -- payload from char:select
|
||||
local chosenChar = nil
|
||||
|
||||
local NEUTRAL = vector3(-1037.0, -2738.0, 20.17) -- flat, always-streamed tarmac
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- helpers
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function ui(payload)
|
||||
SendNUIMessage(payload)
|
||||
end
|
||||
|
||||
local function focusUI(on)
|
||||
SetNuiFocus(on, on)
|
||||
SetNuiFocusKeepInput(false)
|
||||
end
|
||||
|
||||
local function holdPlayer()
|
||||
local ped = PlayerPedId()
|
||||
SetEntityVisible(ped, false, false)
|
||||
SetEntityCollision(ped, false, false)
|
||||
FreezeEntityPosition(ped, true)
|
||||
SetEntityInvincible(ped, true)
|
||||
SetPlayerControl(PlayerId(), false, 0)
|
||||
SetPlayerInvincible(PlayerId(), true)
|
||||
DisplayHud(false)
|
||||
DisplayRadar(false)
|
||||
ClearPedTasksImmediately(ped)
|
||||
end
|
||||
|
||||
local function releasePlayer()
|
||||
local ped = PlayerPedId()
|
||||
SetEntityVisible(ped, true, false)
|
||||
SetEntityCollision(ped, true, true)
|
||||
FreezeEntityPosition(ped, false)
|
||||
SetEntityInvincible(ped, false)
|
||||
SetPlayerControl(PlayerId(), true, 0)
|
||||
SetPlayerInvincible(PlayerId(), false)
|
||||
DisplayHud(true)
|
||||
DisplayRadar(true)
|
||||
end
|
||||
|
||||
--- Dusk, so the sodium palette of the interface matches the world behind it.
|
||||
local function setSceneMood(on)
|
||||
if on then
|
||||
NetworkOverrideClockTime(20, 40, 0)
|
||||
SetWeatherTypeNowPersist('EXTRASUNNY')
|
||||
else
|
||||
NetworkClearClockTimeOverride()
|
||||
ClearOverrideWeatherType()
|
||||
SetWeatherTypeNow('EXTRASUNNY')
|
||||
end
|
||||
end
|
||||
|
||||
--- Keep the world streamed around a point while no player ped is there.
|
||||
local function focusOn(pos)
|
||||
SetFocusPosAndVel(pos.x, pos.y, pos.z, 0.0, 0.0, 0.0)
|
||||
RequestCollisionAtCoord(pos.x, pos.y, pos.z)
|
||||
end
|
||||
|
||||
local function destroyPreview()
|
||||
if previewPed and DoesEntityExist(previewPed) then
|
||||
DeleteEntity(previewPed)
|
||||
end
|
||||
previewPed = nil
|
||||
end
|
||||
|
||||
local function setStage(next, data)
|
||||
stage = next
|
||||
ui({ action = 'stage', stage = next, data = data })
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- boot
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
CreateThread(function()
|
||||
while not NetworkIsSessionStarted() do Wait(50) end
|
||||
|
||||
holdPlayer()
|
||||
setSceneMood(true)
|
||||
|
||||
local first = Config.CinematicShots[1]
|
||||
focusOn(first.from)
|
||||
SetEntityCoords(PlayerPedId(), NEUTRAL.x, NEUTRAL.y, NEUTRAL.z, false, false, false, false)
|
||||
|
||||
Cam.Shots(Config.CinematicShots)
|
||||
|
||||
-- give the world a moment to stream in behind the loading screen
|
||||
SendLoadingScreenMessage(json.encode({
|
||||
eventName = 'rp:server',
|
||||
name = Config.ServerName,
|
||||
status = 'Connected',
|
||||
}))
|
||||
SendLoadingScreenMessage(json.encode({ eventName = 'rp:rules', rules = Config.Rules }))
|
||||
|
||||
local waited = 0
|
||||
while waited < 6000 do
|
||||
SendLoadingScreenMessage(json.encode({
|
||||
eventName = 'rp:progress',
|
||||
fraction = 0.85 + (waited / 6000) * 0.15,
|
||||
step = 'Streaming the city',
|
||||
}))
|
||||
Wait(250)
|
||||
waited = waited + 250
|
||||
end
|
||||
|
||||
SendLoadingScreenMessage(json.encode({ eventName = 'rp:handover' }))
|
||||
Wait(1500)
|
||||
ShutdownLoadingScreenNui()
|
||||
|
||||
focusUI(true)
|
||||
setStage('auth', {
|
||||
serverName = Config.ServerName,
|
||||
serverTag = Config.ServerTag,
|
||||
maxChars = Config.MaxChars,
|
||||
})
|
||||
end)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- authentication
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
RegisterNUICallback('auth:submit', function(data, cb)
|
||||
CreateThread(function()
|
||||
local mode = (data and data.mode == 'register') and 'auth:register' or 'auth:login'
|
||||
local res, err = Core.Callback('rp_session', mode, {
|
||||
username = data.username,
|
||||
password = data.password,
|
||||
})
|
||||
|
||||
if not res then return cb({ ok = false, error = err or 'Something went wrong' }) end
|
||||
|
||||
account = { username = res.username, role = res.role }
|
||||
characters = res.characters or {}
|
||||
cb({ ok = true, account = account, characters = characters, maxChars = res.maxChars })
|
||||
end)
|
||||
end)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- character select
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Stand the highlighted character up so the list is not just text.
|
||||
RegisterNUICallback('char:preview', function(data, cb)
|
||||
CreateThread(function()
|
||||
destroyPreview()
|
||||
|
||||
local appearance
|
||||
for _, c in ipairs(characters) do
|
||||
if c.id == data.id then appearance = c.appearance end
|
||||
end
|
||||
if not appearance then return cb({ ok = false }) end
|
||||
|
||||
appearance = Appearance.Sanitise(appearance)
|
||||
focusOn(Config.CreatorScene.ped)
|
||||
previewPed = PedBuild.CreatePreview(appearance, Config.CreatorScene.ped, Config.CreatorScene.ped.w)
|
||||
if previewPed then
|
||||
PedBuild.Idle(previewPed)
|
||||
Cam.Orbit(previewPed, 2.4, 0.25, Config.CreatorScene.ped.w + 180.0, true)
|
||||
Cam.OrbitFrame(2.4, 0.25, 0.35)
|
||||
end
|
||||
cb({ ok = previewPed ~= nil })
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('char:delete', function(data, cb)
|
||||
CreateThread(function()
|
||||
local res, err = Core.Callback('rp_session', 'char:delete', { id = data.id })
|
||||
if not res then return cb({ ok = false, error = err }) end
|
||||
|
||||
local list = Core.Callback('rp_session', 'char:list')
|
||||
characters = (list and list.characters) or {}
|
||||
destroyPreview()
|
||||
Cam.Shots(Config.CinematicShots)
|
||||
cb({ ok = true, characters = characters })
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('char:select', function(data, cb)
|
||||
CreateThread(function()
|
||||
local res, err = Core.Callback('rp_session', 'char:select', { id = data.id })
|
||||
if not res then return cb({ ok = false, error = err }) end
|
||||
|
||||
spawnData = res
|
||||
chosenChar = res.character
|
||||
destroyPreview()
|
||||
|
||||
-- become the character now, off-screen, so the fly-down reveals them
|
||||
if res.appearance then
|
||||
PedBuild.ApplyToPlayer(Appearance.Sanitise(res.appearance))
|
||||
holdPlayer()
|
||||
end
|
||||
|
||||
cb({ ok = true, spawns = res.spawns, lastPosition = res.lastPosition, character = res.character })
|
||||
end)
|
||||
end)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- character creation
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
--- Read the game's own colour tables so the swatches in the UI are the real
|
||||
--- colours, not an approximation that drifts from what the ped ends up wearing.
|
||||
local paletteCache = nil
|
||||
local function palettes()
|
||||
if paletteCache then return paletteCache end
|
||||
local hair, makeup = {}, {}
|
||||
for i = 0, 63 do
|
||||
local ok, r, g, b = pcall(GetPedHairRgbColor, i)
|
||||
hair[#hair + 1] = ok and { r = r, g = g, b = b } or { r = 40, g = 40, b = 40 }
|
||||
local ok2, r2, g2, b2 = pcall(GetPedMakeupRgbColor, i)
|
||||
makeup[#makeup + 1] = ok2 and { r = r2, g = g2, b = b2 } or { r = 60, g = 40, b = 40 }
|
||||
end
|
||||
paletteCache = { hair = hair, makeup = makeup }
|
||||
return paletteCache
|
||||
end
|
||||
|
||||
--- The creator UI is built from this schema rather than a hardcoded copy, so
|
||||
--- adding a slider is a one-line change in shared/appearance.lua.
|
||||
local function pushDraft(cb)
|
||||
local payload = {
|
||||
ok = true,
|
||||
appearance = draft,
|
||||
limits = PedBuild.Limits(previewPed),
|
||||
palettes = palettes(),
|
||||
schema = {
|
||||
features = Appearance.FEATURES,
|
||||
overlays = Appearance.OVERLAYS,
|
||||
components = Appearance.COMPONENTS,
|
||||
props = Appearance.PROPS,
|
||||
},
|
||||
}
|
||||
if cb then cb(payload) else ui({ action = 'creator:sync', data = payload }) end
|
||||
end
|
||||
|
||||
local function rebuildPreview()
|
||||
destroyPreview()
|
||||
focusOn(Config.CreatorScene.ped)
|
||||
previewPed = PedBuild.CreatePreview(draft, Config.CreatorScene.ped, Config.CreatorScene.ped.w)
|
||||
if previewPed then
|
||||
PedBuild.Idle(previewPed)
|
||||
Cam.Orbit(previewPed, 1.5, 0.62, Config.CreatorScene.ped.w + 180.0, true)
|
||||
end
|
||||
end
|
||||
|
||||
RegisterNUICallback('creator:start', function(data, cb)
|
||||
CreateThread(function()
|
||||
draft = Appearance.Default(data and data.gender or 'm')
|
||||
rebuildPreview()
|
||||
pushDraft(cb)
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:gender', function(data, cb)
|
||||
CreateThread(function()
|
||||
draft = Appearance.Default(data.gender == 'f' and 'f' or 'm')
|
||||
rebuildPreview()
|
||||
pushDraft(cb)
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:parent', function(data, cb)
|
||||
if not draft or not previewPed then return cb({ ok = false }) end
|
||||
local key = data.key
|
||||
if key == 'father' or key == 'mother' then
|
||||
draft.parents[key] = math.floor(tonumber(data.value) or 0)
|
||||
elseif key == 'shapeMix' or key == 'skinMix' then
|
||||
draft.parents[key] = tonumber(data.value) or 0.5
|
||||
end
|
||||
SetPedHeadBlendData(previewPed,
|
||||
draft.parents.father, draft.parents.mother, 0,
|
||||
draft.parents.father, draft.parents.mother, 0,
|
||||
draft.parents.shapeMix + 0.0, draft.parents.skinMix + 0.0, 0.0, false)
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:feature', function(data, cb)
|
||||
if not draft or not previewPed then return cb({ ok = false }) end
|
||||
local id = math.floor(tonumber(data.id) or 0)
|
||||
local v = math.max(-1.0, math.min(1.0, tonumber(data.value) or 0.0))
|
||||
draft.features[tostring(id)] = v
|
||||
SetPedFaceFeature(previewPed, id, v + 0.0)
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:overlay', function(data, cb)
|
||||
if not draft or not previewPed then return cb({ ok = false }) end
|
||||
local slot
|
||||
for _, o in ipairs(Appearance.OVERLAYS) do if o.key == data.key then slot = o end end
|
||||
if not slot then return cb({ ok = false }) end
|
||||
|
||||
local ov = draft.overlays[data.key]
|
||||
if data.index ~= nil then ov.index = math.floor(tonumber(data.index)) end
|
||||
if data.opacity ~= nil then ov.opacity = tonumber(data.opacity) end
|
||||
if data.colour ~= nil then ov.colour = math.floor(tonumber(data.colour)) end
|
||||
|
||||
local index = (ov.index == nil or ov.index < 0) and 255 or ov.index
|
||||
SetPedHeadOverlay(previewPed, slot.id, index, ov.opacity + 0.0)
|
||||
if slot.tint and index ~= 255 then
|
||||
SetPedHeadOverlayColor(previewPed, slot.id, slot.tint == 'hair' and 1 or 2, ov.colour, ov.colour)
|
||||
end
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:hair', function(data, cb)
|
||||
if not draft or not previewPed then return cb({ ok = false }) end
|
||||
if data.style ~= nil then draft.hair.style = math.floor(tonumber(data.style)) end
|
||||
if data.colour ~= nil then draft.hair.colour = math.floor(tonumber(data.colour)) end
|
||||
if data.highlight ~= nil then draft.hair.highlight = math.floor(tonumber(data.highlight)) end
|
||||
SetPedComponentVariation(previewPed, 2, draft.hair.style, 0, 0)
|
||||
SetPedHairColor(previewPed, draft.hair.colour, draft.hair.highlight)
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:eyes', function(data, cb)
|
||||
if not draft or not previewPed then return cb({ ok = false }) end
|
||||
draft.eyeColour = math.floor(tonumber(data.value) or 0)
|
||||
SetPedEyeColor(previewPed, draft.eyeColour)
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:component', function(data, cb)
|
||||
if not draft or not previewPed then return cb({ ok = false }) end
|
||||
local comp
|
||||
for _, c in ipairs(Appearance.COMPONENTS) do if c.key == data.key then comp = c end end
|
||||
if not comp then return cb({ ok = false }) end
|
||||
|
||||
local cc = draft.components[data.key]
|
||||
if data.drawable ~= nil then cc.drawable = math.floor(tonumber(data.drawable)) end
|
||||
if data.texture ~= nil then cc.texture = math.floor(tonumber(data.texture)) end
|
||||
|
||||
-- a new drawable usually has a different number of textures
|
||||
local maxTex = math.max(0, GetNumberOfPedTextureVariations(previewPed, comp.id, cc.drawable) - 1)
|
||||
if cc.texture > maxTex then cc.texture = 0 end
|
||||
|
||||
SetPedComponentVariation(previewPed, comp.id, cc.drawable, cc.texture, 0)
|
||||
cb({ ok = true, texture = cc.texture, maxTexture = maxTex })
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:prop', function(data, cb)
|
||||
if not draft or not previewPed then return cb({ ok = false }) end
|
||||
local prop
|
||||
for _, p in ipairs(Appearance.PROPS) do if p.key == data.key then prop = p end end
|
||||
if not prop then return cb({ ok = false }) end
|
||||
|
||||
local pp = draft.props[data.key]
|
||||
if data.drawable ~= nil then pp.drawable = math.floor(tonumber(data.drawable)) end
|
||||
if data.texture ~= nil then pp.texture = math.floor(tonumber(data.texture)) end
|
||||
|
||||
if pp.drawable < 0 then
|
||||
ClearPedProp(previewPed, prop.id)
|
||||
cb({ ok = true, texture = 0, maxTexture = 0 })
|
||||
else
|
||||
local maxTex = math.max(0, GetNumberOfPedPropTextureVariations(previewPed, prop.id, pp.drawable) - 1)
|
||||
if pp.texture > maxTex then pp.texture = 0 end
|
||||
SetPedPropIndex(previewPed, prop.id, pp.drawable, pp.texture, true)
|
||||
cb({ ok = true, texture = pp.texture, maxTexture = maxTex })
|
||||
end
|
||||
end)
|
||||
|
||||
--- Camera framing per creator section.
|
||||
local FRAMES = {
|
||||
heritage = { radius = 1.05, height = 0.66, pitch = 0.02 },
|
||||
face = { radius = 0.85, height = 0.68, pitch = 0.0 },
|
||||
hair = { radius = 1.05, height = 0.70, pitch = 0.0 },
|
||||
body = { radius = 2.60, height = 0.20, pitch = 0.45 },
|
||||
clothing = { radius = 2.30, height = 0.28, pitch = 0.40 },
|
||||
identity = { radius = 1.90, height = 0.45, pitch = 0.25 },
|
||||
}
|
||||
|
||||
RegisterNUICallback('creator:frame', function(data, cb)
|
||||
local f = FRAMES[data.section] or FRAMES.face
|
||||
Cam.OrbitFrame(f.radius, f.height, f.pitch)
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:rotate', function(data, cb)
|
||||
Cam.Nudge(tonumber(data.delta) or 0.0)
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:submit', function(data, cb)
|
||||
CreateThread(function()
|
||||
local res, err = Core.Callback('rp_session', 'char:create', {
|
||||
firstName = data.firstName,
|
||||
lastName = data.lastName,
|
||||
dob = data.dob,
|
||||
gender = draft and draft.model or 'm',
|
||||
backstory = data.backstory,
|
||||
appearance = draft,
|
||||
})
|
||||
if not res then return cb({ ok = false, error = err }) end
|
||||
|
||||
local list = Core.Callback('rp_session', 'char:list')
|
||||
characters = (list and list.characters) or {}
|
||||
destroyPreview()
|
||||
draft = nil
|
||||
cb({ ok = true, characters = characters })
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('creator:cancel', function(_, cb)
|
||||
destroyPreview()
|
||||
draft = nil
|
||||
Cam.Shots(Config.CinematicShots)
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- spawn
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
RegisterNUICallback('spawn:preview', function(data, cb)
|
||||
CreateThread(function()
|
||||
if not spawnData then return cb({ ok = false }) end
|
||||
for _, sp in ipairs(spawnData.spawns) do
|
||||
if sp.id == data.id then
|
||||
local from = Cam.Position()
|
||||
focusOn(vector3(sp.coords.x, sp.coords.y, sp.coords.z))
|
||||
Cam.FlyTo(
|
||||
from, vector3(sp.cam.look.x, sp.cam.look.y, sp.cam.look.z),
|
||||
vector3(sp.cam.pos.x, sp.cam.pos.y, sp.cam.pos.z),
|
||||
vector3(sp.coords.x, sp.coords.y, sp.coords.z),
|
||||
2200
|
||||
)
|
||||
break
|
||||
end
|
||||
end
|
||||
cb({ ok = true })
|
||||
end)
|
||||
end)
|
||||
|
||||
RegisterNUICallback('spawn:confirm', function(data, cb)
|
||||
CreateThread(function()
|
||||
local res, err = Core.Callback('rp_session', 'spawn:confirm', { id = data.id })
|
||||
if not res then return cb({ ok = false, error = err }) end
|
||||
|
||||
cb({ ok = true })
|
||||
focusUI(false)
|
||||
ui({ action = 'stage', stage = 'flying' })
|
||||
|
||||
local c = res.coords
|
||||
local ped = PlayerPedId()
|
||||
|
||||
-- stream the ground before anyone stands on it
|
||||
focusOn(vector3(c.x, c.y, c.z))
|
||||
RequestCollisionAtCoord(c.x, c.y, c.z)
|
||||
local waited = 0
|
||||
while not HasCollisionLoadedAroundEntity(ped) and waited < 8000 do
|
||||
RequestCollisionAtCoord(c.x, c.y, c.z)
|
||||
Wait(50)
|
||||
waited = waited + 50
|
||||
end
|
||||
|
||||
SetEntityCoordsNoOffset(ped, c.x, c.y, c.z, false, false, false)
|
||||
SetEntityHeading(ped, c.h or 0.0)
|
||||
|
||||
local from = Cam.Position()
|
||||
Cam.FlyTo(
|
||||
from, vector3(c.x, c.y, c.z),
|
||||
vector3(c.x - 2.4, c.y - 2.4, c.z + 1.4), vector3(c.x, c.y, c.z + 0.2),
|
||||
3400,
|
||||
function()
|
||||
SetEntityVisible(ped, true, false)
|
||||
releasePlayer()
|
||||
setSceneMood(false)
|
||||
ClearFocus()
|
||||
Cam.Release(1400)
|
||||
ui({ action = 'stage', stage = 'live' })
|
||||
TriggerEvent('rp:ui:spawned', chosenChar)
|
||||
end
|
||||
)
|
||||
end)
|
||||
end)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- misc
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
RegisterNUICallback('ui:log', function(data, cb)
|
||||
print(('^3[rp_ui]^7 %s'):format(tostring(data and data.message)))
|
||||
cb({ ok = true })
|
||||
end)
|
||||
|
||||
AddEventHandler('onResourceStop', function(res)
|
||||
if res ~= GetCurrentResourceName() then return end
|
||||
destroyPreview()
|
||||
Cam.Release(0)
|
||||
focusUI(false)
|
||||
releasePlayer()
|
||||
setSceneMood(false)
|
||||
ClearFocus()
|
||||
end)
|
||||
@@ -0,0 +1,160 @@
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Building a freemode ped from an appearance table.
|
||||
--
|
||||
-- The same code paints the creator preview and the live player, so what you
|
||||
-- saw while editing is exactly what walks into the world.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
PedBuild = {}
|
||||
|
||||
local NO_OVERLAY = 255 -- the native's "none" value; our model stores -1
|
||||
|
||||
local function loadModel(name)
|
||||
local hash = joaat(name)
|
||||
if not IsModelInCdimage(hash) or not IsModelValid(hash) then return nil end
|
||||
RequestModel(hash)
|
||||
local waited = 0
|
||||
while not HasModelLoaded(hash) and waited < 10000 do
|
||||
Wait(10)
|
||||
waited = waited + 10
|
||||
end
|
||||
if not HasModelLoaded(hash) then return nil end
|
||||
return hash
|
||||
end
|
||||
|
||||
PedBuild.LoadModel = loadModel
|
||||
|
||||
--- Apply an appearance table to an existing freemode ped.
|
||||
function PedBuild.Apply(ped, a)
|
||||
if not ped or not DoesEntityExist(ped) or type(a) ~= 'table' then return end
|
||||
|
||||
SetPedHeadBlendData(
|
||||
ped,
|
||||
a.parents.father, a.parents.mother, 0,
|
||||
a.parents.father, a.parents.mother, 0,
|
||||
a.parents.shapeMix + 0.0, a.parents.skinMix + 0.0, 0.0,
|
||||
false
|
||||
)
|
||||
|
||||
for _, f in ipairs(Appearance.FEATURES) do
|
||||
local v = a.features[tostring(f.id)]
|
||||
if v then SetPedFaceFeature(ped, f.id, v + 0.0) end
|
||||
end
|
||||
|
||||
for _, o in ipairs(Appearance.OVERLAYS) do
|
||||
local ov = a.overlays[o.key]
|
||||
if ov then
|
||||
local index = (ov.index == nil or ov.index < 0) and NO_OVERLAY or ov.index
|
||||
SetPedHeadOverlay(ped, o.id, index, ov.opacity + 0.0)
|
||||
if o.tint and index ~= NO_OVERLAY then
|
||||
-- tint type 1 = hair palette, 2 = make-up palette
|
||||
SetPedHeadOverlayColor(ped, o.id, o.tint == 'hair' and 1 or 2, ov.colour, ov.colour)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
SetPedComponentVariation(ped, 2, a.hair.style, 0, 0)
|
||||
SetPedHairColor(ped, a.hair.colour, a.hair.highlight)
|
||||
SetPedEyeColor(ped, a.eyeColour)
|
||||
|
||||
for _, c in ipairs(Appearance.COMPONENTS) do
|
||||
local cc = a.components[c.key]
|
||||
if cc then SetPedComponentVariation(ped, c.id, cc.drawable, cc.texture, 0) end
|
||||
end
|
||||
|
||||
for _, p in ipairs(Appearance.PROPS) do
|
||||
local pp = a.props[p.key]
|
||||
if pp then
|
||||
if not pp.drawable or pp.drawable < 0 then
|
||||
ClearPedProp(ped, p.id)
|
||||
else
|
||||
SetPedPropIndex(ped, p.id, pp.drawable, pp.texture, true)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--- How many variations this particular model actually has, so the creator's
|
||||
--- sliders stop at real values instead of guessed ones.
|
||||
function PedBuild.Limits(ped)
|
||||
local limits = { components = {}, props = {}, overlays = {}, hair = 0 }
|
||||
if not ped or not DoesEntityExist(ped) then return limits end
|
||||
|
||||
limits.hair = math.max(0, GetNumberOfPedDrawableVariations(ped, 2) - 1)
|
||||
|
||||
for _, c in ipairs(Appearance.COMPONENTS) do
|
||||
local drawables = math.max(0, GetNumberOfPedDrawableVariations(ped, c.id) - 1)
|
||||
local current = GetPedDrawableVariation(ped, c.id)
|
||||
limits.components[c.key] = {
|
||||
drawable = drawables,
|
||||
texture = math.max(0, GetNumberOfPedTextureVariations(ped, c.id, current) - 1),
|
||||
}
|
||||
end
|
||||
|
||||
for _, p in ipairs(Appearance.PROPS) do
|
||||
local drawables = math.max(-1, GetNumberOfPedPropDrawableVariations(ped, p.id) - 1)
|
||||
local current = GetPedPropIndex(ped, p.id)
|
||||
limits.props[p.key] = {
|
||||
drawable = drawables,
|
||||
texture = math.max(0, GetNumberOfPedPropTextureVariations(ped, p.id, current) - 1),
|
||||
}
|
||||
end
|
||||
|
||||
for _, o in ipairs(Appearance.OVERLAYS) do
|
||||
limits.overlays[o.key] = o.max
|
||||
end
|
||||
|
||||
return limits
|
||||
end
|
||||
|
||||
--- Create a standalone preview ped (never networked).
|
||||
function PedBuild.CreatePreview(appearance, coords, heading)
|
||||
local modelName = Appearance.MODELS[appearance.model] or Appearance.MODELS.m
|
||||
local hash = loadModel(modelName)
|
||||
if not hash then return nil end
|
||||
|
||||
local ped = CreatePed(2, hash, coords.x, coords.y, coords.z, heading + 0.0, false, false)
|
||||
SetModelAsNoLongerNeeded(hash)
|
||||
|
||||
SetEntityInvincible(ped, true)
|
||||
FreezeEntityPosition(ped, true)
|
||||
SetBlockingOfNonTemporaryEvents(ped, true)
|
||||
SetPedDefaultComponentVariation(ped)
|
||||
SetPedCanRagdoll(ped, false)
|
||||
SetEntityCollision(ped, false, false)
|
||||
|
||||
PedBuild.Apply(ped, appearance)
|
||||
return ped
|
||||
end
|
||||
|
||||
--- Turn the local player into this character.
|
||||
function PedBuild.ApplyToPlayer(appearance)
|
||||
local modelName = Appearance.MODELS[appearance.model] or Appearance.MODELS.m
|
||||
local hash = loadModel(modelName)
|
||||
if not hash then return false end
|
||||
|
||||
SetPlayerModel(PlayerId(), hash)
|
||||
SetModelAsNoLongerNeeded(hash)
|
||||
|
||||
local ped = PlayerPedId()
|
||||
SetPedDefaultComponentVariation(ped)
|
||||
PedBuild.Apply(ped, appearance)
|
||||
-- freemode peds start with no head blend applied until this settles a frame
|
||||
Wait(50)
|
||||
PedBuild.Apply(ped, appearance)
|
||||
return true
|
||||
end
|
||||
|
||||
--- A calm idle so the preview does not stand like a mannequin.
|
||||
function PedBuild.Idle(ped)
|
||||
local dict = 'anim@heists@heist_corona@team_idles@female_a'
|
||||
RequestAnimDict(dict)
|
||||
local waited = 0
|
||||
while not HasAnimDictLoaded(dict) and waited < 3000 do
|
||||
Wait(10)
|
||||
waited = waited + 10
|
||||
end
|
||||
if HasAnimDictLoaded(dict) then
|
||||
TaskPlayAnim(ped, dict, 'idle', 2.0, 2.0, -1, 1, 0, false, false, false)
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user