Экономика: ушедшие рыбы без наград — выплаты 10% цены убраны из обеих веток схода в updateReel() (escape>=1 и near-miss 1.45·d0), общий хвост вынесен в fishEscaped() (тост «Сбежала! <emoji> <имя> <вес> кг» → WAIT → biteTimer → sfx('escape')), устаревший near-miss-комментарий удалён; формат сейва не менялся. Тост обрыва: теперь с рыбой («💥 Линия лопнула! 🐟 Карась 0.5 кг» / «💥 Рыба крупнее предела удочки! …», разбор ov>1.15 сохранён); босс-обрыв фазы не тронут. Закрывает камень #12 из Этапа T4 (утешения за сходы = 43–47% раннего дохода, фармить сходы было выгоднее). Тесты: §9c переписан (пин save.quests/стрика — монеты не изменились; тост «Сбежала!» с весом без «🪙»), (2b) переиспользован (сход монет не даёт, HUD в согласии с сейвом), +1 чек тоста обрыва (чтение в следующем eval сразу после waitState('SNAP'): тост 1.6 с > SNAP 1.0 с); orca шаг 9 — «сход без выплат». 129/129, exit 0; Orca 61/61 live. Доки: DEVLOG Этап 23, PHYSICS-RESEARCH исходы, README/AGENTS 128→129.

