Newer
Older
rpg / apps / game / tools / checks / resonator.mjs
/**
 * Сценарий resonator — полный цикл квеста «Голос для резонатора» (акт 2).
 * 1) крючок акта 2: флаг act2_hook -> Клинт в Ржавой роще;
 * 2) Клинт: clint_first -> флаги met_clint + quest_resonator_taken;
 * 3) звон у трёх деревьев-механизмов (интерактивы tone_tree_N):
 *    vars.tones растёт до 3, деревья помечены флагами;
 * 4) сдача: clint_tune (квест-стадия ready) -> quest_resonator_done,
 *    кат-сцена настройки: тост «Резонатор держит тон», конец;
 * 5) инварианты чисты после полного квеста.
 * Запуск: node tools/agent.mjs run tools/checks/resonator.mjs
 */
import { withChecks } from '../lib.mjs';

/** Деревья-механизмы (data/quests.ts QUEST_TONE_TILES → интерактивы tone_tree_N). */
const TREES = ['tone_tree_1', 'tone_tree_2', 'tone_tree_3'];

export default async function ({ pretty }) {
    return withChecks(
        'resonator',
        async (t) => {
            const { c } = t;
            await c.run('крючок акта 2: Клинт появляется в роще', async () => {
                await t.boot();
                await t.sleepEnemies();
                await t.goToArea('zvenets', { x: 26, y: 14 });
                await t.sleepEnemies();
                await t.ctx.agent.command('scene:setFlag', { flag: 'act2_hook' });
                // К восточному переходу — телепортом в Звенце: пешком из юго-восточного
                // спавна маршрут уводит через город и мост не доживает.
                await t.ctx.agent.command('scene:teleport', { x: 25, y: 10 });
                await t.goToArea('rust_grove', { x: 26, y: 10 });
                await t.sleepEnemies();
                const s = await t.ctx.agent.snapshot();
                c.expect((s.npcs ?? []).some((n) => n.id === 'clint'), 'Клинта нет в роще после act2_hook', s.npcs);
                return null;
            });
            await c.run('Клинт: clint_first, флаги знакомства и взятия квеста', async () => {
                const talked = await t.talkTo('clint'); // подойти и нажать E
                c.expect(talked, 'не подошли к Клинту');
                const o = await t.ctx.agent.waitFor('s.dialogue?.id === "clint_first"', { timeoutTicks: 2400 });
                c.expect(o.ok, 'диалог clint_first не открылся', o.snapshot.dialogue);
                const d = await t.ctx.agent.runDialogue(900);
                c.expect(d, 'диалог clint_first не завершился');
                const w = await t.ctx.agent.waitFor(
                    's.flags.includes("met_clint") && s.flags.includes("quest_resonator_taken")',
                    { timeoutTicks: 300 }
                );
                c.expect(w.ok, 'граф не поднял флаги знакомства и взятия квеста', w.snapshot.flags);
                return null;
            });
            await c.run('звон у деревьев-механизмов: tones 0 -> 3', async () => {
                for (const [i, id] of TREES.entries()) {
                    const used = await t.useInteractable(id);
                    c.expect(used, `не подошли к ${id}`);
                    const w = await t.ctx.agent.waitFor(
                        `s.vars.tones === ${i + 1} && s.flags.includes("tone_tree_${i + 1}")`,
                        { timeoutTicks: 2400 }
                    );
                    c.expect(w.ok, `${id} не отдало тон`, { vars: w.snapshot.vars });
                }
                return null;
            });
            await c.run('сдача: clint_tune, кат-сцена настройки, флаг done', async () => {
                // Звон у деревьев шумит: краулер у (10,15) просыпается и может
                // убить героя по дороге к Клинту — усыпляем перед сдачей.
                await t.sleepEnemies();
                const talked = await t.talkTo('clint');
                c.expect(talked, 'не подошли к Клинту для сдачи');
                const o = await t.ctx.agent.waitFor('s.dialogue?.id === "clint_tune"', { timeoutTicks: 2400 });
                c.expect(o.ok, 'квест-стадия не дала clint_tune', o.snapshot.dialogue);
                const d = await t.ctx.agent.runDialogue(900);
                c.expect(d, 'диалог clint_tune не завершился');
                const w = await t.ctx.agent.waitFor('s.flags.includes("quest_resonator_done")', {
                    timeoutTicks: 300
                });
                c.expect(w.ok, 'нет флага сдачи резонатора', w.snapshot.flags);
                // Кат-сцена настройки: тост и конец (действие блокируется до её конца).
                const toast = await t.ctx.agent.waitFor('(s.lastToast?.text ?? "").includes("держит тон")', {
                    timeoutTicks: 900
                });
                c.expect(toast.ok, 'нет тоста настройки', toast.snapshot.lastToast);
                await t.ctx.agent.waitFor('!s.cutscene?.active', { timeoutTicks: 900 });
                return null;
            });
            await c.run('инварианты чисты после полного квеста', async () => {
                await t.expectInvariantsClean('после квеста резонатора');
                return null;
            });
        },
        { pretty }
    );
}