Newer
Older
rpg / apps / game / tools / checks / art_b4.mjs
/**
 * Сценарий art_b4 — визуальная проверка AI-арта B4 (задача 5):
 * столбы, моты, верёвка колокола, очаг, записка, прилавок, резонатор,
 * деревья-механизмы, Старая Машина (view.tex) и иконки сумки 8×8.
 * Скриншоты — /tmp/rpg_b4_*.png; численных assert'ов минимум: смена области,
 * открытая сумка, инварианты. Запуск: node tools/agent.mjs run tools/checks/art_b4.mjs
 */
import { withChecks } from '../lib.mjs';

const shot = async (t, name) => {
    await t.ctx.agent.step(2, { render: true });
    await t.ctx.page.screenshot({ path: `/tmp/rpg_b4_${name}.png` });
};

export default async function ({ pretty }) {
    return withChecks(
        'art_b4',
        async (t) => {
            const { c } = t;
            let a; // агентный мост появляется после boot
            await c.run('луга: столб и мот', async () => {
                await t.boot();
                a = t.ctx.agent;
                await t.sleepEnemies();
                await a.command('scene:teleport', { x: 47, y: 27 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 60 });
                await shot(t, 'sign_meadows');
                await a.command('scene:teleport', { x: 15, y: 28 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 60 });
                await shot(t, 'mote');
                return null;
            });
            await c.run('сумка: иконки предметов', async () => {
                for (const id of ['clock', 'cloth', 'bellflower', 'salt', 'water_flask', 'mote', 'map_scroll']) {
                    await a.command('scene:give', { id });
                }
                await a.press('inventory');
                // Открытие — push с fade: сцена сменится, потом fade отыграть.
                await a.waitFor('s.scene !== "location"', { timeoutTicks: 300 });
                await a.step(14, { render: true });
                await t.ctx.page.screenshot({ path: '/tmp/rpg_b4_inventory.png' });
                await a.press('menu');
                await a.waitFor('s.scene === "location"', { timeoutTicks: 300 });
                return null;
            });
            await c.run('Звенец: столб и верёвка колокола', async () => {
                await t.goToArea('zvenets', { x: 54, y: 28 });
                await a.command('scene:teleport', { x: 5, y: 19 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 60 });
                await shot(t, 'sign_zvenets');
                await a.command('scene:teleport', { x: 27, y: 15 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 60 });
                await shot(t, 'bell_rope');
                return null;
            });
            await c.run('дом Ирвина: очаг и записка', async () => {
                c.expect(await t.interactAt(13, 12), 'дверь дома Ирвина не открылась');
                await a.waitFor('s.area === "house_elder"', { timeoutTicks: 1200 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 600 });
                await a.command('scene:teleport', { x: 8, y: 2 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 60 });
                await shot(t, 'hearth_note');
                // Шаг в проём (5,8) — назад в Звенец.
                c.expect(await a.walkTo(5, 8, { timeoutTicks: 3000 }), 'не вышли из дома');
                await a.waitFor('s.area === "zvenets"', { timeoutTicks: 1200 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 600 });
                return null;
            });
            await c.run('лавка Милы: прилавок', async () => {
                c.expect(await t.interactAt(43, 19), 'дверь лавки не открылась');
                await a.waitFor('s.area === "shop"', { timeoutTicks: 1200 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 600 });
                await a.command('scene:teleport', { x: 5, y: 5 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 60 });
                await shot(t, 'counter_shop');
                c.expect(await a.walkTo(5, 8, { timeoutTicks: 3000 }), 'не вышли из лавки');
                await a.waitFor('s.area === "zvenets"', { timeoutTicks: 1200 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 600 });
                return null;
            });
            await c.run('роща: дерево-механизм и резонатор', async () => {
                await t.goToArea('rust_grove', { x: 54, y: 20 });
                const s = await a.snapshot();
                const tree = (s.interactables ?? []).find((i) => i.id === 'tone_tree_1');
                c.expect(tree, 'tone_tree_1 не найден', s.interactables?.map((i) => i.id));
                await a.command('scene:teleport', { x: tree.tile.x, y: tree.tile.y + 2 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 60 });
                await shot(t, 'tone_tree');
                await a.command('scene:teleport', { x: 24, y: 18 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 60 });
                await shot(t, 'resonator');
                return null;
            });
            await c.run('зал Машины: Старая Машина', async () => {
                await t.goToArea('old_siding', { x: 46, y: 20 });
                await t.goToArea('machine_hall', { x: 28, y: 31 });
                await a.command('scene:teleport', { x: 13, y: 4 });
                await a.waitFor('!s.transitioning', { timeoutTicks: 60 });
                await shot(t, 'machine_hall');
                return null;
            });
            const inv = await a.invariants();
            const errs = inv.filter((i) => i.severity === 'error');
            c.expect(errs.length === 0, 'инварианты не чисты', errs);
        },
        { pretty }
    );
}