Newer
Older
rpg / v2 / apps / game / tools / checks / panels.mjs
/**
 * Гейт панелей 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'); // панель тоже

        // ---- Время суток в дев-панели (слайдер/пресеты) ----
        await client.call('devOpen');
        await client.call('devSetClock', 1134); // пресет «18:54» — золотой час
        const di2 = await client.call('devInfo');
        const ci2 = await client.call('clockInfo');
        check('дев-панель: метка времени 18:54 после перемотки',
            di2.clock === '18:54' && ci2.label === '18:54', { dev: di2.clock, clock: ci2.label });
        check('дев-панель: свет пересчитан сразу (закат 0.45)',
            Math.abs(ci2.nightFactor - 0.45) < 0.01, ci2);
        const shotTime = await client.screenshot('/tmp/v2_panels_devtime.png');
        check('скриншот панели со временем снят (визуально)', shotTime.length > 0, shotTime);
        await client.call('devSetClock', 480); // вернуть утро (дальнейшие проверки не темнеют)
        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));