Newer
Older
rpg / apps / game / tools / checks / music.mjs
/**
 * Сценарий music — музыкальный слой: тема лугов генерируется правилами
 * (data/music.ts, сид) и компилируется движковым секвенсором в рантайме
 * (playTheme → playLoopBuffer), факт запуска — в window.__gameAudioLog.
 * 1) новая игра в лугах: music/meadows в логе (тема играет) + амбиент;
 * 2) тема не перезапускается при возврате в ту же область (guard);
 * 3) уход в Звенец (темы нет): слот темы снят (themeKey '') и тема лугов
 *    не перезапускается. Динамическая музыка разрешена: стем (настороженность)
 *    и тема боя (погоня тяжеловеса у тропы — его аггро-радиус на пути).
 * Запуск: node tools/agent.mjs run tools/checks/music.mjs
 */
import { startDevServer, openGame, Checks } from '../lib.mjs';

const readLog = (page) => page.evaluate(() => window.__gameAudioLog ?? null);

/** Подождать появления ключа в логе (компиляция темы занимает кадры). */
async function waitKey({ page, agent }, key, tries = 12) {
    for (let i = 0; i < tries; i++) {
        if ((await readLog(page)).some((e) => e.key === key)) return true;
        await agent.step(30);
    }
    return false;
}

export default async function ({ pretty }) {
    const c = new Checks('music');
    const server = await startDevServer();
    let ctx;
    try {
        await c.run('новая игра в лугах: тема скомпилирована и играет', async () => {
            ctx = await openGame({ url: server.url, newGame: true });
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
            c.expect(
                await waitKey(ctx, 'music/meadows'),
                'темы лугов нет в аудио-логе (компиляция не удалась?)',
                await readLog(ctx.page)
            );
            c.expect(
                (await readLog(ctx.page)).some((e) => e.key === 'ambience/meadows'),
                'амбиент лугов пропал при старте темы'
            );
            return null;
        });
        await c.run('возврат в ту же область: тема не перезапускается (guard)', async () => {
            const count = () =>
                readLog(ctx.page).then((log) => log.filter((e) => e.key === 'music/meadows').length);
            const before = await count();
            // Шагнуть туда-обратно мимо переходов, сцена не меняется — guard ключа.
            await ctx.agent.walkTo(15, 14, { timeoutTicks: 1500 });
            await ctx.agent.walkTo(14, 14, { timeoutTicks: 1500 });
            c.expect((await count()) === before, 'тема перезапустилась без смены ключа', {
                before,
                after: await count()
            });
            return null;
        });
        await c.run('уход в Звенец: слот темы снят, тема лугов не перезапускается', async () => {
            const meadowsCount = () =>
                readLog(ctx.page).then((log) => log.filter((e) => e.key === 'music/meadows').length);
            const before = await meadowsCount();
            // Пешком к тропе в Звенец (26,14) — честный step-переход.
            const walked = await ctx.agent.walkTo(26, 14, { timeoutTicks: 3000 });
            c.expect(walked, 'walkTo(26,14) не дошёл до перехода');
            const themeKey = () =>
                ctx.page.evaluate(() => {
                    const g = window.__game;
                    return g ? g.themeKey : null;
                });
            // Слот темы освобождается (тема Звенца пуста; тема боя погони —
            // временный ключ, ждём его снятия после деаггро).
            for (let i = 0; i < 12 && (await themeKey()) !== ''; i++) {
                await ctx.agent.step(30);
            }
            c.expect((await themeKey()) === '', 'слот темы не снят при уходе из лугов');
            c.expect(
                (await meadowsCount()) === before,
                'тема лугов перезапустилась (guard ключа сломан)',
                { before, after: await meadowsCount() }
            );
            return null;
        });
    } finally {
        await ctx?.browser?.close();
        server.stop();
    }
    return c.finish({ pretty }).ok ? 0 : 1;
}