SOURCE / PINNED RELEASE
Made of little things.
Pickle Rustlers
- Release
- 039e51f55e34…
- Author-recorded commit
- 40608586a8a1…
- License
- LICENSE
- Author’s source reference
- nostr://npub12lm3rxsva8kfempyjkxupt09e02n3fezurkyadyqfejdc5e0r2kscqty6r/wss%3A%2F%2Fgit.napplet.soy%2F/n-bad44e7ab35
Archive hash verified: f38e99afa0992052…. The source-to-build association is the author’s claim; it has not been independently rebuilt.
// Keyboard, touch and gamepad merged into one set of named actions per frame.
import { KeyboardInput, type KeyboardAction } from './keyboard.js';
import { createGamepadInput, type GamepadBindings } from './gamepad.js';
import { createShellGamepadSource } from './gamepad-source.js';
export interface InputFrame {
throttle: number; // -1 (brake / reverse) .. 1 (gallop)
steer: number; // -1 left .. 1 right
shoot: boolean; // pressed this frame
lasso: boolean;
pause: boolean;
confirm: boolean; // start / continue
}
const bindings: GamepadBindings = {
moveX: [{ axis: 0 }, { button: 14, scale: -1 }, { button: 15 }],
moveY: [{ axis: 1 }, { button: 12, scale: -1 }, { button: 13 }],
gas: [{ button: 7 }],
brake: [{ button: 6 }],
shoot: [{ button: 0 }, { button: 5 }],
lasso: [{ button: 2 }, { button: 1 }, { button: 4 }],
pause: [{ button: 9 }],
};
export class Input {
private keyboard: KeyboardInput;
private listeners = new AbortController();
private stick = { x: 0, y: 0, id: -1 };
private touchShoot = false;
private touchLasso = false;
private controllerSource = createShellGamepadSource(() => this.gamepad?.reset());
private gamepad = createGamepadInput(bindings, this.controllerSource);
touchSeen = false;
constructor(
private stickEl: HTMLElement,
private knobEl: HTMLElement,
shootBtn: HTMLElement,
lassoBtn: HTMLElement,
private onActivity: (kind: 'touch' | 'keys') => void,
allowAction: (action: KeyboardAction) => boolean,
) {
// KeyboardInput ignores keys typed into text fields, such as the score-card note.
this.keyboard = new KeyboardInput(() => this.onActivity('keys'), allowAction);
const signal = this.listeners.signal;
window.addEventListener('blur', () => this.clear(), { signal });
document.addEventListener('visibilitychange', () => this.clear(), { signal });
stickEl.addEventListener('pointerdown', (e) => {
e.preventDefault();
this.stick.id = e.pointerId;
stickEl.setPointerCapture(e.pointerId);
this.moveStick(e);
this.markTouch();
}, { signal });
stickEl.addEventListener('pointermove', (e) => {
if (e.pointerId === this.stick.id) this.moveStick(e);
}, { signal });
const endStick = (e: PointerEvent) => {
if (e.pointerId !== this.stick.id) return;
this.stick = { x: 0, y: 0, id: -1 };
this.knobEl.style.transform = '';
};
stickEl.addEventListener('pointerup', endStick, { signal });
stickEl.addEventListener('pointercancel', endStick, { signal });
stickEl.addEventListener('lostpointercapture', endStick, { signal });
const button = (el: HTMLElement, set: () => void) => {
el.addEventListener('pointerdown', (e) => {
e.preventDefault();
set();
el.classList.add('down');
this.markTouch();
}, { signal });
const up = () => el.classList.remove('down');
el.addEventListener('pointerup', up, { signal });
el.addEventListener('pointercancel', up, { signal });
el.addEventListener('pointerleave', up, { signal });
};
button(shootBtn, () => (this.touchShoot = true));
button(lassoBtn, () => (this.touchLasso = true));
}
private markTouch(): void {
this.touchSeen = true;
this.onActivity('touch');
}
private moveStick(e: PointerEvent): void {
const rect = this.stickEl.getBoundingClientRect();
const r = rect.width / 2;
let x = (e.clientX - rect.left - r) / r;
let y = (e.clientY - rect.top - r) / r;
const len = Math.hypot(x, y);
if (len > 1) {
x /= len;
y /= len;
}
this.stick.x = x;
this.stick.y = y;
this.knobEl.style.transform = `translate(${x * r * 0.55}px, ${y * r * 0.55}px)`;
}
clear(): void {
this.keyboard.clear();
this.stick = { x: 0, y: 0, id: -1 };
this.knobEl.style.transform = '';
this.touchShoot = this.touchLasso = false;
this.gamepad.reset();
}
dispose(): void {
this.listeners.abort();
this.keyboard.dispose();
this.controllerSource.dispose();
this.gamepad.dispose();
this.clear();
}
read(): InputFrame {
const pressed = this.keyboard.read();
let throttle = (this.keyboard.down('w', 'arrowup') ? 1 : 0) - (this.keyboard.down('s', 'arrowdown') ? 1 : 0);
let steer = (this.keyboard.down('d', 'arrowright') ? 1 : 0) - (this.keyboard.down('a', 'arrowleft') ? 1 : 0);
let shoot = pressed.has('shoot');
let lasso = pressed.has('lasso');
let pause = pressed.has('pause');
let confirm = shoot;
if (this.stick.id !== -1) {
// Gentle curve so small thumb movements steer precisely.
const sx = this.stick.x;
steer = Math.sign(sx) * Math.min(1, Math.abs(sx) ** 1.4 * 1.3);
throttle = Math.max(-1, Math.min(1, -this.stick.y * 1.25));
}
if (this.touchShoot) shoot = confirm = true;
if (this.touchLasso) lasso = true;
const frame = this.gamepad.poll();
for (const pad of frame.players) {
const a = pad.actions;
if (Math.abs(a.moveX.value) > Math.abs(steer)) steer = a.moveX.value;
const padThrottle = a.gas.value - a.brake.value || -a.moveY.value;
if (Math.abs(padThrottle) > Math.abs(throttle)) throttle = padThrottle;
if (a.shoot.pressed) shoot = confirm = true;
if (a.lasso.pressed) lasso = true;
if (a.pause.pressed) pause = true;
}
this.touchShoot = this.touchLasso = false;
return { throttle, steer, shoot, lasso, pause, confirm };
}
}
