diff --git a/AGENTS.md b/AGENTS.md index cf177b1..a89d3f8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -11,7 +11,7 @@ npm install # puppeteer-core npm test # = node test.js ``` - Ждать все PASS и exit 0. Сейчас проверок 100. + Ждать все PASS и exit 0. Сейчас проверок 107. 3. Браузерный плейтест (вкладка встроенного браузера Orca, через Orca CLI): ```bash node tests/orca-playtest.js # из каталога репо; требует ЗАПУЩЕННЫЙ Orca app и orca-ide (или ORCA_CLI_COMMAND) @@ -22,7 +22,7 @@ ## Подводные камни - `tests/test.js` берёт браузер из `process.env.CHROMIUM || '/usr/bin/chromium-browser'`, а URL игры резолвит динамически от каталога репо (тот же паттерн, что в `shots.js`). Если тест падает на старте — поправьте имя/путь chromium под свой чекаут (переменная `CHROMIUM`). -- Тесты гоняют игру через внутренние глобалы в `page.evaluate`: `state`, `save`, `biteTimer`, `reel`, `SPECIES`, `SPECIES_NIGHT`, `rod()`, `SHINY_CHANCE`, `forceBoss`, `forceBite`, `forceVariant`, `VARIANTS`, `VARIANT_CHAIN`, `rollVariant()`, `variantChance()`, `cardInfo`, `fishForce()`, `stateBoss`, `bossPhase`, `dayPhase`, `pickSpecies()`, `setDayTime()`, `todayStr()`, `dateShift()`, `isoWeek()`, `achBonus()`, `forceWeather`, `weather`, `weatherT`, `weatherGap`, `WEATHER`, `WEATHER_GAP_MIN`, `WEATHER_GAP_MAX`, `WEATHER_DURATION`, `weatherActive()` и имена состояний `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`, `VARIANT_CHAIN`, `rollVariant()`, `variantChance()`, `cardInfo`, `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` и имена состояний `MENU/IDLE/CHARGE/CAST/WAIT/APPROACH/BITE/REEL/SNAP/CARD`. Переименование любого из них ломает тесты молча — имена держать, либо обновлять тест в том же коммите. - Ввод на Pointer Events (`pointerdown`/`pointerup`), а не touch/mouse — так тест управляет игрой мышью. Не заменять на тач-обработчики. - Сейв: ключ localStorage `fishing-save-v1`. При изменении формата — миграцию добавлять в `loadSave()` (старые сейвы обязаны оставаться читаемыми). @@ -33,7 +33,8 @@ - Босс (M4): тест-форс появления `forceBoss`, фазы боя `stateBoss`/`bossPhase`, босс-ветки в `updateReel()` (обрыв фазы) и `SNAP` в `updateGame()` (авто-переход), награда ×5 в `catchFish()`. - Варианты рыб (M6): таблица `VARIANTS` (порядок ролла: gold → silver → rainbow, иначе normal), `variantChance()` — единственная точка вероятностей (дождь M8 уже здесь — ×2,5, см. строку «Погода (M8)»; gold — живая `SHINY_CHANCE`), тест-форс `forceVariant`, босс всегда normal, улучшение на карточке (кнопка `#btnCardUp`, цепочка `VARIANT_CHAIN`, cost = 0.6×Δ ценности, формула — в `cardUpNext()`). - Погода (M8): таблица `WEATHER` (rain — `variantMult` ×2,5; fog — `biteMult` ×1,5), таймеры в реальном времени — `updateWeather()` в `frame()` (gap `WEATHER_GAP_MIN..MAX` 120–240 с, длительность `WEATHER_DURATION` 120 с, под форсом не гоняются), `weatherActive()` — единственная точка чтения (форс приоритетнее), эффекты только в `variantChance()` (rain) и `biteChance()` (fog), тест-форс `forceWeather` ('rain'/'fog'/null, паттерн `forceBoss`); погода не персистится в сейв. Заметка: туман может дать p>1 → насыщение 100% клёва (осознанно). -- Сложность вываживания: `updateReel()`. + - Скины (M-skins): таблицы `BOAT_SKINS`/`BOBBER_SKINS` (лодка: Обычная 0 / «Красная лодка» 800 / «Лодка-домик» 2000 🪙; поплавок: Обычный 0 / Красный 500 / Неоновый 1500 🪙), сейв `save.boatSkin(s)`/`save.bobberSkin(s)` + миграция в `loadSave()` (включая гарантию бесплатного скина 0), вкладка «Скины» 🎨 (`#tabSkins`, строка `skinRow(list,i,owned,equipped,group)`, ветки buyskin/select в `shopClick()`: покупка со списанием и авто-экипировкой, выбор — только купленное), отрисовка `drawBoat()`/`drawBobber()` с clamp 0..2 (скин 2: каюта у лодки, неоновое свечение `shadowBlur` в save/restore у поплавка); чистая косметика — без эффектов на геймплей. + - Сложность вываживания: `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. Награды» после сейва. diff --git a/README.md b/README.md index 79a59f2..e53cfcf 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ - Босс-рыба — Осётр-босс (шанс при поимке осётра): 3 фазы «рвёт и уходит», награда ×5, достижение «Босс-рыбак» - Варианты рыб: серебряная ×2, золотая ×3, радужная ×5 — бейдж и оформление карточки улова; на карточке — улучшение варианта (⬆) — ценность улова растёт - Погода: дождь (🌧️) — шанс серебряной/золотой/радужной ×2,5; туман (🌫️) — клёв ×1,5; событие идёт 120 с, следующее — раз в 2–4 минуты —  +- Скины лодки и поплавка 🎨 — вкладка «Скины» в лавке: лодка «Красная лодка» 800 🪙 / «Лодка-домик» 2000 🪙, поплавок «Красный» 500 🪙 / «Неоновый» 1500 🪙 — чистая косметика (без эффектов на геймплей) - Квесты дня (3 шт, генерация по дню), стрик входов со «щитом», 18 достижений с бонусами - Дневник рыбака — коллекция, максимальный улов по каждому виду, статистика - Магазин с вкладками: Снасти 🎣, Дневник 📔, Награды 🎯, Достижения 🏅 @@ -60,7 +61,7 @@ python3 -m http.server 8000 ```bash cd tests npm install # ставит puppeteer-core -node test.js # 100 проверок +node test.js # 107 проверок ``` Нужен установленный Chromium (`executablePath` в `test.js` при необходимости поправить). diff --git a/docs/DEVLOG.md b/docs/DEVLOG.md index 86811c5..319230f 100644 --- a/docs/DEVLOG.md +++ b/docs/DEVLOG.md @@ -379,3 +379,20 @@ git add -A && git commit -m "что изменилось" && git push **Заметки (осознанные решения и ловушки):** - Пре-эксзистинг-флейка §9h (~4,5%/прогон): вид рыбы не был закреплён — ролл вида с rarity≥2 выдавал ачивку `rare_fish` (coinPct 0,01→0,02 и +монеты) и ломал точную проверку `achBonus()`/монет. Захарденено пином `pickSpecies = () => SPECIES[0]` (карась, rarity 0) по паттерну §10c(4). - Туман: итоговая вероятность клёва может превысить 1 (p = p_базы×1,5) → насыщение 100% клёва. Осознанно — туман «клевое» событие; `Math.min(1,p)` не добавляли. + +## Этап 19. M-skins: Скины лодки и поплавка + +**Что сделано в `index.html`:** + +1. **Таблицы `BOAT_SKINS`/`BOBBER_SKINS`** — лодка: «Обычная» (0), «Красная лодка» (800 🪙), «Лодка-домик» (2000 🪙); поплавок: «Обычный» (0), «Красный» (500 🪙), «Неоновый» (1500 🪙). Чистая косметика — coin-sink, без эффектов на геймплей (монеты, физика, погода не затронуты). +2. **Сейв и миграция** — `save.boatSkins:[0]`/`save.boatSkin:0`, `save.bobberSkins:[0]`/`save.bobberSkin:0` (купленные id + экипированный). Миграция в `loadSave()`: нет массивов/полей → дефолты `[0]`/`0`; битые значения (скин вне 0..2, экипированный не в купленных) → 0; бесплатный скин 0 гарантирован — при битом сейве (напр. `boatSkins:[1]`) без `0` в купленных дописывается `push(0)`, иначе лавка показывала бы «Купить бесплатно» скин 0, хотя лодка уже рендерится как скин 0. +3. **Вкладка «Скины» 🎨** (`#tabSkins`, `setTab('skins')`) — группы «Лодка»/«Поплавок», строка `skinRow(list,i,owned,equipped,group)` (group передаётся явно: 'boat'/'bobber' — без identity-чека таблиц). `shopClick()`: ветка `buyskin` (защиты: уже куплено / не хватает монет → тост; списание, push, авто-экипировка, persist) и `select` (защита: не куплено — выбрать нельзя; монеты не тратятся). +4. **Отрисовка** — `drawBoat()`: корпус по `save.boatSkin` с clamp 0..2 (скин 1 — красный градиент, скин 2 — синий + каюта с ярко-жёлтым окном); `drawBobber()`: по `save.bobberSkin` (скин 2 — неон `#aaff00` + `shadowBlur=10` в `ctx.save()/restore()`, чтобы свечение не перетекало на другие элементы). + +**Тесты:** +7 проверок в `tests/test.js` (§10e: данные таблиц (id/cost); миграция старого сейва → массивы `[0]`, экипированные 0/0; битый сейв (скин вне 0..2, экипированный не в купленных) → 0; покупка «Красная лодка» за 1000 монет (coins=200, `boatSkins=[0,1]`, авто-экип, «Выбрано ✓» disabled); не хватает монет (тост, без изменений); выбор после покупки (монеты не тратятся); вкладка/группы/6 названий + смоук отрисовки скинов 2 (~1 с, без pageerror)). Итог **107/107 PASS, exit 0** (`node tools/check.mjs` — OK). + +**Monte-Carlo:** не требовался — физика вываживания не тронута; скины влияют только на отрисовку. + +**Заметки (осознанные решения и ловушки):** +- Brute-force проверка «coin-tolerance» ассертов §10e (ищутся ли посторонние источники монет, ломающие точные проверки) — пинить нечего: монеты каждого шага задаются явно. +- Известный suite-level риск: проход границы даты 00:00 за время прогона (~0.2%) может сдвинуть точные ассерты; guard в `tests/test.js` — в бэклоге (отдельная задача). diff --git a/index.html b/index.html index 4797ffa..49cb35b 100644 --- a/index.html +++ b/index.html @@ -234,6 +234,7 @@ button{font-family:inherit;cursor:pointer} +
@@ -353,17 +354,31 @@ const LURE_UPG_COST=[300,900,2200]; const ROD_UPG_NEXT=['фонарик (ночь), леска ×1.15','тормоз ×1.15, катушка ×1.10','леска ещё ×1.15, крупные +0.10']; const LURE_UPG_NEXT=['клёв ×1.15','редкие +1.0','крупные +0.15']; +/* Этап 4: скины лодки и поплавка — чистая косметика (coin-sink), в лавке вкладка «Скины» */ +const BOAT_SKINS=[ + {id:0, name:'Обычная', cost:0}, + {id:1, name:'Красная лодка', cost:800}, + {id:2, name:'Лодка-домик', cost:2000} +]; +const BOBBER_SKINS=[ + {id:0, name:'Обычный', cost:0}, + {id:1, name:'Красный', cost:500}, + {id:2, name:'Неоновый', cost:1500} +]; + /* ========================================================= СОХРАНЕНИЕ ========================================================= */ const SAVE_KEY='fishing-save-v1'; 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}; + bossBeaten:false,bossCount:0, + boatSkins:[0],boatSkin:0,bobberSkins:[0],bobberSkin:0}; // Этап 4: скины лодки/поплавка 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}; + bossBeaten:false,bossCount:0, + boatSkins:[0],boatSkin:0,bobberSkins:[0],bobberSkin:0}; // Этап 4: скины лодки/поплавка } function loadSave(){ try{ const s=localStorage.getItem(SAVE_KEY); if(s) Object.assign(save,JSON.parse(s)); }catch(e){} @@ -411,6 +426,15 @@ function loadSave(){ // M4: босс (Осётр-босс) — новые поля, дефолты для старых сейвов if(typeof save.bossBeaten!=='boolean') save.bossBeaten=false; if(typeof save.bossCount!=='number') save.bossCount=0; + // Этап 4: скины лодки/поплавка — дефолты для старых сейвов и чинка битых значений + if(!save.boatSkins||!Array.isArray(save.boatSkins)) save.boatSkins=[0]; + if(save.boatSkins.indexOf(0)<0) save.boatSkins.push(0); // бесплатный скин 0 гарантирован (битый сейв) + if(!save.bobberSkins||!Array.isArray(save.bobberSkins)) save.bobberSkins=[0]; + if(save.bobberSkins.indexOf(0)<0) save.bobberSkins.push(0); + if(typeof save.boatSkin!=='number'||save.boatSkin<0||save.boatSkin>2) save.boatSkin=0; + if(typeof save.bobberSkin!=='number'||save.bobberSkin<0||save.bobberSkin>2) save.bobberSkin=0; + if(save.boatSkins.indexOf(save.boatSkin)<0) save.boatSkin=0; // экипированного нет в купленных (битый сейв) + if(save.bobberSkins.indexOf(save.bobberSkin)<0) save.bobberSkin=0; // M2: стрик входа и квесты дня (новый день — перегенерация) applyStreak(); refreshQuests(); @@ -1395,14 +1419,23 @@ function drawBoat(){ // блик-отражение под лодкой (визуал) ctx.fillStyle='rgba(0,20,40,0.28)'; ctx.beginPath(); ctx.ellipse(bx,by+17,34,4.5,0,0,7); ctx.fill(); - // лодка + // лодка (Этап 4: скины — градиент корпуса и кромка по save.boatSkin) + const skin=Math.max(0,Math.min(2,save.boatSkin|0)); + const bcols=[['#c9773c','#96521f','#8a4519'],['#c0392b','#7b241c','#6e221a'],['#5d6d7e','#34495e','#2c3a47']][skin]; const bg=ctx.createLinearGradient(bx-32,by,bx+32,by+15); - bg.addColorStop(0,'#c9773c'); bg.addColorStop(1,'#96521f'); + bg.addColorStop(0,bcols[0]); bg.addColorStop(1,bcols[1]); ctx.fillStyle=bg; ctx.beginPath(); ctx.moveTo(bx-32,by); ctx.lineTo(bx+32,by); ctx.lineTo(bx+21,by+15); ctx.lineTo(bx-21,by+15); ctx.closePath(); ctx.fill(); - ctx.fillStyle='#8a4519'; ctx.fillRect(bx-32,by,64,4); + ctx.fillStyle=bcols[2]; ctx.fillRect(bx-32,by,64,4); + // «Лодка-домик»: каюта на корме корпуса (над линией борта) с ярко-жёлтым окном (визуал) + if(skin===2){ + ctx.fillStyle='#2c3a47'; + ctx.fillRect(bx-31,by-11,16,11); + ctx.fillStyle='rgba(255,220,100,0.9)'; + ctx.fillRect(bx-28,by-9,9,5); + } // рыбак ctx.fillStyle='#34495e'; ctx.fillRect(bx-9,by-18,12,12); @@ -1446,15 +1479,21 @@ function drawBobber(){ // тёмная кромка воды под поплавком (визуал) ctx.fillStyle='rgba(0,20,40,0.25)'; ctx.beginPath(); ctx.ellipse(x,y+6,7,1.8,0,0,7); ctx.fill(); - ctx.fillStyle='#fff'; + // Этап 4: скины — поплавок по save.bobberSkin (0 обычный, 1 красный, 2 неоновый) + const skin=Math.max(0,Math.min(2,save.bobberSkin|0)); + const bot=['#fff','#e74c3c','#aaff00'][skin]; + const top=['#e74c3c','#e74c3c','#aaff00'][skin]; + if(skin===2){ ctx.save(); ctx.shadowColor='rgba(170,255,0,0.8)'; ctx.shadowBlur=10; } // неоновое свечение — обязательно save/restore, чтобы не перетечь на другие элементы + ctx.fillStyle=bot; ctx.beginPath(); ctx.arc(x,y,5.5,0,7); ctx.fill(); - ctx.fillStyle='#e74c3c'; + ctx.fillStyle=top; ctx.beginPath(); ctx.arc(x,y,5.5,Math.PI,0); ctx.fill(); // блик (визуал) ctx.fillStyle='rgba(255,255,255,0.85)'; ctx.beginPath(); ctx.arc(x-1.8,y-1.8,1.4,0,7); ctx.fill(); - ctx.strokeStyle='rgba(255,255,255,0.9)'; ctx.lineWidth=1.5; + ctx.strokeStyle=skin===2?'#77cc00':'rgba(255,255,255,0.9)'; ctx.lineWidth=1.5; ctx.beginPath(); ctx.arc(x,y,5.5,0,7); ctx.stroke(); + if(skin===2) ctx.restore(); } function drawLineAndBobber(){ // след поплавка: копим в забросе, чистим в остальных состояниях (визуал) @@ -1753,6 +1792,7 @@ function setTab(t){ shopTab=t; renderShop(); $('tabJournal').classList.toggle('active',t==='journal'); $('tabRewards').classList.toggle('active',t==='rewards'); $('tabAch').classList.toggle('active',t==='ach'); + $('tabSkins').classList.toggle('active',t==='skins'); } function lvPips(lv){ let s=''; for(let k=1;k<=4;k++) s+=k<=lv?'●':'○'; return s; } function itemRow(item,i,type,owned,cur){ @@ -1780,6 +1820,23 @@ function itemRow(item,i,type,owned,cur){ '