SOURCE / PINNED RELEASE
Made of little things.
super-platformer
- Release
- b61593abc2c6…
- Author-recorded commit
- bbd35ca510f6…
- License
- LICENSE
- Author’s source reference
- nostr://npub16uawymqtmdg4j6zcl6snt5apzfp4ek2g8lpasnqa75tspdpglcqsgpatvc/wss%3A%2F%2Fgit.napplet.soy%2F/n-01288b1ab38
Archive hash verified: d2529eb9ad1961f2…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
import type { InputAction } from './game.js';
/** Merge independent devices so releasing one cannot cancel another. */
export class InputSources {
private sources = new Map<string, Set<InputAction>>();
constructor(private target: { press(action: InputAction): void; release(action: InputAction): void }) {}
set(source: string, actions: Iterable<InputAction>): void {
const before = new Set([...this.sources.values()].flatMap((set) => [...set]));
const next = new Set(actions);
if (next.size) this.sources.set(source, next);
else this.sources.delete(source);
const after = new Set([...this.sources.values()].flatMap((set) => [...set]));
for (const action of before) if (!after.has(action)) this.target.release(action);
for (const action of after) if (!before.has(action)) this.target.press(action);
}
clear(): void {
for (const source of this.sources.keys()) this.set(source, []);
}
}
export type ControllerState = 'waiting' | 'connected' | 'unsupported' | 'unavailable';
export class ControllerInput {
private previous = new Set<number>();
private activeIndex: number | null = null;
// Require neutral controls after focus changes / connecting, avoiding accidental actions.
private armed = false;
constructor(private input: InputSources, private commands: {
playing(): boolean;
primary(): void;
pause(): void;
restart(): void;
}) {}
reset(): void {
this.input.set('controller', []);
this.previous.clear();
this.armed = false;
this.activeIndex = null;
}
update(pads: readonly (Gamepad | null)[]): ControllerState {
const supported = pads.filter((pad): pad is Gamepad => !!pad?.connected && pad.mapping === 'standard');
const pad = supported.find((pad) => pad.index === this.activeIndex) ?? supported[0];
if (!pad) {
this.reset();
return pads.some((pad) => pad?.connected) ? 'unsupported' : 'waiting';
}
if (pad.index !== this.activeIndex) {
this.reset();
this.activeIndex = pad.index;
}
const down = new Set(pad.buttons.flatMap((button, index) => button.pressed ? [index] : []));
const axis = pad.axes[0] ?? 0;
if (!this.armed) {
this.armed = down.size === 0 && Math.abs(axis) < 0.25;
this.previous = down;
return 'connected';
}
const pressed = (index: number): boolean => down.has(index) && !this.previous.has(index);
const wasPlaying = this.commands.playing();
let menuAction = false;
if (pressed(9)) {
if (wasPlaying) this.commands.pause();
else this.commands.primary();
menuAction = true;
} else if (!wasPlaying && pressed(0)) {
this.commands.primary();
menuAction = true;
} else if (!wasPlaying && pressed(3)) {
this.commands.restart();
menuAction = true;
}
const actions: InputAction[] = [];
if (wasPlaying && !menuAction) {
if (down.has(14) || axis < -0.25) actions.push('left');
if (down.has(15) || axis > 0.25) actions.push('right');
if (down.has(0)) actions.push('jump');
if (down.has(2)) actions.push('fire');
}
this.input.set('controller', actions);
if (menuAction) this.armed = false;
this.previous = down;
return 'connected';
}
}
