Этап 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.
@@ -51,7 +51,7 @@
|
|||||||
| `СОСТОЯНИЕ ИГРЫ` | все мутабельные глобалы геймплея |
|
| `СОСТОЯНИЕ ИГРЫ` | все мутабельные глобалы геймплея |
|
||||||
| `ЛОГИКА` | `pickSpecies()`, `biteChance()`, `rollWeight()`, `hook()`, `fishForce()`, `updateReel()`, `catchFish()`, `updateGame()` |
|
| `ЛОГИКА` | `pickSpecies()`, `biteChance()`, `rollWeight()`, `hook()`, `fishForce()`, `updateReel()`, `catchFish()`, `updateGame()` |
|
||||||
| `ОТРИСОВКА` | `UI_FONT`, `drawSky/Water/Scenery/AmbientFish/Boat/Bobber/Meters/Weather()`, свечение Болота (`drawGlowAura()`/`fishGlow()`/`GLOW_RGBA`), декор `drawSwampDecor()` (`pads`/`frogs`), `draw()` |
|
| `ОТРИСОВКА` | `UI_FONT`, `drawSky/Water/Scenery/AmbientFish/Boat/Bobber/Meters/Weather()`, свечение Болота (`drawGlowAura()`/`fishGlow()`/`GLOW_RGBA`), декор `drawSwampDecor()` (`pads`/`frogs`), `draw()` |
|
||||||
| `АКВАРИУМ (Этап 46)` | `aqOpen`, `initAquarium()`/`updateAquarium()`/`drawAquarium()`, `aqTap()`/`aqReleaseSel()`, слоты (`AQ_BASE_SLOTS`/`AQ_MAX_SLOTS`/`AQ_SLOT_COSTS`) |
|
| `АКВАРИУМ (Этап 46)` | `aqOpen`, `initAquarium()`/`updateAquarium()`/`drawAquarium()`, `aqTap()`/`aqReleaseSel()`, слоты (`AQ_BASE_SLOTS`/`AQ_MAX_SLOTS`/`AQ_SLOT_COSTS`), DOM-слой портретов `#aqFishLayer` (`aqBuildFishEl()`/`aqPlaceFish()`/`aqFishEls`, вариант `.var<id>`/`--f1`) |
|
||||||
| `Y1: SDK Яндекс Игр` | обёртка `YG`, реклама (`tryInterstitial()`, `#btnRv`), пауза платформы |
|
| `Y1: SDK Яндекс Игр` | обёртка `YG`, реклама (`tryInterstitial()`, `#btnRv`), пауза платформы |
|
||||||
| `Y5: i18n RU/EN` | таблица `T`, `t()`/`tt()`, `setLang()`, `resolveLang()`, `renderMenu()` |
|
| `Y5: i18n RU/EN` | таблица `T`, `t()`/`tt()`, `setLang()`, `resolveLang()`, `renderMenu()` |
|
||||||
| `ЦИКЛ` | `frame()` — requestAnimationFrame, дельта-тайм, пауза |
|
| `ЦИКЛ` | `frame()` — requestAnimationFrame, дельта-тайм, пауза |
|
||||||
@@ -102,7 +102,7 @@ node tools/check.mjs # блок-за-блоком прогоняет J
|
|||||||
```bash
|
```bash
|
||||||
cd tests
|
cd tests
|
||||||
npm install # ставит puppeteer-core
|
npm install # ставит puppeteer-core
|
||||||
npm test # = node test.js → ждать 218/218 PASS и exit 0
|
npm test # = node test.js → ждать 222/222 PASS и exit 0
|
||||||
```
|
```
|
||||||
|
|
||||||
- Браузер берётся из `process.env.CHROMIUM || '/usr/bin/chromium-browser'` — при необходимости поправьте путь.
|
- Браузер берётся из `process.env.CHROMIUM || '/usr/bin/chromium-browser'` — при необходимости поправьте путь.
|
||||||
@@ -168,7 +168,7 @@ node tests/pack-smoke.js # прогон игры из распакованно
|
|||||||
```bash
|
```bash
|
||||||
# 1. поправить index.html
|
# 1. поправить index.html
|
||||||
node tools/check.mjs # 2. синтаксис
|
node tools/check.mjs # 2. синтаксис
|
||||||
cd tests && npm install && npm test # 3. главный рубеж: ждать 218/218 PASS, exit 0
|
cd tests && npm install && npm test # 3. главный рубеж: ждать 222/222 PASS, exit 0
|
||||||
node tools/emoji-font.py # (если добавляли emoji) + --check
|
node tools/emoji-font.py # (если добавляли emoji) + --check
|
||||||
node tests/gen-screenshots.js # 4. переснять скриншоты, если менялась картинка
|
node tests/gen-screenshots.js # 4. переснять скриншоты, если менялась картинка
|
||||||
node tools/pack.mjs && node tests/pack-smoke.js # 5. перед публикацией
|
node tools/pack.mjs && node tests/pack-smoke.js # 5. перед публикацией
|
||||||
@@ -179,9 +179,9 @@ git add -A && git commit -m "…" && git push
|
|||||||
|
|
||||||
### Тесты и их ограничения
|
### Тесты и их ограничения
|
||||||
|
|
||||||
- Тесты гоняют игру через внутренние глобалы в `page.evaluate`: `state`, `save`, `biteTimer`, `reel`, `SPECIES`, `SPECIES_NIGHT`, `rod()`, `lure()`, `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/MAX`, `WEATHER_DURATION`, `weatherActive()`, `BOAT_SKINS`, `BOBBER_SKINS`, `LOCATIONS`, `RIVER_SPECIES(_NIGHT)`, `LAKE_SPECIES(_NIGHT)`, `SEA_SPECIES(_NIGHT)`, `OCEAN_SPECIES(_NIGHT)`, `SWAMP_SPECIES(_NIGHT)` (+ `*_IDS`/`*_WEIGHTS` локаций), `SAVE_KEY`, `loadSave`, `freshSave`, `ACHIEVEMENTS`, `checkAchievements`, `questText`, `YG` (+ `YG.counts`), `forceLang`, `setLang()`, `LANG`, `trName()`, `trRarity()`, `pauseOpen`, `uiOpen`, `dayTime` и имена состояний `MENU/IDLE/CHARGE/CAST/WAIT/APPROACH/BITE/REEL/SNAP/CARD`; аквариум (этап 46): `aqOpen`, `aqFishRt`, `aqSel`, `aqTank`, `AQ_BASE_SLOTS`, `AQ_MAX_SLOTS`, `AQ_SLOT_COSTS`, `AQ_VARIANT_FX`, `AQ_VARIANT_HUE`, `initAquarium`, `updateAquarium`, `drawAquarium`, `aqFishSize`, `aqTap`, `showAqPop`, `hideAqPop`, `renderAqBar`, `aqReleaseSel`, `openAquarium`, `closeAquarium`, `syncKeepBtn`, `cardSpec`, `save.aqFish`/`save.aqSlots`.
|
- Тесты гоняют игру через внутренние глобалы в `page.evaluate`: `state`, `save`, `biteTimer`, `reel`, `SPECIES`, `SPECIES_NIGHT`, `rod()`, `lure()`, `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/MAX`, `WEATHER_DURATION`, `weatherActive()`, `BOAT_SKINS`, `BOBBER_SKINS`, `LOCATIONS`, `RIVER_SPECIES(_NIGHT)`, `LAKE_SPECIES(_NIGHT)`, `SEA_SPECIES(_NIGHT)`, `OCEAN_SPECIES(_NIGHT)`, `SWAMP_SPECIES(_NIGHT)` (+ `*_IDS`/`*_WEIGHTS` локаций), `SAVE_KEY`, `loadSave`, `freshSave`, `ACHIEVEMENTS`, `checkAchievements`, `questText`, `YG` (+ `YG.counts`), `forceLang`, `setLang()`, `LANG`, `trName()`, `trRarity()`, `pauseOpen`, `uiOpen`, `dayTime` и имена состояний `MENU/IDLE/CHARGE/CAST/WAIT/APPROACH/BITE/REEL/SNAP/CARD`; аквариум (этап 46): `aqOpen`, `aqFishRt`, `aqSel`, `aqTank`, `AQ_BASE_SLOTS`, `AQ_MAX_SLOTS`, `AQ_SLOT_COSTS`, `AQ_VARIANT_RECOLOR`, `AQ_VARIANT_CLASS`, `aqApplyVariant`, `aqFishEls`, `aqBuildFishEl`, `aqPlaceFish`, `initAquarium`, `updateAquarium`, `drawAquarium`, `aqFishSize`, `aqTap`, `showAqPop`, `hideAqPop`, `renderAqBar`, `aqReleaseSel`, `openAquarium`, `closeAquarium`, `syncKeepBtn`, `cardSpec`, `save.aqFish`/`save.aqSlots`.
|
||||||
**Переименование любого из них ломает тесты молча** — имена держать, либо обновлять тест в том же коммите.
|
**Переименование любого из них ломает тесты молча** — имена держать, либо обновлять тест в том же коммите.
|
||||||
- Числа проверок зафиксированы в README (`218/218` в `tests/test.js`, `76/76` в Orca, `6/6` в pack-smoke) — при добавлении проверок обновлять доки в том же коммите.
|
- Числа проверок зафиксированы в README (`222/222` в `tests/test.js`, `76/76` в Orca, `6/6` в pack-smoke) — при добавлении проверок обновлять доки в том же коммите.
|
||||||
- Изменение физики вываживания требует **Monte-Carlo-проверки точными формулами** (см. `docs/PHYSICS-RESEARCH.md`, §4.7) и синхронной правки тестов.
|
- Изменение физики вываживания требует **Monte-Carlo-проверки точными формулами** (см. `docs/PHYSICS-RESEARCH.md`, §4.7) и синхронной правки тестов.
|
||||||
- Пины шансов в тестах снимаются на **нейтральной снасти** (снапшот/рестор `save.lure`/`lureLv` внутри eval).
|
- Пины шансов в тестах снимаются на **нейтральной снасти** (снапшот/рестор `save.lure`/`lureLv` внутри eval).
|
||||||
- В браузере Orca (живой плейтест) нельзя использовать фиксированные `sleep` для ожидания состояний — rAF троттлится; только polling с таймаутом.
|
- В браузере Orca (живой плейтест) нельзя использовать фиксированные `sleep` для ожидания состояний — rAF троттлится; только polling с таймаутом.
|
||||||
@@ -202,7 +202,7 @@ git add -A && git commit -m "…" && git push
|
|||||||
- `catchFish()` обнуляет `reel` — все обращения к `reel` в `updateGame()` под гардом `if(state==='REEL'&&reel)`.
|
- `catchFish()` обнуляет `reel` — все обращения к `reel` в `updateGame()` под гардом `if(state==='REEL'&&reel)`.
|
||||||
- `shadowBlur` (неоновый поплавок, свечение босса) всегда в паре `save`/`restore`.
|
- `shadowBlur` (неоновый поплавок, свечение босса) всегда в паре `save`/`restore`.
|
||||||
- `save.caught[id]` — объект `{n, best, cp, sv, qz, sn, em, dm, rb}` (не число); `sn` исторически = «shiny» = gold-вариант.
|
- `save.caught[id]` — объект `{n, best, cp, sv, qz, sn, em, dm, rb}` (не число); `sn` исторически = «shiny» = gold-вариант.
|
||||||
- Аквариум: `#aqBar{pointer-events:none}` + `.aqpill{pointer-events:auto}` (иначе невидимая полоса перехватывает тапы по сцене), `body.aq-open` прячет `#hud`, сентинел `aqSel=-1`, «выпустить» — без монет (продажу из банки не вводить).
|
- Аквариум: `#aqBar{pointer-events:none}` + `.aqpill{pointer-events:auto}` (иначе невидимая полоса перехватывает тапы по сцене), `body.aq-open` прячет `#hud`, сентинел `aqSel=-1`, «выпустить» — без монет (продажу из банки не вводить). Жильцы — DOM-слой `#aqFishLayer` (`pointer-events:none`, `body.aq-open`, `clip-path` по банке), индекс `aqFishEls` обязан совпадать с `save.aqFish`/`aqFishRt` (в `initAquarium` битый вид даёт `null`, но не сдвигает индексы; `aqReleaseSel` удаляет DOM точечно) — иначе рыбы «перепутаются».
|
||||||
- Пустой клёв и «ночь без фонарика» — легальные состояния ветки `WAIT`, а не ошибки; `forceBite` не работает ночью без `hasLight()`.
|
- Пустой клёв и «ночь без фонарика» — легальные состояния ветки `WAIT`, а не ошибки; `forceBite` не работает ночью без `hasLight()`.
|
||||||
- Headless Chromium в этом чекауте может не видеть `/tmp` по `file://` (smoke авто-откатывается во временный каталог под `$HOME`).
|
- Headless Chromium в этом чекауте может не видеть `/tmp` по `file://` (smoke авто-откатывается во временный каталог под `$HOME`).
|
||||||
|
|
||||||
@@ -223,7 +223,7 @@ git add -A && git commit -m "…" && git push
|
|||||||
| `docs/DEVLOG.md` | Журнал разработки: этапы 1–46, найденные баги, решения, Monte-Carlo |
|
| `docs/DEVLOG.md` | Журнал разработки: этапы 1–46, найденные баги, решения, Monte-Carlo |
|
||||||
| `docs/BROWSER-TESTING.md` | Плейтест через Orca CLI: таблица шагов, 13 подводных камней, справочник CLI |
|
| `docs/BROWSER-TESTING.md` | Плейтест через Orca CLI: таблица шагов, 13 подводных камней, справочник CLI |
|
||||||
| `docs/PHYSICS-RESEARCH.md` | Исследование физики вываживания и формулы баланса |
|
| `docs/PHYSICS-RESEARCH.md` | Исследование физики вываживания и формулы баланса |
|
||||||
| `tests/test.js` | Главный автотест (headless Chromium + puppeteer-core), 218 проверок |
|
| `tests/test.js` | Главный автотест (headless Chromium + puppeteer-core), 222 проверок |
|
||||||
| `tests/gen-screenshots.js` | Генератор скриншотов wiki: 15 кадров × 3 вьюпорта, RU/EN |
|
| `tests/gen-screenshots.js` | Генератор скриншотов wiki: 15 кадров × 3 вьюпорта, RU/EN |
|
||||||
| `tests/orca-playtest.js` | Плейтест в живом браузере Orca через CLI, 76 проверок |
|
| `tests/orca-playtest.js` | Плейтест в живом браузере Orca через CLI, 76 проверок |
|
||||||
| `tests/pack-smoke.js` | Smoke-прогон игры из распакованного архива, 6 проверок |
|
| `tests/pack-smoke.js` | Smoke-прогон игры из распакованного архива, 6 проверок |
|
||||||
|
|||||||
@@ -34,7 +34,7 @@
|
|||||||
- Погода: дождь (🌧️) — шанс особых рыб ×3; туман (🌫️) — клёв ×1,5; событие идёт 120 с, следующее — раз в 2–4 минуты — <img src="screenshots-wiki/mobp-weather-rain.png" width="200" alt="дождь">
|
- Погода: дождь (🌧️) — шанс особых рыб ×3; туман (🌫️) — клёв ×1,5; событие идёт 120 с, следующее — раз в 2–4 минуты — <img src="screenshots-wiki/mobp-weather-rain.png" width="200" alt="дождь">
|
||||||
- Скины лодки и поплавка 🎨 — вкладка «Скины» в лавке: лодка «Красная лодка» 800 🪙 / «Лодка-домик» 2000 🪙, поплавок «Красный» 500 🪙 / «Неоновый» 1500 🪙 — чистая косметика (без эффектов на геймплей)
|
- Скины лодки и поплавка 🎨 — вкладка «Скины» в лавке: лодка «Красная лодка» 800 🪙 / «Лодка-домик» 2000 🪙, поплавок «Красный» 500 🪙 / «Неоновый» 1500 🪙 — чистая косметика (без эффектов на геймплей)
|
||||||
- Локации: Озёра (65 536 🪙) — судак, хариус, гигантский рак; Море (131 072 🪙) — скумбрия, тунец, рыба-меч; Океан (196 608 🪙 = ×1.5 Моря) — глубоководные гиганты: синий кит, гигантский кальмар, скат-манта, гигантский осьминог; Волшебное болото (90 000 🪙) — светящиеся обитатели (светлячок, лунная рыбка, изумрудный светляк, фиалковый огонёк, дух болота), ночью рыбы и фоновые стайки светятся разными цветами, вода — тёмно-моховая, на поверхности кувшинки с лилиями и прыгающие лягушки; у каждой — свой состав днём/ночью, своя вода (цвет, волны, рыбы в толще) и свой берег (Река — лесистая полоса, Озёра — холмы, Море — парусник и чайки, Океан — открытый горизонт, Болото — мшистый берег с камышом), чем дальше вода — тем доходнее улов; покупка и переключение во вкладке 🗺️ лавки — <img src="screenshots-wiki/mobp-lake.png" width="200" alt="озёра"> <img src="screenshots-wiki/mobp-sea.png" width="200" alt="море"> <img src="screenshots-wiki/mobp-ocean.png" width="200" alt="океан"> <img src="screenshots-wiki/mobp-swamp.png" width="200" alt="болото днём"> <img src="screenshots-wiki/mobp-swamp-night.png" width="200" alt="болото ночью">
|
- Локации: Озёра (65 536 🪙) — судак, хариус, гигантский рак; Море (131 072 🪙) — скумбрия, тунец, рыба-меч; Океан (196 608 🪙 = ×1.5 Моря) — глубоководные гиганты: синий кит, гигантский кальмар, скат-манта, гигантский осьминог; Волшебное болото (90 000 🪙) — светящиеся обитатели (светлячок, лунная рыбка, изумрудный светляк, фиалковый огонёк, дух болота), ночью рыбы и фоновые стайки светятся разными цветами, вода — тёмно-моховая, на поверхности кувшинки с лилиями и прыгающие лягушки; у каждой — свой состав днём/ночью, своя вода (цвет, волны, рыбы в толще) и свой берег (Река — лесистая полоса, Озёра — холмы, Море — парусник и чайки, Океан — открытый горизонт, Болото — мшистый берег с камышом), чем дальше вода — тем доходнее улов; покупка и переключение во вкладке 🗺️ лавки — <img src="screenshots-wiki/mobp-lake.png" width="200" alt="озёра"> <img src="screenshots-wiki/mobp-sea.png" width="200" alt="море"> <img src="screenshots-wiki/mobp-ocean.png" width="200" alt="океан"> <img src="screenshots-wiki/mobp-swamp.png" width="200" alt="болото днём"> <img src="screenshots-wiki/mobp-swamp-night.png" width="200" alt="болото ночью">
|
||||||
- **Аквариум 🐠** — декоративная сцена коллекции: с карточки улова кнопка «🐠 В аквариум» кладёт бесплатную копию экземпляра (монеты за улов платятся как обычно, модель C — экономика не тронута); старт 6 мест, расширение до 12 за монеты (1 500 → 100 000 🪙); тап по рыбе — вес и редкость, «Выпустить» освобождает слот (без монет). Вход — из хаба «Меню»; чистая косметика — <img src="screenshots-wiki/mobp-aquarium.png" width="200" alt="аквариум">
|
- **Аквариум 🐠** — декоративная сцена коллекции: с карточки улова кнопка «🐠 В аквариум» кладёт бесплатную копию экземпляра (монеты за улов платятся как обычно, модель C — экономика не тронута); старт 6 мест, расширение до 12 за монеты (1 500 → 100 000 🪙); жильцы — те же SVG-портреты с вариантной перекраской, что на карточке улова; тап по рыбе — вес и редкость, «Выпустить» освобождает слот (без монет). Вход — из хаба «Меню»; чистая косметика — <img src="screenshots-wiki/mobp-aquarium.png" width="200" alt="аквариум">
|
||||||
- Квесты дня (3 шт, генерация по дню), стрик входов со «щитом», 38 достижений с бонусами
|
- Квесты дня (3 шт, генерация по дню), стрик входов со «щитом», 38 достижений с бонусами
|
||||||
- Дневник рыбака — коллекция, максимальный улов по каждому виду, статистика
|
- Дневник рыбака — коллекция, максимальный улов по каждому виду, статистика
|
||||||
- Магазин с вкладками: Лавка 🛒, Дневник 📔, Награды 🎯, Достижения 🏅, Скины 🎨, Локации 🗺️
|
- Магазин с вкладками: Лавка 🛒, Дневник 📔, Награды 🎯, Достижения 🏅, Скины 🎨, Локации 🗺️
|
||||||
@@ -77,7 +77,7 @@ SDK подхватывается автоматически на площадк
|
|||||||
```bash
|
```bash
|
||||||
cd tests
|
cd tests
|
||||||
npm install # ставит puppeteer-core
|
npm install # ставит puppeteer-core
|
||||||
node test.js # 218 проверок
|
node test.js # 222 проверок
|
||||||
```
|
```
|
||||||
|
|
||||||
Нужен установленный Chromium (`executablePath` в `test.js` при необходимости поправить).
|
Нужен установленный Chromium (`executablePath` в `test.js` при необходимости поправить).
|
||||||
|
|||||||
@@ -863,3 +863,22 @@ git add -A && git commit -m "что изменилось" && git push
|
|||||||
**Скриншоты:** `tests/gen-screenshots.js` — +кадр `aquarium` (Этап 46) → 14→15 кадров; в кадре засеваются `save.aqFish` = карась (normal, 0.6 кг), щука (silver, 7.4 кг), золотая рыбка (rainbow, 0.8 кг), светлячок (normal, 2.4 кг) и вызывается `openAquarium()`. Оба набора перегенерированы: `screenshots-wiki/` и `screenshots-wiki-en/` — по 45 PNG (включая `desk/mobl/mobp-aquarium.png`). README: фича «Аквариум 🐠» + изображение `mobp-aquarium.png`, «15 кадров × 3 вьюпорта», 38 достижений, 218 тестов.
|
**Скриншоты:** `tests/gen-screenshots.js` — +кадр `aquarium` (Этап 46) → 14→15 кадров; в кадре засеваются `save.aqFish` = карась (normal, 0.6 кг), щука (silver, 7.4 кг), золотая рыбка (rainbow, 0.8 кг), светлячок (normal, 2.4 кг) и вызывается `openAquarium()`. Оба набора перегенерированы: `screenshots-wiki/` и `screenshots-wiki-en/` — по 45 PNG (включая `desk/mobl/mobp-aquarium.png`). README: фича «Аквариум 🐠» + изображение `mobp-aquarium.png`, «15 кадров × 3 вьюпорта», 38 достижений, 218 тестов.
|
||||||
|
|
||||||
**Доки:** AGENTS.md (новый пункт «Аквариум (Этап 46)», глобалы, «Награды» 36→38, «Скриншоты» 14→15), KODA.md (код-карта, контент, глобалы, «где менять», грабли, счётчики), README.md (фича, счётчики), `docs/PLAN-AQUARIUM.md` (статус «реализован» + заметка об отступлении §7), этот DEVLOG.
|
**Доки:** AGENTS.md (новый пункт «Аквариум (Этап 46)», глобалы, «Награды» 36→38, «Скриншоты» 14→15), KODA.md (код-карта, контент, глобалы, «где менять», грабли, счётчики), README.md (фича, счётчики), `docs/PLAN-AQUARIUM.md` (статус «реализован» + заметка об отступлении §7), этот DEVLOG.
|
||||||
|
|
||||||
|
### Ревизия Этапа 46 (по просьбе владельца): жильцы банки — те же портреты, что на карточке улова
|
||||||
|
|
||||||
|
**Задача:** в аквариуме рыбы рисовались процедурным силуэтом `drawFishShape` (эллипс + хвост) и лишь отдалённо напоминали вид; на карточке улова и в дневнике давно стоят подробные SVG-портреты (`#fish-<id>`, этап 42). Владелец попросил, чтобы жильцы выглядели так же, как в карточке улова.
|
||||||
|
|
||||||
|
**Решение — DOM-слой портретов поверх canvas-сцены** (вода/грунт/растения/пузыри/стекло остаются canvas-рисунком):
|
||||||
|
- Новый слой `#aqFishLayer` (пустой `<div>` в `body`, сразу после `<canvas>`): `position:fixed; inset:0; z-index:5; pointer-events:none; display:none`, включается только под сценой (`body.aq-open #aqFishLayer{display:block}`). Клики не перехватывает — тапы по рыбам по-прежнему ловит canvas (`aqTap`).
|
||||||
|
- `aqBuildFishEl(i)` создаёт на каждого жильца `<div class="aqfish"><svg viewBox="0 0 160 100"><use href="#fish-<id>"/></svg></div>` — ровно тот же спрайт, что у дневника/карточки (`<use>` в общий `FISH-SVG`). Размер — прежний `aqFishSize(sp,w)`: длина `2·size`, высота по соотношению 160:100.
|
||||||
|
- `aqPlaceFish(el,rt)` каждый кадр из `updateAquarium(dt)` ставит `transform: translate(x,y) translate(-50%,-50%) rotate(...) scale(dir,1)` — движение/разворот/«качание хвоста» как было, но уже для портрета (крен ±4° вместо анимации хвоста).
|
||||||
|
- `initAquarium()` пересобирает слой (очистка + `clip-path: inset(...round 18px)` по банке, чтобы рыбы не вылезали за стекло), `aqReleaseSel()` удаляет DOM-элемент вместе с записью (индексами, без «телепорта» остальных).
|
||||||
|
- **Вариант — тем же оформлением, что на карточке:** перекраска `--f1`/`--f3` (наследуется сквозь `<use>`; copper/gold/quartz/emerald/diamond — цвета 1:1 из CSS карточки) + классы `.var<id>` в `#aqFishLayer` (silver — `grayscale+brightness`, rainbow — `aqrainbowhue`, остальные — цветной `drop-shadow`); normal — без оформления. Побочно это заменило прежний hoo-хака `AQ_VARIANT_FX`/`AQ_VARIANT_HUE` + подкрашивающий ореол.
|
||||||
|
- **На canvas осталось:** вода/лучи/грунт/растения/пузыри/стекло/рамка выбранной рыбы + **аура свечения болотных** (`drawGlowAura`+`glowIntensity()` рисуется ДО портрета, поэтому уходит за силуэт, как и задумано).
|
||||||
|
- Итог: 8 вариантов × 37 видов в банке выглядят идентично карточке улова; геймплей/баланс/сейв/экономика не тронуты (портреты — чистая визуализация `save.aqFish`).
|
||||||
|
|
||||||
|
**Тесты:** в блоке §AQ добавлен подблок «портреты» (+4 проверки): на каждый `aqFish` — `.aqfish` с `use href="#fish-<id>"` (те же спрайты, что `#cardFishUse`), класс варианта и `--f1` ровно у перекрашиваемых, `transform` следует за `aqFishRt` индекс-в-индекс, слой виден только под сценой и `pointer-events:none`. Итого **222/222 PASS, exit 0**; `check.mjs` OK; `tools/balance.mjs` — пины без изменений.
|
||||||
|
|
||||||
|
**Скриншоты:** перегенерированы только кадры `aquarium` (оба набора, 6 PNG) — в банке теперь SVG-портреты. Остальные кадры не трогали: набор в целом не побитово-детерминирован (анимации/время), а canvas-сцены ловли ревизия не касается, так что лишний шум в диффе не нужен.
|
||||||
|
|
||||||
|
**Доки:** AGENTS.md (пункт «Аквариум» + глобалы: `AQ_VARIANT_RECOLOR`/`AQ_VARIANT_CLASS`/`aqApplyVariant`/`aqFishEls`/`aqBuildFishEl`/`aqPlaceFish` вместо `AQ_VARIANT_FX`/`AQ_VARIANT_HUE`), KODA.md (код-карта, глобалы), этот DEVLOG.
|
||||||
|
|||||||
@@ -170,8 +170,9 @@ if(save.aqFish.length>save.aqSlots) save.aqFish.length=save.aqSlots;
|
|||||||
|
|
||||||
## 15. Риски
|
## 15. Риски
|
||||||
|
|
||||||
1. **Canvas = силуэты**, не SVG-портреты (`<symbol id="fish-*">` — DOM-only). Визуал банки — `drawFishShape` +
|
1. ~~**Canvas = силуэты**, не SVG-портреты (`<symbol id="fish-*">` — DOM-only). Визуал банки — `drawFishShape` +
|
||||||
вариантовый оттенок + свечение болотных. Растеризацию SVG в MVP не берём (дорого, риск с `file://`).
|
вариантовый оттенок + свечение болотных.~~ **Закрыто ревизией:** жильцы — DOM-слой `#aqFishLayer` с теми же
|
||||||
|
SVG-портретами, что на карточке улова (вода/стекло/ауры остаются canvas); растеризация SVG не понадобилась.
|
||||||
2. **`uiOpen`/пауза**: сцена живёт под `uiOpen` — focus/visibility-пауза (`YG.onPause`) должна работать;
|
2. **`uiOpen`/пауза**: сцена живёт под `uiOpen` — focus/visibility-пауза (`YG.onPause`) должна работать;
|
||||||
`#aqBar`/`#btnPause` не должны «залипать» (проверить `syncDom` + плейтест).
|
`#aqBar`/`#btnPause` не должны «залипать» (проверить `syncDom` + плейтест).
|
||||||
3. **Раздувание сейва**: экземпляров ≤ `AQ_MAX_SLOTS` — не проблема.
|
3. **Раздувание сейва**: экземпляров ≤ `AQ_MAX_SLOTS` — не проблема.
|
||||||
|
|||||||
@@ -243,6 +243,26 @@ body.aq-open #hud{display:none}
|
|||||||
box-shadow:0 12px 34px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.1)}
|
box-shadow:0 12px 34px rgba(0,0,0,.55),inset 0 1px 0 rgba(255,255,255,.1)}
|
||||||
#aqPopName{font-size:17px;font-weight:800}
|
#aqPopName{font-size:17px;font-weight:800}
|
||||||
#aqPopSub{font-size:12px;color:#9fc3e2;margin:4px 0 10px}
|
#aqPopSub{font-size:12px;color:#9fc3e2;margin:4px 0 10px}
|
||||||
|
/* Ревизия: жильцы банки — те же SVG-портреты, что в карточке улова. Слой поверх canvas (вода/
|
||||||
|
стекло/ауры рисуются в drawAquarium), но под баром/поповером (#aqBar/#aqPop, z-index 26).
|
||||||
|
Вне сцены выключен (body.aq-open); клики не перехватывает (pointer-events:none). */
|
||||||
|
#aqFishLayer{position:fixed;inset:0;z-index:5;pointer-events:none;display:none;overflow:hidden}
|
||||||
|
body.aq-open #aqFishLayer{display:block}
|
||||||
|
#aqFishLayer .aqfish{position:absolute;left:0;top:0;will-change:transform}
|
||||||
|
#aqFishLayer .aqfish svg{width:100%;height:100%;display:block;overflow:visible}
|
||||||
|
/* Вариант — те же правила, что у карточки улова (#card.<variant>-card .bigemoji):
|
||||||
|
перекраска --f1/--f3 идёт inline (наследуется сквозь <use>), фильтры — классами */
|
||||||
|
#aqFishLayer .aqfish.varcopper{filter:drop-shadow(0 0 12px rgba(217,154,98,.9))}
|
||||||
|
#aqFishLayer .aqfish.varsilver{filter:grayscale(.85) brightness(1.25) drop-shadow(0 0 12px rgba(210,228,245,.9))}
|
||||||
|
#aqFishLayer .aqfish.vargold{filter:drop-shadow(0 0 12px rgba(255,215,94,.9))}
|
||||||
|
#aqFishLayer .aqfish.varquartz{filter:drop-shadow(0 0 12px rgba(205,191,224,.9))}
|
||||||
|
#aqFishLayer .aqfish.varemerald{filter:drop-shadow(0 0 12px rgba(46,189,107,.9))}
|
||||||
|
#aqFishLayer .aqfish.vardiamond{filter:drop-shadow(0 0 12px rgba(109,184,242,.95))}
|
||||||
|
#aqFishLayer .aqfish.varrainbow{animation:aqrainbowhue 2.4s linear infinite}
|
||||||
|
@keyframes aqrainbowhue{
|
||||||
|
from{filter:hue-rotate(0deg) drop-shadow(0 0 12px rgba(255,120,220,.8))}
|
||||||
|
to{filter:hue-rotate(360deg) drop-shadow(0 0 12px rgba(120,220,255,.8))}
|
||||||
|
}
|
||||||
|
|
||||||
.twobtns{display:flex;gap:10px;margin-top:16px}
|
.twobtns{display:flex;gap:10px;margin-top:16px}
|
||||||
.twobtns .big{margin-top:0;width:auto;flex:1}
|
.twobtns .big{margin-top:0;width:auto;flex:1}
|
||||||
@@ -1220,6 +1240,9 @@ body.aq-open #hud{display:none}
|
|||||||
</svg>
|
</svg>
|
||||||
<!-- FISH-SVG:END -->
|
<!-- FISH-SVG:END -->
|
||||||
<canvas id="game"></canvas>
|
<canvas id="game"></canvas>
|
||||||
|
<!-- Этап 46 (ревизия): рыбы-жильцы банки — DOM-слой тех же SVG-портретов, что на карточке улова
|
||||||
|
(спрайт FISH-SVG + вариантное оформление). Слой выключен вне сцены (body.aq-open) -->
|
||||||
|
<div id="aqFishLayer" aria-hidden="true"></div>
|
||||||
|
|
||||||
<div id="hud">
|
<div id="hud">
|
||||||
<div class="hudleft">
|
<div class="hudleft">
|
||||||
@@ -3538,11 +3561,29 @@ let aqSel=-1; // выбранный экземпляр (-1 — ниче
|
|||||||
let aqFromMenu=false; // вход был из хаба «Меню» (аналог shopFromMenu): «Назад» вернёт в #gamemenu
|
let aqFromMenu=false; // вход был из хаба «Меню» (аналог shopFromMenu): «Назад» вернёт в #gamemenu
|
||||||
let aqTank={x:0,y:0,w:0,h:0,sandY:0}; // геометрия банки (пересобирается в initAquarium)
|
let aqTank={x:0,y:0,w:0,h:0,sandY:0}; // геометрия банки (пересобирается в initAquarium)
|
||||||
let aqPlants=[], aqBubbles=[]; // декор сцены (растения/пузыри) — тоже не в сейве
|
let aqPlants=[], aqBubbles=[]; // декор сцены (растения/пузыри) — тоже не в сейве
|
||||||
|
let aqFishEls=[]; // DOM-элементы жильцов (#aqFishLayer) — те же SVG-портреты, что на карточке; индекс = save.aqFish
|
||||||
|
|
||||||
/* Подкраска варианта в банке: сдвиг hue силуэта + цветной блик-ореол (normal — без эффекта) */
|
/* Вариантное оформление жильца — ровно как на карточке улова: перекраска тела/плавников
|
||||||
const AQ_VARIANT_FX={copper:'#d99a62',silver:'#dfe9f5',quartz:'#cdbfe0',gold:'#ffd75e',
|
(--f1/--f3, наследуется сквозь <use> в спрайт) для copper/gold/quartz/emerald/diamond,
|
||||||
emerald:'#2ebd6b',diamond:'#6db8f2',rainbow:'#c56cf0'};
|
плюс класс .var<id> по варианту — CSS в #aqFishLayer добавляет цветной drop-shadow
|
||||||
const AQ_VARIANT_HUE={copper:14,silver:0,quartz:35,gold:-8,emerald:-45,diamond:18,rainbow:0};
|
(silver — grayscale+brightness, rainbow — вращение оттенка). normal — без эффекта,
|
||||||
|
как и на карточке. */
|
||||||
|
const AQ_VARIANT_RECOLOR={
|
||||||
|
copper:{f1:'hsl(25,55%,55%)',f3:'hsl(25,50%,35%)'},
|
||||||
|
gold:{f1:'#ffd75e',f3:'hsl(45,70%,40%)'},
|
||||||
|
quartz:{f1:'hsl(270,25%,72%)',f3:'hsl(270,25%,45%)'},
|
||||||
|
emerald:{f1:'hsl(150,55%,42%)',f3:'hsl(150,50%,28%)'},
|
||||||
|
diamond:{f1:'hsl(200,60%,78%)',f3:'hsl(200,50%,45%)'}
|
||||||
|
};
|
||||||
|
/* CSS-классы оформления варианта в банке — те же имена, что у карточки (var<id>),
|
||||||
|
кроме normal (на карточке normal тоже без оформления) */
|
||||||
|
const AQ_VARIANT_CLASS=['copper','silver','quartz','gold','emerald','diamond','rainbow'];
|
||||||
|
function aqApplyVariant(el,variant){
|
||||||
|
const r=AQ_VARIANT_RECOLOR[variant.id];
|
||||||
|
if(r){ el.style.setProperty('--f1',r.f1); el.style.setProperty('--f3',r.f3); }
|
||||||
|
else{ el.style.removeProperty('--f1'); el.style.removeProperty('--f3'); }
|
||||||
|
for(const id of AQ_VARIANT_CLASS) el.classList.toggle('var'+id,variant.id===id);
|
||||||
|
}
|
||||||
|
|
||||||
/* Размер силуэта в банке: от веса к пределу вида (w/wMax) и редкости; кламп — чтобы влезал */
|
/* Размер силуэта в банке: от веса к пределу вида (w/wMax) и редкости; кламп — чтобы влезал */
|
||||||
function aqFishSize(sp,w){
|
function aqFishSize(sp,w){
|
||||||
@@ -3550,6 +3591,34 @@ function aqFishSize(sp,w){
|
|||||||
const s=8+rel*22+sp.rarity*1.6;
|
const s=8+rel*22+sp.rarity*1.6;
|
||||||
return Math.max(7,Math.min(Math.min(W,H)*0.11,s));
|
return Math.max(7,Math.min(Math.min(W,H)*0.11,s));
|
||||||
}
|
}
|
||||||
|
/* Позиция жильца в DOM: та же система координат, что у canvas (W/H — CSS-px), элемент
|
||||||
|
центрируется на (x,y) и зеркалится по направлению. Вызывается каждый кадр из updateAquarium. */
|
||||||
|
function aqPlaceFish(el,rt){
|
||||||
|
if(!el||!rt) return;
|
||||||
|
el.style.transform='translate('+rt.x.toFixed(1)+'px,'+rt.y.toFixed(1)+'px) translate(-50%,-50%)'
|
||||||
|
+' rotate('+(Math.sin(rt.tail||0)*4).toFixed(2)+'deg) scale('+rt.dir+',1)';
|
||||||
|
}
|
||||||
|
/* DOM-элемент жильца: <div class=aqfish><svg viewBox=0 0 160 100><use href="#fish-<id>"/></svg></div>.
|
||||||
|
Те же SVG-портреты и вариантное оформление, что на карточке улова (Э42 + ревизия). Размер —
|
||||||
|
aqFishSize (прежняя шкала): длина 2·size, высота по соотношению 160:100. */
|
||||||
|
function aqBuildFishEl(i){
|
||||||
|
const f=save.aqFish[i];
|
||||||
|
const sp=SPECIES.find(s=>s.id===f.id); if(!sp) return null;
|
||||||
|
const variant=VARIANTS.find(v=>v.id===f.v)||VARIANTS[0];
|
||||||
|
const size=aqFishSize(sp,f.w);
|
||||||
|
const el=document.createElement('div');
|
||||||
|
el.className='aqfish'; el.dataset.i=i;
|
||||||
|
el.style.width=(size*2).toFixed(1)+'px';
|
||||||
|
el.style.height=(size*1.25).toFixed(1)+'px';
|
||||||
|
const svg=document.createElementNS('http://www.w3.org/2000/svg','svg');
|
||||||
|
svg.setAttribute('viewBox','0 0 160 100');
|
||||||
|
const use=document.createElementNS('http://www.w3.org/2000/svg','use');
|
||||||
|
use.setAttribute('href','#fish-'+sp.id);
|
||||||
|
svg.appendChild(use); el.appendChild(svg);
|
||||||
|
aqApplyVariant(el,variant);
|
||||||
|
aqPlaceFish(el,aqFishRt[i]);
|
||||||
|
return el;
|
||||||
|
}
|
||||||
/* Раскладка банки и экземпляров по текущим W/H; вызывается при входе и на resize */
|
/* Раскладка банки и экземпляров по текущим W/H; вызывается при входе и на resize */
|
||||||
function initAquarium(){
|
function initAquarium(){
|
||||||
aqTank.x=14; aqTank.y=14;
|
aqTank.x=14; aqTank.y=14;
|
||||||
@@ -3571,6 +3640,17 @@ function initAquarium(){
|
|||||||
spd:rand(12,26), vy:rand(5,12)
|
spd:rand(12,26), vy:rand(5,12)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
// Рыбы-жильцы — DOM-слой тех же SVG-портретов (по индексу save.aqFish); слой обрезан по банке
|
||||||
|
const layer=$('aqFishLayer');
|
||||||
|
layer.innerHTML='';
|
||||||
|
layer.style.clipPath='inset('+aqTank.y+'px '+(W-aqTank.x-aqTank.w).toFixed(1)+'px '
|
||||||
|
+(H-aqTank.y-aqTank.h).toFixed(1)+'px '+aqTank.x+'px round 18px)';
|
||||||
|
aqFishEls=[];
|
||||||
|
for(let i=0;i<n;i++){
|
||||||
|
const el=aqBuildFishEl(i); // индекс массива = индекс save.aqFish (даже если вид битый → null)
|
||||||
|
aqFishEls[i]=el;
|
||||||
|
if(el) layer.appendChild(el);
|
||||||
|
}
|
||||||
aqPlants=[];
|
aqPlants=[];
|
||||||
const np=5+((Math.random()*4)|0);
|
const np=5+((Math.random()*4)|0);
|
||||||
for(let k=0;k<np;k++) aqPlants.push({
|
for(let k=0;k<np;k++) aqPlants.push({
|
||||||
@@ -3599,13 +3679,15 @@ function updateAquarium(dt){
|
|||||||
if(f.y<top) f.y=top;
|
if(f.y<top) f.y=top;
|
||||||
if(f.y>bot) f.y=bot;
|
if(f.y>bot) f.y=bot;
|
||||||
}
|
}
|
||||||
|
for(let i=0;i<aqFishRt.length;i++) aqPlaceFish(aqFishEls[i],aqFishRt[i]); // синхронизируем DOM-портреты
|
||||||
for(const b of aqBubbles){
|
for(const b of aqBubbles){
|
||||||
b.y-=b.v*dt; b.ph+=dt*2;
|
b.y-=b.v*dt; b.ph+=dt*2;
|
||||||
if(b.y<aqTank.y+10){ b.y=aqTank.sandY-4; b.x=rand(left,right); }
|
if(b.y<aqTank.y+10){ b.y=aqTank.sandY-4; b.x=rand(left,right); }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/* Отрисовка банки: вода/стекло (hexLerp к nightMix — общий цикл суток), грунт, растения,
|
/* Отрисовка банки: вода/стекло (hexLerp к nightMix — общий цикл суток), грунт, растения,
|
||||||
пузыри, рыбы (drawFishShape + подкраска варианта, свечение болотных, рамка выбора) */
|
пузыри, аура болотных и рамка выбора. Сами рыбы-жильцы — DOM-слой #aqFishLayer
|
||||||
|
(SVG-портреты, как на карточке улова) поверх canvas. */
|
||||||
function drawAquarium(){
|
function drawAquarium(){
|
||||||
const nm=nightMix();
|
const nm=nightMix();
|
||||||
const x=aqTank.x, y=aqTank.y, w=aqTank.w, h=aqTank.h;
|
const x=aqTank.x, y=aqTank.y, w=aqTank.w, h=aqTank.h;
|
||||||
@@ -3660,26 +3742,18 @@ function drawAquarium(){
|
|||||||
for(const b of aqBubbles){
|
for(const b of aqBubbles){
|
||||||
ctx.beginPath(); ctx.arc(b.x+Math.sin(b.ph)*3,b.y,b.r,0,7); ctx.stroke();
|
ctx.beginPath(); ctx.arc(b.x+Math.sin(b.ph)*3,b.y,b.r,0,7); ctx.stroke();
|
||||||
}
|
}
|
||||||
// рыбы-жильцы
|
// рыбы-жильцы: сами портреты — DOM-слой #aqFishLayer (те же SVG-спрайты и вариантное
|
||||||
for(let i=0;i<save.aqFish.length;i++){
|
// оформление, что на карточке улова); на canvas — только ореол свечения болотных
|
||||||
|
// (уходит ЗА портрет) и рамка выбора
|
||||||
|
for(let i=0;i<aqFishRt.length;i++){
|
||||||
const f=save.aqFish[i], rt=aqFishRt[i];
|
const f=save.aqFish[i], rt=aqFishRt[i];
|
||||||
if(!rt) continue;
|
if(!f||!rt) continue;
|
||||||
const sp=SPECIES.find(s=>s.id===f.id); if(!sp) continue;
|
const sp=SPECIES.find(s=>s.id===f.id); if(!sp) continue;
|
||||||
const variant=VARIANTS.find(v=>v.id===f.v)||VARIANTS[0];
|
|
||||||
const size=aqFishSize(sp,f.w);
|
const size=aqFishSize(sp,f.w);
|
||||||
const fx=AQ_VARIANT_FX[variant.id];
|
if(sp.glow) drawGlowAura(rt.x,rt.y,size*1.5,sp.glow,glowIntensity()); // болотные — свечение вида
|
||||||
if(fx) drawGlowAura(rt.x,rt.y,size*1.4,fx,0.55+0.35*Math.sin(time*2.4+i)); // подкраска варианта
|
|
||||||
if(sp.glow) drawGlowAura(rt.x,rt.y,size*1.5,sp.glow,glowIntensity()); // болотные — свечение вида
|
|
||||||
const hue=sp.hue+(AQ_VARIANT_HUE[variant.id]||0);
|
|
||||||
drawFishShape(rt.x,rt.y,size,rt.dir,hue,1,Math.sin(rt.tail)*0.3);
|
|
||||||
if(fx){ // блик-искра у особых вариантов
|
|
||||||
const a=0.5+0.5*Math.sin(time*3+i*1.7);
|
|
||||||
ctx.fillStyle='rgba(255,255,255,'+(0.5*a).toFixed(3)+')';
|
|
||||||
ctx.beginPath(); ctx.arc(rt.x-size*0.2,rt.y-size*0.55,Math.max(1,size*0.08),0,7); ctx.fill();
|
|
||||||
}
|
|
||||||
if(aqSel===i){ // рамка выбранной рыбы
|
if(aqSel===i){ // рамка выбранной рыбы
|
||||||
ctx.strokeStyle='#c8fff4'; ctx.lineWidth=2;
|
ctx.strokeStyle='#c8fff4'; ctx.lineWidth=2;
|
||||||
ctx.beginPath(); ctx.ellipse(rt.x,rt.y,size*1.35,size*0.95,0,0,7); ctx.stroke();
|
ctx.beginPath(); ctx.ellipse(rt.x,rt.y,size*1.25,size*0.85,0,0,7); ctx.stroke();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.restore();
|
ctx.restore();
|
||||||
@@ -3737,6 +3811,8 @@ function aqReleaseSel(){
|
|||||||
if(aqSel<0||aqSel>=save.aqFish.length) return;
|
if(aqSel<0||aqSel>=save.aqFish.length) return;
|
||||||
save.aqFish.splice(aqSel,1);
|
save.aqFish.splice(aqSel,1);
|
||||||
aqFishRt.splice(aqSel,1);
|
aqFishRt.splice(aqSel,1);
|
||||||
|
const el=aqFishEls[aqSel]; if(el) el.remove(); // портрет (DOM) уносим точечно — остальных не трогаем
|
||||||
|
aqFishEls.splice(aqSel,1);
|
||||||
aqSel=-1;
|
aqSel=-1;
|
||||||
hideAqPop();
|
hideAqPop();
|
||||||
persist();
|
persist();
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 108 KiB After Width: | Height: | Size: 106 KiB |
|
Before Width: | Height: | Size: 152 KiB After Width: | Height: | Size: 153 KiB |
|
Before Width: | Height: | Size: 191 KiB After Width: | Height: | Size: 200 KiB |
|
Before Width: | Height: | Size: 106 KiB After Width: | Height: | Size: 115 KiB |
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 155 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 195 KiB |
@@ -3211,11 +3211,13 @@ async function toGameMenu(p) {
|
|||||||
|
|
||||||
// --- AQ. Этап 46: Аквариум (миграция / «В аквариум» / лимит / слот / сцена / выпуск) ---
|
// --- AQ. Этап 46: Аквариум (миграция / «В аквариум» / лимит / слот / сцена / выпуск) ---
|
||||||
// Глобалы игры (page.evaluate): aqOpen, aqFishRt, aqSel, aqTank, AQ_BASE_SLOTS, AQ_MAX_SLOTS,
|
// Глобалы игры (page.evaluate): aqOpen, aqFishRt, aqSel, aqTank, AQ_BASE_SLOTS, AQ_MAX_SLOTS,
|
||||||
// AQ_SLOT_COSTS, AQ_VARIANT_FX/HUE, initAquarium, updateAquarium, drawAquarium, aqFishSize,
|
// AQ_SLOT_COSTS, AQ_VARIANT_RECOLOR, AQ_VARIANT_CLASS, aqApplyVariant, initAquarium,
|
||||||
|
// updateAquarium, drawAquarium, aqFishSize, aqFishEls, aqBuildFishEl, aqPlaceFish,
|
||||||
// aqTap, showAqPop, hideAqPop, renderAqBar, aqReleaseSel, openAquarium, closeAquarium,
|
// aqTap, showAqPop, hideAqPop, renderAqBar, aqReleaseSel, openAquarium, closeAquarium,
|
||||||
// syncKeepBtn, cardSpec, save.aqFish/save.aqSlots — имена не переименовывать (тесты молча ломаются).
|
// syncKeepBtn, cardSpec, save.aqFish/save.aqSlots — имена не переименовывать (тесты молча ломаются).
|
||||||
// DOM: #btnGmAquarium, #btnKeep, #aqBar, #btnAqBack, #aqCount, #btnAqSlot, #aqPop,
|
// DOM: #btnGmAquarium, #btnKeep, #aqBar, #btnAqBack, #aqCount, #btnAqSlot, #aqPop,
|
||||||
// #aqPopName, #aqPopSub, #btnAqRelease.
|
// #aqPopName, #aqPopSub, #btnAqRelease, #aqFishLayer (DOM-слой портретов — ревизия: жильцы
|
||||||
|
// те же SVG-спрайты #fish-<id>, что на карточке улова; вариант — класс .var<id> + --f1/--f3).
|
||||||
|
|
||||||
// (1) данные: freshSave() кладёт aqFish:[] и aqSlots:AQ_BASE_SLOTS; кноб слотов (6/12 + лестница цен)
|
// (1) данные: freshSave() кладёт aqFish:[] и aqSlots:AQ_BASE_SLOTS; кноб слотов (6/12 + лестница цен)
|
||||||
const aqFresh = await page.evaluate(() => {
|
const aqFresh = await page.evaluate(() => {
|
||||||
@@ -3371,6 +3373,60 @@ async function toGameMenu(p) {
|
|||||||
aqSc2.open === false && aqSc2.barHidden === true && aqSc2.gm === true && aqSc2.ui === true,
|
aqSc2.open === false && aqSc2.barHidden === true && aqSc2.gm === true && aqSc2.ui === true,
|
||||||
JSON.stringify(aqSc2));
|
JSON.stringify(aqSc2));
|
||||||
|
|
||||||
|
// (6b) ревизия: жильцы банки — те же SVG-портреты, что на карточке улова (спрайт #fish-<id>),
|
||||||
|
// вариант — класс .var<id> (silver/rainbow — фильтры, copper/gold/quartz/emerald/diamond — drop-shadow),
|
||||||
|
// DOM-элемент на каждый save.aqFish, transform следует за aqFishRt (индекс в индекс), слой
|
||||||
|
// виден только под сценой (body.aq-open) и не перехватывает клики
|
||||||
|
await page.click('#btnGmAquarium');
|
||||||
|
await sleep(200);
|
||||||
|
const aqDom = await page.evaluate(() => {
|
||||||
|
save.aqFish = [{ id: 'carass', v: 'normal', w: 0.6 }, { id: 'pike', v: 'silver', w: 7.4 },
|
||||||
|
{ id: 'gold', v: 'rainbow', w: 0.8 }, { id: 'tench', v: 'emerald', w: 3 }];
|
||||||
|
save.aqSlots = AQ_BASE_SLOTS; persist();
|
||||||
|
initAquarium();
|
||||||
|
const layer = document.getElementById('aqFishLayer');
|
||||||
|
const els = [...layer.querySelectorAll('.aqfish')];
|
||||||
|
return {
|
||||||
|
n: els.length, len: save.aqFish.length,
|
||||||
|
hrefs: els.map(e => e.querySelector('use').getAttribute('href')),
|
||||||
|
ids: save.aqFish.map(f => '#fish-' + f.id),
|
||||||
|
variants: els.map(e => AQ_VARIANT_CLASS.filter(id => e.classList.contains('var' + id)).join(',')),
|
||||||
|
f1: els.map(e => e.style.getPropertyValue('--f1')),
|
||||||
|
rt: aqFishRt.map(r => [r.x, r.y]),
|
||||||
|
tr: els.map(e => e.style.transform),
|
||||||
|
click: getComputedStyle(layer).pointerEvents,
|
||||||
|
cardHref: document.getElementById('cardFishUse').getAttribute('href')
|
||||||
|
};
|
||||||
|
});
|
||||||
|
const aqDomTrack = await page.evaluate(() => {
|
||||||
|
const els = [...document.querySelectorAll('#aqFishLayer .aqfish')];
|
||||||
|
return els.every((e, i) => {
|
||||||
|
const m = /translate\(([-\d.]+)px,\s*([-\d.]+)px\)/.exec(e.style.transform);
|
||||||
|
return m && aqFishRt[i] && Math.abs(+m[1] - aqFishRt[i].x) < 0.1 && Math.abs(+m[2] - aqFishRt[i].y) < 0.1;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
ok('Этап 46 портреты: на каждый aqFish — .aqfish с <use href="#fish-<id>"> (те же спрайты, что на карточке)',
|
||||||
|
aqDom.n === aqDom.len && aqDom.len === 4
|
||||||
|
&& JSON.stringify(aqDom.hrefs) === JSON.stringify(aqDom.ids)
|
||||||
|
&& aqDom.cardHref.indexOf('#fish-') === 0,
|
||||||
|
JSON.stringify(aqDom));
|
||||||
|
ok('Этап 46 портреты: вариант как на карточке — .var<id> по варианту (silver/rainbow/emerald) + --f1 у перекраски; normal — без класса',
|
||||||
|
aqDom.variants[0] === '' && aqDom.variants[1] === 'silver'
|
||||||
|
&& aqDom.variants[2] === 'rainbow' && aqDom.variants[3] === 'emerald'
|
||||||
|
&& aqDom.f1[0] === '' && aqDom.f1[3] !== '' && aqDom.click === 'none',
|
||||||
|
JSON.stringify(aqDom));
|
||||||
|
ok('Этап 46 портреты: transform каждого .aqfish следует за aqFishRt (индекс в индекс), слой виден в сцене',
|
||||||
|
aqDomTrack === true && aqDom.tr.every(t => t.indexOf('translate(') === 0),
|
||||||
|
JSON.stringify({ track: aqDomTrack, tr: aqDom.tr }));
|
||||||
|
await page.click('#btnAqBack');
|
||||||
|
await sleep(150);
|
||||||
|
const aqDomClosed = await page.evaluate(() => ({
|
||||||
|
disp: getComputedStyle(document.getElementById('aqFishLayer')).display,
|
||||||
|
cls: document.body.classList.contains('aq-open')
|
||||||
|
}));
|
||||||
|
ok('Этап 46 портреты: вне сцены слой скрыт (display:none, body без aq-open)',
|
||||||
|
aqDomClosed.disp === 'none' && aqDomClosed.cls === false, JSON.stringify(aqDomClosed));
|
||||||
|
|
||||||
// (7) слот: при достатке монет #btnAqSlot списывает AQ_SLOT_COSTS[aqSlots-AQ_BASE_SLOTS] и даёт +1 слот
|
// (7) слот: при достатке монет #btnAqSlot списывает AQ_SLOT_COSTS[aqSlots-AQ_BASE_SLOTS] и даёт +1 слот
|
||||||
await page.click('#btnGmAquarium');
|
await page.click('#btnGmAquarium');
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
|
|||||||