# Процесс разработки Как игру «Рыбалка» спроектировали, собрали, проверили и опубликовали. Документ описывает этапы, инструменты и повторяемые команды — чтобы любой (включая вас через полгода) мог воспроизвести и продолжить процесс. ``` ТЗ → макет-прототип → среда проверки → автотесты → визуал → баги → публ. → поддержка 1 2 3 4 5 6 7 8 ``` --- ## Этап 1. Требования и дизайн (ТЗ) **Исходное ТЗ:** браузерная игра про рыбалку, играбельная на телефоне. Ключевые решения: | Решение | Зачем | |---|---| | Один самодостаточный `index.html` (HTML+CSS+JS) | Нет сборки, зависимостей и бэкенда. Файл можно открыть где угодно: телефон, десктоп, статический хостинг | | Вся графика на Canvas, без внешних ассетов | Игра работает офлайн, грузится мгновенно | | Портретная ориентация, управление одним пальцем | Все действия — тап/удержание по экрану, больших тач-мишеней (кнопки ≥ 44 px) | | HTML-оверлеи для меню/магазина/карточек, Canvas — только мир игры | Кнопки и текст резкие и доступные, игровой мир — 60 fps | | WebAudio для звуков (без файлов), `navigator.vibrate` для тактильного отклика | Снова ноль внешних ресурсов; на телефоне — вибрация на клёв/рывки/обрыв | | Прогресс в `localStorage` | Мобильная игра без аккаунтов и сервера | Механика (единый цикл): ``` бросок (удержание = мощность) → ожидание → клёв («!») → подсечка (тап в окно) → вываживание (тяни/отпускай) → {улов / обрыв / срыв} → монеты → магазин ``` ## Этап 2. Реализация Структура `index.html` (все секции помечены комментариями в файле): 1. **Данные** — `SPECIES` (8 рыб: сила, редкость, цена, мин. дистанция), `RODS`, `LURES`. *Баланс игры крутится здесь.* 2. **Сохранение** — `save` + `localStorage` (монеты, снасти, коллекция, статистика). 3. **Холст** — масштабирование под `devicePixelRatio`, пересчёт layout'а при `resize`. 4. **Звук** — генеративный WebAudio: 12 эффектов из осцилляторов и шума. 5. **Состояния** — машина из 9 состояний: `MENU → IDLE → CHARGE → CAST → WAIT → APPROACH → BITE → REEL → SNAP/CARD → (IDLE)`. 6. **Физика вываживания** — `updateReel()`: напряжение шнура, дистанция, фазы «тянет/отдыхает». *Сложность крутится здесь.* 7. **Отрисовка** — небо, вода, рыбы-силуэты, лодка, поплавок, частицы, риплы, метры. 8. **Ввод** — Pointer Events (`pointerdown/up`), работают и палец, и мышь (важно для автотестов!). 9. **UI** — магазин, дневник, карточка улова. ## Этап 3. Среда проверки Игра проверена в контейнере **Alpine Linux 3.24**. Минимальный набор инструментов: | Инструмент | Версия (в нашем прогоне) | Зачем | Установка | |---|---|---|---| | Node.js | v24.18.1 | Статический анализ JS, запуск автотестов | `apk add nodejs npm` | | Chromium | 151.0.7922 | Headless-браузер для настоящего прогона игры | `apk add chromium` | | puppeteer-core | 25.9.0 | Управление Chromium через CDP (клики, скриншоты, чтение состояний) | `npm i puppeteer-core` (в `tests/`) | | Noto Color Emoji | — | Чтобы в headless-скриншотах эмодзи рендерились как на телефоне | `apk add font-noto-emoji` | | git | 2.54.0 | Публикация | `apk add git` | | curl | — | Проверка API Gitea, smoke-запросы | уже есть в Alpine | > В браузере на телефоне для *игры* не нужен ни один инструмент из таблицы — только сам браузер. ## Этап 4. Статическая проверка Первый рубеж — компиляция кода настоящим движком (V8): ```bash # вытащить JS из HTML и проверить синтаксис node --check game.js ``` Ловит опечатки, несбалансированные скобки, висячие конструкции — до запуска в браузере. ## Этап 5. Автотесты (плейтест через CDP) `tests/test.js` — сценарный автотест: headless Chromium загружает игру, а «игрок» управляет мышью (в браузере мышь порождает те же Pointer Events, что и палец). Тест читает и подталкивает внутренние состояния игры (`state`, `biteTimer`, `reel.d`) через `page.evaluate`, чтобы покрыть все ветки без ожидания случайностей. **Что покрыто (54 проверки):** - меню → старт → зарядка → бросок → ожидание - клёв (ускорен через `biteTimer`) → подсечка → `REEL` - **все три исхода вываживания**: улов → карточка, обрыв шнура (`SNAP`), срыв рыбы - магазин: открытие, покупка удочки и приманки (с проверкой баланса и экипировки) - дневник: рендер, пойманная рыба в коллекции - **сохранение после перезагрузки страницы** (coins/снасти/статистика) - shiny: ×3 к цене, счётчик `sn`, бейдж; near-miss: 10% цены за сошедшую рыбу; миграция старого сейва - **M2 (9 проверок)**: квест выполняется уловом (+reward), перегенерация квестов на новый день, стрик (день+1, награда дня), «щит» стрика (прощение пропуска и контр-кейс), достижения (бонус 1% к монетам), вкладки лавки 🎯/🏅 - **M3 (6 проверок)**: фазы суток по `dayTime`, ночная таблица рыб (тот же ролл → другая рыба), ночь без фонарика (клёва нет, тост разовый), ночь с фонариком (клёв работает), «Фонарик» в лавке, миграция сейва (`dayTime`/`lastSaveMs`) - **M4 (5 проверок)**: старт босс-боя (`stateBoss`/`bossPhase`), 3 фазы боя (обрывы фаз без штрафа, дистанция растёт по фазам), награда ×5 + `bossBeaten`/`bossCount`, достижение `boss_first`, босс-карточка - ноль ошибок в консоли и `pageerror` Запуск: ```bash cd tests npm install node test.js # при необходимости: CHROMIUM=/путь/к/хромью ``` ## Этап 6. Визуальная проверка `tests/shots.js` — та же техника, но с фиксацией скриншотов каждого состояния (`screenshots/shot-*.png`). Скриншоты просматривает разработчик: ловит проблемы, которые не видны по коду и логам (сбившиеся метки, отсутствующие глифы, нечитаемые метры). ## Этап 7. Найденные баги (ради чего всё это) Автотест + скриншоты поймали то, что ручное «посмотрел глазами» пропустило: | # | Баг | Как нашли | Лечение | |---|---|---|---| | 1 | **Критический:** после поимки рыбы игра вешалась навсегда. `catchFish()` ставил `reel=null`, но кейс `REEL` в `updateGame()` на следующей строке читал `reel.holding` → исключение убивало цикл `requestAnimationFrame` | Автотест: второй бросок не происходил; `pageerror` показал точную строку | `if(state==='REEL'&&reel){...}` | | 2 | Точка крепления лески висела в углу экрана в меню/карточке | Визуальный осмотр скриншотов | Условие отрисовки по состояниям | | 3 | Рыбка-маркер не стояла на краю полосы «дистанция» | Визуальный осмотр | Полоса = оставшаяся дистанция, маркер на её конце | | 4 | Глиф `✕` (U+2715) отсутствовал в некоторых шрифтах | Скриншот магазина | Заменён на `×` (U+00D7) | **Вывод:** для игр с таймерами и случайностями автотест, который *управляет* временем и состоянием, покрывает за минуту то, что вручную ищут часами. ## Этап 8. Публикация в Gitea Использовано: Gitea API (создание репозитория) + обычный `git push`. ```bash # 1. Создать репозиторий (токен: Настройки → Приложения → Токены, права: Repository) curl -X POST -H "Authorization: token <ТОКЕН>" -H "Content-Type: application/json" \ -d '{"name":"fishing-game","private":false,"auto_init":false}' \ https://git.shstk.ru/api/v1/user/repos # 2. Наполнить ветку и запушить git init -b main git add -A git commit -m "..." git remote add origin https://git.shstk.ru/andrey/fishing-game.git git push -u origin main # git попросит логин/пароль (или токен в URL — см. ниже) ``` Игра доступна анонимно по сырой ссылке: `https://git.shstk.ru/andrey/fishing-game/raw/branch/main/index.html` ### 🔐 Гигиена секретов (проверено на себе) 1. **Токен — одноразовый.** Для публикации достаточно кратковременного токена с правами на репозиторий; после push его **отзываем** (в нашем случае — отозван, старый токен теперь отдаёт `401`). 2. **Токен не попадает в историю и конфиг.** Если передавать токен в URL (`https://user:TOKEN@host/...`), после push удалите его из конфига: `git remote set-url origin https://git.shstk.ru/andrey/fishing-game.git` 3. **В репозиторий и в документы — только плейсхолдеры** (`<ТОКЕН>`), реальные секреты нигде не фиксируются. ## Этап 9. Поддержка и развитие **Крутить баланс:** константы `SPECIES` / `RODS` / `LURES` в начале `index.html`; физика вываживания — в `updateReel()`. **Цикл изменений:** ```bash # 1. поправить index.html # 2. прогнать автотест cd tests && node test.js # ждём 54/54 # 3. (опционально) свежие скриншоты node shots.js # 4. закоммитить и запушить git add -A && git commit -m "что изменилось" && git push ``` **Идеи развития** (по нарастающей сложности): - продажи улова / рыбный прайс по редкости - мировая таблица лидеров (нужен простой бэкенд или Gitea-Actions с webhook'ом) - офлайн-режим PWA (service worker в тот же файл) --- ## Шпаргалка: минимум инструментов по этапам | Этап | Инструменты | |---|---| | Игра (конечный пользователь) | Любой браузер на телефоне/десктопе | | Написание/изменение кода | Текстовый редактор (или LLM-сессия) | | Статическая проверка | Node.js | | Автотесты | Node.js + npm + Chromium + puppeteer-core | | Визуальная проверка | Скриншоты из автотеста + Noto Color Emoji в headless | | Публикация | git + curl + Gitea (токен с минимальными правами, отзываемый) | --- ## Этап 10. M1: shiny-рыба, near-miss и починка тест-запуска Короткий лог по M1: 1. **Починка запуска автотестов** — `tests/test.js` хардкодил `executablePath: '/usr/bin/chromium'` и URL `file:///fishing/index.html`; в этом чекауте их нет. Теперь, как в `shots.js`: `process.env.CHROMIUM || '/usr/bin/chromium-browser'`, а путь к игре резолвится динамически — `'file://' + path.resolve(__dirname, '..', 'index.html')`. 2. **Shiny-рыба** — каждый улов роллит шанс `SHINY_CHANCE` (3%): золотая версия вида продаётся за `SHINY_MULT` (×3) к цене с тем же разбросом ±5%, на карточке — бейдж «✨ ШИНИ!» и золотое оформление (рамка/свечение), в дневнике у вида появляется метка «✨ N» (счётчик `sn`). Формат сейва: `caught[id] = {n, best, sn}`; миграция в `loadSave()` дописывает `sn = 0` старым записям — числовые и `{n,best}`-форматы читаются без потерь. 3. **Near-miss при сходе** — если рыба сбежала в вываживании (ветка `d ≥ d0×1.4` в `updateReel()`), игрок получает утешение: 10% её цены (`floor(вес × цена/кг × 0.1)`, если > 0) и тост «Сбежала! …». Обрыв (SNAP) выплаты не даёт. 4. **Тесты** — добавлены 4 проверки: shiny (монеты ×3 в диапазоне, `sn = 1`, бейдж в DOM), миграция старого сейва (число и `{n,best}` без `sn`), near-miss (монеты ≈10% цены и тост «Сбежала»). Итог: **34/34 PASS, exit 0** (число в README и этом документе обновлено). ## Этап 11. Инструмент браузерного плейтеста (Orca CLI) **Что.** `tests/orca-playtest.js` — второй слой плейтестов: тот же полный геймплей, но в **настоящей вкладке браузера Orca app** (не в headless Chromium). Управление — командами Orca CLI (`tab create` / `eval` / `screenshot` / `console` / `tab close`), только встроенные модули Node. Проверяет 31 ok(): состояния игры, DOM карточки/лавки/дневника, бейдж «ШИНИ», тост near-miss, чистоту консоли; скриншоты `orca-*.png` — в каталог запуска. Полное описание, таблица шагов и камешки: `docs/BROWSER-TESTING.md`. **Почему.** Headless puppeteer-тест — главный рубеж, но он живёт в своей среде. Вкладка браузера Orca троттлит `requestAnimationFrame` (фоновая вкладка), имеет свой CLI-roundtrip и свою консоль: плейтест «вживую» ловит именно эти отличия и гарантирует, что игра играбельна в том браузере, которым пользуются люди. **Найденные камешки (ради чего писали этот слой):** | # | Камень | Как нашли | Лечение в скрипте | |---|---|---|---| | 1 | **rAF троттлинг фоновой вкладки**: игровое время идёт медленнее реального (кадр ≤ 0.05 c игрового) | Фиксированные sleep не доходили до нужного состояния | Все ожидания — polling с таймаутом (шаг 300 мс, таймауты 10–30 c); фиксированные sleep — только «удержание кнопки» | | 2 | **Окно подсечки (0.35–0.8 c) короче CLI-roundtrip (~0.3–0.5 c)** — тап опаздывает | Подсечка случайно промахивалась | `biteTimer = 0.05` → ждём BITE → `biteT = 5` (расширяем окно) → СНАЧАЛА скриншот, потом тап | | 3 | **Цитирование**: JS с кавычками, переданный одной строкой через shell (PTY), ломается | Повреждённые выражения в `eval` | Только `child_process.execFile` с массивом аргументов, без shell | | 4 | **Transient-зависания CLI** (единичный hang `eval` на ~1 мин, проходит повтором) | Прогон вешался на одном вызове | Таймаут `execFile` 20 c + ретраи ×3; `tab create` ретраев не имеет (дубль вкладки) | | 5 | **Тосты живут 1.6 c** — отдельный вызов для чтения текста опаздывает | Пустой тост в `orca-escape` | Текст тоста читаем в ТОМ ЖЕ eval, что и ожидание WAIT | **Как использовать.** Запущенный Orca app + `orca-ide` в PATH (или `ORCA_CLI_COMMAND`); из каталога репо: `node tests/orca-playtest.js`. Если Orca не запущен — пропустить и не блокировать (главный рубеж — `tests/test.js`). В AGENTS.md — пункт 3 раздела «Проверка». ## Этап 12. M2: награды — квесты дня, стрик входов, достижения **Что добавлено в `index.html` (секция «M2. Награды» после сейва):** 1. **Квесты дня** — `save.quests = {date, list:[3]}`. Три квеста генерируются по дню (seed — FNV-1a от `YYYY-MM-DD`, шаблоны без повторов): «поймай N вида», «заработай N 🪙», «улов ≥ N кг», «N забросов», «N рыб редкости R», «поймай шини». Прогресс растёт в `questHit()` (вызывается из `doCast()`/`catchFish()`); выполненный квест выдаёт награду один раз (`save.questsDoneTotal` — счётчик для достижения). Новый день (первый заход после смены даты) — перегенерация. 2. **Стрик входов со «щитом»** — `save.streak = {last, days, shieldWeek}`. При загрузке `applyStreak()`: вчерашний визит → день+1; пропуск в середине недели → день+1, если `shieldWeek` ещё не равен текущей ISO-неделе (ISO — `isoWeek()`, «YYYY-Www», неделя с понедельника); иначе стрик сбрасывается в день 1. Награда дня 1–7: 25/50/75/100/125/150/200 🪙. Первый визит (визитов не было) запускает стрик с дня 1 **без** награды — иначе миграция старых сейвов (без поля `streak`) давала бы «воздушные» монеты. 3. **Достижения** — 16 штук в `ACHIEVEMENTS` (первый улов, 10/100 забросов, заработано 1k/10k, 3/8 видов, редкая/легендарная рыба, первый шини, 10/20 кг, 10/50 обрывов — для этого `save.snaps++` в ветке SNAP, стрик 7, выполнено 5 квестов). `checkAchievements()` — по событиям (бросок/улов/обрыв/покупка/пополнение кошелька); выданные дают постоянные бонусы через `achBonus()`: +1–2% к монетам, +0.02–0.06 с к окну подсечки, +5–10 к нагрузке шнура. 4. **UI** — в лавке две вкладки: 🎯 «Награды» (полоса стрика с точками дня 1–7 + квесты дня с прогрессом и наградой) и 🏅 «Достижения» (выданные — яркие с ✓, остальные приглушённые с бонусом). 5. **Сейв** — новые поля только через миграцию/дефолты в `loadSave()`; старые сейвы читаются (проверяется тестом миграции — он не тронут). **Тесты:** +9 проверок в `tests/test.js` (итого **43/43 PASS, exit 0**), +2 шага (🎯/🏅) в `tests/orca-playtest.js` (итого **35/35 PASS, exit 0**). Точные проверки монет (M1 shiny, M1 near-miss) получили «гвард»: на время проверки квесты/стрик/счётчики фиксируются в eval, чтобы случайная награда не исказила баланс. **Найденные камешки:** | # | Камень | Как нашли | Лечение | |---|---|---|---| | 1 | Ветка `else if(shopTab==='journal')` терялась при вставке новых вкладок — журнал дублировался, новые вкладки недостижимы | Проваленный прогон: вкладки 🎯/🏅 рендерили «Снасти» | Явные `else if` по каждой вкладке + дубликат удалён | | 2 | `h='…'` без `let` в новых ветках `renderShop()` → ReferenceError в strict mode | `pageerror: h is not defined` | `let h` в каждой ветке | | 3 | Стрик давал «воздушные» +25 при миграции старых сейвов (без поля `streak`) — ломал точную проверку монет M1-теста | Провал `coins===77` после reload | Первый визит (last=null) — стрик стартует без награды | | 4 | `page.click('#btnRecall')` утаскивает мышь на кнопку → следующий `mouse.down()` в puppeteer-тесте падал на кнопку, а не на canvas | Тест вешался на `WAIT` (state=IDLE) | `page.mouse.move(195, 600)` после клика по кнопке | | 5 | На **первопланарной** вкладке Okno BITE (0.35–0.8 c реального времени) закрывалось до применения отдельного eval `biteT = 5` | Плейтест упал на `REEL` (state=WAIT) | Расширение окна — в ТОМ ЖЕ eval, что детекция BITE (`state === "BITE" ? (biteT = 5, "yes") : …`) | ## Этап 13. Спринт 2: ночная рыбалка + босс-рыба (M3+M4) **M3 — день/ночь, что добавлено в `index.html` (секция «M3: ЦИКЛ ДНЯ/НОЧИ»):** 1. **Цикл суток** — `DAY_LENGTH = 300` (секунд на полные сутки), глобалы `dayTime` (0..1) и `dayPhase` (утро 0–0.25 → день 0.25–0.5 → вечер 0.5–0.75 → ночь 0.75–1). Время идёт в реальном времени: в `frame()` каждый кадр `setDayTime(dayTime + dt/DAY_LENGTH)`. Пока игрока не было, сутки догоняются при загрузке (`dayTime + (Date.now() - lastSaveMs)`, mod 1). 2. **Визуал фаз** — `SKY_PHASES`: свой градиент неба на каждую фазу (утро — оранжевый, день — голубой, вечер — закатный, ночь — тёмно-синий); солнце/луна ходят по дуге (положение из `dayTime`, ночью — полумесяц), облака ночью приглушены, на воду ложится тёмный оверлей (прозрачность 0–0.3 по фазе). 3. **Ночной клёв** — `SPECIES_NIGHT` (клон `SPECIES` со смещёнными весами спавна: сом/щука/окунь активнее, дневные виды реже). `pickSpecies()` берёт ночную таблицу вечером и ночью. Ночью **без фонарика клёв не стартует**: `biteTimer` тикает вхолостую, разово показывается тост «Ночью нужен фонарик» (флаг `nightNoLureToast`, сброс — новым забросом). 4. **Фонарик** — новая приманка `LURES id=4` (500 🪙, флаг `nightOnly`, в лавке — пометка «ночь»): на клёв не влияет, но разрешает ночную рыбалку (проверка `save.lures.includes(4)`). 5. **Сейв** — `persist()` пишет `save.dayTime`/`save.lastSaveMs`; миграция в `loadSave()`: старый сейв стартует с утра (`dayTime≈0`), `lastSaveMs` проставляется — старые сейвы читаются (есть тест). **M4 — босс-рыба (Осётр-босс):** 1. **Появление** — при клёве осётра шанс 30% (проверка в `startBite()` — где рождается клёв; воркер M4 отклонился от «в `pickSpecies()`», отклонение согласовано): рыба помечается `sp.boss = true` клоном объекта — таблица `SPECIES` не мутируется. Для тестов — глобал `forceBoss`. Босс отличается: окно подсечки короче (0.25–0.5 c, ветка в `biteWindow()`), «злость» выше (сила 0.95), силуэт рыбы ×1.5. 2. **Бой в 3 фазы** — глобалы `stateBoss`/`bossPhase`; босс-ветка в `updateReel()`: у босса в фазах 1–2 `d ≤ 0` — «босс рвёт леску» (`SNAP` **без штрафа**: `save.snaps` не растёт) и без ввода игрока начинается следующая фаза глубже (`d = 0.6·d0`, затем `1.0·d0`); в фазе 3 — обычный улов. Настоящий обрыв (по натяжению) и near-miss завершают босс-бой (`stateBoss=false`). 3. **Награда** — ×5 к цене улова (множитель в `catchFish()`); поля `save.bossBeaten`/`save.bossCount` (дефолты для старых сейвов в `loadSave()`); тост «🏆 БОСС ПОБЕЖДЁН! +N 🪙»; всплеск и частицы крупнее. 4. **Достижения** — 17-е достижение `boss_first` «Босс-рыбак» (+3% к монетам). 5. **UI** — босс-карточка: имя «Осётр-БОСС», бейдж «🏆 БОСС!», золотое оформление (`boss-card`); во время боя над метрами — строка «⚠️ БОСС-РЫБА · ФАЗА N/3». **Тесты:** +11 проверок в `tests/test.js` (M3 — 6: фазы суток, ночная таблица, ночь без/с фонариком, «Фонарик» в лавке, миграция сейва; M4 — 5: старт боя, 3 фазы, награда ×5, достижение, карточка). Итог **54/54 PASS, exit 0**. В `tests/orca-playtest.js` +3 шага (ночь `dayTime=0.85` + скриншот, «Фонарик» в лавке, босс-бой до карточки со сбросом `forceBoss` в `finally`) — итого **46/46 PASS, exit 0**. В `tests/shots.js` добавлены `shot-night.png` (ночь, `dayTime=0.85`) и `shot-boss.png` (босс-карточка, полный цикл через `forceBoss`), плюс недостающие `shot-wait.png`/`shot-reset.png` — всего 11 кадров, скопированы в `screenshots/`. **Найденные камешки:** | # | Камень | Как нашли | Лечение | |---|---|---|---| | 1 | Тост «Ночью нужен фонарик» показывался бы на каждом кадре: ночью ветка «клёв не стартует» срабатывает на каждом тике `biteTimer ≤ 0` | Ревью при написании M3 | Флаг `nightNoLureToast` — тост разовый; сброс новым забросом | | 2 | Босс-флаг «залипал» бы в общей таблице: пометить объект осётра в `SPECIES` как босса — все следующие осётры тоже боссы | Ревью при написании M4 | В `startBite()` босс-осётр клонируется (`Object.assign`), таблица не трогается | | 3 | Настоящий обрыв (по натяжению) не завершал бы босс-бой: `stateBoss` оставался бы `true`, и логика фаз ездила бы по уже кончившемуся бою | Тест M4 (проверки после обрывов фаз) | `stateBoss=false; bossPhase=0` в ветках натяжения и near-miss | | 4 | В состоянии `SNAP` отпускание пальца не сбрасывает `reel.holding` (`onUp` слушает только `CHARGE`/`REEL`) — после авто-перехода в следующую фазу босса игра «втягивает» сама, как будто палец ещё на экране | Тест M4: дистанция фазы 2 не сходилась с 0.6·d0 | В тестах `holding=false` в том же eval, что детекция `SNAP`; для игрока некритично (окно 0.6 c), в будущем — чистить `holding` при выходе из `SNAP` | | 5 | `console`-ошибок нет, но квестные тосты затирали проверочный тост фонарика в тесте | Прогон M3-тестов | На время проверки квесты очищаются (`save.quests={list:[]}`), как в M2 | ## Этап 14. Спринт 3: параллельные worktree — баги, визуал, тест-инфра, порядок, баланс **Новый процесс.** Впервые задачи гонялись не последовательно в одном чекауте, а параллельно: 5 потоков (T1–T5), каждый — свой воркер (opencode через Orca orchestration) в собственном worktree и своей ветке, мерж в main — по очереди после приёмки. Чтобы мержи были бесконфликтными, зоны правок заранее разведены: `index.html` одновременно правили максимум два потока (логика vs CSS/рендер), `tests/test.js` закреплён за одним, `README.md`/`AGENTS.md` — разделены. **Что сделано по потокам:** 1. **T4 «Порядок»** (`sprint3-techdebt`): корневой `.gitignore` (мусор плейтестов больше не светится в `git status`), `tools/check.mjs` — автоматизация проверки синтаксиса JS из `index.html` (с номером строки при ошибке), `npm test` в `tests/package.json` теперь реально запускает плейтест (раньше был заглушкой — см. AGENTS.md), актуализация «Проверка». 2. **T1 «Баг-хант»** (`sprint3-bugs`): аудит логики с puppeteer-пробниками; починено 4 бага — (1) квест «Сделай N забросов» падал TypeError'ом (`questHit('casts')` без `condFn`) и молча съедал `persist()`/достижения после заброса; (2) фазовый обрыв босса не сбрасывал `reel.tension` — «обрыв без штрафа» превращался в настоящий; (3)–(4) HUD монет не обновлялся после квестовой награды на забросе и near-miss. +4 регресс-теста (секция 9l) → **58**. 3. **T3 «Тест-инфра»** (`sprint3-testing`): харденинг `orca-playtest.js` — pageerror-хуки (47-я проверка), ретраи CLI с бэкоффом, диагностика кнопок, закрытие вкладки в `finally`; `shots.js` — ожидание отрисовки через `waitForFunction`+двойной rAF вместо sleep'ов, выбор кадров через argv (`node shots.js --list`, подмножество); актуализация BROWSER-TESTING.md/README. 4. **T2 «Визуал»** (`sprint3-visual`): плавные переходы неба день/ночь со звёздами, интерполированная вода с дорожкой бликов, анимация хвостов и след поплавка, ночной фонарь на лодке, полировка CSS панелей/кнопок/карточек/тостов, адаптив узких экранов. 11 скриншотов перегенерированы. Механика не тронута (54/54 без правки тестов — критерий). Один затык: воркер завис на 25 минут в ожидании умершего tool-call — оживлен пинком в терминал, доработал без рестарта. 5. **T5 «Играбельность/сложность»** (`sprint3-playability`): 4 живые сессии во встроенном браузере Orca (бот с реакцией 140 мс, 620 забросов): день с нуля, ночь без/с фонариком, босс `forceBoss`, контрольный замер после баланса. **Вердикты: клёв, вываживание, экономика и босс — too_easy, прогрессия — ok** (клёв p50 4.8 c и 0 пустых забросов; 0 сходов на 329 уловов; 192–979 мон/мин; босс 18/18 без шанса проиграть). Баланс-коммит только числами: тайминги клёва `rand(2.5,7)→(4,10)` и аналоги, веса щуки 12→9, сома 7→5, осётра 3.5→2.5 (цена 80→65), ночные веса притушены; константы, зафиксированные тестами (карась 100/40, цены удочки/приманки), не тронуты. После правки: живой ретест p50 клёва 8.1 c, 58/58 ×6 прогонов. **Интеграция:** мержи в main по очереди (techdebt FF → testing → bugs → visual → playability), автослияние `index.html` прошло чисто на обоих пересечениях, после каждого мержа — полный прогон (58/58) + `node tools/check.mjs`. **Найденные камешки:** | # | Камень | Как нашли | Лечение | |---|---|---|---| | 1 | `worker-start` при 4 одновременных запусках: 3 из 4 промптов не принялись (`agent_prompt_stalled`) — TUI opencode не успели | Гейт PO по receipt'ам (state=failed) | Ретрай `--retry-of` с `--terminal` на уже прогретой TUI + `--worktree path:…` (без него — `terminal_worktree_mismatch`) | | 2 | Worktree создаётся от дефолтной базы репо (`origin/main`), а она отстала от локального main | Вопрос T4 координатору + сверка `git log` всех 5 worktree | Воркеру — `git merge --ff-only 5c95309`; пушить main почаще или держать origin актуальным | | 3 | `commit.gpgsign=true` ронял коммиты воркеров (pinentry некому вводить в headless) | ask от T3 | Коммиты воркеров — `--no-gpg-sign` (локальные ветки, без пуша) | | 4 | Воркер T2 завис на 25 мин: tool-call умер, агент вечно «ждал результатов» | Чекпоинт: замороженный счётчик токенов в TUI | Пинок `terminal send` с инструкцией прервать (esc) и продолжить — рестарт не понадобился | | 5 | Живая игра слишком легка: клёв мгновенный, сходов нет, босс без риска — головой по числам это не видно, только плейтестом | T5: замеры в живом браузере | Баланс числами (см. выше); системно — вываживание и ×5 босса требуют правки логики, оставлено как рекомендации | ## Этап 15. Спринт 4: симуляторская физика **План спринта.** Вываживание из «держалки с таймерами» превратить в перетягивание: у рыбы появляется выносливость, у катушки — тормоз, силы сторон честные, а отпускание в рывке — осознанная стратегия, а не промах. Исследование и план с инвариантами: `docs/PHYSICS-RESEARCH.md` (коммит `fafa0ad`). Поток: T2 — P0-физика (`70ff964`), T3 — независимое ревью с Monte-Carlo (`«к мержу после фиксов»`, 2 MAJOR), T2b — ребаланс по ревью (`60f3755`), T4 — тесты + живой плейтест + замеры ботами (`cc6238a`, вердикт «Релиз готов»), T5 — релиз. **Что добавлено (механики по пунктам):** 1. **Выносливость рыбы (stamina)** — `reel.st`: стартовое `st0 = 0.65+0.35·min(1,w/wMax)` (крупная выносливее), у босса `1` (свежий). Убывает весь бой в `updateReel()`: быстрее в рывке, при удержании и при «сдаче в рывок» (pull без holding). Свежая рыба тянет дольше (рывок `(0.8+2.2·st)·(0.6+0.7·ag)·rand(0.8,1.25)` с), уставшая — короче и слабее: при `st ≤ 0.12` флаг `tired` — сила рывков ×0.45 (`fishForce`), втягивание быстрее до ×1.6. UI: тонкая фиолетовая полоска под дистанцией (затухает к нулю), при усталости — 💤 и статус «💤 Рыба устала — втягивай!»; силуэт уставшей рыбы вялый (амплитуда ×0.35). 2. **Тормоз катушки (drag)** — новая черта удилищ в `RODS` (`drag` 34/47/63/87): в рывок сила рыбы `fishForce()` перетягивается против тормоза — если рывок сильнее (`over = F−drag > 0`), леска **скользит** (дистанция растёт `over·0.22`) и нагрузка растёт быстрее; ниже тормоза — нормальное втягивание. UI: отметка drag на жёлтой полосе нагрузки («выше — леска скользит»). 3. **Честный баланс сил (T2b)** — сила рыбы `fishForce(r,ag,pull)`: в рывке `(16+34·st)·(0.5+0.9·ag)·(0.7+0.5·ov)`, в затишье `5+7·ag` (уставшая ×0.45); натяжение `10+max(0,over)·1.1+22·ag` (в затишье ×0.35); втягивание `(11+3·reel)·(1+0.6·(1−st))`; свободный бег в рывке `F·0.32`; старт боя `d0 = 45+7·w`; near-miss-порог `1.45·d0` (утешение 10% цены). 4. **Escape: отпускать в рывок безопасно** — фикс MAJOR-1: escape-бар больше не растёт при `pull && !holding` (стоимость отпускания — только дистанция, «сдался рыбе»), а растёт только при зависании в затишье без удержания (`0.2+ag·0.4`/с, гашение 0.5/с при holding) — пассивная игра (не держать кнопку) = гарантированный сход. Подсказка «⚡ РЫБА ТЯНЕТ — ОТПУСТИ! ⚡» теперь соответствует механике (MAJOR-2). 5. **Пустые клёвы** — поплавок молчит: по таймеру в WAIT ролл `biteChance()` (приманка/дистанция/ночь), промах — новый таймер `rand(2.5,5)/bite`. Тест-форс — глобал `forceBite` (let, паттерн `forceBoss`), сброс — в btnResetYes. **Тесты (числа):** - `tests/test.js`: +7 проверок (61→**68**): ст-инициализация, ст-убывание, ослабление `fishForce` (F2<0.6·F1), tired-флаг, drag-скольжение (d растёт, tension>drag), drag-втягивание (d падает, tension