import {
Container,
DialogueBox,
DialogueRunner,
GameState,
type DialogueBoxOptions,
type DialogueGraph
} from '@rpg/engine';
import { applyDialogueEffect, type DialogueEffectSink } from './DialogueEffects';
/**
* Диалоги игры: DialogueRunner движка ходит по графам и применяет флаги
* к GameState, DialogueBox рисует реплики и варианты, do[]-эффекты идут
* в зарегистрированный EffectSink.
*/
export class DialogueSystem {
private box: DialogueBox;
private runner: DialogueRunner;
private sink: DialogueEffectSink | null = null;
/** Сюжетное событие «диалог завершён» — для триггеров/квестов. */
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, mood, choices }) => {
this.box.show({ speaker, text, mood, choices });
this.onLineShown?.();
},
hide: () => this.box.hide()
});
this.runner.onEffect = (op) => {
if (this.sink) applyDialogueEffect(op, this.sink);
};
}
/** Зарегистрировать приёмник do[]-эффектов (сцена с геймплеем). */
registerSink(sink: DialogueEffectSink): void {
this.sink = sink;
}
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 {
if (this.box.revealing) {
this.box.skipReveal();
return;
}
if (this.runner.waitingForChoice) {
this.box.activate();
return;
}
this.runner.advance();
}
/** Листать варианты up/down (клавиатура/геймпад). */
moveCursor(delta: number): void {
this.box.moveCursor(delta);
}
/** Печать текста; звать из update сцены. */
update(dt: number): void {
this.box.update(dt);
}
/** Выбрать показанный вариант (агентный мост; индексы — из agentState.choices). */
pickChoice(index: number): void {
this.runner.pick(index);
}
/** Выбрать вариант по тексту (агентный мост). false — такого варианта нет. */
pickChoiceByText(text: string): boolean {
const index = this.runner.choices.findIndex((c) => c.text === text);
if (index < 0) return false;
this.runner.pick(index);
return true;
}
/** Состояние диалога для агентного моста (снапшот; не для логики). */
get agentState(): {
id: string | null;
nodeId: string | null;
speaker: string | null;
text: string | null;
mood: string | null;
tags: string[];
choices: string[];
path: 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,
mood: node?.mood ?? null,
tags: node?.tags ?? [],
choices: this.runner.choices.map((c) => c.text),
path: this.runner.path,
waitingForChoice: this.runner.waitingForChoice
};
}
}