/**
* Ввод: клавиатура (маппинг «действие -> клавиши»), геймпад (стандартная карта)
* и указатель (мышь/тач) в координатах виртуального разрешения.
*/
export interface PointerState {
/** Виртуальные пиксели. */
x: number;
y: number;
down: boolean;
/** Нажато в этом кадре. */
justPressed: boolean;
/** Отпущено в этом кадре. */
justReleased: boolean;
/** true, если это событие тача (для мобильного управления). */
isTouch: boolean;
/** Сколько фиксированных шагов удержана (0 — только нажата). */
downTicks: number;
}
export interface PointerEventHandlers {
onPointerDown?: (p: PointerState) => void;
onPointerUp?: (p: PointerState) => void;
onPointerMove?: (p: PointerState) => void;
}
export class InputManager {
private keysDown = new Set<string>();
private keysPressed = new Set<string>();
private keysReleased = new Set<string>();
private keyActions = new Map<string, string>();
// --- геймпад (стандартная карта кнопок) ---
/** action -> индексы кнопок стандартной карты. */
private padActions = new Map<string, number[]>();
private padDown = new Set<number>();
private padPressed = new Set<number>();
private padReleased = new Set<number>();
private pointer: PointerState = {
x: 0,
y: 0,
down: false,
justPressed: false,
justReleased: false,
isTouch: false,
downTicks: 0
};
private canvas: HTMLCanvasElement;
private virtualWidth: number;
private virtualHeight: number;
private handlers: PointerEventHandlers;
constructor(
canvas: HTMLCanvasElement,
virtualWidth: number,
virtualHeight: number,
handlers: PointerEventHandlers = {}
) {
this.canvas = canvas;
this.virtualWidth = virtualWidth;
this.virtualHeight = virtualHeight;
this.handlers = handlers;
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
canvas.addEventListener('pointerdown', this.onPointerDown);
canvas.addEventListener('pointerup', this.onPointerUp);
canvas.addEventListener('pointermove', this.onPointerMove);
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
}
/** Маппинг: действие -> список кодов клавиш (e.code). */
bindActions(map: Record<string, string[]>): void {
for (const [action, codes] of Object.entries(map)) {
for (const code of codes) {
this.keyActions.set(code, action);
}
}
}
/**
* Маппинг геймпада: действие -> индексы кнопок стандартной карты
* (0=A, 1=B, 8=Select, 9=Start, 12..15 — крестовина).
*/
bindGamepad(map: Record<string, number[]>): void {
for (const [action, buttons] of Object.entries(map)) {
this.padActions.set(action, buttons);
}
}
/**
* Опрос геймпадов. Вызывается движком в начале каждого фиксированного шага
* (до scenes.update).
*/
update(): void {
this.padPressed.clear();
this.padReleased.clear();
if (typeof navigator === 'undefined' || !navigator.getGamepads) return;
for (const pad of navigator.getGamepads()) {
if (!pad) continue;
pad.buttons.forEach((btn, i) => {
const pressed = btn.pressed;
if (pressed && !this.padDown.has(i)) {
this.padPressed.add(i);
}
if (!pressed && this.padDown.has(i)) {
this.padReleased.add(i);
}
if (pressed) {
this.padDown.add(i);
} else {
this.padDown.delete(i);
}
});
break; // опрашиваем первый подключённый геймпад
}
}
/** Оси левого стика (-1..1) первого подключённого геймпада (или null). */
getLeftStick(): { x: number; y: number } | null {
if (typeof navigator === 'undefined' || !navigator.getGamepads) return null;
for (const pad of navigator.getGamepads()) {
if (!pad) continue;
const [x = 0, y = 0] = pad.axes;
return {
x: Math.abs(x) < 0.15 ? 0 : x, // мёртвая зона
y: Math.abs(y) < 0.15 ? 0 : y
};
}
return null;
}
/** Действие активно (зажато): клавиатура или геймпад. */
isActionActive(action: string): boolean {
for (const [code, a] of this.keyActions) {
if (a === action && this.keysDown.has(code)) return true;
}
const padButtons = this.padActions.get(action);
if (padButtons) {
for (const b of padButtons) {
if (this.padDown.has(b)) return true;
}
}
return false;
}
/** Действие нажато именно в этом тике. */
isActionJustPressed(action: string): boolean {
for (const code of this.keysPressed) {
if (this.keyActions.get(code) === action) return true;
}
const padButtons = this.padActions.get(action);
if (padButtons) {
for (const b of padButtons) {
if (this.padPressed.has(b)) return true;
}
}
return false;
}
/** Действие отпущено именно в этом тике (клавиатура или геймпад). */
isActionJustReleased(action: string): boolean {
for (const code of this.keysReleased) {
if (this.keyActions.get(code) === action) return true;
}
const padButtons = this.padActions.get(action);
if (padButtons) {
for (const b of padButtons) {
if (this.padReleased.has(b)) return true;
}
}
return false;
}
isKeyDown(code: string): boolean {
return this.keysDown.has(code);
}
wasKeyPressed(code: string): boolean {
return this.keysPressed.has(code);
}
wasKeyReleased(code: string): boolean {
return this.keysReleased.has(code);
}
getPointer(): Readonly<PointerState> {
return this.pointer;
}
/** Вызывается в конце каждого тика: очищает «just pressed/released», копит downTicks. */
endTick(): void {
this.keysPressed.clear();
this.keysReleased.clear();
this.padPressed.clear();
this.padReleased.clear();
this.pointer.justPressed = false;
this.pointer.justReleased = false;
if (this.pointer.down) {
this.pointer.downTicks += 1;
} else {
this.pointer.downTicks = 0;
}
}
dispose(): void {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
this.canvas.removeEventListener('pointerdown', this.onPointerDown);
this.canvas.removeEventListener('pointerup', this.onPointerUp);
this.canvas.removeEventListener('pointermove', this.onPointerMove);
}
private onKeyDown = (e: KeyboardEvent): void => {
if (!this.keysDown.has(e.code)) {
this.keysPressed.add(e.code);
}
this.keysDown.add(e.code);
};
private onKeyUp = (e: KeyboardEvent): void => {
this.keysDown.delete(e.code);
this.keysReleased.add(e.code);
};
private toVirtual(e: PointerEvent): { x: number; y: number } {
const rect = this.canvas.getBoundingClientRect();
return {
x: ((e.clientX - rect.left) / rect.width) * this.virtualWidth,
y: ((e.clientY - rect.top) / rect.height) * this.virtualHeight
};
}
private onPointerDown = (e: PointerEvent): void => {
const { x, y } = this.toVirtual(e);
this.pointer.x = x;
this.pointer.y = y;
this.pointer.down = true;
this.pointer.justPressed = true;
this.pointer.downTicks = 0;
this.pointer.isTouch = e.pointerType === 'touch';
this.handlers.onPointerDown?.(this.pointer);
};
private onPointerUp = (e: PointerEvent): void => {
const { x, y } = this.toVirtual(e);
this.pointer.x = x;
this.pointer.y = y;
this.pointer.down = false;
this.pointer.justReleased = true;
this.pointer.isTouch = e.pointerType === 'touch';
this.handlers.onPointerUp?.(this.pointer);
};
private onPointerMove = (e: PointerEvent): void => {
const { x, y } = this.toVirtual(e);
this.pointer.x = x;
this.pointer.y = y;
this.handlers.onPointerMove?.(this.pointer);
};
}