SOURCE / PINNED RELEASE
Made of little things.
Big Rig Rush
- Release
- ec9b6549f752…
- Author-recorded commit
- 74b729258b1a…
- License
- LICENSE
- Author’s source reference
- nostr://npub1sj2vketepqw9v6fmge5fdzuj7atj9sv6qv2jss05n2z6rl64lcfqumdsnt/wss%3A%2F%2Fgit.napplet.soy%2F/n-0a99f0cae40
Archive hash verified: 900e22810340ccb1…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
export interface Controls {
up: boolean;
down: boolean;
left: boolean;
right: boolean;
/** Held trigger: the A-10's Gatling gun. */
gun: boolean;
/** The A-10's throttle lever: hold M to push it up, N to pull it back. */
throttleUp: boolean;
throttleDown: boolean;
/** Held pickle button: the A-10 ripples its bombs. */
bomb: boolean;
}
export type InputAction = 'confirm' | 'pause' | 'horn' | 'mute' | 'shiftUp' | 'shiftDown' | 'tow' | 'hit' | 'fire' | 'bomb' | 'weapon';
const ACTIONS: InputAction[] = ['horn', 'shiftUp', 'shiftDown', 'tow', 'hit', 'fire', 'bomb', 'weapon'];
const KEYMAP: Record<string, keyof Controls> = {
KeyW: 'up',
ArrowUp: 'up',
KeyS: 'down',
ArrowDown: 'down',
KeyA: 'left',
ArrowLeft: 'left',
KeyD: 'right',
ArrowRight: 'right',
KeyG: 'gun',
KeyM: 'throttleUp',
KeyN: 'throttleDown',
KeyB: 'bomb',
};
/** Keys that are held controls and also fire a one-off action when first pressed. */
const PRESS_ACTIONS: Record<string, InputAction> = { KeyM: 'shiftUp', KeyN: 'shiftDown', KeyB: 'bomb' };
const CONTROL_NAMES: (keyof Controls)[] = ['up', 'down', 'left', 'right', 'gun', 'throttleUp', 'throttleDown', 'bomb'];
/** Merges held keys and held on-screen buttons into one set of controls. */
export class Input {
readonly controls: Controls = { up: false, down: false, left: false, right: false, gun: false, throttleUp: false, throttleDown: false, bomb: false };
private readonly keys = new Set<keyof Controls>();
private controlFor(code: string): keyof Controls | undefined {
return KEYMAP[code];
}
private readonly touches = new Map<keyof Controls, Set<number>>();
constructor(
private readonly onAction: (action: InputAction) => void,
private readonly onTouch: () => void,
) {
window.addEventListener('keydown', (event) => this.keyDown(event));
window.addEventListener('keyup', (event) => {
const control = this.controlFor(event.code);
if (control) {
this.keys.delete(control);
this.refresh();
}
});
}
attachButtons(buttons: Iterable<HTMLElement>): void {
for (const button of buttons) {
const control = button.dataset.control as keyof Controls | undefined;
if (!control || !CONTROL_NAMES.includes(control)) continue;
const held = new Set<number>();
this.touches.set(control, held);
const release = (event: PointerEvent) => {
held.delete(event.pointerId);
button.classList.toggle('held', held.size > 0);
this.refresh();
};
button.addEventListener('pointerdown', (event) => {
event.preventDefault();
if (event.pointerType === 'touch') this.onTouch();
try {
button.setPointerCapture(event.pointerId);
} catch {
// The pointer may already be gone; the release handlers still clean up.
}
held.add(event.pointerId);
button.classList.add('held');
this.refresh();
});
button.addEventListener('pointerup', release);
button.addEventListener('pointercancel', release);
button.addEventListener('lostpointercapture', release);
button.addEventListener('contextmenu', (event) => event.preventDefault());
}
}
/** Tap buttons that fire a one-off action (gear shifts, horn). */
attachActionButtons(buttons: Iterable<HTMLElement>): void {
for (const button of buttons) {
const action = button.dataset.action as InputAction | undefined;
if (!action || !ACTIONS.includes(action)) continue;
button.addEventListener('pointerdown', (event) => {
event.preventDefault();
if (event.pointerType === 'touch') this.onTouch();
button.classList.add('held');
this.onAction(action);
});
const release = () => button.classList.remove('held');
button.addEventListener('pointerup', release);
button.addEventListener('pointercancel', release);
button.addEventListener('pointerleave', release);
button.addEventListener('contextmenu', (event) => event.preventDefault());
}
}
/** Drop everything held, e.g. when the frame loses focus. */
reset(): void {
this.keys.clear();
for (const held of this.touches.values()) held.clear();
document.querySelectorAll('.held').forEach((el) => el.classList.remove('held'));
this.refresh();
}
private keyDown(event: KeyboardEvent): void {
const control = this.controlFor(event.code);
if (control) {
event.preventDefault();
this.keys.add(control);
this.refresh();
const action = PRESS_ACTIONS[event.code];
if (action && !event.repeat) this.onAction(action);
return;
}
if (event.repeat) return;
if (event.code === 'KeyP' || event.code === 'Escape') {
event.preventDefault();
this.onAction('pause');
} else if (event.code === 'KeyH') {
event.preventDefault();
this.onAction('horn');
} else if (event.code === 'KeyR') {
event.preventDefault();
this.onAction('tow');
} else if (event.code === 'KeyF') {
event.preventDefault();
this.onAction('hit');
} else if (event.code === 'KeyV') {
event.preventDefault();
this.onAction('weapon');
} else if (event.code === 'KeyK') {
event.preventDefault();
this.onAction('mute');
} else if (event.code === 'Space' && !(event.target instanceof HTMLButtonElement)) {
event.preventDefault();
this.onAction('fire');
} else if (event.code === 'Enter' || event.code === 'Space') {
// Let a focused button handle its own activation.
if (event.target instanceof HTMLButtonElement) return;
event.preventDefault();
this.onAction('confirm');
}
}
private refresh(): void {
for (const name of CONTROL_NAMES) {
this.controls[name] = this.keys.has(name) || (this.touches.get(name)?.size ?? 0) > 0;
}
}
}
