diff --git a/AGENTS.md b/AGENTS.md index 80441e9..3d852a8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -55,7 +55,7 @@ - Закрытие по тапу мимо диалога: `backdropClose(el,fn)` (секция «КНОПКИ», после Y2-хендлеров) — `pointerdown`, где `e.target ===` сам оверлей (тап на подложку, не .panel), закрывает диалог тем же действием, что и кнопка: `#shop`→`closeShop`, `#resetDlg`→«Отмена» (`btnResetNo.click()`), `#pause`→`closePause`, `#gamemenu`→«Продолжить» (`btnGmResume.click()`), `#card`→«Отлично!» (`btnCardOk.click()`, вкл. интерстишел). `#menu` — экран старта, не диалог. Новый оверлей-диалог без `backdropClose` не добавлять. Подводный камень: у `#card` pop-анимация (`#card{animation:pop .3s}` — transform scale) — пока идёт (300 мс), hit-area подложки сужена и тап мимо уходит на канвас (в реальном флоу state='CARD' → onDown no-op, безвредно; тест §X ждёт 400 мс после showCard). - Встроенный emoji-шрифт (Э28): все emoji UI и canvas рисует вшитый шрифт — субсет Noto Color Emoji (COLRv1, OFL) как base64 `@font-face font-family:'EmojiEmbedded'` в index.html между маркерами `` (внешний .woff2 невозможен: публикуется только один index.html). В font-stack `html,body` и во всех `ctx.font` (константа `UI_FONT`, секция «ОТРИСОВКА») стоит ПЕРВЫМ — единый Google-стиль emoji на всех платформах; headless-скриншоты не зависят от системных шрифтов. Первый кадр ждёт `document.fonts.load('16px "EmojiEmbedded"')` с timeout-фолбэком 1.5 с. Генератор `tools/emoji-font.py` (venv: `python3 -m venv tools/.venv && tools/.venv/bin/pip install fonttools brotli`; исходник `tools/fonts-src/NotoColorEmoji-Regular.ttf` из google/fonts, всё в gitignore): `python3 tools/emoji-font.py` — перегенерация по текущим emoji index.html (ОБЯЗАТЕЛЬНО при добавлении новых emoji), `--check` — падает, если субсет не покрывает index.html (✓ № ● ○ ↩ — в NCE их нет, это ок, рисуются системными шрифтами). Тест §Y7 (tests/test.js) гоняет `document.fonts.check('16px "EmojiEmbedded"','🪙')`; семейство переименовывать нельзя (тест + @font-face + UI_FONT). - **Портреты рыб (SVG-спрайт)**: исходники `assets/fish/fish-.svg` (37 файлов — 5 болотных добавлено этапом 45, viewBox 0 0 160 100, цвета `var(--f1..f4, hsl-fallback)`; переменные НЕ объявлять на ``/``); встраивание — `node tools/embed-fish.mjs` (идемпотентно, блок между маркерами `FISH-SVG:BEGIN/END` в index.html, сразу после ``; самопроверка + `check.mjs` внутри); приёмка — `tools/fish-preview.html` (file://); дневник/карточка показывают через `` (дневник `.jrow .je svg`, карточка `#cardEmoji svg`, `showCard` ставит href у `#cardFishUse`); перекраска вариантов — CSS-переменные `--f1`/`--f3` в `#card.-card .bigemoji` (наследуются сквозь ``; silver/rainbow — существующие filter); правка портрета = правка `assets/fish/*.svg` + повторный запуск embed-скрипта. Эмодзи рыб УБРАНЫ (этап 42): поле emoji удалено из SPECIES, тосты без эмодзи, иконка квеста catch_species — mini-SVG ``; субсет emoji-шрифта перегенерирован (рыбных глифов нет). -- **Аквариум (Этап 46)**: декоративная сцена коллекции ЭКЗЕМПЛЯРОВ (вид+вариант+вес), **НЕ локация** — в `LOCATIONS`/`SPECIES_BY_LOCATION`/`BOSS_BY_LOCATION` не входит, роллы/боссы/баланс/пины не трогает (`tools/balance.mjs` не менялся). Данные: `save.aqFish[]` (`{id,v,w}`) + `save.aqSlots` (старт `AQ_BASE_SLOTS=6`, макс `AQ_MAX_SLOTS=12`), константы `AQ_BASE_SLOTS`/`AQ_MAX_SLOTS`/`AQ_SLOT_COSTS=[1500,4000,9000,20000,45000,100000]` в секции ДАННЫЕ рядом со скинами/локациями. **Модель C — бесплатная КОПИЯ**: `catchFish()` НЕ меняется (монеты/квесты/ачивки/реклама начисляются на улове как раньше), кнопка `#btnKeep` «🐠 В аквариум» на карточке улова кладёт копию экземпляра (`cardSpec={sp,w,variant}` в `showCard()`, `syncKeepBtn()` — `disabled` + подпись/`title` «Нет места» при `save.aqFish.length>=save.aqSlots`), затем `persist()`/`checkAchievements()`/тост `aq_kept`/закрытие карточки/`state='IDLE'`/`tryInterstitial()` (единообразно с `#btnCardOk`). Сцена: `aqOpen`, `aqFishRt[]` (раскладка `x,y,dir,ph,spd,vy,tail` — НЕ персистится, индекс = `save.aqFish`), `aqSel` (сентинел `-1` = ничего), `aqTank`/`aqPlants`/`aqBubbles`, `initAquarium()`/`updateAquarium(dt)`/`drawAquarium()` (вода `hexLerp` к `nightMix()` — общий цикл суток; размер `aqFishSize(sp,w)`; сетка стартовых позиций, разворот у стенок, пузыри). **Ревизия: жильцы банки — те же SVG-портреты, что на карточке улова** (DOM-слой `#aqFishLayer` поверх canvas, спрайт `#fish-` + вариант `.var`/`--f1`/`--f3` 1:1 с карточкой; `aqBuildFishEl(i)`/`aqPlaceFish(el,rt)`/`aqFishEls[]`, transform обновляется в `updateAquarium`; слой включается только под `body.aq-open`, `pointer-events:none`, `clip-path` по банке). На canvas — вода/грунт/растения/пузыри/стекло, свечение болотных `drawGlowAura`+`glowIntensity()` (уходит ЗА портрет) и рамка выбора; `drawFishShape` в банке больше не используется (остался для рыб в сценах ловли). Точки жизненного цикла: `frame()` (`if(aqOpen) updateAquarium(dt)`), `draw()` (`if(aqOpen){ drawAquarium(); ctx.restore(); return; }` — банка ВМЕСТО озера), `onDown()` (ранний `if(aqOpen){ aqTap(x,y); return; }`), `resize()` (`if(aqOpen) initAquarium()`), `syncDom()` (под `aqOpen` `#btnPause`/`#btnRecall` не показываются). DOM: вход `#btnGmAquarium` в хабе `#gamemenu` → `openAquarium()`/`closeAquarium()` (аналог `shopFromMenu` — флаг `aqFromMenu`, «Назад» возвращает в гменю; `YG.gameplayStop()`; `uiOpen=true`); бар `#aqBar` (`#btnAqBack`, `#aqCount` «N/M», `#btnAqSlot` — цена `AQ_SLOT_COSTS[aqSlots-AQ_BASE_SLOTS]`, при максе `disabled`), поповер `#aqPop` (`#aqPopName`/`#aqPopSub` — вес · редкость, БЕЗ цены), тап по рыбе `aqTap(x,y)` (ближайшая в радиусе → `aqSel` → `showAqPop()`). «Выпустить» = `#btnAqRelease` → `aqReleaseSel()` — точечное удаление экземпляра ИНДЕКСАМИ (без `initAquarium`, позиции остальных сохраняются) **БЕЗ монет** (освобождение слота; продажа из банки была в плане §7, но по решению владельца заменена на выпуск — экономика не затронута). Миграция `loadSave()`: `aqFish` фильтруется по `SPECIES` (`Number.isFinite(w)&&w>0`), вариант → `'normal'`, `aqSlots` — `Math.trunc` + клампы `AQ_BASE_SLOTS..AQ_MAX_SLOTS`, хвост обрезается по слотам. Ачивки 36→38: `aq_first` «Первый жилец» / `aq_full` «Аквариум полон» — обе `bonus:{}` (пин `achBonus().coinPct=0.18` не сдвинут). i18n: `gm_aquarium`, `aq_title`, `aq_keep`, `aq_back`, `aq_slots`, `aq_buy_slot`, `aq_max`, `aq_release`, `aq_released`, `aq_no_room`, `aq_kept` (RU/EN). Emoji: 🐠 добавлен во встроенный субсет (перегенерация `tools/emoji-font.py`, `--check` OK). Подводные камни: `body.aq-open` прячет `#hud` (CSS + `syncDom`), а `#aqBar{pointer-events:none}` + `.aqpill{pointer-events:auto}` — иначе невидимая полоса бара перехватывала бы тапы по сцене; сентинел `aqSel=-1` (все проверки индекса — `aqSel>=0`); «выпустить» без монет — не вводить продажу (сознательное решение, дыра ×2 дохода закрыта). +- **Аквариум (Этап 46)**: декоративная сцена коллекции ЭКЗЕМПЛЯРОВ (вид+вариант+вес), **НЕ локация** — в `LOCATIONS`/`SPECIES_BY_LOCATION`/`BOSS_BY_LOCATION` не входит, роллы/боссы/баланс/пины не трогает (`tools/balance.mjs` не менялся). Данные: `save.aqFish[]` (`{id,v,w}`) + `save.aqSlots` (старт `AQ_BASE_SLOTS=6`, макс `AQ_MAX_SLOTS=12`), константы `AQ_BASE_SLOTS`/`AQ_MAX_SLOTS`/`AQ_SLOT_COSTS=[1500,4000,9000,20000,45000,100000]` в секции ДАННЫЕ рядом со скинами/локациями. **Модель C — бесплатная КОПИЯ**: `catchFish()` НЕ меняется (монеты/квесты/ачивки/реклама начисляются на улове как раньше), кнопка `#btnKeep` «🐠 В аквариум» на карточке улова кладёт копию экземпляра (`cardSpec={sp,w,variant}` в `showCard()`, `syncKeepBtn()` — `disabled` + подпись/`title` «Нет места» при `save.aqFish.length>=save.aqSlots`), затем `persist()`/`checkAchievements()`/тост `aq_kept`/закрытие карточки/`state='IDLE'`/`tryInterstitial()` (единообразно с `#btnCardOk`). Сцена: `aqOpen`, `aqFishRt[]` (раскладка `x,y,dir,ph,spd,vy,tail` — НЕ персистится, индекс = `save.aqFish`), `aqSel` (сентинел `-1` = ничего), `aqTank`/`aqPlants`/`aqBubbles`, `initAquarium()`/`updateAquarium(dt)`/`drawAquarium()` (вода `hexLerp` к `nightMix()` — общий цикл суток; размер `aqFishSize(w)` — от АБСОЛЮТНОГО веса по общей с вываживанием кривой `fishSizeFromW()` (Этап 41): `clamp(fishSizeFromW(w)·AQ_FISH_SCALE·k, AQ_FISH_SIZE_MIN·k, min(W,H)·AQ_FISH_SIZE_REL·k)`, `K=0.7`/`MIN=7`/`REL=0.16`, десктоп-множитель `k=AQ_DESKTOP_SCALE=2` при `min(W,H)>=AQ_DESKTOP_MIN=600` (иначе 1) — ревизия после Этапа 46, `w/wMax`+`rarity` больше не влияют; сетка стартовых позиций, разворот у стенок, пузыри). **Ревизия: жильцы банки — те же SVG-портреты, что на карточке улова** (DOM-слой `#aqFishLayer` поверх canvas, спрайт `#fish-` + вариант `.var`/`--f1`/`--f3` 1:1 с карточкой; `aqBuildFishEl(i)`/`aqPlaceFish(el,rt)`/`aqFishEls[]`, transform обновляется в `updateAquarium`; слой включается только под `body.aq-open`, `pointer-events:none`, `clip-path` по банке). На canvas — вода/грунт/растения/пузыри/стекло, свечение болотных `drawGlowAura`+`glowIntensity()` (уходит ЗА портрет) и рамка выбора; `drawFishShape` в банке больше не используется (остался для рыб в сценах ловли). Точки жизненного цикла: `frame()` (`if(aqOpen) updateAquarium(dt)`), `draw()` (`if(aqOpen){ drawAquarium(); ctx.restore(); return; }` — банка ВМЕСТО озера), `onDown()` (ранний `if(aqOpen){ aqTap(x,y); return; }`), `resize()` (`if(aqOpen) initAquarium()`), `syncDom()` (под `aqOpen` `#btnPause`/`#btnRecall` не показываются). DOM: вход `#btnGmAquarium` в хабе `#gamemenu` → `openAquarium()`/`closeAquarium()` (аналог `shopFromMenu` — флаг `aqFromMenu`, «Назад» возвращает в гменю; `YG.gameplayStop()`; `uiOpen=true`); бар `#aqBar` (`#btnAqBack`, `#aqCount` «N/M», `#btnAqSlot` — цена `AQ_SLOT_COSTS[aqSlots-AQ_BASE_SLOTS]`, при максе `disabled`), поповер `#aqPop` (`#aqPopName`/`#aqPopSub` — вес · редкость, БЕЗ цены), тап по рыбе `aqTap(x,y)` (ближайшая в радиусе → `aqSel` → `showAqPop()`). «Выпустить» = `#btnAqRelease` → `aqReleaseSel()` — точечное удаление экземпляра ИНДЕКСАМИ (без `initAquarium`, позиции остальных сохраняются) **БЕЗ монет** (освобождение слота; продажа из банки была в плане §7, но по решению владельца заменена на выпуск — экономика не затронута). Миграция `loadSave()`: `aqFish` фильтруется по `SPECIES` (`Number.isFinite(w)&&w>0`), вариант → `'normal'`, `aqSlots` — `Math.trunc` + клампы `AQ_BASE_SLOTS..AQ_MAX_SLOTS`, хвост обрезается по слотам. Ачивки 36→38: `aq_first` «Первый жилец» / `aq_full` «Аквариум полон» — обе `bonus:{}` (пин `achBonus().coinPct=0.18` не сдвинут). i18n: `gm_aquarium`, `aq_title`, `aq_keep`, `aq_back`, `aq_slots`, `aq_buy_slot`, `aq_max`, `aq_release`, `aq_released`, `aq_no_room`, `aq_kept` (RU/EN). Emoji: 🐠 добавлен во встроенный субсет (перегенерация `tools/emoji-font.py`, `--check` OK). Подводные камни: `body.aq-open` прячет `#hud` (CSS + `syncDom`), а `#aqBar{pointer-events:none}` + `.aqpill{pointer-events:auto}` — иначе невидимая полоса бара перехватывала бы тапы по сцене; сентинел `aqSel=-1` (все проверки индекса — `aqSel>=0`); «выпустить» без монет — не вводить продажу (сознательное решение, дыра ×2 дохода закрыта). ## Запуск и публикация diff --git a/README.md b/README.md index 10cb51f..f57a5d3 100644 --- a/README.md +++ b/README.md @@ -34,7 +34,7 @@ - Погода: дождь (🌧️) — шанс особых рыб ×3; туман (🌫️) — клёв ×1,5; событие идёт 120 с, следующее — раз в 2–4 минуты — дождь - Скины лодки и поплавка 🎨 — вкладка «Скины» в лавке: лодка «Красная лодка» 800 🪙 / «Лодка-домик» 2000 🪙, поплавок «Красный» 500 🪙 / «Неоновый» 1500 🪙 — чистая косметика (без эффектов на геймплей) - Локации: Озёра (65 536 🪙) — судак, хариус, гигантский рак; Море (131 072 🪙) — скумбрия, тунец, рыба-меч; Океан (196 608 🪙 = ×1.5 Моря) — глубоководные гиганты: синий кит, гигантский кальмар, скат-манта, гигантский осьминог; Волшебное болото (90 000 🪙) — светящиеся обитатели (светлячок, лунная рыбка, изумрудный светляк, фиалковый огонёк, дух болота), ночью рыбы и фоновые стайки светятся разными цветами, вода — тёмно-моховая, на поверхности кувшинки с лилиями и прыгающие лягушки; у каждой — свой состав днём/ночью, своя вода (цвет, волны, рыбы в толще) и свой берег (Река — лесистая полоса, Озёра — холмы, Море — парусник и чайки, Океан — открытый горизонт, Болото — мшистый берег с камышом), чем дальше вода — тем доходнее улов; покупка и переключение во вкладке 🗺️ лавки — озёра море океан болото днём болото ночью -- **Аквариум 🐠** — декоративная сцена коллекции: с карточки улова кнопка «🐠 В аквариум» кладёт бесплатную копию экземпляра (монеты за улов платятся как обычно, модель C — экономика не тронута); старт 6 мест, расширение до 12 за монеты (1 500 → 100 000 🪙); жильцы — те же SVG-портреты с вариантной перекраской, что на карточке улова; тап по рыбе — вес и редкость, «Выпустить» освобождает слот (без монет). Вход — из хаба «Меню»; чистая косметика — аквариум +- **Аквариум 🐠** — декоративная сцена коллекции: с карточки улова кнопка «🐠 В аквариум» кладёт бесплатную копию экземпляра (монеты за улов платятся как обычно, модель C — экономика не тронута); старт 6 мест, расширение до 12 за монеты (1 500 → 100 000 🪙); жильцы — те же SVG-портреты с вариантной перекраской, что на карточке улова, и размером от абсолютного веса (та же шкала `fishSizeFromW`, что при вываживании; на десктопе жильцы вдвое крупнее); тап по рыбе — вес и редкость, «Выпустить» освобождает слот (без монет). Вход — из хаба «Меню»; чистая косметика — аквариум - Квесты дня (3 шт, генерация по дню), стрик входов со «щитом», 38 достижений с бонусами - Дневник рыбака — коллекция, максимальный улов по каждому виду, статистика - Магазин с вкладками: Лавка 🛒, Дневник 📔, Награды 🎯, Достижения 🏅, Скины 🎨, Локации 🗺️ diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index 42df4ad..0ada151 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -904,3 +904,40 @@ git add -A && git commit -m "что изменилось" && git push **Скриншоты:** перегенерированы `node tests/gen-screenshots.js` — кадры дневника/карточки/аквариума и локаций содержат новые портреты. **Доки / файлы:** 32 × `assets/fish/fish-*.svg`, `index.html` (только блок FISH-SVG), `tools/fish-preview.html`, этот DEVLOG. + +## Этап 48. Размер рыбы в аквариуме — от абсолютного веса (ревизия Этапа 46) + +**Задача:** владелец заметил, что размер жильцов банки считался от `w/wMax` + `rarity`, хотя в игре уже есть узловая таблица абсолютного веса `FISH_SIZE_KNOTS`/`fishSizeFromW()` (Этап 41, используется `drawHookedFish()`). Нужно переиспользовать её, чтобы «крупнее вес → крупнее рыба» работало в банке так же, как в воде. + +**Решение — `aqFishSize(w) = clamp(fishSizeFromW(w)·0.7·k, 7·k, min(W,H)·0.16·k)`, где `k=2` только для десктопа:** +- `aqFishSize` (`index.html`, секция АКВАРИУМ) больше не считает `rel=w/wMax` и не прибавляет `rarity` — только читает общую кривую воды. Формула: `s = fishSizeFromW(w)×AQ_FISH_SCALE×k`, кламп `[AQ_FISH_SIZE_MIN×k, min(W,H)×AQ_FISH_SIZE_REL×k]`. +- Новые константы: `AQ_FISH_SCALE=0.7` (масштаб к шкале воды), `AQ_FISH_SIZE_MIN=7` (мелкая рыба читаема/тапается), `AQ_FISH_SIZE_REL=0.16` (верхний кламп = 16% меньшей стороны экрана). +- **Ревизия (по просьбе владельца): десктоп ×2.** Мобильный вид одобрен владельцем как есть, поэтому множитель `k=AQ_DESKTOP_SCALE=2` включается только при `min(W,H)>=AQ_DESKTOP_MIN=600` (иначе `k=1`). Так десктоп 1280×720 и mobile-landscape 800×450 (min=450 < 600) остаются различными: банка на большом экране пустует сильнее, и жильцы там вдвое крупнее. Оба клампа (и `MIN`, и `REL`) масштабируются на `k` вместе с телом — иначе на десктопе `MIN`-рыба «отставала» бы от верхней границы, а `REL`-кламп резал бы только что увеличенных гигантов. +- Сигнатура `aqFishSize(sp,w)` → `aqFishSize(w)`: `sp` больше не нужен; обновлены 3 вызова — `aqBuildFishEl` (`f.w`), `drawAquarium` (`f.w`, `sp` остался для `sp.glow`), `aqTap` (`f.w`). Имя `aqFishSize` сохранено (глобал в тестах/AGENTS). +- Таблицу/кривую НЕ дублируем: `fishSizeFromW` объявлен в секции «ОТРИСОВКА» раньше секции аквариума — доступен как есть. + +**Размеры (DOM-портрет = `2·size × 1.25·size`).** На телефоне 390×844 мобильный кламп (cap 125 px) режет только 100-кг кит; на десктопе 1280×720 cap = 460 px — в диапазоне 0.1–100 кг кламп не срабатывает вовсе: + +| Вес | Моб. DOM | Десктоп DOM | +|---|---|---| +| 0.1–0.5 кг | 14×9 (MIN) | 28×18 (MIN×2) | +| 1 кг | 16×10 | 32×20 | +| 3 кг | 27×17 | 54×33 | +| 4.8 кг | 33×20 | 66×41 | +| 8 кг | 40×25 | 79×49 | +| 16 кг | 49×30 | 97×61 | +| 40 кг | 78×49 | 157×98 | +| 72 кг | 119×75 | 239×149 | +| 100 кг | **125×78 (clamp)** | 293×183 | + +Мобильный/ландшафт-800 не тронуты ревизией. Ранее (w/wMax+rarity) 100 кг кит давал 73 px длины, теперь на десктопе 292 px, на мобильном 125 px; мелочь 0.1–0.7 кг на мобильном сглаживается `MIN=7` до 14 px, на десктопе `MIN×2` — до 28 px. + +**Не тронуто:** `FISH_SIZE_KNOTS`/`fishSizeFromW`/`drawHookedFish` (вода), `wMin/wMax`, формат/миграция `save.aqFish`, баланс/роллы/физика, `drawFishShape` (в банке по-прежнему не используется). + +**Тесты:** не правились — размеры в §AQ никто не пинит (проверяются `.aqfish`, `use href`, варианты, `transform`; `aqTap` жмёт точно центр). Итого **222/222 PASS, exit 0**; `node tools/check.mjs` OK; `tools/balance.mjs` — пины без изменений (Река 65.1/125.9 · Озёра 194.1/249.0 · Болото 240.3/328.9 · Море 297.3/476.7 · Океан 329.1/552.7). + +**Скриншоты:** перегенерированы только кадры `aquarium` (оба набора, 6 PNG) — набор в целом не побитово-детерминирован (анимации/время), остальные кадры не тронуты (паттерн ревизии Этапа 47). На кадре `save.aqFish` = карась 0.6 кг, щука 7.4 кг, золотая 0.8 кг, светлячок 2.4 кг. + +**Доки:** AGENTS.md (пункт «Аквариум» — формула `aqFishSize(w)` + `AQ_FISH_SCALE`/`AQ_FISH_SIZE_MIN`/`AQ_FISH_SIZE_REL`), README.md (фича «Аквариум» — размер по весу), этот DEVLOG. + +**Файлы:** `index.html`, `AGENTS.md`, `README.md`, `docs/DEVLOG.md`, `screenshots-wiki*/{desk,mobl,mobp}-aquarium.png`. diff --git a/index.html b/index.html index ef90dd9..49a7c0f 100644 --- a/index.html +++ b/index.html @@ -4164,11 +4164,21 @@ function aqApplyVariant(el,variant){ for(const id of AQ_VARIANT_CLASS) el.classList.toggle('var'+id,variant.id===id); } -/* Размер силуэта в банке: от веса к пределу вида (w/wMax) и редкости; кламп — чтобы влезал */ -function aqFishSize(sp,w){ - const rel=sp.wMax>0?Math.min(1,Math.max(0,w/sp.wMax)):0.4; - const s=8+rel*22+sp.rarity*1.6; - return Math.max(7,Math.min(Math.min(W,H)*0.11,s)); +/* Размер силуэта в банке — от АБСОЛЮТНОГО веса, как у рыбы при вываживании + (drawHookedFish, Этап 41): fishSizeFromW(w) × AQ_FISH_SCALE, кламп + [AQ_FISH_SIZE_MIN, min(W,H)×AQ_FISH_SIZE_REL], чтобы мелочь была читаема, + а гигант не занял всю банку. Редкость/предел вида (wMax) больше не влияют. + ДЕСКТОП: на большом экране (min(W,H) >= AQ_DESKTOP_MIN) всё ×2 — мобильный + вид одобрен владельцем как есть, десктопный получает более крупных жильцов. */ +const AQ_FISH_SCALE=0.7; // масштаб к шкале воды (fishSizeFromW) +const AQ_FISH_SIZE_MIN=7; // нижний кламп (мелкая рыба читаема/тапается) +const AQ_FISH_SIZE_REL=0.16; // верхний кламп = 16% меньшей стороны экрана +const AQ_DESKTOP_MIN=600; // min(W,H) >= 600 px — экран считаем десктопным +const AQ_DESKTOP_SCALE=2; // десктоп: жильцы вдвое крупнее +function aqFishSize(w){ + const k=Math.min(W,H)>=AQ_DESKTOP_MIN?AQ_DESKTOP_SCALE:1; // только десктоп ×2 + const s=fishSizeFromW(w)*AQ_FISH_SCALE*k; + return Math.max(AQ_FISH_SIZE_MIN*k,Math.min(Math.min(W,H)*AQ_FISH_SIZE_REL*k,s)); } /* Позиция жильца в DOM: та же система координат, что у canvas (W/H — CSS-px), элемент центрируется на (x,y) и зеркалится по направлению. Вызывается каждый кадр из updateAquarium. */ @@ -4179,12 +4189,12 @@ function aqPlaceFish(el,rt){ } /* DOM-элемент жильца:
. Те же SVG-портреты и вариантное оформление, что на карточке улова (Э42 + ревизия). Размер — - aqFishSize (прежняя шкала): длина 2·size, высота по соотношению 160:100. */ + aqFishSize(w) от абсолютного веса: длина 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 size=aqFishSize(f.w); const el=document.createElement('div'); el.className='aqfish'; el.dataset.i=i; el.style.width=(size*2).toFixed(1)+'px'; @@ -4328,7 +4338,7 @@ function drawAquarium(){ const f=save.aqFish[i], rt=aqFishRt[i]; if(!f||!rt) continue; const sp=SPECIES.find(s=>s.id===f.id); if(!sp) continue; - const size=aqFishSize(sp,f.w); + const size=aqFishSize(f.w); if(sp.glow) drawGlowAura(rt.x,rt.y,size*1.5,sp.glow,glowIntensity()); // болотные — свечение вида if(aqSel===i){ // рамка выбранной рыбы ctx.strokeStyle='#c8fff4'; ctx.lineWidth=2; @@ -4355,7 +4365,7 @@ function aqTap(x,y){ const rt=aqFishRt[i], f=save.aqFish[i]; if(!f) continue; const sp=SPECIES.find(s=>s.id===f.id); if(!sp) continue; - const r=aqFishSize(sp,f.w)*1.2+10; + const r=aqFishSize(f.w)*1.2+10; const d=(rt.x-x)*(rt.x-x)+(rt.y-y)*(rt.y-y); if(d