Механика: escape — рыба сбегает, если долго не тянуть (красная полоса дистанции, тост, +10% цены) + 3 автотеста (61/61)

This commit is contained in:
2026-09-01 22:54:17 +03:00
parent 851de88c2a
commit 6e22edddf3
3 changed files with 75 additions and 2 deletions
+4
View File
@@ -10,3 +10,7 @@ tests/shot-*.png
# Локальные worktree спринтов (Orca)
sprint3-*/
sprint2-*/
sprint4-*/
# Секреты (никогда не коммитить)
.gitea_token
+28 -2
View File
@@ -187,7 +187,8 @@ button{font-family:inherit;cursor:pointer}
Дальше бросок = реже и жирнее рыба!</p>
<p style="margin-top:8px">Видишь <b style="color:#ff6b5e">!</b> — быстро жми, чтобы зацепить.<br>
При вываживании: тяни, когда рыба отдыхает,<br>
отпусти, когда она <b>тянет</b> — иначе лопнет!</p>
отпусти, когда она <b>тянет</b> — иначе лопнет!<br>
Не тяни слишком долго — рыба <b>сбегает</b> (красная полоса)!</p>
<p style="margin-top:8px">У каждой рыбы свой <b>вес</b>, продажа — по цене за кг.<br>
У удочек есть <b>предел веса</b>: перевесишь — шнур лопнет! <br>
Лучшие снасти = больше шансов на трофей 🏆</p>
@@ -726,7 +727,7 @@ 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, 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};
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};
}
function doCast(){
const p=Math.max(0.12,power);
@@ -769,6 +770,25 @@ function updateReel(dt){
if(pulling) reel.d+=(3+4*ag)*(0.5+0.4*ov)*dt; // крупная/тяжело несёт шнур
}
reel.tension=Math.max(0,reel.tension);
// escape: рыба ускользает если долго не тянешь
if(reel.holding){
reel.escape=Math.max(0,reel.escape-0.5*dt);
}else{
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');
return;
}
if(reel.tension>=reel.line){
state='SNAP'; snapT=1.0; shake=0.5;
save.snaps++; persist(); // счётчик обрывов (достижения)
@@ -1326,6 +1346,12 @@ function drawMeters(){
// --- дистанция (снизу) ---
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();
// escape: красная заливка справа налево (рыба ускользает)
if(reel.escape>0.01){
const ew=(dw-4)*reel.escape;
ctx.fillStyle='rgba(231,76,60,0.7)';
rr(dx+dw-2-ew,dy+2,ew,dh-4,5); ctx.fill();
}
// rem = доля оставшейся дистанции: полоска «сгорает» к лодке, рыбка на её конце
const rem=Math.max(0,Math.min(1,reel.d/reel.d0));
if(rem>0.01){
+43
View File
@@ -735,6 +735,49 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
state = 'IDLE'; reel = null; stateBoss = false; bossPhase = 0; biteFish = null;
});
// --- 9f. escape: рыба ускользает если долго не тянешь ---
// полный цикл: IDLE → CHARGE → WAIT → BITE → REEL
await page.mouse.move(195, 600);
await page.mouse.down();
await sleep(500);
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');
// ставим рыбу в покой, чтобы escape не мешал, и фиксируем aggr
await page.evaluate(() => { reel.phase = 'calm'; reel.phaseT = 9999; reel.holding = false; });
await sleep(100);
const escBefore = await page.evaluate(() => reel.escape);
await sleep(2000); // 2 с без удержания — escape растёт
const escAfter = await page.evaluate(() => ({ escape: reel.escape, state }));
ok('escape: растёт когда палец отпущен',
escAfter.escape > escBefore && escAfter.state === 'REEL',
JSON.stringify({ before: escBefore, after: escAfter.escape }));
// теперь жмём палец — escape должен уменьшаться
await page.mouse.down();
await sleep(800);
const escHeld = await page.evaluate(() => reel.escape);
await page.mouse.up();
ok('escape: уменьшается когда палец нажат',
escHeld < escAfter.escape,
JSON.stringify({ afterHold: escHeld, was: escAfter.escape }));
// рыба сбегает при escape >= 1
await page.evaluate(() => { reel.escape = 0.95; reel.holding = false; reel.phase = 'calm'; reel.phaseT = 9999; });
await sleep(600);
const escGone = await page.evaluate(() => ({ escape: reel.escape, state, toast: document.getElementById('toast').textContent }));
ok('escape: рыба сбегает при escape >= 1 (state=WAIT, тост «Сбежала!»)',
escGone.state === 'WAIT' && escGone.toast.includes('Сбежала!'),
JSON.stringify(escGone));
// сброс для следующих тестов
await page.evaluate(() => {
state = 'IDLE'; reel = null; biteFish = null;
});
// --- 10. Ошибки консоли ---
ok('нет ошибок JS (pageerror)', pageErrors.length === 0, pageErrors.join(' | '));
ok('нет ошибок консоли', consoleErrors.length === 0, consoleErrors.join(' | '));