import { Container, Graphics } from 'pixi.js';
import { PixelText } from './PixelText';
/**
* Универсальное окно диалога: имя говорящего, текст, варианты ответа, подсказка «далее».
* Жанронезависимо: игра сама решает, чей это диалог и что идёт дальше.
* Подходит как view для DialogueRunner.
*/
export interface DialogueLine {
speaker: string;
text: string;
}
export interface DialogueChoiceItem {
index: number;
text: string;
}
export interface DialogueBoxOptions {
/** Виртуальные пиксели. */
width: number;
height: number;
/** Отступы панели от краёв экрана. */
margin: number;
/** Выбор варианта ответа (из DialogueRunner.pick). */
onChoice?: (index: number) => void;
/** Размер шрифта (виртуальные пиксели). */
size?: number;
}
export class DialogueBox {
readonly view: Container;
visible = false;
private panel: Graphics;
private nameText: PixelText;
private bodyText: PixelText;
private hintText: PixelText;
private choiceViews: { item: DialogueChoiceItem; label: PixelText }[] = [];
private onChoice: ((index: number) => void) | null;
private readonly width: number;
private readonly height: number;
private readonly margin: number;
private readonly size: number;
constructor(options: DialogueBoxOptions) {
this.width = options.width;
this.height = options.height;
this.margin = options.margin;
// 8–9px у VT323 нечитаемы (тонкие штрихи размазываются) — минимум 10.
this.size = Math.max(options.size ?? 10, 10);
this.onChoice = options.onChoice ?? null;
this.view = new Container();
this.view.visible = false;
this.view.eventMode = 'static';
this.panel = new Graphics();
this.nameText = new PixelText({ text: '', size: this.size, color: 0xf0d878 });
this.bodyText = new PixelText({
text: '',
size: this.size,
color: 0xffffff,
wordWrapWidth: options.width - options.margin * 4,
lineHeight: this.size + 3
});
this.hintText = new PixelText({ text: 'далее ▸', size: this.size, color: 0xaaaaaa });
this.view.addChild(this.panel, this.nameText, this.bodyText, this.hintText);
this.layout();
}
private layout(): void {
const w = this.width;
const h = this.height;
const m = this.margin;
this.panel.rect(m, h - m - 54, w - m * 2, 54).fill({ color: 0x101018, alpha: 0.92 });
this.panel.rect(m, h - m - 54, w - m * 2, 54).stroke({ color: 0x8899aa, width: 1 });
this.nameText.position.set(m + 5, h - m - 52);
this.bodyText.position.set(m + 5, h - m - 42);
this.hintText.position.set(w - m - 42, h - m - 10);
}
/**
* Показать реплику. Если есть choices — рисуются кликабельные варианты
* (текст реплики обычно скрыт: runner показывает вопрос предыдущим узлом).
*/
show(line: { speaker?: string; text: string; choices?: DialogueChoiceItem[] }): void {
this.nameText.text = line.speaker ?? '';
this.bodyText.text = line.text;
this.hintText.visible = !line.choices || line.choices.length === 0;
this.view.visible = true;
this.visible = true;
for (const c of this.choiceViews) c.label.destroy();
this.choiceViews = [];
if (line.choices && line.choices.length > 0) {
const m = this.margin;
let y = this.height - m - 42;
for (const item of line.choices) {
const label = new PixelText({
text: `▸ ${item.text}`,
size: this.size,
color: 0xcccccc,
wordWrapWidth: this.width - m * 4
});
label.position.set(m + 5, y);
label.eventMode = 'static';
label.cursor = 'pointer';
label.on('pointerover', () => (label.style.fill = 0xf0d878));
label.on('pointerout', () => (label.style.fill = 0xcccccc));
label.on('pointertap', () => this.onChoice?.(item.index));
this.view.addChild(label);
this.choiceViews.push({ item, label });
y += label.height + 3;
}
}
}
hide(): void {
this.view.visible = false;
this.visible = false;
for (const c of this.choiceViews) c.label.destroy();
this.choiceViews = [];
}
}