SOURCE / PINNED RELEASE
Made of little things.
Supersonic RC Revive
- Release
- 1ba42f1ca1d6…
- Author-recorded commit
- baecad10b1cd…
- License
- LICENSE
- Author’s source reference
- nostr://npub1ye5ptcxfyyxl5vjvdjar2ua3f0hynkjzpx552mu5snj3qmx5pzjscpknpr/wss%3A%2F%2Fgit.napplet.soy%2F/n-143146b0d6f
Archive hash verified: 90d22b206672eba4…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Browser host: the original's screen flow (title → select → help → loading →
// race), its 1 s fades, and a fixed 40 fps Director frame clock.
import { PerspectiveCamera, 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';
/** 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 interface Progress {
load(): Promise<ModeName[]>;
save(unlocked: ModeName[]): void;
}
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;
private unlocked = new Set<ModeName>(['free_driving']);
/** "the milliSeconds" */
private ms = 0;
private lastTime = 0;
private accumulator = 0;
onStatus: (text: string) => void = () => {};
/**
* 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;
/**
* A second look at controller menu presses (the secret code's controller
* entry, after any open panel): returning true takes the press, so the
* screens behind it do not.
*/
menuIntentFilter: ((intent: MenuIntent) => boolean) | null = null;
private padHint = false;
constructor(private readonly host: HTMLElement, private readonly progress: Progress | null) {
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);
for (const m of (await this.progress?.load()) ?? []) this.unlocked.add(m);
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. Only between races. If the pack
* cannot be built this throws and keeps the previous scene, so the caller can
* reinstall the old pack.
*/
async reloadAssets(): Promise<void> {
if (this.racing) throw new Error('Finish the race before switching asset packs.');
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 race is running (asset packs can only be switched between races). */
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.fadeIn();
this.screens.showTitle(() => this.fadeOut(() => this.selectStart()), this.packSwitch);
}
private selectStart(): void {
this.fadeIn();
this.mixer?.sound(8).stop();
if (this.mixer) this.mixer.sound(8).volume = 255;
this.mixer?.sound(8).play({ member: 'SSRMusicSplashScreen', loopCount: 0 });
this.screens.showSelect(this.unlocked, (mode) => this.fadeOut(() => this.helpStart(mode)), () => this.fadeOut(() => this.localStart()));
}
private helpStart(mode: ModeName): void {
this.fadeIn();
this.screens.showHelp(() => {
this.mixer?.sound(8).fadeOut(500);
this.fadeOut(() => this.gameStart(mode));
});
}
/** gameTypeManager.start → realStart → realStart2. */
private gameStart(name: ModeName): void {
const level = this.level!;
this.screens.showLoading();
this.fadeIn(() => {
const dm = new DrivingManager(level, this.input);
const mode = createMode(name, {
dm,
input: this.input,
markers: level.markers,
mixer: this.mixer,
ms: () => this.ms,
unlock: (m) => this.unlock(m),
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.selectStart();
for (const resolve of this.idleWaiters.splice(0)) resolve();
}
private raceEnd(): void {
const race = this.race;
if (!race) return;
this.race = null;
race.sound?.destroy();
race.dm.destroy();
this.view?.setMode(null);
this.screens.hideRace();
this.input.clear();
this.selectStart();
for (const resolve of this.idleWaiters.splice(0)) resolve();
}
private unlock(mode: ModeName): void {
if (this.unlocked.has(mode)) return;
this.unlocked.add(mode);
this.progress?.save([...this.unlocked]);
}
// --- 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);
// 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 (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;
if (this.menuIntentFilter?.(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) {
view.sync(race.dm);
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));
}
}
