Newer
Older
rpg / packages / engine / src / ui / MenuSceneBase.ts
import { Container } from 'pixi.js';
import type { Scene } from '../scene/SceneManager';
import type { MenuList } from './MenuList';
import { readMenuInput, type MenuActionMap } from './menuInput';

/**
 * Базовая сцена-меню: панель поверх вызывающей сцены (push/pop) с клавиатурной
 * навигацией. Устраняет дублирование «up/down/confirm/cancel → moveCursor/
 * activate + гард перехода» из четырёх сцен игры. Игра строит панель и меню
 * своим стилем в build(); ввод читается по карте действий сцены.
 */
export interface MenuSceneContext {
    /** Ввод (InputManager или заглушка). */
    input: { isActionJustPressed(action: string): boolean };
    /** true, пока стек сцен в переходе — ввод блокируется. */
    inputBlocked(): boolean;
}

export abstract class MenuSceneBase implements Scene {
    /** Корень вью сцены; build() добавляет его в uiRoot. */
    protected readonly view = new Container();
    /** Список меню (null — сцена без списка, например просмотр сумки). */
    protected menu: MenuList | null = null;

    protected constructor(
        private readonly ctx: MenuSceneContext,
        private readonly actions: MenuActionMap
    ) {}

    /** Построить панель/заголовок/меню и добавить this.view в uiRoot. */
    protected abstract build(): void;

    enter(): void {
        this.build();
    }

    exit(): void {
        this.view.destroy({ children: true });
        this.menu = null;
    }

    render(): void {}

    update(_dt: number): void {
        if (this.ctx.inputBlocked()) return;
        const index = this.menu?.index ?? 0;
        for (const ev of readMenuInput(this.ctx.input, index, this.actions)) {
            switch (ev.kind) {
                case 'move':
                    this.onMove(ev.delta, ev.index);
                    break;
                case 'adjust':
                    this.onAdjust(ev.delta, ev.index);
                    break;
                case 'confirm':
                    this.onConfirm(ev.index);
                    break;
                case 'cancel':
                    this.onCancel();
                    break;
                case 'action':
                    this.onAction(ev.action, ev.index);
                    break;
            }
        }
    }

    /** Дефолт: двигать курсор списка. */
    protected onMove(delta: number, _index: number): void {
        this.menu?.moveCursor(delta);
    }

    /** Подстройка значения выбранной строки (настройки); по умолчанию — ничего. */
    protected onAdjust(_delta: number, _index: number): void {}

    /** Дефолт: активировать кнопку под фокусом. */
    protected onConfirm(_index: number): void {
        this.menu?.activate();
    }

    /** Назад/закрыть; по умолчанию — ничего (главное меню не закрывается). */
    protected onCancel(): void {}

    /** Прочее действие сцены (extra из карты); по умолчанию — ничего. */
    protected onAction(_action: string, _index: number): void {}
}