Newer
Older
rpg / apps / game / tools / final-smoke.mjs
/**
 * Финальный визуальный смоук (финал ревью): переходы луга<->Звенец x3,
 * бой с врагом, тост знака — проверка отсутствия наслоения вьюх/звука
 * после повторных переходов. Скриншоты в /tmp/rpg_final_*.png.
 * Запуск: node apps/game/tools/final-smoke.mjs [url]
 */
import { fightNearest, openGame, startDevServer } from './lib.mjs';

const server = await startDevServer();
let ctx;
try {
    ctx = await openGame({ url: server.url, newGame: true });
    const { page, agent } = ctx;
    const fail = [];

    /** Туда-обратно: пешком к тропе (26,14) -> Звенец -> колодец (9,6) -> луга. */
    const roundTrip = async (n) => {
        await agent.command('scene:sleepAll');
        const w1 = await agent.walkTo(26, 14, { timeoutTicks: 3000 });
        if (!w1) fail.push(`поездка ${n}: не дошёл до тропы`);
        const z = await agent.waitFor('s.area === "zvenets"', { timeoutTicks: 1200 });
        if (!z.ok) fail.push(`поездка ${n}: не в Звенце`);
        await agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
        if (await agent.approach(9, 6)) agent.press('interact'); // колодец у дома Ирвина
        else fail.push(`поездка ${n}: не подошли к колодцу`);
        const m = await agent.waitFor('s.area === "meadows"', { timeoutTicks: 1200 });
        if (!m.ok) fail.push(`поездка ${n}: колодец не вернул на луга`);
        await agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
        await page.screenshot({ path: `/tmp/rpg_final_trip${n}.png` });
    };

    for (let n = 1; n <= 3; n++) await roundTrip(n);
    console.log('3 поездки туда-обратно: ок');

    // Тост знака у тропы (25,13): подходим и жмём действие.
    if (await agent.approach(25, 13)) agent.press('interact');
    else fail.push('не подошли к знаку у тропы');
    const t = await agent.waitFor('(s.lastToast?.text ?? "").includes("Звенец")', { timeoutTicks: 1500 });
    console.log('тост знака:', t.ok ? t.snapshot.lastToast?.text : 'НЕ ПОКАЗАН');
    if (!t.ok) fail.push('тост знака не показан');

    // Бой: подойти к ближайшему живому врагу, доворот facing, конус
    // (fightNearest — см. lib.mjs; позиция врага плавает, до 3 попыток).
    const hit = await fightNearest(agent);
    console.log('бой:', hit ? 'враг получил урон' : 'УРОНА НЕТ');
    if (!hit) fail.push('враг не получил урон');
    await page.screenshot({ path: '/tmp/rpg_final_combat.png' });

    // Наслоение звука: после 3-го возврата амбиент не должен перезапуститься
    // (guard ключа). Лог — кольцевой на 24 записи, старт с бута давно вытеснен,
    // поэтому следим за хвостом лога после перехода.
    let ambRestarted = false;
    for (let i = 0; i < 4 && !ambRestarted; i++) {
        await agent.step(30);
        const tail = await page.evaluate(() =>
            (window.__gameAudioLog ?? []).slice(-8).map((e) => e.key)
        );
        ambRestarted = tail.includes('ambience/meadows');
    }
    console.log('перезапуск амбиента после 3-го возврата:', ambRestarted ? 'ЕСТЬ' : 'нет');
    if (ambRestarted) fail.push('амбиент перезапустился после возврата на луга');
    // Объекты в снапшоте по одному на id; враги — без id в снапшоте, поэтому
    // проверяем, что их число не растёт от поездок (наслоения вьюх нет).
    const snap = await agent.snapshot();
    const dupIds = (arr) => arr.length - new Set(arr.map((x) => x.id)).size;
    const dup = dupIds(snap.interactables ?? []);
    console.log('дубли id объектов:', dup);
    if (dup > 0) fail.push(`дубли в снапшоте: ${dup}`);

    const errs = ((await agent.invariants()) ?? []).filter((i) => i.severity === 'error');
    if (errs.length) fail.push(`инварианты: ${JSON.stringify(errs)}`);

    console.log(fail.length ? 'ПРОВАЛЫ: ' + fail.join('; ') : 'ИТОГ: чисто');
    process.exitCode = fail.length ? 1 : 0;
} finally {
    try {
        await ctx?.browser?.close();
    } catch {}
    server.stop();
}