Визуал: живая оснастка — крючок под водой, топление поплавка при клёве, диагональное вываживание. 1) Оснастка (WAIT/APPROACH/BITE): поводок от поплавка вниз, крючок (дуга+жало) и наживка; глубина крючка — 42–54% водной колонны по дальности заброса (hookY()); новый глобал castDist (дальность 0..0.97 из doCast), hookPos (позиция крючка, экспорт для отрисовки); эмодзи наживки с крючка убран. 2) Поклёвка: в APPROACH дрожание поплавка растёт по мере приближения рыбы, цель подплывания — hookPos (рыба идёт диагональю от поверхности к наживке, не телепортируется); в BITE поплавок топится (bobber.y=waterY+9, кромка воды поверх нижней половины — drawBobber(sink)), рыба дёргается у крючка в такт топлению (time*22). 3) Вываживание: fishReelPos() — рыба по диагонали от глубины крючка (castTo.x, hookY()) к лодке у поверхности (boatX+24, waterY+10) по прогрессу d/d0 (ease), подводный сегмент лески bobber→рыба; при holding линия удочка-поплавок-рыба натянута (sag=0, поплавок — точка пересечения прямой tip→fish с поверхностью, сглаживание dt*8), при отпускании провис (pull 16 / calm 12); риплы/брызги в REEL/BITE — по позиции поплавка, не castTo.x. Только canvas: механика (updateReel/startBite/hook/catchFish), сейв, баланс и имена тест-глобалов из AGENTS.md не тронуты. tests/shots.js: кадр reel — reel.holding=true + пауза 650 мс (поплавок доходит до натянутого положения). 129/129, exit 0; кадры wait/bite/reel проверены по скриншотам, свежие в screenshots/.
This commit is contained in:
+87
-15
@@ -833,6 +833,8 @@ let time=0;
|
||||
let charge=0, power=0;
|
||||
let castT=0, castFrom={x:0,y:0}, castTo={x:0,y:0};
|
||||
let bobber={x:0,y:0};
|
||||
let castDist=0; // дальность заброса 0..0.97 (визуал: глубина крючка оснастки)
|
||||
let hookPos={x:0,y:0}; // позиция крючка (визуал; пригодится lane поклёвки/вываживания)
|
||||
let biteTimer=0, biteT=0, biteFish=null, rippleTimer=0;
|
||||
let reel=null;
|
||||
let snapT=0, shake=0;
|
||||
@@ -1009,6 +1011,7 @@ function fishForce(r,ag,pull){
|
||||
function doCast(){
|
||||
const p=Math.max(0.12,power);
|
||||
const dist=Math.min(0.97,(0.10+0.80*p)*lure().cast);
|
||||
castDist=dist; // запоминаем дальность (визуал: глубина крючка оснастки)
|
||||
castFrom={x:tip.x,y:tip.y};
|
||||
castTo={x:boatX+30+dist*Math.max(1,W-boatX-70), y:waterY-2};
|
||||
castT=0; state='CAST'; sfx('cast');
|
||||
@@ -1050,7 +1053,7 @@ function updateReel(dt){
|
||||
else reel.d-=(11+3*r.reel)*(1+0.6*(1-reel.st))*dt; // втягивание, усталую быстрее (до ×1.6)
|
||||
reel.tick-=dt;
|
||||
if(reel.tick<=0){ sfx('tick'); reel.tick=0.12; }
|
||||
if(Math.random()<0.3) splash(castTo.x,waterY,1,40);
|
||||
if(Math.random()<0.3) splash(bobber.x,waterY,1,40); // брызги у поплавка (он едет за рыбой)
|
||||
}else{
|
||||
reel.tension-=38*dt;
|
||||
if(pulling) reel.d+=F*0.32*dt; // свободный бег: рыба реально отходит (стоимость отпускания — дистанция)
|
||||
@@ -1193,6 +1196,7 @@ function updateGame(dt){
|
||||
}
|
||||
case 'APPROACH':
|
||||
updateBiteFishIn(dt);
|
||||
bobber.y=waterY-3+Math.sin(time*2)*1.5+Math.sin(time*14)*1.5*Math.min(1,biteFish?biteFish.t:0); // предвестники: дрожание, сильнее по мере приближения рыбы
|
||||
if(biteFish){
|
||||
if(biteFish.t>=1){
|
||||
state='BITE';
|
||||
@@ -1204,8 +1208,8 @@ function updateGame(dt){
|
||||
break;
|
||||
case 'BITE':
|
||||
biteT-=dt;
|
||||
bobber.y=waterY+5+Math.sin(time*22)*2;
|
||||
if(Math.random()<0.2) addRipple(castTo.x,waterY+3,26);
|
||||
bobber.y=waterY+9+Math.sin(time*22)*3; // топление: глубже и резче (синхронно с дёрганием рыбы у крючка)
|
||||
if(Math.random()<0.2) addRipple(bobber.x,waterY+3,26);
|
||||
if(biteT<=0){
|
||||
if(biteFish) biteFish.mode='out';
|
||||
state='WAIT'; biteTimer=rand(5,11)/lure().bite;
|
||||
@@ -1215,9 +1219,20 @@ function updateGame(dt){
|
||||
case 'REEL':
|
||||
updateReel(dt);
|
||||
if(state==='REEL'&&reel){
|
||||
bobber.x=castTo.x+(reel.holding?Math.sin(time*30)*2:0);
|
||||
bobber.y=waterY-2+Math.sin(time*9)*2;
|
||||
if(Math.random()<0.25) addRipple(castTo.x,waterY+3,24);
|
||||
const fp=fishReelPos();
|
||||
let tx,ty;
|
||||
if(reel.holding){
|
||||
// натянуто: поплавок на пересечении прямой tip→fish с поверхностью (tip–поплавок–рыба коллинеарны, fp.y>waterY>tip.y)
|
||||
const k=(waterY-tip.y)/(fp.y-tip.y);
|
||||
tx=tip.x+(fp.x-tip.x)*k; ty=waterY-2;
|
||||
}else{
|
||||
// отпускание: провис, поплавок едет за рыбой с отставанием (покачивание на поверхности)
|
||||
tx=castTo.x+(fp.x-castTo.x)*0.55; ty=waterY-2+Math.sin(time*9)*2;
|
||||
}
|
||||
const sm=Math.min(1,dt*8); // сглаживание, чтобы не телепортировалось
|
||||
bobber.x+=(tx-bobber.x)*sm;
|
||||
bobber.y+=(ty-bobber.y)*sm;
|
||||
if(Math.random()<0.25) addRipple(bobber.x,waterY+3,24);
|
||||
}
|
||||
break;
|
||||
case 'SNAP':
|
||||
@@ -1241,9 +1256,10 @@ function updateBiteFishIn(dt){
|
||||
biteFish.t+=dt/0.45;
|
||||
const t=Math.min(1,biteFish.t);
|
||||
const e=t*t*(3-2*t);
|
||||
biteFish.x=biteFish.sx+(castTo.x-biteFish.sx)*e;
|
||||
biteFish.y=biteFish.sy+(waterY+14-biteFish.sy)*e;
|
||||
biteFish.dir=castTo.x>biteFish.sx?1:-1;
|
||||
// рыба плывёт диагональю от поверхности вниз к крючку/наживке (hookPos — кадр прошлого, паттерн как раньше)
|
||||
biteFish.x=biteFish.sx+(hookPos.x-biteFish.sx)*e;
|
||||
biteFish.y=biteFish.sy+(hookPos.y-biteFish.sy)*e;
|
||||
biteFish.dir=hookPos.x>biteFish.sx?1:-1;
|
||||
}
|
||||
function updateBiteFishOut(dt){
|
||||
biteFish.t+=dt/0.6;
|
||||
@@ -1528,7 +1544,7 @@ function drawLineTo(x,y,sag){
|
||||
ctx.quadraticCurveTo((tip.x+x)/2,(tip.y+y)/2+sag,x,y);
|
||||
ctx.stroke();
|
||||
}
|
||||
function drawBobber(){
|
||||
function drawBobber(sink){
|
||||
const x=bobber.x,y=bobber.y;
|
||||
// тёмная кромка воды под поплавком (визуал)
|
||||
ctx.fillStyle='rgba(0,20,40,0.25)';
|
||||
@@ -1548,6 +1564,33 @@ function drawBobber(){
|
||||
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();
|
||||
// топление (визуал, lane 2): в BITE нижняя половина уходит под воду — полупрозрачная кромка воды поверх низа поплавка
|
||||
// рисуем ВНЕ save/restore неона, чтобы свечение не перетекло на слой воды
|
||||
if(sink){
|
||||
ctx.fillStyle='rgba(10,30,60,0.35)';
|
||||
ctx.beginPath(); ctx.arc(x,y,5.6,0,Math.PI); ctx.closePath(); ctx.fill(); // нижняя половина под водой
|
||||
ctx.strokeStyle='rgba(150,200,235,0.55)'; ctx.lineWidth=1; // тонкая светлая линия уровня воды
|
||||
ctx.beginPath(); ctx.moveTo(x-5.6,y); ctx.lineTo(x+5.6,y); ctx.stroke();
|
||||
}
|
||||
}
|
||||
function hookY(){
|
||||
// глубина крючка на середине воды: доля глубины 0.42..0.54 по дальности заброса (визуал)
|
||||
return waterY+(H-waterY)*(0.42+castDist*0.12);
|
||||
}
|
||||
function drawRig(){
|
||||
// Живой оснастки (lane 1): поводок от поплавка вниз, крючок и наживка.
|
||||
// Только визуал — механика/баланс не трогается. hookPos экспортируем для следующих шагов.
|
||||
const hy=hookY(); // глубина крючка (середина воды, доля 0.42..0.54 по дальности)
|
||||
const hx=bobber.x+Math.sin(time*2.2)*3; // лёгкое покачивание в противофазе к поплавку
|
||||
hookPos={x:hx,y:hy};
|
||||
// вертикальный бледный поводок (бледнее надводной лески 0.8 → 0.35)
|
||||
ctx.strokeStyle='rgba(230,240,255,0.35)'; ctx.lineWidth=1;
|
||||
ctx.beginPath(); ctx.moveTo(bobber.x,bobber.y); ctx.lineTo(hx,hy); ctx.stroke();
|
||||
// крючок: дуга + жало (серый ~5px)
|
||||
ctx.strokeStyle='#cfd6dd'; ctx.lineWidth=1.4; ctx.lineCap='round';
|
||||
ctx.beginPath(); ctx.arc(hx,hy+4,4.5,Math.PI*0.1,Math.PI*1.6); ctx.stroke();
|
||||
const eb=Math.PI*1.6, ex=hx+4.5*Math.cos(eb), ey=(hy+4)+4.5*Math.sin(eb);
|
||||
ctx.beginPath(); ctx.moveTo(ex,ey); ctx.lineTo(ex+2.5,ey-2); ctx.stroke(); // жало
|
||||
}
|
||||
function drawLineAndBobber(){
|
||||
// след поплавка: копим в забросе, чистим в остальных состояниях (визуал)
|
||||
@@ -1583,9 +1626,20 @@ function drawLineAndBobber(){
|
||||
}
|
||||
drawLineTo(bobber.x,bobber.y,14); drawBobber(); return;
|
||||
}
|
||||
const sag=state==='REEL'?(reel.phase==='pull'?4:12):18+Math.sin(time*2)*3;
|
||||
let sag;
|
||||
if(state==='REEL'){
|
||||
sag=reel.holding?0:(reel.phase==='pull'?16:12); // holding — натянуто (прямая tip→поплавок); отпускание — провис (рывок больше)
|
||||
}else{
|
||||
sag=18+Math.sin(time*2)*3;
|
||||
}
|
||||
drawLineTo(bobber.x,bobber.y,sag);
|
||||
drawBobber();
|
||||
drawBobber(state==='BITE'?1:0); // топление: в BITE нижняя половина под водой, в WAIT/APPROACH/REEL — нет
|
||||
if(state==='REEL'&&reel){ // подводный сегмент лески bobber→рыба (бледнее надводной), чтобы леска шла к рыбе
|
||||
const fp=fishReelPos();
|
||||
ctx.strokeStyle='rgba(230,240,255,0.35)'; ctx.lineWidth=1;
|
||||
ctx.beginPath(); ctx.moveTo(bobber.x,bobber.y); ctx.lineTo(fp.x,fp.y); ctx.stroke();
|
||||
}
|
||||
if(state==='WAIT'||state==='APPROACH'||state==='BITE') drawRig(); // живая оснастка (в REEL крючок «в рыбе» — не рисуем)
|
||||
if(state==='BITE'){
|
||||
const yy=bobber.y-24+Math.sin(time*12)*2;
|
||||
ctx.font='800 28px system-ui'; ctx.textAlign='center';
|
||||
@@ -1595,12 +1649,22 @@ function drawLineAndBobber(){
|
||||
ctx.fillText('!',bobber.x,yy);
|
||||
}
|
||||
}
|
||||
function fishReelPos(){
|
||||
// lane 3: позиция пойманной рыбы в REEL по прогрессу вываживания — по диагонали от глубины крючка (середина воды) к лодке у поверхности (d0>0 гарантировано)
|
||||
const p=Math.max(0,Math.min(1,1-reel.d/reel.d0));
|
||||
const pe=p*p*(3-2*p); // лёгкое ease — плавный подход к лодке
|
||||
const k=reel.tired?0.35:1; // уставшая рыба вялая — амплитуда покачивания ниже
|
||||
const hy=hookY(); // старт пути — точка крючка (середина воды)
|
||||
const x=castTo.x+(boatX+24-castTo.x)*pe + Math.sin(time*7*k)*4*k; // сохраняем покачивание
|
||||
const y=hy+(waterY+10-hy)*pe + Math.sin(time*5*k)*3*k; // диагональ: из глубины к лодке у поверхности
|
||||
return {x:x,y:y};
|
||||
}
|
||||
function drawHookedFish(){
|
||||
if(state!=='REEL'||!reel) return;
|
||||
const sp=reel.sp;
|
||||
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 fp=fishReelPos();
|
||||
const x=fp.x, y=fp.y;
|
||||
const dir=Math.sin(time*2.5*k)>0?1:-1;
|
||||
const fs=8+(reel.w/sp.wMax)*10+sp.rarity;
|
||||
if(reel.boss){ // босс: золотая аура (визуал)
|
||||
@@ -1787,7 +1851,15 @@ function draw(){
|
||||
drawSky();
|
||||
drawWater();
|
||||
drawAmbientFish();
|
||||
if(biteFish&&biteFish.mode!=='out') drawFishShape(biteFish.x,biteFish.y,12+biteFish.sp.rarity*2.5,biteFish.dir,biteFish.sp.hue,0.8,Math.sin(time*8)*0.25);
|
||||
if(biteFish&&biteFish.mode!=='out'){
|
||||
if(state==='BITE'){
|
||||
// топление (lane 2): рыба дёргается у крючка, синхронно с фазами топления поплавка (time*22)
|
||||
drawFishShape(hookPos.x+Math.sin(time*25)*7, hookPos.y+Math.cos(time*22)*5, 12+biteFish.sp.rarity*2.5, Math.sin(time*25)>0?1:-1, biteFish.sp.hue, 0.8, Math.sin(time*8)*0.25);
|
||||
}else{
|
||||
// APPROACH: подплывает к поплавку как раньше
|
||||
drawFishShape(biteFish.x,biteFish.y,12+biteFish.sp.rarity*2.5,biteFish.dir,biteFish.sp.hue,0.8,Math.sin(time*8)*0.25);
|
||||
}
|
||||
}
|
||||
else if(biteFish&&biteFish.mode==='out') drawFishShape(biteFish.x,biteFish.y,12+biteFish.sp.rarity*2.5,biteFish.dir,biteFish.sp.hue,0.5,Math.sin(time*8)*0.25);
|
||||
drawBoat();
|
||||
drawLineAndBobber();
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 133 KiB After Width: | Height: | Size: 138 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 157 KiB After Width: | Height: | Size: 157 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 132 KiB After Width: | Height: | Size: 139 KiB |
+2
-1
@@ -91,7 +91,8 @@ async function snap(page, name) {
|
||||
await page.mouse.down(); await page.mouse.up();
|
||||
await waitState(page, 'REEL');
|
||||
// покажем фазу «тянет» для наглядности
|
||||
await page.evaluate(() => { reel.phase = 'pull'; reel.phaseT = 5; reel.tension = 0.55 * rod().line; reel.d = reel.d0 * 0.55; });
|
||||
await page.evaluate(() => { reel.phase = 'pull'; reel.phaseT = 5; reel.tension = 0.55 * rod().line; reel.d = reel.d0 * 0.55; reel.holding = true; });
|
||||
await sleep(650); // время ИГРЫ: поплавок доходит до натянутого положения (tip–поплавок–рыба в одну линию)
|
||||
await waitPaint(page);
|
||||
await snap(page, 'reel');
|
||||
// доловим
|
||||
|
||||
Reference in New Issue
Block a user