Этап 45: локация «Волшебное болото» (id 4, 90 000 🪙) — светящиеся рыбы, лягушки, кувшинки/лилии. (1) Данные: LOCATIONS[4] (тёмно-моховая вода 6 hex, слабая рябь; в массиве ПОСЛЕДНИЙ, по цене — между Озёрами 65536 и Морем 131072); 5 новых видов строго в КОНЕЦ SPECIES (32→37, по одному на редкость r0–r4, у каждого glow:'#hex' — 5 разных цветов): glowfin Светлячок #5ff2ff 8🪙, moonfish Лунная рыбка #b9c8ff 13, emberfin Изумрудный светляк #57ffa8 20, violetlure Фиалковый огонёк #d17bff 38, spiritfin Дух болота #ffe07a 70; SWAMP_IDS/SWAMP_DAY_WEIGHTS/SWAMP_NIGHT_WEIGHTS/SWAMP_SPECIES(_NIGHT) — паттерн LAKE/SEA/OCEAN без пересечений; NIGHT_WEIGHTS — ключи всем 37 id. (2) pickSpecies(): вложенный тернарник → табличный доступ SPECIES_BY_LOCATION(_NIGHT) по save.location + guard (вне 0..4 → Река); роллы 0..3 бит-в-идентичны (lc3/lc4/lc4b/lc4c + речные §9j/M4/M6 не тронуты). (3) Свечение (только ночь): nightMix()>0.35 — тот же порог, что у фонаря; drawGlowAura() (радиальный градиент + 'lighter', save/restore, кэш GLOW_RGBA) — фоновые рыбы болота (AMBIENT_FISH[4] — 7 рыб + palette из 4 цветов, цвет стабильно по индексу), зацепившаяся/вываживаемая (fishGlow — вид с glow), боссовая золотая аура сохранена; карточка — класс glow-card + --glow (только normal-вариант, drop-shadow, не конфликтует с фильтрами вариантов), дневник — drop-shadow на SVG. (4) Декорации: initScenery() ветка i===4 (bank+reed+snag+pine в болотной гамме), ГЛОБАЛЬНЫЕ pads[] (8–14 кувшинок с вырезом, 45% с лилиями — жёлтые/белые/розовые) и frogs[] (3–5, sit→jump по параболе на соседнюю кувшинку, 3–7 с) — очищаются/пересобираются в initScenery() (resize + обе ветки shopClick); updateDecor(dt) — покачивание/прыжки; drawSwampDecor() — ОТДЕЛЬНЫЙ слой после drawWater/drawAmbientFish и до drawBoat (дusk берега не гасит; локальное притухание ночью). (5) Босс: BOSS_BY_LOCATION[4]='spiritfin' (Дух болота, 20–50 кг, ×5 — паттерн Этапа 37). (6) Магазин/i18n/ачивки: тост welcome_swamp (ru/en, тернарник → if/else), ачивки 34→36: swamp_first (bonus:{coinPct:0.02}, locations-based) + swamp_boss (bonus:{}, bosses.spiritfin) → пин achBonus().coinPct 0.16→0.18; species_8 = все 37. (7) Баланс: SWAMP_DAY_WEIGHTS {glowfin:3,moonfish:4,emberfin:28,violetlure:13,spiritfin:4.5}, SWAMP_NIGHT_WEIGHTS {glowfin:2.5,moonfish:2.5,emberfin:20,violetlure:30,spiritfin:4} — Болото 240.3/328.9 🪙 (ночь/день ×1.37), порядок по цене Озёра 194.1/249.0 < Болото < Море 297.3/476.7 в ОБЕ фазы; пины остальных вод не изменились (Река 65.1/125.9 · Озёра 194.1/249.0 · Море 297.3/476.7 · Океан 329.1/552.7). (8) tools/balance.mjs: 8→10 таблиц, +SWAMP_SPECIES(_NIGHT) в return, сводка/пары отношений — сортировка ПО ЦЕНЕ (болото встает между Озёрами и Морем), пины в шапке. (9) Портреты: 5 flat-SVG (каждый — отдельным художником) assets/fish/fish-{glowfin,moonfish,emberfin,violetlure,spiritfin}.svg (viewBox 0 0 160 100, var(--f1..f4,hsl-fallback)); embed-fish.mjs IDS 32→37 (в конец), 37 <symbol> в index.html; fish-preview.html 32→37 карточек. (10) Тесты (тот же коммит): lc1 (37/5 локаций/90000/names/sprites 37/oceanOrder slice(23,32)/swampOrder/swampGlow/swampRar), lc2 (+SWAMP-таблицы, пересечения, nightW 37), lc4d (новый: 3000+3000 роллов болота — все 5 ≥1, чужих нет), lc5 (+location:4 не сбрасывается, location:5→0), lc8w (новый: покупка 90000 + welcome_swamp), lc9 (37), lc10 (mid29), lc11 (новый: glow #rrggbb, 5 цветов различны, веса покрывают id, NIGHT_WEIGHTS — все 37), a22d (36), a36loc (+swamp_first), пин 0.18, §9k2 (+{loc:4, spiritfin, swamp_boss}). 198→204 PASS, exit 0. (11) Скриншоты: gen-screenshots.js +кадры swamp/swamp-night (12→14 × 3 вьюпорта, dayTime 0.3/0.85) — 42 RU + 42 EN PNG (screenshots-wiki/, screenshots-wiki-en/). (12) Доки: AGENTS.md (локации/босс/баланс/баланс-скрипт/награды/портреты/скриншоты), README (5 локаций, 37 видов, 36 ачивок, 204 теста, 14 кадров, mobp-swamp*), KODA.md (SWAMP_*, SPECIES_BY_LOCATION, drawGlowAura/fishGlow/GLOW_RGBA, drawSwampDecor, pads/frogs), DEVLOG (Этап 45), PLAN-SWAMP.md (статус «реализован»). Проверено: 204/204 PASS exit 0, check.mjs OK, balance.mjs — пины без изменений + Болото в цели, emoji-font --check OK (52 глифа, новых emoji нет), ревью @oracle — критичных замечаний нет (.gitignore + .opencode/, fish-preview, осколок комментария species_8 — закрыты в этом же коммите). Миграция сейва не тронута (граница из LOCATIONS.length; location=4 покрыт lc5(f)).

