/**
* Гейт панелей UI (трек 28): лоад-скрин скрыт после первого кадра, слоты
* сейвов в меню (auto + 3 ручных: сохранить/загрузить/удалить), панель
* сумки по I (мир стоит), дев-панель F3 + вьювер моделей движка (статы,
* скины, закрытие). Скриншоты: /tmp/v2_panels_*.png.
*
* Запуск: node apps/game/tools/checks/panels.mjs [--port 5326]
*/
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { launchBrowser, startDevServer, openGame, parseArgs } from '@rpg/engine/tools/agent-lib.mjs';
import { decodePng } from '@rpg/engine/tools/png.mjs';
const args = parseArgs(process.argv.slice(2));
const port = Number(args.port ?? 5326);
const server = startDevServer({ port, cwd: fileURLToPath(new URL('../../', import.meta.url)) });
const report = { name: 'panels', ok: false, checks: [] };
const check = (title, ok, info = null) => {
report.checks.push({ title, ok, info });
console.error(`${ok ? '✓' : '✗'} ${title}${info ? ' — ' + JSON.stringify(info) : ''}`);
};
/** Скриншот → RGBA-буфер. */
async function shot(client, path) {
await client.screenshot(path);
return decodePng(readFileSync(path)).rgba;
}
try {
await server.waitReady;
const browser = await launchBrowser();
try {
const client = await openGame(browser, { url: `http://localhost:${port}/` });
check('лоад-скрин скрыт после первого кадра',
await client.evaluate(() => document.getElementById('loading').hidden) === true);
await client.call('startGame', false); // страница стартует в меню
await client.call('setManual', true);
await client.call('stepTicks', 10);
// ---- Слоты сейвов ----
await client.call('gotoMenu');
await client.call('stepTicks', 1);
const menu1 = await client.call('menuInfo');
check('меню открыто, 4 слота (auto + 3 ручных)',
menu1.open === true && menu1.slots.length === 4 &&
menu1.slots.map((s) => s.slot).join(',') === 'auto,s1,s2,s3', menu1.slots.map((s) => s.slot));
check('автосейв в меню записан (has, мета с областью)',
menu1.hasAuto === true &&
menu1.slots[0].meta && typeof menu1.slots[0].meta.savedAt === 'number', menu1.slots[0].meta);
// ломаем состояние (уезд героя), сохраняем в ручной слот, загружаем
await client.call('startGame', false);
await client.call('heroTeleport', 205, 145); // далеко от спавна
await client.call('stepTicks', 2);
await client.call('saveTo', 's1');
const saved = await client.call('menuInfo');
check('saveTo записал слот s1 с метой',
saved.slots[1].has === true && saved.slots[1].meta.title === 'meadows', saved.slots[1].meta);
await client.call('heroTeleport', 95, 125);
await client.call('stepTicks', 2);
check('loadFrom вернул героя в слот-позицию',
await client.call('loadFrom', 's1') === true);
await client.call('stepTicks', 2);
const back = await client.call('heroInfo');
check('позиция из сейва восстановлена',
Math.abs(back.pos[0] - 205) < 1 && Math.abs(back.pos[2] - 145) < 1, back.pos);
await client.call('saveDelete', 's1');
check('saveDelete чистит слот',
(await client.call('menuInfo')).slots[1].has === false);
check('loadFrom пустого слота — false',
await client.call('loadFrom', 's1') === false);
// ---- Панель сумки (I) ----
await client.call('toggleBag');
const bag1 = await client.call('bagInfo');
check('панель сумки открыта (мост + DOM)',
bag1.open === true &&
await client.evaluate(() => !document.getElementById('bag').hidden));
const pos1 = (await client.call('heroInfo')).pos;
await client.call('stepTicks', 30);
const pos2 = (await client.call('heroInfo')).pos;
check('мир стоит при открытой сумке',
pos1[0] === pos2[0] && pos1[2] === pos2[2], { pos1, pos2 });
check('сумка рисует содержимое (пустая — «Пусто.»)',
await client.evaluate(() => document.getElementById('bag').textContent.includes('Пусто.')) === true);
await client.call('toggleBag');
check('панель сумки закрылась',
(await client.call('bagInfo')).open === false);
// ---- Дев-панель + вьювер ----
const dev1 = await client.call('devInfo');
check('каталог объектов: персонажи + природа + строения',
dev1.entries.length >= 14 &&
dev1.entries.some((e) => e.id.startsWith('char:')) &&
dev1.entries.some((e) => e.id === 'deer') &&
dev1.entries.some((e) => e.id === 'machine'), dev1.entries.length);
await client.call('devOpen');
check('панель списка открыта',
await client.evaluate(() => !document.getElementById('dev').hidden) === true);
const heroId = dev1.entries.find((e) => e.id.startsWith('char:')).id;
check('вьювер открылся', await client.call('devOpenEntry', heroId) === true);
const st1 = await client.call('devStats');
check('статы манекена: 164 вокс / 5 костей / 19 тайлей (трек 29 — скины деталей)',
st1.voxels === 164 && st1.bones === 5 && st1.tiles === 19, st1);
// кадр вьювера рисует модель (зона канваса не пустая)
await client.call('stepTicks', 1);
const viewPng = await shot(client, '/tmp/v2_panels_view.png');
let lit = 0;
for (let i = 0; i < viewPng.length; i += 4) if (viewPng[i] + viewPng[i + 1] + viewPng[i + 2] > 45) lit++;
check('вьювер рисует модель (не пустой кадр)', lit > 500, lit);
check('вьювер закрылся, мир снова тикается',
await client.call('devClose') === undefined &&
await client.evaluate(() => document.getElementById('devview').hidden) === true);
check('каталог открыт повторно', await client.call('devOpenEntry', 'deer') === true);
const st2 = await client.call('devStats');
check('статы оленя: без риги, есть воксели',
st2.bones === 0 && st2.voxels > 50, st2);
await client.call('devClose');
await client.call('devClose'); // панель тоже
const errs = client.errors();
check('нет pageerror за прогон', errs.length === 0, errs.slice(0, 3));
} finally {
await browser.close();
}
} catch (e) {
report.error = String(e && e.stack ? e.stack : e);
console.error(report.error);
} finally {
server.stop();
}
report.ok = report.checks.length > 0 && report.checks.every((c) => c.ok);
console.log(JSON.stringify(report));