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 {
createGamepadInput,
type GamepadBindings,
} from '../docs/examples/gamepad.js';
import { idleInput, type Input } from './physics.js';
import { clamp } from './track.js';
export const racingBindings: GamepadBindings = {
steer: [{ axis: 0, scale: -1 }, { button: 14 }, { button: 15, scale: -1 }],
gas: [{ button: 7 }, { button: 0 }],
serviceBrake: [{ button: 6 }],
drift: [{ button: 1 }],
recover: [{ button: 3 }],
confirm: [{ button: 0 }],
pause: [{ button: 9 }],
previousCar: [{ button: 4 }],
nextCar: [{ button: 5 }],
previousTrack: [{ button: 12 }],
nextTrack: [{ button: 13 }],
};
export class ControllerDriving {
private pad;
selected: number | null = null;
constructor(options?: Parameters<typeof createGamepadInput>[1]) {
this.pad = createGamepadInput(racingBindings, options);
}
poll() {
const frame = this.pad.poll();
const disconnected =
this.selected !== null && frame.disconnected.includes(this.selected);
if (disconnected) this.selected = null;
// Claim a slot deliberately. Another pad cannot steal the car, and a
// replacement in the same browser slot must rejoin with a fresh press.
let joined = false;
if (this.selected === null) {
const joining = frame.players.find(
(p) => p.mapped && p.actions.confirm.pressed,
);
if (joining) {
this.selected = joining.index;
joined = true;
}
}
const active = frame.players.find((p) => p.index === this.selected);
const a = active?.actions;
const drive: Input = {
throttle: (a?.gas.value || 0) - (a?.serviceBrake.value || 0),
steer: a?.steer.value || 0,
brake: a?.drift.down || false,
reset: false,
};
return {
drive,
disconnected,
joined,
status: frame.status,
selected: this.selected,
unmapped: frame.players.some((p) => !p.mapped),
confirm: !!a?.confirm.pressed,
back: !!a?.drift.pressed,
pause: !!a?.pause.pressed,
recover: !!a?.recover.pressed,
carStep: Number(!!a?.nextCar.pressed) - Number(!!a?.previousCar.pressed),
trackStep:
Number(!!a?.nextTrack.pressed) - Number(!!a?.previousTrack.pressed),
};
}
reset() {
this.pad.reset();
}
dispose() {
this.pad.dispose();
}
}
export function mixDriving(...sources: Input[]): Input {
// Strongest steering wins; opposing equal-strength inputs cancel.
const signed = (key: 'steer' | 'throttle') => {
const values = sources.map((s) => s[key]);
const positive = Math.max(0, ...values),
negative = Math.min(0, ...values);
return positive === -negative
? 0
: positive > -negative
? positive
: negative;
};
return {
steer: signed('steer'),
throttle: signed('throttle'),
brake: sources.some((s) => s.brake),
reset: sources.some((s) => s.reset),
};
}
export function steeringAt(x: number, left: number, width: number) {
return clamp((left + width / 2 - x) / Math.max(1, width / 2 - 26), -1, 1);
}
export function pedalAt(x: number, y: number, width: number, height: number) {
if (x < 0 || y < 0 || x > width || y > height) return '';
return y < height * 0.5 ? 'go' : x < width * 0.5 ? 'stop' : 'drift';
}
export class TouchDriving {
value = idleInput();
private steeringPointer: number | null = null;
private pedalPointer: number | null = null;
constructor(
private steering: HTMLElement,
private pedals: HTMLElement,
) {
const steer = (e: PointerEvent) => {
const r = steering.getBoundingClientRect();
this.setSteering(steeringAt(e.clientX, r.left, r.width));
};
steering.addEventListener('pointerdown', (e) => {
if (this.steeringPointer !== null) return;
e.preventDefault();
steering.focus({ preventScroll: true });
this.steeringPointer = e.pointerId;
steering.setPointerCapture(e.pointerId);
steer(e);
steering.classList.add('pressed');
});
steering.addEventListener('pointermove', (e) => {
if (e.pointerId === this.steeringPointer) steer(e);
});
const releaseSteer = (e: PointerEvent) => {
if (e.pointerId !== this.steeringPointer) return;
this.steeringPointer = null;
this.setSteering(0);
steering.classList.remove('pressed');
};
for (const event of ['pointerup', 'pointercancel', 'lostpointercapture'])
steering.addEventListener(event, releaseSteer as EventListener);
steering.addEventListener('keydown', (e) => {
if (!['ArrowLeft', 'ArrowRight', 'Home'].includes(e.key)) return;
e.preventDefault();
e.stopPropagation();
this.setSteering(
e.key === 'Home'
? 0
: clamp(
this.value.steer + (e.key === 'ArrowLeft' ? 0.15 : -0.15),
-1,
1,
),
);
});
steering.addEventListener('keyup', (e) => {
if (['ArrowLeft', 'ArrowRight', 'Home'].includes(e.key)) {
e.stopPropagation();
this.setSteering(0);
}
});
steering.addEventListener('blur', () => {
if (this.steeringPointer === null) this.setSteering(0);
});
const pedal = (e: PointerEvent) => {
const r = pedals.getBoundingClientRect();
this.setPedal(
pedalAt(e.clientX - r.left, e.clientY - r.top, r.width, r.height),
);
};
pedals.addEventListener('pointerdown', (e) => {
if (this.pedalPointer !== null) return;
e.preventDefault();
this.pedalPointer = e.pointerId;
pedals.setPointerCapture(e.pointerId);
pedal(e);
});
pedals.addEventListener('pointermove', (e) => {
if (e.pointerId === this.pedalPointer) pedal(e);
});
const releasePedal = (e: PointerEvent) => {
if (e.pointerId !== this.pedalPointer) return;
this.pedalPointer = null;
this.setPedal('');
};
for (const event of ['pointerup', 'pointercancel', 'lostpointercapture'])
pedals.addEventListener(event, releasePedal as EventListener);
pedals.querySelectorAll<HTMLButtonElement>('button').forEach((button) => {
button.addEventListener('keydown', (e) => {
if (![' ', 'Enter'].includes(e.key)) return;
e.preventDefault();
e.stopPropagation();
this.setPedal(button.dataset.drive!);
});
button.addEventListener('keyup', (e) => {
if ([' ', 'Enter'].includes(e.key)) {
e.stopPropagation();
this.setPedal('');
}
});
button.addEventListener('blur', () => {
if (this.pedalPointer === null) this.setPedal('');
});
});
}
private setSteering(value: number) {
this.value.steer = value;
const travel = Math.max(1, this.steering.clientWidth / 2 - 26);
this.steering.style.setProperty('--thumb-x', `${-value * travel}px`);
this.steering.setAttribute(
'aria-valuenow',
String(Math.round(value * 100)),
);
this.steering.setAttribute(
'aria-valuetext',
value === 0
? 'Centred'
: `${Math.round(Math.abs(value) * 100)}% ${value > 0 ? 'left' : 'right'}`,
);
}
private setPedal(mode: string) {
this.value.throttle =
mode === 'go' || mode === 'drift' ? 1 : mode === 'stop' ? -1 : 0;
this.value.brake = mode === 'drift';
this.pedals.querySelectorAll<HTMLElement>('[data-drive]').forEach((b) => {
b.classList.toggle('pressed', b.dataset.drive === mode);
b.setAttribute('aria-pressed', String(b.dataset.drive === mode));
});
}
clear() {
const steeringId = this.steeringPointer,
pedalId = this.pedalPointer;
this.steeringPointer = null;
this.pedalPointer = null;
if (steeringId !== null && this.steering.hasPointerCapture(steeringId))
this.steering.releasePointerCapture(steeringId);
if (pedalId !== null && this.pedals.hasPointerCapture(pedalId))
this.pedals.releasePointerCapture(pedalId);
this.setSteering(0);
this.setPedal('');
this.steering.classList.remove('pressed');
}
}