This commit is contained in:
2026-09-21 22:59:15 +03:00
parent 3e9c5b7b45
commit 7f545347b4
101 changed files with 1142 additions and 119 deletions
+1
View File
@@ -7,6 +7,7 @@ tools/fonts-src/
tools/.venv/
# Служебное
.opencode/
.po-handoff.md
*.log
.DS_Store
+8 -8
View File
File diff suppressed because one or more lines are too long
+20 -20
View File
@@ -41,16 +41,16 @@
| Секция | О чём |
|---|---|
| `ДАННЫЕ` | `SPECIES`, `SPECIES_NIGHT`, `LOCATIONS`, `RIVER_*` / `LAKE_*` / `SEA_*` / `OCEAN_*`, `BOSS_BY_LOCATION` (босс каждой локации, этап 37), `VARIANTS`, `WEATHER`, `RODS`, `LURES`, апгрейды, скины. **Здесь крутится баланс** |
| `ДАННЫЕ` | `SPECIES`, `SPECIES_NIGHT`, `LOCATIONS` (5 локаций, этап 45 — +Болото), `RIVER_*` / `LAKE_*` / `SEA_*` / `OCEAN_*` / `SWAMP_*` + табличный `SPECIES_BY_LOCATION(_NIGHT)`, `BOSS_BY_LOCATION` (босс каждой локации, этап 37; +Болото, этап 45), `VARIANTS`, `WEATHER`, `RODS`, `LURES`, апгрейды, скины. **Здесь крутится баланс** |
| `СОХРАНЕНИЕ` | `save`, `freshSave()`, `loadSave()` (со всеми миграциями), `persist()` |
| `M3: ЦИКЛ ДНЯ/НОЧИ` | `DAY_LENGTH`, `dayTime`, `dayPhase`, `setDayTime()` |
| `M8: ПОГОДА` | `WEATHER`, таймеры, `weatherActive()`, `updateWeather()` |
| `M2: НАГРАДЫ` | квесты дня (`QUEST_TPLS`), стрик (`STREAK_REWARDS`), 34 достижения (`ACHIEVEMENTS`), `achBonus()` |
| `M2: НАГРАДЫ` | квесты дня (`QUEST_TPLS`), стрик (`STREAK_REWARDS`), 36 достижений (`ACHIEVEMENTS`), `achBonus()` |
| `ХОЛСТ И РАЗМЕРЫ` | `resize()`, DPR, layout (`waterY`, `boatX`) |
| `ЗВУК` | `ac()`, `beep()`, `noiseBurst()`, `sfx()`, `vib()` |
| `СОСТОЯНИЕ ИГРЫ` | все мутабельные глобалы геймплея |
| `ЛОГИКА` | `pickSpecies()`, `biteChance()`, `rollWeight()`, `hook()`, `fishForce()`, `updateReel()`, `catchFish()`, `updateGame()` |
| `ОТРИСОВКА` | `UI_FONT`, `drawSky/Water/Boat/Bobber/Meters/Weather()`, `draw()` |
| `ОТРИСОВКА` | `UI_FONT`, `drawSky/Water/Scenery/AmbientFish/Boat/Bobber/Meters/Weather()`, свечение Болота (`drawGlowAura()`/`fishGlow()`/`GLOW_RGBA`), декор `drawSwampDecor()` (`pads`/`frogs`), `draw()` |
| `Y1: SDK Яндекс Игр` | обёртка `YG`, реклама (`tryInterstitial()`, `#btnRv`), пауза платформы |
| `Y5: i18n RU/EN` | таблица `T`, `t()`/`tt()`, `setLang()`, `resolveLang()`, `renderMenu()` |
| `ЦИКЛ` | `frame()` — requestAnimationFrame, дельта-тайм, пауза |
@@ -60,11 +60,11 @@
### Контент игры (кратко)
- **32 вида рыб** (0.05–100 кг, 5 уровней редкости, цена 4–1500 🪙/кг; этап 38 — лестница веса: Озёра ×8, Море ×6, Океан ×4, кит ×2; этап 39 — цены Озёр/Моря/Океана урезаны), четыре локации со СВОЕЙ ихтиофауной (без пересечений, этапы 35/36): Река — 8 базовых (бесплатно), Озёра — 10 озёрных (65 536 🪙; zander/chuch/crayfish этап 5, bleak/roundel/eelpout/tench/navaga этап 33, rudd/cisco этап 35), Море — 5 морских (131 072 🪙 = ×2 Озёр; herring/mackerel/flounder/tuna/swordfish этап 35), Океан — 9 глубоководных (196 608 🪙 = ×1.5 Моря; moray/grouper/turtle/angler/octopus/manta/shark/squid/whale этап 36).
- **37 видов рыб** (0.05–100 кг, 5 уровней редкости, цена 4–1500 🪙/кг; этап 38 — лестница веса: Озёра ×8, Море ×6, Океан ×4, кит ×2; этап 39 — цены Озёр/Моря/Океана урезаны), пять локаций со СВОЕЙ ихтиофауной (без пересечений, этапы 35/36/45; по цене воды: Река → Озёра → Болото → Море → Океан): Река — 8 базовых (бесплатно), Озёра — 10 озёрных (65 536 🪙; zander/chuch/crayfish этап 5, bleak/roundel/eelpout/tench/navaga этап 33, rudd/cisco этап 35), Море — 5 морских (131 072 🪙 = ×2 Озёр; herring/mackerel/flounder/tuna/swordfish этап 35), Океан — 9 глубоководных (196 608 🪙 = ×1.5 Моря; moray/grouper/turtle/angler/octopus/manta/shark/squid/whale этап 36), Волшебное болото — 5 светящихся (90 000 🪙; glowfin/moonfish/emberfin/violetlure/spiritfin этап 45, ночью светятся разными цветами).
- **8 вариантов рыбы**: обычная, медная ×1.5, серебряная ×2, кварцевая ×2.5, золотая ×3, изумрудная ×4, алмазная ×4.5, радужная ×5 (топ). P(особой) ≈ 31%, шанс усиливают наживка и дождь.
- **4 удочки и 4 приманки**, каждая с апгрейдами до 4 уровня (фонарик для ночи, леска, тормоз, катушка, клёв, редкие/крупные).
- **Цикл суток ~5 минут**, погода (дождь 🌧️ ×3 к особым, туман 🌫️ ×1.5 к клёву), **босс в каждой локации** (Река — осётр, Озёра — гигантский рак, Море — рыба-меч, Океан — синий кит; босс весит 20–50 кг) в 3 фазы (награда ×5).
- **Награды**: 3 квеста дня (детерминированы от даты), стрик входов до 7 дней со «щитом», 34 достижения с постоянными бонусами.
- **Цикл суток ~5 минут**, погода (дождь 🌧️ ×3 к особым, туман 🌫️ ×1.5 к клёву), **босс в каждой локации** (Река — осётр, Озёра — гигантский рак, Море — рыба-меч, Океан — синий кит, Болото — дух болота; босс весит 20–50 кг) в 3 фазы (награда ×5).
- **Награды**: 3 квеста дня (детерминированы от даты), стрик входов до 7 дней со «щитом», 36 достижений с постоянными бонусами.
- **Лавка** с вкладками: Лавка 🛒, Дневник 📔, Награды 🎯, Достижения 🏅, Скины 🎨, Локации 🗺️. Плюс хаб «Меню» `#gamemenu` из паузы.
- **Монетизация**: interstitial (2 мин сессии, кулдаун 3 мин) и rewarded «×2 за улов».
- **Локализация RU/EN** через `ysdk.environment.i18n.lang`.
@@ -100,7 +100,7 @@ node tools/check.mjs # блок-за-блоком прогоняет J
```bash
cd tests
npm install # ставит puppeteer-core
npm test # = node test.js → ждать 193/193 PASS и exit 0
npm test # = node test.js → ждать 204/204 PASS и exit 0
```
- Браузер берётся из `process.env.CHROMIUM || '/usr/bin/chromium-browser'` — при необходимости поправьте путь.
@@ -112,8 +112,8 @@ npm test # = node test.js → ждать 193/193 PASS и
```bash
node tests/gen-screenshots.js [каталог-сборки] [папка-вывода] [язык]
# 10 кадров × 3 вьюпорта (десктоп 1280×720, моб. портрет 360×640@2x,
# моб. ландшафт 800×450@1.6x) = 30 PNG, 24-бит без альфа.
# 14 кадров × 3 вьюпорта (десктоп 1280×720, моб. портрет 360×640@2x,
# моб. ландшафт 800×450@1.6x) = 42 PNG, 24-бит без альфа.
# Без аргументов → screenshots-wiki/ (RU); с 'en' → screenshots-wiki-en/.
# README ссылается на screenshots-wiki/mobp-* (мобильный портрет).
@@ -166,7 +166,7 @@ node tests/pack-smoke.js # прогон игры из распакованно
```bash
# 1. поправить index.html
node tools/check.mjs # 2. синтаксис
cd tests && npm install && npm test # 3. главный рубеж: ждать 193/193 PASS, exit 0
cd tests && npm install && npm test # 3. главный рубеж: ждать 204/204 PASS, exit 0
node tools/emoji-font.py # (если добавляли emoji) + --check
node tests/gen-screenshots.js # 4. переснять скриншоты, если менялась картинка
node tools/pack.mjs && node tests/pack-smoke.js # 5. перед публикацией
@@ -177,9 +177,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)`, `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`.
- Тесты гоняют игру через внутренние глобалы в `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`.
**Переименование любого из них ломает тесты молча** — имена держать, либо обновлять тест в том же коммите.
- Числа проверок зафиксированы в README (`193/193` в `tests/test.js`, `76/76` в Orca, `6/6` в pack-smoke) — при добавлении проверок обновлять доки в том же коммите.
- Числа проверок зафиксированы в README (`204/204` в `tests/test.js`, `76/76` в Orca, `6/6` в pack-smoke) — при добавлении проверок обновлять доки в том же коммите.
- Изменение физики вываживания требует **Monte-Carlo-проверки точными формулами** (см. `docs/PHYSICS-RESEARCH.md`, §4.7) и синхронной правки тестов.
- Пины шансов в тестах снимаются на **нейтральной снасти** (снапшот/рестор `save.lure`/`lureLv` внутри eval).
- В браузере Orca (живой плейтест) нельзя использовать фиксированные `sleep` для ожидания состояний — rAF троттлится; только polling с таймаутом.
@@ -189,9 +189,9 @@ git add -A && git commit -m "…" && git push
- Ключ `localStorage` — `fishing-save-v1`, **не менять**.
- Любое изменение формата сейва → новая миграция в `loadSave()`. **Старые сейвы обязаны оставаться читаемыми**; новые счётчики желательно читать через `||0` (тогда миграция не нужна).
- Не менять `id` существующих записей (`SPECIES`, `ACHIEVEMENTS`, `VARIANTS`, `QUEST_TPLS`) — на них завязаны старые сейвы и пины тестов.
- Река (`RIVER_SPECIES(_NIGHT)`) — **только 8 базовых видов**: новые виды добавлять лишь в озёрные таблицы, иначе сломаются роллы и пины тестов.
- **Новые виды — только в КОНЕЦ `SPECIES`** (после `whale`): `SPECIES[0..7]` запинены тестами и квестами, вставка в середину ломает их молча. Новому id обязательны ключи в `NIGHT_WEIGHTS` и в дневной/ночной таблице весов его локации (`*_DAY_WEIGHTS`/`*_NIGHT_WEIGHTS`); тест lc4 (3000 роллов локации) вызывает `pickSpecies()` без аргумента — штраф `minDist` не действует, так что для гарантии выпадения нужен вес ≥ ~2.2. Новый emoji → перегенерация субсета `tools/emoji-font.py`.
- **Баланс «стало дороже» правится сдвигом долей, а не масштабом.** `pickSpecies()` нормирует веса → ожидаемая выручка за улов (Σ(w·V)/Σw, V = середина веса × цена) не меняется от умножения всех весов на константу. Пины при нейтральной снасти: Река 65.1/125.9 · Озёра 121.2/134.1 · Море 190.8/222.0 · Океан 539.5/671.3 🪙 (день/ночь) — считать через `tools/balance.mjs`; при правке `*_WEIGHTS` держать инвариант «новая вода доходнее предыдущей в ОБЕ фазы» и мин. вес вида ≥ ~2.2 (иначе lc4 начинает плавать).
- Река (`RIVER_SPECIES(_NIGHT)`) — **только 8 базовых видов**: новые виды добавлять лишь в таблицы СВОЕЙ воды (`*_IDS` + `*_DAY_WEIGHTS`/`*_NIGHT_WEIGHTS`, как Болото этапа 45), иначе сломаются роллы и пины тестов.
- **Новые виды — только в КОНЕЦ `SPECIES`** (после `spiritfin`, этап 45): `SPECIES[0..7]` запинены тестами и квестами, вставка в середину ломает их молча. Новому id обязательны ключи в `NIGHT_WEIGHTS` и в дневной/ночной таблице весов его локации (`*_DAY_WEIGHTS`/`*_NIGHT_WEIGHTS`); тест lc4 (3000 роллов локации) вызывает `pickSpecies()` без аргумента — штраф `minDist` не действует, так что для гарантии выпадения нужен вес ≥ ~2.2. Новая локация → запись в `LOCATIONS`, табличный `SPECIES_BY_LOCATION(_NIGHT)` (порядок = `LOCATIONS`) и guard «вне 0..4 → Река» в `pickSpecies()` (этап 45). Новый emoji → перегенерация субсета `tools/emoji-font.py`.
- **Баланс «стало дороже» правится сдвигом долей, а не масштабом.** `pickSpecies()` нормирует веса → ожидаемая выручка за улов (Σ(w·V)/Σw, V = середина веса × цена) не меняется от умножения всех весов на константу. Пины при нейтральной снасти: Река 65.1/125.9 · Озёра 194.1/249.0 · Болото 240.3/328.9 · Море 297.3/476.7 · Океан 329.1/552.7 🪙 (день/ночь) — считать через `tools/balance.mjs` (10 таблиц, сводка по ЦЕНЕ воды: Река → Озёра → Болото → Море → Океан); при правке `*_WEIGHTS` держать инвариант «новая (по цене) вода доходнее предыдущей в ОБЕ фазы» и мин. вес вида ≥ ~2.2 (иначе lc4 начинает плавать).
### Известные «грабли»
@@ -217,11 +217,11 @@ git add -A && git commit -m "…" && git push
| `index.html` | Вся игра: HTML + CSS + JS + встроенный emoji-шрифт |
| `README.md` | Описание игры для игрока/читателя, «как играть», команды запуска и публикации |
| `AGENTS.md` | Инструкция для агентов: порядок проверок, подводные камни, «где крутить» баланс |
| `docs/DEVLOG.md` | Журнал разработки: этапы 1–36, найденные баги, решения, Monte-Carlo |
| `docs/DEVLOG.md` | Журнал разработки: этапы 1–45, найденные баги, решения, Monte-Carlo |
| `docs/BROWSER-TESTING.md` | Плейтест через Orca CLI: таблица шагов, 13 подводных камней, справочник CLI |
| `docs/PHYSICS-RESEARCH.md` | Исследование физики вываживания и формулы баланса |
| `tests/test.js` | Главный автотест (headless Chromium + puppeteer-core), 193 проверки |
| `tests/gen-screenshots.js` | Генератор скриншотов wiki: 10 кадров × 3 вьюпорта, RU/EN |
| `tests/test.js` | Главный автотест (headless Chromium + puppeteer-core), 204 проверки |
| `tests/gen-screenshots.js` | Генератор скриншотов wiki: 14 кадров × 3 вьюпорта, RU/EN |
| `tests/orca-playtest.js` | Плейтест в живом браузере Orca через CLI, 76 проверок |
| `tests/pack-smoke.js` | Smoke-прогон игры из распакованного архива, 6 проверок |
| `tests/package.json` | Зависимость `puppeteer-core`; скрипты `test`, `shots` |
@@ -240,10 +240,10 @@ git add -A && git commit -m "…" && git push
| Баланс рыбы/удилищ/приманок | `SPECIES`, `RODS`, `LURES`, цены апгрейдов `upgCost()` / `UPG_MULT` |
| Шансы особых рыб | `VARIANTS`, `variantChance()`, `variantBoost()`, `SHINY_CHANCE` |
| Сложность и физика боя | `updateReel()`, `fishForce()`, константы внутри `hook()` (`d0`, `st0`, drag) |
| Ночь и фонарик | `DAY_LENGTH`, `NIGHT_WEIGHTS` / `LAKE_NIGHT_WEIGHTS`, `hasLight()`, ветка `WAIT` |
| Ночь и фонарик | `DAY_LENGTH`, `NIGHT_WEIGHTS` / `LAKE_NIGHT_WEIGHTS` / `SWAMP_NIGHT_WEIGHTS`, `hasLight()`, ветка `WAIT` |
| Погода | `WEATHER`, `updateWeather()`, `weatherActive()` |
| Квесты / стрик / достижения | `QUEST_TPLS`, `STREAK_REWARDS`, `ACHIEVEMENTS`, `achBonus()` |
| Скины и локации | `BOAT_SKINS`, `BOBBER_SKINS`, `LOCATIONS`, `LAKE_*` / `RIVER_*` |
| Скины и локации | `BOAT_SKINS`, `BOBBER_SKINS`, `LOCATIONS`, `RIVER_*` / `LAKE_*` / `SEA_*` / `OCEAN_*` / `SWAMP_*`, свечение/декор Болота (`glow`, `drawGlowAura()`, `drawSwampDecor()`) |
| Тексты интерфейса | таблица `T = {ru, en}`, `t()` / `tt()`, `renderMenu()` |
| SDK и реклама | секция `Y1: SDK Яндекс Игр`, `tryInterstitial()`, `YG.showRewarded` |
| Миграции сейвов | `loadSave()` |
+9 -9
View File
@@ -16,25 +16,25 @@
4. **У каждой рыбы свой вес**, продажа — по цене за кг своего вида (от 4 до 1500 🪙/кг). Крупная рыба = большие деньги.
5. **У удочек есть предел веса** (3–40 кг): зацепишь более тяжёлую — шнур лопнет почти мгновенно. Чем лучше удочка и приманка, тем выше шанс зацепить крупную. Новый максимум по виду отмечен бейджем «🏆 РЕКОРД!» на карточке улова.
6. На монеты — новые удилища и приманки в лавке 🛒 (кнопка 🛒 в HUD или клик по счётчику монет).
7. **Награды 🎯**: три квеста дня (уловы, монеты, забросы) с наградой за выполнение; стрик входов — ежедневная награда растёт до дня 7, «щит» прощает один пропуск в неделю; 34 достижения 🏅 с постоянными бонусами (+% к монетам, окно подсечки, нагрузка шнура).
7. **Награды 🎯**: три квеста дня (уловы, монеты, забросы) с наградой за выполнение; стрик входов — ежедневная награда растёт до дня 7, «щит» прощает один пропуск в неделю; 36 достижений 🏅 с постоянными бонусами (+% к монетам, окно подсечки, нагрузка шнура).
8. **День/ночь**: полный цикл суток ~5 минут (утро → день → вечер → ночь). Ночью свой состав рыб — сом, щука и окунь активнее, но без 🪯 фонарика клёва не будет — фонарик дают апгрейды удилищ (1-й апгрейд любого удилища).
9. **Босс в каждой локации**: в каждой воде живёт свой босс — Река: осётр, Озёра: гигантский рак, Море: рыба-меч, Океан: синий кит (шанс при поимке соответствующей рыбы). Босс весит 20–50 кг. Бой идёт в 3 фазы — босс «рвёт и уходит», и каждая следующая фаза глубже; удерживай, чтобы втащить. Награда ×5 к цене.
9. **Босс в каждой локации**: в каждой воде живёт свой босс — Река: осётр, Озёра: гигантский рак, Море: рыба-меч, Океан: синий кит, Болото: дух болота (шанс при поимке соответствующей рыбы). Босс весит 20–50 кг. Бой идёт в 3 фазы — босс «рвёт и уходит», и каждая следующая фаза глубже; удерживай, чтобы втащить. Награда ×5 к цене.
10. **Симулятор боёв (релиз 5)**: у рыбы есть выносливость — тонкая фиолетовая полоска под дистанцией: свежая рыба тянет долго и сильно, а уставшая (💤) вянет — втягивай её именно тогда. У катушек — тормоз (drag): рывок сильнее тормоза — леска скользит. Иногда поплавок не берёт вовсе — пустой клёв, подожди ещё немного.
## Возможности
- 32 вида рыб с собственным весом (0.05–50 кг) и ценой за кг (5 уровней редкости), у каждой локации — только свои рыбы: Река — 8 базовых, Озёра — 10 озёрных (судак, хариус, сиг, линь, навага, гигантский рак и другие), Море — 5 морских (скумбрия, камбала, тунец, рыба-меч и другие), Океан — 9 глубоководных (синий кит, гигантский кальмар, скат-манта, гигантский осьминог и другие)
- 37 видов рыб с собственным весом (0.05–100 кг) и ценой за кг (5 уровней редкости), у каждой локации — только свои рыбы: Река — 8 базовых, Озёра — 10 озёрных (судак, хариус, сиг, линь, навага, гигантский рак и другие), Море — 5 морских (скумбрия, камбала, тунец, рыба-меч и другие), Океан — 9 глубоководных (синий кит, гигантский кальмар, скат-манта, гигантский осьминог и другие), Волшебное болото — 5 светящихся (светлячок, лунная рыбка, изумрудный светляк, фиалковый огонёк, дух болота) — ночью светятся разными цветами
- Удилища: предел веса, прочность шнура, скорость подсечки, бонус на крупную рыбу
- Приманки: частота клёва, дальность броска, шанс редких и крупных
- Физика боя: выносливость рыбы (уставшая 💤 вянет и втягивается быстрее), тормоз катушки (drag) — сильнейший рывок проскальзывает, пустые клёвы
- Цикл дня и ночи (~5 мин на сутки): ночью своя таблица рыб (сом/щука/окунь активнее), ночью нужен 🪯 фонарик — апгрейд 1 удилища
- Апгрейды снастей: удилища и приманки улучшаются до 4 уровня (свет фонарика, крепость лески, тормоз, катушка, клёв, редкие и крупные рыбы); следующая снасть в лавке открывается после 4 уровня предыдущей
- Босс-рыба в каждой локации (Река — осётр, Озёра — гигантский рак, Море — рыба-меч, Океан — синий кит; шанс при поимке соответствующей рыбы, босс весит 20–50 кг): 3 фазы «рвёт и уходит», награда ×5, достижение «Босс-рыбак» + пер-босс достижения
- Босс-рыба в каждой локации (Река — осётр, Озёра — гигантский рак, Море — рыба-меч, Океан — синий кит, Болото — дух болота; шанс при поимке соответствующей рыбы, босс весит 20–50 кг): 3 фазы «рвёт и уходит», награда ×5, достижение «Босс-рыбак» + пер-босс достижения
- Варианты рыб (особая рыба ≈31%): медная ×1.5, серебряная ×2, кварцевая ×2.5, золотая ×3, изумрудная ×4, алмазная ×4.5, радужная ×5 (топ, самая редкая) — бейдж и оформление карточки улова; снасть усиливает шанс (тип наживки и её уровень — редким вариантам прибавка больше, топ-снасть ≈×5 к шансу радужной)
- Погода: дождь (🌧️) — шанс особых рыб ×3; туман (🌫️) — клёв ×1,5; событие идёт 120 с, следующее — раз в 2–4 минуты — <img src="screenshots-wiki/mobp-weather-rain.png" width="200" alt="дождь">
- Скины лодки и поплавка 🎨 — вкладка «Скины» в лавке: лодка «Красная лодка» 800 🪙 / «Лодка-домик» 2000 🪙, поплавок «Красный» 500 🪙 / «Неоновый» 1500 🪙 — чистая косметика (без эффектов на геймплей)
- Локации: Озёра (65 536 🪙) — судак, хариус, гигантский рак; Море (131 072 🪙) — скумбрия, тунец, рыба-меч; Океан (196 608 🪙 = ×1.5 Моря) — глубоководные гиганты: синий кит, гигантский кальмар, скат-манта, гигантский осьминог; у каждой — свой состав днём/ночью, своя вода (цвет, волны, рыбы в толще) и свой берег (Река — лесистая полоса, Озёра — холмы, Море — парусник и чайки, Океан — открытый горизонт), чем дальше вода — тем доходнее улов; покупка и переключение во вкладке 🗺️ лавки — <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="океан">
- Квесты дня (3 шт, генерация по дню), стрик входов со «щитом», 34 достижения с бонусами
- Локации: Озёра (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="болото ночью">
- Квесты дня (3 шт, генерация по дню), стрик входов со «щитом», 36 достижений с бонусами
- Дневник рыбака — коллекция, максимальный улов по каждому виду, статистика
- Магазин с вкладками: Лавка 🛒, Дневник 📔, Награды 🎯, Достижения 🏅, Скины 🎨, Локации 🗺️
- Прогресс и коллекция сохраняются в `localStorage` (старые сейвы мигрируются автоматически)
@@ -71,16 +71,16 @@ SDK подхватывается автоматически на площадк
## Тесты
Автотест прогоняет весь геймплей (бросок, подсечка, все исходы вываживания, магазин, дневник, сохранение, варианты рыб (медь/серебро/кварц/золото/изумруд/алмаз/радуга), near-miss, миграция сейва, квесты, стрик, достижения, день/ночь с фонариком, босс-рыба в 3 фазы, SDK-стаб (YG), реклама: RV-бонус и гейты interstitial, i18n (RU/EN), пауза (кнопка + blur/focus окна)) в headless Chromium:
Автотест прогоняет весь геймплей (бросок, подсечка, все исходы вываживания, магазин, дневник, сохранение, варианты рыб (медь/серебро/кварц/золото/изумруд/алмаз/радуга), near-miss, миграция сейва, квесты, стрик, достижения, день/ночь с фонариком, босс-рыба в 3 фазы, локации с эксклюзивной ихтиофауной и Болото со свечением/весами, SDK-стаб (YG), реклама: RV-бонус и гейты interstitial, i18n (RU/EN), пауза (кнопка + blur/focus окна)) в headless Chromium:
```bash
cd tests
npm install # ставит puppeteer-core
node test.js # 193 проверки
node test.js # 204 проверки
```
Нужен установленный Chromium (`executablePath` в `test.js` при необходимости поправить).
Есть и браузерный плейтест «вживую» во встроенном браузере Orca: `node tests/orca-playtest.js` (из каталога репо, требует запущенный Orca app) — полный геймплей, проверка консоли и pageerror; детали в [docs/BROWSER-TESTING.md](docs/BROWSER-TESTING.md).
Скриншоты состояний — `tests/gen-screenshots.js` (headless, 12 кадров × 3 вьюпорта: десктоп 1280×720, мобильный портрет 360×640@2x, мобильный ландшафт 800×450@1.6x, 24-битный PNG без альфа). Без аргументов — русские скриншоты в `screenshots-wiki/`; с аргументом `en` — английские в `screenshots-wiki-en/`. README ссылается на `screenshots-wiki/mobp-*` (мобильный портрет).
Скриншоты состояний — `tests/gen-screenshots.js` (headless, 14 кадров × 3 вьюпорта: десктоп 1280×720, мобильный портрет 360×640@2x, мобильный ландшафт 800×450@1.6x, 24-битный PNG без альфа). Без аргументов — русские скриншоты в `screenshots-wiki/`; с аргументом `en` — английские в `screenshots-wiki-en/`. README ссылается на `screenshots-wiki/mobp-*` (мобильный портрет).
+52
View File
@@ -0,0 +1,52 @@
<!-- Изумрудный светляк (emberfin): плотное тело в 3/4, изумрудная чешуя со светящимися искрами (#57ffa8), хвост — рваное пламя с горящей сердцевиной -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 100" width="160" height="100">
<!-- хвостовой плавник: три рваных языка пламени с вырезами посередине -->
<path d="M47,38 C41,29 34,20 27,12 C23,7 17,3 9,1 C16,8 18,14 16,19 L23,21 L14,26 C17,31 23,35 31,39 C37,42 42,41 47,42 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<path d="M47,43 C38,39 28,35 18,32 C13,30 8,29 1,31 C8,34 10,38 9,42 L16,44 L8,48 C12,52 18,53 27,52 C35,51 42,48 47,48 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<path d="M47,54 C42,61 36,68 29,75 C25,79 20,83 12,89 C18,80 19,75 18,70 L25,68 L17,63 C20,58 25,55 33,53 C39,52 43,54 47,54 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<!-- горящая сердцевина пламени: акцентная изумрудная вспышка -->
<path d="M45,41 C39,34 33,27 27,22 C23,19 18,17 13,16 C18,21 19,25 17,29 L22,30 L16,34 C20,38 26,41 33,42 C39,42 42,41 45,41 Z" fill="#57ffa8" fill-opacity="0.8"/>
<path d="M45,46 C37,44 29,41 21,39 C17,38 13,38 8,39 C13,41 14,44 13,47 L18,48 L13,51 C17,53 22,53 29,51 C35,49 41,47 45,47 Z" fill="#57ffa8" fill-opacity="0.6"/>
<!-- тело: плотное, смотрит вправо -->
<path d="M40,50 C56,24 78,20 104,22 C130,25 143,36 148,50 C143,64 130,75 104,78 C78,80 56,76 40,50 Z" fill="var(--f1,hsl(150,52%,40%))"/>
<!-- брюхо: светлее, блик вдоль нижней кромки -->
<path d="M148,50 C143,64 130,75 104,78 C78,80 56,76 40,50 C56,66 82,70 108,68 C130,66 141,60 148,50 Z" fill="var(--f2,hsl(150,45%,64%))"/>
<!-- спинной плавник -->
<path d="M78,26 C88,12 100,8 116,22 C100,23 88,25 82,30 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<!-- грудной плавник -->
<path d="M104,56 C118,60 124,66 120,78 C112,72 104,66 100,60 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<!-- брюшной плавник -->
<path d="M84,76 C92,86 100,89 108,84 C100,81 94,78 90,73 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<!-- жаберная дуга -->
<path d="M120,32 C114,42 114,50 120,58" fill="none" stroke="var(--f4,hsl(150,60%,19%))" stroke-width="1.8" stroke-linecap="round"/>
<!-- искры по телу: свечение + ядро -->
<circle cx="70" cy="33" r="4" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="70" cy="33" r="2.2" fill="#57ffa8"/>
<circle cx="92" cy="29" r="3.4" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="92" cy="29" r="1.8" fill="#57ffa8"/>
<circle cx="110" cy="40" r="4.6" fill="#57ffa8" fill-opacity="0.25"/>
<circle cx="110" cy="40" r="2.6" fill="#57ffa8"/>
<circle cx="82" cy="50" r="3.6" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="82" cy="50" r="2" fill="#57ffa8"/>
<circle cx="122" cy="57" r="4" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="122" cy="57" r="2.2" fill="#57ffa8"/>
<circle cx="98" cy="64" r="3.4" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="98" cy="64" r="1.8" fill="#57ffa8"/>
<circle cx="62" cy="57" r="4.2" fill="#57ffa8" fill-opacity="0.25"/>
<circle cx="62" cy="57" r="2.4" fill="#57ffa8"/>
<circle cx="134" cy="47" r="3" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="134" cy="47" r="1.6" fill="#57ffa8"/>
<circle cx="76" cy="68" r="2.8" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="76" cy="68" r="1.5" fill="#57ffa8"/>
<!-- угольки, слетающие с пламени -->
<circle cx="18" cy="20" r="2.6" fill="#57ffa8" fill-opacity="0.2"/>
<circle cx="18" cy="20" r="1.4" fill="#57ffa8"/>
<circle cx="14" cy="64" r="2.2" fill="#57ffa8" fill-opacity="0.2"/>
<circle cx="14" cy="64" r="1.2" fill="#57ffa8"/>
<circle cx="10" cy="86" r="2.4" fill="#57ffa8" fill-opacity="0.2"/>
<circle cx="10" cy="86" r="1.3" fill="#57ffa8"/>
<!-- светящийся глаз -->
<circle cx="130" cy="40" r="9" fill="#57ffa8" fill-opacity="0.3"/>
<circle cx="130" cy="40" r="6.5" fill="#fff"/>
<circle cx="131.5" cy="40" r="3" fill="#142638"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

+58
View File
@@ -0,0 +1,58 @@
<!-- Светлячок (glowfin): мелкая стройная полупрозрачная рыбка болота, тёмно-моховое тело ~4:1, светлое брюшко, мелкий вилочный хвост; вдоль бока — ряд циановых чёрточек свечения, по брюху — фотофоры, плавники усеяны циановыми точками, глаз светится цианом -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 100" width="160" height="100">
<!-- свечение хвостового плавника -->
<circle fill="#5ff2ff" cx="19" cy="38" r="1.7"/>
<circle fill="#5ff2ff" cx="19" cy="62" r="1.7"/>
<circle fill="#d6fdff" cx="24.5" cy="50" r="1.4"/>
<!-- хвост: мелкий вилочный -->
<path fill="var(--f3,hsl(150,30%,26%))" d="M35 43 L14 31 L24 48.5 L14 69 L35 57 Z"/>
<!-- тело: мелкое, стройное, заострённая морда -->
<path fill="var(--f1,hsl(150,22%,30%))" d="M146 50 C136 42 114 38 88 38 C62 38 44 42 34 47 L34 53 C44 58 62 62 88 62 C114 62 136 58 146 50 Z"/>
<!-- брюшко: чуть светлее (налёт полупрозрачности) -->
<path fill="var(--f2,hsl(150,18%,46%))" d="M146 50 C136 58 114 62 88 62 C62 62 44 58 34 53 C44 56 62 59 88 59 C114 59 136 56 146 50 Z"/>
<!-- просвет тела: тонкая светлая жилка-полупрозрачность -->
<path fill="var(--f2,hsl(150,18%,46%))" d="M134 47.5 C122 43.5 106 41.8 88 41.8 C70 41.8 56 43.5 46 47 C56 45 70 44 88 44 C106 44 122 45.5 134 47.5 Z"/>
<!-- спинная кромка: тонкая тёмная полоса -->
<path fill="var(--f4,hsl(150,25%,17%))" d="M146 50 C136 42 114 38 88 38 C62 38 44 42 34 47 C44 44 62 41.8 88 41.8 C114 41.8 136 44 146 50 Z"/>
<!-- спинной плавник: низкий, со свечением -->
<path fill="var(--f3,hsl(150,30%,26%))" d="M70 39.5 Q82 24 96 39 Z"/>
<circle fill="#5ff2ff" cx="79" cy="33" r="1.4"/>
<circle fill="#5ff2ff" cx="89" cy="35" r="1.4"/>
<!-- грудной плавник -->
<path fill="var(--f3,hsl(150,30%,26%))" d="M116 53 C112 56.5 111.5 60.5 115 63.5 C118 60 118.5 56.5 117.5 53.5 Z"/>
<circle fill="#5ff2ff" cx="115.5" cy="58" r="1.2"/>
<!-- брюшной плавник -->
<path fill="var(--f3,hsl(150,30%,26%))" d="M86 62 L82 68.5 L94 62.5 Z"/>
<!-- анальный плавник -->
<path fill="var(--f3,hsl(150,30%,26%))" d="M62 61.5 L57 68 L68 62.2 Z"/>
<circle fill="#5ff2ff" cx="63" cy="64" r="1.2"/>
<!-- рот: маленькая тёмная щель на конце морды (внутри силуэта) -->
<path fill="var(--f4,hsl(150,25%,17%))" d="M143 49.6 C145.5 49.9 147 50.6 147.8 51.6 C146.2 52.8 144.6 52.4 143.2 51.2 Z"/>
<!-- свечение вдоль бока: ряд циановых чёрточек -->
<ellipse fill="#5ff2ff" cx="50" cy="50.2" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="60" cy="50" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="70" cy="49.6" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="80" cy="49.2" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="90" cy="48.8" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="100" cy="48.4" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="110" cy="48" rx="2.3" ry="1.4"/>
<!-- яркие ядрышки свечения -->
<ellipse fill="#e8feff" cx="50" cy="49.6" rx="0.9" ry="0.6"/>
<ellipse fill="#e8feff" cx="70" cy="49" rx="0.9" ry="0.6"/>
<ellipse fill="#e8feff" cx="90" cy="48.2" rx="0.9" ry="0.6"/>
<ellipse fill="#e8feff" cx="110" cy="47.4" rx="0.9" ry="0.6"/>
<!-- фотофоры: точки свечения вдоль брюха -->
<circle fill="#5ff2ff" cx="49" cy="55.6" r="1.05"/>
<circle fill="#5ff2ff" cx="60" cy="57.6" r="1.05"/>
<circle fill="#5ff2ff" cx="71" cy="58.9" r="1.05"/>
<circle fill="#5ff2ff" cx="83" cy="59.4" r="1.05"/>
<circle fill="#5ff2ff" cx="95" cy="58.8" r="1.05"/>
<circle fill="#5ff2ff" cx="107" cy="57.6" r="1.05"/>
<circle fill="#5ff2ff" cx="117" cy="55.8" r="1.05"/>
<circle fill="#5ff2ff" cx="126" cy="53.2" r="1.05"/>
<!-- глаз: светится цианом -->
<circle fill="var(--f4,hsl(150,25%,17%))" cx="126.5" cy="47.5" r="4.1"/>
<circle fill="#5ff2ff" cx="126.5" cy="47.5" r="3.2"/>
<circle fill="#bdfbff" cx="126.5" cy="47.5" r="1.7"/>
<circle fill="#f2ffff" cx="126.5" cy="47" r="0.85"/>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

+26
View File
@@ -0,0 +1,26 @@
<!-- Лунная рыбка (moonfish): округлое, почти лунное тело; серебристо-голубые бока с бледно-голубыми (#b9c8ff) полумесячными знаками; крупный светлый глаз с блеском -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 100" width="160" height="100">
<!-- хвост: короткий полумесячный -->
<path fill="var(--f3,hsl(220,40%,34%))" d="M36 50 L9 31 L21 50 L9 69 Z"/>
<!-- спинной плавник: мягкий округлый -->
<path fill="var(--f3,hsl(220,40%,34%))" d="M60 22 C72 2 104 1 120 18 C99 12 76 14 60 22 Z"/>
<!-- анальный плавник -->
<path fill="var(--f3,hsl(220,40%,34%))" d="M64 84 C76 98 102 99 116 88 C98 90 78 88 64 84 Z"/>
<!-- тело: округлое лунное, серо-голубое -->
<path fill="var(--f1,hsl(220,25%,58%))" d="M30 50 C30 24 54 10 88 10 C124 10 148 28 148 50 C148 72 124 90 88 90 C54 90 30 76 30 50 Z"/>
<!-- брюхо: светлее, холодный отблеск -->
<path fill="var(--f2,hsl(220,25%,80%))" d="M30 50 C30 76 54 90 88 90 C124 90 148 72 148 50 C147 66 122 76 88 76 C58 76 32 66 30 50 Z"/>
<!-- узор: бледно-голубые (#b9c8ff = hsl(227,100%,86%)) полумесячные лунки на боку -->
<path fill="var(--f4,hsl(227,100%,86%))" d="M74 34 A17 17 0 1 0 74 62 A30 30 0 0 1 74 34 Z"/>
<path fill="var(--f4,hsl(227,100%,86%))" d="M98 52 A11 11 0 1 0 98 70 A20 20 0 0 1 98 52 Z"/>
<path fill="var(--f4,hsl(227,100%,86%))" d="M112 25 A7 7 0 1 0 112 39 A13 13 0 0 1 112 25 Z"/>
<!-- грудной плавник -->
<path fill="var(--f3,hsl(220,40%,34%))" d="M106 56 C116 58 122 68 118 77 C110 71 104 64 102 58 Z"/>
<!-- рот: маленький, на конце морды -->
<path fill="var(--f3,hsl(220,40%,34%))" d="M143 52 C147 53 149 55 150 57 C147 57 144 55 142 54 Z"/>
<!-- глаз: крупный, светлый, с бледно-голубой радужкой и блеском -->
<circle cx="123" cy="40" r="9.5" fill="#fff"/>
<circle cx="123" cy="40" r="6.5" fill="var(--f4,hsl(227,100%,86%))"/>
<circle cx="124" cy="40.5" r="3.2" fill="#142638"/>
<circle cx="121.5" cy="37.5" r="1.8" fill="#fff"/>
</svg>

After

Width:  |  Height:  |  Size: 2.3 KiB

+55
View File
@@ -0,0 +1,55 @@
<!-- Дух болота (spiritfin): полупрозрачное призрачное тело «каплей» вправо, вместо хвоста — растекающиеся вниз духовные отростки-струи, большие светящиеся золотые глаза, ореол из светящихся дуг вокруг головы, парящие золотые точки-души -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 100" width="160" height="100">
<!-- ореол: светящиеся дуги вокруг головы (сзади тела) -->
<path fill="none" stroke="#ffe07a" stroke-width="7" stroke-linecap="round" opacity="0.08" d="M76 18 Q118 -6 158 26"/>
<path fill="none" stroke="#ffe07a" stroke-width="3" stroke-linecap="round" opacity="0.55" d="M78 18 Q118 -5 158 26"/>
<path fill="none" stroke="#ffe07a" stroke-width="2.6" stroke-linecap="round" opacity="0.7" d="M88 18 Q122 1 158 32"/>
<path fill="none" stroke="#ffe07a" stroke-width="2.4" stroke-linecap="round" opacity="0.6" d="M102 18 Q132 5 158 38"/>
<path fill="none" stroke="#ffe07a" stroke-width="5" stroke-linecap="round" opacity="0.1" d="M138 82 Q162 66 158 42"/>
<path fill="none" stroke="#ffe07a" stroke-width="2.4" stroke-linecap="round" opacity="0.6" d="M138 82 Q162 66 158 42"/>
<!-- души-точки (задний план) -->
<circle fill="#ffe07a" cx="88" cy="10" r="5" opacity="0.18"/>
<circle fill="#ffe07a" cx="88" cy="10" r="2.2"/>
<circle fill="#ffe07a" cx="34" cy="18" r="6" opacity="0.18"/>
<circle fill="#ffe07a" cx="34" cy="18" r="2.6"/>
<circle fill="#ffe07a" cx="16" cy="70" r="5" opacity="0.18"/>
<circle fill="#ffe07a" cx="16" cy="70" r="2.2"/>
<!-- духовные отростки вместо хвоста: растекающиеся вниз струи -->
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.45" d="M40 30 C33 22 26 18 17 18 C26 23 32 28 36 35 Z"/>
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.8" d="M28 60 C24 72 22 84 25 97 C31 89 33 78 36 66 Z"/>
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.7" d="M44 73 C41 83 42 92 47 99 C52 91 51 82 53 73 Z"/>
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.6" d="M60 80 C59 88 62 95 68 100 C71 92 68 86 67 80 Z"/>
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.5" d="M76 84 C76 90 79 95 84 98 C86 92 84 87 82 83 Z"/>
<!-- спинной и грудной плавники: призрачные, полупрозрачные -->
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.85" d="M56 27 C64 12 82 7 96 15 C82 15 68 20 60 29 Z"/>
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.8" d="M116 60 C108 68 100 78 102 88 C112 80 120 70 124 62 Z"/>
<!-- тело: крупная призрачная «капля», полупрозрачная, тянется влево -->
<path fill="var(--f1,hsl(182,30%,40%))" opacity="0.85" d="M150 52 C150 38 138 27 118 23 C96 18.5 68 20 48 29 C38 33.5 30 40 25 47 C22 51 22 55 25 59 C30 66 38 72 48 76 C68 85 96 86.5 118 82 C138 78 150 66 150 52 Z"/>
<!-- брюхо: светлая пологая полоса снизу -->
<path fill="var(--f2,hsl(178,22%,64%))" opacity="0.85" d="M150 52 C150 66 138 78 118 82 C96 86.5 68 85 48 76 C38 72 30 66 25 59 C35 64.5 48 69 62 72 C90 78 122 74 150 52 Z"/>
<!-- духовные вихри на теле -->
<path fill="none" stroke="var(--f2,hsl(178,22%,64%))" stroke-width="2" stroke-linecap="round" opacity="0.5" d="M70 40 Q90 30 112 33"/>
<path fill="none" stroke="var(--f4,hsl(186,38%,20%))" stroke-width="2" stroke-linecap="round" opacity="0.3" d="M64 58 Q88 64 116 60"/>
<path fill="none" stroke="var(--f2,hsl(178,22%,64%))" stroke-width="1.8" stroke-linecap="round" opacity="0.4" d="M78 72 Q98 78 120 72"/>
<!-- жаберная линия у головы -->
<path fill="none" stroke="var(--f4,hsl(186,38%,20%))" stroke-width="2.5" stroke-linecap="round" opacity="0.32" d="M110 30 Q103 52 112 76"/>
<!-- пасть: тёмная щель на нижней части головы -->
<path fill="var(--f4,hsl(186,38%,20%))" d="M150 54 C140 61 128 65 118 65 C130 70 146 65 150 54 Z"/>
<!-- глаз: большой светящийся золотой -->
<circle fill="#ffe07a" cx="122" cy="44" r="13" opacity="0.18"/>
<circle fill="#ffe07a" cx="122" cy="44" r="9" opacity="0.35"/>
<circle fill="#fff3c4" cx="122" cy="44" r="6.5"/>
<circle fill="#142638" cx="122" cy="44" r="3.2"/>
<circle fill="#fff" cx="123.6" cy="42.2" r="1.4"/>
<!-- души-точки (передний план) -->
<circle fill="#ffe07a" cx="150" cy="24" r="6.5" opacity="0.2"/>
<circle fill="#ffe07a" cx="150" cy="24" r="3"/>
<circle fill="#ffe07a" cx="138" cy="88" r="5" opacity="0.2"/>
<circle fill="#ffe07a" cx="138" cy="88" r="2.2"/>
<circle fill="#ffe07a" cx="104" cy="92" r="7" opacity="0.2"/>
<circle fill="#ffe07a" cx="104" cy="92" r="3.2"/>
<circle fill="#ffe07a" cx="58" cy="94" r="5.5" opacity="0.2"/>
<circle fill="#ffe07a" cx="58" cy="94" r="2.4"/>
<circle fill="#ffe07a" cx="72" cy="70" r="4" opacity="0.35"/>
<circle fill="#ffe07a" cx="72" cy="70" r="1.8"/>
</svg>

After

Width:  |  Height:  |  Size: 5.1 KiB

+44
View File
@@ -0,0 +1,44 @@
<!-- Фиалковый огонёк (violetlure): стройное изящное тело в 3/4 вправо, фиалково-сиреневая чешуя (hsl ~280); вдоль бока — бледные светящиеся полоски (#d17bff); над головой — тонкая «удочка»-иллюций с ярким фиолетовым огоньком на конце -->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 160 100" width="160" height="100">
<!-- хвостовой плавник: узкая вилочка с вырезом -->
<path fill="var(--f3,hsl(280,52%,34%))" d="M46 50 C38 42 28 34 14 30 C24 38 28 44 26 50 C28 56 24 62 14 70 C28 66 38 58 46 50 Z"/>
<!-- спинной плавник: длинный низкий «парус» -->
<path fill="var(--f3,hsl(280,52%,34%))" d="M74 28 C82 12 100 6 118 20 C100 22 86 26 78 32 Z"/>
<!-- анальный плавник -->
<path fill="var(--f3,hsl(280,52%,34%))" d="M80 74 C88 88 102 92 114 84 C100 84 88 80 82 72 Z"/>
<!-- тело: стройное, вытянутое, смотрит вправо -->
<path fill="var(--f1,hsl(280,48%,52%))" d="M44 50 C52 33 74 24 100 24 C126 24 143 35 150 50 C143 65 126 76 100 76 C74 76 52 67 44 50 Z"/>
<!-- брюхо: светлее, сиреневый отблеск снизу -->
<path fill="var(--f2,hsl(280,38%,74%))" d="M150 50 C143 65 126 76 100 76 C74 76 52 67 44 50 C58 64 80 70 104 69 C127 68 142 60 150 50 Z"/>
<!-- светящиеся полоски вдоль бока: мягкое свечение + яркая сердцевина -->
<path fill="none" stroke="#d17bff" stroke-width="4.5" stroke-linecap="round" opacity="0.25" d="M58 44 C82 38 110 37 140 44"/>
<path fill="none" stroke="#d17bff" stroke-width="2" stroke-linecap="round" opacity="0.9" d="M58 44 C82 38 110 37 140 44"/>
<path fill="none" stroke="#d17bff" stroke-width="4.5" stroke-linecap="round" opacity="0.25" d="M62 54 C86 60 112 60 138 53"/>
<path fill="none" stroke="#d17bff" stroke-width="2" stroke-linecap="round" opacity="0.9" d="M62 54 C86 60 112 60 138 53"/>
<path fill="none" stroke="#d17bff" stroke-width="4" stroke-linecap="round" opacity="0.22" d="M70 63 C90 68 110 67 128 60"/>
<path fill="none" stroke="#d17bff" stroke-width="1.8" stroke-linecap="round" opacity="0.8" d="M70 63 C90 68 110 67 128 60"/>
<!-- искорки-огоньки на полосках -->
<circle fill="#d17bff" cx="70" cy="41" r="3.2" opacity="0.25"/>
<circle fill="#d17bff" cx="70" cy="41" r="1.5"/>
<circle fill="#d17bff" cx="96" cy="48" r="3.6" opacity="0.25"/>
<circle fill="#d17bff" cx="96" cy="48" r="1.7"/>
<circle fill="#d17bff" cx="124" cy="47" r="3" opacity="0.25"/>
<circle fill="#d17bff" cx="124" cy="47" r="1.4"/>
<!-- грудной плавник -->
<path fill="var(--f3,hsl(280,52%,34%))" d="M110 58 C122 60 128 68 124 78 C116 72 108 66 105 60 Z"/>
<!-- жаберная дуга у головы -->
<path fill="none" stroke="var(--f4,hsl(282,48%,24%))" stroke-width="2" stroke-linecap="round" opacity="0.5" d="M118 30 C112 40 112 60 119 72"/>
<!-- рот: маленький, на конце морды -->
<path fill="var(--f4,hsl(282,48%,24%))" d="M149 52 C143 56 137 56 132 54 C139 59 147 57 149 52 Z"/>
<!-- «удочка»-иллюций: тонкая дуга от лба вверх-вперёд -->
<path fill="none" stroke="var(--f4,hsl(282,48%,24%))" stroke-width="3" stroke-linecap="round" d="M124 28 C124 15 134 8 150 9"/>
<!-- приманка: яркий фиолетовый огонёк с ореолом свечения -->
<circle fill="#d17bff" cx="150" cy="9" r="9" opacity="0.22"/>
<circle fill="#d17bff" cx="150" cy="9" r="5" opacity="0.4"/>
<circle fill="#e9c4ff" cx="150" cy="9" r="3.4"/>
<!-- глаз: белый круг + зрачок, вокруг — фиалковое свечение -->
<circle fill="#d17bff" cx="126" cy="40" r="9.5" opacity="0.22"/>
<circle fill="#fff" cx="126" cy="40" r="7"/>
<circle fill="#142638" cx="127.5" cy="40" r="3.3"/>
<circle fill="#fff" cx="125" cy="37.8" r="1.5"/>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

+23
View File
@@ -810,3 +810,26 @@ git add -A && git commit -m "что изменилось" && git push
**Не тронуто:** небо/облака/звёзды/светила — общие; лодка/поплавок/дорожка света; роллы/баланс/физика; `resize()` по-прежнему без `initFish()` (рыбы не пересобираются по resize — поведение Этапа 43 сохранено).
**Тесты:** новые пины не нужны (визуал не ассертится; canvas-пиксели тестами не проверяются). Итого **193/193 PASS, exit 0**; `check.mjs` OK; `tools/balance.mjs` — пины без изменений. Визуальная приёмка (headless 1280×720, 4 локации × день/ночь, + кроп-контроль горизонта): Река — сплошная травяная полоса на всю ширину, лес с видимыми стволами на её кромке, волновых линий нет (день и ночь: ночью полоса/лес — тёмные силуэты, фонарь лодки светится); Озёра — холмы за деревьями, гладь; Море — парусник (паруса читаются днём и ночью) + чайки; Океан — открытый горизонт. «Светлые точки в небе» в дневном кадре — артефакт даунскейла превью (пиксельная выборка неба: чистый градиент).
## Этап 45. Локация «Волшебное болото» (5-я вода, 90 000 🪙)
**Задача:** по плану `docs/PLAN-SWAMP.md` добавить пятую локацию — **«Волшебное болото»** за 90 000 🪙: светящиеся ночью разными цветами рыбы, прыгающие лягушки, кувшинки и лилии (жёлтые/белые/розовые). Свечение — только ночью (`nightMix() > 0.35`), 5 новых видов по одному на редкость r0–r4, босс + персональная ачивка, доходность — между Озёрами и Морем.
**Решения:**
- **Данные** (секция ДАННЫЕ): `LOCATIONS[4]` — `{id:4,name:'Волшебное болото',nameEn:'Magic Swamp',cost:90000}` (в массиве — ПОСЛЕДНИЙ, но по цене между Озёрами и Морем), вода `#4d6b38/#2e4a28/#122818` днём и `#0d1f12/#08160c/#030a06` ночью — мрачно-болотная тёмно-зелёная, `waves {amp:0.8,amp2:0.5,freq:0.025,freq2:0.02,speed:0.5,speed2:0.35}` — слабая рябь (тише Озёр). 5 новых видов — только **В КОНЕЦ `SPECIES`** (32→37, порядок = `SWAMP_IDS`): glowfin «Светлячок» r0 8🪙 0.3–3 кг, moonfish «Лунная рыбка» r1 13🪙 0.8–8, emberfin «Изумрудный светляк» r2 20🪙 1.5–16, violetlure «Фиалковый огонёк» r3 38🪙 2–24, spiritfin «Дух болота» r4 70🪙 0.5–6; у каждого поле `glow` (#5ff2ff/#b9c8ff/#57ffa8/#d17bff/#ffe07a — 5 цветов, по одному на редкость). `NIGHT_WEIGHTS` — ключи всем 5 id (значения декоративные: у Болота своя таблица, ключи нужны лишь чтобы не было `weight=undefined`). Таблицы Болота — копия паттерна LAKE/SEA/OCEAN, без пересечений: `SWAMP_IDS=['glowfin','moonfish','emberfin','violetlure','spiritfin']`, `SWAMP_DAY_WEIGHTS={glowfin:3,moonfish:4,emberfin:28,violetlure:13,spiritfin:4.5}`, `SWAMP_NIGHT_WEIGHTS={glowfin:2.5,moonfish:2.5,emberfin:20,violetlure:30,spiritfin:4}` (ночью доля редких/дорогих выше — паттерн «ночь доходнее»). `BOSS_BY_LOCATION[4]='spiritfin'` (босс — Дух болота, вес 20–50 кг, награда ×5; новых видов под босса нет). Миграция сейва не потребовалась: граница `save.location>=LOCATIONS.length` авто-масштабируется с этапа 36, `save.bosses` уже миграционно готов.
- **`pickSpecies()` — табличный доступ:** вложенный тернарник заменён на `SPECIES_BY_LOCATION(_NIGHT)[idx]` по `save.location` с guard'ом «вне 0..4 → Река» (порядок таблиц = `LOCATIONS`); роллы локаций 0..3 идентичны прежним — пины §9j/M4/M6 затронуты не были.
- **Баланс** (подбор `node tools/balance.mjs --set`, финал перенесён в index.html и сверен без what-if): Болото **день 240.3 / ночь 328.9 🪙** (ночь/день ×1.37) — по цене и доходу между Озёрами (194.1/249.0) и Морем (297.3/476.7) в ОБЕ фазы; порядок вод по цене/доходу: **Река → Озёра → Болото → Море → Океан**. Пины остальных вод не изменились: Река 65.1/125.9 · Озёра 194.1/249.0 · Море 297.3/476.7 · Океан 329.1/552.7. Инвариант «новая (по цене) вода доходнее предыдущей в ОБЕ фазы»: Озёра/Река ×2.98/×1.98, Болото/Озёра ×1.24/×1.32, Море/Болото ×1.24/×1.45, Океан/Море ×1.11/×1.16 (тонкий дневной запас Океан/Море не задет). Минимальный вес вида 2.5 — тест «каждый вид ≥1» за 3000 роллов с запасом (день — min 171.4 попаданий, ночь — 127.1).
- **`tools/balance.mjs`:** +2 таблицы Болота (8→10), из вырезки данных добавлены `SWAMP_SPECIES(_NIGHT)`; сводка и пары отношений соседних вод сортируются **ПО ЦЕНЕ** `LOCATIONS` (Болото 90000 стоит в массиве последним, но по цене перед Морем/Океаном) — «каждая новая (по цене) вода доходнее предыдущей в ОБЕ фазы» теперь проверяется на парах Озёра/Река, Болото/Озёра, Море/Болото, Океан/Море. Проверки целостности: таблицы без пересечений, все 37 видов достигаемы, нечисловых весов нет.
- **Свечение — только ночь, `nightMix() > 0.35`** (тот же порог, что у фонаря лодки): `GLOW_RGBA` — кэш rgba-строк по hex (не собираются каждый кадр); `drawGlowAura(x,y,r,hex)` — радиальный градиент + `globalCompositeOperation='lighter'` в `save/restore`; общий хелпер `fishGlow(sp,x,y,r)` — аура подплывающей (APPROACH/BITE), уходящей и вываживаемой рыбы (вызов в `drawHookedFish()` внутри существующего save/restore — боссову золотую ауру не ломает). Фоновые стайки — в `drawAmbientFish()` при `save.location===4 && nightMix()>0.35`, цвет каждой рыбы `f.glow` из `palette`. Карточка улова — класс `glow-card` + CSS-переменная `--glow` (правило `#card.glow-card .bigemoji`) только для normal-варианта (чтобы не спорить с фильтрами вариантов); дневник — inline `filter:drop-shadow` на SVG (`class="glow-fish"`). Свечение — оформление вида, в UI не зависит от времени суток.
- **Декорации Болота:** ветка `i===4` в `initScenery()` — мшистый берег `{k:'bank'}`, камыш `{k:'reed'}`, кривые деревья `{k:'snag'}` и туи `{k:'pine'}` болотной гаммы; отдельные от `scenery` слои `pads[]` (8–14 кувшинок с вырезом-прорезью; у 45% — лилия одного из 3 цветов) и `frogs[]` (3–5 лягушек, стартуют на кувшинках). Оба слоя очищаются/пересобираются в `initScenery()`, а значит — в `resize()` и обеих ветках смены локации `shopClick()`. `updateDecor(dt)` — покачивание кувшинок (`sin`, 1.6 px) и прыжки лягушек: `sit` (3–7 с) → параболический `jump` (`x/y` по t плюс `sin(π·t)·jh`) на ближайшую ДРУГУЮ кувшинку (85%) или случайную точку воды; `drawSwampDecor()` — в `draw()` после `drawWater()`/`drawAmbientFish()` и до `drawBoat()` (поверх воды — `dusk()` берега декор не гасит; ночью притухает к #0b1a12). Фигуры — arc/ellipse/lineTo, без `shadowBlur`/градиентов на кадр.
- **`AMBIENT_FISH[4]`** — 7 рыб 12–30 px + `palette` из 4 цветов свечения (циан/зелёный/фиолет/золото); `initFish()` пишет цвет в `f.glow` по индексу рыбы (у локаций 0..3 — `null`).
- **Магазин / i18n / ачивки:** тост `welcome_swamp` (RU «Добро пожаловать в Волшебное болото! Здесь всё светится...» / EN «Welcome to the Magic Swamp! Everything glows here...»), в `shopClick` покупка — ветка `if/else if(loc.id===4)` вместо тернарника; название/описание локации и 5 видов — в `LOCATIONS`/`SPECIES` (`nameEn`/`descEn`). Ачивки 34→36: `swamp_first` «Тайна болота» (locations-based, `coinPct:0.02`) + `swamp_boss` «Победа над Духом болота» (пер-босс, `bonus:{}` без coinPct). Пин §10c(8) `achBonus().coinPct` **0.16→0.18** (в сумму вошла `swamp_first` +0.02); `species_8` теперь = все 37.
- **Портреты:** 5 SVG `assets/fish/fish-<id>.svg` (viewBox 0 0 160 100, `var(--f1..f4)`-заливки), id добавлены в `tools/embed-fish.mjs` (32→37), спрайт перевстроен — в index.html 37 `<symbol id="fish-...">`. Emoji-шрифт не тронут (новых emoji нет, `--check` OK).
**Не тронуто:** роллы локаций 0..3, `SPECIES[0..7]`, цены/веса существующих видов, физика боя/`updateReel()`, формат сейва (миграции не нужны), SDK/реклама/i18n-механика. Свечение и декор — чисто визуал.
**Тесты:** новые/обновлённые: **lc1** (SPECIES 32→37, LOCATIONS 4→5, costs `0,65536,131072,196608,90000`, `swampOrder=slice(32)`, `swampGlow`/`swampRar`, океанский `oceanOrder` ограничен `slice(23,32)`, SVG-спрайт 37 symbol), **lc2** (+`SWAMP_SPECIES(_NIGHT)`=5, порядок = `SWAMP_IDS`, пересечения с 4 водами пусты), **lc4d** (новый: роллы Болота 3000 днём + 3000 ночью — все 5 видов ≥1, чужих вод нет), **lc8w** (покупка Болота: списание 90000, авто-переход location=4, тост `welcome_swamp`), **lc9** (дневник 32→37), **lc10** (mid-лист swamp-видами), **lc11** (новый: у 5 видов glow — `#rrggbb`, 5 цветов различны, `SWAMP_DAY/NIGHT_WEIGHTS` покрывают все id, `NIGHT_WEIGHTS` — ключи всех 37), **a22d** (ACHIEVEMENTS 34→36 + `swamp_first`/`swamp_boss`), **a36loc** (+`swamp_first` для локаций `[0,4]`), пин §10c(8) 0.16→0.18, **§9k2** (+кейс Болота: босс `spiritfin`, `save.bosses.spiritfin`, ачивка `swamp_boss`). Итого **204/204 PASS, exit 0** (было 193); `check.mjs` OK; `tools/balance.mjs` — цифры выше, «здоровье» роллов без замечаний; `tools/emoji-font.py --check` OK.
**Скриншоты:** `tests/gen-screenshots.js` — +кадры `swamp` (день) и `swamp-night` (ночь со свечением) → 12→14 кадров; RU-набор перегенерирован (`screenshots-wiki/`, 42 PNG, включая `desk/mobl/mobp-swamp*.png`). EN-набор в этом этапе не переснимался (36 PNG без swamp; при необходимости — `node tests/gen-screenshots.js . screenshots-wiki-en en`). README: строка локаций + 2 изображения Болота (день/ночь), «14 кадров × 3 вьюпорта».
**Доки:** AGENTS.md (строки «Локации», «Баланс Озёр», «Баланс-скрипт», «Босс», «Награды», «Портреты рыб», «Скриншоты»), README.md (виды/локации/боссы/достижения/тесты/кадры), KODA.md (код-карта: `SWAMP_*`/`SPECIES_BY_LOCATION`, свечение/декор, пины, счётчики), этот DEVLOG.
+145
View File
@@ -0,0 +1,145 @@
# План: локация «Волшебное болото» (id 4, 90 000 🪙)
Статус: **реализован (Этап 45)** — см. запись «Этап 45» в `docs/DEVLOG.md`; тело плана ниже оставлено как историческая фиксация.
Ориентир по коду: `index.html` ~3861 строка; ссылки на строки даны на момент составления плана
(перед реализацией сверять — номера могли сдвинуться).
## 1. Задача
Добавить пятую локацию — **«Волшебное болото»** за **90 000 🪙**:
- светящиеся рыбы, которые **ночью** светятся в темноте **разными цветами**;
- **прыгающие лягушки**;
- **кувшинки** и **лилии** (жёлтые, белые, розовые).
## 2. Зафиксированные решения (ответы пользователя)
| Развилка | Решение |
|---|---|
| Когда светятся рыбы | **Только ночью** — включать при `nightMix() > 0.35` (тот же порог, что у фонаря на лодке, `index.html:2584`). Днём болото — обычная мрачная вода. |
| Сколько новых видов | **5 видов** по одному на редкость r0–r4, у каждого своё поле `glow:'#hex'` (разные цвета). |
| Босс | **Да** — светящийся босс + персональная ачивка (паттерн остальных 4 локаций). |
| Доходность | «Не принципиально» → берём **между Озёрами и Морем** (уважает связку «дороже вода = дороже улов» при цене 90 000). Ориентир день ~240 / ночь ~330 🪙; точные цифры подбираются `balance.mjs --set` и фиксируются. |
| Свечение в карточке/дневнике | **Да** — CSS `drop-shadow` по полю `glow` вида. |
## 3. Предлагаемые виды (имена/цвета можно менять)
| id | RU | EN | r | weight | wMin–wMax | glow | цена |
|---|---|---|---|---|---|---|---|
| `glowfin` | Светлячок | Glowfin | 0 | 38 | 0.3–3 | `#5ff2ff` циан | 8 |
| `moonfish` | Лунная рыбка | Moonfish | 1 | 24 | 0.8–8 | `#b9c8ff` бледно-голубой | 13 |
| `emberfin` | Изумрудный светляк | Emerald Glowfish | 2 | 14 | 1.5–16 | `#57ffa8` зелёный | 20 |
| `violetlure` | Фиалковый огонёк | Violet Lure | 3 | 5 | 2–24 | `#d17bff` фиолетовый | 38 |
| `spiritfin` | Дух болота | Swamp Spirit | 4 | 2.5 | 0.5–6 | `#ffe07a` золотой | 70 |
- **Босс** — `spiritfin` (легендарный дух; вес по общему правилу 20–50 кг, награда ×5).
- Веса и цены — стартовая заготовка, финал через `node tools/balance.mjs --set ...`.
- «Здоровье» теста «каждый вид ≥1»: минимальный вес вида ≥ ~2.2 (иначе lc4 за 3000 роллов
заплывёт). `moonfish`/`emberfin` в норме; при желании боссу можно добавить вес.
- **Дополнительные ачивки**: `swamp_first` (локация, `coinPct:0.02`, как `lake_first`) и
`swamp_boss` (без `coinPct`, как `lake_boss`). Это поднимет `ACHIEVEMENTS.length` 34→36
и пин `achBonus().coinPct` 0.16→0.18 — тесты и доки обновить явно.
## 4. Карта изменений по коду
### 4.1 Данные (`index.html`, секция «ДАННЫЕ»)
| Что | Где | Правка |
|---|---|---|
| Локация | `1143-1156` (`LOCATIONS`) | Запись `{id:4, name:'Волшебное болото', ... cost:90000}`; `water` — тёмно-моховой 6-hex (день/ночь); `waves` — слабая рябь (`amp~0.8`). Миграция сейва (`1382-1387`) уже завязана на `LOCATIONS.length`. |
| Новые виды | `1072-1118` (`SPECIES`) | Только **в КОНЕЦ** (после `whale`) — `SPECIES[0..7]` запинены тестами/квестами (§9j/M4/M6). Каждому поле `glow`. |
| Ночные веса | `1127` (`NIGHT_WEIGHTS`) | Ключ каждому новому id (иначе `weight=undefined`). |
| Таблицы болота | `1176-1196` | `SWAMP_IDS`, `SWAMP_DAY_WEIGHTS`, `SWAMP_NIGHT_WEIGHTS`, `SWAMP_SPECIES(_NIGHT)` — копия паттерна LAKE/SEA/OCEAN, без пересечений. |
| Босс | `1201` (`BOSS_BY_LOCATION`) | `4:'spiritfin'`. |
| Фоновые рыбы | `1809-1814` (`AMBIENT_FISH`) | 5-я запись + `palette` (циан/зелёный/фиолет/золото) для «разных цветов». |
### 4.2 Логика
- `pickSpecies()` (`1907-1925`): заменить вложенный тернарник на табличный доступ по
`save.location` + guard на диапазон. Поведение локаций 0..3 идентично; болото больше
не «проваливается» в реку.
- `startBite()` (`1950`): босс подхватится из `BOSS_BY_LOCATION[4]` автоматически;
`save.bosses` уже миграционно готов (`1371-1372`).
### 4.3 Свечение (только ночь)
- `drawFishShape`/`drawAmbientFish` (`2517-2540`): аура радиальным градиентом +
`globalCompositeOperation='lighter'`, цвет из `palette`/`glow`, только при
`nightMix()>0.35` и `save.location===4`.
- `draw()` (`2934-2943`) и `drawHookedFish()` (`2731-2745`): аура для вида с `glow` ночью
(паттерн боссовой ауры `2739-2743`). Обязательно `save/restore` либо сброс `shadowBlur`.
- Карточка (`showCard` `3645`, CSS `175-190`): класс `glow-card` для вида с `glow` **при
normal-варианте** (чтобы не конфликтовать с фильтрами вариантов) → `drop-shadow` цвета `glow`.
- Дневник (`renderShop` journal `3501-3521`): маркер-класс на SVG для светящихся видов.
### 4.4 Декорации (лягушки, кувшинки, лилии)
- `initScenery()` (`1836-1853`): ветка `i===4` — фон (камыш/кривые деревья) + отдельные
массивы `pads[]`/`frogs[]` (очищаются вместе с `scenery`).
- `updateDecor(dt)` (`2240-2266`): параболический прыжок лягушек; покачивание кувшинок.
- Новая `drawSwampDecor()`: кувшинки (эллипс с вырезом), лилии жёлтые/белые/розовые
(лепестки), лягушки. Вызов в `draw()` (`2924`) **после** `drawWater`/`drawAmbientFish`
и до `drawBoat` — лилии поверх воды.
- Вызовы `initScenery()` не трогаем (уже есть в `resize` и обеих ветках `shopClick`).
### 4.5 Магазин / i18n / ачивки
- `shopClick` тост (`3586`): ветка `id===4` → `welcome_swamp`.
- `T` ru/en (`3060+` / `3157+`): название и описание локации, `welcome_swamp`,
`nameEn`/`descEn`/описания видов.
- `ACHIEVEMENTS` (`1618-1655`): `swamp_first` (+0.02), `swamp_boss` (без `coinPct`).
### 4.6 Портреты (основной объём)
- 5 файлов `assets/fish/fish-<id>.svg` (viewBox `0 0 160 100`, цвета через `var(--f1/--f3)`).
- Добавить id в `tools/embed-fish.mjs:24-29`, прогнать `node tools/embed-fish.mjs`.
### 4.7 Тесты (`tests/test.js`) — в том же коммите
- `lc1` (`1956-2010`): `SPECIES.length` 32→37, `LOCATIONS` 4→5, `costs` +`,90000`,
`names`, `sprites` 32→37, `swampOrder`, поля новых видов.
- `lc2` (`2013-2045`): `SWAMP_*` длины/порядок/пересечения.
- Новый тест роллов болота (аналог `lc4c` `2112-2132`).
- `lc5` (`2135-2171`): кейс `location:4`.
- Покупка болота — аналог `lc8c` (`2247-2259`).
- `lc9` (`2261-2266`): дневник 32→37.
- `lc10` (`2268-2290`): дополнить `mid24` swamp-видами.
- `a22d` (`2309-2326`): `ACHIEVEMENTS.length` 34→36.
- Пин `0.16` (`1648-1666`): 0.16→0.18.
- Новый data-тест: у swamp-видов есть `glow`, цвета различны, `*_WEIGHTS` покрывают все id.
### 4.8 Инструменты / доки / визуал
- `tools/balance.mjs`: таблицы болота в `TABLES`, пины в шапке; сортировать сводку
отношений по цене (т.к. 90000 < Моря/Океана, а порядок массива иной).
- `tests/gen-screenshots.js`: кадр `swamp` (день) и/или `swamp-night` со свечением.
- `AGENTS.md`, `README.md`, `KODA.md`, `docs/DEVLOG.md` («Этап 45»).
- Emoji-шрифт не трогаем (лягушки/лилии — фигуры canvas), пока не появятся новые emoji.
## 5. Порядок работ (фазы)
1. Данные: `LOCATIONS` + `SPECIES` + веса + `BOSS_BY_LOCATION` + табличный `pickSpecies`.
→ `node tools/check.mjs`.
2. Баланс: `node tools/balance.mjs` (+`--set`), зафиксировать числа.
3. Портреты: 5 SVG + `embed-fish.mjs`.
4. Свечение рыб (ambient/bite/reel, карточка/дневник).
5. Декорации: лягушки/кувшинки/лилии + фон болота.
6. Магазин/тосты/i18n/ачивки.
7. Тесты (все пины выше) → `cd tests && npm test`.
8. Доки + скриншоты + при желании `orca-playtest.js`.
## 6. Проверка (порядок команд)
1. `node tools/check.mjs`
2. `node tools/embed-fish.mjs` (сам прогонит check)
3. `node tools/balance.mjs` — пины Река/Озёра/Море/Океан не изменились, болото в цели
4. `cd tests && npm test` — все PASS, exit 0
5. Опционально: `node tests/orca-playtest.js`, `node tests/gen-screenshots.js`
## 7. Риски
1. `SPECIES` — только в конец (иначе river-пины/квесты ломаются молча).
2. `pickSpecies` табличный рефакторинг — нужен guard, чтобы не изменить роллы локаций 0..3.
3. Порядок отрисовки лилий/лягушек — строго после воды.
4. Порог свечения и производительность (`shadowBlur`/градиенты на мобиле).
5. Числовые пины тестов и `achBonus().coinPct` — обновить всё в одном коммите.
+511 -28
View File
@@ -188,6 +188,9 @@ button{font-family:inherit;cursor:pointer}
border:1px solid #ffd75e;box-shadow:0 0 14px rgba(255,215,94,.75);animation:recpulse .8s ease-in-out infinite}
#card.boss-card .panel{border-color:#ffd75e;box-shadow:0 0 34px rgba(255,215,94,.55),0 10px 40px rgba(0,0,0,.5)}
#card.boss-card .bigemoji{filter:drop-shadow(0 0 12px rgba(255,215,94,.95))}
/* Фаза свечения «Болота»: светящийся вид в normal-варианте — цветной drop-shadow по --glow
(цвет ставит showCard; класс только для normal, чтобы не спорить с фильтрами вариантов) */
#card.glow-card .bigemoji{filter:drop-shadow(0 0 16px var(--glow,#5ff2ff)) drop-shadow(0 6px 10px rgba(0,0,0,.4))}
/* Перекраска SVG-портрета под вариант: --f1 (тело) / --f3 (плавники) — переменные наследуются
сквозь <use> в спрайт; silver/rainbow закрываются filter-правилами выше, boss — тоже */
#card.copper-card .bigemoji{--f1:hsl(25,55%,55%);--f3:hsl(25,50%,35%)}
@@ -952,6 +955,236 @@ button{font-family:inherit;cursor:pointer}
<circle fill="#fff" cx="137" cy="45" r="2.9"/>
<circle fill="#142638" cx="137.8" cy="45" r="2.2"/>
</symbol>
<symbol id="fish-glowfin" viewBox="0 0 160 100">
<!-- свечение хвостового плавника -->
<circle fill="#5ff2ff" cx="19" cy="38" r="1.7"/>
<circle fill="#5ff2ff" cx="19" cy="62" r="1.7"/>
<circle fill="#d6fdff" cx="24.5" cy="50" r="1.4"/>
<!-- хвост: мелкий вилочный -->
<path fill="var(--f3,hsl(150,30%,26%))" d="M35 43 L14 31 L24 48.5 L14 69 L35 57 Z"/>
<!-- тело: мелкое, стройное, заострённая морда -->
<path fill="var(--f1,hsl(150,22%,30%))" d="M146 50 C136 42 114 38 88 38 C62 38 44 42 34 47 L34 53 C44 58 62 62 88 62 C114 62 136 58 146 50 Z"/>
<!-- брюшко: чуть светлее (налёт полупрозрачности) -->
<path fill="var(--f2,hsl(150,18%,46%))" d="M146 50 C136 58 114 62 88 62 C62 62 44 58 34 53 C44 56 62 59 88 59 C114 59 136 56 146 50 Z"/>
<!-- просвет тела: тонкая светлая жилка-полупрозрачность -->
<path fill="var(--f2,hsl(150,18%,46%))" d="M134 47.5 C122 43.5 106 41.8 88 41.8 C70 41.8 56 43.5 46 47 C56 45 70 44 88 44 C106 44 122 45.5 134 47.5 Z"/>
<!-- спинная кромка: тонкая тёмная полоса -->
<path fill="var(--f4,hsl(150,25%,17%))" d="M146 50 C136 42 114 38 88 38 C62 38 44 42 34 47 C44 44 62 41.8 88 41.8 C114 41.8 136 44 146 50 Z"/>
<!-- спинной плавник: низкий, со свечением -->
<path fill="var(--f3,hsl(150,30%,26%))" d="M70 39.5 Q82 24 96 39 Z"/>
<circle fill="#5ff2ff" cx="79" cy="33" r="1.4"/>
<circle fill="#5ff2ff" cx="89" cy="35" r="1.4"/>
<!-- грудной плавник -->
<path fill="var(--f3,hsl(150,30%,26%))" d="M116 53 C112 56.5 111.5 60.5 115 63.5 C118 60 118.5 56.5 117.5 53.5 Z"/>
<circle fill="#5ff2ff" cx="115.5" cy="58" r="1.2"/>
<!-- брюшной плавник -->
<path fill="var(--f3,hsl(150,30%,26%))" d="M86 62 L82 68.5 L94 62.5 Z"/>
<!-- анальный плавник -->
<path fill="var(--f3,hsl(150,30%,26%))" d="M62 61.5 L57 68 L68 62.2 Z"/>
<circle fill="#5ff2ff" cx="63" cy="64" r="1.2"/>
<!-- рот: маленькая тёмная щель на конце морды (внутри силуэта) -->
<path fill="var(--f4,hsl(150,25%,17%))" d="M143 49.6 C145.5 49.9 147 50.6 147.8 51.6 C146.2 52.8 144.6 52.4 143.2 51.2 Z"/>
<!-- свечение вдоль бока: ряд циановых чёрточек -->
<ellipse fill="#5ff2ff" cx="50" cy="50.2" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="60" cy="50" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="70" cy="49.6" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="80" cy="49.2" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="90" cy="48.8" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="100" cy="48.4" rx="2.3" ry="1.4"/>
<ellipse fill="#5ff2ff" cx="110" cy="48" rx="2.3" ry="1.4"/>
<!-- яркие ядрышки свечения -->
<ellipse fill="#e8feff" cx="50" cy="49.6" rx="0.9" ry="0.6"/>
<ellipse fill="#e8feff" cx="70" cy="49" rx="0.9" ry="0.6"/>
<ellipse fill="#e8feff" cx="90" cy="48.2" rx="0.9" ry="0.6"/>
<ellipse fill="#e8feff" cx="110" cy="47.4" rx="0.9" ry="0.6"/>
<!-- фотофоры: точки свечения вдоль брюха -->
<circle fill="#5ff2ff" cx="49" cy="55.6" r="1.05"/>
<circle fill="#5ff2ff" cx="60" cy="57.6" r="1.05"/>
<circle fill="#5ff2ff" cx="71" cy="58.9" r="1.05"/>
<circle fill="#5ff2ff" cx="83" cy="59.4" r="1.05"/>
<circle fill="#5ff2ff" cx="95" cy="58.8" r="1.05"/>
<circle fill="#5ff2ff" cx="107" cy="57.6" r="1.05"/>
<circle fill="#5ff2ff" cx="117" cy="55.8" r="1.05"/>
<circle fill="#5ff2ff" cx="126" cy="53.2" r="1.05"/>
<!-- глаз: светится цианом -->
<circle fill="var(--f4,hsl(150,25%,17%))" cx="126.5" cy="47.5" r="4.1"/>
<circle fill="#5ff2ff" cx="126.5" cy="47.5" r="3.2"/>
<circle fill="#bdfbff" cx="126.5" cy="47.5" r="1.7"/>
<circle fill="#f2ffff" cx="126.5" cy="47" r="0.85"/>
</symbol>
<symbol id="fish-moonfish" viewBox="0 0 160 100">
<!-- хвост: короткий полумесячный -->
<path fill="var(--f3,hsl(220,40%,34%))" d="M36 50 L9 31 L21 50 L9 69 Z"/>
<!-- спинной плавник: мягкий округлый -->
<path fill="var(--f3,hsl(220,40%,34%))" d="M60 22 C72 2 104 1 120 18 C99 12 76 14 60 22 Z"/>
<!-- анальный плавник -->
<path fill="var(--f3,hsl(220,40%,34%))" d="M64 84 C76 98 102 99 116 88 C98 90 78 88 64 84 Z"/>
<!-- тело: округлое лунное, серо-голубое -->
<path fill="var(--f1,hsl(220,25%,58%))" d="M30 50 C30 24 54 10 88 10 C124 10 148 28 148 50 C148 72 124 90 88 90 C54 90 30 76 30 50 Z"/>
<!-- брюхо: светлее, холодный отблеск -->
<path fill="var(--f2,hsl(220,25%,80%))" d="M30 50 C30 76 54 90 88 90 C124 90 148 72 148 50 C147 66 122 76 88 76 C58 76 32 66 30 50 Z"/>
<!-- узор: бледно-голубые (#b9c8ff = hsl(227,100%,86%)) полумесячные лунки на боку -->
<path fill="var(--f4,hsl(227,100%,86%))" d="M74 34 A17 17 0 1 0 74 62 A30 30 0 0 1 74 34 Z"/>
<path fill="var(--f4,hsl(227,100%,86%))" d="M98 52 A11 11 0 1 0 98 70 A20 20 0 0 1 98 52 Z"/>
<path fill="var(--f4,hsl(227,100%,86%))" d="M112 25 A7 7 0 1 0 112 39 A13 13 0 0 1 112 25 Z"/>
<!-- грудной плавник -->
<path fill="var(--f3,hsl(220,40%,34%))" d="M106 56 C116 58 122 68 118 77 C110 71 104 64 102 58 Z"/>
<!-- рот: маленький, на конце морды -->
<path fill="var(--f3,hsl(220,40%,34%))" d="M143 52 C147 53 149 55 150 57 C147 57 144 55 142 54 Z"/>
<!-- глаз: крупный, светлый, с бледно-голубой радужкой и блеском -->
<circle cx="123" cy="40" r="9.5" fill="#fff"/>
<circle cx="123" cy="40" r="6.5" fill="var(--f4,hsl(227,100%,86%))"/>
<circle cx="124" cy="40.5" r="3.2" fill="#142638"/>
<circle cx="121.5" cy="37.5" r="1.8" fill="#fff"/>
</symbol>
<symbol id="fish-emberfin" viewBox="0 0 160 100">
<!-- хвостовой плавник: три рваных языка пламени с вырезами посередине -->
<path d="M47,38 C41,29 34,20 27,12 C23,7 17,3 9,1 C16,8 18,14 16,19 L23,21 L14,26 C17,31 23,35 31,39 C37,42 42,41 47,42 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<path d="M47,43 C38,39 28,35 18,32 C13,30 8,29 1,31 C8,34 10,38 9,42 L16,44 L8,48 C12,52 18,53 27,52 C35,51 42,48 47,48 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<path d="M47,54 C42,61 36,68 29,75 C25,79 20,83 12,89 C18,80 19,75 18,70 L25,68 L17,63 C20,58 25,55 33,53 C39,52 43,54 47,54 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<!-- горящая сердцевина пламени: акцентная изумрудная вспышка -->
<path d="M45,41 C39,34 33,27 27,22 C23,19 18,17 13,16 C18,21 19,25 17,29 L22,30 L16,34 C20,38 26,41 33,42 C39,42 42,41 45,41 Z" fill="#57ffa8" fill-opacity="0.8"/>
<path d="M45,46 C37,44 29,41 21,39 C17,38 13,38 8,39 C13,41 14,44 13,47 L18,48 L13,51 C17,53 22,53 29,51 C35,49 41,47 45,47 Z" fill="#57ffa8" fill-opacity="0.6"/>
<!-- тело: плотное, смотрит вправо -->
<path d="M40,50 C56,24 78,20 104,22 C130,25 143,36 148,50 C143,64 130,75 104,78 C78,80 56,76 40,50 Z" fill="var(--f1,hsl(150,52%,40%))"/>
<!-- брюхо: светлее, блик вдоль нижней кромки -->
<path d="M148,50 C143,64 130,75 104,78 C78,80 56,76 40,50 C56,66 82,70 108,68 C130,66 141,60 148,50 Z" fill="var(--f2,hsl(150,45%,64%))"/>
<!-- спинной плавник -->
<path d="M78,26 C88,12 100,8 116,22 C100,23 88,25 82,30 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<!-- грудной плавник -->
<path d="M104,56 C118,60 124,66 120,78 C112,72 104,66 100,60 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<!-- брюшной плавник -->
<path d="M84,76 C92,86 100,89 108,84 C100,81 94,78 90,73 Z" fill="var(--f3,hsl(150,55%,27%))"/>
<!-- жаберная дуга -->
<path d="M120,32 C114,42 114,50 120,58" fill="none" stroke="var(--f4,hsl(150,60%,19%))" stroke-width="1.8" stroke-linecap="round"/>
<!-- искры по телу: свечение + ядро -->
<circle cx="70" cy="33" r="4" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="70" cy="33" r="2.2" fill="#57ffa8"/>
<circle cx="92" cy="29" r="3.4" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="92" cy="29" r="1.8" fill="#57ffa8"/>
<circle cx="110" cy="40" r="4.6" fill="#57ffa8" fill-opacity="0.25"/>
<circle cx="110" cy="40" r="2.6" fill="#57ffa8"/>
<circle cx="82" cy="50" r="3.6" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="82" cy="50" r="2" fill="#57ffa8"/>
<circle cx="122" cy="57" r="4" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="122" cy="57" r="2.2" fill="#57ffa8"/>
<circle cx="98" cy="64" r="3.4" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="98" cy="64" r="1.8" fill="#57ffa8"/>
<circle cx="62" cy="57" r="4.2" fill="#57ffa8" fill-opacity="0.25"/>
<circle cx="62" cy="57" r="2.4" fill="#57ffa8"/>
<circle cx="134" cy="47" r="3" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="134" cy="47" r="1.6" fill="#57ffa8"/>
<circle cx="76" cy="68" r="2.8" fill="#57ffa8" fill-opacity="0.22"/>
<circle cx="76" cy="68" r="1.5" fill="#57ffa8"/>
<!-- угольки, слетающие с пламени -->
<circle cx="18" cy="20" r="2.6" fill="#57ffa8" fill-opacity="0.2"/>
<circle cx="18" cy="20" r="1.4" fill="#57ffa8"/>
<circle cx="14" cy="64" r="2.2" fill="#57ffa8" fill-opacity="0.2"/>
<circle cx="14" cy="64" r="1.2" fill="#57ffa8"/>
<circle cx="10" cy="86" r="2.4" fill="#57ffa8" fill-opacity="0.2"/>
<circle cx="10" cy="86" r="1.3" fill="#57ffa8"/>
<!-- светящийся глаз -->
<circle cx="130" cy="40" r="9" fill="#57ffa8" fill-opacity="0.3"/>
<circle cx="130" cy="40" r="6.5" fill="#fff"/>
<circle cx="131.5" cy="40" r="3" fill="#142638"/>
</symbol>
<symbol id="fish-violetlure" viewBox="0 0 160 100">
<!-- хвостовой плавник: узкая вилочка с вырезом -->
<path fill="var(--f3,hsl(280,52%,34%))" d="M46 50 C38 42 28 34 14 30 C24 38 28 44 26 50 C28 56 24 62 14 70 C28 66 38 58 46 50 Z"/>
<!-- спинной плавник: длинный низкий «парус» -->
<path fill="var(--f3,hsl(280,52%,34%))" d="M74 28 C82 12 100 6 118 20 C100 22 86 26 78 32 Z"/>
<!-- анальный плавник -->
<path fill="var(--f3,hsl(280,52%,34%))" d="M80 74 C88 88 102 92 114 84 C100 84 88 80 82 72 Z"/>
<!-- тело: стройное, вытянутое, смотрит вправо -->
<path fill="var(--f1,hsl(280,48%,52%))" d="M44 50 C52 33 74 24 100 24 C126 24 143 35 150 50 C143 65 126 76 100 76 C74 76 52 67 44 50 Z"/>
<!-- брюхо: светлее, сиреневый отблеск снизу -->
<path fill="var(--f2,hsl(280,38%,74%))" d="M150 50 C143 65 126 76 100 76 C74 76 52 67 44 50 C58 64 80 70 104 69 C127 68 142 60 150 50 Z"/>
<!-- светящиеся полоски вдоль бока: мягкое свечение + яркая сердцевина -->
<path fill="none" stroke="#d17bff" stroke-width="4.5" stroke-linecap="round" opacity="0.25" d="M58 44 C82 38 110 37 140 44"/>
<path fill="none" stroke="#d17bff" stroke-width="2" stroke-linecap="round" opacity="0.9" d="M58 44 C82 38 110 37 140 44"/>
<path fill="none" stroke="#d17bff" stroke-width="4.5" stroke-linecap="round" opacity="0.25" d="M62 54 C86 60 112 60 138 53"/>
<path fill="none" stroke="#d17bff" stroke-width="2" stroke-linecap="round" opacity="0.9" d="M62 54 C86 60 112 60 138 53"/>
<path fill="none" stroke="#d17bff" stroke-width="4" stroke-linecap="round" opacity="0.22" d="M70 63 C90 68 110 67 128 60"/>
<path fill="none" stroke="#d17bff" stroke-width="1.8" stroke-linecap="round" opacity="0.8" d="M70 63 C90 68 110 67 128 60"/>
<!-- искорки-огоньки на полосках -->
<circle fill="#d17bff" cx="70" cy="41" r="3.2" opacity="0.25"/>
<circle fill="#d17bff" cx="70" cy="41" r="1.5"/>
<circle fill="#d17bff" cx="96" cy="48" r="3.6" opacity="0.25"/>
<circle fill="#d17bff" cx="96" cy="48" r="1.7"/>
<circle fill="#d17bff" cx="124" cy="47" r="3" opacity="0.25"/>
<circle fill="#d17bff" cx="124" cy="47" r="1.4"/>
<!-- грудной плавник -->
<path fill="var(--f3,hsl(280,52%,34%))" d="M110 58 C122 60 128 68 124 78 C116 72 108 66 105 60 Z"/>
<!-- жаберная дуга у головы -->
<path fill="none" stroke="var(--f4,hsl(282,48%,24%))" stroke-width="2" stroke-linecap="round" opacity="0.5" d="M118 30 C112 40 112 60 119 72"/>
<!-- рот: маленький, на конце морды -->
<path fill="var(--f4,hsl(282,48%,24%))" d="M149 52 C143 56 137 56 132 54 C139 59 147 57 149 52 Z"/>
<!-- «удочка»-иллюций: тонкая дуга от лба вверх-вперёд -->
<path fill="none" stroke="var(--f4,hsl(282,48%,24%))" stroke-width="3" stroke-linecap="round" d="M124 28 C124 15 134 8 150 9"/>
<!-- приманка: яркий фиолетовый огонёк с ореолом свечения -->
<circle fill="#d17bff" cx="150" cy="9" r="9" opacity="0.22"/>
<circle fill="#d17bff" cx="150" cy="9" r="5" opacity="0.4"/>
<circle fill="#e9c4ff" cx="150" cy="9" r="3.4"/>
<!-- глаз: белый круг + зрачок, вокруг — фиалковое свечение -->
<circle fill="#d17bff" cx="126" cy="40" r="9.5" opacity="0.22"/>
<circle fill="#fff" cx="126" cy="40" r="7"/>
<circle fill="#142638" cx="127.5" cy="40" r="3.3"/>
<circle fill="#fff" cx="125" cy="37.8" r="1.5"/>
</symbol>
<symbol id="fish-spiritfin" viewBox="0 0 160 100">
<!-- ореол: светящиеся дуги вокруг головы (сзади тела) -->
<path fill="none" stroke="#ffe07a" stroke-width="7" stroke-linecap="round" opacity="0.08" d="M76 18 Q118 -6 158 26"/>
<path fill="none" stroke="#ffe07a" stroke-width="3" stroke-linecap="round" opacity="0.55" d="M78 18 Q118 -5 158 26"/>
<path fill="none" stroke="#ffe07a" stroke-width="2.6" stroke-linecap="round" opacity="0.7" d="M88 18 Q122 1 158 32"/>
<path fill="none" stroke="#ffe07a" stroke-width="2.4" stroke-linecap="round" opacity="0.6" d="M102 18 Q132 5 158 38"/>
<path fill="none" stroke="#ffe07a" stroke-width="5" stroke-linecap="round" opacity="0.1" d="M138 82 Q162 66 158 42"/>
<path fill="none" stroke="#ffe07a" stroke-width="2.4" stroke-linecap="round" opacity="0.6" d="M138 82 Q162 66 158 42"/>
<!-- души-точки (задний план) -->
<circle fill="#ffe07a" cx="88" cy="10" r="5" opacity="0.18"/>
<circle fill="#ffe07a" cx="88" cy="10" r="2.2"/>
<circle fill="#ffe07a" cx="34" cy="18" r="6" opacity="0.18"/>
<circle fill="#ffe07a" cx="34" cy="18" r="2.6"/>
<circle fill="#ffe07a" cx="16" cy="70" r="5" opacity="0.18"/>
<circle fill="#ffe07a" cx="16" cy="70" r="2.2"/>
<!-- духовные отростки вместо хвоста: растекающиеся вниз струи -->
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.45" d="M40 30 C33 22 26 18 17 18 C26 23 32 28 36 35 Z"/>
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.8" d="M28 60 C24 72 22 84 25 97 C31 89 33 78 36 66 Z"/>
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.7" d="M44 73 C41 83 42 92 47 99 C52 91 51 82 53 73 Z"/>
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.6" d="M60 80 C59 88 62 95 68 100 C71 92 68 86 67 80 Z"/>
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.5" d="M76 84 C76 90 79 95 84 98 C86 92 84 87 82 83 Z"/>
<!-- спинной и грудной плавники: призрачные, полупрозрачные -->
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.85" d="M56 27 C64 12 82 7 96 15 C82 15 68 20 60 29 Z"/>
<path fill="var(--f3,hsl(184,34%,28%))" opacity="0.8" d="M116 60 C108 68 100 78 102 88 C112 80 120 70 124 62 Z"/>
<!-- тело: крупная призрачная «капля», полупрозрачная, тянется влево -->
<path fill="var(--f1,hsl(182,30%,40%))" opacity="0.85" d="M150 52 C150 38 138 27 118 23 C96 18.5 68 20 48 29 C38 33.5 30 40 25 47 C22 51 22 55 25 59 C30 66 38 72 48 76 C68 85 96 86.5 118 82 C138 78 150 66 150 52 Z"/>
<!-- брюхо: светлая пологая полоса снизу -->
<path fill="var(--f2,hsl(178,22%,64%))" opacity="0.85" d="M150 52 C150 66 138 78 118 82 C96 86.5 68 85 48 76 C38 72 30 66 25 59 C35 64.5 48 69 62 72 C90 78 122 74 150 52 Z"/>
<!-- духовные вихри на теле -->
<path fill="none" stroke="var(--f2,hsl(178,22%,64%))" stroke-width="2" stroke-linecap="round" opacity="0.5" d="M70 40 Q90 30 112 33"/>
<path fill="none" stroke="var(--f4,hsl(186,38%,20%))" stroke-width="2" stroke-linecap="round" opacity="0.3" d="M64 58 Q88 64 116 60"/>
<path fill="none" stroke="var(--f2,hsl(178,22%,64%))" stroke-width="1.8" stroke-linecap="round" opacity="0.4" d="M78 72 Q98 78 120 72"/>
<!-- жаберная линия у головы -->
<path fill="none" stroke="var(--f4,hsl(186,38%,20%))" stroke-width="2.5" stroke-linecap="round" opacity="0.32" d="M110 30 Q103 52 112 76"/>
<!-- пасть: тёмная щель на нижней части головы -->
<path fill="var(--f4,hsl(186,38%,20%))" d="M150 54 C140 61 128 65 118 65 C130 70 146 65 150 54 Z"/>
<!-- глаз: большой светящийся золотой -->
<circle fill="#ffe07a" cx="122" cy="44" r="13" opacity="0.18"/>
<circle fill="#ffe07a" cx="122" cy="44" r="9" opacity="0.35"/>
<circle fill="#fff3c4" cx="122" cy="44" r="6.5"/>
<circle fill="#142638" cx="122" cy="44" r="3.2"/>
<circle fill="#fff" cx="123.6" cy="42.2" r="1.4"/>
<!-- души-точки (передний план) -->
<circle fill="#ffe07a" cx="150" cy="24" r="6.5" opacity="0.2"/>
<circle fill="#ffe07a" cx="150" cy="24" r="3"/>
<circle fill="#ffe07a" cx="138" cy="88" r="5" opacity="0.2"/>
<circle fill="#ffe07a" cx="138" cy="88" r="2.2"/>
<circle fill="#ffe07a" cx="104" cy="92" r="7" opacity="0.2"/>
<circle fill="#ffe07a" cx="104" cy="92" r="3.2"/>
<circle fill="#ffe07a" cx="58" cy="94" r="5.5" opacity="0.2"/>
<circle fill="#ffe07a" cx="58" cy="94" r="2.4"/>
<circle fill="#ffe07a" cx="72" cy="70" r="4" opacity="0.35"/>
<circle fill="#ffe07a" cx="72" cy="70" r="1.8"/>
</symbol>
</svg>
<!-- FISH-SVG:END -->
<canvas id="game"></canvas>
@@ -1115,6 +1348,13 @@ const SPECIES=[
{id:'shark', name:'Большая белая акула', nameEn:'Great White Shark', rarity:3, weight:2.5, minDist:0.78, strength:0.78, wMin:20.0, wMax:56.0, price:31, hue:210},
{id:'squid', name:'Гигантский кальмар', nameEn:'Giant Squid', rarity:4, weight:2.5, minDist:0.85, strength:0.85, wMin:16.0, wMax:48.0, price:37, hue:300},
{id:'whale', name:'Синий кит', nameEn:'Blue Whale', rarity:4, weight:2, minDist:0.92, strength:0.95, wMin:40.0, wMax:100.0, price:67, hue:230},
/* Этап 45: Волшебное болото — 5 светящихся видов (только в конец массива: SPECIES[0..7]
запинены тестами/квестами). Новое поле glow — цвет ночного свечения (у старых видов нет) */
{id:'glowfin', name:'Светлячок', nameEn:'Glowfin', rarity:0, weight:38, minDist:0.05, strength:0.28, wMin:0.3, wMax:3.0, price:8, hue:185, glow:'#5ff2ff'},
{id:'moonfish', name:'Лунная рыбка', nameEn:'Moonfish', rarity:1, weight:24, minDist:0.30, strength:0.40, wMin:0.8, wMax:8.0, price:13, hue:225, glow:'#b9c8ff'},
{id:'emberfin', name:'Изумрудный светляк', nameEn:'Emerald Glowfish', rarity:2, weight:14, minDist:0.55, strength:0.58, wMin:1.5, wMax:16.0, price:20, hue:150, glow:'#57ffa8'},
{id:'violetlure',name:'Фиалковый огонёк', nameEn:'Violet Lure', rarity:3, weight:5, minDist:0.75, strength:0.80, wMin:2.0, wMax:24.0, price:38, hue:280, glow:'#d17bff'},
{id:'spiritfin', name:'Дух болота', nameEn:'Swamp Spirit', rarity:4, weight:2.5, minDist:0.88, strength:0.88, wMin:0.5, wMax:6.0, price:70, hue:45, glow:'#ffe07a'},
];
const RARITY_NAMES=['Обычная','Необычная','Редкая','Эпическая','Легендарная'];
const RARITY_NAMES_EN=['Common','Uncommon','Rare','Epic','Legendary']; // Y5: i18n — EN-названия редкостей
@@ -1124,13 +1364,13 @@ const RARITY_COLORS=['#9fc3e2','#2ecc71','#4da3ff','#c56cf0','#ffd75e'];
Этап 5: +ночные веса новых видов (zander 6 / chuch 10 / crayfish 2.5);
Этап 33: +озёрные ночные веса пяти новых видов (значения нужны ВСЕМ видам — SPECIES_NIGHT
строится как NIGHT_WEIGHTS[s.id], у пропущенного id вес стал бы undefined) */
const NIGHT_WEIGHTS={carass:40,perch:80,bream:10,carp:8,pike:22,catfish:18,sturge:4,gold:1.2,zander:6,chuch:10,crayfish:2.5,bleak:20,roundel:14,eelpout:18,tench:12,navaga:3,rudd:14,cisco:9,herring:30,mackerel:16,flounder:8,tuna:3,swordfish:1.5,moray:20,grouper:18,turtle:8,angler:6,octopus:4,manta:2.5,shark:2.5,squid:2,whale:2}; // Этап 36: +океанские id (у Океана свои таблицы — значения декоративные, главное — ключи есть)
const NIGHT_WEIGHTS={carass:40,perch:80,bream:10,carp:8,pike:22,catfish:18,sturge:4,gold:1.2,zander:6,chuch:10,crayfish:2.5,bleak:20,roundel:14,eelpout:18,tench:12,navaga:3,rudd:14,cisco:9,herring:30,mackerel:16,flounder:8,tuna:3,swordfish:1.5,moray:20,grouper:18,turtle:8,angler:6,octopus:4,manta:2.5,shark:2.5,squid:2,whale:2,glowfin:38,moonfish:24,emberfin:14,violetlure:5,spiritfin:2.5}; // Этап 36: +океанские id; Этап 45: +болотные (у Болота свои таблицы — значения декоративные, главное — ключи есть, иначе weight=undefined)
const SPECIES_NIGHT=SPECIES.map(s=>Object.assign({},s,{weight:NIGHT_WEIGHTS[s.id]}));
/* Этап 5: Локации — Река (родная вода, бесплатно) и Озёра (65536 🪙, новые рыбы).
Этап 35: +Море (131072 🪙). Этап 36: +Океан (196608 🪙).
Этап 35: +Море (131072 🪙). Этап 36: +Океан (196608 🪙). Этап 45: +Волшебное болото (90000 🪙).
У каждой локации СВОЯ ихтиофауна, без пересечений:
Река=8, Озёра=10, Море=5, Океан=9 (см. *_IDS ниже).
Река=8, Озёра=10, Море=5, Океан=9, Болото=5 (см. *_IDS ниже).
water: [dayTop, dayMid, dayBot, nightTop, nightMid, nightBot] — базовые пары цветов
градиента drawWater(): день ↔ ночь плавно через nightMix (логика не меняется).
Этап 43: визуальное различение локаций — (1) вода различима по цвету (Река/Озёра не
@@ -1138,7 +1378,8 @@ const SPECIES_NIGHT=SPECIES.map(s=>Object.assign({},s,{weight:NIGHT_WEIGHTS[s.id
6 hex на локацию — значения можно двигать); (2) waves — характер волн поверхности в
drawWater(): amp/amp2 — амплитуда, freq/freq2 — частота, speed/speed2 — скорость;
amp/amp2=0 — линии НЕ рисуются (Река — без волн, спокойная вода — ревизия Этапа 44);
Озёра — длинная медленная гладь; Море — умеренная волна; Океан — крупные зыби.
Озёра — длинная медленная гладь; Море — умеренная волна; Океан — крупные зыби;
Этап 45: Волшебное болото — слабая рябь поверх мрачной тёмно-моховой воды.
Чисто визуал, на механику/баланс не влияет. */
const LOCATIONS=[
{id:0, name:'Река', nameEn:'River', cost:0, desc:'родная вода', descEn:'home waters',
@@ -1152,7 +1393,10 @@ const LOCATIONS=[
waves:{amp:3.5,amp2:4.5,freq:0.045,freq2:0.035,speed:1.8,speed2:1.2}},
{id:3, name:'Океан', nameEn:'Ocean', cost:196608, desc:'глубоководные гиганты: синий кит, гигантский кальмар, скат-манта, гигантский осьминог и другие', descEn:'deep-sea giants: blue whale, giant squid, manta ray, giant octopus and more',
water:['#1a5fa8','#0c3d74','#031a3c','#081430','#050d22','#020611'],
waves:{amp:6,amp2:8,freq:0.035,freq2:0.028,speed:1.5,speed2:1.0}}
waves:{amp:6,amp2:8,freq:0.035,freq2:0.028,speed:1.5,speed2:1.0}},
{id:4, name:'Волшебное болото', nameEn:'Magic Swamp', cost:90000, desc:'светящиеся обитатели: светлячок, лунная рыбка, изумрудный светляк, фиалковый огонёк и дух болота', descEn:'glowing dwellers: glowfin, moonfish, emerald glowfish, violet lure and swamp spirit',
water:['#4d6b38','#2e4a28','#122818','#0d1f12','#08160c','#030a06'], // Этап 45: мрачно-болотная тёмно-зелёная вода (ночь — почти чёрная с зеленью)
waves:{amp:0.8,amp2:0.5,freq:0.025,freq2:0.02,speed:0.5,speed2:0.35}} // слабая рябь (тише Озёр)
];
/* Река — только 8 базовых видов: роллы реки остаются прежними (новые виды заходят только в Озёра).
Фильтр по id: порядок и веса записей — ровно как в SPECIES/SPECIES_NIGHT */
@@ -1194,11 +1438,27 @@ const OCEAN_DAY_WEIGHTS={moray:40,grouper:32,turtle:10,angler:8,octopus:5,manta:
const OCEAN_NIGHT_WEIGHTS={moray:26,grouper:22,turtle:11,angler:8,octopus:8,manta:5,shark:4.5,squid:4.5,whale:4};
const OCEAN_SPECIES=OCEAN_IDS.map(id=>Object.assign({},SPECIES.find(s=>s.id===id),{weight:OCEAN_DAY_WEIGHTS[id]}));
const OCEAN_SPECIES_NIGHT=OCEAN_IDS.map(id=>Object.assign({},SPECIES.find(s=>s.id===id),{weight:OCEAN_NIGHT_WEIGHTS[id]}));
/* Этап 45: Волшебное болото — только свои 5 светящихся видов (паттерн LAKE/SEA/OCEAN).
Баланс (tools/balance.mjs, нейтральная снасть): день ≈240.3 🪙, ночь ≈328.9 🪙 —
между Озёрами (194.1/249.0) и Морем (297.3/476.7) в ОБЕ фазы; ночь/день ×1.37.
Ночью доля редких/дорогих (violetlure/emberfin) выше, дешёвый glowfin и spiritfin падают.
Минимальный вес ≥2.5 (тест «каждый вид ≥1» за 3000 роллов проходит с запасом).
Значения NIGHT_WEIGHTS новых id — декоративные (у Болота своя таблица). */
const SWAMP_IDS=['glowfin','moonfish','emberfin','violetlure','spiritfin'];
const SWAMP_DAY_WEIGHTS={glowfin:3,moonfish:4,emberfin:28,violetlure:13,spiritfin:4.5};
const SWAMP_NIGHT_WEIGHTS={glowfin:2.5,moonfish:2.5,emberfin:20,violetlure:30,spiritfin:4};
const SWAMP_SPECIES=SWAMP_IDS.map(id=>Object.assign({},SPECIES.find(s=>s.id===id),{weight:SWAMP_DAY_WEIGHTS[id]}));
const SWAMP_SPECIES_NIGHT=SWAMP_IDS.map(id=>Object.assign({},SPECIES.find(s=>s.id===id),{weight:SWAMP_NIGHT_WEIGHTS[id]}));
/* Этап 45: табличный выбор таблицы ролла по save.location (0..4) — используется в pickSpecies();
guard «вне 0..4 → Река» там же. Порядок обязан совпадать с LOCATIONS */
const SPECIES_BY_LOCATION=[RIVER_SPECIES,LAKE_SPECIES,SEA_SPECIES,OCEAN_SPECIES,SWAMP_SPECIES];
const SPECIES_BY_LOCATION_NIGHT=[RIVER_SPECIES_NIGHT,LAKE_SPECIES_NIGHT,SEA_SPECIES_NIGHT,OCEAN_SPECIES_NIGHT,SWAMP_SPECIES_NIGHT];
/* Этап 37: босс в каждой локации — id босса по локации (Река=осётр как в M4,
Озёра=гигантский рак, Море=рыба-меч, Океан=синий кит). Босс — существующий вид
с runtime-флагом boss:true (паттерн M4), новых видов нет. Чтение — в startBite() */
const BOSS_BY_LOCATION={0:'sturge',1:'crayfish',2:'swordfish',3:'whale'};
с runtime-флагом boss:true (паттерн M4), новых видов нет. Чтение — в startBite()
Этап 45: Болото=дух болота (spiritfin) */
const BOSS_BY_LOCATION={0:'sturge',1:'crayfish',2:'swordfish',3:'whale',4:'spiritfin'};
/* M6: шанс gold-варианта (let — автотест форсирует значения, как раньше) */
let SHINY_CHANCE=0.06;
@@ -1624,7 +1884,7 @@ const ACHIEVEMENTS=[
{id:'gear_max', name:'Максимальная снасть', nameEn:'Max Gear', desc:'Улучши снасть до 4 уровня', descEn:'Upgrade a gear to level 4', icon:'⚒️', bonus:{coinPct:0.01}, check:s=>Object.values(s.rodLv||{}).some(v=>v>=4)||Object.values(s.lureLv||{}).some(v=>v>=4)}, // Этап 22: M5 апгрейды
{id:'titan', name:'Титан', nameEn:'Titan', desc:'Купи удочку «Титан»', descEn:'Buy the Titanium rod', icon:'💪', bonus:{coinPct:0.02}, check:s=>(s.rods||[]).indexOf(3)>=0},
{id:'species_3', name:'Ботаник', nameEn:'Botanist', desc:'Поймай 3 вида рыб', descEn:'Catch 3 fish species', icon:'📖', bonus:{coinPct:0.01}, check:s=>SPECIES.filter(sp=>caughtN(s,sp.id)>0).length>=3},
{id:'species_8', name:'Энциклопедия', nameEn:'Encyclopedia', desc:'Поймай все виды', descEn:'Catch all species', icon:'🏆', bonus:{coinPct:0.02}, check:s=>SPECIES.every(sp=>caughtN(s,sp.id)>0)}, // Этап 5: все 11 видов, этап 33: все 16, этап 35: все 23, этап 36: все 32 (id старый сейв; desc и чек — «все», масштабируются автоматически)
{id:'species_8', name:'Энциклопедия', nameEn:'Encyclopedia', desc:'Поймай все виды', descEn:'Catch all species', icon:'🏆', bonus:{coinPct:0.02}, check:s=>SPECIES.every(sp=>caughtN(s,sp.id)>0)}, // Этап 5: все 11 видов, этап 33: все 16, этап 35: все 23, этап 36: все 32, этап 45: все 37 (id старый сейв; desc и чек — «все», масштабируются автоматически)
{id:'rare_fish', name:'Редкий гость', nameEn:'Rare Guest', desc:'Поймай рыбу редкости «Редкая»+ ', descEn:'Catch a Rare+ fish', icon:'⭐', bonus:{coinPct:0.01}, check:s=>SPECIES.some(sp=>sp.rarity>=2&&caughtN(s,sp.id)>0)},
{id:'legendary_fish',name:'Легенда', nameEn:'Legend', desc:'Поймай рыбу редкости «Легендарная»', descEn:'Catch a Legendary fish', icon:'👑',bonus:{coinPct:0.02}, check:s=>SPECIES.some(sp=>sp.rarity>=4&&caughtN(s,sp.id)>0)},
{id:'first_shiny', name:'Золотая жила', nameEn:'Gold Rush', desc:'Поймай золотую рыбу', descEn:'Catch a golden fish', icon:'✨', bonus:{coinPct:0.01}, check:s=>SPECIES.some(sp=>{const c=s.caught&&s.caught[sp.id]; return c&&c.sn>0;})},
@@ -1646,12 +1906,14 @@ const ACHIEVEMENTS=[
{id:'lake_first', name:'Новые берега', nameEn:'New Shores', desc:'Открой локацию «Озёра»', descEn:'Unlock the Lakes location', icon:'🗺️', bonus:{coinPct:0.02}, check:s=>(s.locations||[]).indexOf(1)>=0}, // Этап 22: локации
{id:'sea_first', name:'Широкая вода', nameEn:'Open Water', desc:'Открой локацию «Море»', descEn:'Unlock the Sea location', icon:'⚓', bonus:{coinPct:0.02}, check:s=>(s.locations||[]).indexOf(2)>=0}, // Этап 36: локации (locations-based — пин §10c(8) achBonus не трогает)
{id:'ocean_first', name:'Глубина', nameEn:'The Deep', desc:'Открой локацию «Океан»', descEn:'Unlock the Ocean location', icon:'🌊', bonus:{coinPct:0.02}, check:s=>(s.locations||[]).indexOf(3)>=0}, // Этап 36: локации
{id:'swamp_first', name:'Тайна болота', nameEn:'Swamp Secret', desc:'Открой локацию «Волшебное болото»', descEn:'Unlock the Magic Swamp location', icon:'🗺️', bonus:{coinPct:0.02}, check:s=>(s.locations||[]).indexOf(4)>=0}, // Этап 45: локация Болото (locations-based — пин §10c(8) achBonus не трогает)
{id:'boss_first', name:'Босс-рыбак', nameEn:'Boss Angler', desc:'Победи рыбу-босса', descEn:'Defeat a boss fish', icon:'🏆', bonus:{coinPct:0.03}, check:s=>(s.bossBeaten===true)||(s.bossCount>0)}, // Этап 37: текст нейтральный — босс в каждой локации
// Этап 37: пер-босс ачивки (без coinPct — пин §10c(8) сумма achBonus().coinPct=0.16 не сдвигается;
// bonus:{} пустой — bonusText() в вкладке «Достижения» требует объект, coinPct там нет)
{id:'lake_boss', name:'Победа над Раком', nameEn:'Crayfish Slayer', desc:'Победи босса Озёр', descEn:'Defeat the Lakes boss', icon:'🗺️', bonus:{}, check:s=>!!(s.bosses&&s.bosses.crayfish)}, // Э42: иконка локации (эмодзи рыб убраны)
{id:'sea_boss', name:'Победа над Рыбой-мечом', nameEn:'Swordfish Slayer', desc:'Победи босса Моря', descEn:'Defeat the Sea boss', icon:'⚓', bonus:{}, check:s=>!!(s.bosses&&s.bosses.swordfish)}, // Э42: иконка локации (эмодзи рыб убраны)
{id:'ocean_boss', name:'Победа над Китом', nameEn:'Whale Slayer', desc:'Победи босса Океана', descEn:'Defeat the Ocean boss', icon:'🌊', bonus:{}, check:s=>!!(s.bosses&&s.bosses.whale)}, // Э42: иконка локации (эмодзи рыб убраны)
{id:'swamp_boss', name:'Победа над Духом болота', nameEn:'Spirit Slayer', desc:'Победи Духа болота', descEn:'Defeat the Swamp Spirit', icon:'🗺️', bonus:{}, check:s=>!!(s.bosses&&s.bosses.spiritfin)}, // Этап 45: босс Болота (без coinPct — пин §10c(8); bonus:{} пустой — bonusText() требует объект)
];
/* Сумма бонусов по выданным достижениям */
function achBonus(){
@@ -1772,6 +2034,11 @@ let weatherT=0; // M8: остаток секунд активного событ
let weatherGap=WEATHER_GAP_MIN+Math.random()*(WEATHER_GAP_MAX-WEATHER_GAP_MIN); // M8: сек до следующего события
let ambient=[]; // фоновые рыбы
let scenery=[]; // Этап 44: береговые силуэты на горизонте (по локации)
/* Этап 45: декор «Волшебного болота» — ОТДЕЛЬНЫЕ от scenery слои поверх воды.
Кувшинки/лилии и лягушки рисуются после воды (drawSwampDecor), поэтому dusk()
береговых силуэтов их не гасит; пересобираются в initScenery() вместе со scenery. */
let pads=[]; // кувшинки: {x,y,r,ph,bob,notch,lily,lilyColor}
let frogs=[]; // лягушки: {x,y,s,state,t,dur,jh,dir,x0,y0,x1,y1}
let clouds=[], sparkles=[];
let rain=[]; // M8: частицы дождя (визуал)
let particles=[], ripples=[];
@@ -1805,12 +2072,15 @@ const rand=(a,b)=>a+Math.random()*(b-a);
/* Этап 43: фоновые рыбы — масштаб и оттенок по локации (индекс = save.location, clamp по
той же границе, что LOCATIONS в drawWater): Река — как было (8–22), Озёра — зеленоватые,
Море — синеватые и покрупнее, Океан — редкие крупные тёмно-синие. Чисто визуал.
Этап 45: Болото — мелкая тёмно-зелёная стайка + palette цветов свечения (initFish раскладывает
цвета по рыбам, ночную ауру рисует drawAmbientFish).
Вызовы: старт (СТАРТ), смена локации в shopClick (buylocation/selectlocation) */
const AMBIENT_FISH=[
{n:9,sMin:8, sMax:22,hMin:130,hMax:220},
{n:9,sMin:10,sMax:26,hMin:120,hMax:200},
{n:8,sMin:12,sMax:32,hMin:160,hMax:230},
{n:6,sMin:18,sMax:44,hMin:180,hMax:250}
{n:6,sMin:18,sMax:44,hMin:180,hMax:250},
{n:7,sMin:12,sMax:30,hMin:120,hMax:170,palette:['#5ff2ff','#57ffa8','#d17bff','#ffe07a']}
];
function initFish(){
const f=AMBIENT_FISH[Math.max(0,Math.min(AMBIENT_FISH.length-1,save.location|0))];
@@ -1818,7 +2088,10 @@ function initFish(){
for(let i=0;i<f.n;i++){
ambient.push({
x:Math.random()*W, depth:rand(0.12,0.92), size:rand(f.sMin,f.sMax),
speed:rand(10,32), dir:Math.random()<0.5?1:-1, hue:rand(f.hMin,f.hMax), ph:rand(0,6.28)
speed:rand(10,32), dir:Math.random()<0.5?1:-1, hue:rand(f.hMin,f.hMax), ph:rand(0,6.28),
// Фаза свечения: только у Болота (есть palette) — стабильный цвет из палитры по индексу рыбы;
// у локаций 0..3 поле = null и нигде не читается
glow:f.palette?f.palette[i%f.palette.length]:null
});
}
}
@@ -1826,9 +2099,12 @@ function initFish(){
Река — береговая полоса НА ВСЮ ШИРИНУ (трава, 12–17 px, тёмная кромка у воды) + хвойный
лес, посаженный НА ЕЁ КРАЮ (дальний ряд светлее, ближний темнее), Озёра — холмы +
лиственные деревья, Море — парусник (медленно дрейфует) + чайки (летят, машут крыльями),
Океан — открытый горизонт БЕЗ берега (вода и небо). Элементы: {k:'bank',h} /
{k:'pine',layer,by?} (by — база ствола: у реки — верх кромки берега) / {k:'hill'} /
{k:'tree'} / {k:'ship',dir,v} / {k:'gull',y,v,ph}.
Океан — открытый горизонт БЕЗ берега (вода и небо). Элементы: {k:'bank',h,c?,c2?} /
{k:'pine',layer,by?,c?} (by — база ствола: у реки — верх кромки берега; c — цвет) /
{k:'hill'} / {k:'tree',c?} / {k:'ship',dir,v} / {k:'gull',y,v,ph} /
{k:'reed',h,lean,ph} (камыш) / {k:'snag',h,lean} (кривое дерево) — болотная гамма.
Этап 45: Болото дополнительно собирает отдельные слои pads[]/frogs[] (кувшинки/лилии,
лягушки) — рисуются поверх воды в drawSwampDecor(), прыжки/покачивание — updateDecor(dt).
Вызовы initScenery(): resize(), старт (в resize), смена локации в shopClick —
новую точку смены локации без initScenery() не добавлять. Дрейф парусника/чаек —
updateDecor(dt). Отрисовка drawScenery() — между drawSky() и drawWater() (базу
@@ -1836,6 +2112,8 @@ function initFish(){
function initScenery(){
const i=Math.max(0,Math.min(LOCATIONS.length-1,save.location|0));
scenery=[];
pads=[]; // Этап 45: болотный декор пересобирается вместе с силуэтами (смена локации/ресайз)
frogs=[];
if(i===0){ // Река — береговая полоса на всю ширину + лес в два ряда, посаженный на её кромку
const bankH=12+((Math.random()*6)|0); // 12–17 px
scenery.push({k:'bank',h:bankH});
@@ -1848,6 +2126,37 @@ function initScenery(){
}else if(i===2){ // Море — парусник + чайки (берега нет — открытый горизонт)
scenery.push({k:'ship',x:rand(W*0.25,W*0.8),v:rand(2.5,5),s:rand(0.8,1.15),dir:Math.random()<0.5?1:-1});
for(let k=0;k<3;k++) scenery.push({k:'gull',x:rand(0,W),y:rand(26,72),v:rand(14,26),ph:rand(0,6.28)});
}else if(i===4){ // Этап 45: Болото — мшистый задний берег: камыш + кривые деревья + туи
const bankH=10+((Math.random()*5)|0); // 10–14 px
scenery.push({k:'bank',h:bankH,c:'#3d5130',c2:'#28381f'});
for(let k=0;k<14;k++) scenery.push({k:'reed',x:rand(-12,W+12),h:rand(18,42),lean:rand(2,6),ph:rand(0,6.28)});
for(let k=0;k<6;k++) scenery.push({k:'snag',x:rand(-8,W+8),h:rand(14,30),lean:rand(-0.5,0.5)});
for(let k=0;k<8;k++) scenery.push({k:'pine',layer:0,x:rand(-12,W+12),h:rand(14,26),by:waterY-bankH,c:'#33503a'});
for(let k=0;k<6;k++) scenery.push({k:'pine',layer:1,x:rand(-12,W+12),h:rand(10,20),by:waterY-bankH,c:'#22392a'});
// Кувшинки (8–14): кувшинка = эллипс с вырезом-прорезью; на части — лилия (3 цвета)
const np=8+((Math.random()*7)|0);
for(let k=0;k<np;k++){
pads.push({
x:rand(10,W-10),
y:waterY+rand(18,Math.max(30,H-waterY-30)), // на воде, ниже линии воды
r:rand(9,17),
ph:rand(0,6.28), bob:0,
notch:rand(0,6.28), // угол выреза-прорези
lily:Math.random()<0.45,
lilyColor:['#ffe07a','#f2efe2','#ff9ec4'][(Math.random()*3)|0] // жёлтый/белый/розовый
});
}
// Лягушки (3–5): стартуют на кувшинках, прыгают по параболе на соседние (см. updateDecor)
const nf=3+((Math.random()*3)|0);
for(let k=0;k<nf;k++){
const p=pads.length?pads[(Math.random()*pads.length)|0]:null;
frogs.push({
x:p?p.x:rand(20,W-20), y:p?p.y-2:waterY+rand(30,120),
s:rand(4.5,7),
state:'sit', t:rand(0,3), dur:0.5, jh:14, dir:1,
x0:0,y0:0,x1:0,y1:0
});
}
}
// i===3 — Океан: берега нет вообще (открытая вода)
}
@@ -1908,10 +2217,14 @@ function pickSpecies(dist){
const l=lure();
// M3: ночь/вечер — ночная таблица, иначе дневная (жёсткий переключатель)
// Этап 5: локация — Река (8 видов) или Озёра (все 16, этап 33); логика ролла по весам без изменений
// Этап 35: локации — у каждой СВОИ виды (без пересечений); Этап 36: +Океан → четыре локации: Река 8 / Озёра 10 / Море 5 / Океан 9
const ocean=save.location===3, sea=save.location===2, lake=save.location===1;
// Этап 35: локации — у каждой СВОИ виды (без пересечений); Этап 36: +Океан; Этап 45: +Болото →
// пять локаций: Река 8 / Озёра 10 / Море 5 / Океан 9 / Болото 5.
// Этап 45: табличный доступ по save.location вместо вложенного тернарника (роллы 0..3 идентичны);
// guard: вне диапазона 0..4 (битый сейв/тест) — Река, как было раньше по умолчанию
const night=(dayPhase==='night'||dayPhase==='evening');
const tbl=ocean?(night?OCEAN_SPECIES_NIGHT:OCEAN_SPECIES):(sea?(night?SEA_SPECIES_NIGHT:SEA_SPECIES):(lake?(night?LAKE_SPECIES_NIGHT:LAKE_SPECIES):(night?RIVER_SPECIES_NIGHT:RIVER_SPECIES)));
const li=save.location|0;
const idx=(li>=0&&li<SPECIES_BY_LOCATION.length)?li:0;
const tbl=night?SPECIES_BY_LOCATION_NIGHT[idx]:SPECIES_BY_LOCATION[idx];
let total=0;
const ws=tbl.map(s=>{
let w=s.weight;
@@ -2244,6 +2557,38 @@ function updateDecor(dt){
if(o.k==='ship'){ o.x+=o.dir*o.v*dt; if(o.dir>0&&o.x>W+40) o.x=-40; if(o.dir<0&&o.x<-40) o.x=W+40; }
else if(o.k==='gull'){ o.x+=o.v*dt; if(o.x>W+30) o.x=-30; }
}
// Этап 45: Болото — покачивание кувшинок (sin, амплитуда 1–2 px) и прыжки лягушек.
// Лягушка сидит (таймер 3–7 с) → параболический прыжок (t 0..1) на соседнюю кувшинку → садится.
for(const p of pads) p.bob=Math.sin(time*1.1+p.ph)*1.6;
for(const fr of frogs){
if(fr.state==='jump'){
fr.t+=dt/fr.dur;
if(fr.t>=1){ // приземлилась
fr.x=fr.x1; fr.y=fr.y1; fr.state='sit'; fr.t=rand(3,7);
}else{
const k=fr.t;
fr.x=fr.x0+(fr.x1-fr.x0)*k;
fr.y=fr.y0+(fr.y1-fr.y0)*k-Math.sin(Math.PI*k)*fr.jh; // парабола dx/dy
}
}else{
fr.t-=dt;
if(fr.t<=0){ // выбор цели: ближайшая ДРУГАЯ кувшинка, иначе случайная точка воды
let tgt=null,best=1e9;
for(const p of pads){
const d=(p.x-fr.x)*(p.x-fr.x)+(p.y-fr.y)*(p.y-fr.y);
if(d>16&&d<best){ best=d; tgt=p; }
}
fr.x0=fr.x; fr.y0=fr.y;
if(tgt&&Math.random()<0.85){ fr.x1=tgt.x; fr.y1=tgt.y-2; }
else{
fr.x1=Math.max(20,Math.min(W-20,fr.x+rand(-90,90)));
fr.y1=Math.min(H-24,Math.max(waterY+24,fr.y+rand(-24,36)));
}
fr.dur=rand(0.4,0.7); fr.jh=rand(10,22); fr.t=0; fr.state='jump';
fr.dir=fr.x1>=fr.x0?1:-1;
}
}
}
// M8: дождь — полоски летят вниз-влево, ушедшие за нижний край возвращаются сверху
if(weatherActive()==='rain'){
for(const dr of rain){
@@ -2401,16 +2746,18 @@ function drawScenery(){
for(const o of scenery){
if(o.k==='bank'){
// Река: береговая полоса на всю ширину (трава + тёмная влажная кромка у воды);
// рисуется ПЕРВОЙ (в scenery стоит первой) — лес сажается на её верхнюю кромку
ctx.fillStyle=dusk('#4e7a4e');
// рисуется ПЕРВОЙ (в scenery стоит первой) — лес сажается на её верхнюю кромку.
// Этап 45: у Болота — своя мшистая гамма (c/c2), у остальных локаций цвета те же
ctx.fillStyle=dusk(o.c||'#4e7a4e');
ctx.fillRect(0,waterY-o.h,W,o.h+3);
ctx.fillStyle=dusk('#39583c');
ctx.fillStyle=dusk(o.c2||'#39583c');
ctx.fillRect(0,waterY-3,W,6);
}else if(o.k==='pine'){
// сосна: ствол + 3 яруса (дальний ряд — светлее и выше, ближний — темнее);
// база ствола — o.by (у реки — верх кромки берега), по умолчанию линия воды
// база ствола — o.by (у реки — верх кромки берега), по умолчанию линия воды;
// Этап 45: цвет o.c — болотные туи темнее речных сосен
const pb=o.by==null?y0:o.by;
ctx.fillStyle=o.layer?dusk('#24452f'):dusk('#3a6a4a');
ctx.fillStyle=dusk(o.c||(o.layer?'#24452f':'#3a6a4a'));
ctx.fillRect(o.x-1.2,pb-3,2.4,4);
for(let t=0;t<3;t++){
const by=pb-3-t*o.h*0.26, hw=o.h*0.3*(1-0.3*t), th=o.h*0.42;
@@ -2422,14 +2769,40 @@ function drawScenery(){
ctx.fillStyle=dusk('#4a7a5e');
ctx.beginPath(); ctx.ellipse(o.x,y0+4,o.rx,o.ry,0,Math.PI,0); ctx.fill();
}else if(o.k==='tree'){
// лиственное дерево: ствол + крона из трёх кругов
ctx.fillStyle=dusk('#3c6b4f');
// лиственное дерево: ствол + крона из трёх кругов (o.c — опциональный цвет)
ctx.fillStyle=dusk(o.c||'#3c6b4f');
ctx.fillRect(o.x-1,y0-o.h*0.5,2,o.h*0.5+1);
ctx.beginPath();
ctx.arc(o.x,y0-o.h,o.h*0.42,0,7);
ctx.arc(o.x-o.h*0.3,y0-o.h*0.78,o.h*0.34,0,7);
ctx.arc(o.x+o.h*0.3,y0-o.h*0.78,o.h*0.34,0,7);
ctx.fill();
}else if(o.k==='reed'){
// Этап 45: камыш — пучок тонких стеблей с метёлками, качается на ветру (болотная гамма)
const sway=Math.sin(time*0.9+o.ph)*o.lean;
for(let t=0;t<4;t++){
const off=(t-1.5)*2.2;
const h=o.h*(0.72+0.07*t);
const tx=o.x+off+sway*(0.6+t*0.14), ty=y0-h;
ctx.strokeStyle=dusk('#3f5a2e'); ctx.lineWidth=1.2;
ctx.beginPath(); ctx.moveTo(o.x+off,y0);
ctx.quadraticCurveTo(o.x+off+sway*0.4,y0-h*0.6,tx,ty); ctx.stroke();
ctx.fillStyle=dusk('#7a6a35'); // метёлка
ctx.beginPath(); ctx.ellipse(tx,ty,1.7,4.5,sway*0.06,0,7); ctx.fill();
}
}else if(o.k==='snag'){
// Этап 45: кривое болотное дерево — наклонный ствол + пара голых веток
const ln=o.lean||0;
ctx.save(); // не оставляем lineCap/lineWidth остальным слоям кадра
ctx.strokeStyle=dusk('#5a5340'); ctx.lineWidth=2.4; ctx.lineCap='round';
ctx.beginPath(); ctx.moveTo(o.x,y0);
ctx.quadraticCurveTo(o.x+ln*o.h*0.5,y0-o.h*0.55,o.x+ln*o.h,y0-o.h); ctx.stroke();
ctx.lineWidth=1.6;
ctx.beginPath(); ctx.moveTo(o.x+ln*o.h*0.25,y0-o.h*0.5);
ctx.lineTo(o.x+ln*o.h*0.25-o.h*0.3,y0-o.h*0.62); ctx.stroke();
ctx.beginPath(); ctx.moveTo(o.x+ln*o.h*0.4,y0-o.h*0.72);
ctx.lineTo(o.x+ln*o.h*0.4+o.h*0.26,y0-o.h*0.8); ctx.stroke();
ctx.restore();
}else if(o.k==='ship'){
// парусник: киль/мачта/два паруса (парус светлее силуэта — читается на небе)
ctx.save(); ctx.translate(o.x,0); ctx.scale(o.dir,1);
@@ -2514,6 +2887,32 @@ function drawWater(){
ctx.stroke();
}
}
/* Фаза свечения «Волшебного болота»: аура-свечение (радиальный градиент + 'lighter').
rgba-строки цветов кэшируются по hex — строки не собираются каждый кадр (мобильные аллокации).
Сам градиент позиционный, поэтому создаётся на отрисовку (как боссовая аура), но с save/restore. */
const GLOW_RGBA={}; // hex -> {in:'rgba(...,0.42)',out:'rgba(...,0)'}
function glowColors(hex){
let c=GLOW_RGBA[hex];
if(!c){
const h=hex.replace('#','');
const r=parseInt(h.substr(0,2),16),g=parseInt(h.substr(2,2),16),b=parseInt(h.substr(4,2),16);
c=GLOW_RGBA[hex]={in:'rgba('+r+','+g+','+b+',0.42)',out:'rgba('+r+','+g+','+b+',0)'};
}
return c;
}
function drawGlowAura(x,y,r,hex){
const c=glowColors(hex);
ctx.save(); // изолируем 'lighter' и fillStyle — не ломаем остальную отрисовку
ctx.globalCompositeOperation='lighter';
const g=ctx.createRadialGradient(x,y,r*0.12,x,y,r);
g.addColorStop(0,c.in); g.addColorStop(1,c.out);
ctx.fillStyle=g; ctx.fillRect(x-r,y-r,r*2,r*2);
ctx.restore();
}
// Аура светящегося вида только ночью (общий порог nightMix()>0.35, как у фонаря лодки)
function fishGlow(sp,x,y,r){
if(sp&&sp.glow&&nightMix()>0.35) drawGlowAura(x,y,r,sp.glow);
}
function drawFishShape(x,y,size,dir,hue,alpha,wob){
ctx.save(); ctx.translate(x,y); ctx.scale(dir,1);
const w=wob||0;
@@ -2533,11 +2932,68 @@ function drawFishShape(x,y,size,dir,hue,alpha,wob){
ctx.restore();
}
function drawAmbientFish(){
const swampGlow=save.location===4&&nightMix()>0.35; // свечение фоновых рыб — только Болото ночью
for(const f of ambient){
const y=waterY+20+f.depth*(H-waterY-50)+Math.sin(time*2+f.ph)*2;
if(swampGlow&&f.glow) drawGlowAura(f.x,y,f.size*2.6,f.glow); // цвет каждой рыбы из palette
drawFishShape(f.x,y,f.size,f.dir,f.hue,0.22+0.2*(1-f.depth),Math.sin(time*5+f.ph)*0.28);
}
}
/* Этап 45: декор «Волшебного болота» — кувшинки с лилиями и прыгающие лягушки.
Отдельный слой ПОВЕРХ воды (вызов в draw() после drawWater/drawAmbientFish, до drawBoat):
рисуется после воды, поэтому dusk() берега его не гасит. К ночи декор лишь притухает
(hexLerp к тёмно-зелёному), днём — живой, а ночное свечение рыб (B2) остаётся видимым.
Фигуры простые (arc/ellipse/lineTo) без shadowBlur и градиентов на кадр. */
function drawSwampDecor(){
if(!pads.length&&!frogs.length) return;
const nm=nightMix();
const dusk=c=>hexLerp(c,'#0b1a12',nm*0.55);
// --- кувшинки (+ лилии на части из них) ---
for(const p of pads){
const x=p.x, y=p.y+p.bob;
// кувшинка: эллипс с радиальным вырезом-прорезью (Pac-Man-образный клин к центру)
ctx.fillStyle=dusk('#2f6b3a');
ctx.strokeStyle=dusk('#1d4527'); ctx.lineWidth=1;
ctx.beginPath();
ctx.moveTo(x,y);
ctx.arc(x,y,p.r,p.notch+0.24,p.notch-0.24+Math.PI*2);
ctx.closePath(); ctx.fill(); ctx.stroke();
// блик на листе (визуал)
ctx.fillStyle=dusk('#3f7f4a');
ctx.beginPath(); ctx.ellipse(x-p.r*0.28,y-p.r*0.22,p.r*0.42,p.r*0.2,-0.3,0,7); ctx.fill();
// лилия: 6 лепестков-эллипсов одного из трёх цветов (жёлтый/белый/розовый)
if(p.lily){
const lx=x+p.r*0.28, ly=y-p.r*0.26;
ctx.fillStyle=dusk(p.lilyColor);
for(let k=0;k<6;k++){
const a=k/6*Math.PI*2+p.ph*0.2;
ctx.beginPath();
ctx.ellipse(lx+Math.cos(a)*3.2,ly+Math.sin(a)*2.4,3.4,1.9,a,0,7);
ctx.fill();
}
}
}
// --- лягушки (тело + задние лапы, тёмно-зелёные; прыгают по параболе из updateDecor) ---
for(const fr of frogs){
const s=fr.s, dir=fr.dir||1;
ctx.save(); ctx.translate(fr.x,fr.y); ctx.scale(dir,1);
ctx.fillStyle=dusk('#2f5a2a');
// задние лапы
ctx.beginPath(); ctx.ellipse(-s*0.55,s*0.55,s*0.72,s*0.34,-0.4,0,7); ctx.fill();
ctx.beginPath(); ctx.ellipse(s*0.1,s*0.72,s*0.78,s*0.3,0.35,0,7); ctx.fill();
// тело
ctx.beginPath(); ctx.ellipse(0,0,s,s*0.72,0,0,7); ctx.fill();
// голова + глаз
ctx.fillStyle=dusk('#3f7a38');
ctx.beginPath(); ctx.ellipse(s*0.68,-s*0.28,s*0.46,s*0.4,0,0,7); ctx.fill();
ctx.fillStyle='rgba(10,25,10,0.9)';
ctx.beginPath(); ctx.arc(s*0.84,-s*0.4,s*0.13,0,7); ctx.fill();
// передние лапы
ctx.fillStyle=dusk('#2f5a2a');
ctx.beginPath(); ctx.ellipse(s*0.5,s*0.5,s*0.5,s*0.2,0.5,0,7); ctx.fill();
ctx.restore();
}
}
function drawBoat(){
const bx=boatX, by=waterY+Math.sin(time*1.5)*1.5;
// блик-отражение под лодкой (визуал)
@@ -2741,6 +3197,7 @@ function drawHookedFish(){
gg.addColorStop(0,'rgba(255,215,94,0.35)'); gg.addColorStop(1,'rgba(255,215,94,0)');
ctx.fillStyle=gg; ctx.fillRect(x-fs*2.6,y-fs*2.6,fs*5.2,fs*5.2);
}
fishGlow(sp,x,y,fs*2.6); // Фаза свечения: аура вида с glow ночью (внутри save/restore — не ломает боссовую)
drawFishShape(x,y,fs,dir,sp.hue,0.75,Math.sin(time*9*k)*0.3*k); // силуэт крупнее у тяжёлых (по весу)
}
function drawParticles(){
@@ -2931,16 +3388,25 @@ function draw(){
drawScenery(); // Этап 44: береговые силуэты (перед водой — базу закрывает градиент)
drawWater();
drawAmbientFish();
drawSwampDecor(); // Этап 45: кувшинки/лилии/лягушки Болота — поверх воды, до лодки
if(biteFish&&biteFish.mode!=='out'){
const bs=12+biteFish.sp.rarity*2.5; // размер силуэта зацепившейся рыбы
if(state==='BITE'){
// топление (lane 2): рыба дёргается у крючка, синхронно с фазами топления поплавка (time*22)
drawFishShape(hookPos.x+Math.sin(time*25)*7, hookPos.y+Math.cos(time*22)*5, 12+biteFish.sp.rarity*2.5, Math.sin(time*25)>0?1:-1, biteFish.sp.hue, 0.8, Math.sin(time*8)*0.25);
const bx=hookPos.x+Math.sin(time*25)*7, by=hookPos.y+Math.cos(time*22)*5;
fishGlow(biteFish.sp,bx,by,bs*2.6); // Фаза свечения: аура вида ночью
drawFishShape(bx,by,bs, Math.sin(time*25)>0?1:-1, biteFish.sp.hue, 0.8, Math.sin(time*8)*0.25);
}else{
// APPROACH: подплывает к поплавку как раньше
drawFishShape(biteFish.x,biteFish.y,12+biteFish.sp.rarity*2.5,biteFish.dir,biteFish.sp.hue,0.8,Math.sin(time*8)*0.25);
fishGlow(biteFish.sp,biteFish.x,biteFish.y,bs*2.6);
drawFishShape(biteFish.x,biteFish.y,bs,biteFish.dir,biteFish.sp.hue,0.8,Math.sin(time*8)*0.25);
}
}
else if(biteFish&&biteFish.mode==='out') drawFishShape(biteFish.x,biteFish.y,12+biteFish.sp.rarity*2.5,biteFish.dir,biteFish.sp.hue,0.5,Math.sin(time*8)*0.25);
else if(biteFish&&biteFish.mode==='out'){ // рыба ушла — светящийся силуэт гаснет, но аура ещё видна
const bs=12+biteFish.sp.rarity*2.5;
fishGlow(biteFish.sp,biteFish.x,biteFish.y,bs*2.6);
drawFishShape(biteFish.x,biteFish.y,bs,biteFish.dir,biteFish.sp.hue,0.5,Math.sin(time*8)*0.25);
}
drawBoat();
drawLineAndBobber();
drawHookedFish();
@@ -3114,6 +3580,7 @@ const T={
welcome_lakes:'Добро пожаловать на Озёра!',
welcome_sea:'Добро пожаловать в Море!',
welcome_ocean:'Добро пожаловать в Океан!',
welcome_swamp:'Добро пожаловать в Волшебное болото! Здесь всё светится...',
location_set:'Локация: {name}',
progress_reset:'Прогресс сброшен 🔄',
night_light:'Ночью нужен фонарик — улучши удилище (первый апгрейд)',
@@ -3211,6 +3678,7 @@ const T={
welcome_lakes:'Welcome to the Lakes!',
welcome_sea:'Welcome to the Sea!',
welcome_ocean:'Welcome to the Ocean!',
welcome_swamp:'Welcome to the Magic Swamp! Everything glows here...',
location_set:'Location: {name}',
progress_reset:'Progress reset 🔄',
night_light:'Night needs a flashlight — upgrade your rod (first upgrade)',
@@ -3508,7 +3976,9 @@ function renderShop(){
const nm=c.n-spKeys.reduce((a,k)=>a+(c[k]||0),0);
// M6: мини-счётчики вариантов (обычная + все особые по лестнице VARIANTS);
// Y5: i18n — подписи вариантов из VARIANTS через trName
h+='<div class="jrow"><span class="je"><svg viewBox="0 0 160 100"><use href="#fish-'+s.id+'"/></svg></span><span class="jn"><b>'+trName(s)+'</b>'+
// Фаза свечения: светящийся вид — маркер-класс + drop-shadow цвета glow на портрете
const glowAttr=s.glow?' class="glow-fish" style="filter:drop-shadow(0 0 6px '+s.glow+')"':'';
h+='<div class="jrow"><span class="je"><svg'+glowAttr+' viewBox="0 0 160 100"><use href="#fish-'+s.id+'"/></svg></span><span class="jn"><b>'+trName(s)+'</b>'+
'<span class="jv jv-n" title="'+trName(VARIANTS[0])+'">'+vc(nm)+'</span>'+
spKeys.map(k=>{const v=VARIANTS.find(x=>VARIANT_KEY[x.id]===k);
return '<span class="jv jv-'+k+'" title="'+trName(v)+'">'+vc(c[k])+'</span>';}).join('')+'<br>'+
@@ -3583,7 +4053,14 @@ function shopClick(e){
initFish(); // Этап 43: фоновые рыбы по новой воде
initScenery(); // Этап 44: силуэты новой воды
persist(); updateHUD(); sfx('coin'); renderShop();
toast(loc.id===1?t('welcome_lakes'):(loc.id===2?t('welcome_sea'):(loc.id===3?t('welcome_ocean'):tt('bought',{name:trName(loc)})))); // Этап 35/36: у каждой локации свой тост
// Этап 35/36/45: у каждой локации свой тост (id 0 «Река» не покупается — общий «Куплено»)
let buyToast;
if(loc.id===1) buyToast=t('welcome_lakes');
else if(loc.id===2) buyToast=t('welcome_sea');
else if(loc.id===3) buyToast=t('welcome_ocean');
else if(loc.id===4) buyToast=t('welcome_swamp');
else buyToast=tt('bought',{name:trName(loc)});
toast(buyToast);
}else{
if(save.locations.indexOf(i)<0){ sfx('no'); return; } // не куплено — выбрать нельзя (паттерн защиты buy)
save.location=i;
@@ -3659,6 +4136,12 @@ function showCard(sp,w,val,rec,variant,boss){
// M4: босс: бейдж 🏆 + золотое оформление карточки
$('cardBoss').classList.toggle('hidden',!boss);
$('card').classList.toggle('boss-card',!!boss);
// Фаза свечения: светящийся вид (glow) в normal-варианте — цветной drop-shadow портрета;
// CSS-переменная --glow читается правилом #card.glow-card .bigemoji. Свечение — оформление
// вида (не зависит от времени суток), вариант normal — чтобы не спорить с фильтрами вариантов
const glowOn=!!sp.glow&&variant.id==='normal';
$('card').classList.toggle('glow-card',glowOn);
if(sp.glow) $('card').style.setProperty('--glow',sp.glow);
// Y2: rewarded «монеты ×2» — бонус = полная цена улова (rvVal), сброс флага
rvVal=val; rvDone=false;
$('btnRv').classList.remove('hidden');
Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 192 KiB

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 179 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 193 KiB

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 75 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 198 KiB

After

Width:  |  Height:  |  Size: 195 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 KiB

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 302 KiB

After

Width:  |  Height:  |  Size: 308 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 238 KiB

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 269 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 123 KiB

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 129 KiB

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 254 KiB

After

Width:  |  Height:  |  Size: 261 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 368 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 285 KiB

After

Width:  |  Height:  |  Size: 287 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 323 KiB

After

Width:  |  Height:  |  Size: 317 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 262 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 150 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 295 KiB

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 146 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 156 KiB

After

Width:  |  Height:  |  Size: 163 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 286 KiB

After

Width:  |  Height:  |  Size: 289 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 189 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 KiB

After

Width:  |  Height:  |  Size: 197 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 177 KiB

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 200 KiB

After

Width:  |  Height:  |  Size: 201 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 78 KiB

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 195 KiB

After

Width:  |  Height:  |  Size: 196 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 80 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 260 KiB

After

Width:  |  Height:  |  Size: 255 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 308 KiB

After

Width:  |  Height:  |  Size: 312 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 239 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 271 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 132 KiB

After

Width:  |  Height:  |  Size: 137 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 125 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 258 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 156 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 114 KiB

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 136 KiB

After

Width:  |  Height:  |  Size: 133 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 296 KiB

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 310 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 263 KiB

After

Width:  |  Height:  |  Size: 264 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 305 KiB

After

Width:  |  Height:  |  Size: 306 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 144 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 163 KiB

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 298 KiB

After

Width:  |  Height:  |  Size: 298 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 152 KiB

After

Width:  |  Height:  |  Size: 156 KiB

+20 -5
View File
@@ -3,8 +3,9 @@
* Генерация скриншотов «Рыбалки» — витринный набор (страница вики «Скриншоты»,
* материалы для Яндекс Игр).
*
* Снимает 12 состояний игры (меню, лавка, достижения, дневник, ожидание дня,
* дождь, ночь, Озёра, Море, Океан, вываживание, карточка улова) на трёх вьюпортах:
* Снимает 14 состояний игры (меню, лавка, достижения, дневник, ожидание дня,
* дождь, ночь, Озёра, Море, Океан, Волшебное болото — день и ночь, вываживание,
* карточка улова) на трёх вьюпортах:
* - desk: 1280x720 @1x (десктоп, 16:9)
* - mobp: 360x640 @2x -> 720x1280 (мобильный, портрет, 9:16)
* - mobl: 800x450 @1.6x -> 1280x720 (мобильный, ландшафт, 16:9)
@@ -39,7 +40,7 @@
* Зависит от тест-глобалов игры (AGENTS.md, «Подводные камни»):
* state, forceBite, forceWeather, biteTimer, reel, catchFish, SHINY_CHANCE,
* SPECIES, setDayTime, save, dayTime, dayPhase, forceLang, setLang, initFish (Этап 43),
* initScenery (Этап 44 — силуэты берега в кадрах lake/sea/ocean)
* initScenery (Этап 44 — силуэты берега в кадрах lake/sea/ocean; Этап 45 — swamp/swamp-night)
* и DOM-кнопок: #btnPlay, #btnShop, #tabAch, #tabJournal, #btnCloseShop,
* #btnCardOk, #card, #shop, #menu.
*/
@@ -59,7 +60,7 @@ const VIEWPORTS = [
{ key: 'mobp', w: 360, h: 640, dsf: 2 },
{ key: 'mobl', w: 800, h: 450, dsf: 1.6 },
];
const FRAMES = ['menu', 'shop', 'ach', 'journal', 'wait', 'weather-rain', 'night', 'lake', 'sea', 'ocean', 'reel', 'card']; // Этап 43: +кадры Море/Океан (визуальное различение локаций)
const FRAMES = ['menu', 'shop', 'ach', 'journal', 'wait', 'weather-rain', 'night', 'lake', 'sea', 'ocean', 'swamp', 'swamp-night', 'reel', 'card']; // Этап 43: +кадры Море/Океан (визуальное различение локаций); Этап 45: +кадры Волшебного болота (день/ночь со свечением)
// ---------- 24-битный PNG (без альфа): ffmpeg предпочтительнее, иначе ImageMagick
function findBin(name) {
@@ -201,7 +202,21 @@ async function captureSet(browser, vp) {
await page.evaluate(() => { save.locations = [0, 1, 2, 3]; save.location = 3; initFish(); initScenery(); setDayTime(0.3); });
await waitPaint(page);
await shoot(page, `${vp.key}-ocean.png`);
await page.evaluate(() => { save.location = 0; save.locations = [0]; initFish(); initScenery(); });
// Волшебное болото — save.location=4, день, dayTime 0.3 (Этап 45: мрачная моховая вода,
// камыш/кривые деревья, кувшинки/лилии, прыгающие лягушки; свечения нет — nightMix()<=0.35)
await page.evaluate(() => { save.locations = [0, 1, 2, 3, 4]; save.location = 4; initFish(); initScenery(); setDayTime(0.3); });
await waitPaint(page);
await shoot(page, `${vp.key}-swamp.png`);
// Волшебное болото — ночь, dayTime 0.85 (Этап 45: светящиеся рыбы — nightMix()>0.35,
// ауры цветом glow каждого вида/палитры)
await page.evaluate(() => { setDayTime(0.85); });
await waitPaint(page);
await shoot(page, `${vp.key}-swamp-night.png`);
// сброс на Реку, день (dayTime возвращаем после ночного болотного кадра)
await page.evaluate(() => { save.location = 0; save.locations = [0]; initFish(); initScenery(); setDayTime(0.3); });
// вываживание: recall → заброс + клёв + подсечка → REEL
// biteTimer сбрасывается при завершении CAST, поэтому ставим его после заброса
+139 -31
View File
@@ -812,13 +812,14 @@ async function toGameMenu(p) {
pickSpecies = window.__m4Pick;
});
// --- 9k2. Этап 37: босс в каждой локации — Озёра (рак) / Море (рыба-меч) / Океан (кит) ---
// --- 9k2. Этап 37: босс в каждой локации — Озёра (рак) / Море (рыба-меч) / Океан (кит) / Болото (дух, Этап 45) ---
// Паттерн §9k: monkey-patch pickSpecies + forceBoss + save.location; единое правило веса:
// любой босс 20–50 кг (hook), награда ×5, save.bosses[id]=true, пер-босс ачивка (без coinPct)
const b37list = [
{ loc: 1, id: 'crayfish', ach: 'lake_boss', name: 'Гигантский рак' },
{ loc: 2, id: 'swordfish', ach: 'sea_boss', name: 'Рыба-меч' },
{ loc: 3, id: 'whale', ach: 'ocean_boss', name: 'Синий кит' },
{ loc: 4, id: 'spiritfin', ach: 'swamp_boss', name: 'Дух болота' }, // Этап 45: Волшебное болото
];
await page.evaluate(() => { window.__b37Pick = pickSpecies; });
for (const b37c of b37list) {
@@ -827,6 +828,9 @@ async function toGameMenu(p) {
forceBoss = true; SHINY_CHANCE = 0;
pickSpecies = () => SPECIES.find(s => s.id === c.id);
save.location = c.loc;
// ВАЖНО: save.locations не трогаем — doCast() зовёт checkAchievements(), и «купленная»
// локация выдала бы lake/sea/ocean/swamp_first (+2% coinPct) ещё до catchFish →
// проверка награды босса (±10%) поплыла бы. Боссу достаточно save.location (forceBoss).
save.ach = {}; save.bosses = {}; save.bossBeaten = false; save.bossCount = 0;
save.caught = {};
save.quests = { date: todayStr(), list: [] };
@@ -1645,8 +1649,11 @@ async function toGameMenu(p) {
&& Object.values(m6card).every(r => r.href === '#fish-carass'),
JSON.stringify(m6card));
// (8) full_variants: все 8 видов × 8 вариантов → +5000 ровно один раз, coinPct +0.05, повторно — без выплаты
// (8) full_variants: все виды во всех 8 вариантах → +5000 ровно один раз, coinPct, повторно — без выплаты
// Этап 45: в сумму coinPct входит swamp_first (локация 4) — пин 0.16→0.18
const m6fv = await page.evaluate(() => {
const locSnap = save.locations.slice();
save.locations = locSnap.indexOf(4) >= 0 ? locSnap : locSnap.concat([4]); // Этап 45: swamp_first даёт +0.02
save.ach = {};
save.casts = 0; save.earned = 0; save.snaps = 0; save.questsDoneTotal = 0;
save.bossBeaten = false; save.bossCount = 0;
@@ -1658,12 +1665,13 @@ async function toGameMenu(p) {
const a1 = { coins: save.coins, pct: achBonus().coinPct, got: !!save.ach.full_variants };
checkAchievements();
const a2 = { coins: save.coins };
save.locations = locSnap; // не загрязнять следующие тесты
return { coinsBefore, a1, a2 };
});
ok('M6 full_variants: все 8 видов во всех 8 вариантах → +5000 ровно один раз, achBonus().coinPct включает 0.05 (сумма 0.16: +first_silver/first_rainbow, Этап 22), повторный вызов — без выплаты',
ok('M6 full_variants: все виды во всех 8 вариантах → +5000 ровно один раз, achBonus().coinPct включает 0.05 (сумма 0.18: +first_silver/first_rainbow, Этап 22, + swamp_first, Этап 45), повторный вызов — без выплаты',
m6fv.a1.got === true
&& m6fv.a1.coins === m6fv.coinsBefore + 5000
&& Math.abs(m6fv.a1.pct - 0.16) < 1e-9
&& Math.abs(m6fv.a1.pct - 0.18) < 1e-9
&& m6fv.a2.coins === m6fv.a1.coins,
JSON.stringify(m6fv));
@@ -1952,7 +1960,7 @@ async function toGameMenu(p) {
});
// --- 10f. Локации: Озёра + Море + Океан (Этап 35: у каждой локации только свои виды, Этап 36: +Океан) ---
// (1) данные: 32 вида в SPECIES (8 речных + 10 озёрных + 5 морских + 9 океанских), LOCATIONS (4 записи, по 6 hex в water)
// (1) данные: 37 видов в SPECIES (8 речных + 10 озёрных + 5 морских + 9 океанских + 5 болотных, Этап 45), LOCATIONS (5 записей, по 6 hex в water)
const lc1 = await page.evaluate(() => {
const g = id => SPECIES.find(s => s.id === id);
const z = g('zander'), c = g('chuch'), k = g('crayfish');
@@ -1961,6 +1969,8 @@ async function toGameMenu(p) {
const h = g('herring'), m = g('mackerel'), f = g('flounder'), tu = g('tuna'), sw = g('swordfish');
const mo = g('moray'), gr = g('grouper'), turt = g('turtle'), an = g('angler'), oc = g('octopus');
const ma = g('manta'), sh = g('shark'), sq = g('squid'), wh = g('whale');
// Этап 45: болотные виды (5 — в КОНЦЕ SPECIES, у каждого поле glow)
const gf = g('glowfin'), mf = g('moonfish'), ef = g('emberfin'), vl = g('violetlure'), sf = g('spiritfin');
const p = s => s ? [s.name, s.rarity, s.price, s.wMin, s.wMax].join(',') : 'нет';
return {
n: SPECIES.length, z: p(z), c: p(c), k: p(k),
@@ -1968,16 +1978,22 @@ async function toGameMenu(p) {
rd: p(rd), ci: p(ci), h: p(h), m: p(m), f: p(f), tu: p(tu), sw: p(sw),
mo: p(mo), gr: p(gr), turt: p(turt), an: p(an), oc: p(oc),
ma: p(ma), sh: p(sh), sq: p(sq), wh: p(wh),
gf: p(gf), mf: p(mf), ef: p(ef), vl: p(vl), sf: p(sf),
// порядок: 0..7 (река) не сдвигаются, новые виды — только в КОНЦЕ массива
// (Этап 45: океан — явный slice(23,32), иначе swamp-виды попали бы в oceanOrder)
first8: SPECIES.slice(0, 8).map(s => s.id).join(','),
lakeOrder: SPECIES.slice(8, 18).map(s => s.id).join(','),
seaOrder: SPECIES.slice(18, 23).map(s => s.id).join(','),
oceanOrder: SPECIES.slice(23).map(s => s.id).join(','),
oceanOrder: SPECIES.slice(23, 32).map(s => s.id).join(','),
swampOrder: SPECIES.slice(32).map(s => s.id).join(','),
// у каждого вида есть обязательные поля (nameEn — для EN-ветки trName; поле emoji убрано в этапе 42 — портреты из SVG-спрайта)
fieldsOk: SPECIES.every(s => s.id && s.name && s.nameEn && typeof s.rarity === 'number' &&
typeof s.weight === 'number' && typeof s.minDist === 'number' && typeof s.strength === 'number' &&
typeof s.wMin === 'number' && typeof s.wMax === 'number' && s.wMax > s.wMin &&
typeof s.price === 'number' && typeof s.hue === 'number'),
// Этап 45: у новых болотных видов glow — строка #rrggbb, редкости ровно 0..4 по одной
swampGlow: [gf, mf, ef, vl, sf].every(s => s && typeof s.glow === 'string' && /^#[0-9a-fA-F]{6}$/.test(s.glow)),
swampRar: [gf, mf, ef, vl, sf].map(s => s && s.rarity).join(','),
locs: LOCATIONS.length,
costs: LOCATIONS.map(l => l.cost).join(','),
names: LOCATIONS.map(l => l.name).join(','),
@@ -1988,8 +2004,8 @@ async function toGameMenu(p) {
spriteIdsOk: SPECIES.every(s => document.getElementById('fish-' + s.id) !== null)
};
});
ok('локации данные: SPECIES=32 (8 река + 10 озёра + 5 море + 9 океан, этапы 35/36) — zander «Судак» r2 8🪙, crayfish r3 40🪙, navaga r3 50🪙; озёрные новинки rudd «Краснопёрка» r1 5🪙, cisco «Сиг» r2 18🪙; морские herring «Сельдь» r0 8🪙, mackerel «Скумбрия» r1 18🪙, flounder «Камбала» r2 33🪙, tuna «Тунец» r3 46🪙, swordfish «Рыба-меч» r4 81🪙; океанские moray «Гигантская мурена» r0 5🪙, grouper «Горбуна» r0 5🪙, turtle «Морская черепаха» r1 9🪙, angler «Удильщик» r2 21🪙, octopus «Гигантский осьминог» r2 23🪙, manta «Скат-манта» r3 29🪙, shark «Большая белая акула» r3 31🪙, squid «Гигантский кальмар» r4 37🪙, whale «Синий кит» r4 67🪙 40–100 кг; этап 38: лестница веса (Озёра ×8 / Море ×6, меч ×4 / Океан ×4, кит ×2; Река не меняется); этап 39: цены Озёр/Моря/Океана урезаны (Озёра ≈×3 Реки, Море ≈×4, Океан ≈×5 — по дню; Река не меняется); первые 8 id не сдвинуты, новые — в конце; все поля заполнены; LOCATIONS 4 (Река 0 / Озёра 65536 / Море 131072 / Океан 196608), у каждой 6 hex в water; SVG-спрайт: 32 symbol, id на каждый вид',
lc1.n === 32 && lc1.z === 'Судак,2,8,8,48' && lc1.c === 'Хариус,1,6,1.6,16' && lc1.k === 'Гигантский рак,3,40,1.6,8'
ok('локации данные: SPECIES=37 (8 река + 10 озёра + 5 море + 9 океан + 5 болото, этапы 35/36/45) — zander «Судак» r2 8🪙, crayfish r3 40🪙, navaga r3 50🪙; озёрные новинки rudd «Краснопёрка» r1 5🪙, cisco «Сиг» r2 18🪙; морские herring «Сельдь» r0 8🪙, mackerel «Скумбрия» r1 18🪙, flounder «Камбала» r2 33🪙, tuna «Тунец» r3 46🪙, swordfish «Рыба-меч» r4 81🪙; океанские moray «Гигантская мурена» r0 5🪙, grouper «Горбуна» r0 5🪙, turtle «Морская черепаха» r1 9🪙, angler «Удильщик» r2 21🪙, octopus «Гигантский осьминог» r2 23🪙, manta «Скат-манта» r3 29🪙, shark «Большая белая акула» r3 31🪙, squid «Гигантский кальмар» r4 37🪙, whale «Синий кит» r4 67🪙 40–100 кг; болотные glowfin «Светлячок» r0 8🪙, moonfish «Лунная рыбка» r1 13🪙, emberfin «Изумрудный светляк» r2 20🪙, violetlure «Фиалковый огонёк» r3 38🪙, spiritfin «Дух болота» r4 70🪙 (редкости 0..4 по одной, glow — #rrggbb); этап 38: лестница веса (Озёра ×8 / Море ×6, меч ×4 / Океан ×4, кит ×2; Река не меняется); этап 39: цены Озёр/Моря/Океана урезаны (Озёра ≈×3 Реки, Море ≈×4, Океан ≈×5 — по дню; Река не меняется); первые 8 id не сдвинуты, новые — в конце; все поля заполнены; LOCATIONS 5 (Река 0 / Озёра 65536 / Море 131072 / Океан 196608 / Волшебное болото 90000), у каждой 6 hex в water; SVG-спрайт: 37 symbol, id на каждый вид',
lc1.n === 37 && lc1.z === 'Судак,2,8,8,48' && lc1.c === 'Хариус,1,6,1.6,16' && lc1.k === 'Гигантский рак,3,40,1.6,8'
&& lc1.b === 'Плотва,0,4,0.4,4.8' && lc1.r === 'Густера,0,5,0.8,7.2' && lc1.e === 'Вьюн,1,11,0.4,3.2'
&& lc1.t === 'Линь,2,16,2.4,32' && lc1.n2 === 'Навага,3,50,2.4,20'
&& lc1.rd === 'Краснопёрка,1,5,0.8,9.6' && lc1.ci === 'Сиг,2,18,4,28'
@@ -2000,47 +2016,64 @@ async function toGameMenu(p) {
&& lc1.oc === 'Гигантский осьминог,2,23,16,48' && lc1.ma === 'Скат-манта,3,29,24,60'
&& lc1.sh === 'Большая белая акула,3,31,20,56' && lc1.sq === 'Гигантский кальмар,4,37,16,48'
&& lc1.wh === 'Синий кит,4,67,40,100'
&& lc1.gf === 'Светлячок,0,8,0.3,3' && lc1.mf === 'Лунная рыбка,1,13,0.8,8'
&& lc1.ef === 'Изумрудный светляк,2,20,1.5,16' && lc1.vl === 'Фиалковый огонёк,3,38,2,24'
&& lc1.sf === 'Дух болота,4,70,0.5,6'
&& lc1.first8 === 'carass,perch,bream,carp,pike,catfish,sturge,gold'
&& lc1.lakeOrder === 'zander,chuch,crayfish,bleak,roundel,eelpout,tench,navaga,rudd,cisco'
&& lc1.seaOrder === 'herring,mackerel,flounder,tuna,swordfish'
&& lc1.oceanOrder === 'moray,grouper,turtle,angler,octopus,manta,shark,squid,whale'
&& lc1.swampOrder === 'glowfin,moonfish,emberfin,violetlure,spiritfin'
&& lc1.fieldsOk === true
&& lc1.locs === 4 && lc1.costs === '0,65536,131072,196608' && lc1.names === 'Река,Озёра,Море,Океан' && lc1.water === true
&& lc1.sprites === 32 && lc1.spriteIdsOk === true,
&& lc1.swampGlow === true && lc1.swampRar === '0,1,2,3,4'
&& lc1.locs === 5 && lc1.costs === '0,65536,131072,196608,90000' && lc1.names === 'Река,Озёра,Море,Океан,Волшебное болото' && lc1.water === true
&& lc1.sprites === 37 && lc1.spriteIdsOk === true,
JSON.stringify(lc1));
// (2) таблицы: река — ровно старые 8 (порядок), озёра — только свои 10, море — только свои 5, океан — только свои 9, ночные веса — числа; пересечений нет
// (2) таблицы: река — ровно старые 8 (порядок), озёра — только свои 10, море — только свои 5, океан — только свои 9, болото — только свои 5, ночные веса — числа; пересечений нет
const lc2 = await page.evaluate(() => {
const old8 = 'carass,perch,bream,carp,pike,catfish,sturge,gold';
const lakeIds = LAKE_SPECIES.map(s => s.id);
const seaIds = SEA_SPECIES.map(s => s.id);
const oceanIds = OCEAN_SPECIES.map(s => s.id);
const swampIds = SWAMP_SPECIES.map(s => s.id);
return {
river: RIVER_SPECIES.map(s => s.id).join(','),
riverN: RIVER_SPECIES_NIGHT.map(s => s.id).join(','),
lake: LAKE_SPECIES.length, lakeN: LAKE_SPECIES_NIGHT.length,
sea: SEA_SPECIES.length, seaN: SEA_SPECIES_NIGHT.length,
ocean: OCEAN_SPECIES.length, oceanN: OCEAN_SPECIES_NIGHT.length,
swamp: SWAMP_SPECIES.length, swampN: SWAMP_SPECIES_NIGHT.length,
lakeOrder: lakeIds.join(','), seaOrder: seaIds.join(','), oceanOrder: oceanIds.join(','),
// пересечения между водами (река∩озёра, река∩море, озёра∩море, океан∩река, океан∩озёра, океан∩море) пусты
swampOrder: swampIds.join(','), swampNOrder: SWAMP_SPECIES_NIGHT.map(s => s.id).join(','),
// пересечения между водами (река∩озёра, река∩море, озёра∩море, океан∩река, океан∩озёра, океан∩море, болото∩всё) пусты
xRL: lakeIds.filter(id => old8.split(',').indexOf(id) >= 0).join(','),
xRS: seaIds.filter(id => old8.split(',').indexOf(id) >= 0).join(','),
xLS: seaIds.filter(id => lakeIds.indexOf(id) >= 0).join(','),
xOR: oceanIds.filter(id => old8.split(',').indexOf(id) >= 0).join(','),
xOL: oceanIds.filter(id => lakeIds.indexOf(id) >= 0).join(','),
xOS: oceanIds.filter(id => seaIds.indexOf(id) >= 0).join(','),
nightW: SPECIES_NIGHT.length === 32 && SPECIES_NIGHT.every(s => typeof s.weight === 'number' && isFinite(s.weight))
// Этап 45: пересечения болота с остальными водами
xSR: swampIds.filter(id => old8.split(',').indexOf(id) >= 0).join(','),
xSL: swampIds.filter(id => lakeIds.indexOf(id) >= 0).join(','),
xSS: swampIds.filter(id => seaIds.indexOf(id) >= 0).join(','),
xSO: swampIds.filter(id => oceanIds.indexOf(id) >= 0).join(','),
nightW: SPECIES_NIGHT.length === 37 && SPECIES_NIGHT.every(s => typeof s.weight === 'number' && isFinite(s.weight))
};
});
ok('локации таблицы: RIVER_SPECIES(_NIGHT) — ровно старые 8 (порядок сохранён), LAKE_SPECIES(_NIGHT) — только свои 10 (без речных), SEA_SPECIES(_NIGHT) — только свои 5, OCEAN_SPECIES(_NIGHT) — только свои 9 (этап 36); пересечений между водами нет; у всех 32 в SPECIES_NIGHT вес — число',
ok('локации таблицы: RIVER_SPECIES(_NIGHT) — ровно старые 8 (порядок сохранён), LAKE_SPECIES(_NIGHT) — только свои 10 (без речных), SEA_SPECIES(_NIGHT) — только свои 5, OCEAN_SPECIES(_NIGHT) — только свои 9 (этап 36), SWAMP_SPECIES(_NIGHT) — только свои 5 (этап 45, порядок = SWAMP_IDS); пересечений между водами нет; у всех 37 в SPECIES_NIGHT вес — число',
lc2.river === 'carass,perch,bream,carp,pike,catfish,sturge,gold' && lc2.riverN === lc2.river
&& lc2.lake === 10 && lc2.lakeN === 10 && lc2.sea === 5 && lc2.seaN === 5
&& lc2.ocean === 9 && lc2.oceanN === 9
&& lc2.swamp === 5 && lc2.swampN === 5
&& lc2.lakeOrder === 'zander,chuch,crayfish,bleak,roundel,eelpout,tench,navaga,rudd,cisco'
&& lc2.seaOrder === 'herring,mackerel,flounder,tuna,swordfish'
&& lc2.oceanOrder === 'moray,grouper,turtle,angler,octopus,manta,shark,squid,whale'
&& lc2.swampOrder === 'glowfin,moonfish,emberfin,violetlure,spiritfin'
&& lc2.swampNOrder === lc2.swampOrder
&& lc2.xRL === '' && lc2.xRS === '' && lc2.xLS === ''
&& lc2.xOR === '' && lc2.xOL === '' && lc2.xOS === ''
&& lc2.xSR === '' && lc2.xSL === '' && lc2.xSS === '' && lc2.xSO === ''
&& lc2.nightW === true,
JSON.stringify(lc2));
@@ -2131,6 +2164,29 @@ async function toGameMenu(p) {
lc4c.dayAll === true && lc4c.nightAll === true && lc4c.onlyOcean === true,
JSON.stringify(lc4c));
// (4d) Этап 45: роллы болота — ровно свои 5 болотных видов (каждый ≥1), видов других вод нет
const lc4d = await page.evaluate(() => {
save.location = 4;
setDayTime(0.3);
const day = {};
for (let i = 0; i < 3000; i++) { const id = pickSpecies().id; day[id] = (day[id] || 0) + 1; }
setDayTime(0.85);
const night = {};
for (let i = 0; i < 3000; i++) { const id = pickSpecies().id; night[id] = (night[id] || 0) + 1; }
save.location = 0;
const ids = SWAMP_IDS;
const others = RIVER_IDS.concat(LAKE_IDS, SEA_IDS, OCEAN_IDS);
const min = o => Math.min.apply(null, ids.map(id => o[id] || 0));
return {
dayAll: ids.every(id => day[id] > 0), nightAll: ids.every(id => night[id] > 0),
onlySwamp: [...Object.keys(day), ...Object.keys(night)].every(id => others.indexOf(id) < 0),
dayMin: min(day), nightMin: min(night)
};
});
ok('локации роллы болота: 3000 днём и 3000 ночью (location=4) — все 5 болотных видов встречаются (каждый ≥1), речных/озёрных/морских/океанских нет',
lc4d.dayAll === true && lc4d.nightAll === true && lc4d.onlySwamp === true,
JSON.stringify(lc4d));
// (5) миграция: старый сейв (без locations/location) → [0]/0; location=5 → 0; location=1 при locations=[0] → 0; Этап 36: Море/Океан не сбрасываются (баг границы >1)
const lc5 = await page.evaluate(() => {
const base = { coins: 50, rod: 0, lure: 0, rods: [0], lures: [0], rodLv: { 0: 1 }, lureLv: { 0: 1 },
@@ -2159,15 +2215,20 @@ async function toGameMenu(p) {
localStorage.setItem(SAVE_KEY, JSON.stringify(Object.assign({}, base, { locations: [0, 3], location: 3 })));
loadSave();
r.e = { locs: save.locations.slice(), loc: save.location };
// (f) Этап 45: Болото (location=4) — граница LOCATIONS.length расширена, не сбрасывается на reload
localStorage.setItem(SAVE_KEY, JSON.stringify(Object.assign({}, base, { locations: [0, 4], location: 4 })));
loadSave();
r.f = { locs: save.locations.slice(), loc: save.location };
save.locations = [0]; save.location = 0; // не загрязнять следующие тесты (lc6 покупает Озёра)
return r;
});
ok('локации миграция: старый сейв (без locations/location) → [0]/0; location=5 → 0; location=1 при locations=[0] (не куплено) → 0; location=2 (Море) и location=3 (Океан) не сбрасываются (граница из LOCATIONS.length, баг >1 исправлен)',
ok('локации миграция: старый сейв (без locations/location) → [0]/0; location=5 (вне 0..4) → 0; location=1 при locations=[0] (не куплено) → 0; location=2 (Море), location=3 (Океан) и location=4 (Болото) не сбрасываются (граница из LOCATIONS.length, баг >1 исправлен)',
lc5.a.locs.join(',') === '0' && lc5.a.loc === 0
&& lc5.b.locs.join(',') === '0' && lc5.b.loc === 0
&& lc5.c.locs.join(',') === '0' && lc5.c.loc === 0
&& lc5.d.locs.join(',') === '0,2' && lc5.d.loc === 2
&& lc5.e.locs.join(',') === '0,3' && lc5.e.loc === 3,
&& lc5.e.locs.join(',') === '0,3' && lc5.e.loc === 3
&& lc5.f.locs.join(',') === '0,4' && lc5.f.loc === 4,
JSON.stringify(lc5));
// (6) покупка: 66536 монет → «Озёра» (65536) — списание, авто-переход, тост
@@ -2258,23 +2319,38 @@ async function toGameMenu(p) {
&& lc8o.toast.includes('Добро пожаловать в Океан!'),
JSON.stringify(lc8o));
// (9) дневник: 32 строки (новые виды видны в энциклопедии)
// (8d) Этап 45: покупка Волшебного болота: 90000 → списание, авто-переход, тост «Добро пожаловать в Волшебное болото!»
await page.evaluate(() => { save.coins = 90000; save.locations = [0, 1, 2, 3]; save.location = 0; renderShop(); });
await page.click('#tabLocations');
await sleep(150);
await page.click('button[data-act="buylocation"][data-i="4"]');
const lc8w = await page.evaluate(() => ({
coins: save.coins, locs: save.locations.slice(), loc: save.location,
toast: document.getElementById('toast').textContent
}));
ok('локации покупка болота: 90000 → «Волшебное болото» (90000) — coins=0, locations=[0,1,2,3,4], авто-переход location=4, тост «Добро пожаловать в Волшебное болото!»',
lc8w.coins === 0 && lc8w.locs.join(',') === '0,1,2,3,4' && lc8w.loc === 4
&& lc8w.toast.includes('Добро пожаловать в Волшебное болото!'),
JSON.stringify(lc8w));
// (9) дневник: 37 строк (новые виды видны в энциклопедии)
await page.click('#tabJournal');
await sleep(150);
const lc9 = await page.evaluate(() => document.querySelectorAll('#shopBody .jrow').length);
ok('локации дневник: вкладка journal — 32 строки .jrow (новые виды в энциклопедии)',
lc9 === 32, 'rows=' + lc9);
ok('локации дневник: вкладка journal — 37 строк .jrow (новые виды в энциклопедии)',
lc9 === 37, 'rows=' + lc9);
// (10) масштаб ачивок: species_8 — все 32 вида; full_variants — 32×8 вариантов, +5000 разово
// (10) масштаб ачивок: species_8 — все 37 видов; full_variants — 37×8 вариантов, +5000 разово
const lc10 = await page.evaluate(() => {
const desc = ACHIEVEMENTS.find(a => a.id === 'species_8').desc;
const old8 = ['carass', 'perch', 'bream', 'carp', 'pike', 'catfish', 'sturge', 'gold'];
const mid24 = ['zander', 'chuch', 'crayfish', 'bleak', 'roundel', 'eelpout', 'tench', 'navaga', 'rudd', 'cisco', 'herring', 'mackerel', 'flounder', 'tuna', 'swordfish', 'moray', 'grouper', 'turtle', 'angler', 'octopus', 'manta', 'shark', 'squid', 'whale'];
// Этап 45: 29 новых видов (10 озёрных этапа 33/35 + 5 морских этапа 35 + 9 океанских этапа 36 + 5 болотных)
const mid29 = ['zander', 'chuch', 'crayfish', 'bleak', 'roundel', 'eelpout', 'tench', 'navaga', 'rudd', 'cisco', 'herring', 'mackerel', 'flounder', 'tuna', 'swordfish', 'moray', 'grouper', 'turtle', 'angler', 'octopus', 'manta', 'shark', 'squid', 'whale', 'glowfin', 'moonfish', 'emberfin', 'violetlure', 'spiritfin'];
save.ach = {}; save.caught = {};
for (const id of old8) save.caught[id] = { n: 1, best: 0, sn: 1, sv: 1, rb: 1, cp: 1, qz: 1, em: 1, dm: 1 };
checkAchievements();
const mid = { s8: !!save.ach.species_8, fv: !!save.ach.full_variants };
for (const id of mid24) save.caught[id] = { n: 1, best: 0, sn: 1, sv: 1, rb: 1, cp: 1, qz: 1, em: 1, dm: 1 };
for (const id of mid29) save.caught[id] = { n: 1, best: 0, sn: 1, sv: 1, rb: 1, cp: 1, qz: 1, em: 1, dm: 1 };
const c0 = save.coins;
checkAchievements();
const after = { s8: !!save.ach.species_8, fv: !!save.ach.full_variants, d1: save.coins - c0 };
@@ -2282,13 +2358,36 @@ async function toGameMenu(p) {
after.d2 = save.coins - c0;
return { desc, mid, after };
});
ok('локации ачивки: desc species_8 содержит «все виды»; 8 старых ×8 вариантов — species_8/full_variants НЕ выданы; +24 новых (15 этапов 33/35 + 9 океанских) → обе выданы, +5000 ровно один раз',
ok('локации ачивки: desc species_8 содержит «все виды»; 8 старых ×8 вариантов — species_8/full_variants НЕ выданы; +29 новых (10 озёрных этапов 33/35 + 5 морских + 9 океанских + 5 болотных, Этап 45) → обе выданы, +5000 ровно один раз',
lc10.desc.includes('все виды')
&& lc10.mid.s8 === false && lc10.mid.fv === false
&& lc10.after.s8 === true && lc10.after.fv === true
&& lc10.after.d1 === 5000 && lc10.after.d2 === 5000,
JSON.stringify(lc10));
// (11) Этап 45: данные свечения — у 5 болотных видов glow #rrggbb (5 цветов РАЗЛИЧНЫ),
// SWAMP_DAY/NIGHT_WEIGHTS покрывают все SWAMP_IDS числом >0, NIGHT_WEIGHTS — ключи всех 5 id
const lc11 = await page.evaluate(() => {
const ids = SWAMP_IDS.slice();
const glows = ids.map(id => SPECIES.find(s => s.id === id).glow);
const weightsOk = tbl => ids.every(id => typeof tbl[id] === 'number' && isFinite(tbl[id]) && tbl[id] > 0);
const keyOf = tbl => ids.every(id => Object.prototype.hasOwnProperty.call(tbl, id));
return {
glowFmt: glows.every(c => typeof c === 'string' && /^#[0-9a-fA-F]{6}$/.test(c)),
glowUnique: new Set(glows).size === glows.length,
dayW: weightsOk(SWAMP_DAY_WEIGHTS), nightW: weightsOk(SWAMP_NIGHT_WEIGHTS),
dayKeys: keyOf(SWAMP_DAY_WEIGHTS), nightKeys: keyOf(SWAMP_NIGHT_WEIGHTS),
nwKeys: keyOf(NIGHT_WEIGHTS),
nwAll: SPECIES.every(s => typeof NIGHT_WEIGHTS[s.id] === 'number' && isFinite(NIGHT_WEIGHTS[s.id]))
};
});
ok('Этап 45 свечение/веса: у 5 болотных видов glow — #rrggbb, все 5 цветов различны; SWAMP_DAY_WEIGHTS/SWAMP_NIGHT_WEIGHTS покрывают все id SWAMP_IDS числами >0; NIGHT_WEIGHTS имеет ключи всех 5 болотных id (и всех 37 видов)',
lc11.glowFmt === true && lc11.glowUnique === true
&& lc11.dayW === true && lc11.nightW === true
&& lc11.dayKeys === true && lc11.nightKeys === true
&& lc11.nwKeys === true && lc11.nwAll === true,
JSON.stringify(lc11));
// сброс локаций к дефолту
await page.evaluate(() => {
save.location = 0; save.locations = [0]; persist();
@@ -2306,12 +2405,13 @@ async function toGameMenu(p) {
a22t.achDesc === 'Поймай золотую рыбу' && a22t.quest === 'Поймай золотую рыбу' && a22t.noShiny,
JSON.stringify(a22t));
// (2) данные: 11 новых достижений (Этап 22) + sea_first/ocean_first (Этап 36) + lake_boss/sea_boss/ocean_boss (Этап 37),
// ACHIEVEMENTS.length===34, id уникальны, name/desc/icon/bonus/check
// (2) данные: 11 новых достижений (Этап 22) + sea_first/ocean_first (Этап 36) + lake_boss/sea_boss/ocean_boss (Этап 37)
// + swamp_first/swamp_boss (Этап 45), ACHIEVEMENTS.length===36, id уникальны, name/desc/icon/bonus/check
const a22d = await page.evaluate(() => {
const ids = ['first_silver', 'first_rainbow', 'rain_variant', 'fog_catch', 'skin_first',
'skins_all', 'lake_first', 'gear_max', 'titan', 'weight_1000', 'night_first',
'sea_first', 'ocean_first', 'lake_boss', 'sea_boss', 'ocean_boss'];
'sea_first', 'ocean_first', 'lake_boss', 'sea_boss', 'ocean_boss',
'swamp_first', 'swamp_boss'];
const list = ACHIEVEMENTS.map(a => a.id);
return {
total: ACHIEVEMENTS.length,
@@ -2321,8 +2421,8 @@ async function toGameMenu(p) {
return a && a.name && a.desc && a.icon && a.bonus && typeof a.check === 'function'; })
};
});
ok('Этап 22 данные: 11 новых достижений (варианты/погода/скины/локации/снасти/вес/ночь) + sea_first/ocean_first (Этап 36) + lake_boss/sea_boss/ocean_boss (Этап 37), ACHIEVEMENTS.length===34, id уникальны, поля на месте',
a22d.all === true && a22d.total === 34 && a22d.unique === true && a22d.fields === true,
ok('Этап 22 данные: 11 новых достижений (варианты/погода/скины/локации/снасти/вес/ночь) + sea_first/ocean_first (Этап 36) + lake_boss/sea_boss/ocean_boss (Этап 37) + swamp_first/swamp_boss (Этап 45), ACHIEVEMENTS.length===36, id уникальны, поля на месте',
a22d.all === true && a22d.total === 36 && a22d.unique === true && a22d.fields === true,
JSON.stringify(a22d));
// (3) геймплей: ночной улов в дождь, серебряный вариант — одним уловом проверяются
@@ -2456,7 +2556,7 @@ async function toGameMenu(p) {
JSON.stringify(a22lk));
// (8b) Этап 36: sea_first/ocean_first — locations-based (паттерн lake_first 1:1; ачивки не отзываются —
// каждую проверяем отдельной парой шагов с чисткой save.ach)
// каждую проверяем отдельной парой шагов с чисткой save.ach); Этап 45: +swamp_first (location 4)
const a36loc = await page.evaluate(() => {
const r = {};
save.ach = {};
@@ -2473,12 +2573,20 @@ async function toGameMenu(p) {
save.locations = [0, 3];
checkAchievements();
r.oceanGot = !!save.ach.ocean_first;
save.ach = {};
save.locations = [0];
checkAchievements();
r.swampNot = !!save.ach.swamp_first;
save.locations = [0, 4];
checkAchievements();
r.swampGot = !!save.ach.swamp_first;
save.locations = [0];
return r;
});
ok('Этап 36 sea_first/ocean_first: locations [0] — не выданы; [0,2] (Море) → sea_first; [0,3] (Океан) → ocean_first',
ok('Этап 36/45 sea_first/ocean_first/swamp_first: locations [0] — не выданы; [0,2] (Море) → sea_first; [0,3] (Океан) → ocean_first; [0,4] (Болото) → swamp_first',
a36loc.seaNot === false && a36loc.seaGot === true
&& a36loc.oceanNot === false && a36loc.oceanGot === true,
&& a36loc.oceanNot === false && a36loc.oceanGot === true
&& a36loc.swampNot === false && a36loc.swampGot === true,
JSON.stringify(a36loc));
// (9) gear_max/titan: максимум 3 ур. — не выдано; lureLv=4 → gear_max; rods с 3 → titan
+19 -12
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
/* balance.mjs — доходность рыб по локациям (вспомогательный скрипт балансировки).
Считает E[выручку за улов] = Σ(w·V)/Σw (V = середина диапазона веса × цена) по всем
8 таблицам ролла (Река/Озёра/Море/Океан × день/вечер-ночь) — та же модель, что
10 таблицам ролла (Река/Озёра/Море/Океан/Болото × день/вечер-ночь) — та же модель, что
pickSpecies() в index.html, плюс сводку отношений локаций и «здоровье» (риск
теста «каждый вид ≥1»).
Данные НЕ дублируются: фрагмент секции «ДАННЫЕ» вырезается из index.html по якорям
@@ -23,9 +23,11 @@
node tools/balance.mjs --price zander:8 --price tuna:46
Чистый Node, без зависимостей (стиль tools/check.mjs). Пины при нейтральной снасти:
Река 65.1/125.9 · Озёра 194.1/214.5 · Море 262.7/304.2 · Океан 329.1/398.9
(день/ночь, 🪙 за улов; этапы 38/39 — лестница весов + урезанные цены) — скрипт
обязан воспроизводить их до и после любых правок баланса. */
Река 65.1/125.9 · Озёра 194.1/249.0 · Болото 240.3/328.9 · Море 297.3/476.7 ·
Океан 329.1/552.7 (день/ночь, 🪙 за улов; этапы 38/39/40 — лестница весов + урезанные
цены + ночные веса; болото — этап 45) — скрипт обязан воспроизводить их до и после
любых правок баланса. Пары отношений в сводке строятся ПО ЦЕНЕ локации
(Река→Озёра→Болото→Море→Океан), а не по порядку массива LOCATIONS. */
import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';
@@ -52,7 +54,7 @@ if (b < 0) fail('не найдено закрытие «];» таблицы VARI
const D = new Function(html.slice(a, b + 3) +
'\n;return {SPECIES, SPECIES_NIGHT, RIVER_SPECIES, RIVER_SPECIES_NIGHT,' +
'LAKE_SPECIES, LAKE_SPECIES_NIGHT, SEA_SPECIES, SEA_SPECIES_NIGHT,' +
'OCEAN_SPECIES, OCEAN_SPECIES_NIGHT,' +
'OCEAN_SPECIES, OCEAN_SPECIES_NIGHT, SWAMP_SPECIES, SWAMP_SPECIES_NIGHT,' +
'LOCATIONS, VARIANTS, SHINY_CHANCE};')();
/* ---------- 2. Опции CLI ---------- */
@@ -64,8 +66,8 @@ for (let i = 0; i < argv.length; i++) {
const arg = argv[i];
if (arg === '--help' || arg === '-h') {
console.log(
`balance.mjs — доходность рыб по локациям: E[🪙 за улов] = Σ(w·V)/Σw по 8 таблицам
(Река/Озёра/Море/Океан × день/вечер-ночь), сводка отношений и «здоровье» роллов.
`balance.mjs — доходность рыб по локациям: E[🪙 за улов] = Σ(w·V)/Σw по 10 таблицам
(Река/Озёра/Болото/Море/Океан × день/вечер-ночь), сводка отношений и «здоровье» роллов.
Данные берутся из index.html (секция «ДАННЫЕ») — правки баланса видны сразу.
Опции:
--dist <0..1> дистанция заброса: при dist<minDist вес ×0.06 (по умолчанию 1)
@@ -107,6 +109,8 @@ const TABLES = [
{ loc: 2, phase: 'вечер/ночь', tbl: D.SEA_SPECIES_NIGHT },
{ loc: 3, phase: 'день', tbl: D.OCEAN_SPECIES },
{ loc: 3, phase: 'вечер/ночь', tbl: D.OCEAN_SPECIES_NIGHT },
{ loc: 4, phase: 'день', tbl: D.SWAMP_SPECIES },
{ loc: 4, phase: 'вечер/ночь', tbl: D.SWAMP_SPECIES_NIGHT },
];
const ROLLS = 3000; // объём теста «каждый вид ≥1» (lc4)
const SAFE_P0 = 0.001; // порог риска: P(вид ни разу не выпал за 3000 роллов) выше — ⚠
@@ -197,14 +201,17 @@ console.log('── СВОДКА ' + dash('── СВОДКА '));
console.log('локация'.padEnd(12) + 'день'.padStart(9) + 'вечер/ночь'.padStart(13) + 'ночь/день'.padStart(11));
const by = {};
for (const r of results) (by[r.loc] ||= {})[r.phase] = r.calc.total;
for (const loc of D.LOCATIONS) {
// локации по ЦЕНЕ (дороже вода = доходнее): пары отношений читаются по возрастанию цены,
// а не по порядку массива LOCATIONS (90000 болото стоит перед Морем/Океаном по цене)
const locsByCost = [...D.LOCATIONS].sort((x, y) => x.cost - y.cost);
for (const loc of locsByCost) {
const d = by[loc.id]['день'], n = by[loc.id]['вечер/ночь'];
console.log(loc.name.padEnd(12) + f1(d).padStart(9) + f1(n).padStart(13) + xr(n / d).padStart(11));
}
console.log('');
// отношения соседних вод: инвариант — каждая новая вода доходнее предыдущей в ОБЕ фазы
for (let i = 1; i < D.LOCATIONS.length; i++) {
const prev = D.LOCATIONS[i - 1], cur = D.LOCATIONS[i];
// отношения соседних вод: инвариант — каждая новая (по цене) вода доходнее предыдущей в ОБЕ фазы
for (let i = 1; i < locsByCost.length; i++) {
const prev = locsByCost[i - 1], cur = locsByCost[i];
console.log(`${cur.name}/${prev.name}: день ${xr(by[cur.id]['день'] / by[prev.id]['день'])} · ночь ${xr(by[cur.id]['вечер/ночь'] / by[prev.id]['вечер/ночь'])}`);
}
console.log('');
@@ -236,5 +243,5 @@ console.log(inter.length ? `⚠ пересечения таблиц локаци
console.log(badWeights ? `⚠ нечисловых/отрицательных весов: ${badWeights}` : `✓ веса всех ${knownIds.size * 2} записей (${TABLES.length} таблиц) — числа`);
console.log(unreachable.length ? `⚠ виды вне роллов локаций: ${unreachable.join(', ')}` : `✓ все ${D.SPECIES.length} видов достигаемы в роллах`);
console.log('');
console.log('босс «Осётр» (Река) клюёт вне обычного ролла (награда ×5) — в доходность не входит.');
console.log('боссы локаций (Река/Озёра/Море/Океан/Болото) клюют вне обычного ролла (награда ×5) — в доходность не входят.');
console.log('туман (клёв ×1.5) на цену улова не влияет; дождь учтён строкой «при дожде».');
+4 -3
View File
@@ -1,11 +1,11 @@
#!/usr/bin/env node
// tools/embed-fish.mjs — встраивает 32 SVG-портрета рыб (assets/fish/fish-<id>.svg)
// tools/embed-fish.mjs — встраивает 37 SVG-портретов рыб (assets/fish/fish-<id>.svg)
// в index.html как СКРЫТЫЙ SVG-спрайт из <symbol> (id="fish-<id>", viewBox 0 0 160 100).
//
// Запуск: node tools/embed-fish.mjs (из каталога репо или откуда угодно — пути от репо)
// - идемпотентен: если маркеры FISH-SVG:BEGIN/END уже есть — заменяет ВСЁ между ними;
// если нет — вставляет блок сразу ПОСЛЕ открывающего тега <body> (в начале файла).
// - после записи — самопроверка: 32 вхождения id="fish-, оба маркера, затем node tools/check.mjs.
// - после записи — самопроверка: 37 вхождений id="fish-, оба маркера, затем node tools/check.mjs.
// Чистый Node, без npm-зависимостей. Детерминирован: одинаковый ввод → одинаковый index.html.
//
// ВАЖНО: это НЕ подключение UI. Дневник/карточка/JS-логику НЕ трогаем —
@@ -20,12 +20,13 @@ const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..');
const INDEX = join(ROOT, 'index.html');
const ASSETS = join(ROOT, 'assets', 'fish');
// Порядок = порядок SPECIES в index.html (карасс → кит).
// Порядок = порядок SPECIES в index.html (карасс → кит → дух болота).
const IDS = [
'carass', 'perch', 'bream', 'carp', 'pike', 'catfish', 'sturge', 'gold',
'zander', 'chuch', 'crayfish', 'bleak', 'roundel', 'eelpout', 'tench', 'navaga',
'rudd', 'cisco', 'herring', 'mackerel', 'flounder', 'tuna', 'swordfish',
'moray', 'grouper', 'turtle', 'angler', 'octopus', 'manta', 'shark', 'squid', 'whale',
'glowfin', 'moonfish', 'emberfin', 'violetlure', 'spiritfin',
];
const BEGIN = '<!-- FISH-SVG:BEGIN (генерируется tools/embed-fish.mjs — не править руками) -->';

Some files were not shown because too many files have changed in this diff Show More