/**
* Сценарий 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 { startDevServer, openGame, Checks } from '../lib.mjs';
/** Реплика героя в elder_first (выбор по тексту — не по индексу). */
const RING_REPLY = 'Прозвоню дорогу до прудов и вернусь до темноты.';
export default async function ({ pretty }) {
const c = new Checks('quest-bells');
const server = await startDevServer();
let ctx;
try {
await c.run('Ирвин: elder_first, выбор по тексту, флаги квеста', async () => {
ctx = await openGame({ url: server.url, newGame: true });
await ctx.agent.command('scene:sleepAll');
await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
// NPC в Звенце: тропа на востоке (26,14), затем Ирвин (12,9).
const walk = await ctx.agent.walkTo(26, 14, { timeoutTicks: 3000 });
c.expect(walk, 'не дошёл до тропы в Звенец');
await ctx.agent.waitFor('s.area === "zvenets"', { timeoutTicks: 600 });
await ctx.agent.waitFor('!s.transitioning', { timeoutTicks: 600 });
await ctx.agent.tapTile(12, 9); // Ирвин — клик издалека
const o = await 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 ctx.agent.snapshot();
if (s.dialogue?.waitingForChoice) break;
if (!s.dialogue) break;
await ctx.agent.press('advance'); // догнать печать или следующая реплика
}
const picked = await ctx.agent.pickChoiceByText(RING_REPLY);
c.expect(picked, 'вариант по тексту не найден', (await ctx.agent.snapshot()).dialogue);
const d = await ctx.agent.runDialogue(900);
c.expect(d, 'диалог не завершился после выбора');
const w = await 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 ctx.agent.command('scene:setVar', { id: 'flowers', value: 3 });
await ctx.agent.tapTile(12, 9);
const o = await ctx.agent.waitFor('s.dialogue?.id === "elder_hand_in"', { timeoutTicks: 900 });
c.expect(o.ok, 'квест-стадия не дала elder_hand_in', o.snapshot.dialogue);
const d = await ctx.agent.runDialogue(900);
c.expect(d, 'диалог сдачи не завершился');
const w = await 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 t = await ctx.agent.waitFor('(s.lastToast?.text ?? "").includes("гудит")', {
timeoutTicks: 900
});
c.expect(t.ok, 'нет тоста посадки', t.snapshot.lastToast);
await ctx.agent.waitFor('!s.cutscene?.active', { timeoutTicks: 900 });
return null;
});
await c.run('Мила: trader_first (полотно) -> trader_after (эпилог)', async () => {
await ctx.agent.tapTile(17, 11);
const o = await ctx.agent.waitFor('s.dialogue?.id === "trader_first"', { timeoutTicks: 900 });
c.expect(o.ok, 'первый диалог Милы не открылся', o.snapshot.dialogue);
const d1 = await ctx.agent.runDialogue(900);
c.expect(d1, 'первый диалог Милы не завершился');
const inv = await 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 ctx.agent.tapTile(17, 11);
const o2 = await ctx.agent.waitFor('s.dialogue?.id === "trader_after"', { timeoutTicks: 900 });
c.expect(o2.ok, 'эпилог trader_after не открылся', o2.snapshot.dialogue);
const d2 = await ctx.agent.runDialogue(600);
c.expect(d2, 'эпилог не завершился');
return null;
});
await c.run('инварианты чисты после полного квеста', 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;
}