Files
redl-gamepanel/resources/[local]/fiverp-loadscreen/html/app.js
T
Claude Opus 5andClaude Opus 5 c857979a55 FiveRP: the server from the hosting account, not the earlier one
Wrong server was published before. This is the one that runs on the
hosting account the video was made on: FiveRP - two-step registration
(account, then an identity printed onto a passport), login that returns
you to your resident, and a loading screen driven by the game's own
streaming events.

  resources/[local]/fiverp-auth          NUI + scrypt hashes + oxmysql
  resources/[local]/fiverp-characters    identity, character load, spawn
  resources/[local]/fiverp-loadscreen    loading screen
  sql/schema.sql                         accounts, characters
  docs/DESIGN.md                         the design system every screen follows
  docs/screens/                          shot from the live server

Only our own code is in here. cfx-server-data and oxmysql are fetched by
install.sh instead of being vendored, which keeps the repo at 3.7 MB.

install.sh does the whole box in one command: recommended FXServer
build, cfx-server-data, oxmysql, MariaDB with the schema, resources,
server.cfg (mode 600 - it carries the key and the database password),
boot entry, then it waits for the Cfx registration. Tested end to end on
a spare install root: 29 resources scanned, database and schema created,
and it stopped exactly where a wrong licence key should stop it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-12 23:42:20 +00:00

134 lines
4.4 KiB
JavaScript

/* FiveRP — loading screen behaviour.
The game drives this page by posting messages at `window`. The only field
that is a genuine 0..1 measure of progress is `loadProgress.loadFraction`;
everything else is narration we turn into the phase and detail lines. */
(function () {
'use strict';
const elPct = document.getElementById('pct');
const elFill = document.getElementById('fill');
const elPhase = document.getElementById('phase');
const elDetail = document.getElementById('detail');
const elTip = document.getElementById('tip');
/* ------------------------------------------------------------- progress */
// Shown and target values are kept apart so the number eases toward the
// truth instead of jumping. The game reports progress in coarse steps.
let shown = 0;
let target = 0;
function setTarget(fraction) {
const pct = Math.max(0, Math.min(100, fraction * 100));
// Never walk backwards: a later phase reporting a lower fraction would
// otherwise make the bar visibly retreat.
if (pct > target) target = pct;
}
function tick() {
const gap = target - shown;
if (gap > 0.05) {
shown += Math.max(gap * 0.08, 0.05);
if (shown > target) shown = target;
elFill.style.width = shown.toFixed(2) + '%';
elPct.textContent = Math.floor(shown);
}
requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
/* ---------------------------------------------------------------- phases */
function phase(text) {
if (elPhase.textContent !== text) elPhase.textContent = text;
}
function detail(text) {
const clean = String(text || '').trim();
if (!clean) return;
elDetail.textContent = clean.length > 88 ? clean.slice(0, 87) + '…' : clean;
}
// Init function names arrive as engine internals ("CPathServer::InitSession").
// Strip the noise so the line reads as a status, not a stack trace.
function humanise(name) {
return String(name || '')
.replace(/^dlc_?/i, 'DLC ')
.replace(/::.*$/, '')
.replace(/^C(?=[A-Z])/, '')
.replace(/([a-z])([A-Z])/g, '$1 $2');
}
window.addEventListener('message', function (event) {
const data = event.data || {};
switch (data.eventName) {
case 'loadProgress':
setTarget(Number(data.loadFraction) || 0);
break;
case 'startInitFunctionOrder':
phase('Loading game');
break;
case 'startInitFunction':
phase('Loading game');
break;
case 'initFunctionInvoking':
phase('Loading game');
detail(humanise(data.name));
// The engine gives idx/count per order; use it when loadProgress is
// quiet so the bar keeps creeping during long orders.
if (data.count) setTarget(0.1 + 0.5 * (Number(data.idx) / Number(data.count)));
break;
case 'startDataFileEntries':
phase('Streaming content');
detail('Mounting ' + (data.count || 0) + ' data files');
break;
case 'performMappedDataFileEntry':
phase('Streaming content');
detail(String(data.name || '').split('/').pop());
break;
case 'onLogLine':
detail(data.message);
break;
case 'onDataFileEntry':
phase('Streaming content');
break;
}
});
/* ------------------------------------------------------------------ tips */
const TIPS = [
'Your account and your character are separate — one login can hold your identity for good.',
'Names are issued once. Pick a first and last name you want to be known by.',
'Press F8 at any time to open the console if something looks wrong.',
'Your passport is proof of residency — officers may ask to see it.',
'Use a real email address. It is the only way to recover an account.',
'Speak in character. The city is more fun when everyone stays in the story.',
'Lost? The Legion Square spawn is the heart of downtown Los Santos.'
];
let tipIndex = 0;
setInterval(function () {
tipIndex = (tipIndex + 1) % TIPS.length;
elTip.classList.add('is-swapping');
setTimeout(function () {
elTip.textContent = TIPS[tipIndex];
elTip.classList.remove('is-swapping');
}, 450);
}, 7000);
/* Nothing has reported in yet — creep to a believable floor so the screen
never sits at a dead 0% while the engine warms up. */
setTimeout(function () { setTarget(0.04); }, 400);
})();