/**
* CLI dry-run диалогов: прогон графов через DialogueRunner на реальном
* GameState (без Pixi) + структурный анализ. Печатает дерево «узел →
* выборы → куда», сироты, циклы, тихие концы и битые ссылки.
*
* Запуск: npm run dialogues:dry [-- <id>]
* (vite-node — чтобы работал TS/JSON-импорт из src.)
*/
import { GameState, DialogueRunner } from '@rpg/engine';
import { DIALOGUES } from '../../src/data/dialogues';
import { analyzeGraph, checkGraph, type GraphRefs } from '../../src/data/dialogueRules';
import { FLAGS, VARS } from '../../src/data/ids';
import { ITEMS } from '../../src/data/items';
import { DIALOGUE_CUSTOM } from '../../src/data/effects';
/** Реестры игры для checkGraph (те же множества, что в validate.ts). */
function refs(): GraphRefs {
return {
flags: new Set(Object.keys(FLAGS)),
vars: new Set(Object.keys(VARS)),
items: new Set(Object.keys(ITEMS)),
customs: new Set(Object.keys(DIALOGUE_CUSTOM)),
strings: new Set()
};
}
/** Пресеты состояния: в каких условиях гоняем каждый граф. */
const PRESETS: { name: string; setup: (s: GameState) => void }[] = [
{ name: 'fresh', setup: () => {} },
{
name: 'quest-taken+flowers',
setup: (s) => {
s.setFlag('quest_bells_taken');
s.setFlag('met_elder');
s.setVar('flowers', 3);
}
},
{
name: 'quest-done',
setup: (s) => {
s.setFlag('quest_bells_taken');
s.setFlag('met_elder');
s.setFlag('quest_bells_done');
}
}
];
/** Дерево реплик: что покажет раннер и куда можно пойти — см. main(). */
const main = (): void => {
const only = process.argv[2];
let errors = 0;
for (const [id, graph] of Object.entries(DIALOGUES)) {
if (only && id !== only) continue;
console.log(`\n=== ${id} (start: ${graph.start}) ===`);
const a = analyzeGraph(graph);
const bad = checkGraph(id, graph, refs()).filter((i) => i.severity === 'error');
errors += bad.length;
for (const inv of bad) console.log(` [${inv.severity}] ${inv.id}: ${inv.message}`);
for (const o of a.orphans) console.log(` [warn] сирота: «${o}»`);
for (const cycle of a.textlessCycles) console.log(` [error] цикл без текста: ${cycle.join(' → ')}`);
for (const end of a.silentEnds) console.log(` [warn] тихий конец: «${end}»`);
// Прогон на каждом пресете: какие реплики реально покажутся.
for (const preset of PRESETS) {
const state = new GameState();
preset.setup(state);
const runner = new DialogueRunner(state);
const shown: string[] = [];
runner.setView({
show: (n) => shown.push(`${n.speaker ? n.speaker + ': ' : ''}${n.text}` +
(n.choices.length > 0 ? ` [${n.choices.map((c) => c.text).join(' | ')}]` : '')),
hide: () => {}
});
runner.start(graph);
while (runner.active && !runner.waitingForChoice) runner.advance();
console.log(` [${preset.name}] ${shown.join(' → ') || '(молча)'}`);
}
}
if (errors > 0) process.exitCode = 1;
};
main();