diff --git a/.gitignore b/.gitignore index 533e2d7..3a8cc3e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,10 +7,16 @@ tests/shot-*.png *.log .DS_Store +# Артефакт сборки для Яндекс Игр (node tools/pack.mjs) +dist/ + # Локальные worktree спринтов (Orca) sprint3-*/ sprint2-*/ sprint4-*/ +# Локальная документация (защита от случайного git add -A) +/*.pdf + # Секреты (никогда не коммитить) .gitea_token diff --git a/AGENTS.md b/AGENTS.md index 70bc111..35a72ce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,19 +11,20 @@ npm install # puppeteer-core npm test # = node test.js ``` - Ждать все PASS и exit 0. Сейчас проверок 129. + Ждать все PASS и exit 0. Сейчас проверок 146. 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/shots.js` пишет `shot-*.png` в каталог запуска (запускать из `tests/`); README ссылается на `screenshots/` в корне — свежие скопировать туда. В headless нужен шрифт emoji (Noto Color Emoji), иначе в скриншотах квадраты. +5. Публикация в Яндекс Играх: `node tools/pack.mjs` (сборка dist/fishing-yandex.zip + самопроверка 5 пунктов) → `node tests/pack-smoke.js` (smoke играбельности КОПИИ из архива, 6 чек) → загрузить zip в консоль. Доки: раздел README «Публикация в Яндекс Играх», запись Этап 27 в DEVLOG. ## Подводные камни - `tests/test.js` берёт браузер из `process.env.CHROMIUM || '/usr/bin/chromium-browser'`, а URL игры резолвит динамически от каталога репо (тот же паттерн, что в `shots.js`). Если тест падает на старте — поправьте имя/путь chromium под свой чекаут (переменная `CHROMIUM`). - Граница полуночи: если прогон стартует в пределах 5 минут от 00:00, test.js сам ждёт безопасное окно (дата не должна смениться посреди прогона — падают date-ассерты квестов/стрика). Это нормально, не баг. -- Тесты гоняют игру через внутренние глобалы в `page.evaluate`: `state`, `save`, `biteTimer`, `reel`, `SPECIES`, `SPECIES_NIGHT`, `rod()`, `SHINY_CHANCE`, `forceBoss`, `forceBite`, `forceVariant`, `VARIANTS`, `rollVariant()`, `variantChance()`, `fishForce()`, `stateBoss`, `bossPhase`, `dayPhase`, `pickSpecies()`, `setDayTime()`, `todayStr()`, `dateShift()`, `isoWeek()`, `achBonus()`, `forceWeather`, `weather`, `weatherT`, `weatherGap`, `WEATHER`, `WEATHER_GAP_MIN`, `WEATHER_GAP_MAX`, `WEATHER_DURATION`, `weatherActive()`, `BOAT_SKINS`, `BOBBER_SKINS`, `LOCATIONS`, `RIVER_SPECIES`, `RIVER_SPECIES_NIGHT`, `LAKE_SPECIES`, `LAKE_SPECIES_NIGHT`, `SAVE_KEY`, `loadSave`, `freshSave`, `ACHIEVEMENTS`, `checkAchievements`, `questText` и имена состояний `MENU/IDLE/CHARGE/CAST/WAIT/APPROACH/BITE/REEL/SNAP/CARD`. Переименование любого из них ломает тесты молча — имена держать, либо обновлять тест в том же коммите. +- Тесты гоняют игру через внутренние глобалы в `page.evaluate`: `state`, `save`, `biteTimer`, `reel`, `SPECIES`, `SPECIES_NIGHT`, `rod()`, `SHINY_CHANCE`, `forceBoss`, `forceBite`, `forceVariant`, `VARIANTS`, `rollVariant()`, `variantChance()`, `fishForce()`, `stateBoss`, `bossPhase`, `dayPhase`, `pickSpecies()`, `setDayTime()`, `todayStr()`, `dateShift()`, `isoWeek()`, `achBonus()`, `forceWeather`, `weather`, `weatherT`, `weatherGap`, `WEATHER`, `WEATHER_GAP_MIN`, `WEATHER_GAP_MAX`, `WEATHER_DURATION`, `weatherActive()`, `BOAT_SKINS`, `BOBBER_SKINS`, `LOCATIONS`, `RIVER_SPECIES`, `RIVER_SPECIES_NIGHT`, `LAKE_SPECIES`, `LAKE_SPECIES_NIGHT`, `SAVE_KEY`, `loadSave`, `freshSave`, `ACHIEVEMENTS`, `checkAchievements`, `questText`, `YG` (и `YG.counts`) и имена состояний `MENU/IDLE/CHARGE/CAST/WAIT/APPROACH/BITE/REEL/SNAP/CARD`. Переименование любого из них ломает тесты молча — имена держать, либо обновлять тест в том же коммите. - Ввод на Pointer Events (`pointerdown`/`pointerup`), а не touch/mouse — так тест управляет игрой мышью. Не заменять на тач-обработчики. - Сейв: ключ localStorage `fishing-save-v1`. При изменении формата — миграцию добавлять в `loadSave()` (старые сейвы обязаны оставаться читаемыми). @@ -39,6 +40,8 @@ - Сложность вываживания: `updateReel()`. - Физика боя (спринт 4): `fishForce()` (сила рыбы: рывок `(16+34·st)·(0.5+0.9·ag)·(0.7+0.5·ov)`, затишье `5+7·ag`, уставшая ×0.45) и `updateReel()` — выносливость `st` (старт `st0`, флаг `tired` при `st≤0.12`: рывки ×0.45, втягивание быстрее до ×1.6), тормоз катушки (черта `drag` в `RODS`: в рывке выше тормоза леска скользит `over·0.22`), натяжение (`10+over·1.1+22·ag`, ×0.35 в затишье), втягивание `(11+3·reel)·(1+0.6·(1−st))`, свободный бег `F·0.32`, escape-бар растёт только при затишье без удержания (отпускание в рывок безопасно), near-miss `1.45·d0`, `d0=45+7·w`. Пустые клёвы: ролл `biteChance()` в ветке WAIT (тест-форс `forceBite`). Тесты (§10) завязаны на эти константы — при ребалансе гонять Monte-Carlo точными формулами (docs/PHYSICS-RESEARCH.md, §4.7) и править тесты в том же коммите. - Награды (M2): шаблоны квестов `QUEST_TPLS`, награды стрика `STREAK_REWARDS`, достижения `ACHIEVEMENTS` — секция «M2. Награды» после сейва. Этап 22: ачивок 29 (идут `ACHIEVEMENTS`-записями по темам: снасти у экономики, варианты/погода/вес/ночь у рыболовных, скины/локации у магазинных); новые счётчики сейва `save.kgTotal`/`nightCatch`/`wxRainV`/`wxFog` (чтение через `||0` — без миграции) ставятся в `catchFish()` до `checkAchievements()` (rain-счётчик — только вариант ≠ normal, босс всегда normal); тест-пин §10c(8): сумма `achBonus().coinPct` (все 8 видов × 4 варианта) = 0.16. + - SDK Яндекс Игр (Y1): секция «Y1: SDK Яндекс Игр» перед «ЦИКЛ» — обёртка YG (стаб при file:// без ?sdk=1: скрипт не создаётся, no-op'ы + счётчики counts), динамическая загрузка /sdk.js (http(s)) с retry/warn, LoadingAPI.ready() первым кадром, GameplayAPI.start/stop в btnPlay/openShop/closeShop/btnReset/btnResetNo, game_api_pause/resume + visibilitychange → YG.onPause/onResume (AC.suspend, пауза всего игрового времени в frame()). + - Реклама (Y2): константы `AD_FIRST_MS`/`AD_COOLDOWN_MS` (секция Y1), точка interstitial — `tryInterstitial()` в хендлере `btnCardOk` (гейт: первый показ ≥2 мин сессии, кулдаун 3 мин; `lastAdTs` — осознанно «попытка», не показ — при троттлинге платформы wasShown=false кулдаун сгорает), RV-кнопка `#btnRv` «📺 Реклама: ×2 за этот улов» на карточке улова (`showCard` сбрасывает `rvVal`/`rvDone`), методы `YG.showFullscreen/showRewarded` (+watchdog 90 с), `endAd()`; карточка улова БЕЗ `gameplayStop()` — осознанно (часть цикла улова); реклама не персистится в сейв (только `persist()` перед показом и после бонуса). ## Запуск и публикация diff --git a/README.md b/README.md index dbe05a6..ea3c45b 100644 --- a/README.md +++ b/README.md @@ -55,14 +55,25 @@ python3 -m http.server 8000 # → http://localhost:8000 ``` +## Публикация в Яндекс Играх + +Архив для платформы (только `index.html` в корне архива, с самопроверкой — набор файлов, размер, имена, sha256, синтаксис JS): + +```bash +node tools/pack.mjs # → dist/fishing-yandex.zip +node tests/pack-smoke.js # перед загрузкой в консоль: smoke упакованной копии (игра из архива, 6 проверок) +``` + +SDK подхватывается автоматически на площадке; локально (file://) игра работает без него (встроенный стаб). Локальная отладка с SDK: открыть игру с параметром `?sdk=1` и положить `sdk.js` рядом с игрой (или поднять локальный прокси SDK Яндекса). В консоли разработчика (games.yandex.ru): включить монетизацию, опция облачных сейвов — выключена (игра сама хранит сейв в `localStorage`), платформы — без Android TV, `debug-mode=16` — для проверки лоадера. + ## Тесты -Автотест прогоняет весь геймплей (бросок, подсечка, все исходы вываживания, магазин, дневник, сохранение, варианты рыб (золотая/серебряная/радужная), near-miss, миграция сейва, квесты, стрик, достижения, день/ночь с фонариком, босс-рыба в 3 фазы) в headless Chromium: +Автотест прогоняет весь геймплей (бросок, подсечка, все исходы вываживания, магазин, дневник, сохранение, варианты рыб (золотая/серебряная/радужная), near-miss, миграция сейва, квесты, стрик, достижения, день/ночь с фонариком, босс-рыба в 3 фазы, SDK-стаб (YG), реклама: RV-бонус и гейты interstitial) в headless Chromium: ```bash cd tests npm install # ставит puppeteer-core -node test.js # 129 проверок +node test.js # 146 проверок ``` Нужен установленный Chromium (`executablePath` в `test.js` при необходимости поправить). diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index bda66e5..86d90bc 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -470,3 +470,75 @@ git add -A && git commit -m "что изменилось" && git push **Заметки (осознанные решения и ловушки):** - Закрывает камень #12 из Этапа T4: утешительные выплаты за сходы давали 43–47% раннего дохода (фармить сходы было выгоднее осторожной игры). - `fishEscaped(sp)` — единственная точка исхода «рыба ушла»: обе ветки сходятся в неё, новые тексты/звук — править в одном месте. + +## Этап 24. Y1: SDK Яндекс Игр + +**Что сделано в `index.html`:** + +1. **Обёртка YG** — секция «Y1: SDK Яндекс Игр» перед «ЦИКЛ», единственная точка контакта с платформой: стаб при file:// без `?sdk=1` (скрипт `/sdk.js` не создаётся — чистая консоль; no-op'ы + счётчики вызовов `YG.counts` для тестов) и http(s) — динамическая загрузка `/sdk.js` (один ретраи, warn, 3-сек slow-warn), апгрейд стаб → реальный SDK в `YG.init()` (однократно, catch-up `ready()`, если SDK доинициализировался позже). +2. **`LoadingAPI.ready()`** — первым кадром `frame()` + catch-up в `init()`. +3. **`GameplayAPI.start/stop`** — btnPlay/openShop/closeShop/btnReset/btnResetNo; карточка улова — без stop (часть цикла улова, осознанно). +4. **Пауза платформы** — `game_api_pause/resume` + `visibilitychange` → `YG.onPause/onResume` (`AC.suspend()`; при `YG.paused` в `frame()` заморожено всё игровое время). + +**Тесты:** +7 проверок в `tests/test.js` (§Y1: стаб в file:// — SDK не создан и счётчики на месте, `ready()` ровно один раз первым кадром, start/stop-разметка btnPlay/btnShop/btnCloseShop/btnReset/btnResetNo по `YG.counts`, пауза/резюме — заморозка frame()). Итог **136/136 PASS, exit 0** (`node tools/check.mjs` — OK). Ревью F1–F7 закрыты (заморозка frame(), loader-ретраи, src по протоколу, guard btnPlay, доки). + +**Monte-Carlo:** не требовался — физика вываживания не тронута; SDK не влияет на геймплей. + +**Заметки (осознанные решения и ловушки):** +- Стаб при file:// без `?sdk=1` — осознанно: скрипт SDK не создаётся (чистая консоль), игра остаётся полностью играбельной вне платформы. +- Резюме (`YG.onResume`) не снимается, пока открыта реклама — снимает `endAd()` (guard `adShowing`; путь: visibilitychange/game_api_resume, а оверлей рекламы ещё открыт). + +## Этап 25. Y2: Монетизация + +**Что сделано в `index.html`:** + +1. **Interstitial** через `ysdk.adv.showFullscreenAdv` — на закрытии карточки улова (`tryInterstitial()` в хендлере `btnCardOk`): логическая пауза (треб. 4.4), гейты `AD_FIRST_MS` (первый показ не раньше 2 мин сессии) / `AD_COOLDOWN_MS` (между показами 3 мин), `persist()` **ДО** показа (4.2 — улов не теряется при убийстве страницы платформой), `YG.onPause` — звук + процесс (4.7), watchdog 90 с (1.14) — если SDK не закрыл рекламу, паузу снимаем сами. +2. **Rewarded «монеты ×2»** — RV-кнопка `#btnRv` «📺 Реклама: ×2 за этот улов» на карточке улова через `ysdk.adv.showRewardedVideo`: `onRewarded` → +val (цена улова), раз на улов (`rvDone`), бонус не блокирует закрытие карточки (4.5.2); честная формулировка (4.5.1); простой текстовый button — НЕ имитация рекламного блока платформы (1.16). +3. **Стаб** (file:// без SDK): fullscreen → `false` (реклама не показана, мгновенный resume), RV — синхронная симуляция успешного просмотра (локальная разработка, all-в-одном-клике флоу). + +**Тесты:** +9 проверок в `tests/test.js` (§Y2: RV-карточка (кнопка видна/включена, текст «×2», `rvVal` = цене улова), RV-клик (×2 монет, раз на улов, повторный клик no-op), interstitial-гейты 2 мин / кулдаун 3 мин по счётчику `fsAdv` (adShowing/paused=false), cleanup — возврат в MENU). Итог **145/145 PASS, exit 0** (`node tools/check.mjs` — OK). Кадр `card-rv` в `tests/shots.js`. Ревью F1–F5 закрыты (guard adShowing в резюме, watchdog, дельта-пин, текст кнопки). + +**Monte-Carlo:** не требовался — физика вываживания не тронута; реклама не влияет на геймплей. + +**Заметки (осознанные решения и ловушки):** +- Карточка улова БЕЗ `gameplayStop()` — осознанно (часть цикла улова, не выход из геймплея). +- `lastAdTs` — осознанно «попытка», не «показ»: при троттлинге платформы `wasShown=false` кулдаун всё равно сгорает. +- Реклама не персистится в сейв (как погода): `persist()` только перед показом и после бонуса. + +## Этап 26. Y3: Полировка + +**Что сделано в `index.html`:** + +1. **`-webkit-touch-callout:none`** — в универсальный селектор (треб. 1.6.1.8: лонгтап не вызывает системный touch-callout на iOS; user-select и contextmenu были закрыты ранее). +2. **`.rvbtn{min-height:44px}`** — гарантия тап-таргета RV-кнопки ≥44px независимо от line-height/шрифта платформы (по замечанию UI-ревью). +3. **Дедуп watchdog** — тело watchdog вынесено в метод `YG._startAdWatchdog()`, вызов из `showFullscreen`/`showRewarded` после успешного `adv.*`; `endAd()` без изменений. + +**Тесты:** +1 проверка в `tests/test.js` (§Y3: computed-style `.rvbtn` min-height 44px). Итог **146/146 PASS, exit 0** (`node tools/check.mjs` — OK). + +**Monte-Carlo:** не требовался — изменения только в CSS и декомпозиции YG. + +**Заметки (осознанные решения и ловушки):** +- Финальное gate-ревью Y1–Y3: PASS по таблице требований 1.1–8.x (частично 1.20/1.23 — emoji-шрифт на старых Win ОС, осознанно). +- Доки README/AGENTS.md синхронны с реальным числом проверок (146). + +## Этап 27. Y4: Упаковка + +**Что сделано:** + +1. **`tools/pack.mjs`** (чистый Node, без зависимостей) — сборка `dist/fishing-yandex.zip` для Яндекс Игр (треб. 1.22): ТОЛЬКО `index.html` в корне архива, без docs/tests/screenshots/прочего. Сборщик: `zip -j` (основной, deflate; junk-путь — файл сразу в корне), при отсутствии — `python3 -m zipfile -c` (fallback; наличие обеих команд проверяется заранее, если обеих нет — понятная ошибка и exit 1). `dist/` создаётся автоматически, старый архив удаляется перед сборкой; `dist/` добавлен в `.gitignore`. +2. **Самопроверка в том же скрипте** — после сборки архив распаковывается в каталог tmp (`fs.mkdtemp`, встроенный zip-ридер: EOCD + центральная директория + `zlib.inflateRawSync`, `unzip` не нужен) и проверяются 5 пунктов, каждый с PASS/FAIL: (a) ровно один файл `index.html` в корне; (b) размер архива < 100 МБ (размер выводится); (c) имена без пробелов/кириллицы (regex); (d) sha256 распакованного файла == sha256 исходного `index.html` (byte-идентичность: игра в архиве = игра в репо); (e) синтаксис JS, вынутый из распакованного файла (тот же паттерн извлечения `