'use strict'; /* Капчуры игровых состояний для визуальной проверки. * * Запуск (из каталога tests/): * node shots.js # все кадры * node shots.js menu boss # только выбранные кадры (прогон всё равно полный, * # пропускается только сохранение остальных файлов) * node shots.js --list # список доступных кадров * * Кадры: menu charge wait bite reel card card-rv night weather-rain lake boss shop journal gamemenu reset * * Стабильность: вместо фиксированных sleep на ожидание UI — waitForFunction * (DOM-состояние) и двойной requestAnimationFrame (кадр реально отрисован). * Фиксированные sleep остаются только там, где это ВРЕМЯ ИГРЫ (удержание * кнопки при зарядке/забросе), а не ожидание отрисовки. */ const puppeteer = require('puppeteer-core'); const path = require('path'); const GAME_URL = 'file://' + path.resolve(__dirname, '..', 'index.html'); const CHROMIUM = process.env.CHROMIUM || '/usr/bin/chromium-browser'; const sleep = ms => new Promise(r => setTimeout(r, ms)); const FRAMES = ['menu', 'charge', 'wait', 'bite', 'reel', 'card', 'card-rv', 'night', 'weather-rain', 'lake', 'boss', 'shop', 'journal', 'gamemenu', 'reset']; // Какие кадры снимать: argv (имена через пробел) или все. let want; const arg = process.argv[2]; if (arg === '--list') { console.log(FRAMES.join('\n')); process.exit(0); } if (process.argv.length > 2) { const asked = process.argv.slice(2); const bad = asked.filter(a => !FRAMES.includes(a)); if (bad.length) { console.error('Неизвестные кадры: ' + bad.join(', ') + '\nДоступны: ' + FRAMES.join(' ')); process.exit(2); } want = new Set(asked); } else { want = new Set(FRAMES); } async function waitState(page, st, timeout = 8000) { const t0 = Date.now(); while (Date.now() - t0 < timeout) { const s = await page.evaluate(() => state); if (s === st) return s; await sleep(40); } throw new Error('timeout waiting "' + st + '"'); } // Ждём DOM-условие в странице (вместо sleep «на глаз»). function waitFor(page, fn, timeout = 8000) { return page.waitForFunction(fn, { timeout, polling: 50 }); } // Ждём фактической отрисовки: два подряд requestAnimationFrame. const waitPaint = page => page.evaluate(() => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)))); // Скриншот только выбранного кадра. async function snap(page, name) { if (!want.has(name)) return; await page.screenshot({ path: 'shot-' + name + '.png' }); console.log('shot-' + name + '.png'); } (async () => { console.log('кадры: ' + [...want].join(' ')); const browser = await puppeteer.launch({ executablePath: CHROMIUM, headless: true, args: ['--no-sandbox', '--disable-gpu', '--mute-audio'], defaultViewport: { width: 390, height: 844, deviceScaleFactor: 2 }, }); const page = await browser.newPage(); await page.goto(GAME_URL, { waitUntil: 'load' }); await waitFor(page, () => !document.getElementById('menu').classList.contains('hidden')); await waitPaint(page); await snap(page, 'menu'); await page.click('#btnPlay'); await page.mouse.move(195, 600); await page.mouse.down(); await sleep(650); // удержание — это игровое время зарядки, не ожидание отрисовки await waitPaint(page); await snap(page, 'charge'); await page.mouse.up(); await waitState(page, 'WAIT'); await waitPaint(page); await snap(page, 'wait'); await page.evaluate(() => { forceBite = true; biteTimer = 0.05; }); await waitState(page, 'BITE', 4000); await waitPaint(page); await snap(page, 'bite'); await page.mouse.down(); await page.mouse.up(); await waitState(page, 'REEL'); // покажем фазу «тянет» для наглядности await page.evaluate(() => { reel.phase = 'pull'; reel.phaseT = 5; reel.tension = 0.55 * rod().line; reel.d = reel.d0 * 0.55; reel.holding = true; }); await sleep(650); // время ИГРЫ: поплавок доходит до натянутого положения (tip–поплавок–рыба в одну линию) await waitPaint(page); await snap(page, 'reel'); // доловим await page.evaluate(() => { reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5; }); await page.mouse.down(); await waitState(page, 'CARD', 5000); await page.mouse.up(); await waitPaint(page); await snap(page, 'card'); // Y2: rewarded «монеты ×2» — клик по #btnRv (стаб — синхронный бонус), пауза, // кадр карточки с «✓ Бонус получен» и удвоенным cardVal await page.click('#btnRv'); await sleep(300); await waitPaint(page); await snap(page, 'card-rv'); await page.click('#btnCardOk'); await waitState(page, 'IDLE'); // M3: ночь — dayTime 0.85 (фаза night): тёмное небо, луна, тёмная вода; поплавок в воде await page.mouse.move(195, 600); await page.mouse.down(); await sleep(450); await page.mouse.up(); // удержание заброса await waitState(page, 'WAIT'); await page.evaluate(() => { setDayTime(0.85); biteTimer = 30; }); // ночь; клёв отложен, тост не нужен await waitPaint(page); await snap(page, 'night'); // M8: дождь — forceWeather='rain': полоски дождя + пилюля 🌧️ в HUD (как night-кадр: форс состояния) await page.evaluate(() => { forceWeather = 'rain'; }); await waitFor(page, () => rain.length === 40); // частицы дождя инициализированы await waitPaint(page); await snap(page, 'weather-rain'); await page.evaluate(() => { forceWeather = null; }); // Этап 5: Озёра — save.location=1: свой цвет воды (день, dayTime 0.3); дефолт (Река) возвращаем await page.evaluate(() => { save.locations = [0, 1]; save.location = 1; setDayTime(0.3); }); await waitPaint(page); await snap(page, 'lake'); await page.evaluate(() => { save.location = 0; save.locations = [0]; }); // M4: босс — forceBoss + осётр, полный цикл до босс-карточки await page.click('#btnRecall'); await waitState(page, 'IDLE'); await page.evaluate(() => { setDayTime(0.3); // назад к дню window.__pick = pickSpecies; pickSpecies = () => SPECIES.find(s => s.id === 'sturge'); forceBoss = true; SHINY_CHANCE = 0; }); await page.mouse.move(195, 600); await page.mouse.down(); await sleep(450); await page.mouse.up(); // удержание заброса await waitState(page, 'WAIT'); await page.evaluate(() => { forceBite = true; biteTimer = 0.05; }); await waitState(page, 'BITE', 4000); await page.mouse.down(); await page.mouse.up(); await waitState(page, 'REEL'); // босс-бой: фазы 1-2 — d→0 «босс рвёт леску» (SNAP без штрафа) → авто-REEL глубже; фаза 3 — улов for (let phase = 1; phase <= 3; phase++) { await page.evaluate(() => { reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5; }); await page.mouse.down(); if (phase < 3) { await waitState(page, 'SNAP', 5000); await page.evaluate(() => { if (reel) reel.holding = false; }); // в обрыве не втягиваем await page.mouse.up(); await waitState(page, 'REEL', 5000); } else { await waitState(page, 'CARD', 5000); await page.mouse.up(); } } await waitPaint(page); await snap(page, 'boss'); await page.click('#btnCardOk'); await page.evaluate(() => { forceBoss = false; SHINY_CHANCE = 0.03; pickSpecies = window.__pick; }); await page.evaluate(() => { save.coins = 30000; }); await page.click('#btnShop'); await waitFor(page, () => !document.getElementById('shop').classList.contains('hidden')); await waitPaint(page); await snap(page, 'shop'); await page.click('#tabJournal'); await waitFor(page, () => document.getElementById('shopBody').textContent.indexOf('Бросков') >= 0); await waitPaint(page); await snap(page, 'journal'); await page.click('#btnCloseShop'); // Хаб «Меню» (#gamemenu): пауза → «В меню» (там теперь сброс и звук) await page.click('#btnPause'); await waitFor(page, () => !document.getElementById('pause').classList.contains('hidden')); await page.click('#btnPauseMenu'); await waitFor(page, () => !document.getElementById('gamemenu').classList.contains('hidden')); await waitPaint(page); await snap(page, 'gamemenu'); // диалог подтверждения сброса (открывается из «Меню»; закрываем «Отменой» — прогресс не трогаем) await page.click('#btnReset'); await waitFor(page, () => !document.getElementById('resetDlg').classList.contains('hidden')); await waitPaint(page); await snap(page, 'reset'); await page.click('#btnResetNo'); await browser.close(); console.log('screenshots done: ' + [...want].length + '/' + FRAMES.length); process.exit(0); })().catch(e => { console.error('CRASH:', e.message); process.exit(2); });