/**
* Сценарий 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 }
);
}