SOURCE / PINNED RELEASE
Made of little things.
Napplet Machines V8
- Release
- b3f614e739f0…
- Author-recorded commit
- 534f19acb4ba…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b5572362d4a
Archive hash verified: 0f7feb97e2c3d336…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import './napplet-settings.js';
import { storage } from '@napplet/sdk';
import { runtimeHasDomain } from './domain-availability.js';
import {
Road,
presets,
copyTrack,
MIN_POINT_WIDTH,
MAX_POINT_WIDTH,
validateTrack,
decodeTrack,
clamp,
lerp,
wrapAngle,
} from './track.js';
import { carPortrait, courseStyle } from './art.js';
import { Renderer } from './render.js';
import { TrackEditor } from './editor.js';
import { ControllerDriving, TouchDriving, mixDriving } from './controls.js';
import {
DT,
cars,
createWorld,
advanceBody,
stepWorld,
ranking,
idleInput,
slipAngle,
type Body,
type World,
type Input,
type Driver,
} from './physics.js';
import { Multiplayer, errorText, type Start, type Command } from './network.js';
import './styles.css';
const $ = <T extends HTMLElement = HTMLElement>(id: string) =>
document.getElementById(id) as T;
const button = (id: string, fn: () => void | Promise<void>) =>
$(id).addEventListener('click', () => {
void action(fn);
});
const widthInput = $<HTMLInputElement>('widthInput');
widthInput.min = String(MIN_POINT_WIDTH);
widthInput.max = String(MAX_POINT_WIDTH);
const status = (message: string, error = false) => {
$('status').textContent = message;
$('status').classList.toggle('error', error);
};
async function action(fn: () => void | Promise<void>) {
try {
await fn();
} catch (e) {
status(errorText(e), true);
}
}
let track = copyTrack(presets[0]),
road = new Road(track),
car = 0,
view = 'garage',
world: World | null = null,
localId = 'you',
paused = false,
online = false,
follow = true;
let frameId = 0,
last = performance.now(),
accumulator = 0,
hudAt = 0,
sendAt = 0,
sequence = 0,
lastSnapshot = -1,
lastPacket = 0,
anchorTick = 0,
anchorTime = 0;
let pending: Command[] = [],
snapshots: { world: World; at: number }[] = [],
remoteQueues = new Map<string, Command[]>(),
prediction: Body | null = null;
let rendered: Body[] = [];
const pressed = new Set<string>();
const controllers = new ControllerDriving();
let controllerDrive = idleInput(),
resetRequested = false,
controllerLost = false;
const multiplayer = new Multiplayer();
let renderer: Renderer;
try {
renderer = new Renderer($<HTMLCanvasElement>('scene'));
} catch (e) {
status(errorText(e), true);
$('raceButton').setAttribute('disabled', '');
throw e;
}
const editor = new TrackEditor($<HTMLCanvasElement>('plan'), track);
const formatTime = (t: number) =>
`${Math.floor(t / 60)
.toString()
.padStart(2, '0')}:${(t % 60).toFixed(1).padStart(4, '0')}`;
function selectCar(index: number) {
car = index;
document.querySelectorAll<HTMLElement>('[data-car]').forEach((el) => {
el.classList.toggle('selected', Number(el.dataset.car) === car);
el.setAttribute('aria-pressed', String(Number(el.dataset.car) === car));
});
const c = cars[car];
$('carDetail').style.setProperty('--car', c.color);
$('carDetail').innerHTML =
`<div class="ride-hero"><span class="ride-number">0${car + 1}</span>${carPortrait(car)}<div class="car-name">${c.name}</div><div class="car-tag">${c.tag}</div></div>${['Acceleration', 'Top speed', 'Grip', 'Weight'].map((s, i) => `<div class="stat"><span>${s}</span><span class="bars" aria-label="${c.stats[i]} out of 5">${Array.from({ length: 5 }, (_, j) => `<i class="${j < c.stats[i] ? 'filled' : ''}"></i>`).join('')}</span></div>`).join('')}`;
}
cars.forEach((c, i) => {
const b = document.createElement('button');
b.dataset.car = String(i);
b.style.setProperty('--car', c.color);
b.innerHTML = '<i class="car-icon"></i>';
b.setAttribute('aria-label', `Choose ${c.name}`);
b.title = c.name;
b.onclick = () => selectCar(i);
$('carPicker').append(b);
});
selectCar(0);
presets.forEach((p, i) =>
$<HTMLSelectElement>('trackSelect').add(new Option(p.name, String(i))),
);
function updateRoad() {
road = new Road(track);
renderer.setRoad(road);
editor.track = track;
editor.road = road;
$('sceneTitle').textContent = track.name;
$('sceneSubtitle').textContent = courseStyle(track.name).subtitle;
$('curveNotice').hidden = road.closedSections.size === 0;
$('curveNotice').textContent =
'Tight inside corners are filled and rounded; walls follow the closed edge.';
$('scene').dataset.closedSections = String(road.closedSections.size);
$('scene').dataset.roadSamples = String(road.samples.length);
$('trackLength').textContent = `${Math.round(road.length)} M`;
$('wallFact').textContent = track.points.every((p) => p.wall)
? 'WALLED'
: 'OPEN EDGES';
$<HTMLInputElement>('trackName').value = track.name;
editor.draw();
updatePoint();
}
function updatePoint() {
const p = track.points[editor.selected];
$('pointLabel').textContent = p
? `POINT ${String(editor.selected + 1).padStart(2, '0')}`
: 'SELECT A POINT';
$('pointCount').textContent = `${track.points.length} / 32`;
$<HTMLInputElement>('heightInput').disabled = !p;
$<HTMLInputElement>('widthInput').disabled = !p;
$<HTMLInputElement>('wallThicknessInput').disabled = !p || !p.wall;
$('sectionLabel').textContent = p
? `SECTION ${editor.selected + 1} → ${((editor.selected + 1) % track.points.length) + 1}`
: 'SELECT A SECTION';
$<HTMLInputElement>('segmentWall').disabled = !p;
$<HTMLButtonElement>('removePoint').disabled = !p || track.points.length <= 4;
$<HTMLButtonElement>('addPoint').disabled = track.points.length >= 32;
if (p) {
$<HTMLInputElement>('heightInput').value = String(p.z);
$('heightValue').textContent = `Z ${p.z.toFixed(1)} m`;
$<HTMLInputElement>('segmentWall').checked = p.wall;
$<HTMLInputElement>('widthInput').value = String(p.width);
$('widthValue').textContent = `${Number(p.width.toFixed(2))} m`;
$<HTMLInputElement>('wallThicknessInput').value = String(p.wallThickness);
$('wallThicknessValue').textContent = `${p.wallThickness.toFixed(1)} m`;
}
}
let roadDirty = false;
editor.onChange = () => {
roadDirty = true;
$('saveState').textContent = 'UNSAVED';
};
editor.onSelect = updatePoint;
function setView(next: string) {
if (world) return;
view = next;
$('app').classList.toggle('editor', next === 'editor');
document
.querySelectorAll<HTMLElement>('[data-view]')
.forEach((b) => b.classList.toggle('active', b.dataset.view === next));
for (const v of ['garage', 'rooms', 'editor'])
$(v + 'Panel').hidden = v !== next;
$('planWrap').hidden = next !== 'editor';
if (next === 'editor') {
editor.draw();
updatePoint();
}
if (next === 'rooms' && !multiplayer.room) void refreshRooms();
}
document
.querySelectorAll<HTMLElement>('[data-view]')
.forEach((b) => b.addEventListener('click', () => setView(b.dataset.view!)));
$<HTMLSelectElement>('trackSelect').addEventListener('change', (e) => {
track = copyTrack(presets[Number((e.target as HTMLSelectElement).value)]);
editor.selected = 0;
editor.track = track;
editor.fit();
updateRoad();
});
$<HTMLInputElement>('trackName').addEventListener('input', (e) => {
track.name =
(e.target as HTMLInputElement).value.slice(0, 48) || 'Untitled circuit';
$('sceneTitle').textContent = track.name;
$('sceneSubtitle').textContent = courseStyle(track.name).subtitle;
$('saveState').textContent = 'UNSAVED';
});
for (const id of [
'heightInput',
'widthInput',
'segmentWall',
'wallThicknessInput',
])
$(id).addEventListener('input', () => {
const p = track.points[editor.selected];
if (id === 'heightInput' && p) p.z = Number($<HTMLInputElement>(id).value);
if (id === 'widthInput' && p)
p.width = Number($<HTMLInputElement>(id).value);
if (id === 'wallThicknessInput' && p)
p.wallThickness = Number($<HTMLInputElement>(id).value);
if (id === 'segmentWall' && p) p.wall = $<HTMLInputElement>(id).checked;
editor.onChange();
updatePoint();
});
button('addPoint', () => editor.setTool('add'));
button('removePoint', () => editor.remove());
button('allWalls', () => {
track.points.forEach((p) => (p.wall = true));
editor.onChange();
});
button('noWalls', () => {
track.points.forEach((p) => (p.wall = false));
editor.onChange();
});
editor.onNotice = (message) => status(message);
editor.onTool = () => {
for (const [id, tool] of [
['drawTool', 'draw'],
['moveTool', 'move'],
['addPoint', 'add'],
['wallTool', 'walls'],
]) {
$(id).classList.toggle('active', editor.tool === tool);
$(id).setAttribute('aria-pressed', String(editor.tool === tool));
}
$('planMode').textContent = {
move: 'PLAN / DRAG POINTS',
draw: 'PLAN / DRAW LOOP',
add: 'PLAN / PLACE POINTS',
walls: 'PLAN / SELECT SECTION',
}[editor.tool];
$('editorHint').textContent = {
move: 'Drag a point. Width and elevation apply to that point; the curve closes automatically.',
draw: 'Draw one loop on the plan, then release. At least four points.',
add: 'Click anywhere on the plan to place a point in the nearest section. Esc finishes.',
walls:
'Click a section in the plan, then set its walls and thickness below.',
}[editor.tool];
};
button('drawTool', () => editor.setTool('draw'));
button('moveTool', () => editor.setTool('move'));
button('wallTool', () => editor.setTool('walls'));
window.addEventListener('keydown', (e) => {
if (e.code === 'Escape' && view === 'editor' && !world)
editor.setTool('move');
});
editor.onTool();
button('saveTrack', async () => {
if (!runtimeHasDomain('storage'))
throw new Error(
'Saving is unavailable in this host; your current track is still playable.',
);
if (!validateTrack(track))
throw new Error('Track needs 4–32 separated points and a name.');
await storage.setItem('pocket-circuit.track.v1', JSON.stringify(track));
$('saveState').textContent = 'SAVED';
status('Track saved. Load saved brings it back after reopening.');
});
button('loadTrack', async () => {
if (!runtimeHasDomain('storage'))
throw new Error('Saving is unavailable in this host.');
const saved = await storage.getItem('pocket-circuit.track.v1');
if (!saved)
throw new Error('No saved track yet. Draw a circuit and save it first.');
const parsed = decodeTrack(JSON.parse(saved));
if (!parsed)
throw new Error('Saved track has an unsupported or damaged format.');
track = parsed;
editor.setTool('move');
editor.selected = 0;
editor.track = track;
editor.fit();
updateRoad();
$('saveState').textContent = 'SAVED';
status('Saved track loaded.');
});
function roster(count: number): Driver[] {
const ds: Driver[] = [{ id: 'you', name: 'You', car, ai: false }];
const names = ['Pip', 'Mochi', 'Rivet', 'Nori', 'Bolt'];
for (let i = 1; i < count; i++)
ds.push({
id: `ai-${i}`,
name: names[i - 1],
car: (i + car) % 4,
ai: true,
});
return ds;
}
function begin(w: World, isOnline: boolean) {
world = w;
scene.dataset.countdown = String(w.countdown);
scene.dataset.racing = 'true';
online = isOnline;
localId = isOnline ? multiplayer.actor : 'you';
paused = false;
pending = [];
snapshots = [];
remoteQueues.clear();
sequence = w.bodies.find((body) => body.id === localId)?.ack || 0;
if (isOnline && w.bodies.find((body) => body.id === localId)?.ai)
localId = `spectator:${multiplayer.actor}`;
lastSnapshot = -1;
lastPacket = performance.now();
prediction = null;
accumulator = 0;
sendAt = 0;
$('app').classList.remove('editor');
$('app').classList.add('racing');
$('raceHud').hidden = false;
$('raceActions').hidden = false;
$('sceneInfo').hidden = true;
$('sceneBottom').hidden = true;
$('planWrap').hidden = true;
$('results').hidden = true;
$('touchControls').hidden = false;
$('pauseButton').hidden = isOnline;
$('reconnectRace').hidden = !isOnline;
$('pauseButton').textContent = 'Pause';
$('cameraButton').textContent = follow ? 'Overview' : 'Follow';
$('modeTag').textContent = isOnline
? 'ONLINE / ' + w.bodies.filter((b) => !b.ai).length + ' PLAYERS'
: 'LOCAL / ' + w.bodies.length + ' CARS';
clearInput();
$<HTMLCanvasElement>('scene').focus();
status(
isOnline
? 'Race connected. The host judges collisions and the finish.'
: 'WASD / arrows to drive · Space to drift · R to recover',
);
}
function startLocal() {
if (multiplayer.room)
throw new Error('Leave your online room before starting a local race.');
updateRoad();
begin(
createWorld(roster(Number($<HTMLSelectElement>('gridSize').value)), road),
false,
);
}
button('raceButton', startLocal);
button('testTrack', startLocal);
function exit() {
scene.dataset.racing = 'false';
world = null;
online = false;
prediction = null;
clearInput();
$('app').classList.remove('racing');
$('raceHud').hidden = true;
$('raceActions').hidden = true;
$('results').hidden = true;
$('countdown').hidden = true;
$('touchControls').hidden = true;
$('sceneInfo').hidden = false;
$('sceneBottom').hidden = false;
renderer.zoom = 1;
$('modeTag').textContent = 'LOCAL / 6 CARS';
setView(multiplayer.room ? 'rooms' : view);
}
button('exitRace', async () => {
if (online && multiplayer.room) {
await multiplayer.leave();
status('Left the online race.');
}
exit();
});
button('againButton', async () => {
if (online && multiplayer.isHost) {
await multiplayer.sendControl({ type: 'return' });
multiplayer.start = null;
} else if (online) {
await multiplayer.leave();
}
exit();
});
button('pauseButton', () => {
paused = !paused;
controllerLost = false;
clearInput();
$('pauseButton').textContent = paused ? 'Resume' : 'Pause';
});
button('reconnectRace', () => multiplayer.retry());
button('recoverButton', () => {
if (world && !paused) resetRequested = true;
});
button('cameraButton', () => {
follow = !follow;
$('cameraButton').textContent = follow ? 'Overview' : 'Follow';
});
button('orbitLeft', () => {
renderer.azimuth -= 0.25;
});
button('orbitRight', () => {
renderer.azimuth += 0.25;
});
button('fitPlan', () => {
editor.fit();
editor.draw();
});
button('fullPlan', () => {
editor.zoom = 1;
editor.draw();
});
button('zoomIn', () => {
renderer.zoom = clamp(renderer.zoom + 0.15, 0.6, 2);
});
button('zoomOut', () => {
renderer.zoom = clamp(renderer.zoom - 0.15, 0.6, 2);
});
let orbit: { x: number; y: number; id: number } | null = null;
const scene = $<HTMLCanvasElement>('scene');
scene.addEventListener('pointerdown', (e) => {
if (world) return;
scene.setPointerCapture(e.pointerId);
orbit = { x: e.clientX, y: e.clientY, id: e.pointerId };
});
scene.addEventListener('pointermove', (e) => {
if (!orbit || orbit.id !== e.pointerId) return;
renderer.azimuth += (e.clientX - orbit.x) * 0.008;
renderer.elevation = clamp(
renderer.elevation + (e.clientY - orbit.y) * 0.006,
0.35,
1.3,
);
orbit = { x: e.clientX, y: e.clientY, id: e.pointerId };
});
scene.addEventListener('pointerup', () => (orbit = null));
scene.addEventListener('pointercancel', () => (orbit = null));
const touchDriving = new TouchDriving($('steeringPad'), $('pedalPad'));
function clearInput() {
controllerLost = false;
pressed.clear();
touchDriving.clear();
controllerDrive = idleInput();
resetRequested = false;
scene.dataset.inputSteer = '0';
scene.dataset.inputThrottle = '0';
scene.dataset.inputBrake = 'false';
controllers.reset();
}
window.addEventListener('resize', clearInput);
window.addEventListener('keydown', (e) => {
if (
!world ||
e.target instanceof HTMLInputElement ||
e.target instanceof HTMLSelectElement
)
return;
if (
[
'ArrowUp',
'ArrowDown',
'ArrowLeft',
'ArrowRight',
'KeyW',
'KeyA',
'KeyS',
'KeyD',
'Space',
'KeyR',
].includes(e.code)
) {
e.preventDefault();
if (e.code === 'KeyR') {
if (!e.repeat) resetRequested = true;
} else pressed.add(e.code);
}
if (e.code === 'Escape' && !online) {
paused = !paused;
clearInput();
$('pauseButton').textContent = paused ? 'Resume' : 'Pause';
}
});
window.addEventListener('keyup', (e) => pressed.delete(e.code));
window.addEventListener('blur', () => {
clearInput();
if (world && !online) {
paused = true;
$('pauseButton').textContent = 'Resume';
}
});
document.addEventListener('visibilitychange', () => {
clearInput();
accumulator = 0;
last = performance.now();
if (document.hidden && world && !online) {
paused = true;
$('pauseButton').textContent = 'Resume';
}
});
function input(): Input {
const keyboard: Input = {
throttle:
Number(pressed.has('KeyW') || pressed.has('ArrowUp')) -
Number(pressed.has('KeyS') || pressed.has('ArrowDown')),
steer:
Number(pressed.has('KeyA') || pressed.has('ArrowLeft')) -
Number(pressed.has('KeyD') || pressed.has('ArrowRight')),
brake: pressed.has('Space'),
reset: resetRequested,
};
resetRequested = false;
return mixDriving(keyboard, touchDriving.value, controllerDrive);
}
function pollController() {
const pad = controllers.poll();
controllerDrive = pad.drive;
const label =
pad.selected !== null
? `PAD ${pad.selected + 1}`
: pad.unmapped
? 'UNMAPPED PAD'
: pad.status === 'blocked'
? 'PAD BLOCKED'
: pad.status === 'unavailable'
? 'NO GAMEPAD API'
: 'PRESS A / ✕';
if ($('controllerState').textContent !== label)
$('controllerState').textContent = label;
$('controllerState').title = pad.unmapped
? 'This controller has no standard mapping. Keyboard and touch remain available.'
: 'Left stick / D-pad: steer · RT/R2: gas · LT/L2: brake · B/○: drift · Y/△: recover · Menu: pause';
$('controllerState').dataset.state =
pad.selected === null ? 'waiting' : 'connected';
if (pad.disconnected) {
clearInput();
controllerLost = true;
status(
'Controller disconnected. Press A / ✕ to reconnect, or use touch / keyboard.',
);
if (world && !online) {
paused = true;
$('pauseButton').textContent = 'Resume';
}
return;
}
if (pad.joined) controllerLost = false;
if (pad.status !== 'ready' || pad.joined) return;
if (world) {
if (world.ended && pad.confirm) $('againButton').click();
else if (pad.pause && !online) $('pauseButton').click();
else if (paused && pad.back) $('exitRace').click();
else if (!paused && pad.recover) resetRequested = true;
} else if (view === 'garage') {
if (pad.carStep) selectCar((car + pad.carStep + cars.length) % cars.length);
if (pad.trackStep) {
const select = $<HTMLSelectElement>('trackSelect');
select.selectedIndex =
(select.selectedIndex + pad.trackStep + presets.length) %
presets.length;
select.dispatchEvent(new Event('change'));
}
if (pad.confirm) $('raceButton').click();
} else if (view === 'rooms' && pad.confirm) {
const target = multiplayer.room
? multiplayer.isHost
? 'startRoom'
: 'readyButton'
: 'hostButton';
const control = $<HTMLButtonElement>(target);
if (control && !control.disabled) control.click();
}
}
let audio: AudioContext | null = null,
osc: OscillatorNode | null = null,
gain: GainNode | null = null,
sound = false;
button('soundButton', async () => {
sound = !sound;
if (sound) {
audio ??= new AudioContext();
await audio.resume();
if (!osc) {
osc = audio.createOscillator();
gain = audio.createGain();
osc.type = 'sawtooth';
osc.connect(gain);
gain.connect(audio.destination);
gain.gain.value = 0;
osc.start();
}
}
$('soundButton').textContent = sound ? '♪ On' : '♪ Off';
$('soundButton').setAttribute(
'aria-label',
sound ? 'Disable sound' : 'Enable sound',
);
});
function showResults() {
if (!world) return;
$('results').hidden = false;
const order = ranking(world),
position = order.findIndex((b) => b.id === localId) + 1;
$('resultTitle').textContent =
position === 1
? 'Small car. Big win.'
: `Finished ${position || '—'}${position === 2 ? 'nd' : position === 3 ? 'rd' : 'th'}.`;
$('resultOrder').replaceChildren(
...order.map((b, i) => {
const li = document.createElement('li'),
n = document.createElement('span'),
t = document.createElement('span');
n.textContent = `${i + 1}. ${b.name}${b.id === localId ? ' · YOU' : ''}`;
t.textContent = b.finished ? formatTime(b.finished) : 'DNF';
li.append(n, t);
return li;
}),
);
}
function updateHud() {
if (!world) return;
const own = world.bodies.find((b) => b.id === localId),
pos = ranking(world).findIndex((b) => b.id === localId) + 1;
$('position').innerHTML = `${pos || '—'}<span>/${world.bodies.length}</span>`;
$('lap').textContent = `LAP ${Math.min(3, (own?.lap || 0) + 1)} / 3`;
$('raceTime').textContent = formatTime(world.time);
const body = rendered.find((b) => b.id === localId) || own;
$('speed').textContent = String(
Math.round(Math.hypot(body?.vx || 0, body?.vy || 0) * 3.6),
);
$('driftState').hidden = !(
body?.grounded &&
Math.hypot(body.vx, body.vy) > 5 &&
Math.abs(slipAngle(body)) > 0.22
);
$('raceOrder').replaceChildren(
...ranking(world).map((b, i) => {
const li = document.createElement('li'),
dot = document.createElement('i'),
text = document.createElement('span');
dot.style.background = cars[b.car].color;
text.textContent = `${i + 1} ${b.name}${b.id === localId ? ' · YOU' : ''}`;
li.classList.toggle('you', b.id === localId);
li.append(dot, text);
return li;
}),
);
if (world.ended) showResults();
}
async function refreshRooms() {
if (multiplayer.room) return;
$('roomList').textContent = 'Finding rooms…';
try {
const rooms = await multiplayer.list();
$('roomList').replaceChildren();
if (!rooms.length)
$('roomList').textContent = 'No rooms yet. Start the first grid.';
for (const r of rooms) {
const b = document.createElement('button'),
name = document.createElement('span'),
count = document.createElement('small');
b.className = 'room-item';
name.textContent = r.name;
count.textContent = `${r.peers.length}/${r.capacity} · Join`;
b.append(name, count);
b.disabled = r.peers.length >= r.capacity;
b.onclick = () => void action(() => joinRoom(r.room));
$('roomList').append(b);
}
status('Room list updated.');
} catch (e) {
$('roomList').textContent = 'Rooms unavailable. Refresh to retry.';
status(errorText(e), true);
}
}
function configureProfile() {
multiplayer.profile = {
name:
$<HTMLInputElement>('playerName').value.trim().slice(0, 18) || 'Racer',
car,
ready: false,
};
}
let roomBusy = false;
async function joinRoom(code: string) {
if (roomBusy || multiplayer.room) return;
roomBusy = true;
configureProfile();
try {
status('Joining room…');
await multiplayer.join(code);
} finally {
roomBusy = false;
}
}
button('refreshRooms', refreshRooms);
button('joinCode', () => joinRoom($<HTMLInputElement>('roomCode').value));
button('hostButton', async () => {
if (roomBusy || multiplayer.room) return;
roomBusy = true;
configureProfile();
$<HTMLButtonElement>('hostButton').disabled = true;
try {
status('Creating room…');
await multiplayer.host(
$<HTMLInputElement>('roomName').value,
Number($<HTMLSelectElement>('roomCapacity').value),
);
} finally {
roomBusy = false;
$<HTMLButtonElement>('hostButton').disabled = false;
}
});
button('leaveRoom', async () => {
await multiplayer.leave();
status('Left the room.');
void refreshRooms();
});
button('readyButton', async () => {
multiplayer.profile.ready = !multiplayer.profile.ready;
await multiplayer.hello();
updateRoom();
});
const retry = document.createElement('button');
retry.id = 'reconnect';
retry.textContent = 'Reconnect';
retry.onclick = () => void action(() => multiplayer.retry());
$('activeRoom').append(retry);
button('startRoom', async () => {
if (!multiplayer.room || !multiplayer.isHost) return;
const drivers = multiplayer.roster(),
count = multiplayer.room.capacity;
for (let i = drivers.length; i < count; i++)
drivers.push({
id: `ai-${i}`,
name: ['Pip', 'Mochi', 'Rivet', 'Nori', 'Bolt', 'Pip'][i],
car: i % 4,
ai: true,
});
await multiplayer.begin(track, createWorld(drivers, road));
});
function updateRoom() {
const r = multiplayer.room;
$('browseRooms').hidden = !!r;
$('activeRoom').hidden = !r;
if (!r) return;
$('roomTitle').textContent = r.name;
$<HTMLInputElement>('activeCode').value = r.room;
$('startRoom').hidden = !multiplayer.isHost;
$('readyButton').hidden = multiplayer.isHost;
$('readyButton').textContent = multiplayer.profile.ready
? 'Ready ✓'
: 'Ready up';
const expected = multiplayer.isHost ? r.peers.length - 1 : 1,
connected = multiplayer.connected.size;
const all = connected >= expected;
$('connectionState').textContent = all
? expected
? 'Connected'
: 'Waiting for players'
: `Connecting · ${connected}/${expected} peers`;
const canStart = r.peers.every(
(id) =>
id === multiplayer.actor ||
(multiplayer.members.get(id)?.ready && multiplayer.connected.has(id)),
);
$<HTMLButtonElement>('startRoom').disabled = !canStart;
$('members').replaceChildren(
...r.peers.map((id) => {
const p = multiplayer.members.get(id),
row = document.createElement('div'),
name = document.createElement('span'),
state = document.createElement('small'),
dot = document.createElement('b');
row.className = 'member';
dot.style.background = cars[p?.car || 0].color;
name.append(
dot,
document.createTextNode(
(p?.name || 'Connecting…') +
(id === multiplayer.actor ? ' · YOU' : ''),
),
);
state.textContent =
id === multiplayer.authority
? 'HOST'
: p?.ready
? 'READY'
: 'NOT READY';
row.append(name, state);
return row;
}),
);
}
multiplayer.onUpdate = () => {
updateRoom();
if (world && online && multiplayer.isHost && multiplayer.room)
for (const b of world.bodies)
if (!b.ai && !multiplayer.room.peers.includes(b.id)) {
b.ai = true;
b.name = b.name.slice(0, 12) + ' · AI';
}
};
multiplayer.onError = (m) => status(m, true);
multiplayer.onEnded = (m) => {
exit();
status(m);
};
multiplayer.onStart = (s: Start) => {
track = copyTrack(s.track);
updateRoad();
begin(structuredClone(s.world), true);
};
multiplayer.onInput = (from, commands) => {
const b = world?.bodies.find((b) => b.id === from);
if (!b || b.ai || !world) return;
const q = remoteQueues.get(from) || [],
seen = new Set(q.map((c) => c.seq));
for (const c of commands)
if (c.seq > b.ack && c.seq <= b.ack + 120 && !seen.has(c.seq)) {
q.push(c);
seen.add(c.seq);
}
q.sort((a, b) => a.seq - b.seq);
remoteQueues.set(from, q.slice(0, 120));
};
multiplayer.onSnapshot = (w) => {
if (!world || multiplayer.isHost || w.tick <= lastSnapshot) return;
if (
w.bodies.length !== world.bodies.length ||
w.bodies.some((b, i) => b.id !== world!.bodies[i].id)
)
return;
const own = w.bodies.find((b) => b.id === localId);
if (own && own.ack > sequence) return;
const now = performance.now();
lastPacket = now;
lastSnapshot = w.tick;
anchorTick = w.tick;
anchorTime = now;
world = structuredClone(w);
if (own) {
pending = pending.filter((c) => c.seq > own.ack);
prediction = structuredClone(own);
if (!w.ended && w.countdown === 0)
for (const c of pending) advanceBody(prediction, c.input, road);
}
snapshots.push({ world: structuredClone(w), at: now });
if (snapshots.length > 24) snapshots.shift();
};
function guestStep(inp: Input) {
if (!world) return;
const b = prediction || world.bodies.find((b) => b.id === localId);
if (!b) return;
if (!prediction) prediction = structuredClone(b);
if (world.countdown > 0 || world.ended) return;
if (pending.length >= 120) {
status('Waiting for host · input history full', true);
return;
}
const command = { seq: ++sequence, input: { ...inp } };
pending.push(command);
advanceBody(prediction!, inp, road);
}
function remoteStep(b: Body) {
if (b.id === localId) return false;
const q = remoteQueues.get(b.id);
if (q?.length) {
const command = q.shift()!;
if (command.seq > b.ack) {
advanceBody(b, command.input, road);
b.ack = command.seq;
}
} else advanceBody(b, idleInput(), road);
return true;
}
function interpolated(now: number): Body[] {
if (!world) return [];
if (!online || multiplayer.isHost) return world.bodies;
const tick = anchorTick + (now - anchorTime) / 1000 / DT - 6;
let a = snapshots[0]?.world,
b = a;
for (let i = 1; i < snapshots.length; i++) {
b = snapshots[i].world;
if (b.tick >= tick) break;
a = b;
}
if (!a || !b)
return prediction
? world.bodies.map((x) => (x.id === localId ? prediction! : x))
: world.bodies;
const t = clamp((tick - a.tick) / (b.tick - a.tick || 1), 0, 1);
return b.bodies.map((body, i) => {
if (body.id === localId && prediction) return prediction;
const prev = a!.bodies[i];
if (
!prev ||
prev.falls !== body.falls ||
Math.hypot(prev.x - body.x, prev.y - body.y) > 8
)
return body;
return {
...body,
x: lerp(prev.x, body.x, t),
y: lerp(prev.y, body.y, t),
z: lerp(prev.z, body.z, t),
angle: prev.angle + wrapAngle(body.angle - prev.angle) * t,
pitch: lerp(prev.pitch, body.pitch, t),
roll: lerp(prev.roll, body.roll, t),
steering: lerp(prev.steering, body.steering, t),
handbrake: lerp(prev.handbrake, body.handbrake, t),
};
});
}
function tick(now: number) {
frameId = requestAnimationFrame(tick);
const elapsed = Math.min(0.1, (now - last) / 1000);
last = now;
pollController();
if (roadDirty) {
roadDirty = false;
updateRoad();
}
if (view === 'editor' && !world) editor.draw();
if (world) {
accumulator += elapsed;
if (!paused) {
let steps = 0;
while (accumulator >= DT && steps++ < 6) {
const inp = input();
scene.dataset.inputSteer = inp.steer.toFixed(3);
scene.dataset.inputThrottle = inp.throttle.toFixed(3);
scene.dataset.inputBrake = String(inp.brake);
if (online && !multiplayer.isHost) guestStep(inp);
else
stepWorld(
world,
road,
new Map([[localId, inp]]),
online ? remoteStep : undefined,
);
accumulator -= DT;
}
} else accumulator = 0;
if (online && now >= sendAt) {
sendAt = Math.max(sendAt + (multiplayer.isHost ? 50 : 1000 / 30), now);
if (multiplayer.isHost) {
if (multiplayer.start) multiplayer.start.world = structuredClone(world);
void multiplayer.sendLatest({
type: 'world',
race: multiplayer.start?.race,
world,
});
} else if (pending.length)
void multiplayer.sendLatest({
type: 'input',
race: multiplayer.start?.race,
commands: pending.slice(0, 120),
});
}
rendered = interpolated(now);
const own = rendered.find((b) => b.id === localId);
renderer.render(rendered, follow ? own : undefined, localId);
scene.dataset.localX = (own?.x || 0).toFixed(4);
scene.dataset.localY = (own?.y || 0).toFixed(4);
scene.dataset.localSpeed = Math.hypot(own?.vx || 0, own?.vy || 0).toFixed(
4,
);
scene.dataset.localSlip = (own ? slipAngle(own) : 0).toFixed(4);
scene.dataset.localPitch = (own?.pitch || 0).toFixed(4);
scene.dataset.localRoll = (own?.roll || 0).toFixed(4);
scene.dataset.localSteering = (own?.steering || 0).toFixed(4);
scene.dataset.localHandbrake = (own?.handbrake || 0).toFixed(4);
const remote = rendered.find((body) => body.id === multiplayer.authority);
scene.dataset.remoteSteering = (remote?.steering || 0).toFixed(4);
scene.dataset.remotePitch = (remote?.pitch || 0).toFixed(4);
scene.dataset.remoteHandbrake = (remote?.handbrake || 0).toFixed(4);
scene.dataset.remoteX = (remote?.x || 0).toFixed(4);
scene.dataset.remoteY = (remote?.y || 0).toFixed(4);
scene.dataset.tick = String(world.tick);
scene.dataset.ack = String(
world.bodies.find((b) => b.id === localId)?.ack || 0,
);
scene.dataset.pending = String(pending.length);
scene.dataset.countdown = String(world.countdown);
scene.dataset.racing = 'true';
const countdown =
world.countdown > 0
? String(Math.ceil(world.countdown))
: paused
? controllerLost
? 'PAD LOST'
: 'PAUSED'
: online && !multiplayer.isHost && now - lastPacket > 2500
? 'WAITING'
: '';
$('countdown').hidden = !countdown;
$('countdown').textContent = countdown;
if (now > hudAt) {
hudAt = now + 120;
updateHud();
}
if (gain && audio && osc) {
gain.gain.setTargetAtTime(
sound && !paused ? 0.018 : 0,
audio.currentTime,
0.15,
);
osc.frequency.setTargetAtTime(
45 + Math.hypot(own?.vx || 0, own?.vy || 0) * 5,
audio.currentTime,
0.08,
);
}
} else {
const demos = createWorld(roster(4), road).bodies;
renderer.render(demos);
scene.dataset.racing = 'false';
if (gain && audio) gain.gain.setTargetAtTime(0, audio.currentTime, 0.1);
}
}
updateRoad();
frameId = requestAnimationFrame(tick);
if (!multiplayer.available) {
$('networkBadge').textContent = 'UNAVAILABLE';
$<HTMLButtonElement>('hostButton').disabled = true;
$<HTMLButtonElement>('joinCode').disabled = true;
}
window.addEventListener('pagehide', () => {
cancelAnimationFrame(frameId);
controllers.dispose();
touchDriving.clear();
void multiplayer.leave();
renderer.dispose();
void audio?.close();
});
