/**
* Финальный визуальный смоук (финал ревью): переходы луга<->Звенец 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();
}