M5: Этап 1 — апгрейды снастей (lv 1..4, save.rodLv/lureLv + миграция), фонарик → первый апгрейд удилища (LURES id=4 удалён), гейтинг лавки lv(i-1)===4; по ревью: доки/скриншот/индентация/NIT. 82/82, exit 0
This commit is contained in:
@@ -11,7 +11,7 @@
|
||||
npm install # puppeteer-core
|
||||
npm test # = node test.js
|
||||
```
|
||||
Ждать все PASS и exit 0. Сейчас проверок 68.
|
||||
Ждать все PASS и exit 0. Сейчас проверок 82.
|
||||
3. Браузерный плейтест (вкладка встроенного браузера Orca, через Orca CLI):
|
||||
```bash
|
||||
node tests/orca-playtest.js # из каталога репо; требует ЗАПУЩЕННЫЙ Orca app и orca-ide (или ORCA_CLI_COMMAND)
|
||||
@@ -28,8 +28,8 @@
|
||||
|
||||
## Где крутить
|
||||
|
||||
- Баланс (рыба/удилища/приманки): константы `SPECIES`, `RODS`, `LURES` в начале скрипта.
|
||||
- Ночь (M3): длительность суток `DAY_LENGTH` (сек), ночная таблица рыб `SPECIES_NIGHT`, фонарик — `LURES id=4` (секция «M3: Цикл дня/ночи»).
|
||||
- Баланс (рыба/удилища/приманки): константы `SPECIES`, `RODS`, `LURES` в начале скрипта. Апгрейды снастей (уровни 1..4): `ROD_UPG_COST`/`LURE_UPG_COST`/`ROD_UPG_NEXT`/`LURE_UPG_NEXT` (ДАННЫЕ), уровни `save.rodLv`/`save.lureLv`, эффективные статы — `effRod()`/`effLure()` (глобалы `rod()`/`lure()` отдают эффективные), гейтинг открытия — в `itemRow()`/`shopClick()`.
|
||||
- Ночь (M3): длительность суток `DAY_LENGTH` (сек), ночная таблица рыб `SPECIES_NIGHT`, фонарик — апгрейд 1 любого удилища (`save.rodLv[save.rod]>=2`, проверка `hasLight()` в ветке WAIT, секция «M3: Цикл дня/ночи»).
|
||||
- Босс (M4): тест-форс появления `forceBoss`, фазы боя `stateBoss`/`bossPhase`, босс-ветки в `updateReel()` (обрыв фазы) и `SNAP` в `updateGame()` (авто-переход), награда ×5 в `catchFish()`.
|
||||
- Сложность вываживания: `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) и править тесты в том же коммите.
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
5. **У удочек есть предел веса** (3–40 кг): зацепишь более тяжёлую — шнур лопнет почти мгновенно. Чем лучше удочка и приманка, тем выше шанс зацепить крупную. Новый максимум по виду отмечен бейджем «🏆 РЕКОРД!» на карточке улова.
|
||||
6. На монеты — новые удилища и приманки в магазине 🛒.
|
||||
7. **Награды 🎯**: три квеста дня (уловы, монеты, забросы) с наградой за выполнение; стрик входов — ежедневная награда растёт до дня 7, «щит» прощает один пропуск в неделю; 17 достижений 🏅 с постоянными бонусами (+% к монетам, окно подсечки, нагрузка шнура).
|
||||
8. **День/ночь**: полный цикл суток ~5 минут (утро → день → вечер → ночь). Ночью свой состав рыб — сом, щука и окунь активнее, но без 🪯 фонарика из лавки клёва не будет.
|
||||
8. **День/ночь**: полный цикл суток ~5 минут (утро → день → вечер → ночь). Ночью свой состав рыб — сом, щука и окунь активнее, но без 🪯 фонарика клёва не будет — фонарик дают апгрейды удилищ (1-й апгрейд любого удилища).
|
||||
9. **Осётр-босс**: при поимке осётра есть шанс встретить босса. Бой идёт в 3 фазы — босс «рвёт и уходит», и каждая следующая фаза глубже; удерживай, чтобы втащить. Награда ×5 к цене.
|
||||
10. **Симулятор боёв (релиз 5)**: у рыбы есть выносливость — тонкая фиолетовая полоска под дистанцией: свежая рыба тянет долго и сильно, а уставшая (💤) вянет — втягивай её именно тогда. У катушек — тормоз (drag): рывок сильнее тормоза — леска скользит. Иногда поплавок не берёт вовсе — пустой клёв, подожди ещё немного.
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
- Удилища: предел веса, прочность шнура, скорость подсечки, бонус на крупную рыбу
|
||||
- Приманки: частота клёва, дальность броска, шанс редких и крупных
|
||||
- Физика боя: выносливость рыбы (уставшая 💤 вянет и втягивается быстрее), тормоз катушки (drag) — сильнейший рывок проскальзывает, пустые клёвы
|
||||
- Цикл дня и ночи (~5 мин на сутки): ночью своя таблица рыб (сом/щука/окунь активнее), без 🪯 фонарика ночью клёва нет
|
||||
- Цикл дня и ночи (~5 мин на сутки): ночью своя таблица рыб (сом/щука/окунь активнее), ночью нужен 🪯 фонарик — апгрейд 1 удилища
|
||||
- Апгрейды снастей: удилища и приманки улучшаются до 4 уровня (свет фонарика, крепость лески, тормоз, катушка, клёв, редкие и крупные рыбы); следующая снасть в лавке открывается после 4 уровня предыдущей
|
||||
- Босс-рыба — Осётр-босс (шанс при поимке осётра): 3 фазы «рвёт и уходит», награда ×5, достижение «Босс-рыбак»
|
||||
- Квесты дня (3 шт, генерация по дню), стрик входов со «щитом», 17 достижений с бонусами
|
||||
- Дневник рыбака — коллекция, максимальный улов по каждому виду, статистика
|
||||
|
||||
@@ -46,7 +46,7 @@ node tests/orca-playtest.js # скриншоты orca-*.png кладутся
|
||||
| 8 | `SHINY_CHANCE = 1` → полный цикл → скриншот → проверка DOM → `SHINY_CHANCE = 0.03` | бейдж «ШИНИ» показан, золотая карточка |
|
||||
| 9 | цикл до `REEL` → `reel.d = reel.d0 * 1.4 + 2` → polling `WAIT` **с чтением тоста в том же eval** | тост содержит «Сбежала» |
|
||||
| 10 | `setDayTime(0.85)` **с чтением `dayPhase` в том же eval** → скриншот | `dayPhase === "night"` |
|
||||
| 11 | «🛒» → вкладка «Снасти» → поиск «Фонарик» в `#shopBody` | в снастях есть «Фонарик» (500 🪙, ночь) |
|
||||
| 11 | «🛒» → вкладка «Снасти» → поиск «фонарик» в `#shopBody` | в снастях есть апгрейд-фонарик (⬆ 400 🪙, пометка про ночь) |
|
||||
| 12 | `forceBoss = true` + `pickSpecies` → осётр → полный цикл: `REEL` (старт боя: `stateBoss`, `bossPhase=1`) → 3 фазы (в каждой `reel.d = 0.4` → `SNAP` **со сбросом `holding` в том же eval** → авто-`REEL` следующей фазы; фаза 3 → `CARD`) → скриншот → `forceBoss = false` в `finally` | старт босс-боя, карточка после 3 фаз (`stateBoss=false`), имя «Осётр-БОСС» с бейджем |
|
||||
| 13 | `console --limit 100` | нет сообщений уровня `error` |
|
||||
| 13b | чтение `window.__errs` | нет pageerror за прогон (JS-ошибки и unhandledrejection) |
|
||||
|
||||
@@ -319,3 +319,24 @@ git add -A && git commit -m "что изменилось" && git push
|
||||
| 13 | Стартовый клёв p90 13.4 с и ночной простой ~25% суток без фонарика — на границе комфорта первых минут | T4: бот 100 забросов стартовой снастью | Не блокер — в следующий спринт (NIT) |
|
||||
|
||||
**Итог.** Вердикт T4: «Релиз готов» — все рубежи зелёные (68/68 ×3, 47/47 live, 11/11 скриншотов), крашей и pageerror нет, сейв/миграции не трогались (новых персистентных полей нет: `st`/`tired` — поля боя, `drag` — константа RODS, `forceBite` — тест-форс). Балансные находки (камни 10–13) — не блокеры, backlog следующего спринта.
|
||||
|
||||
## Этап 16. M5: Апгрейды снастей (уровни 1..4, фонарик, гейтинг лавки)
|
||||
|
||||
**Что сделано в `index.html`:**
|
||||
|
||||
1. **Уровни снастей 1..4** — новые поля сейва `save.rodLv`/`save.lureLv` (объект «уровень по id»; купленная снасть — уровень 1). Эффективные статы — `effRod()`/`effLure()` (база × множители уровней): удилище Lv2 — леска ×1.15, Lv3 — тормоз ×1.15 и катушка ×1.10, Lv4 — леска ещё ×1.15 и крупные +0.10; приманка Lv2 — клёв ×1.15, Lv3 — редкие +1.0, Lv4 — крупные +0.15. Стоимость переходов 1→2/2→3/3→4: `ROD_UPG_COST=[400,1200,3000]`, `LURE_UPG_COST=[300,900,2200]`; подписи «что даёт следующий уровень» — `ROD_UPG_NEXT`/`LURE_UPG_NEXT`. UI лавки: пипсы ●/○ и «N ур.» у купленной снасти, кнопка ⬆ со стоимостью (на 4 ур. исчезает), у закрытых строк — 🔒.
|
||||
2. **Фонарик больше не приманка** — `LURES id=4` удалён из данных (`LURES.length===4`, поле `nightOnly` убрано): фонарик — апгрейд 1 любого удилища (2+ уровень): `hasLight()` — `rodLvOf(save.rod)>=2`, проверка в ветке WAIT (ночью без фонарика клёв не стартует, таймер тикает вхолостую, тост разовый — флаг `nightNoLightToast`, сброс новым забросом).
|
||||
3. **Гейтинг открытия** — снасть №i открывается, когда предыдущая снасть того же типа на 4 ур. (`lv(i−1)===4`): в UI строка `locked` + 🔒 + «Откроется: улучши «…» до 4 ур.», в `shopClick()` buy-ветке — двойная защита (при закрытом гейтинге покупка не проходит). Покупка разблокированной снасти стартует с уровня 1.
|
||||
4. **Миграция старых сейвов** (`loadSave()`): нет `rodLv`/`lureLv` → купленным присваивается уровень 1; приманка `id=4` вычёркивается из `save.lures`, `save.lure===4` → 0. Старые сейвы читаются (проверяется тестом §10b(1)).
|
||||
|
||||
**Тесты:** +14 проверок в `tests/test.js` (§10b: миграция сейва; данные — `LURES.length===4`, без `nightOnly`; точные эффективные статы удочки и приманки на всех 4 уровнях; `hasLight()`; гейтинг лавки (locked/🔒/нет buy → снятие после 4 ур.); двойная защита buy; покупка апгрейда (1000→600 🪙, фонарик включился); макс. уровень (кнопки upgrade нет); не хватает монет; ночь без/с фонариком; покупка разблокированной снасти — уровень 1). Итог **82/82 PASS, exit 0** (`node tools/check.mjs` — OK).
|
||||
|
||||
**БЛОК БАЛАНСА (M-1: «принято без ребаланса»).** Ревью подняло вопрос: не тривиализирует ли M5 босса на максимальной снасти (камень #10 этапа 15: «босс на Титане too_easy: 20/20 побед, ни одного опасного состояния»)? Monte-Carlo по точным формулам `updateReel()` (скрипт `/tmp/opencode/boss_mc.mjs`, N=400/сценарий, dt=1/60, боты §4.4 доки) для **босс / Титан lv4 + Блёстка lv4** (line 342.25, drag 100.05, reel 2.53, size 0.95):
|
||||
|
||||
| Бот | Исход |
|
||||
|---|---|
|
||||
| «умный» (держать в calm / отпускать в pull) | **43.5%** улов / SNAP 0 / сход 56.5% |
|
||||
| «держать» (всегда держать) | **77.3%** улов / **SNAP 22.8%** |
|
||||
| пассив | 100% сход |
|
||||
|
||||
Модель валидирована на эталоне этапа 15 (босс/Титан lv1): smart 55.8% vs доки 59.5%, hold 67.3/32.8 vs 62/38 — в пределах MC-шума (N=400, σ≈2.5–3 п.п.). Числа lv4-кейса: size-апгрейд (0.95) утяжеляет босса — E[w]≈25.7 кг (против ~20 кг на lv1), d0≈225 px — и именно это режет «умный» улов вдвое (59.5→43.5%), а «держать» остаётся доходным, но рвёт леску в ~23% боёв. **Вывод: 100%-стратегии нет, hold рвётся ~23% — тривиализации нет; M-1 принят без ребаланса.** M5 расширяет разрыв камня #10 (босс с прогрессией снастей стал *ещё* доступнее, но остался честным боем) — принято осознанно: снасти — награда за прогресс, а риск босса держит физика (stamina/drag/near-miss), не гейтинг.
|
||||
|
||||
@@ -34,8 +34,9 @@
|
||||
`price` (🪙/кг). `SPECIES_NIGHT` — клон с ночными весами.
|
||||
- `RODS` (4 удочки): `maxW` (предел веса), `line` (прочность шнура 100/140/190/260),
|
||||
`reel` (скорость втягивания 1.0–2.3), `size` (бонус на крупную).
|
||||
- `LURES` (5 приманок): `cast` (дальность), `bite` (частота клёва), `rare`
|
||||
(шанс редких/крупных), `nightOnly` (фонарик).
|
||||
- `LURES` (4 приманки): `cast` (дальность), `bite` (частота клёва), `rare`
|
||||
(шанс редких/крупных). Поле `nightOnly` удалено (M5): фонарик — не
|
||||
приманка, а апгрейд 1 любого удилища (2+ уровень, `hasLight()`).
|
||||
- `SHINY_CHANCE` (3%), `forceBoss` (тест-форс босса).
|
||||
|
||||
### 1.2 Бросок
|
||||
@@ -50,8 +51,11 @@
|
||||
|
||||
### 1.3 Клёв и подсечка
|
||||
|
||||
- `WAIT`: `biteTimer` тикает; при `≤ 0` — всегда `startBite()` (кроме ночи
|
||||
без фонарика). **Пустых клёв нет: 100% забросов приводят к рыбе.**
|
||||
- `WAIT`: `biteTimer` тикает; при `≤ 0` — ролл `forceBite || Math.random() <
|
||||
biteChance()` → `startBite()`, иначе новый таймер `rand(2.5,5)/lure().bite`
|
||||
(пустые клёвы ЕСТЬ: «молчание» ~10–35% забросов, ночью тише — `×0.85`).
|
||||
Ночью без фонарика (M5: апгрейд 1 удилища) таймер тикает вхолостую —
|
||||
клёв не стартует.
|
||||
- `startBite()`: `pickSpecies(dist)` (вес вида × редкость-бонус приманки,
|
||||
штраф `×0.06` за близкую дистанцию); осётр → босс с шансом 30% (`forceBoss`
|
||||
форсирует). Рыба плывёт к поплавку за 0.45 с (`APPROACH`,
|
||||
@@ -612,6 +616,7 @@ stamina-дуга не видна. Т2b подогнал числа (только
|
||||
| сом/Карбон (крупная) | 66% / 0 / 34% — 20.4 с (уловы) | 79% / SNAP 21% | 100% сход, 5.7 с |
|
||||
| сом/Стекло (крупная, слабая) | 39% / 0 / 62% | 39% / **SNAP 61%** | 100% сход |
|
||||
| босс/Титан | **59.5%** / 0 / 40% — 42.8 с | 62% / **SNAP 38%** | 100% сход, 6.7 с |
|
||||
| босс/Титан lv4 + Блёстка lv4 (M5 max) | **43.5%** / 0 / 56.5% — 55.2 с | 77.3% / **SNAP 22.8%** — 25.8 с | 100% сход |
|
||||
| босс/Карбон | 0% / 0 / 100% (недогрейд) | 40% / SNAP 60% | 100% сход |
|
||||
|
||||
Смысл метрик: умный >80% мелочи ✓, крупная 50–80% ✓ (66% на Карбоне;
|
||||
|
||||
+94
-19
@@ -91,6 +91,11 @@ button{font-family:inherit;cursor:pointer}
|
||||
.buybtn.owned:hover{filter:none;background:rgba(255,255,255,.22)}
|
||||
.buybtn.current{background:#2ecc71;color:#04263d;box-shadow:0 2px 8px rgba(46,204,113,.4)}
|
||||
.buybtn:disabled{opacity:.45;cursor:default}
|
||||
.buybtn.upgbt{background:#5c55a8}
|
||||
.rowbtns{display:flex;gap:6px;flex-shrink:0}
|
||||
.lvtag{color:#ffd75e;font-weight:800;font-size:12px;margin-left:5px;white-space:nowrap}
|
||||
.item.locked{opacity:.5}
|
||||
.item.locked:hover{background:rgba(255,255,255,.06);border-color:rgba(255,255,255,.09);transform:none}
|
||||
.statsline{font-size:13px;color:#9fc3e2;margin-bottom:6px}
|
||||
.jrow{display:flex;align-items:center;gap:10px;padding:9px 10px;background:rgba(255,255,255,.05);
|
||||
border:1px solid rgba(255,255,255,.06);border-radius:10px;margin-bottom:6px;font-size:14px;text-align:left;
|
||||
@@ -279,18 +284,23 @@ const LURES=[
|
||||
{id:1, name:'Червь', cost:150, cast:1.00, bite:1.3, rare:0.2, size:0.12, desc:'Клёв на 30% чаще, шанс крупной выше'},
|
||||
{id:2, name:'Воблер', cost:450, cast:1.15, bite:1.2, rare:0.8, size:0.28, desc:'Дальше бросок, приманивает крупных'},
|
||||
{id:3, name:'Золотая блёстка',cost:1200, cast:1.20, bite:1.4, rare:2.0, size:0.50, desc:'Лучшее, что есть. К ней плывут легенды'},
|
||||
{id:4, name:'Фонарик', cost:500, cast:1.00, bite:1.0, rare:0.0, size:0.00, nightOnly:true, desc:'Позволяет рыбачить ночью'},
|
||||
];
|
||||
/* M5: апгрейды снастей — уровни 1..4 (макс), стоимость переходов 1→2, 2→3, 3→4 */
|
||||
const ROD_UPG_COST=[400,1200,3000];
|
||||
const LURE_UPG_COST=[300,900,2200];
|
||||
/* Что даёт следующий уровень (индекс = текущий уровень − 1) — подпись в лавке */
|
||||
const ROD_UPG_NEXT=['фонарик (ночь), леска ×1.15','тормоз ×1.15, катушка ×1.10','леска ещё ×1.15, крупные +0.10'];
|
||||
const LURE_UPG_NEXT=['клёв ×1.15','редкие +1.0','крупные +0.15'];
|
||||
|
||||
/* =========================================================
|
||||
СОХРАНЕНИЕ
|
||||
========================================================= */
|
||||
const SAVE_KEY='fishing-save-v1';
|
||||
let save={coins:0,rod:0,lure:0,rods:[0],lures:[0],caught:{},muted:false,casts:0,earned:0,
|
||||
let save={coins:0,rod:0,lure:0,rods:[0],lures:[0],rodLv:{0:1},lureLv:{0:1},caught:{},muted:false,casts:0,earned:0,
|
||||
snaps:0,ach:{},quests:null,streak:{last:null,days:0,shieldWeek:null},questsDoneTotal:0,
|
||||
bossBeaten:false,bossCount:0};
|
||||
function freshSave(muted){
|
||||
return {coins:0,rod:0,lure:0,rods:[0],lures:[0],caught:{},muted:muted,casts:0,earned:0,
|
||||
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};
|
||||
}
|
||||
@@ -311,8 +321,19 @@ function loadSave(){
|
||||
if(typeof save.questsDoneTotal!=='number') save.questsDoneTotal=0;
|
||||
if(!save.quests||typeof save.quests!=='object') save.quests=null;
|
||||
if(!save.streak||typeof save.streak!=='object') save.streak={last:null,days:0,shieldWeek:null};
|
||||
// M3: приманки (фонарик) — дефолт для старых сейвов
|
||||
// M3: приманки — дефолт для старых сейвов
|
||||
if(!save.lures||!Array.isArray(save.lures)) save.lures=[0];
|
||||
if(!save.rods||!Array.isArray(save.rods)) save.rods=[0];
|
||||
// M5: фонарик больше не приманка (id=4) — убираем из старых сейвов
|
||||
if(save.lures.includes(4)) save.lures=save.lures.filter(x=>x!==4);
|
||||
if(!save.lures.length) save.lures=[0];
|
||||
if(save.lure===4) save.lure=0;
|
||||
// M5: уровни снастей 1..4 — в старых сейвах их нет: купленным — уровень 1
|
||||
if(!save.rodLv||typeof save.rodLv!=='object') save.rodLv={};
|
||||
if(!save.lureLv||typeof save.lureLv!=='object') save.lureLv={};
|
||||
for(const i of save.rods) if(!(save.rodLv[i]>=1)) save.rodLv[i]=1;
|
||||
for(const i of save.lures) if(!(save.lureLv[i]>=1)) save.lureLv[i]=1;
|
||||
delete save.lureLv[4];
|
||||
// M3: цикл дня/ночи — без dayTime старт с утра; день идёт в реальном времени,
|
||||
// пока игрока не было, сдвиг догоняет (mod 1)
|
||||
if(typeof save.dayTime!=='number'||save.dayTime<0) save.dayTime=0;
|
||||
@@ -337,7 +358,7 @@ function persist(){
|
||||
const DAY_LENGTH=300; // секунд на полный день
|
||||
let dayTime=0; // 0..1 — позиция в сутках
|
||||
let dayPhase='morning'; // morning (0-0.25) day (0.25-0.5) evening (0.5-0.75) night (0.75-1.0)
|
||||
let nightNoLureToast=false; // тост «Ночью нужен фонарик» — разово, не каждый кадр
|
||||
let nightNoLightToast=false; // тост «Ночью нужен фонарик» — разово, не каждый кадр
|
||||
function phaseOf(t){ return t<0.25?'morning':(t<0.5?'day':(t<0.75?'evening':'night')); }
|
||||
function setDayTime(t){ t%=1; if(t<0)t+=1; dayTime=t; dayPhase=phaseOf(dayTime); }
|
||||
|
||||
@@ -621,8 +642,29 @@ let clouds=[], sparkles=[];
|
||||
let particles=[], ripples=[];
|
||||
let bobberTrail=[]; // визуал: след поплавка при забросе
|
||||
|
||||
const rod=()=>RODS[save.rod];
|
||||
const lure=()=>LURES[save.lure];
|
||||
/* M5: уровни снастей (1..4) и эффективные статы: базовые + эффекты апгрейдов */
|
||||
function rodLvOf(i){ const v=save.rodLv&&save.rodLv[i]>=1?save.rodLv[i]:1; return Math.max(1,Math.min(4,v)); }
|
||||
function lureLvOf(i){ const v=save.lureLv&&save.lureLv[i]>=1?save.lureLv[i]:1; return Math.max(1,Math.min(4,v)); }
|
||||
function effRod(i){
|
||||
const b=RODS[i], lv=rodLvOf(i);
|
||||
const e=Object.assign({},b,{light:lv>=2}); // M5: Lv2 — фонарик (ночная рыбалка)
|
||||
if(lv>=2) e.line*=1.15; // Lv2: леска крепче
|
||||
if(lv>=3){ e.drag*=1.15; e.reel*=1.10; } // Lv3: тормоз и катушка
|
||||
if(lv>=4){ e.line*=1.15; e.size+=0.10; } // Lv4: леска ещё + крупные
|
||||
return e;
|
||||
}
|
||||
function effLure(i){
|
||||
const b=LURES[i], lv=lureLvOf(i);
|
||||
const e=Object.assign({},b);
|
||||
if(lv>=2) e.bite*=1.15; // Lv2: клёв чаще
|
||||
if(lv>=3) e.rare+=1.0; // Lv3: редкие
|
||||
if(lv>=4) e.size+=0.15; // Lv4: крупные
|
||||
return e;
|
||||
}
|
||||
const rod=()=>effRod(save.rod);
|
||||
const lure=()=>effLure(save.lure);
|
||||
/* M5: фонарик — не приманка, а апгрейд 1 любого удилища (2+ ур.) */
|
||||
const hasLight=()=>rodLvOf(save.rod)>=2;
|
||||
const rand=(a,b)=>a+Math.random()*(b-a);
|
||||
|
||||
function initFish(){
|
||||
@@ -902,7 +944,7 @@ function updateGame(dt){
|
||||
bobber={x:castTo.x,y:waterY-2};
|
||||
state='WAIT';
|
||||
biteTimer=rand(4,10)/lure().bite;
|
||||
nightNoLureToast=false; // новый заброс — тост о фонарике может показаться снова
|
||||
nightNoLightToast=false; // новый заброс — тост о фонарике может показаться снова
|
||||
splash(castTo.x,waterY,14,100);
|
||||
addRipple(castTo.x,waterY,40);
|
||||
sfx('splash'); vib(10);
|
||||
@@ -920,9 +962,10 @@ function updateGame(dt){
|
||||
if(biteFish&&biteFish.mode==='out') updateBiteFishOut(dt);
|
||||
biteTimer-=dt;
|
||||
if(biteTimer<=0&&!biteFish){
|
||||
if(dayPhase==='night'&&!save.lures.includes(4)){
|
||||
// M3: ночью без фонарика клёва нет — таймер тикает вхолостую
|
||||
if(!nightNoLureToast){ toast('Ночью нужен фонарик'); nightNoLureToast=true; }
|
||||
if(dayPhase==='night'&&!hasLight()){
|
||||
// M5: ночью без фонарика клёва нет — таймер тикает вхолостую
|
||||
// (фонарик — апгрейд 1 удилища: save.rodLv[save.rod]>=2)
|
||||
if(!nightNoLightToast){ toast('Ночью нужен фонарик — улучши удилище (первый апгрейд)'); nightNoLightToast=true; }
|
||||
}else if(forceBite||Math.random()<biteChance()){
|
||||
startBite();
|
||||
}else{
|
||||
@@ -1534,14 +1577,31 @@ function setTab(t){ shopTab=t; renderShop();
|
||||
$('tabRewards').classList.toggle('active',t==='rewards');
|
||||
$('tabAch').classList.toggle('active',t==='ach');
|
||||
}
|
||||
function lvPips(lv){ let s=''; for(let k=1;k<=4;k++) s+=k<=lv?'●':'○'; return s; }
|
||||
function itemRow(item,i,type,owned,cur){
|
||||
let btn;
|
||||
if(cur) btn='<button class="buybtn current" disabled>✓ Выбрано</button>';
|
||||
else if(owned) btn='<button class="buybtn owned" data-act="use" data-type="'+type+'" data-i="'+i+'">Выбрать</button>';
|
||||
else btn='<button class="buybtn" data-act="buy" data-type="'+type+'" data-i="'+i+'">'+item.cost+' 🪙</button>';
|
||||
const tag=item.nightOnly?' <span style="color:#8fb8dd;font-size:12px">ночь</span>':''; // M3: пометка «ночь»
|
||||
return '<div class="item"><div><div class="itemname">'+item.name+tag+'</div>'+
|
||||
'<div class="itemdesc">'+item.desc+'</div></div>'+btn+'</div>';
|
||||
const list=type==='rod'?RODS:LURES;
|
||||
const lvMap=type==='rod'?(save.rodLv||{}):(save.lureLv||{});
|
||||
const costMap=type==='rod'?ROD_UPG_COST:LURE_UPG_COST;
|
||||
const nextTxt=type==='rod'?ROD_UPG_NEXT:LURE_UPG_NEXT;
|
||||
const lv=owned?Math.max(1,Math.min(4,lvMap[i]>=1?lvMap[i]:1)):1;
|
||||
// M5: гейтинг — снасть №i открывается, когда снасть №(i−1) того же типа на 4 ур.
|
||||
const prevLv=i>0?((lvMap[list[i-1].id]>=1)?lvMap[list[i-1].id]:1):0;
|
||||
const locked=!owned&&i>0&&prevLv<4;
|
||||
let btns='';
|
||||
if(owned){
|
||||
btns+=cur?'<button class="buybtn current" disabled>✓ Выбрано</button>'
|
||||
:'<button class="buybtn owned" data-act="use" data-type="'+type+'" data-i="'+i+'">Выбрать</button>';
|
||||
if(lv<4) btns+='<button class="buybtn upgbt" data-act="upgrade" data-type="'+type+'" data-i="'+i+'">⬆ '+costMap[lv-1]+' 🪙</button>';
|
||||
}else if(!locked){
|
||||
btns='<button class="buybtn" data-act="buy" data-type="'+type+'" data-i="'+i+'">'+item.cost+' 🪙</button>';
|
||||
}
|
||||
let desc=item.desc;
|
||||
if(locked) desc='Откроется: улучши «'+list[i-1].name+'» до 4 ур.';
|
||||
else if(owned&&lv<4) desc+=' · далее: '+nextTxt[lv-1];
|
||||
const lvTag=owned?' <span class="lvtag">'+lvPips(lv)+' '+lv+' ур.</span>':'';
|
||||
return '<div class="item'+(locked?' locked':'')+'"><div><div class="itemname">'+item.name+lvTag+'</div>'+
|
||||
'<div class="itemdesc">'+desc+'</div></div>'+
|
||||
(btns?'<div class="rowbtns">'+btns+'</div>':'<div class="rowbtns"><button class="buybtn" disabled>🔒</button></div>')+'</div>';
|
||||
}
|
||||
function renderShop(){
|
||||
const body=$('shopBody');
|
||||
@@ -1606,14 +1666,29 @@ function shopClick(e){
|
||||
const type=b.dataset.type, i=+b.dataset.i;
|
||||
const list=type==='rod'?RODS:LURES;
|
||||
const owned=type==='rod'?save.rods:save.lures;
|
||||
if(!save.rodLv||typeof save.rodLv!=='object') save.rodLv={};
|
||||
if(!save.lureLv||typeof save.lureLv!=='object') save.lureLv={};
|
||||
const lvMap=type==='rod'?save.rodLv:save.lureLv;
|
||||
const costMap=type==='rod'?ROD_UPG_COST:LURE_UPG_COST;
|
||||
const item=list[i];
|
||||
if(b.dataset.act==='buy'){
|
||||
if(i>0&&lvMap[list[i-1].id]<4){ sfx('no'); toast('Сначала улучши «'+list[i-1].name+'» до 4 ур.'); return; } // M5: гейтинг
|
||||
if(save.coins<item.cost){ sfx('no'); toast('Не хватает монет 🪙'); return; }
|
||||
save.coins-=item.cost; owned.push(i);
|
||||
save.coins-=item.cost; owned.push(i); lvMap[i]=1; // куплено — уровень 1
|
||||
if(type==='rod') save.rod=i; else save.lure=i;
|
||||
persist(); updateHUD(); sfx('coin'); renderShop();
|
||||
toast('Куплено: '+item.name+'!');
|
||||
checkAchievements();
|
||||
}else if(b.dataset.act==='upgrade'){
|
||||
if(owned.indexOf(i)<0){ sfx('no'); return; } // M5: не куплено — апгрейд недоступен (паттерн защиты buy)
|
||||
const lv=(lvMap[i]>=1)?lvMap[i]:1;
|
||||
if(lv>=4) return;
|
||||
const cost=costMap[lv-1];
|
||||
if(save.coins<cost){ sfx('no'); toast('Не хватает монет 🪙'); return; }
|
||||
save.coins-=cost; lvMap[i]=lv+1; // M5: апгрейд — уровень++
|
||||
persist(); updateHUD(); sfx('coin'); renderShop();
|
||||
toast('Улучшено: '+item.name+' → '+(lv+1)+' ур.!');
|
||||
checkAchievements();
|
||||
}else{
|
||||
if(type==='rod') save.rod=i; else save.lure=i;
|
||||
persist(); updateHUD(); sfx('click'); renderShop();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 492 KiB After Width: | Height: | Size: 460 KiB |
@@ -325,13 +325,14 @@ async function forceCatch() {
|
||||
ok('ночь: dayPhase=night при dayTime 0.85', night === 'night', String(night));
|
||||
await shot('orca-night.png');
|
||||
|
||||
// 9c. M3: фонарик в лавке (id=4, пометка «ночь»). Лавка открыта на вкладке,
|
||||
// где её закрыли в шаге 7 (🏅) — переключаем на Снасти.
|
||||
// 9c. M5: фонарик в лавке — апгрейд 1 удилища (не приманка id=4): в строке
|
||||
// удилища кнопка «⬆ 400 🪙» и подпись «фонарик (ночь)». Лавка открыта на
|
||||
// вкладке, где её закрыли в шаге 7 (🏅) — переключаем на Снасти.
|
||||
await clickByText('🛒');
|
||||
await waitCond('!document.getElementById("shop").classList.contains("hidden") ? "yes" : "shop hidden"', 15000, 'лавка открыта');
|
||||
await clickByID('tabTackle');
|
||||
await waitCond('document.getElementById("shopBody").textContent.indexOf("Фонарик") >= 0 ? "yes" : "no flask"', 15000, 'фонарик в снастях');
|
||||
ok('лавка: «Фонарик» в снастях (id=4, пометка «ночь»)', true);
|
||||
await waitCond('document.getElementById("shopBody").textContent.indexOf("фонарик") >= 0 ? "yes" : "no flask"', 15000, 'апгрейд-фонарик в снастях');
|
||||
ok('лавка: фонарик — апгрейд 1 удилища (⬆ 400, пометка про ночь)', true);
|
||||
await clickByText('×');
|
||||
await waitCond('document.getElementById("shop").classList.contains("hidden") ? "yes" : "shop open"', 15000, 'лавка закрыта');
|
||||
|
||||
|
||||
+262
-10
@@ -151,7 +151,9 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
ok('срыв: рыба ушла -> WAIT', true);
|
||||
|
||||
// --- 7. Магазин: покупка и экипировка ---
|
||||
await page.evaluate(() => { save.coins = 10000; });
|
||||
// M5: гейтинг — снасть №(i−1) должна быть на 4 ур.; уровни форсим, смысл
|
||||
// проверки (купить 2-е удилище и 2-ю приманку) сохраняется
|
||||
await page.evaluate(() => { save.coins = 10000; save.rodLv[0]=4; save.lureLv[0]=4; });
|
||||
await page.click('#btnShop');
|
||||
await sleep(150);
|
||||
ok('магазин открыт', await page.$eval('#shop', el => !el.classList.contains('hidden')));
|
||||
@@ -508,32 +510,33 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
m3nb.st==='WAIT' && !m3nb.bf && m3nb.bt<0 && m3nb.toast.includes('Ночью нужен фонарик'),
|
||||
JSON.stringify(m3nb));
|
||||
|
||||
// (d) ночь с фонариком: клёв работает
|
||||
// (d) ночь с фонариком: клёв работает (M5: фонарик = апгрейд 1 удилища)
|
||||
await page.click('#btnRecall');
|
||||
await waitState(page, 'IDLE');
|
||||
await page.mouse.move(195, 600);
|
||||
await page.evaluate(() => { setDayTime(0.8); save.lures=[0,4]; });
|
||||
await page.evaluate(() => { setDayTime(0.8); save.rodLv[save.rod]=2; });
|
||||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||||
await waitState(page, 'WAIT');
|
||||
await page.evaluate(() => { forceBite=true; biteTimer=0.05; });
|
||||
await waitState(page, 'BITE', 4000);
|
||||
ok('M3 ночь с фонариком (save.lures включает 4): клёв работает (BITE)', true);
|
||||
ok('M3 ночь с фонариком (удочка 2+ ур.: save.rodLv[save.rod]>=2): клёв работает (BITE)', true);
|
||||
|
||||
// (e) лавка: «Фонарик» в снастях
|
||||
// (e) лавка: фонарик — не приманка, а апгрейд 1 удилища (кнопка ⬆ 400 🪙,
|
||||
// в подписи «фонарик (ночь)»); уровень 1 форсим, чтобы строка показывала именно его
|
||||
await page.click('#btnShop');
|
||||
await sleep(150);
|
||||
await page.click('#tabTackle');
|
||||
await sleep(150);
|
||||
const m3shop = await page.evaluate(() => {
|
||||
save.lures=[0]; // строка с ценой, а не «Выбрать»
|
||||
save.rodLv[0]=1;
|
||||
renderShop();
|
||||
const items=Array.prototype.slice.call(document.querySelectorAll('#shopBody .item'));
|
||||
const row=items.find(el=>el.textContent.indexOf('Фонарик')>=0);
|
||||
const row=items.find(el=>el.querySelector('[data-act="upgrade"][data-type="rod"]'));
|
||||
return row?{txt:row.textContent}:null;
|
||||
});
|
||||
ok('M3 лавка: «Фонарик» в снастях (500 🪙, пометка «ночь»)',
|
||||
m3shop && m3shop.txt.indexOf('500 🪙')>=0 && m3shop.txt.indexOf('ноч')>=0,
|
||||
m3shop?m3shop.txt:'нет строки');
|
||||
ok('M5 лавка: фонарик = апгрейд 1 удилища (кнопка ⬆ 400 🪙, подпись «фонарик (ночь)»)',
|
||||
m3shop && m3shop.txt.indexOf('400')>=0 && m3shop.txt.indexOf('фонарик')>=0 && m3shop.txt.indexOf('ноч')>=0,
|
||||
m3shop?m3shop.txt:'нет строки апгрейда');
|
||||
await page.click('#btnCloseShop');
|
||||
|
||||
// (f) миграция: старый сейв без dayTime/lastSaveMs
|
||||
@@ -868,6 +871,255 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
ok('пустой клёв: без forceBite и Math.random=0.99+ — клёва нет (WAIT, нет biteFish)',
|
||||
empty.st === 'WAIT' && !empty.bf, JSON.stringify(empty));
|
||||
|
||||
// --- 10b. M5: апгрейды снастей (lv 1..4), фонарик, гейтинг лавки ---
|
||||
// (1) миграция: старый сейв без rodLv/lureLv, фонарик ещё приманка (lures:[0,4], lure:4)
|
||||
await page.evaluate(() => {
|
||||
localStorage.setItem('fishing-save-v1', JSON.stringify({
|
||||
coins: 77, rod: 0, lure: 4, rods: [0], lures: [0, 4], muted: false,
|
||||
caught: {}, casts: 1, earned: 10
|
||||
}));
|
||||
});
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await sleep(500);
|
||||
const m5m = await page.evaluate(() => ({
|
||||
rodLv: save.rodLv, lureLv: save.lureLv,
|
||||
lures: save.lures, lure: save.lure,
|
||||
rods: save.rods, rod: save.rod
|
||||
}));
|
||||
ok('M5 миграция: старый сейв (нет rodLv/lureLv, lures:[0,4], lure:4) → rodLv={0:1}, lureLv={0:1}, lures без 4, lure=0',
|
||||
m5m.rodLv[0]===1 && Object.keys(m5m.rodLv).length===1
|
||||
&& m5m.lureLv[0]===1 && Object.keys(m5m.lureLv).length===1
|
||||
&& m5m.lures.join(',')==='0' && m5m.lure===0
|
||||
&& m5m.rods.join(',')==='0' && m5m.rod===0,
|
||||
JSON.stringify(m5m));
|
||||
|
||||
// (2) данные: фонарика как приманки id=4 нет, поле nightOnly не используется
|
||||
const m5t = await page.evaluate(() => ({
|
||||
n: LURES.length,
|
||||
ids: LURES.map(l => l.id).join(','),
|
||||
noNight: LURES.every(l => !('nightOnly' in l))
|
||||
}));
|
||||
ok('M5 данные: LURES.length===4 (id 0..3, фонарика id=4 нет) и нет поля nightOnly',
|
||||
m5t.n===4 && m5t.ids==='0,1,2,3' && m5t.noNight, JSON.stringify(m5t));
|
||||
|
||||
// (3) эффективные статы удочки 0 по уровням: точные (база × множители реализации)
|
||||
const m5r = await page.evaluate(() => {
|
||||
save.rod = 0;
|
||||
const b = RODS[0], out = {};
|
||||
for (const lv of [1, 2, 3, 4]) {
|
||||
save.rodLv[0] = lv;
|
||||
const r = rod();
|
||||
out[lv] = { line: r.line, drag: r.drag, reel: r.reel, size: r.size, light: r.light };
|
||||
}
|
||||
save.rodLv[0] = 1;
|
||||
return {
|
||||
got: out,
|
||||
exp: {
|
||||
1: { line: b.line, drag: b.drag, reel: b.reel, size: b.size, light: false },
|
||||
2: { line: b.line * 1.15, drag: b.drag, reel: b.reel, size: b.size, light: true },
|
||||
3: { line: b.line * 1.15, drag: b.drag * 1.15, reel: b.reel * 1.10, size: b.size, light: true },
|
||||
4: { line: b.line * 1.15 * 1.15, drag: b.drag * 1.15, reel: b.reel * 1.10, size: b.size + 0.10, light: true }
|
||||
}
|
||||
};
|
||||
});
|
||||
ok('M5 статы удочки 0: Lv1=база (line 100, drag 34, reel 1.0, size 0), Lv2 line≈115 (×1.15), Lv3 drag≈39.1 (×1.15) reel=1.1 (×1.10), Lv4 line≈132.25 (×1.15²) size=0.1',
|
||||
[1, 2, 3, 4].every(lv => {
|
||||
const g = m5r.got[lv], e = m5r.exp[lv];
|
||||
return g.line === e.line && g.drag === e.drag && g.reel === e.reel && g.size === e.size && !!g.light === e.light;
|
||||
}),
|
||||
JSON.stringify(m5r.got));
|
||||
|
||||
// (4) эффективные статы приманки 0 по уровням
|
||||
const m5l = await page.evaluate(() => {
|
||||
save.lure = 0;
|
||||
const b = LURES[0], out = {};
|
||||
for (const lv of [1, 2, 3, 4]) {
|
||||
save.lureLv[0] = lv;
|
||||
const l = lure();
|
||||
out[lv] = { bite: l.bite, rare: l.rare, size: l.size };
|
||||
}
|
||||
save.lureLv[0] = 1;
|
||||
return {
|
||||
got: out,
|
||||
exp: {
|
||||
1: { bite: b.bite, rare: b.rare, size: b.size },
|
||||
2: { bite: b.bite * 1.15, rare: b.rare, size: b.size },
|
||||
3: { bite: b.bite * 1.15, rare: b.rare + 1.0, size: b.size },
|
||||
4: { bite: b.bite * 1.15, rare: b.rare + 1.0, size: b.size + 0.15 }
|
||||
}
|
||||
};
|
||||
});
|
||||
ok('M5 статы приманки 0: Lv1=база (bite 1.0, rare 0, size 0), Lv2 bite=1.15 (×1.15), Lv3 rare=1.0 (+1.0), Lv4 size=0.15 (+0.15)',
|
||||
[1, 2, 3, 4].every(lv => {
|
||||
const g = m5l.got[lv], e = m5l.exp[lv];
|
||||
return g.bite === e.bite && g.rare === e.rare && g.size === e.size;
|
||||
}),
|
||||
JSON.stringify(m5l.got));
|
||||
|
||||
// (5) фонарик = апгрейд 1 удилища: hasLight
|
||||
const m5h = await page.evaluate(() => {
|
||||
save.rodLv[save.rod] = 1; const a = hasLight();
|
||||
save.rodLv[save.rod] = 2; const b = hasLight();
|
||||
save.rodLv[save.rod] = 4; const c = hasLight();
|
||||
save.rodLv[save.rod] = 1;
|
||||
return { a, b, c };
|
||||
});
|
||||
ok('M5 hasLight(): false на 1 ур., true на 2 ур. и 4 ур.',
|
||||
m5h.a === false && m5h.b === true && m5h.c === true, JSON.stringify(m5h));
|
||||
|
||||
// (6) лавка: гейтинг — свежий сейв, rod 1 и lure 1 заблокированы
|
||||
await page.click('#btnPlay');
|
||||
await sleep(150);
|
||||
await page.evaluate(() => {
|
||||
save.rods = [0]; save.lures = [0]; save.rod = 0; save.lure = 0;
|
||||
save.rodLv = { 0: 1 }; save.lureLv = { 0: 1 }; save.coins = 1000;
|
||||
});
|
||||
await page.click('#btnShop');
|
||||
await sleep(150);
|
||||
await page.click('#tabTackle');
|
||||
await sleep(150);
|
||||
const m5g1 = await page.evaluate(() => {
|
||||
const items = Array.prototype.slice.call(document.querySelectorAll('#shopBody .item'));
|
||||
const byName = n => items.find(el => el.querySelector('.itemname').textContent.includes(n));
|
||||
const info = el => ({
|
||||
locked: el.classList.contains('locked'),
|
||||
buy: el.querySelector('[data-act="buy"]') !== null,
|
||||
lock: el.querySelector('button[disabled]') !== null,
|
||||
desc: el.querySelector('.itemdesc').textContent
|
||||
});
|
||||
return { r1: info(byName('Стеклопластик')), l1: info(byName('Червь')) };
|
||||
});
|
||||
ok('M5 гейтинг лавки: свежий сейв (1 ур.) — строки rod 1 и lure 1: класс locked, 🔒, нет buy, «Откроется: … до 4 ур.»',
|
||||
m5g1.r1.locked && !m5g1.r1.buy && m5g1.r1.lock
|
||||
&& m5g1.r1.desc.includes('Откроется') && m5g1.r1.desc.includes('до 4 ур.')
|
||||
&& m5g1.l1.locked && !m5g1.l1.buy && m5g1.l1.lock && m5g1.l1.desc.includes('Откроется'),
|
||||
JSON.stringify(m5g1));
|
||||
|
||||
// (7) лавка: предыдущая снасть на 4 ур. — buy появляется, locked снимается
|
||||
const m5g2 = await page.evaluate(() => {
|
||||
save.rodLv[0] = 4; save.lureLv[0] = 4;
|
||||
renderShop();
|
||||
const items = Array.prototype.slice.call(document.querySelectorAll('#shopBody .item'));
|
||||
const byName = n => items.find(el => el.querySelector('.itemname').textContent.includes(n));
|
||||
const info = el => {
|
||||
const b = el.querySelector('[data-act="buy"]');
|
||||
return { locked: el.classList.contains('locked'), buyTxt: b ? b.textContent : null };
|
||||
};
|
||||
return { r1: info(byName('Стеклопластик')), l1: info(byName('Червь')) };
|
||||
});
|
||||
ok('M5 гейтинг лавки: после rodLv[0]=4 / lureLv[0]=4 — locked снят, кнопки buy «250 🪙» / «150 🪙»',
|
||||
!m5g2.r1.locked && m5g2.r1.buyTxt === '250 🪙' && !m5g2.l1.locked && m5g2.l1.buyTxt === '150 🪙',
|
||||
JSON.stringify(m5g2));
|
||||
|
||||
// (8) двойная защита: buy-ветка shopClick при закрытом гейтинге не покупает
|
||||
const m5d = await page.evaluate(() => {
|
||||
save.rodLv[0] = 1; save.coins = 1000;
|
||||
const before = { rods: save.rods.join(','), coins: save.coins };
|
||||
const el = document.createElement('button');
|
||||
el.dataset.act = 'buy'; el.dataset.type = 'rod'; el.dataset.i = '1';
|
||||
document.body.appendChild(el);
|
||||
let err = null;
|
||||
try { shopClick({ target: el }); } catch (e) { err = String(e); }
|
||||
document.body.removeChild(el);
|
||||
return { err, before,
|
||||
after: { rods: save.rods.join(','), coins: save.coins },
|
||||
toast: document.getElementById('toast').textContent };
|
||||
});
|
||||
ok('M5 двойная защита: shopClick buy (rod 1) при rodLv[0]<4 — rods/coins не изменились, тост «Сначала улучши…»',
|
||||
m5d.err === null && m5d.before.rods === m5d.after.rods && m5d.before.coins === m5d.after.coins
|
||||
&& m5d.toast.includes('Сначала улучши'),
|
||||
JSON.stringify(m5d));
|
||||
|
||||
// (9) покупка апгрейда: 1000 🪙 → 600, rodLv[0]=2, фонарик включился
|
||||
await page.evaluate(() => {
|
||||
save.rods = [0]; save.lures = [0]; save.rod = 0; save.lure = 0;
|
||||
save.rodLv = { 0: 1 }; save.lureLv = { 0: 1 }; save.coins = 1000;
|
||||
renderShop();
|
||||
});
|
||||
await page.click('#shopBody [data-act="upgrade"][data-type="rod"][data-i="0"]');
|
||||
const m5u = await page.evaluate(() => ({
|
||||
coins: save.coins, lv: save.rodLv[0], light: hasLight(),
|
||||
toast: document.getElementById('toast').textContent
|
||||
}));
|
||||
ok('M5 покупка апгрейда: rod 0 1→2 ур. за 400 🪙 (1000→600), hasLight()=true, тост «Улучшено … → 2 ур»',
|
||||
m5u.coins === 600 && m5u.lv === 2 && m5u.light === true
|
||||
&& m5u.toast.includes('Улучшено') && m5u.toast.includes('2 ур'),
|
||||
JSON.stringify(m5u));
|
||||
|
||||
// (10) на макс. уровне (4) кнопки upgrade в строке нет
|
||||
const m5x = await page.evaluate(() => {
|
||||
save.rodLv[0] = 4; renderShop();
|
||||
const r0 = document.querySelectorAll('#shopBody .item')[0];
|
||||
return { hasUpg: r0.querySelector('[data-act="upgrade"]') !== null, txt: r0.textContent };
|
||||
});
|
||||
ok('M5 макс. уровень: на 4 ур. у rod 0 кнопки upgrade нет (пипсы ●●●●, «4 ур.»)',
|
||||
!m5x.hasUpg && m5x.txt.includes('●●●●') && m5x.txt.includes('4 ур.'), m5x.txt);
|
||||
|
||||
// (11) не хватает монет: апгрейд не покупается
|
||||
await page.evaluate(() => { save.rodLv[0] = 1; save.coins = 100; renderShop(); });
|
||||
await page.click('#shopBody [data-act="upgrade"][data-type="rod"][data-i="0"]');
|
||||
const m5p = await page.evaluate(() => ({
|
||||
coins: save.coins, lv: save.rodLv[0],
|
||||
toast: document.getElementById('toast').textContent
|
||||
}));
|
||||
ok('M5 не хватает монет: 100 🪙 на апгрейд за 400 — coins=100, уровень не изменился, тост «Не хватает монет»',
|
||||
m5p.coins === 100 && m5p.lv === 1 && m5p.toast.includes('Не хватает монет'),
|
||||
JSON.stringify(m5p));
|
||||
await page.click('#btnCloseShop');
|
||||
|
||||
// (12) ночь без фонарика (rodLv[save.rod]=1): клёв не стартует, тост (паттерн §9j(c))
|
||||
await page.evaluate(() => {
|
||||
setDayTime(0.8);
|
||||
save.lures = [0]; save.lure = 0;
|
||||
save.rodLv[save.rod] = 1;
|
||||
save.quests = { date: todayStr(), list: [] }; // квестный тост не затирает проверочный
|
||||
state = 'IDLE'; reel = null; biteFish = null; forceBite = false;
|
||||
});
|
||||
await page.mouse.move(195, 600);
|
||||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||||
await waitState(page, 'WAIT');
|
||||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||||
await sleep(600);
|
||||
const m5n = await page.evaluate(() => ({
|
||||
st: state, bf: !!biteFish, bt: biteTimer,
|
||||
toast: document.getElementById('toast').textContent
|
||||
}));
|
||||
ok('M5 ночь без фонарика (rodLv=1): клёв не стартует (WAIT, нет biteFish, biteTimer<0), тост «Ночью нужен фонарик»',
|
||||
m5n.st === 'WAIT' && !m5n.bf && m5n.bt < 0 && m5n.toast.includes('Ночью нужен фонарик'),
|
||||
JSON.stringify(m5n));
|
||||
|
||||
// (13) ночь с фонариком (rodLv[save.rod]=2): клёв работает (паттерн §9j(d))
|
||||
await page.click('#btnRecall');
|
||||
await waitState(page, 'IDLE');
|
||||
await page.mouse.move(195, 600);
|
||||
await page.evaluate(() => { save.rodLv[save.rod] = 2; });
|
||||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||||
await waitState(page, 'WAIT');
|
||||
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
|
||||
await waitState(page, 'BITE', 4000);
|
||||
ok('M5 ночь с фонариком (rodLv=2): клёв работает (BITE)', true);
|
||||
|
||||
// (14) покупка разблокированной снасти стартует с уровня 1
|
||||
await page.evaluate(() => {
|
||||
forceBite = false; state = 'IDLE'; reel = null; biteFish = null;
|
||||
setDayTime(0.3);
|
||||
save.rods = [0]; save.lures = [0]; save.rod = 0; save.lure = 0;
|
||||
save.rodLv = { 0: 4 }; save.lureLv = { 0: 1 }; save.coins = 1000;
|
||||
});
|
||||
await page.click('#btnShop');
|
||||
await sleep(150);
|
||||
await page.click('#tabTackle');
|
||||
await sleep(150);
|
||||
await page.click('#shopBody [data-act="buy"][data-type="rod"][data-i="1"]');
|
||||
const m5b = await page.evaluate(() => ({
|
||||
rods: save.rods.join(','), lv: save.rodLv[1],
|
||||
rod: save.rod, coins: save.coins
|
||||
}));
|
||||
ok('M5 покупка разблокированной снасти: rod 1 за 250 🪙 — rods=[0,1], rodLv[1]=1, снасть экипирована, coins=750',
|
||||
m5b.rods === '0,1' && m5b.lv === 1 && m5b.rod === 1 && m5b.coins === 750,
|
||||
JSON.stringify(m5b));
|
||||
await page.click('#btnCloseShop');
|
||||
|
||||
// --- 11. Ошибки консоли ---
|
||||
ok('нет ошибок JS (pageerror)', pageErrors.length === 0, pageErrors.join(' | '));
|
||||
ok('нет ошибок консоли', consoleErrors.length === 0, consoleErrors.join(' | '));
|
||||
|
||||
Reference in New Issue
Block a user