Ачивки: Этап 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:
@@ -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.
|
||||
|
||||
## Запуск и публикация
|
||||
|
||||
|
||||
@@ -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 минуты — 
|
||||
- Скины лодки и поплавка 🎨 — вкладка «Скины» в лавке: лодка «Красная лодка» 800 🪙 / «Лодка-домик» 2000 🪙, поплавок «Красный» 500 🪙 / «Неоновый» 1500 🪙 — чистая косметика (без эффектов на геймплей)
|
||||
- Локации: Озёра (100 000 🪙) — судак, хариус, гигантский рак; свой состав днём/ночью, свой цвет воды; покупка и переключение во вкладке 🗺️ лавки — 
|
||||
- Квесты дня (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` при необходимости поправить).
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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]);
|
||||
|
||||
@@ -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
@@ -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(' | '));
|
||||
|
||||
Reference in New Issue
Block a user