Ачивки: Этап 22 — +11 достижений (18→29) по итогам M5/M6/M8/скинов/локаций + «shiny»→«золотая рыба». new: first_silver 🥈 / first_rainbow 🌈 / rain_variant 🌧️ (особая в дождь) / fog_catch 🌫️ / skin_first 🎨 / skins_all 🎭 / lake_first 🗺️ / gear_max ⚒️ (4 ур.) / titan 💪 / weight_1000 🚢 / night_first 🌙 — все coinPct 1–2%. Сейв: +kgTotal/nightCatch/wxRainV/wxFog (чтение ||0, без миграции), крючок в catchFish() до checkAchievements() (rain — только вариант ≠ normal, босс normal). Тексты: квест catch_shiny и desc first_shiny — «Поймай золотую рыбу» (id/SHINY_CHANCE не тронуты); README/BROWSER-TESTING/лейблы test.js+orca-playtest — «золотая рыба». Тесты: +11 (§10g: тексты, данные 29/уникальность, геймплей ночной улов в дождь — 3 счётчика+3 ачивки, first_rainbow/fog_catch, граница 999.9/1000, скины, lake, снасти, старый сейв), пин full_variants 0.13→0.16. 128/128, exit 0; доки: DEVLOG Этап 22, AGENTS/README — 128/29.

