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

15 KiB
Raw Blame History

План: Аквариум (Этап 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) — порядок корректен.

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.