Files
fishing-game/docs/PLAN-AQUARIUM.md
T
andrey a168e830cc Этап 46 ревизия: жильцы аквариума — те же SVG-портреты, что на карточке улова. (1) Слой #aqFishLayer (body, после canvas): position:fixed/inset:0/z-index:5/pointer-events:none, включается только под сценой (body.aq-open) — тапы по рыбам по-прежнему ловит canvas (aqTap). (2) aqBuildFishEl(i) — <div class=aqfish><svg viewBox="0 0 160 100"><use href="#fish-<id>"/></svg></div>, тот же спрайт FISH-SVG, что у дневника/карточки; размер от прежнего aqFishSize (длина 2·size, высота по 160:100); aqPlaceFish(el,rt) каждый кадр из updateAquarium ставит translate(x,y)+translate(-50%,-50%)+rotate(±4° по tail)+scale(dir,1). (3) Вариант — 1:1 с карточкой: перекраска --f1/--f3 inline (AQ_VARIANT_RECOLOR, цвета из CSS #card.<variant>-card) + класс .var<id> (AQ_VARIANT_CLASS/aqApplyVariant; silver — grayscale+brightness, rainbow — aqrainbowhue, остальные — цветной drop-shadow). Вместо hue-хака AQ_VARIANT_FX/AQ_VARIANT_HUE (удалены) и прежней искры. (4) initAquarium() пересобирает слой с clip-path по банке (index aqFishEls = index save.aqFish, битый вид → null без сдвига индексов); aqReleaseSel() удаляет DOM-элемент точечно (без телепорта остальных); на canvas остались вода/грунт/растения/пузыри/стекло + аура болотных (уходит ЗА портрет) и рамка выбора. drawFishShape в банке больше не используется (в сценах ловли не тронут). Геймплей/баланс/сейв/экономика не менялись. (5) Тесты 218→222: блок «портреты» в §AQ (use href = #fish-<id> на каждый экземпляр; .var<id>/--f1 по варианту; transform следует за aqFishRt индекс-в-индекс; вне сцены display:none/body без aq-open, pointer-events:none) + обновлён список глобалов в шапке §AQ. (6) Скриншоты: перегенерированы только кадры aquarium (6 PNG, оба набора). (7) Доки: AGENTS.md (пункт «Аквариум» + глобалы), KODA.md (код-карта/глобалы/грабли), README.md, docs/PLAN-AQUARIUM.md (риск №1 закрыт), DEVLOG (подраздел «Ревизия Этапа 46»). Проверено: 222/222 PASS exit 0, check.mjs OK, balance.mjs — пины без изменений (Река 65.1/125.9 · Озёра 194.1/249.0 · Болото 240.3/328.9 · Море 297.3/476.7 · Океан 329.1/552.7), pack.mjs 5/5 + pack-smoke 6/6.
2026-09-22 23:02:00 +03:00

183 lines
15 KiB
Markdown
Raw 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.
# План: Аквариум (Этап 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.aqSlots<AQ_BASE_SLOTS) save.aqSlots=AQ_BASE_SLOTS;
if(save.aqSlots>AQ_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`) — `<button id="btnKeep">🐠 В аквариум</button>`;
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-интерпретатором** (проверено).
- Блок между `<!-- EMOJI-FONT:BEGIN/END -->` (`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/KODA/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` — новый пункт «Аквариум» + глобалы/подводные камни.
- `KODA.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-портреты (`<symbol id="fish-*">` — 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.