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,567 @@
|
||||
/* ---------------------------------------------------------------------------
|
||||
Loading screen behaviour.
|
||||
|
||||
Three jobs:
|
||||
1. draw the city - a procedural night skyline flown over on canvas
|
||||
2. report progress - bound to the load events the game actually emits
|
||||
3. play ambience - synthesised in the browser, nothing is downloaded
|
||||
--------------------------------------------------------------------------- */
|
||||
|
||||
'use strict';
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
|
||||
/* =========================================================================
|
||||
1. The city
|
||||
========================================================================= */
|
||||
|
||||
/* Small deterministic PRNG so the skyline is stable across a resize. */
|
||||
function rng(seed) {
|
||||
let s = seed >>> 0;
|
||||
return () => {
|
||||
s = (s * 1664525 + 1013904223) >>> 0;
|
||||
return s / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
const SKY = {
|
||||
canvas: $('sky'),
|
||||
ctx: null,
|
||||
layers: [],
|
||||
w: 0,
|
||||
h: 0,
|
||||
dpr: 1,
|
||||
t0: performance.now(),
|
||||
};
|
||||
|
||||
/* Depth layers, far to near. Every layer is darker than the hazy sky behind
|
||||
it, which is what makes a silhouette read at all; the far ones are lifted
|
||||
towards the sky colour to stand in for atmosphere. `base` is the ground line
|
||||
as a fraction of the viewport, so near towers rise right through the frame.
|
||||
The difference in scroll rate is what reads as flight. */
|
||||
const LAYER_SPEC = [
|
||||
{ depth: 0.00, speed: 4, minW: 28, maxW: 66, minH: 0.09, maxH: 0.21, base: 0.76, tone: '#20263a', lit: 0.16, win: 0.30, haze: 0.17 },
|
||||
{ depth: 0.25, speed: 11, minW: 36, maxW: 90, minH: 0.13, maxH: 0.30, base: 0.83, tone: '#171c2b', lit: 0.20, win: 0.42, haze: 0.14 },
|
||||
{ depth: 0.50, speed: 24, minW: 50, maxW: 122, minH: 0.19, maxH: 0.42, base: 0.91, tone: '#10141f', lit: 0.24, win: 0.58, haze: 0.10 },
|
||||
{ depth: 0.75, speed: 50, minW: 74, maxW: 178, minH: 0.28, maxH: 0.62, base: 1.02, tone: '#090c14', lit: 0.26, win: 0.74, haze: 0.06 },
|
||||
{ depth: 1.00, speed: 100, minW: 104, maxW: 262, minH: 0.44, maxH: 0.98, base: 1.20, tone: '#04060b', lit: 0.19, win: 0.90, haze: 0.00 },
|
||||
];
|
||||
|
||||
function buildLayer(spec, index, w, h) {
|
||||
const cv = document.createElement('canvas');
|
||||
cv.width = Math.ceil(w * 2);
|
||||
cv.height = h;
|
||||
const c = cv.getContext('2d');
|
||||
const rand = rng(9173 + index * 7717);
|
||||
|
||||
const base = h * spec.base;
|
||||
const towers = [];
|
||||
|
||||
let x = -60;
|
||||
while (x < cv.width + 60) {
|
||||
const bw = spec.minW + rand() * (spec.maxW - spec.minW);
|
||||
const bh = h * (spec.minH + rand() * (spec.maxH - spec.minH));
|
||||
const top = base - bh;
|
||||
|
||||
c.fillStyle = spec.tone;
|
||||
c.fillRect(Math.round(x), Math.round(top), Math.ceil(bw), Math.ceil(h - top));
|
||||
|
||||
/* a hairline of sodium bounce along the roof edge */
|
||||
if (rand() < 0.55) {
|
||||
c.fillStyle = `rgba(255,159,69,${0.05 + spec.glow * 0.16})`;
|
||||
c.fillRect(Math.round(x), Math.round(top), Math.ceil(bw), 1);
|
||||
}
|
||||
|
||||
/* windows */
|
||||
const cell = 6 + spec.depth * 8;
|
||||
const pad = Math.max(2, cell * 0.34);
|
||||
for (let wy = top + pad * 2; wy < base - pad; wy += cell) {
|
||||
for (let wx = x + pad; wx < x + bw - pad; wx += cell) {
|
||||
if (rand() > spec.lit) continue;
|
||||
const warm = rand();
|
||||
/* mostly sodium/tungsten, a few cold offices; distant windows are
|
||||
dimmed so they read as depth rather than as static */
|
||||
const col = warm < 0.72
|
||||
? `rgba(255,${150 + Math.floor(rand() * 50)},${70 + Math.floor(rand() * 40)},${(0.35 + rand() * 0.5) * spec.win})`
|
||||
: `rgba(190,205,225,${(0.18 + rand() * 0.3) * spec.win})`;
|
||||
c.fillStyle = col;
|
||||
c.fillRect(Math.round(wx), Math.round(wy), Math.max(1, cell * 0.34), Math.max(1, cell * 0.42));
|
||||
}
|
||||
}
|
||||
|
||||
/* tall towers get an aviation light */
|
||||
if (bh > h * 0.5 && rand() < 0.5) {
|
||||
towers.push({ x: x + bw / 2, y: top - 2, phase: rand() * Math.PI * 2 });
|
||||
}
|
||||
|
||||
x += bw + 2 + rand() * 16;
|
||||
}
|
||||
|
||||
return { cv, spec, towers, base };
|
||||
}
|
||||
|
||||
function resizeSky() {
|
||||
const dpr = Math.min(window.devicePixelRatio || 1, 1.5);
|
||||
SKY.dpr = dpr;
|
||||
SKY.w = window.innerWidth;
|
||||
SKY.h = window.innerHeight;
|
||||
SKY.canvas.width = Math.floor(SKY.w * dpr);
|
||||
SKY.canvas.height = Math.floor(SKY.h * dpr);
|
||||
SKY.ctx = SKY.canvas.getContext('2d');
|
||||
SKY.ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
SKY.layers = LAYER_SPEC.map((s, i) => buildLayer(s, i, SKY.w, SKY.h));
|
||||
}
|
||||
|
||||
function drawSky(now) {
|
||||
const { ctx, w, h } = SKY;
|
||||
if (!ctx) return;
|
||||
const t = (now - SKY.t0) / 1000;
|
||||
|
||||
/* Sky: light pollution rather than stars. It has to stay brighter than the
|
||||
buildings all the way down, otherwise there is no silhouette to see. */
|
||||
const g = ctx.createLinearGradient(0, 0, 0, h);
|
||||
g.addColorStop(0.00, '#080b14');
|
||||
g.addColorStop(0.34, '#121627');
|
||||
g.addColorStop(0.60, '#23202f');
|
||||
g.addColorStop(0.78, '#412c27');
|
||||
g.addColorStop(0.90, '#6b4527');
|
||||
g.addColorStop(1.00, '#8a5726');
|
||||
ctx.fillStyle = g;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
/* the haze dome over downtown */
|
||||
const halo = ctx.createRadialGradient(w * 0.6, h * 0.9, 0, w * 0.6, h * 0.9, h * 0.85);
|
||||
halo.addColorStop(0, 'rgba(255,159,69,0.34)');
|
||||
halo.addColorStop(0.45, 'rgba(255,140,60,0.12)');
|
||||
halo.addColorStop(1, 'rgba(255,140,60,0)');
|
||||
ctx.fillStyle = halo;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
|
||||
/* a slow descent: everything drifts down and grows a touch over time */
|
||||
const descend = Math.sin(t * 0.06) * 10 + t * 0.9;
|
||||
|
||||
for (const layer of SKY.layers) {
|
||||
const { cv, spec } = layer;
|
||||
const span = cv.width / 2;
|
||||
let ox = -((t * spec.speed) % span);
|
||||
const oy = descend * (0.25 + spec.depth * 0.9);
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.drawImage(cv, ox, oy, cv.width, h);
|
||||
|
||||
/* aviation lights blink on their own phase */
|
||||
for (const tw of layer.towers) {
|
||||
const blink = Math.sin(t * 1.5 + tw.phase);
|
||||
if (blink < 0.72) continue;
|
||||
const px = tw.x + ox;
|
||||
const py = tw.y + oy - h * 0.06;
|
||||
ctx.fillStyle = `rgba(226,72,52,${(blink - 0.72) / 0.28})`;
|
||||
ctx.beginPath();
|
||||
ctx.arc(px, py, 1.6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
if (px > span) {
|
||||
ctx.beginPath();
|
||||
ctx.arc(px - span, py, 1.6, 0, Math.PI * 2);
|
||||
ctx.fill();
|
||||
}
|
||||
}
|
||||
|
||||
/* Aerial perspective: a warm veil laid over everything drawn so far, so
|
||||
each nearer layer sits in front of progressively more atmosphere. */
|
||||
if (spec.haze > 0) {
|
||||
ctx.fillStyle = `rgba(104,72,50,${spec.haze})`;
|
||||
ctx.fillRect(0, 0, w, h);
|
||||
}
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
2. Progress, bound to the game's own load events
|
||||
========================================================================= */
|
||||
|
||||
const Progress = {
|
||||
shown: 0,
|
||||
target: 0,
|
||||
step: 'Waiting for the game',
|
||||
|
||||
set(fraction, step) {
|
||||
if (typeof fraction === 'number' && isFinite(fraction)) {
|
||||
/* never let it walk backwards - it reads as a fault */
|
||||
this.target = Math.max(this.target, Math.min(1, Math.max(0, fraction)));
|
||||
}
|
||||
if (step) this.step = step;
|
||||
},
|
||||
|
||||
tick() {
|
||||
/* ease towards the real figure so long steps still feel alive */
|
||||
this.shown += (this.target - this.shown) * 0.08;
|
||||
const pct = Math.min(100, Math.round(this.shown * 100));
|
||||
$('bar').style.width = (this.shown * 100).toFixed(2) + '%';
|
||||
$('pct').firstChild.nodeValue = String(pct);
|
||||
if ($('step').textContent !== this.step) $('step').textContent = this.step;
|
||||
},
|
||||
};
|
||||
|
||||
/* Turns the game's internal init-function names into something a player can read. */
|
||||
const STEP_NAMES = {
|
||||
MAP: 'Loading the map',
|
||||
BEFORE_MAP_LOADED: 'Preparing the world',
|
||||
AFTER_MAP_LOADED: 'Placing the world',
|
||||
SESSION_INIT: 'Joining the session',
|
||||
INIT_BEFORE_MAP_LOADED: 'Starting up',
|
||||
INIT_AFTER_MAP_LOADED: 'Finishing up',
|
||||
INIT_SESSION: 'Joining the session',
|
||||
};
|
||||
|
||||
window.addEventListener('message', (event) => {
|
||||
const d = event.data || {};
|
||||
switch (d.eventName) {
|
||||
case 'loadProgress':
|
||||
Progress.set(d.loadFraction);
|
||||
break;
|
||||
|
||||
case 'startInitFunction':
|
||||
Progress.set(null, STEP_NAMES[d.type] || 'Starting up');
|
||||
break;
|
||||
|
||||
case 'initFunctionInvoking':
|
||||
if (typeof d.idx === 'number' && typeof d.count === 'number' && d.count > 0) {
|
||||
Progress.set(null, `${STEP_NAMES[d.type] || 'Starting up'} ${d.idx}/${d.count}`);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'startDataFileEntries':
|
||||
Progress.set(null, `Streaming ${d.count} asset packs`);
|
||||
break;
|
||||
|
||||
case 'performMapLoadFunction':
|
||||
Progress.set(null, 'Building the map');
|
||||
break;
|
||||
|
||||
case 'startWarning':
|
||||
case 'onLogLine':
|
||||
if (d.message) Progress.set(null, String(d.message).slice(0, 70));
|
||||
break;
|
||||
|
||||
/* --- our own messages, sent from rp_ui once the client is running --- */
|
||||
case 'rp:rules':
|
||||
if (Array.isArray(d.rules) && d.rules.length) {
|
||||
Notices.load(d.rules);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'rp:server':
|
||||
if (d.name) document.querySelector('.wordmark').textContent = d.name;
|
||||
if (d.serial) $('serial').textContent = d.serial;
|
||||
if (d.status) $('hostcount').textContent = d.status;
|
||||
break;
|
||||
|
||||
case 'rp:progress':
|
||||
Progress.set(d.fraction, d.step);
|
||||
break;
|
||||
|
||||
/* the world is ready: dissolve into the live camera behind us */
|
||||
case 'rp:handover':
|
||||
Progress.set(1, 'Ready');
|
||||
document.body.classList.add('handover');
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
/* =========================================================================
|
||||
3. Posted notices
|
||||
========================================================================= */
|
||||
|
||||
const Notices = {
|
||||
items: [
|
||||
{ title: 'Stay in character', body: 'Your character does not know what you know. Breaking character in the world is the fastest way to lose the story.' },
|
||||
{ title: 'Value your life', body: 'Act like the consequences are permanent. A gun in your face changes what you are willing to do.' },
|
||||
{ title: 'No random deathmatch', body: 'Violence needs a reason the other person can understand. Escalate, do not detonate.' },
|
||||
{ title: 'Do not power game', body: 'Give people a fair chance to react. Winning is not the point of a scene.' },
|
||||
{ title: 'Leave the scene alive', body: 'If you die, your character forgets the thirty minutes before it. No revenge from the grave.' },
|
||||
],
|
||||
i: 0,
|
||||
timer: null,
|
||||
|
||||
load(items) {
|
||||
this.items = items;
|
||||
this.i = 0;
|
||||
this.render();
|
||||
this.schedule();
|
||||
},
|
||||
|
||||
render() {
|
||||
const item = this.items[this.i % this.items.length];
|
||||
const title = $('noticeTitle');
|
||||
const body = $('noticeBody');
|
||||
|
||||
/* restart the entry animation by reflowing the nodes */
|
||||
title.style.animation = 'none';
|
||||
body.style.animation = 'none';
|
||||
void title.offsetWidth;
|
||||
title.style.animation = '';
|
||||
body.style.animation = '';
|
||||
|
||||
title.textContent = item.title;
|
||||
body.textContent = item.body;
|
||||
$('noticeIndex').textContent = `${(this.i % this.items.length) + 1} / ${this.items.length}`;
|
||||
},
|
||||
|
||||
schedule() {
|
||||
clearInterval(this.timer);
|
||||
this.timer = setInterval(() => {
|
||||
this.i += 1;
|
||||
this.render();
|
||||
}, 7600);
|
||||
},
|
||||
};
|
||||
|
||||
/* =========================================================================
|
||||
4. Ambience - synthesised, never downloaded
|
||||
========================================================================= */
|
||||
|
||||
class Ambience {
|
||||
constructor() {
|
||||
this.ctx = null;
|
||||
this.master = null;
|
||||
this.volume = 0.45;
|
||||
this.muted = false;
|
||||
this.started = false;
|
||||
}
|
||||
|
||||
/* A short noise burst with exponential decay makes a serviceable hall. */
|
||||
makeImpulse(seconds, decay) {
|
||||
const rate = this.ctx.sampleRate;
|
||||
const len = Math.floor(rate * seconds);
|
||||
const buf = this.ctx.createBuffer(2, len, rate);
|
||||
for (let ch = 0; ch < 2; ch++) {
|
||||
const data = buf.getChannelData(ch);
|
||||
for (let i = 0; i < len; i++) {
|
||||
data[i] = (Math.random() * 2 - 1) * Math.pow(1 - i / len, decay);
|
||||
}
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
noiseBuffer(seconds) {
|
||||
const rate = this.ctx.sampleRate;
|
||||
const buf = this.ctx.createBuffer(1, rate * seconds, rate);
|
||||
const d = buf.getChannelData(0);
|
||||
let last = 0;
|
||||
for (let i = 0; i < d.length; i++) {
|
||||
/* brown-ish noise: closer to distant traffic than white hiss */
|
||||
const white = Math.random() * 2 - 1;
|
||||
last = (last + 0.02 * white) / 1.02;
|
||||
d[i] = last * 3.2;
|
||||
}
|
||||
return buf;
|
||||
}
|
||||
|
||||
start() {
|
||||
if (this.started) return;
|
||||
const Ctor = window.AudioContext || window.webkitAudioContext;
|
||||
if (!Ctor) return;
|
||||
this.ctx = new Ctor();
|
||||
this.started = true;
|
||||
|
||||
const ctx = this.ctx;
|
||||
this.master = ctx.createGain();
|
||||
this.master.gain.value = 0;
|
||||
this.master.connect(ctx.destination);
|
||||
|
||||
const verb = ctx.createConvolver();
|
||||
verb.buffer = this.makeImpulse(4.2, 2.6);
|
||||
const verbGain = ctx.createGain();
|
||||
verbGain.gain.value = 0.55;
|
||||
verb.connect(verbGain).connect(this.master);
|
||||
|
||||
const dry = ctx.createGain();
|
||||
dry.gain.value = 0.75;
|
||||
dry.connect(this.master);
|
||||
|
||||
const bus = ctx.createGain();
|
||||
bus.connect(dry);
|
||||
bus.connect(verb);
|
||||
|
||||
/* --- the pad: a minor triad that never quite resolves --------------- */
|
||||
const lp = ctx.createBiquadFilter();
|
||||
lp.type = 'lowpass';
|
||||
lp.frequency.value = 420;
|
||||
lp.Q.value = 0.7;
|
||||
lp.connect(bus);
|
||||
|
||||
const lfo = ctx.createOscillator();
|
||||
lfo.frequency.value = 0.031;
|
||||
const lfoAmt = ctx.createGain();
|
||||
lfoAmt.gain.value = 210;
|
||||
lfo.connect(lfoAmt).connect(lp.frequency);
|
||||
lfo.start();
|
||||
|
||||
[55, 82.41, 110, 130.81, 164.81].forEach((freq, i) => {
|
||||
const osc = ctx.createOscillator();
|
||||
osc.type = i % 2 ? 'sine' : 'triangle';
|
||||
osc.frequency.value = freq;
|
||||
osc.detune.value = (i - 2) * 4;
|
||||
|
||||
const g = ctx.createGain();
|
||||
g.gain.value = 0.16 / (1 + i * 0.35);
|
||||
|
||||
/* each voice breathes on its own slow cycle */
|
||||
const breath = ctx.createOscillator();
|
||||
breath.frequency.value = 0.017 + i * 0.009;
|
||||
const breathAmt = ctx.createGain();
|
||||
breathAmt.gain.value = g.gain.value * 0.6;
|
||||
breath.connect(breathAmt).connect(g.gain);
|
||||
breath.start();
|
||||
|
||||
osc.connect(g).connect(lp);
|
||||
osc.start();
|
||||
});
|
||||
|
||||
/* --- distant traffic ------------------------------------------------ */
|
||||
const noise = ctx.createBufferSource();
|
||||
noise.buffer = this.noiseBuffer(8);
|
||||
noise.loop = true;
|
||||
const nf = ctx.createBiquadFilter();
|
||||
nf.type = 'bandpass';
|
||||
nf.frequency.value = 240;
|
||||
nf.Q.value = 0.55;
|
||||
const ng = ctx.createGain();
|
||||
ng.gain.value = 0.09;
|
||||
noise.connect(nf).connect(ng).connect(bus);
|
||||
noise.start();
|
||||
|
||||
const wind = ctx.createOscillator();
|
||||
wind.frequency.value = 0.043;
|
||||
const windAmt = ctx.createGain();
|
||||
windAmt.gain.value = 130;
|
||||
wind.connect(windAmt).connect(nf.frequency);
|
||||
wind.start();
|
||||
|
||||
/* --- a siren, far away, every now and then -------------------------- */
|
||||
const siren = () => {
|
||||
if (!this.ctx || this.ctx.state === 'closed') return;
|
||||
const t = ctx.currentTime;
|
||||
const o = ctx.createOscillator();
|
||||
o.type = 'triangle';
|
||||
const g = ctx.createGain();
|
||||
const pan = ctx.createStereoPanner ? ctx.createStereoPanner() : null;
|
||||
g.gain.value = 0;
|
||||
o.frequency.setValueAtTime(620, t);
|
||||
o.frequency.linearRampToValueAtTime(880, t + 0.7);
|
||||
o.frequency.linearRampToValueAtTime(620, t + 1.4);
|
||||
g.gain.linearRampToValueAtTime(0.014, t + 0.6);
|
||||
g.gain.linearRampToValueAtTime(0.0, t + 2.6);
|
||||
if (pan) {
|
||||
pan.pan.value = Math.random() * 1.6 - 0.8;
|
||||
o.connect(g).connect(pan).connect(verb);
|
||||
} else {
|
||||
o.connect(g).connect(verb);
|
||||
}
|
||||
o.start(t);
|
||||
o.stop(t + 2.8);
|
||||
setTimeout(siren, 24000 + Math.random() * 40000);
|
||||
};
|
||||
setTimeout(siren, 12000 + Math.random() * 12000);
|
||||
|
||||
this.applyVolume(1.8);
|
||||
}
|
||||
|
||||
/* perceptual curve: a linear fader sounds wrong */
|
||||
applyVolume(rampSeconds) {
|
||||
if (!this.master) return;
|
||||
const target = this.muted ? 0 : Math.pow(this.volume, 2.2) * 0.9;
|
||||
const now = this.ctx.currentTime;
|
||||
this.master.gain.cancelScheduledValues(now);
|
||||
this.master.gain.setValueAtTime(this.master.gain.value, now);
|
||||
this.master.gain.linearRampToValueAtTime(target, now + (rampSeconds || 0.12));
|
||||
}
|
||||
|
||||
setVolume(v) {
|
||||
this.volume = Math.min(1, Math.max(0, v));
|
||||
if (this.volume > 0) this.muted = false;
|
||||
this.applyVolume();
|
||||
}
|
||||
|
||||
toggleMute() {
|
||||
this.muted = !this.muted;
|
||||
this.applyVolume();
|
||||
return this.muted;
|
||||
}
|
||||
}
|
||||
|
||||
const ambience = new Ambience();
|
||||
|
||||
function wireAudio() {
|
||||
const vol = $('vol');
|
||||
const volval = $('volval');
|
||||
const mute = $('mute');
|
||||
const hint = $('audiohint');
|
||||
|
||||
const saved = parseInt(window.localStorage.getItem('rp.volume') || '45', 10);
|
||||
vol.value = String(isFinite(saved) ? saved : 45);
|
||||
volval.textContent = vol.value;
|
||||
ambience.volume = Number(vol.value) / 100;
|
||||
|
||||
const tryStart = () => {
|
||||
ambience.start();
|
||||
if (ambience.ctx && ambience.ctx.state === 'suspended') {
|
||||
hint.hidden = false;
|
||||
return false;
|
||||
}
|
||||
hint.hidden = true;
|
||||
return true;
|
||||
};
|
||||
|
||||
/* Autoplay may be blocked; the first input of any kind releases it. */
|
||||
if (!tryStart()) {
|
||||
const release = () => {
|
||||
if (ambience.ctx) ambience.ctx.resume();
|
||||
hint.hidden = true;
|
||||
window.removeEventListener('pointerdown', release);
|
||||
window.removeEventListener('keydown', release);
|
||||
};
|
||||
window.addEventListener('pointerdown', release);
|
||||
window.addEventListener('keydown', release);
|
||||
}
|
||||
|
||||
vol.addEventListener('input', () => {
|
||||
volval.textContent = vol.value;
|
||||
ambience.setVolume(Number(vol.value) / 100);
|
||||
mute.classList.toggle('off', Number(vol.value) === 0);
|
||||
window.localStorage.setItem('rp.volume', vol.value);
|
||||
});
|
||||
|
||||
mute.addEventListener('click', () => {
|
||||
const muted = ambience.toggleMute();
|
||||
mute.classList.toggle('off', muted);
|
||||
});
|
||||
}
|
||||
|
||||
/* =========================================================================
|
||||
boot
|
||||
========================================================================= */
|
||||
|
||||
function serial() {
|
||||
const n = Math.floor(Math.random() * 9000 + 1000);
|
||||
const m = Math.floor(Math.random() * 900 + 100);
|
||||
return `${new Date().getFullYear()}-${n}${m}`;
|
||||
}
|
||||
|
||||
function frame(now) {
|
||||
drawSky(now);
|
||||
Progress.tick();
|
||||
requestAnimationFrame(frame);
|
||||
}
|
||||
|
||||
window.addEventListener('resize', resizeSky);
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
$('serial').textContent = serial();
|
||||
resizeSky();
|
||||
Notices.render();
|
||||
Notices.schedule();
|
||||
wireAudio();
|
||||
requestAnimationFrame(frame);
|
||||
});
|
||||
Reference in New Issue
Block a user