/**
* Гейт снаряжения (трек 32 «крепления предметов»): герой по умолчанию с
* рюкзаком и факелом (механизм креплений к костям риги), число вокселей
* снаряжения сходится с генераторами, снятие/надевание меняет кадр,
* крепления не ломают физику/ходьбу. Скриншоты: /tmp/v2_gear_*.png.
*
* Запуск: node apps/game/tools/checks/gear.mjs [--port 5328]
*/
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 ?? 5328);
const server = startDevServer({ port, cwd: fileURLToPath(new URL('../../', import.meta.url)) });
const report = { name: 'gear', ok: false, checks: [] };
const check = (title, ok, info = null) => {
report.checks.push({ title, ok, info });
console.error(`${ok ? '✓' : '✗'} ${title}${info ? ' — ' + JSON.stringify(info) : ''}`);
};
function countDiff(a, b) {
let n = 0;
for (let i = 0; i < a.length; i += 4) {
if (Math.abs(a[i] - b[i]) > 8 || Math.abs(a[i + 1] - b[i + 1]) > 8 || Math.abs(a[i + 2] - b[i + 2]) > 8) n++;
}
return n;
}
try {
await server.waitReady;
const browser = await launchBrowser();
try {
const client = await openGame(browser, { url: `http://localhost:${port}/` });
await client.call('startGame', false);
await client.call('setManual', true);
await client.call('setAsh', false);
await client.call('setFollow', false); // камера фикс — диф только от снаряжения
await client.call('stepTicks', 5);
// ---- Снаряжение по умолчанию: рюкзак + факел ----
const hi = await client.call('heroInfo');
// рюкзак 6×6×2 (72) + клапан 6×2 (12) = 84 (пряжка — перекраска); факел 6
check('герой по умолчанию: рюкзак + факел (90 вокс)',
hi.gear.includes('backpack') && hi.gear.includes('torch') && hi.gearVoxels === 90, hi.gear);
await client.call('stepTicks', 1);
const shotOn = await client.screenshot('/tmp/v2_gear_on.png');
// ---- Снятие всего: вокселей снаряжения нет, кадр другой ----
await client.call('heroGear', null);
const hi0 = await client.call('heroInfo');
check('без снаряжения: gearVoxels 0', hi0.gear.length === 0 && hi0.gearVoxels === 0, hi0.gear);
await client.call('stepTicks', 1);
const shotOff = await client.screenshot('/tmp/v2_gear_off.png');
const diff = countDiff(decodePng(readFileSync(shotOn)).rgba, decodePng(readFileSync(shotOff)).rgba);
check('снаряжение меняет кадр', diff > 100, { diff });
await client.call('heroGear', ['backpack', 'torch']);
await client.call('stepTicks', 1);
// ---- Только рюкзак: 84 вокс (факел — 6) ----
await client.call('heroGear', ['backpack']);
const hiB = await client.call('heroInfo');
check('только рюкзак: 84 вокс', hiB.gearVoxels === 84, hiB.gear);
await client.call('heroGear', ['backpack', 'torch']);
// ---- Ходьба со снаряжением: герой ходит (механизм не ломает физику) ----
await client.call('heroTeleport', 40, 60);
await client.call('setInput', 1, 1);
await client.call('stepTicks', 60); // 21 вокс/с × 1 с = 2+ тайла
await client.call('setInput', null, null);
const hi2 = await client.call('heroInfo');
check('ходьба со снаряжением: герой сместился и фаза идёт',
hi2.grounded && hi2.phase > 0 && (hi2.tile.x !== 4 || hi2.tile.y !== 6), hi2);
await client.call('stepTicks', 1);
const shotWalk = await client.screenshot('/tmp/v2_gear_walk.png');
const errs = client.errors();
check('нет pageerror за прогон', errs.length === 0, errs.slice(0, 3));
} finally {
await browser.close();
}
} catch (e) {
console.error(e);
report.fatal = String(e);
}
report.ok = report.checks.length > 0 && report.checks.every((c) => c.ok);
console.log(JSON.stringify(report));
process.exit(report.ok ? 0 : 1); // chromium-хэндлы держат event loop — выходим явно