Opus 5 redesign: county-records theme across all NUI screens
Auth becomes RESIDENT INTAKE (form LS-01), character creation an identity-record intake with a live paper card (LS-02), spawn select a county survey grid with five entry points (LS-03). Live validation, click-drag character rotation, animated transitions throughout. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1048
-1278
File diff suppressed because it is too large
Load Diff
+358
-487
@@ -2,596 +2,467 @@
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>JustRP — Loading</title>
|
||||
<title>Los Santos County — Resident Processing</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Cinzel:wght@400;600;700&family=Inter:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Barlow+Condensed:wght@500;600;700&family=IBM+Plex+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
*,*::before,*::after{box-sizing:border-box;margin:0;padding:0}
|
||||
|
||||
:root{
|
||||
--bg: #07070c;
|
||||
--accent: #c8933f;
|
||||
--accent2: #e0aa55;
|
||||
--text: #ede9e4;
|
||||
--text2: rgba(237,233,228,0.55);
|
||||
--text3: rgba(237,233,228,0.28);
|
||||
--ink: #10131A;
|
||||
--ink-2: #171B24;
|
||||
--paper: #E8E4DA;
|
||||
--paper-2: #D6D0C2;
|
||||
--stamp: #C2462E;
|
||||
--rule: rgba(232,228,218,.14);
|
||||
--rule-2: rgba(232,228,218,.07);
|
||||
--dim: rgba(232,228,218,.42);
|
||||
--dim-2: rgba(232,228,218,.24);
|
||||
--sig: 'Barlow Condensed','Oswald',sans-serif;
|
||||
--body: 'IBM Plex Sans',system-ui,sans-serif;
|
||||
--mono: 'IBM Plex Mono',ui-monospace,monospace;
|
||||
}
|
||||
|
||||
html, body {
|
||||
width: 100%; height: 100%; overflow: hidden;
|
||||
background: var(--bg);
|
||||
font-family: 'Inter', sans-serif;
|
||||
color: var(--text);
|
||||
html,body{width:100%;height:100%;overflow:hidden;background:var(--ink);color:var(--paper);font-family:var(--body)}
|
||||
|
||||
/* ── Survey footage ─────────────────────────────── */
|
||||
#flyover{position:fixed;inset:0;width:100%;height:100%;filter:saturate(.55) contrast(1.06)}
|
||||
|
||||
.footage-tint{position:fixed;inset:0;pointer-events:none;
|
||||
background:
|
||||
radial-gradient(ellipse 80% 60% at 50% 42%,transparent 0%,rgba(16,19,26,.5) 68%,rgba(16,19,26,.94) 100%),
|
||||
linear-gradient(180deg,rgba(16,19,26,.88) 0%,transparent 22%,transparent 62%,rgba(16,19,26,.96) 100%);
|
||||
}
|
||||
|
||||
/* ── Canvas cityscape ── */
|
||||
#city-canvas {
|
||||
position: fixed; inset: 0;
|
||||
width: 100%; height: 100%;
|
||||
}
|
||||
.scanlines{position:fixed;inset:0;pointer-events:none;opacity:.5;
|
||||
background:repeating-linear-gradient(180deg,rgba(0,0,0,.22) 0px,rgba(0,0,0,.22) 1px,transparent 1px,transparent 3px)}
|
||||
|
||||
/* ── Vignette overlay ── */
|
||||
.vignette {
|
||||
position: fixed; inset: 0;
|
||||
background: radial-gradient(ellipse at center,
|
||||
transparent 30%,
|
||||
rgba(7,7,12,0.55) 70%,
|
||||
rgba(7,7,12,0.9) 100%
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
.grain{position:fixed;inset:-50%;pointer-events:none;opacity:.19;
|
||||
background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='180' height='180'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
||||
animation:grainshift .7s steps(3) infinite}
|
||||
@keyframes grainshift{
|
||||
0%{transform:translate(0,0)}33%{transform:translate(-3%,2%)}66%{transform:translate(2%,-3%)}100%{transform:translate(0,0)}}
|
||||
|
||||
/* ── Center brand ── */
|
||||
.brand {
|
||||
position: fixed;
|
||||
top: 50%; left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
animation: fadeUp 1.2s cubic-bezier(0.16,1,0.3,1) 0.8s forwards;
|
||||
}
|
||||
/* ── Registration marks ─────────────────────────── */
|
||||
.reg{position:fixed;width:22px;height:22px;pointer-events:none;opacity:0;
|
||||
animation:regIn .5s steps(2) .3s forwards}
|
||||
.reg::before,.reg::after{content:'';position:absolute;background:var(--dim-2)}
|
||||
.reg::before{left:0;right:0;top:50%;height:1px}
|
||||
.reg::after{top:0;bottom:0;left:50%;width:1px}
|
||||
.reg-tl{top:26px;left:26px}.reg-tr{top:26px;right:26px}
|
||||
.reg-bl{bottom:26px;left:26px}.reg-br{bottom:26px;right:26px}
|
||||
@keyframes regIn{to{opacity:1}}
|
||||
|
||||
.brand-title {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: clamp(56px, 7vw, 96px);
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.18em;
|
||||
color: var(--text);
|
||||
line-height: 1;
|
||||
text-shadow: 0 0 80px rgba(200,147,63,0.35);
|
||||
}
|
||||
/* ── Header slate ───────────────────────────────── */
|
||||
.slate{position:fixed;top:26px;left:64px;right:64px;display:flex;justify-content:space-between;
|
||||
align-items:flex-start;gap:24px;opacity:0;animation:fadeDown .8s cubic-bezier(.2,.8,.25,1) .35s forwards}
|
||||
|
||||
.brand-title span {
|
||||
color: var(--accent);
|
||||
}
|
||||
.slate-id{font-family:var(--mono);font-size:10px;letter-spacing:.16em;color:var(--dim);line-height:2}
|
||||
.slate-id b{color:var(--paper);font-weight:500}
|
||||
|
||||
.brand-rule {
|
||||
width: 180px; height: 1px;
|
||||
background: linear-gradient(90deg, transparent, var(--accent), transparent);
|
||||
margin: 20px auto 18px;
|
||||
}
|
||||
.slate-read{font-family:var(--mono);font-size:10px;letter-spacing:.14em;color:var(--dim);
|
||||
text-align:right;line-height:2}
|
||||
.slate-read span{color:var(--stamp)}
|
||||
|
||||
.brand-sub {
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.32em;
|
||||
text-transform: uppercase;
|
||||
color: var(--text2);
|
||||
font-weight: 400;
|
||||
}
|
||||
@keyframes fadeDown{from{opacity:0;transform:translateY(-8px)}to{opacity:1;transform:none}}
|
||||
|
||||
/* ── Rules panel ── */
|
||||
.rules-panel {
|
||||
position: fixed;
|
||||
top: 50%; right: 72px;
|
||||
transform: translateY(-50%) translateX(40px);
|
||||
width: 260px;
|
||||
background: rgba(13,13,24,0.82);
|
||||
border: 1px solid rgba(200,147,63,0.12);
|
||||
border-radius: 10px;
|
||||
padding: 24px 26px;
|
||||
backdrop-filter: blur(12px);
|
||||
opacity: 0;
|
||||
transition: opacity 0.8s ease, transform 0.8s cubic-bezier(0.16,1,0.3,1);
|
||||
}
|
||||
/* ── Title block: left-anchored, not centered ───── */
|
||||
.title{position:fixed;left:64px;top:50%;transform:translateY(-50%);max-width:620px}
|
||||
|
||||
.rules-panel.visible {
|
||||
opacity: 1;
|
||||
transform: translateY(-50%) translateX(0);
|
||||
}
|
||||
.title-eyebrow{font-family:var(--mono);font-size:10px;letter-spacing:.3em;color:var(--stamp);
|
||||
margin-bottom:14px;opacity:0;animation:riseIn .7s cubic-bezier(.2,.8,.25,1) .7s forwards}
|
||||
|
||||
.rules-panel h3 {
|
||||
font-family: 'Cinzel', serif;
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.24em;
|
||||
text-transform: uppercase;
|
||||
color: var(--accent);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.title-main{font-family:var(--sig);font-weight:700;font-size:clamp(64px,9.5vw,140px);
|
||||
line-height:.83;letter-spacing:-.015em;text-transform:uppercase;
|
||||
opacity:0;animation:riseIn .9s cubic-bezier(.2,.8,.25,1) .85s forwards}
|
||||
.title-main em{font-style:normal;display:block;color:var(--stamp)}
|
||||
|
||||
.rules-panel ol {
|
||||
padding-left: 18px;
|
||||
}
|
||||
.title-rule{height:1px;background:var(--rule);margin:26px 0 18px;transform:scaleX(0);
|
||||
transform-origin:left;animation:ruleOut .9s cubic-bezier(.2,.8,.25,1) 1.25s forwards}
|
||||
@keyframes ruleOut{to{transform:scaleX(1)}}
|
||||
|
||||
.rules-panel li {
|
||||
font-size: 12px;
|
||||
line-height: 1.9;
|
||||
color: var(--text2);
|
||||
padding-left: 4px;
|
||||
}
|
||||
.title-sub{font-size:13px;line-height:1.75;color:var(--dim);max-width:390px;
|
||||
opacity:0;animation:riseIn .7s cubic-bezier(.2,.8,.25,1) 1.4s forwards}
|
||||
|
||||
.rules-panel li::marker {
|
||||
color: var(--accent);
|
||||
font-size: 10px;
|
||||
}
|
||||
@keyframes riseIn{from{opacity:0;transform:translateY(14px)}to{opacity:1;transform:none}}
|
||||
|
||||
/* ── Footer ── */
|
||||
.footer {
|
||||
position: fixed;
|
||||
bottom: 0; left: 0; right: 0;
|
||||
padding: 0 60px 42px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 32px;
|
||||
}
|
||||
/* ── Ordinance list ─────────────────────────────── */
|
||||
.ord{position:fixed;right:64px;top:50%;transform:translateY(-50%);width:290px;
|
||||
opacity:0;transition:opacity .9s cubic-bezier(.2,.8,.25,1)}
|
||||
.ord.on{opacity:1}
|
||||
|
||||
.progress-wrap {
|
||||
flex: 1;
|
||||
max-width: 600px;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.8s ease 1.4s forwards;
|
||||
}
|
||||
.ord-head{font-family:var(--mono);font-size:10px;letter-spacing:.2em;color:var(--dim-2);
|
||||
padding-bottom:10px;border-bottom:1px solid var(--rule);margin-bottom:4px}
|
||||
|
||||
.progress-label {
|
||||
font-size: 11px;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--text3);
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.ord-item{display:flex;gap:12px;padding:9px 0;border-bottom:1px solid var(--rule-2);
|
||||
opacity:0;transform:translateX(12px)}
|
||||
.ord.on .ord-item{animation:ordIn .55s cubic-bezier(.2,.8,.25,1) forwards}
|
||||
.ord.on .ord-item:nth-child(2){animation-delay:.05s}
|
||||
.ord.on .ord-item:nth-child(3){animation-delay:.12s}
|
||||
.ord.on .ord-item:nth-child(4){animation-delay:.19s}
|
||||
.ord.on .ord-item:nth-child(5){animation-delay:.26s}
|
||||
.ord.on .ord-item:nth-child(6){animation-delay:.33s}
|
||||
.ord.on .ord-item:nth-child(7){animation-delay:.40s}
|
||||
.ord.on .ord-item:nth-child(8){animation-delay:.47s}
|
||||
@keyframes ordIn{to{opacity:1;transform:none}}
|
||||
|
||||
.progress-label span:last-child {
|
||||
color: var(--accent);
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.ord-no{font-family:var(--mono);font-size:10px;color:var(--stamp);flex-shrink:0;padding-top:2px}
|
||||
.ord-tx{font-size:12px;line-height:1.55;color:var(--dim)}
|
||||
|
||||
.progress-track {
|
||||
height: 2px;
|
||||
background: rgba(255,255,255,0.07);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
}
|
||||
/* ── Processing bar ─────────────────────────────── */
|
||||
.proc{position:fixed;left:64px;right:64px;bottom:52px;
|
||||
opacity:0;animation:fadeUp .8s cubic-bezier(.2,.8,.25,1) 1.6s forwards}
|
||||
@keyframes fadeUp{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
|
||||
|
||||
.progress-fill {
|
||||
height: 100%;
|
||||
width: 0%;
|
||||
background: linear-gradient(90deg, var(--accent), var(--accent2));
|
||||
border-radius: 2px;
|
||||
transition: width 0.4s cubic-bezier(0.16,1,0.3,1);
|
||||
box-shadow: 0 0 12px rgba(200,147,63,0.6);
|
||||
}
|
||||
.proc-top{display:flex;justify-content:space-between;align-items:baseline;
|
||||
font-family:var(--mono);font-size:10px;letter-spacing:.16em;margin-bottom:9px}
|
||||
.proc-stage{color:var(--dim)}
|
||||
.proc-pct{color:var(--paper);font-size:13px;font-variant-numeric:tabular-nums}
|
||||
|
||||
/* ── Volume ── */
|
||||
.vol-wrap {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
opacity: 0;
|
||||
animation: fadeIn 0.8s ease 1.8s forwards;
|
||||
}
|
||||
.proc-track{height:2px;background:rgba(232,228,218,.09);position:relative;overflow:hidden}
|
||||
.proc-fill{position:absolute;inset:0 auto 0 0;width:0%;background:var(--stamp);
|
||||
transition:width .45s cubic-bezier(.2,.8,.25,1)}
|
||||
.proc-fill::after{content:'';position:absolute;right:0;top:-3px;bottom:-3px;width:1px;
|
||||
background:var(--paper);opacity:.85}
|
||||
|
||||
.vol-icon {
|
||||
font-size: 14px;
|
||||
color: var(--text3);
|
||||
user-select: none;
|
||||
}
|
||||
/* segment ticks encode the real load phases */
|
||||
.proc-ticks{position:relative;height:8px;margin-top:6px}
|
||||
.proc-tick{position:absolute;top:0;width:1px;height:4px;background:var(--rule)}
|
||||
.proc-tick span{position:absolute;top:7px;left:0;transform:translateX(-50%);
|
||||
font-family:var(--mono);font-size:8px;letter-spacing:.1em;color:var(--dim-2);white-space:nowrap}
|
||||
|
||||
input[type=range] {
|
||||
-webkit-appearance: none;
|
||||
width: 90px; height: 2px;
|
||||
background: rgba(255,255,255,0.1);
|
||||
border-radius: 2px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
/* ── Audio control ──────────────────────────────── */
|
||||
.aud{position:fixed;right:64px;bottom:52px;display:flex;align-items:center;gap:11px;
|
||||
opacity:0;animation:fadeUp .8s cubic-bezier(.2,.8,.25,1) 1.85s forwards}
|
||||
.aud-lb{font-family:var(--mono);font-size:9px;letter-spacing:.18em;color:var(--dim-2)}
|
||||
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
width: 12px; height: 12px;
|
||||
background: var(--accent);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 6px rgba(200,147,63,0.6);
|
||||
transition: transform 0.15s ease;
|
||||
}
|
||||
input[type=range]{-webkit-appearance:none;appearance:none;width:82px;height:1px;
|
||||
background:var(--rule);outline:none;cursor:pointer}
|
||||
input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;appearance:none;
|
||||
width:9px;height:9px;background:var(--paper);cursor:pointer;
|
||||
transition:transform .18s cubic-bezier(.2,.8,.25,1),background .18s}
|
||||
input[type=range]:hover::-webkit-slider-thumb{transform:scale(1.45);background:var(--stamp)}
|
||||
input[type=range]:focus-visible{outline:1px solid var(--stamp);outline-offset:6px}
|
||||
|
||||
input[type=range]:hover::-webkit-slider-thumb {
|
||||
transform: scale(1.3);
|
||||
@media (prefers-reduced-motion:reduce){
|
||||
*{animation-duration:.01ms!important;transition-duration:.01ms!important}
|
||||
.grain{animation:none}
|
||||
}
|
||||
|
||||
/* ── Animations ── */
|
||||
@keyframes fadeUp {
|
||||
from { opacity: 0; transform: translate(-50%, calc(-50% + 20px)); }
|
||||
to { opacity: 1; transform: translate(-50%, -50%); }
|
||||
}
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
@media (max-width:900px){
|
||||
.ord{display:none}
|
||||
.slate,.title,.proc{left:28px;right:28px}
|
||||
.aud{right:28px}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<canvas id="city-canvas"></canvas>
|
||||
<div class="vignette"></div>
|
||||
<canvas id="flyover"></canvas>
|
||||
<div class="footage-tint"></div>
|
||||
<div class="scanlines"></div>
|
||||
<div class="grain"></div>
|
||||
|
||||
<div class="brand">
|
||||
<div class="brand-title">JUST<span>RP</span></div>
|
||||
<div class="brand-rule"></div>
|
||||
<div class="brand-sub">Los Santos Roleplay</div>
|
||||
<div class="reg reg-tl"></div><div class="reg reg-tr"></div>
|
||||
<div class="reg reg-bl"></div><div class="reg reg-br"></div>
|
||||
|
||||
<div class="slate">
|
||||
<div class="slate-id">
|
||||
LOS SANTOS COUNTY · DEPT. OF RESIDENT AFFAIRS<br>
|
||||
AERIAL SURVEY <b>LS-4471-A</b> · SECTOR <b>07</b>
|
||||
</div>
|
||||
<div class="slate-read">
|
||||
ALT <span id="r-alt">1420</span> FT · HDG <span id="r-hdg">274</span>°<br>
|
||||
<span id="r-clock">00:00:00</span> · REC <span id="r-rec">●</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rules-panel" id="rules">
|
||||
<h3>Server Rules</h3>
|
||||
<ol>
|
||||
<li>Respect every player</li>
|
||||
<li>No metagaming or powergaming</li>
|
||||
<li>No random deathmatch (RDM)</li>
|
||||
<li>No vehicle deathmatch (VDM)</li>
|
||||
<li>Value your character's life</li>
|
||||
<li>Stay in character at all times</li>
|
||||
<li>Admin decisions are final</li>
|
||||
</ol>
|
||||
<div class="title">
|
||||
<div class="title-eyebrow">INTAKE IN PROGRESS</div>
|
||||
<h1 class="title-main">Just<em>RP</em></h1>
|
||||
<div class="title-rule"></div>
|
||||
<p class="title-sub">A persistent roleplay county. Your record begins the moment you land — every job, every debt, every name you make here is kept on file.</p>
|
||||
</div>
|
||||
|
||||
<div class="footer">
|
||||
<div class="progress-wrap">
|
||||
<div class="progress-label">
|
||||
<span id="prog-label">Connecting to server…</span>
|
||||
<span id="prog-pct">0%</span>
|
||||
<div class="ord" id="ord">
|
||||
<div class="ord-head">COUNTY ORDINANCE · CONDITIONS OF RESIDENCY</div>
|
||||
<div class="ord-item"><span class="ord-no">§1</span><span class="ord-tx">Remain in character within county limits.</span></div>
|
||||
<div class="ord-item"><span class="ord-no">§2</span><span class="ord-tx">Your life has value. Act like it does.</span></div>
|
||||
<div class="ord-item"><span class="ord-no">§3</span><span class="ord-tx">No violence without roleplay leading to it.</span></div>
|
||||
<div class="ord-item"><span class="ord-no">§4</span><span class="ord-tx">Vehicles are not weapons.</span></div>
|
||||
<div class="ord-item"><span class="ord-no">§5</span><span class="ord-tx">Knowledge your character never earned is knowledge they do not have.</span></div>
|
||||
<div class="ord-item"><span class="ord-no">§6</span><span class="ord-tx">Losing is part of the story. Play it out.</span></div>
|
||||
<div class="ord-item"><span class="ord-no">§7</span><span class="ord-tx">Staff rulings close the matter.</span></div>
|
||||
</div>
|
||||
<div class="progress-track">
|
||||
<div class="progress-fill" id="prog-fill"></div>
|
||||
|
||||
<div class="proc">
|
||||
<div class="proc-top">
|
||||
<span class="proc-stage" id="stage">ESTABLISHING UPLINK</span>
|
||||
<span class="proc-pct" id="pct">0%</span>
|
||||
</div>
|
||||
<div class="proc-track"><div class="proc-fill" id="fill"></div></div>
|
||||
<div class="proc-ticks" id="ticks"></div>
|
||||
</div>
|
||||
<div class="vol-wrap">
|
||||
<span class="vol-icon">♪</span>
|
||||
<input type="range" id="vol" min="0" max="100" value="35">
|
||||
</div>
|
||||
|
||||
<div class="aud">
|
||||
<span class="aud-lb">ROOM TONE</span>
|
||||
<input type="range" id="vol" min="0" max="100" value="35" aria-label="Ambient volume">
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ── Cityscape canvas ─────────────────────────────────────────────────────────
|
||||
'use strict';
|
||||
const $ = id => document.getElementById(id);
|
||||
const reduced = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
|
||||
const canvas = document.getElementById('city-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
/* ══ Perspective city flyover ══════════════════════
|
||||
Real 3D projection: blocks on a ground grid stream
|
||||
toward the camera and are recycled behind it. */
|
||||
const cv = $('flyover'), cx = cv.getContext('2d');
|
||||
let W, H, FOCAL;
|
||||
|
||||
let W, H, panX = 0;
|
||||
const layers = [];
|
||||
const CAM = { x: 0, y: 210, z: 0, speed: 1.42, drift: 0 };
|
||||
const SPACING = 150, LANES = 11, DEPTH = 46, FAR = SPACING * DEPTH;
|
||||
let blocks = [];
|
||||
|
||||
function randInt(min, max) {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
function generateCity() {
|
||||
layers.length = 0;
|
||||
// 3 depth layers: far (small), mid, near (tall)
|
||||
const configs = [
|
||||
{ count: 80, minH: 0.06, maxH: 0.18, minW: 8, maxW: 30, baseY: 0.68, alpha: 0.18, drift: 0.015 },
|
||||
{ count: 40, minH: 0.12, maxH: 0.28, minW: 14, maxW: 50, baseY: 0.72, alpha: 0.30, drift: 0.030 },
|
||||
{ count: 22, minH: 0.18, maxH: 0.42, minW: 22, maxW: 80, baseY: 0.78, alpha: 0.55, drift: 0.055 },
|
||||
];
|
||||
for (const cfg of configs) {
|
||||
const bldgs = [];
|
||||
let x = -W * 0.3;
|
||||
for (let i = 0; i < cfg.count; i++) {
|
||||
const w = randInt(cfg.minW, cfg.maxW);
|
||||
const h = H * (cfg.minH + Math.random() * (cfg.maxH - cfg.minH));
|
||||
bldgs.push({ x, y: H * cfg.baseY - h, w, h });
|
||||
x += w + randInt(1, 8);
|
||||
}
|
||||
layers.push({ bldgs, alpha: cfg.alpha, drift: cfg.drift });
|
||||
}
|
||||
}
|
||||
|
||||
// Particles
|
||||
const particles = [];
|
||||
|
||||
function initParticles() {
|
||||
particles.length = 0;
|
||||
for (let i = 0; i < 60; i++) {
|
||||
particles.push({
|
||||
x: Math.random() * W,
|
||||
y: Math.random() * H,
|
||||
r: Math.random() * 1.5 + 0.3,
|
||||
vy: -(Math.random() * 0.4 + 0.1),
|
||||
vx: (Math.random() - 0.5) * 0.15,
|
||||
alpha: Math.random() * 0.35 + 0.05,
|
||||
function seedBlocks () {
|
||||
blocks = [];
|
||||
for (let r = 0; r < DEPTH; r++) {
|
||||
for (let l = 0; l < LANES; l++) {
|
||||
// carve a boulevard down the middle lanes
|
||||
if (l === Math.floor(LANES / 2)) continue;
|
||||
if (Math.random() < 0.16) continue;
|
||||
const gx = (l - (LANES - 1) / 2) * SPACING + (Math.random() - .5) * 34;
|
||||
blocks.push({
|
||||
x: gx,
|
||||
z: r * SPACING + Math.random() * 60,
|
||||
w: 46 + Math.random() * 60,
|
||||
d: 46 + Math.random() * 60,
|
||||
h: 34 + Math.pow(Math.random(), 2.1) * 330,
|
||||
lit: Math.random()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function resize () {
|
||||
W = canvas.width = window.innerWidth;
|
||||
H = canvas.height = window.innerHeight;
|
||||
generateCity();
|
||||
initParticles();
|
||||
W = cv.width = innerWidth;
|
||||
H = cv.height = innerHeight;
|
||||
FOCAL = W * 0.82;
|
||||
seedBlocks();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resize);
|
||||
addEventListener('resize', resize);
|
||||
resize();
|
||||
|
||||
// Sky gradient colours cycling slowly
|
||||
const SKY = [
|
||||
{ t: 0, r: 7, g: 7, b: 12 },
|
||||
{ t: 0.5, r: 9, g: 6, b: 18 },
|
||||
{ t: 1, r: 12, g: 8, b: 20 },
|
||||
];
|
||||
let skyT = 0;
|
||||
let skyDir = 0.0004;
|
||||
|
||||
function lerp(a, b, t) { return a + (b - a) * t; }
|
||||
|
||||
function getSky() {
|
||||
const t = (Math.sin(skyT * Math.PI) + 1) / 2;
|
||||
const s = SKY[0], e = SKY[2];
|
||||
return `rgb(${Math.round(lerp(s.r,e.r,t))},${Math.round(lerp(s.g,e.g,t))},${Math.round(lerp(s.b,e.b,t))})`;
|
||||
function project (x, y, z) {
|
||||
const dz = z - CAM.z;
|
||||
if (dz < 12) return null;
|
||||
const s = FOCAL / dz;
|
||||
return { sx: W / 2 + (x - CAM.x) * s, sy: H * 0.62 - (y - CAM.y) * s, s, dz };
|
||||
}
|
||||
|
||||
let lastTime = 0;
|
||||
function drawCity () {
|
||||
// horizon haze
|
||||
const hz = cx.createLinearGradient(0, H * 0.20, 0, H * 0.72);
|
||||
hz.addColorStop(0, '#151A24');
|
||||
hz.addColorStop(.55, '#1B212C');
|
||||
hz.addColorStop(1, '#0D1016');
|
||||
cx.fillStyle = hz;
|
||||
cx.fillRect(0, 0, W, H);
|
||||
|
||||
function drawFrame(ts) {
|
||||
const dt = Math.min((ts - lastTime) / 16.67, 3);
|
||||
lastTime = ts;
|
||||
// sun-scorched band on the horizon — the one warm note
|
||||
const bd = cx.createLinearGradient(0, H * 0.44, 0, H * 0.64);
|
||||
bd.addColorStop(0, 'rgba(194,70,46,0)');
|
||||
bd.addColorStop(.5,'rgba(194,70,46,.13)');
|
||||
bd.addColorStop(1, 'rgba(194,70,46,0)');
|
||||
cx.fillStyle = bd;
|
||||
cx.fillRect(0, 0, W, H);
|
||||
|
||||
skyT += skyDir * dt;
|
||||
if (skyT > 1 || skyT < 0) skyDir = -skyDir;
|
||||
// painter's algorithm: far blocks first
|
||||
blocks.sort((a, b) => (b.z - CAM.z) - (a.z - CAM.z));
|
||||
|
||||
panX += 0.08 * dt;
|
||||
for (const b of blocks) {
|
||||
const dz = b.z - CAM.z;
|
||||
if (dz < 14 || dz > FAR) continue;
|
||||
|
||||
// Sky
|
||||
const skyCol = getSky();
|
||||
const grad = ctx.createLinearGradient(0, 0, 0, H);
|
||||
grad.addColorStop(0, skyCol);
|
||||
grad.addColorStop(0.55, '#0a0814');
|
||||
grad.addColorStop(1, '#040408');
|
||||
ctx.fillStyle = grad;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
const hw = b.w / 2;
|
||||
const bl = project(b.x - hw, 0, b.z);
|
||||
const br = project(b.x + hw, 0, b.z);
|
||||
const tl = project(b.x - hw, b.h, b.z);
|
||||
if (!bl || !br || !tl) continue;
|
||||
|
||||
// Distant stars/glow
|
||||
ctx.save();
|
||||
for (let i = 0; i < 80; i++) {
|
||||
const sx = ((i * 137.5 + panX * 0.3) % (W * 1.4)) - W * 0.2;
|
||||
const sy = (i * 73.1) % (H * 0.55);
|
||||
const s = 0.5 + (i % 3) * 0.4;
|
||||
const a = 0.1 + (Math.sin(ts * 0.001 + i) + 1) * 0.08;
|
||||
ctx.globalAlpha = a;
|
||||
ctx.fillStyle = '#c8d4f0';
|
||||
ctx.beginPath();
|
||||
ctx.arc(sx, sy, s, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
ctx.restore();
|
||||
const fade = Math.max(0, 1 - dz / FAR);
|
||||
const w = br.sx - bl.sx;
|
||||
const h = bl.sy - tl.sy;
|
||||
if (w < 0.6 || h < 0.6) continue;
|
||||
|
||||
// Horizon glow
|
||||
const hGrad = ctx.createRadialGradient(W / 2, H * 0.75, 0, W / 2, H * 0.75, W * 0.6);
|
||||
hGrad.addColorStop(0, 'rgba(200,147,63,0.07)');
|
||||
hGrad.addColorStop(0.6, 'rgba(200,147,63,0.02)');
|
||||
hGrad.addColorStop(1, 'rgba(200,147,63,0)');
|
||||
ctx.fillStyle = hGrad;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
// face
|
||||
cx.globalAlpha = 0.30 + fade * 0.62;
|
||||
cx.fillStyle = '#080A0F';
|
||||
cx.fillRect(bl.sx, tl.sy, w, h);
|
||||
|
||||
// City layers
|
||||
for (const layer of layers) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = layer.alpha;
|
||||
ctx.fillStyle = '#0a0a14';
|
||||
const off = (panX * layer.drift) % W;
|
||||
for (const b of layer.bldgs) {
|
||||
const bx = ((b.x + off) % (W * 1.6)) - W * 0.3;
|
||||
ctx.fillRect(bx, b.y, b.w, b.h);
|
||||
// Windows
|
||||
ctx.fillStyle = 'rgba(200,200,160,0.12)';
|
||||
const cols = Math.max(1, Math.floor(b.w / 6));
|
||||
const rows = Math.max(1, Math.floor(b.h / 10));
|
||||
// roof edge catches the light
|
||||
cx.globalAlpha = 0.13 + fade * 0.30;
|
||||
cx.fillStyle = '#39424F';
|
||||
cx.fillRect(bl.sx, tl.sy, w, Math.max(0.7, h * 0.012));
|
||||
|
||||
// windows — only worth drawing when the block is near
|
||||
if (dz < FAR * 0.42 && w > 7) {
|
||||
const cols = Math.max(1, Math.floor(w / 7));
|
||||
const rows = Math.max(1, Math.floor(h / 11));
|
||||
cx.globalAlpha = fade * 0.5;
|
||||
for (let c = 0; c < cols; c++) {
|
||||
for (let r = 0; r < rows; r++) {
|
||||
if (Math.random() < 0.35) continue;
|
||||
ctx.fillRect(bx + 2 + c * 6, b.y + 3 + r * 10, 3, 4);
|
||||
const k = (c * 37 + r * 71 + Math.floor(b.x)) % 100;
|
||||
if (k / 100 > b.lit * 0.7) continue;
|
||||
cx.fillStyle = k % 9 === 0 ? '#C2462E' : '#C9C2AE';
|
||||
cx.fillRect(bl.sx + 2 + c * 7, tl.sy + 3 + r * 11, 2.4, 3.4);
|
||||
}
|
||||
}
|
||||
ctx.fillStyle = '#0a0a14';
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
cx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
// Ground fog
|
||||
const fogGrad = ctx.createLinearGradient(0, H * 0.75, 0, H);
|
||||
fogGrad.addColorStop(0, 'rgba(7,7,12,0)');
|
||||
fogGrad.addColorStop(1, 'rgba(7,7,12,0.95)');
|
||||
ctx.fillStyle = fogGrad;
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
let last = 0;
|
||||
function frame (t) {
|
||||
const dt = last ? Math.min((t - last) / 16.67, 3) : 1;
|
||||
last = t;
|
||||
|
||||
// Particles
|
||||
for (const p of particles) {
|
||||
p.x += p.vx * dt;
|
||||
p.y += p.vy * dt;
|
||||
if (p.y < -5) { p.y = H + 5; p.x = Math.random() * W; }
|
||||
CAM.z += CAM.speed * dt * 3.1;
|
||||
CAM.drift += 0.0022 * dt;
|
||||
CAM.x = Math.sin(CAM.drift) * 96;
|
||||
CAM.y = 210 + Math.sin(CAM.drift * 0.62) * 34;
|
||||
|
||||
ctx.save();
|
||||
ctx.globalAlpha = p.alpha;
|
||||
ctx.fillStyle = '#c8a060';
|
||||
ctx.beginPath();
|
||||
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
ctx.restore();
|
||||
// recycle blocks that fell behind the camera
|
||||
for (const b of blocks) if (b.z - CAM.z < 10) b.z += FAR;
|
||||
|
||||
drawCity();
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
|
||||
requestAnimationFrame(drawFrame);
|
||||
}
|
||||
/* ══ Instrument readouts ══════════════════════════ */
|
||||
let clockSec = 0;
|
||||
setInterval(() => {
|
||||
clockSec++;
|
||||
const hh = String(Math.floor(clockSec / 3600) % 24).padStart(2, '0');
|
||||
const mm = String(Math.floor(clockSec / 60) % 60).padStart(2, '0');
|
||||
const ss = String(clockSec % 60).padStart(2, '0');
|
||||
$('r-clock').textContent = `${hh}:${mm}:${ss}`;
|
||||
$('r-alt').textContent = Math.round(1420 + Math.sin(CAM.drift * 0.62) * 120);
|
||||
$('r-hdg').textContent = String(Math.round((274 + Math.sin(CAM.drift) * 26 + 360) % 360)).padStart(3, '0');
|
||||
$('r-rec').style.opacity = clockSec % 2 ? '1' : '.25';
|
||||
}, 1000);
|
||||
|
||||
requestAnimationFrame(drawFrame);
|
||||
|
||||
// ── Progress simulation ───────────────────────────────────────────────────────
|
||||
|
||||
const fill = document.getElementById('prog-fill');
|
||||
const pctEl = document.getElementById('prog-pct');
|
||||
const labelEl = document.getElementById('prog-label');
|
||||
const rules = document.getElementById('rules');
|
||||
|
||||
const LABELS = [
|
||||
[0, 'Connecting to server…'],
|
||||
[10, 'Loading world data…'],
|
||||
[25, 'Streaming assets…'],
|
||||
[45, 'Initialising resources…'],
|
||||
[65, 'Authenticating session…'],
|
||||
[80, 'Almost ready…'],
|
||||
[95, 'Loading complete'],
|
||||
/* ══ Processing progress ══════════════════════════ */
|
||||
const STAGES = [
|
||||
[0, 'ESTABLISHING UPLINK'],
|
||||
[12, 'RECEIVING COUNTY MAP DATA'],
|
||||
[30, 'STREAMING STRUCTURES'],
|
||||
[52, 'INDEXING RESIDENT RECORDS'],
|
||||
[72, 'VERIFYING CREDENTIALS'],
|
||||
[88, 'PREPARING INTAKE TERMINAL'],
|
||||
[99, 'CLEARED FOR ENTRY'],
|
||||
];
|
||||
|
||||
let progress = 0;
|
||||
let targetProg = 0;
|
||||
let ruleShown = false;
|
||||
let done = false;
|
||||
// ticks encode the real phase boundaries, not decoration
|
||||
const ticksEl = $('ticks');
|
||||
STAGES.slice(1).forEach(([at, label]) => {
|
||||
const d = document.createElement('div');
|
||||
d.className = 'proc-tick';
|
||||
d.style.left = at + '%';
|
||||
d.innerHTML = `<span>${label.split(' ')[0]}</span>`;
|
||||
ticksEl.appendChild(d);
|
||||
});
|
||||
|
||||
function setProgress(pct) {
|
||||
targetProg = Math.min(100, Math.max(progress, pct));
|
||||
}
|
||||
let shown = 0, target = 0, ordOn = false;
|
||||
|
||||
function animProgress() {
|
||||
if (progress < targetProg) {
|
||||
progress += Math.min((targetProg - progress) * 0.05 + 0.08, targetProg - progress);
|
||||
const p = Math.min(100, Math.round(progress));
|
||||
fill.style.width = p + '%';
|
||||
pctEl.textContent = p + '%';
|
||||
function setTarget (v) { target = Math.min(100, Math.max(target, v)); }
|
||||
|
||||
for (let i = LABELS.length - 1; i >= 0; i--) {
|
||||
if (progress >= LABELS[i][0]) {
|
||||
labelEl.textContent = LABELS[i][1];
|
||||
break;
|
||||
(function tick () {
|
||||
if (shown < target) {
|
||||
shown += Math.min((target - shown) * 0.07 + 0.09, target - shown);
|
||||
const p = Math.min(100, Math.round(shown));
|
||||
$('fill').style.width = p + '%';
|
||||
$('pct').textContent = p + '%';
|
||||
for (let i = STAGES.length - 1; i >= 0; i--) {
|
||||
if (shown >= STAGES[i][0]) { $('stage').textContent = STAGES[i][1]; break; }
|
||||
}
|
||||
if (shown >= 46 && !ordOn) { ordOn = true; $('ord').classList.add('on'); }
|
||||
}
|
||||
setTimeout(tick, 40);
|
||||
})();
|
||||
|
||||
if (progress >= 50 && !ruleShown) {
|
||||
ruleShown = true;
|
||||
rules.classList.add('visible');
|
||||
}
|
||||
}
|
||||
setTimeout(animProgress, 40);
|
||||
}
|
||||
[[700,13],[1500,29],[2700,49],[4400,67],[6400,80],[9000,91]]
|
||||
.forEach(([ms, v]) => setTimeout(() => setTarget(v), ms));
|
||||
|
||||
// Fake progress curve
|
||||
function fakeProgress() {
|
||||
const steps = [
|
||||
[800, 12], [1600, 28], [2800, 48],
|
||||
[4500, 68], [6500, 82], [9000, 91],
|
||||
];
|
||||
for (const [delay, pct] of steps) {
|
||||
setTimeout(() => setProgress(pct), delay);
|
||||
addEventListener('message', e => {
|
||||
const d = e.data;
|
||||
if (!d) return;
|
||||
if (d.eventName === 'loadingScreenCall') {
|
||||
for (const c of (d.data || [])) {
|
||||
if (c.functionName === 'shutdown' || c.type === 'shutdown') {
|
||||
setTarget(100);
|
||||
setTimeout(() => {
|
||||
document.body.style.transition = 'opacity .9s cubic-bezier(.2,.8,.25,1)';
|
||||
document.body.style.opacity = '0';
|
||||
}, 900);
|
||||
}
|
||||
}
|
||||
|
||||
fakeProgress();
|
||||
animProgress();
|
||||
|
||||
// Listen for real completion from Lua (via postMessage or window message)
|
||||
window.addEventListener('message', function(e) {
|
||||
if (!e.data) return;
|
||||
// Standard FiveM loadingscreen shutdown via eventName
|
||||
if (e.data.eventName === 'loadingScreenCall') {
|
||||
const calls = e.data.data || [];
|
||||
for (const call of calls) {
|
||||
if (call.functionName === 'shutdown' || call.type === 'shutdown') {
|
||||
setProgress(100);
|
||||
setTimeout(() => { document.body.style.opacity = '0'; }, 1000);
|
||||
}
|
||||
}
|
||||
// FiveM streams real load progress through these events
|
||||
if (d.eventName === 'loadProgress' && typeof d.loadFraction === 'number') {
|
||||
setTarget(Math.round(d.loadFraction * 100));
|
||||
}
|
||||
});
|
||||
|
||||
// ── Ambient sound ────────────────────────────────────────────────────────────
|
||||
|
||||
const volSlider = document.getElementById('vol');
|
||||
let audioCtx = null;
|
||||
let masterGain = null;
|
||||
/* ══ Room tone ════════════════════════════════════
|
||||
Dispatch-room HVAC hum, not a music pad. */
|
||||
const vol = $('vol');
|
||||
let actx = null, master = null;
|
||||
|
||||
function initAudio () {
|
||||
if (audioCtx) return;
|
||||
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
masterGain = audioCtx.createGain();
|
||||
masterGain.gain.value = volSlider.value / 100 * 0.7;
|
||||
masterGain.connect(audioCtx.destination);
|
||||
if (actx) return;
|
||||
try { actx = new (window.AudioContext || window.webkitAudioContext)(); }
|
||||
catch (e) { return; }
|
||||
|
||||
// Drone frequencies: dark minor chord atmosphere
|
||||
const freqs = [
|
||||
{ f: 55.0, v: 0.18 }, // A1
|
||||
{ f: 65.4, v: 0.10 }, // C2
|
||||
{ f: 82.4, v: 0.12 }, // E2
|
||||
{ f: 110.0, v: 0.07 }, // A2
|
||||
{ f: 130.8, v: 0.04 }, // C3
|
||||
];
|
||||
master = actx.createGain();
|
||||
master.gain.value = vol.value / 100 * 0.6;
|
||||
master.connect(actx.destination);
|
||||
|
||||
for (const { f, v } of freqs) {
|
||||
const osc = audioCtx.createOscillator();
|
||||
const gain = audioCtx.createGain();
|
||||
const filt = audioCtx.createBiquadFilter();
|
||||
// low room hum
|
||||
[[47.5,.20],[71.3,.10],[95,.055]].forEach(([f, g]) => {
|
||||
const o = actx.createOscillator(), gn = actx.createGain(), lp = actx.createBiquadFilter();
|
||||
const lfo = actx.createOscillator(), lg = actx.createGain();
|
||||
lfo.frequency.value = 0.037 + Math.random() * 0.06;
|
||||
lg.gain.value = f * 0.004;
|
||||
lfo.connect(lg); lg.connect(o.frequency);
|
||||
o.type = 'sine'; o.frequency.value = f;
|
||||
lp.type = 'lowpass'; lp.frequency.value = f * 4; lp.Q.value = .4;
|
||||
gn.gain.setValueAtTime(0, actx.currentTime);
|
||||
gn.gain.linearRampToValueAtTime(g, actx.currentTime + 5);
|
||||
o.connect(lp); lp.connect(gn); gn.connect(master);
|
||||
o.start(); lfo.start();
|
||||
});
|
||||
|
||||
// Slow LFO for movement
|
||||
const lfo = audioCtx.createOscillator();
|
||||
const lfoGain = audioCtx.createGain();
|
||||
lfo.frequency.value = 0.05 + Math.random() * 0.1;
|
||||
lfoGain.gain.value = f * 0.003;
|
||||
lfo.connect(lfoGain);
|
||||
lfoGain.connect(osc.frequency);
|
||||
|
||||
osc.type = 'sine';
|
||||
osc.frequency.value = f;
|
||||
filt.type = 'lowpass';
|
||||
filt.frequency.value = f * 3;
|
||||
filt.Q.value = 0.5;
|
||||
gain.gain.setValueAtTime(0, audioCtx.currentTime);
|
||||
gain.gain.linearRampToValueAtTime(v, audioCtx.currentTime + 4);
|
||||
|
||||
osc.connect(filt);
|
||||
filt.connect(gain);
|
||||
gain.connect(masterGain);
|
||||
osc.start();
|
||||
lfo.start();
|
||||
// filtered air noise
|
||||
const buf = actx.createBuffer(1, actx.sampleRate * 3, actx.sampleRate);
|
||||
const dat = buf.getChannelData(0);
|
||||
for (let i = 0; i < dat.length; i++) dat[i] = Math.random() * 2 - 1;
|
||||
const ns = actx.createBufferSource(), bp = actx.createBiquadFilter(), ng = actx.createGain();
|
||||
ns.buffer = buf; ns.loop = true;
|
||||
bp.type = 'bandpass'; bp.frequency.value = 320; bp.Q.value = .35;
|
||||
ng.gain.value = 0.02;
|
||||
ns.connect(bp); bp.connect(ng); ng.connect(master);
|
||||
ns.start();
|
||||
}
|
||||
|
||||
// Subtle reverb-like noise pad
|
||||
const buf = audioCtx.createBuffer(1, audioCtx.sampleRate * 2, audioCtx.sampleRate);
|
||||
const data = buf.getChannelData(0);
|
||||
for (let i = 0; i < data.length; i++) data[i] = (Math.random() * 2 - 1);
|
||||
const noise = audioCtx.createBufferSource();
|
||||
const nFilt = audioCtx.createBiquadFilter();
|
||||
const nGain = audioCtx.createGain();
|
||||
noise.buffer = buf;
|
||||
noise.loop = true;
|
||||
nFilt.type = 'bandpass';
|
||||
nFilt.frequency.value = 80;
|
||||
nFilt.Q.value = 0.2;
|
||||
nGain.gain.value = 0.018;
|
||||
noise.connect(nFilt);
|
||||
nFilt.connect(nGain);
|
||||
nGain.connect(masterGain);
|
||||
noise.start();
|
||||
}
|
||||
addEventListener('click', initAudio, { once: true });
|
||||
addEventListener('keydown', initAudio, { once: true });
|
||||
setTimeout(initAudio, 1100);
|
||||
|
||||
// Start audio on first interaction (browser policy)
|
||||
document.addEventListener('click', initAudio, { once: true });
|
||||
document.addEventListener('keydown', initAudio, { once: true });
|
||||
// Also try after a moment — FiveM NUI may not block it
|
||||
setTimeout(initAudio, 1200);
|
||||
|
||||
volSlider.addEventListener('input', () => {
|
||||
if (masterGain) {
|
||||
masterGain.gain.linearRampToValueAtTime(
|
||||
volSlider.value / 100 * 0.7,
|
||||
audioCtx.currentTime + 0.1
|
||||
);
|
||||
}
|
||||
vol.addEventListener('input', () => {
|
||||
if (master) master.gain.linearRampToValueAtTime(vol.value / 100 * 0.6, actx.currentTime + .08);
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
-- All DB operations go through the internal Python API on :8787
|
||||
|
||||
local API_BASE = "http://127.0.0.1:8787"
|
||||
-- Должен совпадать с JUSTRP_API_SECRET у Python-API. Задаётся в server.cfg:
|
||||
-- set justrp_api_key "ваш-ключ"
|
||||
local API_KEY = GetConvar("justrp_api_key", "change-me")
|
||||
local API_KEY = "JustRP_Internal_2026_Secret"
|
||||
|
||||
-- Per-player session state
|
||||
local Sessions = {}
|
||||
|
||||
Reference in New Issue
Block a user