SOURCE / PINNED RELEASE
Made of little things.
Supersonic RC Revive
- Release
- bb8b570f3600…
- Author-recorded commit
- 073e2d2c0e63…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.shakespeare.diy%2F/SuperSonicRCRevive
Archive hash verified: 857599401cc78951…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Browser host: title → Free Driving, with 1 s fades and a fixed 40 fps clock.
import { PerspectiveCamera, Vector3, WebGLRenderer } from 'three';
import { loadLevelData } from './assets.js';
import { SOUND_NAMES, SoundMixer } from './audio.js';
import { LocalMultiplayer, PLAYER_COLORS, splitViewports } from './localMultiplayer.js';
import { Input, type DirectorKey, type MenuIntent } from './input.js';
import type { LevelData } from './level/data.js';
import { CarSound } from './lingo/carSound.js';
import { DrivingManager, ORIGINAL_FRAME_MS } from './lingo/drivingManager.js';
import { createMode, type GameType, type ModeName } from './lingo/gameplay.js';
import { initPhysics } from './physics/havok.js';
import { WorldView } from './render/world.js';
import { Screens } from './ui/screens.js';
import { remoteColor, type OnlineSession } from './onlineSession.js';
/** The original stage is 640x480; its camera fieldOfView is vertical. */
const STAGE_ASPECT = 4 / 3;
/** Director's default camera projection angle (the arrow's own camera). */
const DEFAULT_FOV = 30;
interface Fade {
from: number;
to: number;
start: number;
done: () => void;
}
interface Race {
name: ModeName;
dm: DrivingManager;
mode: GameType;
sound: CarSound | null;
driving: boolean;
}
export class App {
private readonly renderer: WebGLRenderer;
private readonly camera = new PerspectiveCamera(70, STAGE_ASPECT, 2, 60000);
private readonly arrowCamera = new PerspectiveCamera(DEFAULT_FOV, STAGE_ASPECT, 1, 1000);
private readonly input = new Input();
private readonly mixer: SoundMixer | null;
private readonly screens: Screens;
private level: LevelData | null = null;
private view: WorldView | null = null;
private race: Race | null = null;
private local: LocalMultiplayer | null = null;
private localMenuHeld = false;
private fade: Fade | null = null;
/** "the milliSeconds" */
private ms = 0;
private lastTime = 0;
private accumulator = 0;
onStatus: (text: string) => void = () => {};
onPlayingChange: (playing: boolean) => void = () => {};
onOnlineLeave: () => void = () => {};
onAssetsReload: () => void = () => {};
online: OnlineSession | null = null;
startOnline(session: OnlineSession): void {
this.stopSession();
this.online = session;
this.fade = null;
this.gameStart('free_driving');
}
/** Ending transport lets a disconnected player continue driving offline. */
leaveOnline(): void {
const session = this.online;
this.online = null;
if (session) {
void session.close();
this.view?.clearPlayers();
delete this.renderer.domElement.dataset.localPosition;
delete this.renderer.domElement.dataset.remotePositions;
delete this.renderer.domElement.dataset.onlinePlayers;
delete this.renderer.domElement.dataset.remoteLabels;
this.onOnlineLeave();
}
}
/**
* Offer "Asset pack…" on the title screen (set when the host can download or
* side-load a pack). The promise settles when the switch is over; the button
* stays disabled until then.
*/
packSwitch: (() => Promise<void>) | null = null;
private idleWaiters: Array<() => void> = [];
/**
* Controller menu presses go here first (the pack panel, while it is open);
* it returns true when it took the press, so the screens behind it do not.
*/
menuOverride: ((intent: MenuIntent) => boolean) | null = null;
/**
* Observe controller button press edges anywhere for secret-code entry.
* Returning true consumes the completing press before navigation.
*/
menuIntentFilter: ((intent: MenuIntent) => boolean) | null = null;
private padHint = false;
constructor(private readonly host: HTMLElement) {
this.renderer = new WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
this.renderer.autoClear = false;
host.append(this.renderer.domElement);
this.camera.up.set(0, 0, 1);
this.mixer = typeof AudioContext !== 'undefined' ? new SoundMixer() : null;
this.screens = new Screens(host, { click: () => this.buttonClick() }, this.input);
new ResizeObserver(() => this.resize()).observe(host);
this.resize();
this.input.attach(window);
const resumeAudio = () => this.mixer?.resume();
window.addEventListener('pointerdown', resumeAudio);
window.addEventListener('keydown', resumeAudio);
document.addEventListener('visibilitychange', () => {
if (document.hidden) this.input.clear();
this.lastTime = performance.now();
});
}
/** Receive a discrete control action dispatched by the shell through NAP-KEYS. */
triggerKeyAction(key: DirectorKey): void {
this.input.pulseAction(key);
}
async start(): Promise<void> {
this.level = (await loadLevelData()) ?? null;
if (!this.level) throw new Error('Game data is not bundled in this build.');
this.onStatus('Starting physics…');
await initPhysics();
this.onStatus('Building the track…');
this.view = await WorldView.create();
this.onStatus('Loading sounds…');
await this.mixer?.load(SOUND_NAMES);
this.onStatus('');
this.titleStart();
this.lastTime = performance.now();
requestAnimationFrame((t) => this.frame(t));
}
/**
* Rebuild the track, props and sounds from the asset pack now installed, then
* return to the title screen, stopping any active driving session. If the pack
* cannot be built this throws and keeps the previous scene, so the caller can
* reinstall the old pack.
*/
async reloadAssets(options: { preserveOnlineJoin?: boolean } = {}): Promise<void> {
if (!options.preserveOnlineJoin) this.onAssetsReload();
this.stopSession();
this.fade = null;
this.screens.setFade(0);
this.screens.showLoading();
try {
this.onStatus('Building the track…');
const level = await loadLevelData();
if (!level) throw new Error('That asset pack has no track data.');
const view = await WorldView.create();
this.onStatus('Loading sounds…');
try {
await this.mixer?.load(SOUND_NAMES);
} catch (error) {
view.dispose();
throw error;
}
this.view?.dispose();
this.level = level;
this.view = view;
} finally {
this.onStatus('');
}
this.titleStart();
}
/** Whether a driving session is running. */
get racing(): boolean {
return this.race !== null || this.local !== null;
}
/** Resolves once no race is running. */
whenIdle(): Promise<void> {
return this.racing ? new Promise((resolve) => this.idleWaiters.push(resolve)) : Promise.resolve();
}
/** Back to the title screen without changing assets (after a failed switch). */
showTitle(): void {
if (!this.racing) this.titleStart();
}
// --- screen flow ---------------------------------------------------------------
private buttonClick(): void {
this.mixer?.resume();
this.mixer?.sound(1).play({ member: 'buttonclick' });
}
private fadeIn(done: () => void = () => {}): void {
this.fade = { from: 1, to: 0, start: this.ms, done };
}
private fadeOut(done: () => void): void {
this.fade = { from: 0, to: 1, start: this.ms, done };
}
private titleStart(): void {
this.onPlayingChange(false);
this.fadeIn();
this.screens.showTitle(() => this.fadeOut(() => this.gameStart('free_driving')), this.packSwitch);
}
/** gameTypeManager.start → realStart → realStart2. */
private gameStart(name: ModeName): void {
this.onPlayingChange(true);
const level = this.level!;
this.screens.showLoading();
this.fadeIn(() => {
const dm = new DrivingManager(level, this.input);
if (this.online) {
const offset = new Vector3(110 * this.online.lane, 0, 0).applyQuaternion(dm.car.rb.quaternion);
dm.car.rb.position = dm.car.rb.position.add(offset);
dm.camera.worldPosition.add(offset);
dm.camera.update();
}
const mode = createMode(name, {
dm,
input: this.input,
markers: level.markers,
mixer: this.mixer,
ms: () => this.ms,
exit: () => this.raceEnd(),
});
const sound = this.mixer ? new CarSound(dm.car, this.input, this.mixer) : null;
dm.car.sound = sound;
mode.realStart();
this.view!.setMode(mode);
this.race = { name, dm, mode, sound, driving: false };
// The physics already runs behind the loading screen during this fade.
this.fadeOut(() => {
this.fade = null;
this.screens.setFade(0);
this.screens.showRace();
dm.camera.fadeIn();
mode.realStart2();
this.race!.driving = true;
});
});
}
private localStart(): void {
this.input.clear();
this.local = new LocalMultiplayer(this.level!, this.mixer);
this.view!.setMode(null);
this.screens.showLocal(() => {
if (this.local) this.local.paused = false;
this.input.clear();
}, () => this.localEnd());
this.fadeIn();
}
private localEnd(): void {
this.local?.destroy();
this.local = null;
this.view?.clearPlayers();
this.screens.hideLocal();
this.input.clear();
this.localMenuHeld = false;
this.titleStart();
for (const resolve of this.idleWaiters.splice(0)) resolve();
}
private raceEnd(): void {
const race = this.race;
if (!race) return;
this.leaveOnline();
this.race = null;
race.sound?.destroy();
race.dm.destroy();
this.view?.setMode(null);
this.screens.hideRace();
this.input.clear();
this.titleStart();
for (const resolve of this.idleWaiters.splice(0)) resolve();
}
/** Release every driver before rebuilding assets; reconnect pads on the next start. */
private stopSession(): void {
this.leaveOnline();
this.race?.sound?.destroy();
this.race?.dm.destroy();
this.race = null;
this.local?.destroy();
this.local = null;
this.view?.clearPlayers();
this.view?.setMode(null);
this.screens.hideRace();
this.screens.hideLocal();
this.input.clear();
this.localMenuHeld = false;
for (const resolve of this.idleWaiters.splice(0)) resolve();
}
// --- frame loop ------------------------------------------------------------------
private resize(): void {
const w = Math.max(1, this.host.clientWidth);
const h = Math.max(1, this.host.clientHeight);
this.renderer.setSize(w, h, false);
this.camera.aspect = this.arrowCamera.aspect = w / h;
this.applyFov(this.camera, this.race?.dm.camera.fieldOfView ?? 70);
this.applyFov(this.arrowCamera, DEFAULT_FOV);
}
/** Keep the original's 4:3 framing: vertical FOV in landscape, its horizontal FOV in portrait. */
private applyFov(camera: PerspectiveCamera, fovV: number): void {
const aspect = camera.aspect;
if (aspect >= STAGE_ASPECT) {
camera.fov = fovV;
} else {
const halfH = Math.atan(Math.tan((fovV * Math.PI) / 360) * STAGE_ASPECT);
camera.fov = (2 * Math.atan(Math.tan(halfH) / aspect) * 180) / Math.PI;
}
camera.updateProjectionMatrix();
}
/** One Director frame: actorList stepFrame in the original's order. */
private directorFrame(): void {
this.ms += ORIGINAL_FRAME_MS;
this.local?.step(ORIGINAL_FRAME_MS);
const race = this.race;
if (race) {
race.dm.stepFrame(ORIGINAL_FRAME_MS);
if (this.race === race) race.mode.stepFrame();
}
if (this.fade) {
const f = this.fade;
const t = Math.min(1, (this.ms - f.start) / 1000);
this.screens.setFade(f.from + (f.to - f.from) * t);
if (t >= 1) {
this.fade = null;
f.done();
}
}
}
/** Controllers: race keys for the Director frames below, or menu navigation. */
private pollGamepad(now: number): void {
const { intents, waiting, frame } = this.input.pollGamepad(this.race?.driving === true, now, this.local !== null, this.online !== null);
// Observe real button edges in every state, before any menu navigation.
let codeCompleted = false;
if (frame?.status === 'ready') {
for (const controller of frame.players) {
if (!controller.mapped) continue;
for (const [action, intent] of [['up', 'up'], ['down', 'down'], ['left', 'left'], ['right', 'right'], ['handbrake', 'back'], ['accelerate', 'select']] as const) {
if (controller.actions[action]?.pressed && this.menuIntentFilter?.(intent)) codeCompleted = true;
}
}
}
if (codeCompleted) return;
// Free Driving is also the entry point for plug-and-play split screen.
// Leave keyboard/touch driving intact until a second supported pad appears.
if (!this.online && frame?.status === 'ready' && this.race?.driving && this.race.name === 'free_driving'
&& frame.players.filter((c) => c.mapped).length >= 2) {
const race = this.race;
const position = race.dm.car.rb.position, quaternion = race.dm.car.rb.quaternion;
race.sound?.destroy();
race.dm.destroy();
this.race = null;
this.screens.hideRace();
this.localStart();
this.local!.updateControllers(frame);
// Keep P1 where they were driving instead of restarting at the level start.
const first = this.local!.players[0].driver;
const offset = position.clone().sub(first.car.rb.position);
first.car.rb.position = position;
first.car.rb.quaternion = quaternion;
first.camera.worldPosition.add(offset);
first.camera.update();
console.log('[multiplayer] Free Driving switched to split screen', { players: this.local!.players.length });
return;
}
if (this.local) {
const wasPaused = this.local.paused;
if (frame) this.local.updateControllers(frame);
if (frame && wasPaused && this.local.paused) {
for (const controller of frame.players) {
for (const [action, intent] of [['up', 'up'], ['down', 'down'], ['left', 'left'], ['right', 'right'], ['accelerate', 'select']] as const) {
if (controller.mapped && controller.actions[action]?.pressed) this.screens.navigateLocal(intent);
if (!this.local) return;
}
}
}
const menuDown = this.input.keyPressed(53);
if (menuDown && !this.localMenuHeld) this.local.paused = !this.local.paused;
this.localMenuHeld = menuDown;
if (this.local.players.length) this.mixer?.resume();
return;
}
if (waiting !== this.padHint) {
this.padHint = waiting;
this.onStatus(waiting ? 'Click the game to use your controller' : '');
}
for (const intent of intents) {
if (this.menuOverride?.(intent)) continue;
// Like the fade overlay that blocks the pointer: no presses mid-fade.
if (!this.race && !this.fade) this.screens.navigate(intent);
}
}
private frame(now: number): void {
this.pollGamepad(now);
this.accumulator += Math.min(now - this.lastTime, 250);
this.lastTime = now;
while (this.accumulator >= ORIGINAL_FRAME_MS) {
this.directorFrame();
this.accumulator -= ORIGINAL_FRAME_MS;
}
const race = this.race;
const view = this.view;
this.renderer.setScissorTest(false);
this.renderer.setViewport(0, 0, this.host.clientWidth, this.host.clientHeight);
this.renderer.clear();
if (this.local && view) {
const local = this.local;
const width = Math.max(1, this.host.clientWidth), height = Math.max(1, this.host.clientHeight);
const rects = splitViewports(local.players.length, width, height);
view.syncPlayers(local.players.map((p) => ({ car: p.driver.car, color: PLAYER_COLORS[p.slot], label: `P${p.slot + 1}` })), local.dm.rays);
this.renderer.setScissorTest(true);
local.players.forEach((player, i) => {
const rect = rects[i], cam = player.driver.camera;
this.renderer.setViewport(rect.x, height - rect.y - rect.height, rect.width, rect.height);
this.renderer.setScissor(rect.x, height - rect.y - rect.height, rect.width, rect.height);
this.camera.aspect = rect.width / rect.height;
this.camera.position.copy(cam.worldPosition);
this.camera.up.copy(cam.up).normalize();
this.camera.lookAt(cam.target);
this.applyFov(this.camera, cam.fieldOfView);
this.renderer.clear();
this.renderer.render(view.scene, this.camera);
});
this.renderer.setScissorTest(false);
this.screens.renderLocal(local.players, rects, local.paused, local.status);
} else if (race && view && race.driving) {
if (this.online) {
const online = this.online, car = race.dm.car;
const position = car.rb.position, quaternion = car.rb.quaternion;
const inverse = quaternion.clone().invert();
online.update({ p: position.toArray(), q: quaternion.toArray(), w: car.wheels.map(w => [...w.visualGroupPosition.clone().sub(position).applyQuaternion(inverse).toArray(), w.visualRotationZ, w.pDistanceCovered]) }, now);
view.syncPlayers([{ car, label: 'You', color: PLAYER_COLORS[0] }], race.dm.rays);
const remote = online.remotePlayers(now);
const labelled = remote.map(p => ({ ...p, label: online.label(p.actor), color: PLAYER_COLORS[remoteColor(p.actor)] }));
view.syncRemote(labelled);
// Read-only observations reflect the same poses passed to the renderer.
this.renderer.domElement.dataset.localPosition = position.toArray().join(',');
this.renderer.domElement.dataset.remotePositions = JSON.stringify(remote.map(p => p.pose.p));
this.renderer.domElement.dataset.onlinePlayers = String(remote.length + 1);
this.renderer.domElement.dataset.remoteLabels = JSON.stringify(labelled.map(p => p.label));
} else view.sync(race.dm);
if (!this.online) {
delete this.renderer.domElement.dataset.localPosition;
delete this.renderer.domElement.dataset.remotePositions;
delete this.renderer.domElement.dataset.onlinePlayers;
delete this.renderer.domElement.dataset.remoteLabels;
}
this.camera.aspect = this.host.clientWidth / Math.max(1, this.host.clientHeight);
const cam = race.dm.camera;
this.camera.position.copy(cam.worldPosition);
this.camera.up.copy(cam.up).normalize();
this.camera.lookAt(cam.target);
this.applyFov(this.camera, cam.fieldOfView);
view.syncMode(race.mode, this.camera);
this.renderer.render(view.scene, this.camera);
this.arrowCamera.position.copy(this.camera.position);
this.arrowCamera.quaternion.copy(this.camera.quaternion);
this.renderer.clearDepth();
this.renderer.render(view.arrowScene, this.arrowCamera);
this.screens.renderHud(race.mode);
}
requestAnimationFrame((t) => this.frame(t));
}
}
