import { GameState } from '../core/GameState';
/**
* Модель диалоговых графов и чистые предикаты над ней (без рантайма).
* Граф — данные (TS/JSON): узлы с репликами, выборами и условиями по GameState.
* Рантайм обхода графа — DialogueRunner; валидатор/dry-run/квесты пользуются
* типами и evalConditions отсюда.
*/
/** Условие по переменной состояния. */
export interface VarCondition {
key: string;
op: 'eq' | 'ne' | 'gt' | 'lt' | 'ge' | 'le';
value: number | string;
}
/** Действия, применяемые к GameState при входе в узел/выборе. */
export interface DialogueEffects {
setFlags?: string[];
clearFlags?: string[];
setVars?: Record<string, number | string | boolean>;
/**
* Игровые эффекты как данные: движок только эмитит их через onEffect,
* исполнение — на стороне игры (EffectSink). Неизвестный kind — ошибка
* валидатора, рантайм его игнорирует.
*/
do?: DialogueEffectOp[];
}
/** Одна игровая операция эффекта (что именно она значит — знает игра). */
export interface DialogueEffectOp {
kind: 'giveItem' | 'takeItem' | 'sound' | 'toast' | 'custom';
/** id предмета (give/take), звука (sound) или имя сюжетного эффекта (custom). */
id?: string;
/** Количество (give/take), по умолчанию 1. */
count?: number;
/** Текст всплывашки (toast). */
text?: string;
/** Свободные параметры (custom). */
payload?: Record<string, number | string | boolean>;
}
/** Итог пройденного диалога: где остановились, как дошли, что выбрали. */
export interface DialogueResult {
/** id последнего показанного узла (null — ни одного). */
lastNodeId: string | null;
/** ids узлов по порядку показа. */
path: string[];
/** Выборы игрока: узел, индекс в узле, текст варианта. */
picks: { nodeId: string; index: number; text: string }[];
}
/** Хуки мира: предикаты условий и резолв ключей строк (локализация). */
export interface DialogueHooks {
world?: DialogueWorld;
/** Ключ строки → текст (textKey); нет резолва — ключ и есть текст. */
resolve?: (key: string) => string;
}
/**
* Предикаты мира, которых движок знать не может (сумка, репутация...).
* Игра передаёт реализацию в раннер; без неё hasItem-условия ложны.
*/
export interface DialogueWorld {
hasItem(id: string): boolean;
}
/** Условия показа узла/варианта (все перечисленные группы — AND). */
export interface DialogueConditions {
/** Все эти флаги должны быть установлены. */
when?: string[];
/** Ни один из этих флагов не должен быть установлен. */
whenNot?: string[];
/** Условие по переменной. */
whenVar?: VarCondition;
/** Несколько условий по переменным, AND. */
whenVars?: VarCondition[];
/** Все эти предметы должны быть в сумке (резолв — DialogueWorld). */
hasItem?: string[];
}
export interface DialogueChoice extends DialogueEffects, DialogueConditions {
text: string;
/** Следующий узел (по умолчанию — конец диалога). */
next?: string;
/** Настроение реплики (игра мапит на цвет/портрет). */
mood?: string;
/** Свободные метки для агента/инструментов (на геймплей не влияют). */
tags?: string[];
/** Ключ строки в таблице локализации; переопределяет inline text. */
textKey?: string;
}
export interface DialogueNode extends DialogueEffects, DialogueConditions {
/** Имя говорящего (опционально). */
speaker?: string;
/** Ключ имени говорящего (локализация, как textKey). */
speakerKey?: string;
/** Текст реплики. Узел без текста — «действие»: применяет эффекты и уходит в next. */
text?: string;
/** Варианты ответа игрока. */
choices?: DialogueChoice[];
/** Следующий узел. */
next?: string;
/** Явный конец диалога (для конечных узлов без choices/next). */
end?: boolean;
/** Настроение реплики (игра мапит на цвет/портрет). */
mood?: string;
/** Свободные метки для агента/инструментов (на геймплей не влияют). */
tags?: string[];
/** Ключ строки в таблице локализации; переопределяет inline text. */
textKey?: string;
}
export interface DialogueGraph {
/** id стартового узла. */
start: string;
nodes: Record<string, DialogueNode>;
}
/** Есть ли в условиях хоть что-то для проверки. */
export function hasConditions(c: DialogueConditions): boolean {
return Boolean(
c.when?.length || c.whenNot?.length || c.whenVar || c.whenVars?.length || c.hasItem?.length
);
}
/** Узел-реплика: есть inline text или ключ строки. */
export function hasText(n: { text?: string; textKey?: string }): boolean {
return n.text !== undefined || n.textKey !== undefined;
}
/** Сравнение переменной по op; undefined (переменной нет) не проходит gt/lt/ge/le. */
function checkVar(state: GameState, cond: VarCondition): boolean {
const v = state.getVar(cond.key);
switch (cond.op) {
case 'eq':
return v === cond.value;
case 'ne':
return v !== cond.value;
case 'gt':
return Number(v) > Number(cond.value);
case 'lt':
return Number(v) < Number(cond.value);
case 'ge':
return Number(v) >= Number(cond.value);
case 'le':
return Number(v) <= Number(cond.value);
}
}
/**
* Чистая проверка условий узла/выбора без рантайма: удобна для квестовых
* стадий, dry-run и валидатора. world не задан → hasItem ложен.
*/
export function evalConditions(c: DialogueConditions, state: GameState, world?: DialogueWorld): boolean {
for (const f of c.when ?? []) {
if (!state.hasFlag(f)) return false;
}
for (const f of c.whenNot ?? []) {
if (state.hasFlag(f)) return false;
}
if (c.whenVar && !checkVar(state, c.whenVar)) return false;
for (const cond of c.whenVars ?? []) {
if (!checkVar(state, cond)) return false;
}
if (c.hasItem) {
if (!world) return false;
for (const id of c.hasItem) {
if (!world.hasItem(id)) return false;
}
}
return true;
}