Files
fishing-game/tests/shots.js
T
andrey 4ee0d4fce3 Монеты → лавка и хаб «Меню» (Этап 29): клик по #coins в HUD открывает «Лавку рыбака» на вкладке «Лавка» (#tabTackle: «Снасти»→«Лавка», 🎣→🛒; секции «🎣 Удилища»/«🪱 Приманки» внутри не тронуты); openShop(tab?) — аргумент форсит вкладку (syncTabs() вынесен из setTab), HUD 🛒 зовёт без аргумента — помнит последнюю. Новый диалог «Меню» (#gamemenu, из паузы «В меню»): «Продолжить» (uiOpen=false + gameplayStart), Магазин/Достижения/Дневник (fromMenuShop(tab) → openShop), «Инструкции» (#menu, «Играть» вернёт в игру), 🔊 Звук и 🗑️ Очистить — ПЕРЕНЕСЕНЫ из HUD (id btnMute/btnReset сохранены; в .hudbtns остались 🛒 и ⏸). «В меню» больше не ставит state='MENU': сессия (заброс/вываживание) заморожена под оверлеем и продолжается с того же места; state='MENU' — только загрузка/сброс (там #menu с инструкциями — старт новой игры прежний). shopFromMenu: лавка, открытая из «Меню», при × возвращает в хаб (uiOpen=true, без gameplayStart); из HUD/монет — в игру. Z-порядок: #gamemenu в DOM позже #shop/#resetDlg — при открытии лавки/сброса из хаба гменю прячется, иначе перекрыл бы их. YG-счётчики сбалансированы: вход в хаб из паузы (gameplayStop уже сделал onPause), «Продолжить» → gameplayStart (пин §Y8(4): stop +1, start +0). i18n: gm_* ключи RU/EN, tab_tackle 'Лавка'/'Shop', updateHUD печатает 🔊/🔇 + «Звук», setLang дёргает updateHUD. Все emoji набора уже были в субсете встроенного шрифта — перегенерация tools/emoji-font.py не потребовалась. Тесты: §9/Y1/Y2/Y6 флоу btnReset/btnMute — через toGameMenu() (⏸ → «В меню»); §Y6(5) переписан (хаб открыт, state сохранён), (6) заходит к btnPlay через «Инструкции»; новая §Y8 (HUD-состав, монеты→«Лавка», вкладки из хаба, ×→хаб, mute-тогл по семантике — фикстура сейва §«локации» ставит muted:true, «Продолжить», инструкции). 177/177, exit 0. shots.js: кадр gamemenu (15/15), screenshots/ обновлены, визуально проверены (лавка/хаб/сброс/инструкции). orca-playtest.js: сброс 2b — настоящий UI-путь хаба (не синтетический клик по скрытой кнопке); новая секция 9e — переходы по хабу (монеты, Магазин/Достижения/Дневник + × назад, Инструкции, 🔊-тогл через объект — скаляры CLI приходят JSON-строками, «Продолжить»); закрыты давние флейки живого браузера: сейв профиля с ночью прошлого прогона (шаг 2c — форс дня: ночью без фонарика ветка клёва за гейтом hasLight, forceBite не работает), босс 5–40 кг рвал стартовую удочку натяжением до босс-фаз (выдаём «Титан», save.rod=3 — по UI нельзя, M5-гейтинг) и транзитное SNAP-окно 0.6 с vs CLI-roundtrip (ждём персистентный bossPhase, отпускание в том же eval). 76/76. check.mjs OK, pack 5/5, pack-smoke 6/6. Доки: AGENTS.md (177 чеков, «Меню», монеты→лавка, пауза), README (вкладка «Лавка», хаб), DEVLOG Этап 29, BROWSER-TESTING (п. 11–13, таблица шагов 2b/9e/12).
2026-09-09 15:44:04 +03:00

200 lines
9.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'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); });