/**
* Сценарный слой тулз «Пепельных лугов» поверх жанронезависимого каркаса
* @rpg/engine/tools/agent-lib.mjs: маяк загрузки игры и re-export общих
* примитивов. Смоуки и checks/*.mjs импортируют отсюда.
*/
import { fileURLToPath } from 'node:url';
import {
Checks,
openGame as engineOpenGame,
startDevServer as engineStartDevServer
} from '@rpg/engine/tools/agent-lib.mjs';
export { CHROMIUM, launchBrowser, AgentClient, Checks, parseArgs } from '@rpg/engine/tools/agent-lib.mjs';
/** Каталог игры (index.html + vite-конфиг) — дефолтный cwd dev-сервера. */
const GAME_ROOT = fileURLToPath(new URL('..', import.meta.url));
/** startDevServer по умолчанию поднимает vite из каталога игры, а не из корня монорепо. */
export const startDevServer = (opts = {}) => engineStartDevServer({ cwd: GAME_ROOT, ...opts });
/** Консольный маяк BootScene: ассеты загружены, мост готов. */
export const BEACON = '[boot] ассеты загружены';
/** openGame с игровым маяком загрузки. */
export const openGame = (opts = {}) => engineOpenGame({ beacon: BEACON, ...opts });
/**
* Каркас чек-сценария: поднимает dev-сервер, даёт помощники (boot, goToArea,
* sleepEnemies, readAudioLog, waitAudioLog, expectInvariantsClean) и в любом
* исходе гасит браузер и сервер. Сценарий оставляет только шаги проверки.
*
* export default async function ({ pretty }) {
* return withChecks('имя', async (t) => { ... }, { pretty });
* }
*/
export async function withChecks(name, fn, { pretty } = {}) {
const c = new Checks(name);
const server = await startDevServer();
const h = { ctx: null };
const t = {
c,
/** Контекст игры (заполняется boot). */
get ctx() {
return h.ctx;
},
/** Новая игра + выход из стартового fade-перехода. */
boot: async () => {
h.ctx = await openGame({ url: server.url, newGame: true });
await h.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
return h.ctx;
},
/** Спим всех врагов (безопасная навигация по миру). */
sleepEnemies: () => h.ctx.agent.command('scene:sleepAll'),
/**
* Пешком к тайлу-триггеру step-перехода (via) и дождаться области
* (клик-переходы вроде дверей — явным tapTile в сценарии).
*/
goToArea: async (area, via) => {
const walked = await h.ctx.agent.walkTo(via.x, via.y, { timeoutTicks: 3000 });
c.expect(walked, `walkTo(${via.x},${via.y}) не дошёл до перехода`);
const w = await h.ctx.agent.waitFor(`s.area === "${area}"`, { timeoutTicks: 1200 });
c.expect(w.ok, `после перехода не в «${area}»`, w.snapshot?.area);
await h.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
return w;
},
/** DEV-лог аудио страницы (window.__gameAudioLog) или null вне DEV. */
readAudioLog: () => h.ctx.page.evaluate(() => window.__gameAudioLog ?? null),
/**
* Ждать условие на аудио-логе: декод WAV и компиляция спеков в headless
* занимают заметное время — шагаем и опрашиваем, а не читаем сразу.
*/
waitAudioLog: async (pred, tries = 12) => {
let log = await t.readAudioLog();
for (let i = 0; i < tries && !pred(log); i++) {
await h.ctx.agent.step(30);
log = await t.readAudioLog();
}
return log;
},
/** Ошибки инвариантов моста: пусто — чисто (warn допустим). */
expectInvariantsClean: async (what) => {
const inv = await h.ctx.agent.invariants();
const errs = (inv ?? []).filter((i) => i.severity === 'error');
c.expect(errs.length === 0, `инварианты нарушены${what ? `: ${what}` : ''}`, errs);
return errs;
}
};
try {
await fn(t);
} finally {
try {
await h.ctx?.browser?.close();
} catch {
// браузер мог умереть раньше — сервер всё равно гасим
}
server.stop();
}
return c.finish({ pretty }).ok ? 0 : 1;
}