Newer
Older
rpg / tools / checks / interact-world.mjs
/**
 * Сценарий interact-world — взаимодействие с миром шире (батч 3).
 * 1) столб у тропы: тост с текстом знака;
 * 2) подбор мота: предмет в сумке, вар motes растёт, used:<id>;
 * 3) второй и третий моты: вар копится (addVar, порядок не важен);
 * 4) знак наката у прудов;
 * 5) верёвка колокола: флаг rang_tower, повтор — другой текст;
 * 6) Мила: знакомство -> повторный диалог с веткой по мотам;
 * 7) инвариант interact-used-consistent чист.
 * Запуск: node tools/agent.mjs run tools/checks/interact-world.mjs
 */
import { startDevServer, openGame, Checks } from '../agent-lib.mjs';

export default async function ({ pretty }) {
    const c = new Checks('interact-world');
    const server = await startDevServer();
    let ctx;
    try {
        await c.run('столб у тропы: тост с текстом знака', async () => {
            ctx = await openGame({ url: server.url, newGame: true });
            await ctx.agent.command('scene:sleepAll');
            // Столб (25,13) у тропы в Звенец — клик издалека: герой подойдёт сам.
            await ctx.agent.tapTile(25, 13);
            const w = await ctx.agent.waitFor('(s.lastToast?.text ?? "").includes("Звенец")', {
                timeoutTicks: 1500
            });
            c.expect(w.ok, 'знак не показал текст', w.snapshot.lastToast);
            return null;
        });
        await c.run('мот 1: предмет + вар + used', async () => {
            await ctx.agent.tapTile(11, 14);
            const w = await ctx.agent.waitFor(
                '(s.inventory.find((i) => i.id === "mote")?.count ?? 0) === 1 && s.vars.motes === 1',
                { timeoutTicks: 1500 }
            );
            c.expect(w.ok, 'подбор мота не дал предмет и вар', {
                inventory: w.snapshot.inventory,
                vars: w.snapshot.vars
            });
            const s = await ctx.agent.snapshot();
            const mote = (s.interactables ?? []).find((o) => o.id === 'mote_1');
            c.expect(mote?.used === true, 'мот не помечен used', mote);
            return null;
        });
        await c.run('моты 2 и 3: вар копится (addVar)', async () => {
            await ctx.agent.tapTile(14, 20);
            const w2 = await ctx.agent.waitFor('s.vars.motes === 2', { timeoutTicks: 1500 });
            c.expect(w2.ok, 'второй мот не поднял вар до 2', w2.snapshot.vars);
            await ctx.agent.tapTile(14, 6);
            const w3 = await ctx.agent.waitFor('s.vars.motes === 3', { timeoutTicks: 1500 });
            c.expect(w3.ok, 'третий мот не поднял вар до 3', w3.snapshot.vars);
            return null;
        });
        await c.run('знак наката у прудов', async () => {
            // Тропа на пруды (2,2): step-переход; у берега — столб (3,2).
            const w = await ctx.agent.walkTo(2, 2, { timeoutTicks: 3000 });
            c.expect(w, 'не дошёл до тропы на пруды');
            await ctx.agent.waitFor('s.area === "ponds"', { timeoutTicks: 600 });
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
            await ctx.agent.tapTile(3, 2);
            const ws = await ctx.agent.waitFor('(s.lastToast?.text ?? "").includes("накатом")', {
                timeoutTicks: 900
            });
            c.expect(ws.ok, 'знак наката не показал текст', ws.snapshot.lastToast);
            // Обратно на луга (2,2) прудов.
            const wb = await ctx.agent.walkTo(2, 2, { timeoutTicks: 3000 });
            c.expect(wb, 'не вернулся к тропе с прудов');
            await ctx.agent.waitFor('s.area === "meadows"', { timeoutTicks: 600 });
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
            return null;
        });
        await c.run('верёвка колокола: флаг + повтор с другим текстом', async () => {
            // Звенец через тропу (26,14); верёвка у башни (13,7) — клик издалека.
            const w = await ctx.agent.walkTo(26, 14, { timeoutTicks: 3000 });
            c.expect(w, 'не дошёл до тропы в Звенец');
            await ctx.agent.waitFor('s.area === "zvenets"', { timeoutTicks: 600 });
            await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
            await ctx.agent.tapTile(13, 7);
            const wr = await ctx.agent.waitFor('s.flags.includes("rang_tower")', {
                timeoutTicks: 1500
            });
            c.expect(wr.ok, 'звон не поднял rang_tower', wr.snapshot.flags);
            const s1 = await ctx.agent.snapshot();
            const text1 = s1.lastToast?.text ?? '';
            c.expect(text1.includes('ушёл в землю'), 'первый звон — не тот текст', text1);
            await ctx.agent.tapTile(13, 7);
            await ctx.agent.step(150);
            const s2 = await ctx.agent.snapshot();
            const text2 = s2.lastToast?.text ?? '';
            c.expect(
                text2.includes('снова') && text2 !== text1,
                'повторный звон не сменил текст',
                text2
            );
            return null;
        });
        await c.run('Мила: знакомство -> повтор с веткой по мотам', async () => {
            // Первое знакомство (trader_first): полотно.
            await ctx.agent.tapTile(17, 11);
            const d1 = await ctx.agent.runDialogue(900);
            c.expect(d1, 'первый диалог с Милой не завершился');
            // Повтор (trader_repeat): при моты >= 1 после первой реплики идёт
            // ветка о мота́х — листаем вручную и ловим её текст.
            await ctx.agent.tapTile(17, 11);
            const o = await ctx.agent.waitFor('s.dialogue != null', { timeoutTicks: 900 });
            c.expect(o.ok, 'повторный диалог не открылся');
            await ctx.agent.press('advance');
            const w = await ctx.agent.waitFor(
                's.dialogue != null && (s.dialogue.text ?? "").includes("Моты")',
                { timeoutTicks: 300 }
            );
            c.expect(w.ok, 'в повторном диалоге нет ветки о мота́х', w.snapshot.dialogue);
            const d2 = await ctx.agent.runDialogue(600);
            c.expect(d2, 'повторный диалог не завершился');
            return null;
        });
        await c.run('инвариант: used-флаги и снапшот согласованы', async () => {
            const inv = await ctx.agent.invariants();
            const bad = inv.filter((i) => i.severity === 'error');
            c.expect(bad.length === 0, 'ошибки инвариантов', bad);
            return null;
        });
    } finally {
        await ctx?.browser?.close();
        server.stop();
    }
    return c.finish({ pretty }).ok ? 0 : 1;
}