Newer
Older
rpg / apps / game / tools / checks / quest-bells.mjs
/**
 * Сценарий quest-bells — полный цикл квеста «Три цветка» (акт 1) через мост.
 * 1) Ирвин: elder_first до выбора, вариант по ТЕКСТУ (pickChoiceByText) ->
 *    флаги met_elder + quest_bells_taken;
 * 2) чит-цветы (scene:setVar flowers=3) -> повторный разговор: elder_hand_in
 *    (квест-стадия ready), флаг quest_bells_done, кат-сцена посадки:
 *    vars.flowers -> 0, тост «Поляна гудит», поляна зеленеет;
 * 3) Мила: trader_first (полотно в сумке, met_mila), затем trader_after
 *    (эпилог из квест-стадии);
 * 4) инвариант interact-used-consistent и контент чисты.
 * Запуск: node tools/agent.mjs run tools/checks/quest-bells.mjs
 */
import { withChecks } from '../lib.mjs';

/** Реплика героя в elder_first (выбор по тексту — не по индексу). */
const RING_REPLY = 'Прозвоню дорогу до прудов и вернусь до темноты.';

export default async function ({ pretty }) {
    return withChecks(
        'quest-bells',
        async (t) => {
            const { c } = t;
            await c.run('Ирвин: elder_first, выбор по тексту, флаги квеста', async () => {
                await t.boot();
                await t.sleepEnemies();
                // NPC в Звенце: тропа на востоке (26,14), затем Ирвин (12,9).
                await t.goToArea('zvenets', { x: 26, y: 14 });
                await t.ctx.agent.tapTile(12, 9); // Ирвин — клик издалека
                const o = await t.ctx.agent.waitFor('s.dialogue?.id === "elder_first"', { timeoutTicks: 900 });
                c.expect(o.ok, 'диалог elder_first не открылся', o.snapshot.dialogue);
                // Листаем до выбора: advance при выборе активирует курсор — проверяем снапшот.
                for (let i = 0; i < 12; i++) {
                    const s = await t.ctx.agent.snapshot();
                    if (s.dialogue?.waitingForChoice) break;
                    if (!s.dialogue) break;
                    await t.ctx.agent.press('advance'); // догнать печать или следующая реплика
                }
                const picked = await t.ctx.agent.pickChoiceByText(RING_REPLY);
                c.expect(picked, 'вариант по тексту не найден', (await t.ctx.agent.snapshot()).dialogue);
                const d = await t.ctx.agent.runDialogue(900);
                c.expect(d, 'диалог не завершился после выбора');
                const w = await t.ctx.agent.waitFor(
                    's.flags.includes("met_elder") && s.flags.includes("quest_bells_taken")',
                    { timeoutTicks: 300 }
                );
                c.expect(w.ok, 'граф не поднял флаги знакомства и взятия квеста', w.snapshot.flags);
                return null;
            });
            await c.run('сдача: elder_hand_in, кат-сцена посадки, поляна зеленеет', async () => {
                // Чит-цветы: сам сбор (пруды) покрыт клик-роутингом, здесь — сдача.
                await t.ctx.agent.command('scene:setVar', { id: 'flowers', value: 3 });
                await t.ctx.agent.tapTile(12, 9);
                const o = await t.ctx.agent.waitFor('s.dialogue?.id === "elder_hand_in"', { timeoutTicks: 900 });
                c.expect(o.ok, 'квест-стадия не дала elder_hand_in', o.snapshot.dialogue);
                const d = await t.ctx.agent.runDialogue(900);
                c.expect(d, 'диалог сдачи не завершился');
                const w = await t.ctx.agent.waitFor(
                    's.flags.includes("quest_bells_done") && s.vars.flowers === 0',
                    { timeoutTicks: 900 }
                );
                c.expect(w.ok, 'нет флага сдачи или цветы не списаны', {
                    flags: w.snapshot.flags,
                    vars: w.snapshot.vars
                });
                // Кат-сцена посадки: тост и конец (клики блокируются до её конца).
                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('Мила: trader_first (полотно) -> trader_after (эпилог)', async () => {
                await t.ctx.agent.tapTile(17, 11);
                const o = await t.ctx.agent.waitFor('s.dialogue?.id === "trader_first"', { timeoutTicks: 900 });
                c.expect(o.ok, 'первый диалог Милы не открылся', o.snapshot.dialogue);
                const d1 = await t.ctx.agent.runDialogue(900);
                c.expect(d1, 'первый диалог Милы не завершился');
                const inv = await t.ctx.agent.waitFor(
                    '(s.inventory.find((i) => i.id === "cloth")?.count ?? 0) === 1',
                    { timeoutTicks: 300 }
                );
                c.expect(inv.ok, 'полотно (do[] giveItem) не в сумке', inv.snapshot.inventory);
                // Повторный разговор: квест-стадия-эпилог -> trader_after.
                await t.ctx.agent.tapTile(17, 11);
                const o2 = await t.ctx.agent.waitFor('s.dialogue?.id === "trader_after"', { timeoutTicks: 900 });
                c.expect(o2.ok, 'эпилог trader_after не открылся', o2.snapshot.dialogue);
                const d2 = await t.ctx.agent.runDialogue(600);
                c.expect(d2, 'эпилог не завершился');
                return null;
            });
            await c.run('инварианты чисты после полного квеста', async () => {
                await t.expectInvariantsClean('после полного квеста');
                return null;
            });
        },
        { pretty }
    );
}