Files
Binding-Fignyaac/src/input/KeyboardController.ts
T

99 lines
3.9 KiB
TypeScript

import type { InputSource, InputState } from './InputState';
import type { Dir } from '../core/types';
/**
* Раскладка: WASD — движение, стрелки — прицельная стрельба, пробел —
* атака по ходу движения, Tab/Q — смена оружия, R — рестарт.
*
* ВАЖНО: используем `event.code` (ФИЗИЧЕСКАЯ клавиша), а не `event.key`.
* `code` не зависит от раскладки, поэтому WASD/Q/R работают и в русской
* раскладке (где те же клавиши дают «цфыв»/«й»/«к»). Стрелки/Tab/пробел в
* `code` называются ArrowUp/Tab/Space.
*
* Контроллер держит набор зажатых клавиш и «защёлкивает» однократные действия
* (смена оружия/рестарт). Раз в кадр вызывается poll(), который собирает
* InputState и сбрасывает однократные флаги.
*/
export class KeyboardController implements InputSource {
private held = new Set<string>();
private toggleWeaponEdge = false;
private restartEdge = false;
private attached = false;
private onKeyDown = (e: KeyboardEvent): void => {
const c = e.code;
// Однократные действия ловим по факту нажатия (не по удержанию).
if (!this.held.has(c)) {
if (c === 'Tab' || c === 'KeyQ') this.toggleWeaponEdge = true;
if (c === 'KeyR') this.restartEdge = true;
}
this.held.add(c);
if (PREVENT.has(c)) e.preventDefault();
};
private onKeyUp = (e: KeyboardEvent): void => {
this.held.delete(e.code);
};
private onBlur = (): void => {
// Сбрасываем всё, чтобы клавиши не «залипали» при потере фокуса.
this.held.clear();
};
/**
* Сбросить весь ввод: зажатые клавиши и однократные действия. Звать на
* границе забега (старт новой игры) — иначе клавиша, зажатая в прошлом
* забеге, или залатченная смена оружия «перетекут» в новый.
*/
reset(): void {
this.held.clear();
this.toggleWeaponEdge = false;
this.restartEdge = false;
}
/** Подписаться на события окна. Вызывается один раз при старте. */
attach(target: Window = window): void {
if (this.attached) return;
target.addEventListener('keydown', this.onKeyDown);
target.addEventListener('keyup', this.onKeyUp);
target.addEventListener('blur', this.onBlur);
this.attached = true;
}
/** Собрать снимок ввода и сбросить однократные флаги. */
poll(): InputState {
const down = (code: string) => this.held.has(code);
let moveX = 0;
let moveY = 0;
if (down('KeyW')) moveY -= 1;
if (down('KeyS')) moveY += 1;
if (down('KeyA')) moveX -= 1;
if (down('KeyD')) moveX += 1;
let aimDir: Dir | null = null;
if (down('ArrowUp')) aimDir = 'up';
else if (down('ArrowDown')) aimDir = 'down';
else if (down('ArrowLeft')) aimDir = 'left';
else if (down('ArrowRight')) aimDir = 'right';
const snapshot: InputState = {
moveX,
moveY,
aimDir,
attackHeld: down('Space'),
toggleWeapon: this.toggleWeaponEdge,
restart: this.restartEdge,
};
this.toggleWeaponEdge = false;
this.restartEdge = false;
return snapshot;
}
}
/** Физические клавиши (e.code), у которых гасим поведение браузера (скролл/таб). */
const PREVENT = new Set([
'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', 'Space', 'Tab',
]);