Newer
Older
rpg / packages / engine / src / input / InputManager.ts
/**
 * Ввод: клавиатура (маппинг «действие -> клавиши») и указатель (мышь/тач)
 * в координатах виртуального разрешения.
 */

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);
    };
}