/**
* Полный прогон акта 1: луга -> Звенец -> квест у Ирвина -> (сбор цветов
* эмулируется var'ом) -> сдача -> кат-сцена с колоколом.
* Запуск: node tools/smoke-act1.mjs [url] [скриншот]
*/
import puppeteer from 'puppeteer-core';
const url = process.argv[2] ?? 'http://localhost:5199/';
const shot = process.argv[3] ?? '/tmp/rpg_act1.png';
const browser = await puppeteer.launch({
executablePath: '/usr/bin/chromium',
headless: true,
// autoplay-policy: в headless ctx.resume() без флага может не резолвиться,
// и навигация, завязанная на звук, зависает.
args: ['--no-sandbox', '--enable-unsafe-swiftshader', '--use-angle=swiftshader',
'--autoplay-policy=no-user-gesture-required', '--window-size=960,540']
});
const page = await browser.newPage();
await page.setViewport({ width: 960, height: 540 });
page.on('console', (msg) => console.log(`[консоль] ${msg.text()}`));
page.on('pageerror', (err) => console.log(`[ошибка страницы] ${err.message}\n${err.stack ?? ''}`));
const booted = new Promise((resolve) => {
page.on('console', (msg) => {
if (msg.text().includes('[boot] ассеты загружены')) resolve();
});
});
await page.goto(url, { waitUntil: 'networkidle2', timeout: 20000 });
await booted;
await new Promise((r) => setTimeout(r, 1500));
// «Новая игра»
await page.mouse.click(480, 283);
await new Promise((r) => setTimeout(r, 2500));
// Усыпить сгустков на лугах: путь героя не должен прерываться уроном.
await page.evaluate(() => {
const g = window.__game;
for (const [, en] of g.scenes.current.combat.enemies) en.brain.putToSleep(9999);
});
// Клик в тайл (tx,ty) текущей локации.
async function clickTile(tx, ty) {
const p = await page.evaluate((tx, ty) => {
const g = window.__game;
const rect = document.querySelector('canvas').getBoundingClientRect();
const wr = g.renderer.worldRoot.position;
const wx = (tx - ty) * 16;
const wy = (tx + ty) * 8 + 8;
return { x: rect.left + ((wx + wr.x) / 480) * rect.width,
y: rect.top + ((wy + wr.y) / 270) * rect.height };
}, tx, ty);
await page.mouse.click(p.x, p.y);
}
// Пошагово к цели: кликаем в соседний тайл от героя по направлению к цели —
// он всегда на экране (цель может уходить за нижний край канваса).
async function walkTo(tx, ty) {
for (let i = 0; i < 60; i++) {
const at = await page.evaluate((tx, ty) => {
const t = window.__game.scenes.current?.player?.currentTile();
return t && t.x === tx && t.y === ty;
}, tx, ty);
if (at) return true;
const step = await page.evaluate((tx, ty) => {
const g = window.__game;
const t = g.scenes.current?.player?.currentTile();
if (!t) return null;
// шаг по доминирующей оси к цели
const nx = Math.abs(tx - t.x) >= Math.abs(ty - t.y)
? t.x + Math.sign(tx - t.x) : t.x;
const ny = nx === t.x ? t.y + Math.sign(ty - t.y) : t.y;
const rect = document.querySelector('canvas').getBoundingClientRect();
const wr = g.renderer.worldRoot.position;
const wx = (nx - ny) * 16;
const wy = (nx + ny) * 8 + 8;
return { x: rect.left + ((wx + wr.x) / 480) * rect.width,
y: rect.top + ((wy + wr.y) / 270) * rect.height, nx, ny };
}, tx, ty);
if (!step) return false;
if (i % 10 === 9) {
const t = await page.evaluate(() => {
const sc = window.__game.scenes.current;
return { tile: sc.player?.currentTile(), hp: sc.playerCombat?.hp };
});
console.log(` ...к (${tx},${ty}): герой в`, JSON.stringify(t));
}
await page.mouse.click(step.x, step.y);
await new Promise((r) => setTimeout(r, 700));
}
return false;
}
// 1) Луга -> тропа в Звенец (26,14) — триггер срабатывает на самом тайле.
console.log('шаг 1: луга -> Звенец');
await walkTo(26, 14);
await new Promise((r) => setTimeout(r, 1500));
// на всякий случай снова усыпить (переход мог вернуть героя в бой)
await page.evaluate(() => {
const g = window.__game;
for (const [, en] of g.scenes.current.combat?.enemies ?? []) en.brain.putToSleep(9999);
});
// 2) Звенец: к Ирвину (12,9).
console.log('шаг 2: к Ирвину');
await walkTo(11, 9);
await new Promise((r) => setTimeout(r, 500));
// Листаем диалог до конца: ждём, пока он откроется (герой мог идти к NPC),
// затем жмём Space, пока не закроется. Лишние клики по миру сбрасывают
// отложенный разговор — поэтому именно Space, а не клики.
async function runDialogue() {
let open = false;
for (let i = 0; i < 25; i++) {
if (await page.evaluate(() => window.__game.scenes.current?.dialogue?.active === true)) {
open = true;
break;
}
await new Promise((r) => setTimeout(r, 200));
}
if (!open) { console.log(' ! диалог не открылся'); return; }
for (let i = 0; i < 12; i++) {
if (!(await page.evaluate(() => window.__game.scenes.current?.dialogue?.active === true))) break;
await page.keyboard.press('Space');
await new Promise((r) => setTimeout(r, 350));
}
}
// 3) Диалог с Ирвином: квест «Три цветка».
console.log('шаг 3: квест у Ирвина');
await clickTile(12, 9);
await runDialogue();
// 4) Сбор цветов — эмуляция var'ом (вылазка проверяется smoke-ponds).
console.log('шаг 4: цветы собраны (эмуляция)');
await page.evaluate(() => {
const g = window.__game;
g.state.setVar('flowers', 3);
});
// 5) Сдача Ирвину -> кат-сцена.
console.log('шаг 5: сдача -> кат-сцена');
await clickTile(12, 9);
await runDialogue();
// 6) Даём кат-сцене (~4с) доиграть до финального тоста.
await new Promise((r) => setTimeout(r, 5000));
const state = await page.evaluate(() => {
const g = window.__game;
return { vars: g.state.serialize().vars, flags: g.state.allFlags };
});
console.log('итог:', JSON.stringify(state));
await page.screenshot({ path: shot });
console.log(`Скриншот: ${shot}`);
await browser.close();