Newer
Older
rpg / apps / game / tools / checks / interact.mjs
/**
 * Сценарий interact — интерьеры и интерактивные объекты.
 * 1) дверь: действие E у дома Ирвина -> интерьер house_elder;
 * 2) сундук (once): предмет в сумке, used:<id>, повтор молчит;
 * 3) очаг: реакция зависит от флага quest_bells_done;
 * 4) записка (once): флаг read_note;
 * 5) выход через проём: step kind 'return' -> Звенец, герой на тайле входа;
 * 6) лавка Милы: прилавок меняет реакцию с флагом;
 * 7) размещение NPC по флагу: act2_hook -> Клинт появляется в Ржавой роще,
 *    действие E у него открывает диалог.
 * Запуск: node tools/agent.mjs run tools/checks/interact.mjs
 */
import { withChecks } from '../lib.mjs';

export default async function ({ pretty }) {
    // Тайл, с которого герой нажал E у двери (цель kind 'return').
    let entryTile = null;
    return withChecks(
        'interact',
        async (t) => {
            const { c } = t;
            await c.run('дверь: дом Ирвина -> интерьер', async () => {
                await t.boot();
                await t.sleepEnemies();
                // Добираемся до Звенца пешком (луга -> тропа (26,14)).
                await t.goToArea('zvenets', { x: 26, y: 14 });
                // Подходим к дому (дверь (7,6)) и жмём действие.
                const opened = await t.interactAt(7, 6);
                c.expect(opened, 'не подошли к дому Ирвина');
                entryTile = (await t.ctx.agent.snapshot()).hero.tile;
                const w = await t.ctx.agent.waitFor('s.area === "house_elder"', { timeoutTicks: 1200 });
                c.expect(w.ok, 'действие у двери не открыло интерьер', { area: w.snapshot.area });
                await t.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
                const s = await t.ctx.agent.snapshot();
                c.expect(s.hero.tile.x === 5 && s.hero.tile.y === 7, 'герой не у проёма (5,7)', s.hero.tile);
                return null;
            });
            await c.run('сундук: предмет + used, повтор молчит', async () => {
                const before = await t.ctx.agent.snapshot();
                const had = before.inventory.find((i) => i.id === 'cloth')?.count ?? 0;
                // Подход к сундуку и действие — герой открывает.
                const opened = await t.useInteractable('chest_elder');
                c.expect(opened, 'не подошли к сундуку');
                const w = await t.ctx.agent.waitFor(
                    `(s.inventory.find((i) => i.id === 'cloth')?.count ?? 0) === ${had + 1}`,
                    { timeoutTicks: 1200 }
                );
                c.expect(w.ok, 'сундук не дал полотно', { inventory: w.snapshot.inventory });
                const s = await t.ctx.agent.snapshot();
                const chest = (s.interactables ?? []).find((o) => o.id === 'chest_elder');
                c.expect(chest?.used === true, 'сундук не помечен used', chest);
                c.expect((s.flags ?? []).includes('used:chest_elder'), 'нет флага used:chest_elder');
                // Повтор: тик последнего тоста не меняется.
                const t0 = s.lastToast?.tick ?? -1;
                await t.useInteractable('chest_elder');
                await t.ctx.agent.step(120);
                const s2 = await t.ctx.agent.snapshot();
                c.expect((s2.lastToast?.tick ?? -1) === t0, 'повторное взаимодействие не молчит', s2.lastToast);
                return null;
            });
            await c.run('очаг: реакция меняется с флагом', async () => {
                await t.useInteractable('hearth_elder');
                await t.ctx.agent.step(120);
                const s1 = await t.ctx.agent.snapshot();
                const text1 = s1.lastToast?.text ?? '';
                c.expect(text1.includes('щепки'), 'реакция очага до флага не та', text1);
                await t.ctx.agent.command('scene:setFlag', { flag: 'quest_bells_done' });
                await t.useInteractable('hearth_elder');
                await t.ctx.agent.step(120);
                const s2 = await t.ctx.agent.snapshot();
                c.expect(
                    (s2.lastToast?.text ?? '').includes('ровнее'),
                    'реакция очага после флага не та',
                    s2.lastToast
                );
                return null;
            });
            await c.run('записка: флаг read_note', async () => {
                await t.useInteractable('note_elder');
                await t.ctx.agent.step(120);
                const s = await t.ctx.agent.snapshot();
                c.expect((s.flags ?? []).includes('read_note'), 'записка не подняла read_note', s.flags);
                return null;
            });
            await c.run('выход через проём: возврат в Звенец', async () => {
                // Пешком в проём (5,8) — step-переход kind 'return' срабатывает на шаге.
                await t.ctx.agent.walkTo(5, 8, { timeoutTicks: 300 });
                const w = await t.ctx.agent.waitFor('s.area === "zvenets"', { timeoutTicks: 1200 });
                c.expect(w.ok, 'проём не вернул в Звенец', { area: w.snapshot.area });
                await t.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
                const s = await t.ctx.agent.snapshot();
                c.expect(
                    s.hero.tile.x === entryTile.x && s.hero.tile.y === entryTile.y,
                    'герой не на тайле, откуда нажал E у двери',
                    { entry: entryTile, after: s.hero.tile }
                );
                return null;
            });
            await c.run('лавка Милы: прилавок меняет реакцию', async () => {
                // Действие у двери лавки (21,9) — интерьер; прилавок внутри.
                const opened = await t.interactAt(21, 9);
                c.expect(opened, 'не подошли к двери лавки');
                const w = await t.ctx.agent.waitFor('s.area === "shop"', { timeoutTicks: 1200 });
                c.expect(w.ok, 'лавка не открылась', { area: w.snapshot.area });
                await t.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
                await t.useInteractable('counter_shop');
                await t.ctx.agent.step(120);
                const s1 = await t.ctx.agent.snapshot();
                const text1 = s1.lastToast?.text ?? '';
                c.expect(text1.includes('соль'), 'реакция прилавка до флага не та', text1);
                await t.useInteractable('counter_shop');
                await t.ctx.agent.step(120);
                const s2 = await t.ctx.agent.snapshot();
                c.expect(
                    (s2.lastToast?.text ?? '').includes('цветы'),
                    'прилавок не изменил реакцию после флага',
                    s2.lastToast
                );
                return null;
            });
            await c.run('снапшот: интерактивные объекты области', async () => {
                const s = await t.ctx.agent.snapshot();
                const list = s.interactables ?? [];
                c.expect(list.some((o) => o.id === 'counter_shop'), 'прилавка нет в снапшоте', list);
                c.expect(list.every((o) => typeof o.used === 'boolean'), 'объект без поля used', list);
                return null;
            });
            await c.run('размещение NPC по флагу: Клинт в роще', async () => {
                // Выходим из лавки обратно в Звенец (проём (5,8)).
                await t.ctx.agent.walkTo(5, 8, { timeoutTicks: 300 });
                const back = await t.ctx.agent.waitFor('s.area === "zvenets"', { timeoutTicks: 1200 });
                c.expect(back.ok, 'не вернулись из лавки в Звенец', { area: back.snapshot.area });
                await t.ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
                // Флаг-крючок акта 2: после него Клинт стоит в Ржавой роще.
                await t.ctx.agent.command('scene:setFlag', { flag: 'act2_hook' });
                // Роща: Звенец (26,10) -> роща (2,10).
                await t.goToArea('rust_grove', { x: 26, y: 10 });
                await t.sleepEnemies();
                const s = await t.ctx.agent.snapshot();
                const clint = (s.npcs ?? []).find((n) => n.id === 'clint');
                c.expect(clint !== undefined, 'Клинта нет в роще после act2_hook', s.npcs);
                if (!clint) return null;
                // Действие у Клинта — сцена ждёт подхода героя (E работает только
                // в радиусе); путь через рощу небыстрый — таймаут с запасом.
                const talked = await t.talkTo('clint');
                c.expect(talked, 'диалог Клинта не открылся');
                const d = await t.ctx.agent.runDialogue(2400);
                c.expect(d, 'диалог Клинта не завершился');
                const s2 = await t.ctx.agent.snapshot();
                c.expect((s2.flags ?? []).includes('met_clint'), 'диалог не поднял met_clint', s2.flags);
                return null;
            });
        },
        { pretty }
    );
}