Newer
Older
rpg / apps / game / src / systems / DialogueSystem.ts
import {
    Container,
    DialogueBox,
    DialogueRunner,
    GameState,
    type DialogueBoxOptions,
    type DialogueGraph
} from '@rpg/engine';

/**
 * Диалоги игры: DialogueRunner движка ходит по графам и применяет флаги
 * к GameState, DialogueBox рисует реплики и варианты.
 */
export class DialogueSystem {
    private box: DialogueBox;
    private runner: DialogueRunner;

    /** Сюжетное событие «диалог завершён» — для триггеров/квестов. */
    onDialogueFinished: ((id: string) => void) | null = null;
    /** Звук на каждую новую реплику (колокольчик). */
    onLineShown: (() => void) | null = null;
    /** id стартованного диалога (для onFinish). */
    private currentId: string | null = null;

    constructor(uiRoot: Container, state: GameState, options: DialogueBoxOptions) {
        this.box = new DialogueBox({
            ...options,
            onChoice: (index) => this.runner.pick(index)
        });
        uiRoot.addChild(this.box.view);

        this.runner = new DialogueRunner(state);
        this.runner.setView({
            show: ({ speaker, text, choices }) => {
                this.box.show({ speaker, text, choices });
                this.onLineShown?.();
            },
            hide: () => this.box.hide()
        });
    }

    get active(): boolean {
        return this.runner.active;
    }

    /** Начать диалог по ключу из data/dialogues.ts. */
    start(graph: DialogueGraph, id: string): void {
        this.currentId = id;
        this.runner.onFinish = () => {
            const id = this.currentId;
            this.currentId = null;
            if (id) this.onDialogueFinished?.(id);
        };
        this.runner.start(graph);
    }

    /** Клик/пробел: следующая реплика (во время выбора игнорируется). */
    advance(): void {
        this.runner.advance();
    }

    /** Выбрать показанный вариант (агентный мост; индексы — из agentState.choices). */
    pickChoice(index: number): void {
        this.runner.pick(index);
    }

    /** Состояние диалога для агентного моста (снапшот; не для логики). */
    get agentState(): {
        id: string | null;
        nodeId: string | null;
        speaker: string | null;
        text: string | null;
        choices: string[];
        waitingForChoice: boolean;
    } | null {
        if (!this.runner.active) return null;
        const node = this.runner.node;
        return {
            id: this.currentId,
            nodeId: this.runner.nodeId,
            speaker: node?.speaker ?? null,
            text: node?.text ?? null,
            choices: this.runner.choices.map((c) => c.text),
            waitingForChoice: this.runner.waitingForChoice
        };
    }
}