This commit is contained in:
2026-09-07 10:50:03 +03:00
parent d5212e9b42
commit 4c4b6db1fa
7 changed files with 260 additions and 25 deletions
+3 -3
View File
@@ -11,7 +11,7 @@
npm install # puppeteer-core
npm test # = node test.js
```
Ждать все PASS и exit 0. Сейчас проверок 117.
Ждать все PASS и exit 0. Сейчас проверок 128.
3. Браузерный плейтест (вкладка встроенного браузера Orca, через Orca CLI):
```bash
node tests/orca-playtest.js # из каталога репо; требует ЗАПУЩЕННЫЙ Orca app и orca-ide (или ORCA_CLI_COMMAND)
@@ -23,7 +23,7 @@
- `tests/test.js` берёт браузер из `process.env.CHROMIUM || '/usr/bin/chromium-browser'`, а URL игры резолвит динамически от каталога репо (тот же паттерн, что в `shots.js`). Если тест падает на старте — поправьте имя/путь chromium под свой чекаут (переменная `CHROMIUM`).
- Граница полуночи: если прогон стартует в пределах 5 минут от 00:00, test.js сам ждёт безопасное окно (дата не должна смениться посреди прогона — падают date-ассерты квестов/стрика). Это нормально, не баг.
- Тесты гоняют игру через внутренние глобалы в `page.evaluate`: `state`, `save`, `biteTimer`, `reel`, `SPECIES`, `SPECIES_NIGHT`, `rod()`, `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`, `WEATHER_GAP_MAX`, `WEATHER_DURATION`, `weatherActive()`, `BOAT_SKINS`, `BOBBER_SKINS`, `LOCATIONS`, `RIVER_SPECIES`, `RIVER_SPECIES_NIGHT`, `LAKE_SPECIES`, `LAKE_SPECIES_NIGHT`, `SAVE_KEY`, `loadSave`, `ACHIEVEMENTS`, `checkAchievements` и имена состояний `MENU/IDLE/CHARGE/CAST/WAIT/APPROACH/BITE/REEL/SNAP/CARD`. Переименование любого из них ломает тесты молча — имена держать, либо обновлять тест в том же коммите.
- Тесты гоняют игру через внутренние глобалы в `page.evaluate`: `state`, `save`, `biteTimer`, `reel`, `SPECIES`, `SPECIES_NIGHT`, `rod()`, `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`, `WEATHER_GAP_MAX`, `WEATHER_DURATION`, `weatherActive()`, `BOAT_SKINS`, `BOBBER_SKINS`, `LOCATIONS`, `RIVER_SPECIES`, `RIVER_SPECIES_NIGHT`, `LAKE_SPECIES`, `LAKE_SPECIES_NIGHT`, `SAVE_KEY`, `loadSave`, `freshSave`, `ACHIEVEMENTS`, `checkAchievements`, `questText` и имена состояний `MENU/IDLE/CHARGE/CAST/WAIT/APPROACH/BITE/REEL/SNAP/CARD`. Переименование любого из них ломает тесты молча — имена держать, либо обновлять тест в том же коммите.
- Ввод на Pointer Events (`pointerdown`/`pointerup`), а не touch/mouse — так тест управляет игрой мышью. Не заменять на тач-обработчики.
- Сейв: ключ localStorage `fishing-save-v1`. При изменении формата — миграцию добавлять в `loadSave()` (старые сейвы обязаны оставаться читаемыми).
@@ -38,7 +38,7 @@
- Локации (этап 5): таблица `LOCATIONS` (Река 0 — бесплатно / «Озёра» 100000 🪙; `water` — 6 hex [dayTop,dayMid,dayBot,nightTop,nightMid,nightBot] для градиента `drawWater()`, nightMix-логика та же), сейв `save.locations[]`/`save.location` + миграция в `loadSave()` (гарантия бесплатной Реки, паттерн скинов), вкладка «Локации» 🗺️ (`#tabLocations`, строка `locationRow(loc,i,owned,selected)`, ветки buylocation/selectlocation в `shopClick()`: покупка — списание, push, АВТО-ПЕРЕХОД `save.location`, тост «Добро пожаловать на Озёра!»; выбор — только купленное). Таблицы рыб: `RIVER_SPECIES(_NIGHT)` — ТОЛЬКО 8 базовых видов (роллы реки не меняются: pin-тесты §9j(b)/M4/M6 завязаны на веса и порядок — новые виды в реку НЕ добавлять!), `LAKE_SPECIES(_NIGHT)` — все 11 видов (веса `LAKE_DAY_WEIGHTS`/`LAKE_NIGHT_WEIGHTS`); `SPECIES`/`SPECIES_NIGHT` — полный словарь 11 видов (новые: zander/chuch/crayfish, ночные веса 6/10/2.5). `pickSpecies()` выбирает таблицу по `save.location` и dayPhase (логика ролла та же); босс (sturge) есть в обеих локациях; ачивка species_8 — теперь «Поймай все виды» (все 11, id не менять — старые сейвы).
- Сложность вываживания: `updateReel()`.
- Физика боя (спринт 4): `fishForce()` (сила рыбы: рывок `(16+34·st)·(0.5+0.9·ag)·(0.7+0.5·ov)`, затишье `5+7·ag`, уставшая ×0.45) и `updateReel()` — выносливость `st` (старт `st0`, флаг `tired` при `st≤0.12`: рывки ×0.45, втягивание быстрее до ×1.6), тормоз катушки (черта `drag` в `RODS`: в рывке выше тормоза леска скользит `over·0.22`), натяжение (`10+over·1.1+22·ag`, ×0.35 в затишье), втягивание `(11+3·reel)·(1+0.6·(1−st))`, свободный бег `F·0.32`, escape-бар растёт только при затишье без удержания (отпускание в рывок безопасно), near-miss `1.45·d0`, `d0=45+7·w`. Пустые клёвы: ролл `biteChance()` в ветке WAIT (тест-форс `forceBite`). Тесты (§10) завязаны на эти константы — при ребалансе гонять Monte-Carlo точными формулами (docs/PHYSICS-RESEARCH.md, §4.7) и править тесты в том же коммите.
- Награды (M2): шаблоны квестов `QUEST_TPLS`, награды стрика `STREAK_REWARDS`, достижения `ACHIEVEMENTS` — секция «M2. Награды» после сейва.
- Награды (M2): шаблоны квестов `QUEST_TPLS`, награды стрика `STREAK_REWARDS`, достижения `ACHIEVEMENTS` — секция «M2. Награды» после сейва. Этап 22: ачивок 29 (идут `ACHIEVEMENTS`-записями по темам: снасти у экономики, варианты/погода/вес/ночь у рыболовных, скины/локации у магазинных); новые счётчики сейва `save.kgTotal`/`nightCatch`/`wxRainV`/`wxFog` (чтение через `||0` — без миграции) ставятся в `catchFish()` до `checkAchievements()` (rain-счётчик — только вариант ≠ normal, босс всегда normal); тест-пин §10c(8): сумма `achBonus().coinPct` (все 8 видов × 4 варианта) = 0.16.
## Запуск и публикация
+4 -4
View File
@@ -16,7 +16,7 @@
4. **У каждой рыбы свой вес**, продажа — по цене за кг своего вида (от 20 до 1500 🪙/кг). Крупная рыба = большие деньги.
5. **У удочек есть предел веса** (3–40 кг): зацепишь более тяжёлую — шнур лопнет почти мгновенно. Чем лучше удочка и приманка, тем выше шанс зацепить крупную. Новый максимум по виду отмечен бейджем «🏆 РЕКОРД!» на карточке улова.
6. На монеты — новые удилища и приманки в магазине 🛒.
7. **Награды 🎯**: три квеста дня (уловы, монеты, забросы) с наградой за выполнение; стрик входов — ежедневная награда растёт до дня 7, «щит» прощает один пропуск в неделю; 18 достижений 🏅 с постоянными бонусами (+% к монетам, окно подсечки, нагрузка шнура).
7. **Награды 🎯**: три квеста дня (уловы, монеты, забросы) с наградой за выполнение; стрик входов — ежедневная награда растёт до дня 7, «щит» прощает один пропуск в неделю; 29 достижений 🏅 с постоянными бонусами (+% к монетам, окно подсечки, нагрузка шнура).
8. **День/ночь**: полный цикл суток ~5 минут (утро → день → вечер → ночь). Ночью свой состав рыб — сом, щука и окунь активнее, но без 🪯 фонарика клёва не будет — фонарик дают апгрейды удилищ (1-й апгрейд любого удилища).
9. **Осётр-босс**: при поимке осётра есть шанс встретить босса. Бой идёт в 3 фазы — босс «рвёт и уходит», и каждая следующая фаза глубже; удерживай, чтобы втащить. Награда ×5 к цене.
10. **Симулятор боёв (релиз 5)**: у рыбы есть выносливость — тонкая фиолетовая полоска под дистанцией: свежая рыба тянет долго и сильно, а уставшая (💤) вянет — втягивай её именно тогда. У катушек — тормоз (drag): рывок сильнее тормоза — леска скользит. Иногда поплавок не берёт вовсе — пустой клёв, подожди ещё немного.
@@ -34,7 +34,7 @@
- Погода: дождь (🌧️) — шанс серебряной/золотой/радужной ×2,5; туман (🌫️) — клёв ×1,5; событие идёт 120 с, следующее — раз в 2–4 минуты — ![дождь](screenshots/shot-weather-rain.png)
- Скины лодки и поплавка 🎨 — вкладка «Скины» в лавке: лодка «Красная лодка» 800 🪙 / «Лодка-домик» 2000 🪙, поплавок «Красный» 500 🪙 / «Неоновый» 1500 🪙 — чистая косметика (без эффектов на геймплей)
- Локации: Озёра (100 000 🪙) — судак, хариус, гигантский рак; свой состав днём/ночью, свой цвет воды; покупка и переключение во вкладке 🗺️ лавки — ![озёра](screenshots/shot-lake.png)
- Квесты дня (3 шт, генерация по дню), стрик входов со «щитом», 18 достижений с бонусами
- Квесты дня (3 шт, генерация по дню), стрик входов со «щитом», 29 достижений с бонусами
- Дневник рыбака — коллекция, максимальный улов по каждому виду, статистика
- Магазин с вкладками: Снасти 🎣, Дневник 📔, Награды 🎯, Достижения 🏅, Скины 🎨, Локации 🗺️
- Прогресс и коллекция сохраняются в `localStorage` (старые сейвы мигрируются автоматически)
@@ -57,12 +57,12 @@ python3 -m http.server 8000
## Тесты
Автотест прогоняет весь геймплей (бросок, подсечка, все исходы вываживания, магазин, дневник, сохранение, shiny-рыба, near-miss, миграция сейва, квесты, стрик, достижения, день/ночь с фонариком, босс-рыба в 3 фазы) в headless Chromium:
Автотест прогоняет весь геймплей (бросок, подсечка, все исходы вываживания, магазин, дневник, сохранение, варианты рыб (золотая/серебряная/радужная), near-miss, миграция сейва, квесты, стрик, достижения, день/ночь с фонариком, босс-рыба в 3 фазы) в headless Chromium:
```bash
cd tests
npm install # ставит puppeteer-core
node test.js # 117 проверок
node test.js # 128 проверок
```
Нужен установленный Chromium (`executablePath` в `test.js` при необходимости поправить).
+3 -3
View File
@@ -1,6 +1,6 @@
# Браузерный плейтест через Orca CLI
`tests/orca-playtest.js` — плейтест игры **в настоящем браузере**: он открывает вкладку встроенного браузера Orca app, прогоняет полный геймплей (меню → бросок → клёв → подсечка → улов → лавка (дневник, 🎯, 🏅) → shiny → near-miss → ночь → фонарик в лавке → босс-рыба) через Orca CLI и проверяет состояния, DOM и консоль.
`tests/orca-playtest.js` — плейтест игры **в настоящем браузере**: он открывает вкладку встроенного браузера Orca app, прогоняет полный геймплей (меню → бросок → клёв → подсечка → улов → лавка (дневник, 🎯, 🏅) → золотая рыба → near-miss → ночь → фонарик в лавке → босс-рыба) через Orca CLI и проверяет состояния, DOM и консоль.
В отличие от puppeteer-теста (`tests/test.js`), здесь нет отдельного headless Chromium — управление идёт командами CLI против браузера, которым пользуются люди. Это ловит то, что headless пропускает: отличия среды Orca-браузера, троттлинг фоновых вкладок, реальные CLI-нюансы.
@@ -82,9 +82,9 @@ PASS | зарядка: state=CHARGE
PASS | бросок: state=WAIT
PASS | клёв: state=BITE (через APPROACH)
PASS | подсечка: state=REEL
PASS | шини: улов получен (state=CARD)
PASS | золотая рыба: улов получен (state=CARD)
PASS | скриншот orca-shiny.png
PASS | шини: бейдж «ШИНИ» и золотая карточка | {"badge":"✨ ШИНИ!","gold":true,"hidden":false}
PASS | золотая рыба: бейдж «ЗОЛОТАЯ» и золотая карточка (gold-card) | {"badge":"ЗОЛОТАЯ","gold":true,"hidden":false}
PASS | зарядка: state=CHARGE
PASS | бросок: state=WAIT
PASS | клёв: state=BITE (через APPROACH)
+25
View File
@@ -430,3 +430,28 @@ git add -A && git commit -m "что изменилось" && git push
**Заметки (осознанные решения и ловушки):**
- Глобалы `cardInfo` и `VARIANT_CHAIN` убраны из игры — тесты и AGENTS.md обновлены в том же коммите.
- `sv/sn/rb` в сейве остались: дневник и `full_variants` работают как прежде, старые сейвы без миграции.
## Этап 22. Расширение достижений + тексты «shiny» → «золотая рыба»
**Что сделано в `index.html`:**
1. **Тексты** — «shiny» из видимых мест убран (M6: «shiny» = gold-вариант): квест `catch_shiny` → «Поймай золотую рыбу», desc ачивки `first_shiny` «Золотая жила» → «Поймай золотую рыбу». Идентификаторы `first_shiny`/`catch_shiny` и глобал `SHINY_CHANCE` не тронуты (тесты, старые сейвы); README/BROWSER-TESTING/лейблы orca-playtest приведены к «золотая рыба» (скрипт-файл `orca-shiny.png` имя не менял).
2. **11 новых достижений (18 → 29)** — по итогам Этапов 1–5:
- варианты (M6): `first_silver` «Серебро» (🥈, +1%), `first_rainbow` «Радужный улов» (🌈, +2%);
- погода (M8): `rain_variant` «Дождливый везунчик» — особая (≠normal) рыба в дождь (🌧️, +1%), `fog_catch` «Туманный улов» — любой улов в тумане (🌫️, +1%);
- скины (M-skins): `skin_first` «Красуйся» — куплен любой скин (🎨, +1%), `skins_all` «Стилист» — все 6 (🎭, +2%);
- локации (Этап 5): `lake_first` «Новые берега» — открыты Озёра (🗺️, +2%);
- снасти (M5): `gear_max` «Максимальная снасть» — снасть 4 ур. (⚒️, +1%), `titan` «Титан» — куплена удочка «Титан» (💪, +2%);
- новое: `weight_1000` «Тонна рыбы» — суммарно 1000+ кг (🚢, +2%), `night_first` «Ночной клёв» — первый ночной улов (🌙, +1%).
Все бонусы — `coinPct` (монеты); суммарная инфляция при всех ачивках ~+15% к монетам.
3. **Новые поля сейва** — `save.kgTotal` (сумма кг уловов), `save.nightCatch` (bool), `save.wxRainV`/`save.wxFog` (счётчики уловов в дождь с особым вариантом / в тумане). Дефолты в `save`/`freshSave()`; **миграция не нужна** — все чтения через `||0`/флаг, старые сейвы читаются (тест §10g(10)).
4. **Крючок в `catchFish()`** — до `checkAchievements()`: `kgTotal += w`; `dayPhase==='night'` → `nightCatch=true`; `weatherActive()==='rain' && variant!=='normal'` → `wxRainV++` (босс всегда normal — не заходит), `weatherActive()==='fog'` → `wxFog++`.
**Тесты:** +11 проверок в `tests/test.js` (§10g: тексты (desc/квест, «shiny» в desc нет); данные (11 id, `ACHIEVEMENTS.length===29`, уникальность, поля); геймплей — ночной улов в дождь с серебряным: крючки счётчиков (`nightCatch`, `wxRainV`, `kgTotal` ровно на w) и выдачи `first_silver`+`rain_variant`+`night_first` (туманного нет); `first_rainbow` по `rb=1`; `fog_catch` по `wxFog=1`; граница `weight_1000` (999.9/1000); скины (1 купленный → `skin_first`, все 6 → `skins_all`); `lake_first` ([0]→[0,1]); снасти (макс 3 ур. — нет, 4 ур./«Титан» — да); старый сейв без новых полей — без ложных выдач и ошибок, дефолты `freshSave`). Пин §10c(8) `full_variants` обновлён: сумма `achBonus().coinPct` 0.13 → **0.16** (туда попали `first_silver`+`first_rainbow`). Итог **128/128 PASS, exit 0** (`node tools/check.mjs` — OK).
**Monte-Carlo:** не требовался — физика вываживания не тронута; изменения — только тексты, достижения и счётчики сейва.
**Заметки (осознанные решения и ловушки):**
- Геймплей-тест ночного улова требует фонарика: `save.rodLv[save.rod]=2` (ветка WAIT: ночью без `hasLight()` клёв не стартует — таймер тикает вхолостую).
- «Все рыбы во всех вариантах» — отдельная ачивка не добавлялась: это существующая `full_variants` «Полная коллекция» (11×4).
- Порядок записей в `ACHIEVEMENTS` — тематический (снасти у экономики, варианты/погода/вес/ночь у рыболовных, скины/локации у магазинных); тесты не зависят от порядка (только `ACHIEVEMENTS.length` и id).
+23 -4
View File
@@ -410,13 +410,15 @@ let save={coins:0,rod:0,lure:0,rods:[0],lures:[0],rodLv:{0:1},lureLv:{0:1},caugh
snaps:0,ach:{},quests:null,streak:{last:null,days:0,shieldWeek:null},questsDoneTotal:0,
bossBeaten:false,bossCount:0,
boatSkins:[0],boatSkin:0,bobberSkins:[0],bobberSkin:0,
locations:[0],location:0}; // Этап 4: скины лодки/поплавка; Этап 5: локации (Река/Озёра)
locations:[0],location:0,
kgTotal:0,nightCatch:false,wxRainV:0,wxFog:0}; // Этап 4: скины; Этап 5: локации; Этап 22: счётчики ачивок (кг/ночь/погода)
function freshSave(muted){
return {coins:0,rod:0,lure:0,rods:[0],lures:[0],rodLv:{0:1},lureLv:{0:1},caught:{},muted:muted,casts:0,earned:0,
snaps:0,ach:{},quests:null,streak:{last:null,days:0,shieldWeek:null},questsDoneTotal:0,
bossBeaten:false,bossCount:0,
boatSkins:[0],boatSkin:0,bobberSkins:[0],bobberSkin:0,
locations:[0],location:0}; // Этап 4: скины лодки/поплавка; Этап 5: локации (Река/Озёра)
locations:[0],location:0,
kgTotal:0,nightCatch:false,wxRainV:0,wxFog:0}; // Этап 4: скины; Этап 5: локации; Этап 22: счётчики ачивок
}
function loadSave(){
try{ const s=localStorage.getItem(SAVE_KEY); if(s) Object.assign(save,JSON.parse(s)); }catch(e){}
@@ -622,7 +624,7 @@ function questText(q){
case 'catch_weight': return 'Поймай рыбу от '+q.param+' кг';
case 'casts': return 'Сделай '+q.goal+' забросов за день';
case 'catch_rarity': return 'Поймай рыбу редкости '+RARITY_NAMES[q.param]+'+';
case 'catch_shiny': return 'Поймай shiny-рыбу';
case 'catch_shiny': return 'Поймай золотую рыбу'; // M6: «shiny» = gold-вариант
}
return '?';
}
@@ -700,18 +702,29 @@ const ACHIEVEMENTS=[
{id:'casts_100', name:'Моряк', desc:'Сделай 100 забросов', icon:'⛵', bonus:{coinPct:0.01}, check:s=>s.casts>=100},
{id:'earned_1000', name:'Первая тысяча', desc:'Заработай 1000 монет', icon:'🪙', bonus:{coinPct:0.01}, check:s=>s.earned>=1000},
{id:'earned_10000', name:'Рыбацкий капитал',desc:'Заработай 10 000 монет', icon:'💰', bonus:{coinPct:0.02}, check:s=>s.earned>=10000},
{id:'gear_max', name:'Максимальная снасть',desc:'Улучши снасть до 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:'Титан', desc:'Купи удочку «Титан»', icon:'💪', bonus:{coinPct:0.02}, check:s=>(s.rods||[]).indexOf(3)>=0},
{id:'species_3', name:'Ботаник', desc:'Поймай 3 вида рыб', icon:'📖', bonus:{coinPct:0.01}, check:s=>SPECIES.filter(sp=>caughtN(s,sp.id)>0).length>=3},
{id:'species_8', name:'Энциклопедия', desc:'Поймай все виды', icon:'🏆', bonus:{coinPct:0.02}, check:s=>SPECIES.every(sp=>caughtN(s,sp.id)>0)}, // Этап 5: теперь все 11 видов (id старый сейв)
{id:'rare_fish', name:'Редкий гость', desc:'Поймай рыбу редкости «Редкая»+ ',icon:'⭐', bonus:{coinPct:0.01}, check:s=>SPECIES.some(sp=>sp.rarity>=2&&caughtN(s,sp.id)>0)},
{id:'legendary_fish',name:'Легенда', desc:'Поймай рыбу редкости «Легендарная»',icon:'👑',bonus:{coinPct:0.02}, check:s=>SPECIES.some(sp=>sp.rarity>=4&&caughtN(s,sp.id)>0)},
{id:'first_shiny', name:'Золотая жила', desc:'Поймай shiny-рыбу', icon:'✨', bonus:{coinPct:0.01}, check:s=>SPECIES.some(sp=>{const c=s.caught&&s.caught[sp.id]; return c&&c.sn>0;})},
{id:'first_shiny', name:'Золотая жила', desc:'Поймай золотую рыбу', icon:'✨', bonus:{coinPct:0.01}, check:s=>SPECIES.some(sp=>{const c=s.caught&&s.caught[sp.id]; return c&&c.sn>0;})},
{id:'first_silver', name:'Серебро', desc:'Поймай серебряную рыбу', icon:'🥈', bonus:{coinPct:0.01}, check:s=>SPECIES.some(sp=>{const c=s.caught&&s.caught[sp.id]; return c&&(c.sv||0)>0;})}, // Этап 22: M6 варианты
{id:'first_rainbow',name:'Радужный улов', desc:'Поймай радужную рыбу', icon:'🌈', bonus:{coinPct:0.02}, check:s=>SPECIES.some(sp=>{const c=s.caught&&s.caught[sp.id]; return c&&(c.rb||0)>0;})},
{id:'full_variants',name:'Полная коллекция',desc:'Поймай все виды во всех вариантах',icon:'🌈',bonus:{coinPct:0.05}, check:s=>SPECIES.every(sp=>{const c=s.caught&&s.caught[sp.id]; return c&&c.n>0&&(c.sv||0)>0&&(c.sn||0)>0&&(c.rb||0)>0;})},
{id:'weight_10', name:'Трофей', desc:'Поймай рыбу 10+ кг', icon:'⚖', bonus:{windowS:0.05}, check:s=>SPECIES.some(sp=>caughtBest(s,sp.id)>=10)},
{id:'weight_20', name:'Монстр глубин', desc:'Поймай рыбу 20+ кг', icon:'🐋', bonus:{line:5}, check:s=>SPECIES.some(sp=>caughtBest(s,sp.id)>=20)},
{id:'weight_1000', name:'Тонна рыбы', desc:'Поймай рыбы на 1000+ кг', icon:'🚢', bonus:{coinPct:0.02}, check:s=>(s.kgTotal||0)>=1000}, // Этап 22
{id:'night_first', name:'Ночной клёв', desc:'Поймай рыбу ночью', icon:'🌙', bonus:{coinPct:0.01}, check:s=>s.nightCatch===true},
{id:'rain_variant', name:'Дождливый везунчик',desc:'Поймай особую рыбу в дождь', icon:'🌧️', bonus:{coinPct:0.01}, check:s=>(s.wxRainV||0)>0}, // Этап 22: M8 погода
{id:'fog_catch', name:'Туманный улов', desc:'Поймай рыбу в тумане', icon:'🌫️', bonus:{coinPct:0.01}, check:s=>(s.wxFog||0)>0},
{id:'snaps_10', name:'Упёртый', desc:'10 обрывов шнура', icon:'💥', bonus:{coinPct:0.01}, check:s=>s.snaps>=10},
{id:'snaps_50', name:'Хроники обрывов', desc:'50 обрывов шнура', icon:'📜', bonus:{coinPct:0.01}, check:s=>s.snaps>=50},
{id:'streak_7', name:'Семёрка', desc:'Стрик 7 дней', icon:'🔥', bonus:{coinPct:0.01}, check:s=>s.streak&&s.streak.days>=7},
{id:'quests_5', name:'Дневник искателя',desc:'Выполни 5 квестов', icon:'🎯', bonus:{coinPct:0.01}, check:s=>s.questsDoneTotal>=5},
{id:'skin_first', name:'Красуйся', desc:'Купи скин лодки или поплавка', icon:'🎨', bonus:{coinPct:0.01}, check:s=>(s.boatSkins||[]).length>1||(s.bobberSkins||[]).length>1}, // Этап 22: M-skins
{id:'skins_all', name:'Стилист', desc:'Собери все скины лодки и поплавка',icon:'🎭',bonus:{coinPct:0.02}, check:s=>BOAT_SKINS.every(b=>(s.boatSkins||[]).indexOf(b.id)>=0)&&BOBBER_SKINS.every(b=>(s.bobberSkins||[]).indexOf(b.id)>=0)},
{id:'lake_first', name:'Новые берега', desc:'Открой локацию «Озёра»', icon:'🗺️', bonus:{coinPct:0.02}, check:s=>(s.locations||[]).indexOf(1)>=0}, // Этап 22: локации
{id:'boss_first', name:'Босс-рыбак', desc:'Победи Осётра-босса', icon:'🏆', bonus:{coinPct:0.03}, check:s=>(s.bossBeaten===true)||(s.bossCount>0)},
];
/* Сумма бонусов по выданным достижениям */
@@ -1120,6 +1133,12 @@ function catchFish(sp){
questHit('catch_rarity',q=>sp.rarity>=q.param);
questHit('catch_shiny',q=>shiny); // M6: «shiny-рыба» = gold-вариант
questHit('earn_coins',()=>true,val); // прогресс — монеты улова (с бонусами)
// Этап 22: счётчики достижений — общий вес, ночной улов, уловы в дождь/туман
save.kgTotal=(save.kgTotal||0)+w;
if(dayPhase==='night') save.nightCatch=true;
const wx=weatherActive();
if(wx==='rain'&&variant.id!=='normal') save.wxRainV=(save.wxRainV||0)+1; // босс всегда normal — не заходит
if(wx==='fog') save.wxFog=(save.wxFog||0)+1;
checkAchievements();
persist(); updateHUD();
sfx('catch'); vib([40,60,40]);
+3 -3
View File
@@ -281,17 +281,17 @@ async function forceCatch() {
await waitCond('document.getElementById("shop").classList.contains("hidden") ? "yes" : "shop open"', 15000, 'лавка закрыта');
ok('лавка закрыта', true);
// 8. Shiny (M6: gold-вариант): форсируем шанс, полный цикл, бейдж «ЗОЛОТАЯ» в DOM
// 8. Золотая рыба (M6: gold-вариант): форсируем шанс, полный цикл, бейдж «ЗОЛОТАЯ» в DOM
await evalPage('SHINY_CHANCE = 1; "ok"');
try {
await castToWait();
await forceBite();
await hookIt();
await forceCatch();
ok('шини: улов получен (state=CARD)', true);
ok('золотая рыба: улов получен (state=CARD)', true);
await shot('orca-shiny.png');
const shiny = await evalPage('({badge: document.getElementById("cardVariant").textContent, hidden: document.getElementById("cardVariant").classList.contains("hidden"), gold: document.getElementById("card").classList.contains("gold-card")})');
ok('шини: бейдж «ЗОЛОТАЯ» и золотая карточка (gold-card)',
ok('золотая рыба: бейдж «ЗОЛОТАЯ» и золотая карточка (gold-card)',
!!(shiny && shiny.hidden === false && String(shiny.badge).indexOf('ЗОЛОТАЯ') >= 0 && shiny.gold === true),
JSON.stringify(shiny).slice(0, 120));
} finally {
+199 -8
View File
@@ -239,7 +239,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
&& afterReset.menu, JSON.stringify(afterReset));
ok('сброс: localStorage перезаписан', afterReset.ls === null || JSON.parse(afterReset.ls).coins === 0);
// --- 9a. Шини: форсированный шанс — монеты ×3, счётчик sn, бейдж в карточке ---
// --- 9a. Золотая рыба: форсированный шанс — монеты ×3, счётчик sn, бейдж в карточке ---
await page.click('#btnPlay');
await sleep(150);
// M2-гвард: квесты и стрик не должны случайно выдать монеты и исказить точную проверку
@@ -272,7 +272,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
goldCard: document.getElementById('card').classList.contains('gold-card'),
}));
const shinyBase = shinyInfo.w * shinyInfo.price * 3;
ok('шини: монеты = вес×цена×3 (±5%), sn=1, бейдж «ЗОЛОТАЯ» в карточке',
ok('золотая рыба: монеты = вес×цена×3 (±5%), sn=1, бейдж «ЗОЛОТАЯ» в карточке',
shinyInfo.val >= Math.max(1, Math.round(shinyBase * 0.9))
&& shinyInfo.val <= Math.round(shinyBase * 1.1)
&& shinyInfo.coins === coinsBeforeShiny + shinyInfo.val
@@ -1404,12 +1404,12 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
const a2 = { coins: save.coins };
return { coinsBefore, a1, a2 };
});
ok('M6 full_variants: все 8 видов во всех вариантах → +5000 ровно один раз, achBonus().coinPct включает 0.05 (сумма 0.13), повторный вызов — без выплаты',
m6fv.a1.got === true
&& m6fv.a1.coins === m6fv.coinsBefore + 5000
&& Math.abs(m6fv.a1.pct - 0.13) < 1e-9
&& m6fv.a2.coins === m6fv.a1.coins,
JSON.stringify(m6fv));
ok('M6 full_variants: все 8 видов во всех вариантах → +5000 ровно один раз, achBonus().coinPct включает 0.05 (сумма 0.16: +first_silver/first_rainbow, Этап 22), повторный вызов — без выплаты',
m6fv.a1.got === true
&& m6fv.a1.coins === m6fv.coinsBefore + 5000
&& Math.abs(m6fv.a1.pct - 0.16) < 1e-9
&& m6fv.a2.coins === m6fv.a1.coins,
JSON.stringify(m6fv));
// сброс тест-форсов
await page.evaluate(() => { forceBite = false; forceVariant = null; forceBoss = false; SHINY_CHANCE = 0.03; });
@@ -1838,6 +1838,197 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
closeShop();
});
// --- 10g. Этап 22: расширение достижений + тексты shiny ---
// (1) тексты: first_shiny.desc и квест catch_shiny — «золотую рыбу», «shiny» в desc нет
const a22t = await page.evaluate(() => ({
achDesc: ACHIEVEMENTS.find(a => a.id === 'first_shiny').desc,
quest: questText({ tpl: 'catch_shiny', param: 0, goal: 1 }),
noShiny: ACHIEVEMENTS.every(a => a.desc.indexOf('shiny') < 0)
}));
ok('Этап 22 тексты: first_shiny.desc и квест catch_shiny — «Поймай золотую рыбу»; «shiny» в описаниях достижений нет',
a22t.achDesc === 'Поймай золотую рыбу' && a22t.quest === 'Поймай золотую рыбу' && a22t.noShiny,
JSON.stringify(a22t));
// (2) данные: 11 новых достижений, ACHIEVEMENTS.length===29, 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'];
const list = ACHIEVEMENTS.map(a => a.id);
return {
total: ACHIEVEMENTS.length,
all: ids.every(id => list.indexOf(id) >= 0),
unique: new Set(list).size === list.length,
fields: ids.every(id => { const a = ACHIEVEMENTS.find(x => x.id === id);
return a && a.name && a.desc && a.icon && a.bonus && typeof a.check === 'function'; })
};
});
ok('Этап 22 данные: 11 новых достижений (варианты/погода/скины/локации/снасти/вес/ночь), ACHIEVEMENTS.length===29, id уникальны, поля на месте',
a22d.all === true && a22d.total === 29 && a22d.unique === true && a22d.fields === true,
JSON.stringify(a22d));
// (3) геймплей: ночной улов в дождь, серебряный вариант — одним уловом проверяются
// крючки счётчиков (kgTotal += w, nightCatch, wxRainV) и 3 достижения
await page.evaluate(() => {
state = 'IDLE'; reel = null; biteFish = null;
save.ach = {}; save.caught = {}; save.coins = 0;
save.quests = { date: todayStr(), list: [] };
save.streak = { last: todayStr(), days: 1, shieldWeek: null };
save.kgTotal = 0; save.nightCatch = false; save.wxRainV = 0; save.wxFog = 0;
save.rodLv[save.rod] = 2; // фонарик — ночью без него клёв не стартует (WAIT)
setDayTime(0.8); // ночь
forceWeather = 'rain'; // дождь (форс приоритетнее естественной погоды)
forceVariant = 'silver';// вариант != normal
forceBite = true; biteTimer = 0.05;
});
await page.mouse.move(195, 600);
await page.mouse.down(); await sleep(450); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
await page.evaluate(() => {
window.__a22W = reel.w;
reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5;
});
await page.mouse.down();
await waitState(page, 'CARD', 5000);
await page.mouse.up();
const a22n = await page.evaluate(() => ({
w: window.__a22W,
kg: save.kgTotal,
night: save.nightCatch,
rainV: save.wxRainV,
fog: save.wxFog,
ach: {
silver: !!save.ach.first_silver,
rain: !!save.ach.rain_variant,
night: !!save.ach.night_first,
fogA: !!save.ach.fog_catch
},
phase: dayPhase
}));
ok('Этап 22 ночной дождь: nightCatch=true, wxRainV=1 (wxFog=0), kgTotal вырос ровно на w (первый улов)',
a22n.phase === 'night' && a22n.night === true && a22n.rainV === 1 && a22n.fog === 0
&& a22n.kg === a22n.w,
JSON.stringify(a22n));
ok('Этап 22 ночной дождь: выданы first_silver + rain_variant + night_first (fog_catch нет)',
a22n.ach.silver === true && a22n.ach.rain === true && a22n.ach.night === true && a22n.ach.fogA === false,
JSON.stringify(a22n.ach));
await page.click('#btnCardOk');
await sleep(100);
await page.evaluate(() => {
forceBite = false; forceVariant = null; forceWeather = null; weather = null;
setDayTime(0.3); // обратно к дню
});
// (4) first_rainbow: rb=1 → выдано; silver/gold — нет
const a22rb = await page.evaluate(() => {
save.ach = {};
save.caught = { carass: { n: 1, best: 1, sn: 0, sv: 0, rb: 1 } };
checkAchievements();
const got = { rb: !!save.ach.first_rainbow, sv: !!save.ach.first_silver, gold: !!save.ach.first_shiny };
save.caught = {};
return got;
});
ok('Этап 22 first_rainbow: улов rb=1 → выдано; first_silver/first_shiny — нет',
a22rb.rb === true && a22rb.sv === false && a22rb.gold === false,
JSON.stringify(a22rb));
// (5) fog_catch: wxFog=1 → выдано (счётчик ставится уловом в тумане)
const a22f = await page.evaluate(() => {
save.ach = {};
save.wxRainV = 0; save.wxFog = 1;
checkAchievements();
const got = { fog: !!save.ach.fog_catch, rain: !!save.ach.rain_variant };
save.wxFog = 0;
return got;
});
ok('Этап 22 fog_catch: wxFog=1 → выдано; rain_variant нет (wxRainV=0)',
a22f.fog === true && a22f.rain === false,
JSON.stringify(a22f));
// (6) weight_1000: граница — 999.9 не даёт, 1000 даёт
const a22kg = await page.evaluate(() => {
save.ach = {};
save.kgTotal = 999.9;
checkAchievements();
const notYet = !!save.ach.weight_1000;
save.kgTotal = 1000;
checkAchievements();
const got = !!save.ach.weight_1000;
save.kgTotal = 0;
return { notYet, got };
});
ok('Этап 22 weight_1000: kgTotal 999.9 — не выдано; 1000 — выдано (граница)',
a22kg.notYet === false && a22kg.got === true,
JSON.stringify(a22kg));
// (7) скины: boatSkins=[0,1] → skin_first (skins_all нет); все 6 → skins_all
const a22sk = await page.evaluate(() => {
save.ach = {};
save.boatSkins = [0, 1]; save.bobberSkins = [0];
checkAchievements();
const s1 = { f: !!save.ach.skin_first, a: !!save.ach.skins_all };
save.boatSkins = [0, 1, 2]; save.bobberSkins = [0, 1, 2];
checkAchievements();
const s2 = { f: !!save.ach.skin_first, a: !!save.ach.skins_all };
save.boatSkins = [0]; save.bobberSkins = [0];
return { s1, s2 };
});
ok('Этап 22 скины: boatSkins [0,1] → skin_first, skins_all нет; все 6 скинов → skins_all',
a22sk.s1.f === true && a22sk.s1.a === false && a22sk.s2.f === true && a22sk.s2.a === true,
JSON.stringify(a22sk));
// (8) lake_first: locations [0] — не выдано; [0,1] — выдано
const a22lk = await page.evaluate(() => {
save.ach = {};
save.locations = [0];
checkAchievements();
const notYet = !!save.ach.lake_first;
save.locations = [0, 1];
checkAchievements();
const got = !!save.ach.lake_first;
save.locations = [0];
return { notYet, got };
});
ok('Этап 22 lake_first: locations [0] — не выдано; [0,1] (Озёра) — выдано',
a22lk.notYet === false && a22lk.got === true,
JSON.stringify(a22lk));
// (9) gear_max/titan: максимум 3 ур. — не выдано; lureLv=4 → gear_max; rods с 3 → titan
const a22g = await page.evaluate(() => {
save.ach = {};
save.rodLv = { 0: 1, 1: 3 }; save.lureLv = { 0: 1 };
save.rods = [0, 1];
checkAchievements();
const notYet = { g: !!save.ach.gear_max, t: !!save.ach.titan };
save.rodLv = { 0: 1, 1: 3 }; save.lureLv = { 0: 4 };
save.rods = [0, 1, 3];
checkAchievements();
const got = { g: !!save.ach.gear_max, t: !!save.ach.titan };
save.rodLv = { 0: 1 }; save.lureLv = { 0: 1 }; save.rods = [0];
return { notYet, got };
});
ok('Этап 22 снасти: макс 3 ур. — gear_max/titan не выданы; lureLv=4 → gear_max, «Титан» (rods 3) → titan',
a22g.notYet.g === false && a22g.notYet.t === false && a22g.got.g === true && a22g.got.t === true,
JSON.stringify(a22g));
// (10) старый сейв: без новых полей — без ложных выдач и ошибок; дефолты freshSave
const a22m = await page.evaluate(() => {
const s = { coins: 0, caught: {}, ach: {}, casts: 0, earned: 0, snaps: 0,
bossBeaten: false, bossCount: 0, streak: { last: null, days: 0, shieldWeek: null },
questsDoneTotal: 0, rodLv: { 0: 1 }, lureLv: { 0: 1 }, rods: [0], lures: [0],
boatSkins: [0], bobberSkins: [0], locations: [0] };
const granted = ACHIEVEMENTS.filter(a => a.check(s)).map(a => a.id);
const fresh = freshSave(false);
return { granted, fresh: { kg: fresh.kgTotal, night: fresh.nightCatch, rainV: fresh.wxRainV, fog: fresh.wxFog } };
});
ok('Этап 22 старый сейв: сейв без kgTotal/nightCatch/wxRainV/wxFog — ложных выдач нет, ошибок нет; freshSave — дефолты 0/false/0/0',
a22m.granted.length === 0 && a22m.fresh.kg === 0 && a22m.fresh.night === false
&& a22m.fresh.rainV === 0 && a22m.fresh.fog === 0,
JSON.stringify(a22m));
// --- 11. Ошибки консоли ---
ok('нет ошибок JS (pageerror)', pageErrors.length === 0, pageErrors.join(' | '));
ok('нет ошибок консоли', consoleErrors.length === 0, consoleErrors.join(' | '));