This commit is contained in:
2026-09-07 13:15:28 +03:00
parent 9914fe89a9
commit 126e402dee
7 changed files with 49 additions and 41 deletions
+1 -1
View File
@@ -11,7 +11,7 @@
npm install # puppeteer-core
npm test # = node test.js
```
Ждать все PASS и exit 0. Сейчас проверок 128.
Ждать все PASS и exit 0. Сейчас проверок 129.
3. Браузерный плейтест (вкладка встроенного браузера Orca, через Orca CLI):
```bash
node tests/orca-playtest.js # из каталога репо; требует ЗАПУЩЕННЫЙ Orca app и orca-ide (или ORCA_CLI_COMMAND)
+1 -1
View File
@@ -62,7 +62,7 @@ python3 -m http.server 8000
```bash
cd tests
npm install # ставит puppeteer-core
node test.js # 128 проверок
node test.js # 129 проверок
```
Нужен установленный Chromium (`executablePath` в `test.js` при необходимости поправить).
+15
View File
@@ -455,3 +455,18 @@ git add -A && git commit -m "что изменилось" && git push
- Геймплей-тест ночного улова требует фонарика: `save.rodLv[save.rod]=2` (ветка WAIT: ночью без `hasLight()` клёв не стартует — таймер тикает вхолостую).
- «Все рыбы во всех вариантах» — отдельная ачивка не добавлялась: это существующая `full_variants` «Полная коллекция» (11×4).
- Порядок записей в `ACHIEVEMENTS` — тематический (снасти у экономики, варианты/погода/вес/ночь у рыболовных, скины/локации у магазинных); тесты не зависят от порядка (только `ACHIEVEMENTS.length` и id).
## Этап 23. Ушедшие рыбы без наград + рыба в тосте обрыва
**Что сделано в `index.html`:**
1. **Сходы без выплат** — из обеих веток схода в `updateReel()` убрано «утешение» 10% цены (`floor(вес × цена/кг × 0.1)` + `save.coins+=nm; persist(); updateHUD()`): escape-бар (`reel.escape>=1`) и near-miss (`reel.d>=reel.d0*1.45`). Общий хвост (сброс `stateBoss`/`bossPhase`, тост → `WAIT` → `biteTimer` → `sfx('escape')`) вынесен в `fishEscaped(sp)`; тост один: «Сбежала! <emoji> <имя> <вес> кг». Устаревший комментарий near-miss-ветки удалён. Формат сейва не менялся — миграция не нужна.
2. **Рыба в тосте обрыва** — тост SNAP теперь с указанием рыбы: «💥 Линия лопнула! <emoji> <имя> <вес> кг» / «💥 Рыба крупнее предела удочки! <emoji> <имя> <вес> кг» (разбор `ov>1.15` сохранён). Босс-обрыв фазы («⚠️ Босс рвёт леску! Фаза N/3») не тронут — это не настоящий обрыв, а рыба и так известна.
**Тесты:** переписаны на месте §9c (`tests/test.js`: пин `save.quests`/стрика перед прогоном — монеты не изменились; тост «Сбежала!» с весом, без «🪙») и (2b) (§9l: сход монет не даёт, HUD в согласии с сейвом); +1 проверка в тесте «обрыв: SNAP -> IDLE» — тост начинается с «💥» и содержит «кг» (тост читается в следующем eval сразу после `waitState('SNAP')`: тост живёт 1.6 с, SNAP — 1.0 с, окна хватает). Комментарий шага 9 в `tests/orca-playtest.js`: «сход с утешением» → «сход без выплат». Итог **129/129 PASS, exit 0** (`node tools/check.mjs` — OK). Счётчики проверок в README/AGENTS.md: 128 → 129.
**Monte-Carlo:** не требовался — физика вываживания не тронута; убраны только выплаты.
**Заметки (осознанные решения и ловушки):**
- Закрывает камень #12 из Этапа T4: утешительные выплаты за сходы давали 43–47% раннего дохода (фармить сходы было выгоднее осторожной игры).
- `fishEscaped(sp)` — единственная точка исхода «рыба ушла»: обе ветки сходятся в неё, новые тексты/звук — править в одном месте.
+2 -2
View File
@@ -92,8 +92,8 @@ escape: держа -= 0.5*dt; отпустил += (0.2+0.4*ag)*dt (1 = сбеж
исходы: tension >= line → SNAP (обрыв, save.snaps++)
d <= 0 → улов (у босса фаз 1–2: «рвёт леску», без штрафа,
дальше d = 0.6·d0 / 1.0·d0)
d >= 1.4*d0 → near-miss: +10% цены, «Сбежала!»
escape >= 1 → сбежала: +10% цены
d >= 1.4*d0 → near-miss: «Сбежала!» (без выплат, Этап 23)
escape >= 1 → сбежала: «Сбежала!» (без выплат, Этап 23)
```
### 1.5 Улов и босс
+10 -22
View File
@@ -1064,16 +1064,7 @@ function updateReel(dt){
reel.escape=Math.min(1,reel.escape+(0.2+ag*0.4)*dt);
}
if(reel.escape>=1){
stateBoss=false; bossPhase=0;
const nm=Math.floor(reel.w*sp.price*0.1);
if(nm>0){
save.coins+=nm; persist(); updateHUD();
toast('Сбежала! '+sp.emoji+' '+sp.name+' '+reel.w.toFixed(1)+' кг · +'+nm+' 🪙');
}else{
toast('Сбежала! '+sp.emoji+' '+sp.name+' '+reel.w.toFixed(1)+' кг');
}
state='WAIT'; biteTimer=rand(5,10)/lure().bite;
sfx('escape');
fishEscaped(sp);
return;
}
if(reel.tension>=reel.line){
@@ -1081,7 +1072,7 @@ function updateReel(dt){
save.snaps++; persist(); // счётчик обрывов (достижения)
sfx('snap'); vib(200);
stateBoss=false; bossPhase=0; // M4: настоящий обрыв — босс-бой окончен
toast(ov>1.15?'💥 Рыба крупнее предела удочки!':'💥 Линия лопнула!');
toast((ov>1.15?'💥 Рыба крупнее предела удочки!':'💥 Линия лопнула!')+' '+sp.emoji+' '+sp.name+' '+reel.w.toFixed(1)+' кг');
checkAchievements();
}else if(reel.d<=0){
if(reel.boss && bossPhase<3){
@@ -1093,19 +1084,16 @@ function updateReel(dt){
catchFish(sp);
}
}else if(reel.d>=reel.d0*1.45){
// near-miss T2b: порог 1.45·d0 — рыба сбежала, утешение 10% цены (SNAP выплаты не даёт)
stateBoss=false; bossPhase=0; // M4: босс тоже может уйти — босс-бой окончен
const nm=Math.floor(reel.w*sp.price*0.1);
if(nm>0){
save.coins+=nm; persist(); updateHUD();
toast('Сбежала! '+sp.emoji+' '+sp.name+' '+reel.w.toFixed(1)+' кг · +'+nm+' 🪙');
}else{
toast('Сбежала! '+sp.emoji+' '+sp.name+' '+reel.w.toFixed(1)+' кг');
}
state='WAIT'; biteTimer=rand(5,10)/lure().bite;
sfx('escape');
// M4: босс тоже может уйти — босс-бой окончен
fishEscaped(sp);
}
}
function fishEscaped(sp){
stateBoss=false; bossPhase=0;
toast('Сбежала! '+sp.emoji+' '+sp.name+' '+reel.w.toFixed(1)+' кг');
state='WAIT'; biteTimer=rand(5,10)/lure().bite;
sfx('escape');
}
function catchFish(sp){
const w=reel.w;
const boss=!!reel.boss; // M4: пойман Осётр-босс
+1 -1
View File
@@ -381,7 +381,7 @@ async function forceCatch() {
}
ok('погода снята: пилюля скрыта', true);
// 9. Near-miss: сход с утешением. Тост живёт недолго → текст читаем в ТОМ ЖЕ
// 9. Near-miss: сход без выплат. Тост живёт недолго → текст читаем в ТОМ ЖЕ
// eval, что и ожидание WAIT (не отдельным вызовом).
await castToWait();
await forceBite();
+19 -14
View File
@@ -160,9 +160,13 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
await page.evaluate(() => { reel.tension = rod().line - 0.3; });
await page.mouse.down();
await waitState(page, 'SNAP', 3000).catch(() => {});
const snapToast = await page.evaluate(() => document.getElementById('toast').textContent);
await page.mouse.up();
await waitState(page, 'IDLE', 3000);
ok('обрыв: SNAP -> IDLE', true);
ok('обрыв: в тосте рыба с весом («💥 …» + «кг»)',
snapToast.startsWith('💥') && snapToast.includes('кг'),
snapToast);
// --- 6. Бросок 3: срыв рыбы (не тянем) ---
await page.mouse.down(); await sleep(600); await page.mouse.up();
@@ -304,9 +308,13 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
&& mig.perch.n === 3 && mig.perch.best === 0 && mig.perch.sn === 0,
JSON.stringify(mig));
// --- 9c. Near-miss: сбежавшая рыба даёт 10% своей цены утешением ---
// --- 9c. Near-miss: сбежавшая рыба не даёт наград (утешение убрано) ---
await page.click('#btnPlay');
await sleep(150);
await page.evaluate(() => {
save.quests = { date: todayStr(), list: [] }; // пин: квестовая награда не искажает монеты
save.streak = { last: todayStr(), days: 1, shieldWeek: null };
});
const coinsBeforeNm = await page.evaluate(() => save.coins);
await page.mouse.down(); await sleep(600); await page.mouse.up();
await waitState(page, 'WAIT');
@@ -315,23 +323,19 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
await page.evaluate(() => {
reel.w = 5; // фиксируем вес — утешение детерминированно
window.__testPrice = reel.sp.price;
reel.phase = 'pull'; reel.phaseT = 9999;
reel.d = reel.d0 * 1.38;
});
await waitState(page, 'WAIT', 15000);
const nm = await page.evaluate(() => ({
coins: save.coins,
exp: Math.floor(5 * window.__testPrice * 0.1),
toast: document.getElementById('toast').textContent,
}));
const nmDelta = nm.coins - coinsBeforeNm;
ok('near-miss: сбежавшая рыба даёт +~10% цены (диапазон)',
nm.exp > 0 && Math.abs(nmDelta - nm.exp) <= 1,
JSON.stringify(nm) + ' delta=' + nmDelta);
ok('near-miss: в DOM тост «Сбежала» с весом и +N 🪙',
nm.toast.startsWith('Сбежала!') && nm.toast.includes('кг') && nm.toast.includes('+') && nm.toast.includes('🪙'),
ok('near-miss: сбежавшая рыба монеты не даёт',
nm.coins === coinsBeforeNm,
JSON.stringify(nm) + ' before=' + coinsBeforeNm);
ok('near-miss: в DOM тост «Сбежала» с весом, без монет',
nm.toast.startsWith('Сбежала!') && nm.toast.includes('кг') && !nm.toast.includes('🪙'),
nm.toast);
// --- 9d. M2 квест: catch_weight (порог 0.1 кг) выполняется уловом, +reward ---
@@ -711,8 +715,9 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
ok('багфикс HUD квеста: счётчик монет обновлён сразу после выполнения квеста на забросе',
castsQ.hud === '🪙 ' + castsQ.coins, castsQ.hud);
// (2b) near-miss: утешение +10% цены тоже начисляется вне catchFish — HUD актуален
// (2b) near-miss: сход монет не даёт — HUD в согласии с сейвом
await page.mouse.move(195, 600);
const nmHudBefore = await page.evaluate(() => save.coins);
await page.mouse.down(); await sleep(450); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
@@ -720,7 +725,6 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
await page.evaluate(() => {
reel.w = 5; // фиксируем вес — утешение детерминированно и больше нуля
reel.phase = 'pull'; reel.phaseT = 9999;
reel.d = reel.d0 * 1.38;
});
@@ -729,8 +733,9 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
coins: save.coins,
hud: document.getElementById('coins').textContent,
}));
ok('багфикс HUD near-miss: после «Сбежала! +N 🪙» счётчик монет актуален',
nmHud.hud === '🪙 ' + nmHud.coins, JSON.stringify(nmHud));
ok('near-miss: сход монет не даёт, HUD в согласии с сейвом',
nmHud.coins === nmHudBefore && nmHud.hud === '🪙 ' + nmHud.coins,
JSON.stringify(nmHud) + ' before=' + nmHudBefore);
// (3) босс-бой: фазовый обрыв должен сбрасывать накопленную нагрузку —
// иначе «обрыв фазы без штрафа» с tension≈0.9·line тут же рвал шнур по-настоящему