Newer
Older
rpg / v2 / apps / game / tools / checks / gear.mjs
/**
 * Гейт снаряжения (трек 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 — выходим явно