gen-screenshots.js: 10 кадров (меню, лавка, достижения, дневник, ожидание, дождь, ночь, Озёра, вываживание, карточка) × 3 вьюпорта
@@ -5,19 +5,22 @@
|
||||
## Проверка (lint/typecheck отсутствуют)
|
||||
|
||||
1. Синтаксис: `node tools/check.mjs` — вынимает JS из `index.html` и прогоняет `node --check` (при ошибке показывает номер строки index.html).
|
||||
2. Главный рубеж — плейтест:
|
||||
2. Главный рубеж — плейтест (запускать после правок `index.html` или `test.js`):
|
||||
```bash
|
||||
cd tests
|
||||
npm install # puppeteer-core
|
||||
npm test # = node test.js
|
||||
```
|
||||
Ждать все PASS и exit 0. Сейчас проверок 177 (включая §Y5 i18n, §Y6 паузу, §Y7 шрифт и §Y8 монеты/«Меню»).
|
||||
3. Браузерный плейтест (вкладка встроенного браузера Orca, через Orca CLI):
|
||||
```
|
||||
Ждать все PASS и exit 0.
|
||||
```
|
||||
3. Браузерный плейтест (вкладка встроенного браузера Orca, через Orca CLI, только по просьбе пользователя):
|
||||
```bash
|
||||
node tests/orca-playtest.js # из каталога репо; требует ЗАПУЩЕННЫЙ Orca app и orca-ide (или ORCA_CLI_COMMAND)
|
||||
```
|
||||
|
||||
Проходит тот же геймплей «вживую» в браузере (не headless) и пишет `orca-*.png` в каталог запуска. Если Orca app не запущен — пропустить и НЕ блокировать (главный рубеж — пункт 2). Подробности и подводные камни: `docs/BROWSER-TESTING.md`.
|
||||
4. Скриншоты: `node tests/gen-screenshots.js [каталог-сборки] [папка-вывода] [язык]` — 15 кадров × 3 вьюпорта (1280×720 / 360×640@2x / 800×450@1.6x), 24-битный PNG без альфа. Без аргументов — русские в `screenshots-wiki/`, с `en` — английские в `screenshots-wiki-en/`. README ссылается на `screenshots-wiki/mobp-*` (мобильный портрет). Emoji рисует встроенный шрифт (раздел «Встроенный emoji-шрифт») — системные emoji-шрифты для скриншотов больше не нужны.
|
||||
4. Скриншоты: `node tests/gen-screenshots.js [каталог-сборки] [папка-вывода] [язык]` — 15 кадров × 3 вьюпорта (1280×720 / 360×640@2x / 800×[450@1.6x](mailto:450@1.6x)), 24-битный PNG без альфа. Без аргументов — русские в `screenshots-wiki/`, с `en` — английские в `screenshots-wiki-en/`. README ссылается на `screenshots-wiki/mobp-*` (мобильный портрет). Emoji рисует встроенный шрифт (раздел «Встроенный emoji-шрифт») — системные emoji-шрифты для скриншотов больше не нужны.
|
||||
5. Публикация в Яндекс Играх: `node tools/pack.mjs` (сборка dist/fishing-yandex.zip + самопроверка 5 пунктов) → `node tests/pack-smoke.js` (smoke играбельности КОПИИ из архива, 6 чек) → загрузить zip в консоль. Доки: раздел README «Публикация в Яндекс Играх», запись Этап 27 в DEVLOG.
|
||||
|
||||
## Подводные камни
|
||||
@@ -30,11 +33,11 @@
|
||||
|
||||
## Где крутить
|
||||
|
||||
- Баланс (рыба/удилища/приманки): константы `SPECIES`, `RODS`, `LURES` в начале скрипта (базы: удочки 875/4500/15000, наживки 625/3000/11250). Апгрейды снастей (уровни 1..4): цена перехода — `upgCost(type,i,lv)`: id 0 — фикс `ROD_UPG_COST`/`LURE_UPG_COST` (70/210/500 и 60/160/400), id>0 — база×`UPG_MULT` (0.2/0.6/1.2, округление до 10); подписи уровней `ROD_UPG_NEXT`/`LURE_UPG_NEXT`, уровни `save.rodLv`/`save.lureLv`, эффективные статы — `effRod()`/`effLure()` (глобалы `rod()`/`lure()` отдают эффективные), гейтинг открытия — в `itemRow()`/`shopClick()`.
|
||||
- Баланс (рыба/удилища/приманки): константы `SPECIES`, `RODS`, `LURES` в начале скрипта (базы: удочки 875/4500/15000, наживки 625/3000/11250). Апгрейды снастей (уровни 1..4): цена перехода — `upgCost(type,i,lv)`: id 0 — фикс `ROD_UPG_COST`/`LURE_UPG_COST` (70/210/500 и 60/160/400), id>0 — база×`UPG_MULT` (0.2/0.6/1.2, округление до 10); подписи уровней `ROD_UPG_NEXT`/`LURE_UPG_NEXT`, уровни `save.rodLv`/`save.lureLv`, эффективные статы — `effRod()`/`effLure()` (глобалы `rod()`/`lure()` отдают эффективные), гейтинг открытия — в `itemRow()`/`shopClick()`.
|
||||
- Ночь (M3): длительность суток `DAY_LENGTH` (сек), ночная таблица рыб `SPECIES_NIGHT`, фонарик — апгрейд 1 любого удилища (`save.rodLv[save.rod]>=2`, проверка `hasLight()` в ветке WAIT, секция «M3: Цикл дня/ночи»).
|
||||
- Босс (M4): тест-форс появления `forceBoss`, фазы боя `stateBoss`/`bossPhase`, босс-ветки в `updateReel()` (обрыв фазы) и `SNAP` в `updateGame()` (авто-переход), награда ×5 в `catchFish()`.
|
||||
- Варианты рыб (M6): таблица `VARIANTS` (порядок ролла: gold → silver → rainbow, иначе normal), `variantChance()` — единственная точка вероятностей (дождь M8 уже здесь — ×2,5, см. строку «Погода (M8)»; gold — живая `SHINY_CHANCE`), тест-форс `forceVariant`, босс всегда normal. Рыбу нельзя улучшать: «Улучшить вариант» (M6) убрано в этапе 21 — карточка улова только бейдж/оформление по варианту.
|
||||
- Погода (M8): таблица `WEATHER` (rain — `variantMult` ×2,5; fog — `biteMult` ×1,5), таймеры в реальном времени — `updateWeather()` в `frame()` (gap `WEATHER_GAP_MIN..MAX` 120–240 с, длительность `WEATHER_DURATION` 120 с, под форсом не гоняются), `weatherActive()` — единственная точка чтения (форс приоритетнее), эффекты только в `variantChance()` (rain) и `biteChance()` (fog), тест-форс `forceWeather` ('rain'/'fog'/null, паттерн `forceBoss`); погода не персистится в сейв. Заметка: туман может дать p>1 → насыщение 100% клёва (осознанно).
|
||||
- Погода (M8): таблица `WEATHER` (rain — `variantMult` ×2,5; fog — `biteMult` ×1,5), таймеры в реальном времени — `updateWeather()` в `frame()` (gap `WEATHER_GAP_MIN..MAX` 120–240 с, длительность `WEATHER_DURATION` 120 с, под форсом не гоняются), `weatherActive()` — единственная точка чтения (форс приоритетнее), эффекты только в `variantChance()` (rain) и `biteChance()` (fog), тест-форс `forceWeather` ('rain'/'fog'/null, паттерн `forceBoss`); погода не персистится в сейв. Заметка: туман может дать p>1 → насыщение 100% клёва (осознанно).
|
||||
- Скины (M-skins): таблицы `BOAT_SKINS`/`BOBBER_SKINS` (лодка: Обычная 0 / «Красная лодка» 800 / «Лодка-домик» 2000 🪙; поплавок: Обычный 0 / Красный 500 / Неоновый 1500 🪙), сейв `save.boatSkin(s)`/`save.bobberSkin(s)` + миграция в `loadSave()` (включая гарантию бесплатного скина 0), вкладка «Скины» 🎨 (`#tabSkins`, строка `skinRow(list,i,owned,equipped,group)`, ветки buyskin/select в `shopClick()`: покупка со списанием и авто-экипировкой, выбор — только купленное), отрисовка `drawBoat()`/`drawBobber()` с clamp 0..2 (скин 2: каюта у лодки, неоновое свечение `shadowBlur` в save/restore у поплавка); чистая косметика — без эффектов на геймплей.
|
||||
- Локации (этап 5): таблица `LOCATIONS` (Река 0 — бесплатно / «Озёра» 100000 🪙; `water` — 6 hex [dayTop,dayMid,dayBot,nightTop,nightMid,nightBot] для градиента `drawWater()`, nightMix-логика та же), сейв `save.locations[]`/`save.location` + миграция в `loadSave()` (гарантия бесплатной Реки, паттерн скинов), вкладка «Локации» 🗺️ (`#tabLocations`, строка `locationRow(loc,i,owned,selected)`, ветки buylocation/selectlocation в `shopClick()`: покупка — списание, push, АВТО-ПЕРЕХОД `save.location`, тост «Добро пожаловать на Озёра!»; выбор — только купленное). Таблицы рыб: `RIVER_SPECIES(_NIGHT)` — ТОЛЬКО 8 базовых видов (роллы реки не меняются: pin-тесты §9j(b)/M4/M6 завязаны на веса и порядок — новые виды в реку НЕ добавлять!), `LAKE_SPECIES(_NIGHT)` — все 11 видов (веса `LAKE_DAY_WEIGHTS`/`LAKE_NIGHT_WEIGHTS`); `SPECIES`/`SPECIES_NIGHT` — полный словарь 11 видов (новые: zander/chuch/crayfish, ночные веса 6/10/2.5). `pickSpecies()` выбирает таблицу по `save.location` и dayPhase (логика ролла та же); босс (sturge) есть в обеих локациях; ачивка species_8 — теперь «Поймай все виды» (все 11, id не менять — старые сейвы).
|
||||
- Сложность вываживания: `updateReel()`.
|
||||
@@ -52,3 +55,4 @@
|
||||
|
||||
- Dev-сервер: `python3 -m http.server 8000`; игра так же работает с `file://`.
|
||||
- Процесс разработки, публикация в Gitea, гигиена секретов: `docs/DEVLOG.md`.
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
|
||||
Браузерная мобильная игра про рыбалку. Один HTML-файл, без зависимостей — просто открой в браузере и играй.
|
||||
|
||||
| Меню | Клёв | Вываживание | Улов |
|
||||
| Меню | Лавка | Вываживание | Улов |
|
||||
|---|---|---|---|
|
||||
|  |  |  |  |
|
||||
|  |  |  |  |
|
||||
|
||||
## Как играть
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
- Дневник рыбака — коллекция, максимальный улов по каждому виду, статистика
|
||||
- Магазин с вкладками: Лавка 🛒, Дневник 📔, Награды 🎯, Достижения 🏅, Скины 🎨, Локации 🗺️
|
||||
- Прогресс и коллекция сохраняются в `localStorage` (старые сейвы мигрируются автоматически)
|
||||
- Диалог «Меню» (пауза ⏸ → «В меню»): «Продолжить» (сессия сохраняется), Магазин/Достижения/Дневник, «Инструкции», звук 🔊 и сброс 🗑️ (через диалог подтверждения; настройка звука при сбросе сохраняется) — <img src="screenshots-wiki/mobp-gamemenu.png" width="200" alt="меню">
|
||||
- Ночь: свой состав рыб и тёмная вода при `dayTime=0.85` — <img src="screenshots-wiki/mobp-night.png" width="200" alt="ночь">
|
||||
- Клик по монетам в HUD открывает лавку на вкладке «Лавка» 🛒
|
||||
- Звуки на WebAudio, вибрация на телефоне
|
||||
- Локализация RU/EN — язык определяется автоматически через SDK Яндекс Игр (`ysdk.environment.i18n.lang`); меню адаптируется под устройство (тач/мышь)
|
||||
@@ -83,4 +83,4 @@ node test.js # 162 проверки
|
||||
|
||||
Есть и браузерный плейтест «вживую» во встроенном браузере Orca: `node tests/orca-playtest.js` (из каталога репо, требует запущенный Orca app) — полный геймплей, проверка консоли и pageerror; детали в [docs/BROWSER-TESTING.md](docs/BROWSER-TESTING.md).
|
||||
|
||||
Скриншоты состояний — `tests/gen-screenshots.js` (headless, 15 кадров × 3 вьюпорта: десктоп 1280×720, мобильный портрет 360×640@2x, мобильный ландшафт 800×450@1.6x, 24-битный PNG без альфа). Без аргументов — русские скриншоты в `screenshots-wiki/`; с аргументом `en` — английские в `screenshots-wiki-en/`. README ссылается на `screenshots-wiki/mobp-*` (мобильный портрет).
|
||||
Скриншоты состояний — `tests/gen-screenshots.js` (headless, 10 кадров × 3 вьюпорта: десктоп 1280×720, мобильный портрет 360×640@2x, мобильный ландшафт 800×450@1.6x, 24-битный PNG без альфа). Без аргументов — русские скриншоты в `screenshots-wiki/`; с аргументом `en` — английские в `screenshots-wiki-en/`. README ссылается на `screenshots-wiki/mobp-*` (мобильный портрет).
|
||||
|
||||
|
After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 169 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 176 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 73 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 79 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 86 KiB After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 288 KiB |
|
Before Width: | Height: | Size: 221 KiB After Width: | Height: | Size: 233 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 122 KiB |
|
Before Width: | Height: | Size: 266 KiB After Width: | Height: | Size: 264 KiB |
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 132 KiB |
|
Before Width: | Height: | Size: 247 KiB After Width: | Height: | Size: 251 KiB |
|
Before Width: | Height: | Size: 111 KiB After Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 131 KiB |
|
After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 298 KiB After Width: | Height: | Size: 300 KiB |
|
Before Width: | Height: | Size: 289 KiB After Width: | Height: | Size: 263 KiB |
|
Before Width: | Height: | Size: 137 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 303 KiB After Width: | Height: | Size: 306 KiB |
|
Before Width: | Height: | Size: 153 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 136 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 316 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 128 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 167 KiB After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 190 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 169 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 172 KiB |
|
Before Width: | Height: | Size: 74 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 195 KiB |
|
Before Width: | Height: | Size: 78 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 81 KiB |
|
Before Width: | Height: | Size: 198 KiB After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 70 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 80 KiB |
|
After Width: | Height: | Size: 248 KiB |
|
Before Width: | Height: | Size: 291 KiB After Width: | Height: | Size: 283 KiB |
|
Before Width: | Height: | Size: 240 KiB After Width: | Height: | Size: 231 KiB |
|
Before Width: | Height: | Size: 126 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 262 KiB After Width: | Height: | Size: 266 KiB |
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 253 KiB After Width: | Height: | Size: 252 KiB |
|
Before Width: | Height: | Size: 115 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 126 KiB |
|
After Width: | Height: | Size: 294 KiB |
|
Before Width: | Height: | Size: 299 KiB After Width: | Height: | Size: 298 KiB |
|
Before Width: | Height: | Size: 290 KiB After Width: | Height: | Size: 261 KiB |
|
Before Width: | Height: | Size: 143 KiB After Width: | Height: | Size: 146 KiB |
|
Before Width: | Height: | Size: 304 KiB After Width: | Height: | Size: 304 KiB |
|
Before Width: | Height: | Size: 155 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 157 KiB |
|
Before Width: | Height: | Size: 318 KiB After Width: | Height: | Size: 296 KiB |
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 127 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 148 KiB |
@@ -0,0 +1,50 @@
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const path = require('path');
|
||||
|
||||
const GAME_DIR = '/home/andrey/work/fishing-game';
|
||||
const GAME = 'file://' + path.join(GAME_DIR, 'index.html');
|
||||
const CHROMIUM = process.env.CHROMIUM || '/usr/bin/chromium-browser';
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROMIUM, headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage', '--mute-audio'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });
|
||||
await page.goto(GAME, { waitUntil: 'load' });
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await page.waitForFunction(() => !document.getElementById('menu').classList.contains('hidden'), { timeout: 8000, polling: 50 });
|
||||
|
||||
await page.click('#btnPlay');
|
||||
await page.waitForFunction(() => state === 'IDLE', { timeout: 3000, polling: 50 });
|
||||
|
||||
await page.mouse.move(640, 360);
|
||||
await page.mouse.down(); await sleep(650); await page.mouse.up();
|
||||
await page.waitForFunction(() => state === 'WAIT', { timeout: 3000, polling: 50 });
|
||||
|
||||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; recall(); });
|
||||
await sleep(100);
|
||||
|
||||
const debug = await page.evaluate(() => {
|
||||
return {
|
||||
state: state,
|
||||
forceBite: forceBite,
|
||||
biteTimer: biteTimer,
|
||||
uiOpen: uiOpen,
|
||||
recallExists: typeof recall === 'function',
|
||||
};
|
||||
});
|
||||
console.log('After recall:', debug);
|
||||
|
||||
try {
|
||||
await page.waitForFunction(() => state === 'BITE', { timeout: 3000, polling: 50 });
|
||||
console.log('BITE reached!');
|
||||
} catch (e) {
|
||||
console.log('BITE timeout, state:', await page.evaluate(() => state));
|
||||
}
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,31 @@
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const path = require('path');
|
||||
|
||||
const GAME_DIR = '/home/andrey/work/fishing-game';
|
||||
const GAME = 'file://' + path.join(GAME_DIR, 'index.html');
|
||||
const CHROMIUM = process.env.CHROMIUM || '/usr/bin/chromium-browser';
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROMIUM, headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage', '--mute-audio'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });
|
||||
await page.goto(GAME, { waitUntil: 'load' });
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await page.waitForFunction(() => !document.getElementById('menu').classList.contains('hidden'), { timeout: 8000, polling: 50 });
|
||||
|
||||
await page.click('#btnPlay');
|
||||
await page.waitForFunction(() => state === 'IDLE', { timeout: 3000, polling: 50 });
|
||||
|
||||
// Try clicking the canvas directly
|
||||
const canvas = await page.$('canvas');
|
||||
await canvas.click();
|
||||
await sleep(100);
|
||||
console.log('After click:', await page.evaluate(() => state));
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,55 @@
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const path = require('path');
|
||||
|
||||
const GAME_DIR = '/home/andrey/work/fishing-game';
|
||||
const GAME = 'file://' + path.join(GAME_DIR, 'index.html');
|
||||
const CHROMIUM = process.env.CHROMIUM || '/usr/bin/chromium-browser';
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROMIUM, headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage', '--mute-audio'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });
|
||||
await page.goto(GAME, { waitUntil: 'load' });
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await page.waitForFunction(() => !document.getElementById('menu').classList.contains('hidden'), { timeout: 8000, polling: 50 });
|
||||
|
||||
await page.click('#btnPlay');
|
||||
await page.waitForFunction(() => state === 'IDLE', { timeout: 3000, polling: 50 });
|
||||
|
||||
// Move to canvas center and press mouse
|
||||
await page.evaluate(() => {
|
||||
const canvas = document.querySelector('canvas');
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const event = new PointerEvent('pointerdown', {
|
||||
bubbles: true, cancelable: true,
|
||||
clientX: rect.left + rect.width / 2,
|
||||
clientY: rect.top + rect.height / 2,
|
||||
button: 0,
|
||||
});
|
||||
canvas.dispatchEvent(event);
|
||||
});
|
||||
await sleep(100);
|
||||
console.log('After pointerdown:', await page.evaluate(() => state));
|
||||
|
||||
// Release
|
||||
await page.evaluate(() => {
|
||||
const canvas = document.querySelector('canvas');
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const event = new PointerEvent('pointerup', {
|
||||
bubbles: true, cancelable: true,
|
||||
clientX: rect.left + rect.width / 2,
|
||||
clientY: rect.top + rect.height / 2,
|
||||
button: 0,
|
||||
});
|
||||
canvas.dispatchEvent(event);
|
||||
});
|
||||
await sleep(100);
|
||||
console.log('After pointerup:', await page.evaluate(() => state));
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -0,0 +1,35 @@
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const path = require('path');
|
||||
|
||||
const GAME_DIR = '/home/andrey/work/fishing-game';
|
||||
const GAME = 'file://' + path.join(GAME_DIR, 'index.html');
|
||||
const CHROMIUM = process.env.CHROMIUM || '/usr/bin/chromium-browser';
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROMIUM, headless: true,
|
||||
args: ['--no-sandbox', '--disable-dev-shm-usage', '--mute-audio'],
|
||||
});
|
||||
const page = await browser.newPage();
|
||||
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });
|
||||
await page.goto(GAME, { waitUntil: 'load' });
|
||||
await page.evaluate(() => localStorage.clear());
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await page.waitForFunction(() => !document.getElementById('menu').classList.contains('hidden'), { timeout: 8000, polling: 50 });
|
||||
|
||||
await page.click('#btnPlay');
|
||||
await page.waitForFunction(() => state === 'IDLE', { timeout: 3000, polling: 50 });
|
||||
|
||||
// Use page.mouse.move + down + up
|
||||
await page.mouse.move(640, 360);
|
||||
await page.mouse.down();
|
||||
await sleep(100);
|
||||
console.log('After mouse.down:', await page.evaluate(() => state));
|
||||
|
||||
await page.mouse.up();
|
||||
await sleep(100);
|
||||
console.log('After mouse.up:', await page.evaluate(() => state));
|
||||
|
||||
await browser.close();
|
||||
})();
|
||||
@@ -3,14 +3,11 @@
|
||||
* Генерация скриншотов «Рыбалки» — витринный набор (страница вики «Скриншоты»,
|
||||
* материалы для Яндекс Игр).
|
||||
*
|
||||
* Снимает 15 состояний игры (меню, зарядка, ожидание, поклёвка, вываживание,
|
||||
* карточка улова, карточка с RV-бонусом, ночь, дождь, Озёра, босс, лавка,
|
||||
* дневник, хаб «Меню», диалог сброса) на трёх вьюпортах:
|
||||
* Снимает 10 состояний игры (меню, лавка, достижения, дневник, ожидание дня,
|
||||
* дождь, ночь, Озёра, вываживание, карточка улова) на трёх вьюпортах:
|
||||
* - desk: 1280x720 @1x (десктоп, 16:9)
|
||||
* - mobp: 360x640 @2x -> 720x1280 (мобильный, портрет, 9:16)
|
||||
* - mobl: 800x450 @1.6x -> 1280x720 (мобильный, ландшафт, 16:9;
|
||||
* CSS 800x450: мобильная «низкая» ветка @media(max-height:620px), но
|
||||
* карточка улова целиком влезает — на 640x360@2x дно уезжает за экран)
|
||||
* - mobl: 800x450 @1.6x -> 1280x720 (мобильный, ландшафт, 16:9)
|
||||
*
|
||||
* Требования Яндекс Игр: пропорции 16:9 / 9:16, длинная сторона 1280..2560,
|
||||
* JPEG или 24-разрядный PNG без альфа-канала. Скрипт пишет именно 24-битный
|
||||
@@ -35,20 +32,15 @@
|
||||
*
|
||||
* Детерминизм:
|
||||
* - новый сейв на каждом вьюпорте (localStorage.clear) — кадры согласованы;
|
||||
* - поклёвка форсится тест-глобалом forceBite;
|
||||
* - карточка улова — всегда золотой осётр 12.5 кг (SHINY_CHANCE=1);
|
||||
* цена catchFish() жмитится фактором (0.95+0.1·rnd) — rnd шпилится в 0.5,
|
||||
* фактор = ровно 1.0, итог +2438 во всех трёх сетках;
|
||||
* - закрытие карточки: реальный клик + DOM-фолбэк + жёсткое ожидание
|
||||
* (без «проглоченных» таймаутов — лавка не снимается поверх карточки).
|
||||
*
|
||||
* Зависит от тест-глобалов игры (AGENTS.md, «Подводные камни»):
|
||||
* state, forceBite, forceBoss, forceWeather, biteTimer, reel, catchFish,
|
||||
* SHINY_CHANCE, SPECIES, setDayTime, pickSpecies, save, dayTime, dayPhase,
|
||||
* forceLang, setLang
|
||||
* и DOM-кнопок: #btnPlay, #btnRv, #btnCardOk, #btnShop, #tabJournal,
|
||||
* #btnCloseShop, #btnPause, #btnPauseMenu, #btnReset, #btnResetNo, #card,
|
||||
* #shop, #pause, #gamemenu, #resetDlg, #menu.
|
||||
* state, forceBite, forceWeather, biteTimer, reel, catchFish, SHINY_CHANCE,
|
||||
* SPECIES, setDayTime, save, dayTime, dayPhase, forceLang, setLang
|
||||
* и DOM-кнопок: #btnPlay, #btnShop, #tabAch, #tabJournal, #btnCloseShop,
|
||||
* #btnCardOk, #card, #shop, #menu.
|
||||
*/
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
@@ -66,8 +58,7 @@ const VIEWPORTS = [
|
||||
{ key: 'mobp', w: 360, h: 640, dsf: 2 },
|
||||
{ key: 'mobl', w: 800, h: 450, dsf: 1.6 },
|
||||
];
|
||||
const FRAMES = ['menu', 'charge', 'wait', 'bite', 'reel', 'card', 'card-rv', 'night',
|
||||
'weather-rain', 'lake', 'boss', 'shop', 'journal', 'gamemenu', 'reset'];
|
||||
const FRAMES = ['menu', 'shop', 'ach', 'journal', 'wait', 'weather-rain', 'night', 'lake', 'reel', 'card'];
|
||||
|
||||
// ---------- 24-битный PNG (без альфа): ffmpeg предпочтительнее, иначе ImageMagick
|
||||
function findBin(name) {
|
||||
@@ -136,29 +127,75 @@ async function captureSet(browser, vp) {
|
||||
await page.click('#btnPlay');
|
||||
await waitState(page, 'IDLE');
|
||||
|
||||
// зарядка: удержание заряжает бросок (CHARGE)
|
||||
// лавка рыбака — вкладка «Лавка» (tackle)
|
||||
await page.click('#btnShop');
|
||||
await page.waitForFunction(
|
||||
() => !document.getElementById('shop').classList.contains('hidden'),
|
||||
{ timeout: 3000, polling: 50 });
|
||||
await sleep(300);
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-shop.png`);
|
||||
|
||||
// достижения — вкладка #tabAch
|
||||
await page.click('#tabAch');
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('tabAch').classList.contains('active'),
|
||||
{ timeout: 3000, polling: 50 });
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-ach.png`);
|
||||
|
||||
// дневник — вкладка #tabJournal
|
||||
await page.click('#tabJournal');
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('shopBody').textContent.indexOf('Бросков') >= 0,
|
||||
{ timeout: 3000, polling: 50 });
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-journal.png`);
|
||||
await page.click('#btnCloseShop');
|
||||
await waitState(page, 'IDLE', 3000);
|
||||
|
||||
// ожидание днём — заброс + WAIT
|
||||
await page.mouse.move(cx, cy);
|
||||
await page.mouse.down();
|
||||
await waitState(page, 'CHARGE', 3000);
|
||||
await sleep(650);
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-charge.png`);
|
||||
|
||||
// заброс
|
||||
await page.mouse.up();
|
||||
await waitState(page, 'WAIT');
|
||||
await sleep(1200);
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-wait.png`);
|
||||
|
||||
// поклёвка (тест-форс: рыба идёт на крючок, поплавок тонет)
|
||||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||||
await waitState(page, 'BITE', 4000);
|
||||
await sleep(300);
|
||||
// дождь — forceWeather='rain': полоски дождя + пилюля 🌧️ в HUD
|
||||
await page.evaluate(() => { forceWeather = 'rain'; });
|
||||
await sleep(500);
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-bite.png`);
|
||||
await shoot(page, `${vp.key}-weather-rain.png`);
|
||||
await page.evaluate(() => { forceWeather = null; });
|
||||
|
||||
// подсечка -> бой; эффектная рыба (осётр 12.5 кг, фазы детерминированы)
|
||||
// ночь — dayTime 0.85 (фаза night)
|
||||
await page.evaluate(() => { setDayTime(0.85); });
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-night.png`);
|
||||
await page.evaluate(() => { setDayTime(0.3); });
|
||||
|
||||
// Озёра — save.location=1, день, dayTime 0.3
|
||||
await page.evaluate(() => { save.locations = [0, 1]; save.location = 1; setDayTime(0.3); });
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-lake.png`);
|
||||
await page.evaluate(() => { save.location = 0; save.locations = [0]; });
|
||||
|
||||
// вываживание: recall → заброс + клёв + подсечка → REEL
|
||||
// biteTimer сбрасывается при завершении CAST, поэтому ставим его после заброса
|
||||
await page.evaluate(() => { forceBite = true; recall(); });
|
||||
await page.mouse.move(cx, cy);
|
||||
await page.mouse.down();
|
||||
await waitState(page, 'CHARGE', 3000);
|
||||
await sleep(650);
|
||||
await page.mouse.up();
|
||||
await waitState(page, 'WAIT');
|
||||
await page.evaluate(() => { biteTimer = 0.05; });
|
||||
await waitState(page, 'BITE', 4000);
|
||||
await page.mouse.down(); await page.mouse.up();
|
||||
await waitState(page, 'REEL');
|
||||
await page.evaluate(() => {
|
||||
@@ -177,7 +214,6 @@ async function captureSet(browser, vp) {
|
||||
await page.mouse.up();
|
||||
|
||||
// карточка улова: золотой осётр 12.5 кг (детерминированно);
|
||||
// цена catchFish() жмитится (0.95+0.1·rnd) — rnd шпилится в 0.5 -> фактор 1.0
|
||||
await page.evaluate(() => {
|
||||
SHINY_CHANCE = 1;
|
||||
if (reel) { reel.w = 12.5; reel.boss = false; }
|
||||
@@ -188,137 +224,12 @@ async function captureSet(browser, vp) {
|
||||
}
|
||||
});
|
||||
await waitState(page, 'CARD', 4000);
|
||||
// ждём, пока стихнут тосты (улов/ачивка/квест; toast() гаснет через 1.6 с)
|
||||
await sleep(2200);
|
||||
await waitPaint(page);
|
||||
console.log(' [card]', await page.evaluate(() =>
|
||||
state + ' card=' + document.getElementById('card').classList.contains('hidden')
|
||||
+ ' shop=' + document.getElementById('shop').classList.contains('hidden')));
|
||||
state + ' card=' + document.getElementById('card').classList.contains('hidden')));
|
||||
await shoot(page, `${vp.key}-card.png`);
|
||||
|
||||
// карточка с RV-бонусом: клик по #btnRv (стаб — синхронный бонус ×2)
|
||||
await page.click('#btnRv');
|
||||
await sleep(300);
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-card-rv.png`);
|
||||
|
||||
// закрытие карточки: реальный клик + DOM-фолбэк + жёсткое ожидание
|
||||
await page.click('#btnCardOk');
|
||||
const cardHidden = () => page.waitForFunction(
|
||||
() => document.getElementById('card').classList.contains('hidden'),
|
||||
{ timeout: 2500, polling: 50 }).then(() => true).catch(() => false);
|
||||
if (!(await cardHidden())) {
|
||||
console.log(' btnCardOk: клик мимо, DOM-клик');
|
||||
await page.evaluate(() => document.getElementById('btnCardOk').click());
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('card').classList.contains('hidden'),
|
||||
{ timeout: 2500, polling: 50 });
|
||||
}
|
||||
await waitState(page, 'IDLE', 5000);
|
||||
|
||||
// M3: ночь — новый заброс + dayTime 0.85 (фаза night)
|
||||
await page.mouse.move(cx, cy);
|
||||
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 shoot(page, `${vp.key}-night.png`);
|
||||
|
||||
// M8: дождь — forceWeather='rain': полоски дождя + пилюля 🌧️ в HUD
|
||||
await page.evaluate(() => { forceWeather = 'rain'; });
|
||||
await sleep(500);
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-weather-rain.png`);
|
||||
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 shoot(page, `${vp.key}-lake.png`);
|
||||
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(cx, cy);
|
||||
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 shoot(page, `${vp.key}-boss.png`);
|
||||
await page.click('#btnCardOk');
|
||||
await page.evaluate(() => {
|
||||
forceBoss = false; SHINY_CHANCE = 0.03;
|
||||
pickSpecies = window.__pick;
|
||||
});
|
||||
await waitState(page, 'IDLE', 5000);
|
||||
|
||||
// лавка рыбака
|
||||
await page.evaluate(() => { save.coins = 30000; });
|
||||
await page.click('#btnShop');
|
||||
await page.waitForFunction(
|
||||
() => !document.getElementById('shop').classList.contains('hidden'),
|
||||
{ timeout: 3000, polling: 50 });
|
||||
await sleep(300);
|
||||
await waitPaint(page);
|
||||
console.log(' [shop]', await page.evaluate(() =>
|
||||
state + ' card=' + document.getElementById('card').classList.contains('hidden')
|
||||
+ ' shop=' + document.getElementById('shop').classList.contains('hidden')));
|
||||
await shoot(page, `${vp.key}-shop.png`);
|
||||
|
||||
// дневник: вкладка #tabJournal
|
||||
await page.click('#tabJournal');
|
||||
await page.waitForFunction(
|
||||
() => document.getElementById('shopBody').textContent.indexOf('Бросков') >= 0,
|
||||
{ timeout: 3000, polling: 50 });
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-journal.png`);
|
||||
await page.click('#btnCloseShop');
|
||||
|
||||
// Хаб «Меню» (#gamemenu): пауза → «В меню»
|
||||
await page.click('#btnPause');
|
||||
await page.waitForFunction(
|
||||
() => !document.getElementById('pause').classList.contains('hidden'),
|
||||
{ timeout: 3000, polling: 50 });
|
||||
await page.click('#btnPauseMenu');
|
||||
await page.waitForFunction(
|
||||
() => !document.getElementById('gamemenu').classList.contains('hidden'),
|
||||
{ timeout: 3000, polling: 50 });
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-gamemenu.png`);
|
||||
|
||||
// диалог подтверждения сброса (открывается из «Меню»; закрываем «Отменой»)
|
||||
await page.click('#btnReset');
|
||||
await page.waitForFunction(
|
||||
() => !document.getElementById('resetDlg').classList.contains('hidden'),
|
||||
{ timeout: 3000, polling: 50 });
|
||||
await waitPaint(page);
|
||||
await shoot(page, `${vp.key}-reset.png`);
|
||||
await page.click('#btnResetNo');
|
||||
|
||||
await page.close();
|
||||
}
|
||||
|
||||
|
||||