SOURCE / PINNED RELEASE
Made of little things.
Napplet Machines V8
- Release
- a48ffab2ae1e…
- Author-recorded commit
- 2a817e4d389e…
- License
- LICENSE
- Author’s source reference
- nostr://npub182jczunncwe0jn6frpqwq3e0qjws7yqqnc3auccqv9nte2dnd63scjm4rf/wss%3A%2F%2Fgit.napplet.soy%2F/n-b5572362d4a
Archive hash verified: ecd5161370161eeb…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import type { Controller, ControllerFrame } from '../docs/examples/gamepad.js';
import { idleInput, type Driver, type Input } from './physics.js';
export type InputMethod = 'wasd' | 'arrows' | 'controller' | 'touch';
export type LocalPlayer = {
id: string;
name: string;
car: number;
method: InputMethod;
controller: number | null;
};
export const inputMethods: Record<InputMethod, string> = {
wasd: 'WASD', arrows: 'Arrow keys', controller: 'Controller', touch: 'Touch',
};
export const inputHints: Record<InputMethod, string> = {
wasd: 'WASD drive · Space drift · Left Shift boost · R recover',
arrows: 'Arrows drive · Enter drift · Right Shift boost · Backspace recover',
controller: 'Stick steer · RT / LT gas / brake · B / ○ drift · X / □ boost · Y / △ recover',
touch: 'Steering + pedals · hold Boost · Recover at the top',
};
export function keyboardDriving(keys: Set<string>, method: InputMethod, solo = false): Input {
const input = idleInput();
if (method !== 'wasd' && method !== 'arrows') return input;
const wasd = method === 'wasd' || solo, arrows = method === 'arrows' || solo;
const down = (a: string, b: string) => (wasd && keys.has(a)) || (arrows && keys.has(b));
input.throttle = Number(down('KeyW', 'ArrowUp')) - Number(down('KeyS', 'ArrowDown'));
input.steer = Number(down('KeyA', 'ArrowLeft')) - Number(down('KeyD', 'ArrowRight'));
input.brake = down('Space', 'Enter') || (wasd && keys.has('ControlLeft')) || (arrows && keys.has('ControlRight'));
input.boost = down('ShiftLeft', 'ShiftRight');
return input;
}
export function controllerDriving(pad?: Controller): Input {
if (!pad?.mapped) return idleInput();
const a = pad.actions;
return {
throttle: (a.gas?.value || 0) - (a.serviceBrake?.value || 0),
steer: a.steer?.value || 0,
brake: !!a.drift?.down,
boost: !!a.boost?.down,
reset: false,
};
}
// Browser indices can have gaps. A disconnected/replaced slot requires a new
// button edge before it controls its previous player's car again.
export function assignControllers(players: LocalPlayer[], frame: Pick<ControllerFrame, 'players' | 'disconnected'>) {
const lost: string[] = [], joined: string[] = [];
for (const player of players) {
if (player.controller !== null && frame.disconnected.includes(player.controller)) {
player.controller = null;
lost.push(player.id);
}
}
for (const pad of frame.players) {
if (!pad.mapped || !pad.actions.confirm?.pressed || players.some(p => p.controller === pad.index)) continue;
const waiting = players.find(p => p.method === 'controller' && p.controller === null);
if (waiting) {
waiting.controller = pad.index;
joined.push(waiting.id);
}
}
return { lost, joined };
}
export function localRoster(players: LocalPlayer[], count: number): Driver[] {
const roster: Driver[] = players.map(p => ({ id: p.id, name: p.name, car: p.car, ai: false }));
const names = ['Pip', 'Mochi', 'Rivet', 'Nori', 'Bolt'];
for (let i = roster.length; i < Math.max(players.length, count); i++) {
roster.push({ id: `ai-${i}`, name: names[i - players.length], car: (i + players[0].car) % 4, ai: true });
}
return roster;
}
