Физика: спринт 4 P0 — stamina рыбы, drag катушки, честный баланс сил, пустые клёвы + 7 автотестов (68/68)

This commit is contained in:
2026-09-02 00:51:54 +03:00
parent fafa0adacd
commit 70ff9642b3
15 changed files with 181 additions and 38 deletions
+71 -17
View File
@@ -192,6 +192,8 @@ button{font-family:inherit;cursor:pointer}
<p style="margin-top:8px">У каждой рыбы свой <b>вес</b>, продажа — по цене за кг.<br>
У удочек есть <b>предел веса</b>: перевесишь — шнур лопнет! <br>
Лучшие снасти = больше шансов на трофей 🏆</p>
<p style="margin-top:8px">Не каждый заброс — с клёвом: если молчит — подожди.<br>
У удилищ есть <b>тормоз (drag)</b>: рывок сильнее — леска скользит!</p>
<button id="btnPlay" class="big">Играть</button>
</div>
</div>
@@ -263,11 +265,14 @@ let SHINY_CHANCE=0.03, SHINY_MULT=3;
/* M4: босс-рыба (Осётр-босс) — тест-форс появления (let — автотест, как SHINY_CHANCE) */
let forceBoss=false;
/* Спринт 4: пустые клёвы — тест-форс клёва (let — автотест, паттерн forceBoss) */
let forceBite=false;
const RODS=[
{id:0, name:'Дедова удочка', cost:0, maxW:3, line:100, reel:1.0, size:0.00, desc:'До 3 кг · Нагрузка 100 · Подсечка ×1.0'},
{id:1, name:'Стеклопластик', cost:250, maxW:8, line:140, reel:1.3, size:0.08, desc:'До 8 кг · Нагрузка 140 · Подсечка ×1.3'},
{id:2, name:'Карбоновая', cost:700, maxW:18, line:190, reel:1.7, size:0.16, desc:'До 18 кг · Нагрузка 190 · Подсечка ×1.7'},
{id:3, name:'Титан', cost:1600, maxW:40, line:260, reel:2.3, size:0.28, desc:'До 40 кг · Нагрузка 260 · Подсечка ×2.3'},
{id:0, name:'Дедова удочка', cost:0, maxW:3, line:100, drag:34, reel:1.0, size:0.00, desc:'До 3 кг · Нагрузка 100 · Drag 34 · Подсечка ×1.0'},
{id:1, name:'Стеклопластик', cost:250, maxW:8, line:140, drag:47, reel:1.3, size:0.08, desc:'До 8 кг · Нагрузка 140 · Drag 47 · Подсечка ×1.3'},
{id:2, name:'Карбоновая', cost:700, maxW:18, line:190, drag:63, reel:1.7, size:0.16, desc:'До 18 кг · Нагрузка 190 · Drag 63 · Подсечка ×1.7'},
{id:3, name:'Титан', cost:1600, maxW:40, line:260, drag:87, reel:2.3, size:0.28, desc:'До 40 кг · Нагрузка 260 · Drag 87 · Подсечка ×2.3'},
];
const LURES=[
{id:0, name:'Простой крючок', cost:0, cast:1.00, bite:1.0, rare:0.0, size:0.00, desc:'Надёжно, но рыба не в восторге'},
@@ -694,6 +699,13 @@ function biteWindow(sp){
if(sp.boss) return rand(0.25,0.5); // M4: босс клюёт реже — окно подсечки короче
return Math.max(0.35, 0.8 - sp.rarity*0.08 + save.rod*0.04 + achBonus().windowS);
}
/* Спринт 4: шанс клёва на таймер — не каждый заброс даёт рыбу (дальше = чаще, ночь тише) */
function biteChance(){
const dist=Math.min(1,(castTo.x-(boatX+30))/Math.max(1,W-boatX-70));
let p=0.65+0.25*dist;
if(dayPhase==='night') p*=0.85;
return p;
}
/* Ролл веса рыбы: сдвиг в крупную сторону зависит от удочки и приманки */
function rollWeight(sp){
const B=Math.min(0.95, rod().size + lure().size);
@@ -727,7 +739,14 @@ function hook(){
const aggr=Math.min(1, 0.35*str + 0.75*Math.min(1.25,ov)/1.25); // «злость» рыбы
const d0=40+3.5*w; // чем крупнее, тем дальше от лодки
const line=rod().line+achBonus().line; // прочность с бонусом достижений
reel={d:d0, d0:d0, tension:0, escape:0, holding:false, phase:'calm', phaseT:rand(0.8,1.5), sp:sp, w:w, ov:ov, aggr:aggr, tick:0, line:line, boss:!!sp.boss};
// Спринт 4: выносливость — крупная рыба выносливее, босс свежий
const st0=sp.boss?1:0.65+0.35*Math.min(1,w/sp.wMax);
reel={d:d0, d0:d0, tension:0, escape:0, holding:false, phase:'calm', phaseT:rand(0.8,1.5), sp:sp, w:w, ov:ov, aggr:aggr, tick:0, line:line, boss:!!sp.boss, st:st0, tired:false};
}
/* Спринт 4: сила рыбы (перетягивание с drag) — глобальная, автотесты вызывают */
function fishForce(r,ag,pull){
const base=pull?(16+34*r.st)*(0.5+0.9*ag)*(0.7+0.5*Math.min(1.5,r.ov)):5+7*ag;
return r.tired?0.45*base:base;
}
function doCast(){
const p=Math.max(0.12,power);
@@ -751,23 +770,32 @@ function updateReel(dt){
reel.phaseT-=dt;
if(reel.phaseT<=0){
if(reel.phase==='calm'){
reel.phase='pull'; reel.phaseT=0.3+ag*rand(0.4,0.9);
// Спринт 4: рывок длиннее у свежей рыбы, короче у уставшей
reel.phase='pull'; reel.phaseT=(0.8+2.2*reel.st)*(0.6+0.7*ag)*rand(0.8,1.25);
sfx('pull'); vib(40);
}else{
reel.phase='calm'; reel.phaseT=Math.max(0.35,rand(0.7,1.7)-0.3*ag); vib(15);
// Спринт 4: пауза растёт по мере усталости
reel.phase='calm'; reel.phaseT=Math.max(0.35,(1.3-0.8*reel.st)*rand(0.8,1.3)); vib(15);
}
}
const pulling=reel.phase==='pull';
// Спринт 4: выносливость — расход по фазам и удержанию (свободный бег тоже утомляет)
const drain=(pulling?0.05+0.16*ag:0.008)+(reel.holding?0.035:0.004)+(pulling&&!reel.holding?0.05:0);
reel.st=Math.max(0,reel.st-drain*dt);
if(reel.st<=0.12) reel.tired=true; // устала: рывки слабее, втягивается быстрее
const F=fishForce(reel,ag,pulling);
if(reel.holding){
reel.d-=30*r.reel*dt;
// перегруз (ov>1) — нагрузка растёт кратно: шнур лопнет очень быстро
reel.tension+=(pulling?(14+30*ag)*(0.6+0.9*ov):9)*dt;
// Спринт 4: перетягивание через drag — рывок сильнее тормоза: леска скользит
const over=F-r.drag;
reel.tension+=(8+Math.max(0,over)*1.1+14*ag*(pulling?1:0.35))*dt;
if(over>0) reel.d+=over*0.22*dt;
else reel.d-=(22+8*r.reel)*(1+0.35*(1-reel.st))*dt; // усталую втягивают быстрее
reel.tick-=dt;
if(reel.tick<=0){ sfx('tick'); reel.tick=0.12; }
if(Math.random()<0.3) splash(castTo.x,waterY,1,40);
}else{
reel.tension-=38*dt;
if(pulling) reel.d+=(3+4*ag)*(0.5+0.4*ov)*dt; // крупная/тяжело несёт шнур
if(pulling) reel.d+=F*0.4*dt; // свободный бег: рыба реально отходит
}
reel.tension=Math.max(0,reel.tension);
// escape: рыба ускользает если долго не тянешь
@@ -894,8 +922,11 @@ function updateGame(dt){
if(dayPhase==='night'&&!save.lures.includes(4)){
// M3: ночью без фонарика клёва нет — таймер тикает вхолостую
if(!nightNoLureToast){ toast('Ночью нужен фонарик'); nightNoLureToast=true; }
}else{
}else if(forceBite||Math.random()<biteChance()){
startBite();
}else{
// Спринт 4: пустой клёв — поплавок молчит, попробуем ещё раз
biteTimer=rand(2.5,5)/lure().bite;
}
}
break;
@@ -1283,16 +1314,17 @@ function drawLineAndBobber(){
function drawHookedFish(){
if(state!=='REEL'||!reel) return;
const sp=reel.sp;
const x=castTo.x+Math.sin(time*7)*4;
const y=waterY+26+Math.sin(time*5)*3;
const dir=Math.sin(time*2.5)>0?1:-1;
const k=reel.tired?0.35:1; // Спринт 4: уставшая рыба вялая — медленнее и амплитуда ниже
const x=castTo.x+Math.sin(time*7*k)*4*k;
const y=waterY+26+Math.sin(time*5*k)*3*k;
const dir=Math.sin(time*2.5*k)>0?1:-1;
const fs=8+(reel.w/sp.wMax)*10+sp.rarity;
if(reel.boss){ // босс: золотая аура (визуал)
const gg=ctx.createRadialGradient(x,y,4,x,y,fs*2.6);
gg.addColorStop(0,'rgba(255,215,94,0.35)'); gg.addColorStop(1,'rgba(255,215,94,0)');
ctx.fillStyle=gg; ctx.fillRect(x-fs*2.6,y-fs*2.6,fs*5.2,fs*5.2);
}
drawFishShape(x,y,reel.boss?fs*1.5:fs,dir,sp.hue,0.75,Math.sin(time*9)*0.3); // силуэт крупнее у тяжёлых, босс ×1.5
drawFishShape(x,y,reel.boss?fs*1.5:fs,dir,sp.hue,0.75,Math.sin(time*9*k)*0.3*k); // силуэт крупнее у тяжёлых, босс ×1.5
}
function drawParticles(){
for(const p of particles){
@@ -1341,8 +1373,14 @@ function drawMeters(){
}
ctx.strokeStyle='rgba(255,255,255,0.6)'; ctx.lineWidth=1.5;
ctx.beginPath(); ctx.moveTo(bx-3,by); ctx.lineTo(bx+bw+3,by); ctx.stroke();
// Спринт 4: черта drag — выше неё при удержании леска скользит
const dgy=by+bh*(1-Math.min(1,r.drag/reel.line));
ctx.strokeStyle='rgba(255,255,255,0.9)'; ctx.lineWidth=2;
ctx.beginPath(); ctx.moveTo(bx-2,dgy); ctx.lineTo(bx+bw+2,dgy); ctx.stroke();
const Fm=fishForce(reel,reel.aggr,pulling);
const slipping=reel.holding&&Fm-r.drag>0;
ctx.font='16px system-ui'; ctx.textAlign='center';
ctx.fillText(pulling?'⚡':'💪',bx+bw/2,by-8);
ctx.fillText(slipping?'🌀':pulling?'⚡':(reel.holding?'💪':'·'),bx+bw/2,by-8);
// --- дистанция (снизу) ---
const dw=W-96, dx=22, dy=H-52, dh=13;
ctx.fillStyle='rgba(0,0,0,0.35)'; rr(dx,dy,dw,dh,7); ctx.fill();
@@ -1366,6 +1404,17 @@ function drawMeters(){
ctx.fillText('🐟',fx,dy-6+Math.sin(time*10)*1.5);
ctx.font='18px system-ui';
ctx.fillText('⛵',dx+4,dy+dh+18);
// Спринт 4: выносливость — тонкая полоска под дистанцией (затухает к нулю)
const sdh=5, sdy=dy+dh+3;
ctx.fillStyle='rgba(0,0,0,0.3)'; rr(dx,sdy,dw,sdh,2.5); ctx.fill();
if(reel.st>0.01){
ctx.fillStyle=reel.tired?'#7d5ba6':'#b07cc6';
rr(dx+1.5,sdy+1.5,Math.max(1.5,(dw-3)*reel.st),sdh-3,2); ctx.fill();
}
if(reel.tired){
ctx.font='13px system-ui'; ctx.textAlign='left';
ctx.fillText('💤',dx+dw+6,sdy+sdh-1);
}
// статус
ctx.font='700 15px system-ui'; ctx.textAlign='center';
if(pulling){
@@ -1373,6 +1422,11 @@ function drawMeters(){
ctx.shadowColor='rgba(255,60,40,'+(0.5+0.4*Math.sin(time*10)).toFixed(3)+')'; ctx.shadowBlur=8;
ctx.fillText('⚡ РЫБА ТЯНЕТ — ОТПУСТИ! ⚡',W/2,dy-30);
ctx.shadowBlur=0;
}else if(reel.tired){
ctx.fillStyle='#c9a0f5';
ctx.shadowColor='rgba(120,60,180,0.6)'; ctx.shadowBlur=6;
ctx.fillText('💤 Рыба устала — втягивай!',W/2,dy-30);
ctx.shadowBlur=0;
}else{
ctx.fillStyle='#7ef0a8';
ctx.shadowColor='rgba(0,0,0,0.6)'; ctx.shadowBlur=4;
Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 373 KiB

After

Width:  |  Height:  |  Size: 354 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 318 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 KiB

After

Width:  |  Height:  |  Size: 132 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 432 KiB

After

Width:  |  Height:  |  Size: 437 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 380 KiB

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 160 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 160 KiB

After

Width:  |  Height:  |  Size: 155 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 253 KiB

After

Width:  |  Height:  |  Size: 277 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 493 KiB

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 131 KiB

+1 -1
View File
@@ -160,7 +160,7 @@ async function castToWait() {
// (~0.3-0.5 c) длиннее окна подсечки (0.35-0.8 c), а на ПЕРВОПЛАНАРНОЙ вкладке
// игра идёт в реальном времени.
async function forceBite() {
await evalPage('biteTimer = 0.05; "ok"');
await evalPage('forceBite = true; biteTimer = 0.05; "ok"');
const t0 = Date.now();
let last = '';
while (Date.now() - t0 < 30000) {
+2 -2
View File
@@ -84,7 +84,7 @@ async function snap(page, name) {
await waitState(page, 'WAIT');
await waitPaint(page);
await snap(page, 'wait');
await page.evaluate(() => { biteTimer = 0.05; });
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await waitPaint(page);
await snap(page, 'bite');
@@ -124,7 +124,7 @@ async function snap(page, name) {
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 page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
+107 -18
View File
@@ -57,7 +57,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
await page.screenshot({ path: 'shot-wait.png' });
// --- 3. Клёв и подсечка ---
await page.evaluate(() => { biteTimer = 0.05; });
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
ok('клев: state=BITE', true);
await page.screenshot({ path: 'shot-bite.png' });
@@ -91,7 +91,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
// --- 4b. Перегруз: тяжёлая рыба на слабой удочке рвёт шнур быстро ---
await page.mouse.down(); await sleep(450); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; });
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -100,16 +100,16 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
reel.d = reel.d0 = 150;
});
await page.mouse.down();
await waitState(page, 'SNAP', 4000);
await waitState(page, 'SNAP', 6000);
await page.mouse.up();
await waitState(page, 'IDLE', 3000);
await waitState(page, 'IDLE', 6000); // новая физика: худший кейс (calm→pull) ≈ 2.6 с + SNAP 1 с
ok('перегруз: 12 кг на удочке 3 кг = быстрый обрыв', true);
// --- 4c. Рекорд: улов ниже лучшего — бейджа нет ---
await page.evaluate(() => { for (const s of SPECIES) save.caught[s.id] = { n: 1, best: 9999 }; });
await page.mouse.down(); await sleep(450); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; });
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -125,7 +125,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
// --- 5. Бросок 2: обрыв линии ---
await page.mouse.down(); await sleep(450); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; });
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -139,7 +139,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
// --- 6. Бросок 3: срыв рыбы (не тянем) ---
await page.mouse.down(); await sleep(600); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; });
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -220,7 +220,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
const coinsBeforeShiny = await page.evaluate(() => save.coins);
await page.mouse.down(); await sleep(450); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; SHINY_CHANCE = 1; });
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; SHINY_CHANCE = 1; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -280,7 +280,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
const coinsBeforeNm = await page.evaluate(() => save.coins);
await page.mouse.down(); await sleep(600); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; });
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -322,7 +322,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
const m2qBefore = await page.evaluate(() => save.coins);
await page.mouse.down(); await sleep(450); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; });
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -410,7 +410,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
});
await page.mouse.down(); await sleep(450); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; });
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -498,7 +498,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
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 page.evaluate(() => { forceBite=true; biteTimer=0.05; });
await sleep(600);
const m3nb = await page.evaluate(() => ({
st:state, bf:!!biteFish, bt:biteTimer,
@@ -515,7 +515,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
await page.evaluate(() => { setDayTime(0.8); save.lures=[0,4]; });
await page.mouse.down(); await sleep(450); await page.mouse.up();
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer=0.05; });
await page.evaluate(() => { forceBite=true; biteTimer=0.05; });
await waitState(page, 'BITE', 4000);
ok('M3 ночь с фонариком (save.lures включает 4): клёв работает (BITE)', true);
@@ -570,7 +570,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
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 page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -681,7 +681,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
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 page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -709,7 +709,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
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 page.evaluate(() => { forceBite = true; biteTimer = 0.05; });
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
@@ -742,7 +742,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
await sleep(500);
await page.mouse.up(); // бросок
await waitState(page, 'WAIT');
await page.evaluate(() => { biteTimer = 0.05; }); // ускоряем клёв
await page.evaluate(() => { forceBite = true; biteTimer = 0.05; }); // ускоряем клёв
await waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up(); // подсечка
await waitState(page, 'REEL');
@@ -778,7 +778,96 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
state = 'IDLE'; reel = null; biteFish = null;
});
// --- 10. Ошибки консоли ---
// --- 10. Спринт 4: физика (stamina, drag, пустые клёвы) ---
// Выводим рыбу на крючок и держим один бой для тестов (1)-(4),
// между тестами вручную возвращаем reel в безопасное состояние (d посередине,
// tension=0, line велик — чтобы бой не закончился ловлей/обрывом в окне замера).
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 waitState(page, 'BITE', 4000);
await page.mouse.down(); await page.mouse.up();
await waitState(page, 'REEL');
// (1) stamina: reel.st есть и убывает в бою
const st0 = await page.evaluate(() => {
reel.d0 = 200; reel.d = 100; reel.tension = 0; reel.line = 500;
reel.phase = 'calm'; reel.phaseT = 9999; reel.holding = false;
return reel.st;
});
ok('stamina: reel.st инициализируется (>0)', st0 > 0, 'st0=' + st0.toFixed(3));
await page.mouse.down(); // holding=true — удерживаем
await sleep(2000);
const st1 = await page.evaluate(() => reel.st);
ok('stamina: reel.st убывает в бою (st<st0 после 2 с удержания)', st1 < st0,
'st0=' + st0.toFixed(3) + ' st1=' + st1.toFixed(3));
// (2) stamina: усталая рыба слабее — fishForce глобальна, tired лачится при st<=0.12
const f = await page.evaluate(() => {
const r = { st: 1, tired: false, ov: 1 };
const F1 = fishForce(r, 0.5, true);
r.st = 0.05;
const F2 = fishForce(r, 0.5, true);
return { F1, F2 };
});
ok('stamina: слабая/уставшая рыба тянет слабее (F(st=0.05) < 0.6·F(st=1))',
f.F2 < f.F1 * 0.6, 'F1=' + f.F1.toFixed(1) + ' F2=' + f.F2.toFixed(1));
await page.evaluate(() => {
reel.d0 = 200; reel.d = 100; reel.tension = 0; reel.line = 500;
reel.phase = 'calm'; reel.phaseT = 9999; reel.st = 0.1; reel.tired = false;
});
await sleep(200); // кадры updateReel зафиксируют tired при st<=0.12
const tiredFlag = await page.evaluate(() => reel.tired);
ok('stamina: при st=0.1 рыба устала (reel.tired===true)', tiredFlag === true,
'tired=' + tiredFlag);
// (3) drag: рывок сильнее drag — леска скользит (d уходит, tension за чертой)
await page.evaluate(() => {
reel.w = 12; reel.ov = 4; reel.aggr = 1; reel.st = 1; reel.tired = false;
reel.phase = 'pull'; reel.phaseT = 9999; reel.tension = 0; reel.line = 500;
reel.d0 = 100; reel.d = 100;
});
await sleep(1000); // палец всё ещё нажат (holding=true)
const slip = await page.evaluate(() => ({ d: reel.d, d0: reel.d0, tension: reel.tension, drag: rod().drag }));
ok('drag: рывок сильнее drag — леска скользит (d растёт, tension>drag)',
slip.d > slip.d0 && slip.tension > slip.drag,
JSON.stringify(slip));
// (4) drag: рывок слабее drag — спокойно втягиваем (d падает, tension под чертой)
await page.evaluate(() => {
reel.w = 0.3; reel.ov = 0.1; reel.aggr = 0.2; reel.st = 0.9; reel.tired = false;
reel.phase = 'pull'; reel.phaseT = 9999; reel.tension = 0; reel.line = 500;
reel.d0 = 100; reel.d = 100;
});
await sleep(1000);
const reelIn = await page.evaluate(() => ({ d: reel.d, d0: reel.d0, tension: reel.tension, drag: rod().drag }));
ok('drag: рывок слабее drag — втягивание (d падает, tension<drag)',
reelIn.d < reelIn.d0 && reelIn.tension < reelIn.drag,
JSON.stringify(reelIn));
await page.mouse.up(); // отпустили палец
await page.evaluate(() => { state = 'IDLE'; reel = null; biteFish = null; forceBite = false; });
// (5) пустой клёв: без forceBite и с Math.random=0.99+ клёва нет
await page.evaluate(() => { setDayTime(0.3); 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(() => {
window.__mr = Math.random;
Math.random = () => 0.999; // выше любого biteChance (макс ~0.9 днём)
biteTimer = 0.05;
forceBite = false;
});
await sleep(700);
const empty = await page.evaluate(() => {
Math.random = window.__mr;
return { st: state, bf: !!biteFish };
});
ok('пустой клёв: без forceBite и Math.random=0.99+ — клёва нет (WAIT, нет biteFish)',
empty.st === 'WAIT' && !empty.bf, JSON.stringify(empty));
// --- 11. Ошибки консоли ---
ok('нет ошибок JS (pageerror)', pageErrors.length === 0, pageErrors.join(' | '));
ok('нет ошибок консоли', consoleErrors.length === 0, consoleErrors.join(' | '));