Newer
Older
rpg / tools / smoke-act1.mjs
/**
 * Полный прогон акта 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();