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.
import type { App } from '../app.js';
import { assetDescriptor, OnlineSession, privateAvailable, publicAvailable, type OnlineMode } from '../onlineSession.js';
import { formatCode, parsePrivateCode, privateCode } from '../onlineProtocol.js';
import { localProfilePubkey } from '../onlineNames.js';
import { loadHostAssets } from '../onlineAssets.js';
import { focusTargets, padPress, stepFocus } from './screens.js';
import type { MenuIntent } from '../input.js';
const element = <K extends keyof HTMLElementTagNameMap>(tag: K, text = '') => {
const node = document.createElement(tag);
node.textContent = text;
return node;
};
/** App-owned arcade controls, outside the original bitmap's scaled stage. */
export class OnlineUI {
private readonly entries = element('div');
private readonly dialog = element('div');
private readonly card = element('section');
private readonly input = element('input');
private readonly label = element('label', 'Room code');
private readonly title = element('h2');
private readonly message = element('p');
private readonly joinButton = element('button', 'Join room');
private readonly createButton = element('button', 'New room');
private readonly cancel = element('button', 'Cancel');
private readonly hud = element('div');
private readonly status = element('span');
private readonly invite = element('input');
private session: OnlineSession | null = null;
private generation = 0;
private previousFocus: HTMLElement | null = null;
private publicMode = false;
private assetsLoading = false;
onAssetsLoaded: () => void = () => {};
private entryButtons: Array<[HTMLButtonElement, () => boolean]> = [];
private codeControls: HTMLElement[] = [];
constructor(host: HTMLElement, private readonly app: App) {
this.entries.className = 'online-entry';
const play = element('button', 'Play public');
const room = element('button', 'Private room');
// Hosts without the optional domains simply get no online entry points.
this.entryButtons = ([[play, publicAvailable], [room, privateAvailable]] as Array<[HTMLButtonElement, () => boolean]>).filter(([, available]) => available());
this.enableEntries();
play.addEventListener('click', () => { this.showDialog(true); void this.begin({ kind: 'public' }); });
room.addEventListener('click', () => this.showPrivate());
this.entries.append(...this.entryButtons.map(([button]) => button));
this.dialog.className = 'online-dialog';
this.dialog.hidden = true;
this.card.className = 'online-card';
this.card.setAttribute('role', 'dialog');
this.card.setAttribute('aria-modal', 'true');
this.card.setAttribute('aria-labelledby', 'online-title');
this.title.id = 'online-title';
this.input.id = 'online-code';
this.input.autocomplete = 'off';
this.input.spellcheck = false;
this.input.placeholder = 'ABC-DEF';
this.input.maxLength = 9;
this.input.autocapitalize = 'characters';
this.input.addEventListener('input', () => { this.joinButton.disabled = !this.validCode(); });
this.label.htmlFor = this.input.id;
this.message.setAttribute('role', 'status');
this.joinButton.addEventListener('click', () => this.joinCode(this.input.value));
this.createButton.addEventListener('click', () => this.joinCode(privateCode()));
this.cancel.addEventListener('click', () => this.dismiss());
this.card.append(this.title, this.label, this.input, this.message, this.joinButton, this.createButton, this.cancel);
this.dialog.append(this.card);
this.dialog.addEventListener('keydown', e => {
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); this.dismiss(); }
else if (e.key === 'Enter' && e.target === this.input && !this.joinButton.disabled) { e.preventDefault(); this.joinCode(this.input.value); }
else if (e.key === 'Tab') {
const controls = focusTargets(this.card, 'button, input');
const first = controls[0], last = controls.at(-1);
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last?.focus(); }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first?.focus(); }
}
});
this.hud.className = 'online-hud';
this.hud.hidden = true;
this.status.setAttribute('role', 'status');
this.invite.readOnly = true;
this.invite.setAttribute('aria-label', 'Room code');
this.invite.addEventListener('click', () => this.invite.select());
const select = element('button', 'Select code');
select.addEventListener('click', () => { this.invite.focus(); this.invite.select(); });
const leave = element('button', 'Leave online');
leave.addEventListener('click', () => this.end('Left online game. Driving offline.'));
this.codeControls = [this.invite, select];
const controls = element('div');
controls.append(this.invite, select, leave);
this.hud.append(this.status, controls);
host.append(this.entries, this.dialog, this.hud);
this.entries.hidden = true;
app.onPlayingChange = playing => { this.entries.hidden = playing || !this.entryButtons.length; };
app.onOnlineLeave = () => {
this.session = null;
this.hud.hidden = true;
};
app.onAssetsReload = () => {
++this.generation;
void this.session?.close();
this.session = null;
this.dialog.hidden = this.hud.hidden = true;
this.resetControls();
};
window.addEventListener('pagehide', () => { void this.session?.close(); });
}
navigate(intent: MenuIntent): boolean {
if (this.dialog.hidden) return false;
if (intent === 'back') this.dismiss();
else if (intent === 'select' || intent === 'start') {
const focused = document.activeElement;
if (focused instanceof HTMLButtonElement && !focused.disabled) padPress(focused);
} else stepFocus(focusTargets(this.card, 'button, input'), intent);
return true;
}
private enableEntries(): void {
for (const [button, available] of this.entryButtons) button.disabled = !available();
}
private validCode(): boolean {
try { parsePrivateCode(this.input.value); return true; } catch { return false; }
}
private resetControls(): void {
this.input.disabled = this.createButton.disabled = false;
this.joinButton.disabled = !this.validCode();
}
private showPrivate(): void {
this.showDialog(false);
this.input.focus();
}
private showDialog(publicMode: boolean): void {
this.publicMode = publicMode;
this.title.textContent = publicMode ? 'Public Free Driving' : 'Private room';
this.label.hidden = this.input.hidden = this.joinButton.hidden = this.createButton.hidden = publicMode;
this.previousFocus = document.activeElement as HTMLElement;
this.dialog.hidden = false;
this.message.textContent = 'Type a friend’s room code, or start a new room and share its code. Anyone with the code can join, up to 8 players. The room’s asset pack and custom level download automatically when needed.';
this.resetControls();
}
private dismiss(): void {
++this.generation;
if (this.session && !this.session.ready) { void this.session.close(); this.session = null; }
this.dialog.hidden = true;
this.resetControls();
this.previousFocus?.focus();
}
private joinCode(value: string): void {
let code: string;
try { code = parsePrivateCode(value); }
catch (error) { this.message.textContent = (error as Error).message; this.input.focus(); return; }
this.input.value = formatCode(code);
void this.begin({ kind: 'private', code });
}
private async begin(mode: OnlineMode): Promise<void> {
if (this.app.racing || this.assetsLoading || this.session) return;
const generation = ++this.generation;
this.dialog.hidden = false;
this.message.textContent = mode.kind === 'public' ? 'Finding a public room…' : `Connecting to room ${formatCode(mode.code)}…`;
this.input.disabled = this.joinButton.disabled = this.createButton.disabled = true;
this.cancel.focus();
try {
const [descriptor, profile] = await Promise.all([assetDescriptor(), localProfilePubkey()]);
if (generation !== this.generation) return;
const session = new OnlineSession(descriptor, mode);
session.profile = profile;
this.session = session;
session.onAssetsRequired = async (target, isActive) => {
this.assetsLoading = true;
const entries = Array.from(this.entries.querySelectorAll('button'));
entries.forEach(button => { button.disabled = true; });
this.message.textContent = 'Downloading and loading the room’s game data…';
try {
return await loadHostAssets(target, isActive, async () => {
await this.app.reloadAssets({ preserveOnlineJoin: true });
this.onAssetsLoaded();
if (!this.dialog.hidden && this.session === session) this.cancel.focus();
});
} finally {
this.assetsLoading = false;
this.enableEntries();
}
};
session.onReady = () => {
if (this.session !== session) return;
this.app.onStatus('');
this.resetControls();
this.dialog.hidden = true;
if (this.card.contains(document.activeElement)) (document.activeElement as HTMLElement).blur();
this.invite.value = session.code;
for (const control of this.codeControls) control.hidden = !session.code;
this.hud.hidden = false;
this.status.textContent = session.status;
this.app.startOnline(session);
};
session.onChange = () => {
if (this.session !== session) return;
this.status.textContent = session.status;
if (!session.ready) this.message.textContent = session.status;
};
session.onEnd = reason => {
if (this.session !== session) return;
const wasReady = session.ready;
this.end(wasReady ? reason : '');
if (!wasReady) {
this.dialog.hidden = false;
this.message.textContent = reason;
this.resetControls();
(this.publicMode ? this.cancel : this.input).focus();
}
};
await session.start();
} catch (error) {
if (generation !== this.generation) return;
this.session = null;
this.message.textContent = error instanceof Error ? error.message : String(error);
this.resetControls();
(this.publicMode ? this.cancel : this.input).focus();
}
}
private end(reason: string): void {
const session = this.session;
this.session = null;
if (session) void session.close();
this.app.leaveOnline();
this.hud.hidden = true;
this.app.onStatus(reason);
}
}
