/**
* Скриншот-гейт физики v2 (трек 28: AABB-тело vs воксели + гравитация).
* Проверяет в реальном Chromium (swiftshader) и через мост: тело падает и
* встаёт ровно на землю, идёт walk-циклом, стена дома останавливает по грани,
* кадр меняется от движения, манекен реально рисуется. Скриншоты:
* /tmp/v2_physics_*.png.
*
* Запуск: node apps/demo/tools/checks/physics.mjs [--port 5301]
*/
import { fileURLToPath } from 'node:url';
import { readFileSync } from 'node:fs';
import { sleep, 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 ?? 5301);
const DEMO_DIR = fileURLToPath(new URL('../../', import.meta.url));
const server = startDevServer({ port, cwd: DEMO_DIR });
const report = { name: 'physics', ok: false, checks: [] };
const check = (title, ok, info = null) => {
report.checks.push({ title, ok, info });
console.error(`${ok ? '✓' : '✗'} ${title}${info ? ' — ' + JSON.stringify(info) : ''}`);
};
// Геометрия сцены для ожиданий: стена дома стоит на x=44, земля — грань y=2.
const HOUSE_WALL_X = 44;
const GROUND_Y = 2;
const SPAWN = [38, 46]; // перед южной стеной дома, путь чист
try {
await server.waitReady;
const browser = await launchBrowser();
try {
const client = await openGame(browser, { url: `http://localhost:${port}/` });
const errs = client.errors();
check('нет pageerror при загрузке', errs.length === 0, errs.slice(0, 3));
// Камера фиксированная: слежение за героем off (герой проверяет hero.mjs)
await client.call('setFollow', false);
// Спавн в воздухе (y=6): гейт сразу видит тело до шагов физики
const spawnInfo = await client.call('bodySpawn', SPAWN[0], SPAWN[1], 0, 0);
check('тело спавнится в воздухе (y=6, не grounded)',
spawnInfo && spawnInfo.pos[1] === 6 && spawnInfo.grounded === false, spawnInfo);
// Гравитация: падение до земли, встаёт ровно на грань y=2
const landed = await client.call('bodyStep', 180);
check('гравитация: падает и встаёт на землю y=2, grounded',
landed && landed.grounded === true && Math.abs(landed.pos[1] - GROUND_Y) < 0.02, landed);
// Референсный кадр: манекен стоит на старте
await client.call('setSun', 0.5);
await sleep(300);
const startShot = await client.screenshot('/tmp/v2_physics_start.png');
const start = decodePng(readFileSync(startShot));
// Ходьба на восток к стене дома: тело движется, фаза walk-цикла растёт
const walker = await client.call('bodySpawn', SPAWN[0], SPAWN[1], 2, 0);
await client.call('bodyStep', 240);
const atWall = await client.call('bodyInfo');
check('ходьба: фаза walk-цикла растёт в движении', atWall && atWall.walkT > 1, atWall);
// Стена останавливает по своей западной грани: pos.x ≈ 44 − w/2 − eps
const expectedX = HOUSE_WALL_X - atWall.size[0] / 2 - 1e-4;
check('стена дома останавливает тело по грани (vel=0)',
atWall && atWall.vel[0] === 0 && Math.abs(atWall.pos[0] - expectedX) < 0.01,
{ pos: atWall.pos[0], expectedX });
// Кадр изменился: манекен сместился к стене и в другой фазе цикла
const wallShot = await client.screenshot('/tmp/v2_physics_wall.png');
const wall = decodePng(readFileSync(wallShot));
const walkDiff = countDiff(start.rgba, wall.rgba);
check('кадр изменился: тело дошло до стены', walkDiff > 150, { walkDiff });
// Манекен реально рисуется: без тела кадр возвращается к пустому месту
await client.call('bodyRemove');
await sleep(300);
const emptyShot = await client.screenshot('/tmp/v2_physics_empty.png');
const empty = decodePng(readFileSync(emptyShot));
const bodyDiff = countDiff(wall.rgba, empty.rgba);
check('тело-манекен рисуется в сцене', bodyDiff > 150, { bodyDiff });
report.ok = report.checks.every((c) => c.ok);
} finally {
await browser.close();
}
} catch (e) {
check('прогон без исключений', false, String(e));
} finally {
server.stop();
}
console.log(JSON.stringify(report, null, 2));
process.exit(report.ok ? 0 : 1);
/** Число различающихся RGBA-пикселей двух кадров одного размера. */
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;
}