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.
import { Quaternion, Vector3 } from 'three';
import type { ControllerFrame } from '../../docs/examples/gamepad.js';
import { Input } from './input.js';
import type { LevelData } from './level/data.js';
import { PickupManager } from './lingo/gameplay.js';
import { CarSound } from './lingo/carSound.js';
import { CAR_START_POSITION } from './lingo/car.js';
import { carStart, DrivingManager } from './lingo/drivingManager.js';
import type { SoundMixer } from './audio.js';
export const PLAYER_COLORS = ['#ffcf33', '#4cbcff', '#ff6680', '#7ee894'];
export interface Viewport { x: number; y: number; width: number; height: number }
/** Coordinates are CSS pixels from the top left. */
export function splitViewports(count: number, width: number, height: number): Viewport[] {
if (!count) return [];
// Classic horizontal split: P1 above P2, then a 2×2 grid for 3–4.
const cols = count <= 2 ? 1 : 2;
const rows = Math.ceil(count / cols);
return Array.from({ length: count }, (_, i) => {
const col = i % cols, row = Math.floor(i / cols);
const x = Math.floor(col * width / cols), y = Math.floor(row * height / rows);
return { x, y, width: Math.floor((col + 1) * width / cols) - x, height: Math.floor((row + 1) * height / rows) - y };
});
}
export interface LocalPlayer {
slot: number;
controllerIndex: number;
input: Input;
driver: DrivingManager['players'][number];
sound: CarSound | null;
recovery: PickupManager;
}
/** One physics world, one poll per browser frame, up to four independent cars. */
export class LocalMultiplayer {
readonly dm: DrivingManager;
readonly players: LocalPlayer[] = [];
paused = false;
status = 'Connect a controller. If it is not detected, press any button.';
constructor(private readonly level: LevelData, private readonly mixer: SoundMixer | null) {
this.dm = new DrivingManager(level, new Input(), {}, false);
// Music belongs to the session, rather than to each joining car.
mixer?.sound(8).stop();
mixer?.sound(8).play({ member: 'musicloop1', loopCount: 0 });
if (mixer) mixer.sound(8).volume = 75;
}
updateControllers(frame: ControllerFrame): void {
const present = new Set(frame.players.filter((c) => c.mapped).map((c) => c.index));
// An unavailable API is not evidence that all controllers were unplugged.
if (frame.status !== 'blocked' && frame.status !== 'unavailable') {
for (const player of [...this.players]) {
if (!present.has(player.controllerIndex) || frame.disconnected.includes(player.controllerIndex)) this.remove(player);
}
for (const controller of frame.players) {
if (!controller.mapped || this.players.some((p) => p.controllerIndex === controller.index) || this.players.length >= 4) continue;
const slot = [0, 1, 2, 3].find((s) => !this.players.some((p) => p.slot === s))!;
const input = new Input();
const driver = this.dm.addPlayer(input, this.spawn(slot), `car-p${slot + 1}`);
driver.camera.worldPosition.copy(driver.car.rb.position).add(new Vector3(0, -70, 30));
driver.camera.update();
driver.camera.fadeIn();
const sound = this.mixer ? new CarSound(driver.car, input, this.mixer, 10 + slot * 3, false) : null;
driver.car.sound = sound;
const recovery = new PickupManager({ dm: { car: driver.car, rays: this.dm.rays, havokManager: this.dm.havokManager } });
this.players.push({ slot, controllerIndex: controller.index, input, driver, sound, recovery });
this.players.sort((a, b) => a.slot - b.slot);
console.log('[multiplayer] player added', { player: slot + 1, controllerIndex: controller.index, players: this.players.length });
}
}
for (const player of this.players) {
const controller = frame.players.find((c) => c.index === player.controllerIndex);
player.input.clear();
if (controller && frame.status === 'ready' && !this.paused) player.input.applyController(controller);
}
if (frame.status === 'inactive') this.paused = true;
const unsupported = frame.players.filter((c) => !c.mapped);
this.status = frame.status === 'blocked' || frame.status === 'unavailable'
? 'Controller access unavailable. Return to the menu for single-player controls.'
: unsupported.length
? `Controller ${unsupported.map((c) => c.index + 1).join(', ')} detected without a supported mapping; unable to add ${unsupported.length === 1 ? 'its car' : 'their cars'}.`
: this.players.length
? ''
: 'No controllers detected. Click the game, then press a controller button.';
if (frame.players.some((c) => c.mapped && c.actions.menu?.pressed)) this.paused = !this.paused;
if (this.paused) for (const player of this.players) player.input.clear();
}
private spawn(slot: number) {
const base = carStart(this.level) ?? { position: CAR_START_POSITION.clone(), quaternion: new Quaternion() };
// Search around the level's start for ground and a gap from existing cars.
for (let attempt = 0; attempt < 32; attempt++) {
const offset = new Vector3(((slot + attempt) % 4 - 1.5) * 100, -Math.floor(attempt / 4) * 100, 0);
offset.applyQuaternion(base.quaternion ?? new Quaternion());
const candidate = base.position.clone().add(offset);
const hit = this.dm.rays.modelsUnderRay(candidate.clone().add(new Vector3(0, 0, 300)), new Vector3(0, 0, -1));
if (!hit || hit.isectNormal.z < 0.65 || Math.abs(hit.isectPosition.z - base.position.z) > 250) continue;
candidate.z = hit.isectPosition.z + 40;
if (this.players.every((p) => p.driver.car.rb.position.distanceTo(candidate) > 85)) return { position: candidate, quaternion: base.quaternion };
}
// Keep a unique fallback above the start for unusually small custom levels.
return { position: base.position.clone().add(new Vector3(0, 0, 100 + slot * 100)), quaternion: base.quaternion };
}
step(elapsedMs: number): void {
if (!this.paused && this.players.length) {
this.dm.stepFrame(elapsedMs);
for (const player of this.players) player.recovery.stepFrame();
}
}
private remove(player: LocalPlayer): void {
player.input.clear();
player.sound?.destroy();
this.dm.removePlayer(player.driver);
this.players.splice(this.players.indexOf(player), 1);
console.log('[multiplayer] player removed', { player: player.slot + 1, controllerIndex: player.controllerIndex, players: this.players.length });
}
destroy(): void {
for (const player of [...this.players]) this.remove(player);
this.mixer?.sound(8).stop();
this.dm.destroy();
}
}
