Newer
Older
rpg / apps / game / tools / lib.mjs
/**
 * Сценарный слой тулз «Пепельных лугов» поверх жанронезависимого каркаса
 * @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;
}