Newer
Older
rpg / apps / game / tools / final-smoke.mjs
/**
 * Финальный визуальный смоук (финал ревью): переходы луга<->Звенец x3,
 * бой с врагом, тост знака — проверка отсутствия наслоения вьюх/звука
 * после повторных переходов. Скриншоты в /tmp/rpg_final_*.png.
 * Запуск: node apps/game/tools/final-smoke.mjs [url]
 */
import { 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 });
        await agent.tapTile(9, 6); // колодец у дома Ирвина
        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): текст виден, тик не дублируется.
    await agent.tapTile(25, 13);
    const t = await agent.waitFor('(s.lastToast?.text ?? "").includes("Звенец")', { timeoutTicks: 1500 });
    console.log('тост знака:', t.ok ? t.snapshot.lastToast?.text : 'НЕ ПОКАЗАН');
    if (!t.ok) fail.push('тост знака не показан');

    // Бой: клик по ближайшему живому врагу (авто-подход + удар конусом).
    // Позиция врага плавает (сам идёт на героя) — предикат по минимуму hp живых.
    const before = await agent.snapshot();
    const alive = (s) => (s.enemies ?? []).filter((e) => !e.dead && e.hp > 0);
    const minHp = (s) => Math.min(...alive(s).map((e) => e.hp));
    const foe = alive(before).sort(
        (a, b) => Math.hypot(a.pos.x - before.hero.pos.x, a.pos.y - before.hero.pos.y) -
                  Math.hypot(b.pos.x - before.hero.pos.x, b.pos.y - before.hero.pos.y)
    )[0];
    if (foe) {
        await agent.tapTile(Math.floor(foe.pos.x), Math.floor(foe.pos.y)); // worldToTile = floor
        const hit = await agent.waitFor(
            `(s.enemies.filter((e) => !e.dead && e.hp > 0).length) < ${alive(before).length} || (Math.min(...s.enemies.filter((e) => !e.dead && e.hp > 0).map((e) => e.hp))) < ${minHp(before)}`,
            { timeoutTicks: 1200 }
        );
        console.log('бой:', hit.ok ? `враг получил урон (мин hp ${minHp(before)} -> ${minHp(hit.snapshot)})` : 'УРОНА НЕТ');
        if (!hit.ok) fail.push('враг не получил урон');
    } else {
        console.log('бой: живых врагов рядом нет — пропускаю');
    }
    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 ?? []);
    const dupEnemies = (snap.enemies ?? []).length - before.enemies?.length;
    console.log('дубли id объектов:', dup, '; врагов было/стало:', before.enemies?.length, '/', snap.enemies?.length);
    if (dup > 0) fail.push(`дубли в снапшоте: ${dup}`);
    if (dupEnemies > 0) fail.push(`врагов стало больше: ${before.enemies?.length} -> ${snap.enemies?.length}`);

    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();
}