/**
* Ввод: клавиатура (маппинг «действие -> клавиши») и указатель (мышь/тач)
* в координатах виртуального разрешения.
*/
export interface PointerState {
/** Виртуальные пиксели. */
x: number;
y: number;
down: boolean;
/** Нажато в этом кадре. */
justPressed: boolean;
/** Отпущено в этом кадре. */
justReleased: boolean;
/** true, если это событие тача (для мобильного управления). */
isTouch: boolean;
}
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 keyActions = new Map<string, string>();
private pointer: PointerState = {
x: 0,
y: 0,
down: false,
justPressed: false,
justReleased: false,
isTouch: false
};
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);
}
}
}
/** Действие активно (зажато). */
isActionActive(action: string): boolean {
for (const [code, a] of this.keyActions) {
if (a === action && this.keysDown.has(code)) return true;
}
return false;
}
/** Действие нажато именно в этом тике. */
isActionJustPressed(action: string): boolean {
for (const code of this.keysPressed) {
if (this.keyActions.get(code) === action) return true;
}
return false;
}
isKeyDown(code: string): boolean {
return this.keysDown.has(code);
}
wasKeyPressed(code: string): boolean {
return this.keysPressed.has(code);
}
getPointer(): Readonly<PointerState> {
return this.pointer;
}
/** Вызывается в конце каждого тика: очищает «just pressed/released». */
endTick(): void {
this.keysPressed.clear();
this.pointer.justPressed = false;
this.pointer.justReleased = false;
}
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);
};
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.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);
};
}