# План: Аквариум (Этап 46) Статус: **реализован** (Этап 46; см. запись в `docs/DEVLOG.md`). **Отступление от плана:** §7 «продажа из банки» (`#btnAqSell`) **заменена на «выпустить»** (`#btnAqRelease` → `aqReleaseSel()`) — удаление экземпляра БЕЗ монет, слот просто освобождается. Решение владельца: продажа из банки давала дыру ×2 дохода (рыба и продана на улове, и снова продана из аквариума) — экономика не затронута. Остальные пункты плана выполнены; вместо `aqSellSel`/`aq_sell`/`aq_sold` в игре `aqReleaseSel`/`aq_release`/`aq_released`. Ориентир по коду: `index.html` ~4365 строк; ссылки на строки даны на момент составления плана (перед реализацией сверять — номера могли сдвинуться). ## 1. Задача Добавить в игру **Аквариум** — специальный экран, куда можно «высаживать» пойманных рыб, чтобы они плавали. Не локация для ловли, а **декоративная сцена коллекции**. ## 2. Зафиксированные решения (ответы пользователя) | Развилка | Решение | |---|---| | Роль аквариума | **Косметика + достижения** (пассивного дохода нет — не трогаем экономику). | | Модель заселения | **Инвентарь экземпляров**: храним конкретную рыбу `{id, v:вариант, w:вес}`, а не только вид. | | Монеты при «оставить» | **Модель C — бесплатная копия**: монеты платятся на улове как сейчас (`catchFish` не меняется), кнопка «В аквариум» добавляет **копию** экземпляра. Экономика/квесты/ачивки/реклама/пины тестов не затрагиваются. | | Где открывается | **Сцена на главном canvas** (флаг `aqOpen`, `draw()` рисует банку вместо озера); вход — из хаба «Меню» `#gamemenu`. | | Вместимость | **Слоты + расширения за монеты**: старт `AQ_BASE_SLOTS=6`, максимум `AQ_MAX_SLOTS=12`, лестница цен. | | Значок | **🐠** — требует перегенерации встроенного emoji-субсета (`tools/emoji-font.py`). | ### Почему НЕ в `LOCATIONS` `LOCATIONS` (`index.html:1384`) жёстко связаны с `SPECIES_BY_LOCATION` (`index.html:1454`), `BOSS_BY_LOCATION` (`index.html:1461`), `save.location` (`index.html:1646`) и пинами доходности `tools/balance.mjs`. Запись аквариума туда сломает роллы, боссов, баланс и тесты. Аквариум — **отдельный режим**, а не вода. ## 3. Модель C — как обходим экономику Сейчас `catchFish()` платит **сразу при подсечке**, до карточки: `save.coins+=val; save.earned+=val` (`index.html:2393`), далее `questHit('earn_coins',…)` (`index.html:2410`). На это завязаны точные пины тестов, читающие `save.coins` при открытой карточке: `tests/test.js:411, 503, 601, 1465, 1504, 1548` (`coins === N + val`), а также rewarded `×2` (`rvVal`, `index.html:4167`), ачивки `earned_1000/10000`, квест `earn_coins`. В модели C **всё это остаётся как есть** — `catchFish` не трогаем. Кнопка «В аквариум» лишь **копирует** экземпляр в `save.aqFish`. Плюс: максимум пользы при минимуме риска; вариант и вес трофея сохраняются. Минус (осознанно): концептуальные «дубликаты» (рыба и продана, и в банке). ## 4. Данные и константы | Что | Где | Правка | |---|---|---| | Поля сейва | `save` (`index.html:1572`), `freshSave()` (`index.html:1579`) | `aqFish:[]`, `aqSlots:AQ_BASE_SLOTS`. | | Константы | секция «ДАННЫЕ», после скинов/локаций (~`index.html:1400`) | `AQ_BASE_SLOTS=6`, `AQ_MAX_SLOTS=12`, `AQ_SLOT_COSTS=[1500,4000,9000,20000,45000,100000]` (кноб; в масштабе цен локаций 65k–196k). | | Доступ к данным экземпляра | — | вид: `SPECIES.find(s=>s.id===f.id)`; вариант: `VARIANTS.find(v=>v.id===f.v)`. | ## 5. Миграция `loadSave()` (после блока локаций, `index.html:1647`) Паттерн — как у скинов/локаций (`index.html:1633-1641`). `SPECIES` (`1305`) и `VARIANTS` (`1470`) объявлены выше `loadSave` (`1586`) — порядок корректен. ```js if(!Array.isArray(save.aqFish)) save.aqFish=[]; save.aqFish=save.aqFish .filter(f=>f&&SPECIES.some(s=>s.id===f.id)&&typeof f.w==='number'&&f.w>0) .map(f=>({id:f.id, v:VARIANTS.some(x=>x.id===f.v)?f.v:'normal', w:f.w})); if(typeof save.aqSlots!=='number'||save.aqSlotsAQ_MAX_SLOTS) save.aqSlots=AQ_MAX_SLOTS; if(save.aqFish.length>save.aqSlots) save.aqFish.length=save.aqSlots; ``` Старые сейвы без полей читаются (дефолты), битые значения чинятся. ## 6. Карточка улова (модель C) - HTML: в `#card` рядом с `#btnCardOk` (`index.html:1294-1296`) — ``; CSS-класс по образцу `.rvbtn` (`index.html:207-213`). - Рантайм-хендл: `let cardSpec=null;` — в `showCard()` (`index.html:4143`) сохранять `{sp,w,variant}`; кнопку `disabled` + подпись/`title` «Нет места», если `save.aqFish.length>=save.aqSlots`. - Хендлер рядом с `#btnCardOk` (`index.html:4306`): push `{id:cardSpec.sp.id, v:cardSpec.variant.id, w:cardSpec.w}`; `persist()`; `checkAchievements()`; тост `aq_kept`; закрыть `#card`; `state='IDLE'`; затем `tryInterstitial()` (единообразно с `#btnCardOk`). - `catchFish()` (`index.html:2385`) **не меняется**. - Строки: `renderStaticUI()` (`index.html:3766`), `refreshCardNow()` (`index.html:3798`) — подпись из `T`. ## 7. Сцена аквариума Новая секция `АКВАРИУМ` после «ОТРИСОВКА» (перед `Y1`). - Рантайм: `aqOpen` (активна ли сцена), `aqFishRt[]` (`x,y,dir,ph,tail` — НЕ персистится), `aqSel`, `aqFromMenu`. - `initAquarium()` — раскладка по `W/H`; начальные позиции/скорости экземпляров. - `updateAquarium(dt)` — плавание, разворот у стенок, вертикальный дрейф, анимация хвоста. - `drawAquarium()`: - вода/стекло через `hexLerp` (`index.html:2643`) с привязкой к `nightMix()` (`index.html:2677`) — общий цикл суток; - грунт, растения, пузыри (простые `arc`/`bezier`, без новых ассетов); - рыбы: `drawFishShape(x,y,size,dir,sp.hue,alpha,wob)` (`index.html:2933`); `size` — от `w`/`sp.wMax` и `sp.rarity`; болотные — `drawGlowAura()` (`index.html:2918`); подкраска варианта — сдвиг hue + блик/искра; рамка при `aqSel===i`. - Точки жизненного цикла: - `frame()` (`index.html:3864`): `uiOpen` уже глушит `updateGame`; добавить `if(aqOpen) updateAquarium(dt);` рядом с `updateDecor(dt)` (`index.html:3872`). - `draw()` (`index.html:3402`): сразу после `ctx.save()`/shake — `if(aqOpen){ drawAquarium(); ctx.restore(); return; }`. - `onDown()` (`index.html:3886`): ранний `if(aqOpen){ aqTap(x,y); return; }`. - `resize()` (`index.html:1964`): `if(aqOpen) initAquarium();`. - `syncDom()` (`index.html:2613`): при `aqOpen` не показывать `#btnRecall`/`#btnPause`. - Вход/выход: `#btnGmAquarium` в хабе `#gamemenu` (`index.html:1268-1281`, хендлеры `4232-4250`); `openAquarium()` (спрятать гамеменю, `aqOpen=true; uiOpen=true; YG.gameplayStop(); initAquarium(); показать `#aqBar`), `closeAquarium()` (назад в `#gamemenu`, `uiOpen=true`). Аналог `fromMenuShop` (`index.html:4238`). - DOM-бар `#aqBar`: «Назад» `#btnAqBack`, счётчик `#aqCount` «N/M», «+ Слот (N 🪙)» `#btnAqSlot`, поповер `#aqPop` (имя/вариант/вес + `#btnAqSell`). - Тап по рыбе: `aqTap` ищет ближайшую → `aqSel` → `#aqPop`. **Продажа из банки** (опция): удалить экземпляр и начислить по той же формуле, что `index.html:2392` (`w*sp.price*mult*(1+achBonus().coinPct)`), `persist()`, тост. ## 8. Слоты - `#btnAqSlot`: цена `AQ_SLOT_COSTS[save.aqSlots-AQ_BASE_SLOTS]`; при нехватке — `sfx('no')` + `toast(t('not_enough'))` (паттерн `shopClick`, `index.html:4102`); иначе списать, `save.aqSlots++`, `persist()`, `renderAqBar()`. - При `aqSlots>=AQ_MAX_SLOTS` кнопка `disabled` (или скрыта). ## 9. Ачивки и i18n - `ACHIEVEMENTS` (`index.html:1878`): добавить в конец `aq_first` («Первый жилец») и `aq_full` («Аквариум полон»). **`bonus:{}`** (как пер-босс-ачивки, `index.html:1913`), чтобы пин `achBonus().coinPct=0.18` (§10c(8)) не сдвинулся. `checkAchievements()` уже зовётся в `catchFish`; дополнительно вызвать в хендлере `#btnKeep` и после покупки слота. - `T.ru`/`T.en` (`index.html:3544`): `gm_aquarium`, `aq_title`, `aq_keep`, `aq_back`, `aq_slots`, `aq_buy_slot`, `aq_max`, `aq_sell`, `aq_no_room`, `aq_kept`, `aq_sold`, имена/описания ачивок. - `setLang()` (`index.html:3804`): при `aqOpen` обновлять открытую сцену (`renderAqBar()`). - Заметка: `bonusText('')` у пер-босс-ачивок уже даёт `desc · ` (см. `index.html:4042`) — новый случай не хуже; при желании отдельно поправить `bonusText` (не обязательно). ## 10. Emoji-шрифт - 🐠 добавляется в игру → **обязательна** перегенерация: `tools/.venv/bin/python tools/emoji-font.py`, затем `tools/.venv/bin/python tools/emoji-font.py --check`. - Системный `python3` без `fontTools` — запускать **venv-интерпретатором** (проверено). - Блок между `` (`index.html:9,20`) правится только скриптом. ## 11. Тесты (`tests/test.js`) Добавить проверки и внести новые глобалы в документированный список: - **миграция**: старый сейв без `aqFish` → после reload `Array.isArray(save.aqFish)` и `aqSlots===6` (паттерн §9b/M6(6), `tests/test.js:419-427`); - **keep**: форс-улов → `CARD` → клик `#btnKeep` → `save.aqFish.length===1`, верные `id/v/w`, монеты выросли (модель C); - **лимит**: `save.aqFish.length=save.aqSlots` → кнопка `disabled`; - **слот**: монеты списаны, `aqSlots+1`; - **сцена**: `#btnGmAquarium` → `aqOpen===true`, `#aqBar` виден; `#btnAqBack` → `#gamemenu`; - **продажа из банки**: `aqSellSel()` → экземпляр удалён, монеты +val. - Экспонировать глобалы: `aqOpen`, `openAquarium`, `closeAquarium`, `AQ_BASE_SLOTS`, `AQ_MAX_SLOTS`, `AQ_SLOT_COSTS`, `save.aqFish`/`save.aqSlots`. - Обновить счёт проверок в README/AGENTS (сейчас `204/204`). ## 12. Скриншоты и доки - `tests/gen-screenshots.js`: `FRAMES` (`tests/gen-screenshots.js:63`) + кадр `aquarium`; в кадре засеять `save.aqFish` (напр. carass normal, pike silver, gold rainbow, glowfin) и `openAquarium()`; 2 rAF и снимок. Кадров 14 → 15 × 3 вьюпорта. - `AGENTS.md` — новый пункт «Аквариум» + глобалы/подводные камни. - `README.md` — фича, скриншоты, счётчики. - `docs/DEVLOG.md` — запись «Этап 46». ## 13. Порядок работ (фазы) 1. Данные + константы + миграция → `node tools/check.mjs`. 2. Кнопка карточки `#btnKeep` + хендлер (модель C). 3. Сцена: `initAquarium`/`updateAquarium`/`drawAquarium`, вход из «Меню», `#aqBar`, тап/продажа. 4. Слоты + ачивки + i18n. 5. 🐠 + перегенерация emoji-субсета. 6. Тесты, `gen-screenshots`, доки. ## 14. Проверка (порядок команд) 1. `node tools/check.mjs` 2. `cd tests && npm test` — все PASS, exit 0 3. `tools/.venv/bin/python tools/emoji-font.py --check` 4. `node tests/gen-screenshots.js` 5. `node tools/pack.mjs && node tests/pack-smoke.js` ## 15. Риски 1. ~~**Canvas = силуэты**, не SVG-портреты (`` — DOM-only). Визуал банки — `drawFishShape` + вариантовый оттенок + свечение болотных.~~ **Закрыто ревизией:** жильцы — DOM-слой `#aqFishLayer` с теми же SVG-портретами, что на карточке улова (вода/стекло/ауры остаются canvas); растеризация SVG не понадобилась. 2. **`uiOpen`/пауза**: сцена живёт под `uiOpen` — focus/visibility-пауза (`YG.onPause`) должна работать; `#aqBar`/`#btnPause` не должны «залипать» (проверить `syncDom` + плейтест). 3. **Раздувание сейва**: экземпляров ≤ `AQ_MAX_SLOTS` — не проблема. 4. **Реклама**: `#btnKeep` и `#btnRv` на одной карточке; в модели C конфликта нет, но после «В аквариум» карточка закрывается (интерстишел как обычно). 5. **Баланс не затрагивается** — `tools/balance.mjs` не трогаем, пины доходности живут. 6. **Emoji**: без перегенерации субсета 🐠 рисуется системным шрифтом и `--check` падает — не забыть шаг 5.