Newer
Older
rpg / apps / game / 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 { withChecks } from '../lib.mjs';

export default async function ({ pretty }) {
    return withChecks(
        'interact-world',
        async (t) => {
            const { c } = t;
            await c.run('столб у тропы: тост с текстом знака', async () => {
                await t.boot();
                await t.sleepEnemies();
                // Столб (25,13) у тропы в Звенец — клик издалека: герой подойдёт сам.
                await t.ctx.agent.tapTile(25, 13);
                const w = await t.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 t.ctx.agent.tapTile(11, 14);
                const w = await t.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 t.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 t.ctx.agent.tapTile(14, 20);
                const w2 = await t.ctx.agent.waitFor('s.vars.motes === 2', { timeoutTicks: 1500 });
                c.expect(w2.ok, 'второй мот не поднял вар до 2', w2.snapshot.vars);
                await t.ctx.agent.tapTile(14, 6);
                const w3 = await t.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).
                await t.goToArea('ponds', { x: 2, y: 2 });
                await t.ctx.agent.tapTile(3, 2);
                const ws = await t.ctx.agent.waitFor('(s.lastToast?.text ?? "").includes("накатом")', {
                    timeoutTicks: 900
                });
                c.expect(ws.ok, 'знак наката не показал текст', ws.snapshot.lastToast);
                // Обратно на луга (2,2) прудов.
                await t.goToArea('meadows', { x: 2, y: 2 });
                return null;
            });
            await c.run('верёвка колокола: флаг + повтор с другим текстом', async () => {
                // Звенец через тропу (26,14); верёвка у башни (13,7) — клик издалека.
                await t.goToArea('zvenets', { x: 26, y: 14 });
                await t.ctx.agent.tapTile(13, 7);
                const wr = await t.ctx.agent.waitFor('s.flags.includes("rang_tower")', {
                    timeoutTicks: 1500
                });
                c.expect(wr.ok, 'звон не поднял rang_tower', wr.snapshot.flags);
                const s1 = await t.ctx.agent.snapshot();
                const text1 = s1.lastToast?.text ?? '';
                c.expect(text1.includes('ушёл в землю'), 'первый звон — не тот текст', text1);
                await t.ctx.agent.tapTile(13, 7);
                await t.ctx.agent.step(150);
                const s2 = await t.ctx.agent.snapshot();
                const text2 = s2.lastToast?.text ?? '';
                c.expect(
                    text2.includes('снова') && text2 !== text1,
                    'повторный звон не сменил текст',
                    text2
                );
                return null;
            });
            await c.run('Мила: знакомство -> повтор с веткой по мотам', async () => {
                // Первое знакомство (trader_first): полотно.
                await t.ctx.agent.tapTile(17, 11);
                const d1 = await t.ctx.agent.runDialogue(900);
                c.expect(d1, 'первый диалог с Милой не завершился');
                // Повтор (trader_repeat): при моты >= 1 после первой реплики идёт
                // ветка о мота́х — листаем вручную и ловим её текст.
                await t.ctx.agent.tapTile(17, 11);
                const o = await t.ctx.agent.waitFor('s.dialogue != null', { timeoutTicks: 900 });
                c.expect(o.ok, 'повторный диалог не открылся');
                await t.ctx.agent.press('advance'); // догнать печать (typewriter)
                await t.ctx.agent.press('advance'); // следующая реплика
                const w = await t.ctx.agent.waitFor(
                    's.dialogue != null && (s.dialogue.text ?? "").includes("Моты")',
                    { timeoutTicks: 300 }
                );
                c.expect(w.ok, 'в повторном диалоге нет ветки о мота́х', w.snapshot.dialogue);
                const d2 = await t.ctx.agent.runDialogue(600);
                c.expect(d2, 'повторный диалог не завершился');
                return null;
            });
            await c.run('инвариант: used-флаги и снапшот согласованы', async () => {
                await t.expectInvariantsClean('после обхода мира');
                return null;
            });
        },
        { pretty }
    );
}