200 lines
9.5 KiB
JavaScript
200 lines
9.5 KiB
JavaScript
'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); });
|