Механика веса рыбы
- у каждой рыбы свой вес (wMin..wMax кг), цена за 1 кг (price)
- удилища: предел веса (3-40 кг); перегруз -> шнур лопает почти мгновенно
- лучшая удочка/приманка -> выше шанс крупного улова (rollWeight)
- сложность вываживания зависит от веса и перегруза (ov, aggr)
- дневник: максимальный вес по каждому виду
- карточка улова: вес, цена/кг, бейдж «РЕКОРД!» при новом максимуме
- миграция старых сейвов (caught: число -> {n, best})
- автотест: 24 -> 26 проверок (перегруз, рекорд/антирекорд)
- обновлены README и скриншоты
@@ -12,15 +12,18 @@
|
||||
|
||||
1. **Держи палец** — заряжается мощность броска, **отпусти** — кидаешь удочку. Чем дальше, тем реже, но ценнее рыба.
|
||||
2. Когда поплавок нырнул и появилась **«!**» — быстро нажми экран, чтобы зацепить. У редких рыб окно на подсечку всего доли секунды.
|
||||
3. **Вываживание**: держишь палец — втягиваешь, но растёт нагрузка на шнур (жёлтая полоса справа). Рыба периодически **тянет** (⚡, с вибрацией) — отпусти в этот момент, иначе шнур лопнет. Полоса снизу — оставшаяся дистанция: доведи рыбку до лодки.
|
||||
4. Пойманная рыба приносит монеты. На них — новые удилища и приманки в магазине 🛒.
|
||||
3. **Вываживание**: держишь палец — втягиваешь, но растёт нагрузка на шнур (жёлтая полоса справа). Рыба периодически **тянет** (⚡, с вибрацией) — отпусти в этот момент, иначе шнур лопнет. Полоса снизу — оставшаяся дистанция: доведи рыбку до лодки. Чем крупнее рыба, тем сильнее рвёт и дольше тянет.
|
||||
4. **У каждой рыбы свой вес**, продажа — по цене за кг своего вида (от 20 до 1500 🪙/кг). Крупная рыба = большие деньги.
|
||||
5. **У удочек есть предел веса** (3–40 кг): зацепишь более тяжёлую — шнур лопнет почти мгновенно. Чем лучше удочка и приманка, тем выше шанс зацепить крупную. Новый максимум по виду отмечен бейджем «🏆 РЕКОРД!» на карточке улова.
|
||||
6. На монеты — новые удилища и приманки в магазине 🛒.
|
||||
|
||||
## Возможности
|
||||
|
||||
- 8 видов рыб: от карася до Золотой рыбки (5 уровней редкости)
|
||||
- Магазин: 4 удилища (прочность шнура, скорость подсечки) и 4 приманки (частота клёва, дальность, шанс редких)
|
||||
- Дневник рыбака — коллекция и статистика
|
||||
- Прогресс и коллекция сохраняются в `localStorage`
|
||||
- 8 видов рыб с собственным весом (0.05–40 кг) и ценой за кг (5 уровней редкости)
|
||||
- Удилища: предел веса, прочность шнура, скорость подсечки, бонус на крупную рыбу
|
||||
- Приманки: частота клёва, дальность броска, шанс редких и крупных
|
||||
- Дневник рыбака — коллекция, максимальный улов по каждому виду, статистика
|
||||
- Прогресс и коллекция сохраняются в `localStorage` (старые сейвы мигрируются автоматически)
|
||||
- Звуки на WebAudio, вибрация на телефоне
|
||||
- Вся графика нарисована на Canvas — внешних ресурсов нет
|
||||
|
||||
|
||||
@@ -76,6 +76,9 @@ button{font-family:inherit;cursor:pointer}
|
||||
#card .rarity{font-weight:800;font-size:13px;letter-spacing:1.5px;text-transform:uppercase}
|
||||
#card .cname{font-size:24px;font-weight:800;margin:2px 0 6px}
|
||||
#card .cval{font-size:20px;font-weight:800;color:#ffe9a8;margin-bottom:6px}
|
||||
#card .csub{font-size:13px;color:#9fc3e2;margin-bottom:4px}
|
||||
.record{display:inline-block;background:linear-gradient(#ffd76e,#f5a623);color:#04263d;font-weight:800;font-size:14px;letter-spacing:1px;padding:4px 14px;border-radius:999px;animation:recpulse .8s ease-in-out infinite}
|
||||
@keyframes recpulse{0%,100%{transform:scale(1)}50%{transform:scale(1.09)}}
|
||||
#card{animation:pop .3s ease}
|
||||
@keyframes pop{from{transform:scale(.75);opacity:0}to{transform:scale(1);opacity:1}}
|
||||
</style>
|
||||
@@ -103,6 +106,9 @@ button{font-family:inherit;cursor:pointer}
|
||||
<p style="margin-top:8px">Видишь <b style="color:#ff6b5e">!</b> — быстро жми, чтобы зацепить.<br>
|
||||
При вываживании: тяни, когда рыба отдыхает,<br>
|
||||
отпусти, когда она <b>тянет</b> — иначе лопнет!</p>
|
||||
<p style="margin-top:8px">У каждой рыбы свой <b>вес</b>, продажа — по цене за кг.<br>
|
||||
У удочек есть <b>предел веса</b>: перевесишь — шнур лопнет! <br>
|
||||
Лучшие снасти = больше шансов на трофей 🏆</p>
|
||||
<button id="btnPlay" class="big">Играть</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -124,9 +130,11 @@ button{font-family:inherit;cursor:pointer}
|
||||
<div id="card" class="overlay hidden">
|
||||
<div class="panel">
|
||||
<div class="rarity" id="cardRarity">Обычная</div>
|
||||
<div><span class="record hidden" id="cardRecord">🏆 РЕКОРД!</span></div>
|
||||
<span class="bigemoji" id="cardEmoji">🐟</span>
|
||||
<div class="cname" id="cardName">Карась</div>
|
||||
<div class="cval" id="cardVal">+10 🪙</div>
|
||||
<div class="csub" id="cardSub">0.0 кг · 0 🪙/кг</div>
|
||||
<div class="cval" id="cardVal">+0 🪙</div>
|
||||
<button id="btnCardOk" class="big">Отлично!</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -137,29 +145,29 @@ button{font-family:inherit;cursor:pointer}
|
||||
ДАННЫЕ
|
||||
========================================================= */
|
||||
const SPECIES=[
|
||||
{id:'carass', name:'Карась', emoji:'🐟', rarity:0, weight:100, minDist:0.00, strength:0.25, value:10, hue:45},
|
||||
{id:'perch', name:'Окунь', emoji:'🐠', rarity:0, weight:55, minDist:0.05, strength:0.30, value:15, hue:120},
|
||||
{id:'bream', name:'Лещ', emoji:'🐠', rarity:1, weight:26, minDist:0.30, strength:0.40, value:25, hue:210},
|
||||
{id:'carp', name:'Карп', emoji:'🐡', rarity:1, weight:22, minDist:0.35, strength:0.45, value:30, hue:30},
|
||||
{id:'pike', name:'Щука', emoji:'🦈', rarity:2, weight:12, minDist:0.50, strength:0.60, value:60, hue:100},
|
||||
{id:'catfish',name:'Сом', emoji:'🐡', rarity:2, weight:7, minDist:0.65, strength:0.75, value:90, hue:260},
|
||||
{id:'sturge', name:'Осётр', emoji:'🐋', rarity:3, weight:3.5, minDist:0.80, strength:0.85, value:200, hue:200},
|
||||
{id:'gold', name:'Золотая рыбка', emoji:'🎏', rarity:4, weight:1.2, minDist:0.90, strength:0.90, value:500, hue:45},
|
||||
{id:'carass', name:'Карась', emoji:'🐟', rarity:0, weight:100, minDist:0.00, strength:0.25, wMin:0.1, wMax:0.8, price:20, hue:45},
|
||||
{id:'perch', name:'Окунь', emoji:'🐠', rarity:0, weight:55, minDist:0.05, strength:0.30, wMin:0.1, wMax:1.5, price:22, hue:120},
|
||||
{id:'bream', name:'Лещ', emoji:'🐠', rarity:1, weight:26, minDist:0.30, strength:0.40, wMin:0.3, wMax:3.0, price:25, hue:210},
|
||||
{id:'carp', name:'Карп', emoji:'🐡', rarity:1, weight:22, minDist:0.35, strength:0.45, wMin:0.5, wMax:8.0, price:28, hue:30},
|
||||
{id:'pike', name:'Щука', emoji:'🦈', rarity:2, weight:12, minDist:0.50, strength:0.60, wMin:1.0, wMax:10.0, price:35, hue:100},
|
||||
{id:'catfish',name:'Сом', emoji:'🐡', rarity:2, weight:7, minDist:0.65, strength:0.75, wMin:2.0, wMax:20.0, price:45, hue:260},
|
||||
{id:'sturge', name:'Осётр', emoji:'🐋', rarity:3, weight:3.5, minDist:0.80, strength:0.85, wMin:5.0, wMax:40.0, price:80, hue:200},
|
||||
{id:'gold', name:'Золотая рыбка', emoji:'🎏', rarity:4, weight:1.2, minDist:0.90, strength:0.90, wMin:0.05, wMax:1.0, price:1500, hue:45},
|
||||
];
|
||||
const RARITY_NAMES=['Обычная','Необычная','Редкая','Эпическая','Легендарная'];
|
||||
const RARITY_COLORS=['#9fc3e2','#2ecc71','#4da3ff','#c56cf0','#ffd75e'];
|
||||
|
||||
const RODS=[
|
||||
{id:0, name:'Дедова удочка', cost:0, line:100, reel:1.0, desc:'Нагрузка 100 · Подсечка ×1.0'},
|
||||
{id:1, name:'Стеклопластик', cost:250, line:140, reel:1.3, desc:'Нагрузка 140 · Подсечка ×1.3'},
|
||||
{id:2, name:'Карбоновая', cost:700, line:190, reel:1.7, desc:'Нагрузка 190 · Подсечка ×1.7'},
|
||||
{id:3, name:'Титан', cost:1600, line:260, reel:2.3, desc:'Нагрузка 260 · Подсечка ×2.3'},
|
||||
{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'},
|
||||
];
|
||||
const LURES=[
|
||||
{id:0, name:'Простой крючок', cost:0, cast:1.00, bite:1.0, rare:0.0, desc:'Надёжно, но рыба не в восторге'},
|
||||
{id:1, name:'Червь', cost:150, cast:1.00, bite:1.3, rare:0.2, desc:'Клёв на 30% чаще'},
|
||||
{id:2, name:'Воблер', cost:450, cast:1.15, bite:1.2, rare:0.8, desc:'Дальше бросок, приманивает редких'},
|
||||
{id:3, name:'Золотая блёстка',cost:1200, cast:1.20, bite:1.4, rare:2.0, desc:'Лучшее, что есть. Легенды плывут к ней'},
|
||||
{id:0, name:'Простой крючок', cost:0, cast:1.00, bite:1.0, rare:0.0, size:0.00, desc:'Надёжно, но рыба не в восторге'},
|
||||
{id:1, name:'Червь', cost:150, cast:1.00, bite:1.3, rare:0.2, size:0.12, desc:'Клёв на 30% чаще, шанс крупной выше'},
|
||||
{id:2, name:'Воблер', cost:450, cast:1.15, bite:1.2, rare:0.8, size:0.28, desc:'Дальше бросок, приманивает крупных'},
|
||||
{id:3, name:'Золотая блёстка',cost:1200, cast:1.20, bite:1.4, rare:2.0, size:0.50, desc:'Лучшее, что есть. К ней плывут легенды'},
|
||||
];
|
||||
|
||||
/* =========================================================
|
||||
@@ -167,7 +175,13 @@ const LURES=[
|
||||
========================================================= */
|
||||
const SAVE_KEY='fishing-save-v1';
|
||||
let save={coins:0,rod:0,lure:0,rods:[0],lures:[0],caught:{},muted:false,casts:0,earned:0};
|
||||
function loadSave(){ try{ const s=localStorage.getItem(SAVE_KEY); if(s) Object.assign(save,JSON.parse(s)); }catch(e){} }
|
||||
function loadSave(){
|
||||
try{ const s=localStorage.getItem(SAVE_KEY); if(s) Object.assign(save,JSON.parse(s)); }catch(e){}
|
||||
// миграция старых сейвов: caught[id] был числом → теперь {n, best}
|
||||
for(const k in save.caught){
|
||||
if(typeof save.caught[k]==='number') save.caught[k]={n:save.caught[k],best:0};
|
||||
}
|
||||
}
|
||||
function persist(){ try{ localStorage.setItem(SAVE_KEY,JSON.stringify(save)); }catch(e){} }
|
||||
|
||||
/* =========================================================
|
||||
@@ -316,7 +330,12 @@ function pickSpecies(dist){
|
||||
return SPECIES[0];
|
||||
}
|
||||
function biteWindow(sp){ return Math.max(0.35, 0.8 - sp.rarity*0.08 + save.rod*0.04); }
|
||||
function distTo(sp){ return 55 + sp.strength*75; }
|
||||
/* Ролл веса рыбы: сдвиг в крупную сторону зависит от удочки и приманки */
|
||||
function rollWeight(sp){
|
||||
const B=Math.min(0.95, rod().size + lure().size);
|
||||
const s=Math.pow(Math.random(), 2/(1+2*B)); // B=0 → смещено к мелкой, B↑ → ближе к равномерной
|
||||
return sp.wMin + s*(sp.wMax-sp.wMin);
|
||||
}
|
||||
|
||||
function startBite(){
|
||||
const dist=Math.min(1,(castTo.x-(boatX+30))/Math.max(1,W-boatX-70));
|
||||
@@ -335,8 +354,11 @@ function hook(){
|
||||
biteFish=null;
|
||||
sfx('hook'); vib(30);
|
||||
state='REEL';
|
||||
const d0=distTo(sp);
|
||||
reel={d:d0, d0:d0, tension:0, holding:false, phase:'calm', phaseT:rand(0.8,1.5), sp:sp, tick:0};
|
||||
const w=rollWeight(sp);
|
||||
const ov=w/rod().maxW; // 1.0 = рыба ровно на пределе удочки
|
||||
const aggr=Math.min(1, 0.35*sp.strength + 0.75*Math.min(1.25,ov)/1.25); // «злость» рыбы
|
||||
const d0=40+3.5*w; // чем крупнее, тем дальше от лодки
|
||||
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};
|
||||
}
|
||||
function doCast(){
|
||||
const p=Math.max(0.12,power);
|
||||
@@ -352,31 +374,33 @@ function recall(){
|
||||
}
|
||||
}
|
||||
function updateReel(dt){
|
||||
const r=rod(), sp=reel.sp, st=sp.strength;
|
||||
const r=rod(), sp=reel.sp, ag=reel.aggr, ov=reel.ov;
|
||||
reel.phaseT-=dt;
|
||||
if(reel.phaseT<=0){
|
||||
if(reel.phase==='calm'){
|
||||
reel.phase='pull'; reel.phaseT=0.35+st*rand(0.3,0.9);
|
||||
reel.phase='pull'; reel.phaseT=0.3+ag*rand(0.4,0.9);
|
||||
sfx('pull'); vib(40);
|
||||
}else{
|
||||
reel.phase='calm'; reel.phaseT=rand(0.7,1.6); vib(15);
|
||||
reel.phase='calm'; reel.phaseT=Math.max(0.35,rand(0.7,1.7)-0.3*ag); vib(15);
|
||||
}
|
||||
}
|
||||
const pulling=reel.phase==='pull';
|
||||
if(reel.holding){
|
||||
reel.d-=30*r.reel*dt;
|
||||
reel.tension+=(pulling?22+st*55:9)*dt;
|
||||
// перегруз (ov>1) — нагрузка растёт кратно: шнур лопнет очень быстро
|
||||
reel.tension+=(pulling?(14+30*ag)*(0.6+0.9*ov):9)*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+=7*st*dt;
|
||||
if(pulling) reel.d+=(3+4*ag)*(0.5+0.4*ov)*dt; // крупная/тяжело несёт шнур
|
||||
}
|
||||
reel.tension=Math.max(0,reel.tension);
|
||||
if(reel.tension>=r.line){
|
||||
state='SNAP'; snapT=1.0; shake=0.5;
|
||||
sfx('snap'); vib(200); toast('💥 Линия лопнула!');
|
||||
sfx('snap'); vib(200);
|
||||
toast(ov>1.15?'💥 Рыба крупнее предела удочки!':'💥 Линия лопнула!');
|
||||
}else if(reel.d<=0){
|
||||
catchFish(sp);
|
||||
}else if(reel.d>=reel.d0*1.4){
|
||||
@@ -385,9 +409,12 @@ function updateReel(dt){
|
||||
}
|
||||
}
|
||||
function catchFish(sp){
|
||||
const val=Math.round(sp.value*(0.9+Math.random()*0.3));
|
||||
const w=reel.w;
|
||||
const val=Math.max(1,Math.round(w*sp.price*(0.95+Math.random()*0.1)));
|
||||
save.coins+=val; save.earned+=val;
|
||||
save.caught[sp.id]=(save.caught[sp.id]||0)+1;
|
||||
const prev=save.caught[sp.id]||{n:0,best:0};
|
||||
const isRecord=w>prev.best;
|
||||
save.caught[sp.id]={n:prev.n+1, best:Math.max(prev.best,w)};
|
||||
persist(); updateHUD();
|
||||
sfx('catch'); vib([40,60,40]);
|
||||
splash(castTo.x,waterY,26,140);
|
||||
@@ -396,7 +423,7 @@ function catchFish(sp){
|
||||
life:rand(0.5,0.9),max:0.9,size:rand(2,4),color:'255,215,94'});
|
||||
}
|
||||
reel=null; state='CARD';
|
||||
showCard(sp,val);
|
||||
showCard(sp,w,val,isRecord);
|
||||
}
|
||||
|
||||
function updateGame(dt){
|
||||
@@ -652,7 +679,7 @@ function drawHookedFish(){
|
||||
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;
|
||||
drawFishShape(x,y,10+sp.rarity*2.5,dir,sp.hue,0.75);
|
||||
drawFishShape(x,y,8+(reel.w/sp.wMax)*10+sp.rarity,dir,sp.hue,0.75); // силуэт крупнее у тяжёлых
|
||||
}
|
||||
function drawParticles(){
|
||||
for(const p of particles){
|
||||
@@ -814,11 +841,11 @@ function renderShop(){
|
||||
}else{
|
||||
let h='<div class="statsline">Бросков: '+save.casts+' · Заработано: '+save.earned+' 🪙</div>';
|
||||
for(const s of SPECIES){
|
||||
const c=save.caught[s.id]||0;
|
||||
if(c>0){
|
||||
const c=save.caught[s.id]||{n:0,best:0};
|
||||
if(c.n>0){
|
||||
h+='<div class="jrow"><span class="je">'+s.emoji+'</span><span class="jn"><b>'+s.name+'</b><br>'+
|
||||
'<span style="font-size:11px;color:'+RARITY_COLORS[s.rarity]+'">'+RARITY_NAMES[s.rarity]+' · '+s.value+' 🪙</span></span>'+
|
||||
'<span class="jc">×'+c+'</span></div>';
|
||||
'<span style="font-size:11px;color:'+RARITY_COLORS[s.rarity]+'">'+RARITY_NAMES[s.rarity]+' · '+s.price+' 🪙/кг</span></span>'+
|
||||
'<span class="jc">×'+c.n+'<br>макс '+c.best.toFixed(1)+' кг</span></div>';
|
||||
}else{
|
||||
h+='<div class="jrow unknown"><span class="je">❓</span><span class="jn">???</span>'+
|
||||
'<span class="jc">не пойман</span></div>';
|
||||
@@ -845,12 +872,14 @@ function shopClick(e){
|
||||
persist(); updateHUD(); sfx('click'); renderShop();
|
||||
}
|
||||
}
|
||||
function showCard(sp,val){
|
||||
function showCard(sp,w,val,rec){
|
||||
$('cardRarity').textContent=RARITY_NAMES[sp.rarity];
|
||||
$('cardRarity').style.color=RARITY_COLORS[sp.rarity];
|
||||
$('cardEmoji').textContent=sp.emoji;
|
||||
$('cardName').textContent=sp.name;
|
||||
$('cardSub').textContent=w.toFixed(1)+' кг · '+sp.price+' 🪙/кг';
|
||||
$('cardVal').textContent='+'+val+' 🪙';
|
||||
$('cardRecord').classList.toggle('hidden',!rec);
|
||||
$('card').classList.remove('hidden');
|
||||
}
|
||||
|
||||
|
||||
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 80 KiB |
|
Before Width: | Height: | Size: 192 KiB After Width: | Height: | Size: 156 KiB |
|
Before Width: | Height: | Size: 179 KiB After Width: | Height: | Size: 208 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 245 KiB After Width: | Height: | Size: 234 KiB |
@@ -1,9 +1,6 @@
|
||||
'use strict';
|
||||
/* Автотест игры: прогон механик через CDP в headless Chromium */
|
||||
const puppeteer = require('puppeteer-core');
|
||||
const path = require('path');
|
||||
const GAME_URL = 'file://' + path.resolve(__dirname, '..', 'index.html');
|
||||
const CHROMIUM = process.env.CHROMIUM || '/usr/bin/chromium';
|
||||
|
||||
const results = [];
|
||||
function ok(name, cond, extra = '') {
|
||||
@@ -25,7 +22,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
(async () => {
|
||||
const browser = await puppeteer.launch({
|
||||
executablePath: CHROMIUM,
|
||||
executablePath: '/usr/bin/chromium',
|
||||
headless: true,
|
||||
args: ['--no-sandbox', '--disable-gpu', '--hide-scrollbars', '--mute-audio'],
|
||||
defaultViewport: { width: 390, height: 844, deviceScaleFactor: 2 },
|
||||
@@ -36,7 +33,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
page.on('console', m => { if (m.type() === 'error') consoleErrors.push(m.text()); });
|
||||
page.on('pageerror', e => pageErrors.push(String(e)));
|
||||
|
||||
await page.goto(GAME_URL, { waitUntil: 'load' });
|
||||
await page.goto('file:///fishing/index.html', { waitUntil: 'load' });
|
||||
await sleep(600);
|
||||
|
||||
// --- 1. Меню ---
|
||||
@@ -76,14 +73,53 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
const cardInfo = await page.evaluate(() => ({
|
||||
visible: !document.getElementById('card').classList.contains('hidden'),
|
||||
name: document.getElementById('cardName').textContent,
|
||||
sub: document.getElementById('cardSub').textContent,
|
||||
val: document.getElementById('cardVal').textContent,
|
||||
rec: !document.getElementById('cardRecord').classList.contains('hidden'),
|
||||
coins: save.coins,
|
||||
}));
|
||||
ok('карточка улова показана', cardInfo.visible, JSON.stringify(cardInfo));
|
||||
const wMatch = cardInfo.sub.match(/([\d.]+) кг/);
|
||||
ok('на карточке есть вес рыбы', !!wMatch && cardInfo.sub.includes('🪙/кг'));
|
||||
ok('первый улов вида = рекорд', cardInfo.rec);
|
||||
await page.screenshot({ path: 'shot-card.png' });
|
||||
await page.click('#btnCardOk');
|
||||
ok('после карты: state=IDLE', (await page.evaluate(() => state)) === 'IDLE');
|
||||
|
||||
// --- 4b. Перегруз: тяжёлая рыба на слабой удочке рвёт шнур быстро ---
|
||||
await page.mouse.down(); await sleep(450); 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');
|
||||
await page.evaluate(() => {
|
||||
reel.w = 12; reel.ov = 12 / rod().maxW; reel.aggr = 1;
|
||||
reel.d = reel.d0 = 150;
|
||||
});
|
||||
await page.mouse.down();
|
||||
await waitState(page, 'SNAP', 4000);
|
||||
await page.mouse.up();
|
||||
await waitState(page, 'IDLE', 3000);
|
||||
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 waitState(page, 'BITE', 4000);
|
||||
await page.mouse.down(); await page.mouse.up();
|
||||
await waitState(page, 'REEL');
|
||||
await page.evaluate(() => { reel.d = 0.4; reel.phase = 'calm'; reel.phaseT = 5; });
|
||||
await page.mouse.down();
|
||||
await waitState(page, 'CARD', 5000);
|
||||
await page.mouse.up();
|
||||
ok('улов ниже рекорда: бейдж «РЕКОРД!» скрыт', await page.evaluate(() =>
|
||||
document.getElementById('cardRecord').classList.contains('hidden')));
|
||||
await page.click('#btnCardOk');
|
||||
await sleep(100);
|
||||
|
||||
// --- 5. Бросок 2: обрыв линии ---
|
||||
await page.mouse.down(); await sleep(450); await page.mouse.up();
|
||||
await waitState(page, 'WAIT');
|
||||
@@ -119,6 +155,8 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
ok('магазин открыт', await page.$eval('#shop', el => !el.classList.contains('hidden')));
|
||||
await page.screenshot({ path: 'shot-shop.png' });
|
||||
|
||||
const rodDesc = await page.$eval('#shopBody .item .itemdesc', el => el.textContent);
|
||||
ok('у удилища указан предел веса', rodDesc.includes('кг'));
|
||||
await page.click('#shopBody [data-act="buy"][data-type="rod"]');
|
||||
const rodAfter = await page.evaluate(() => ({ rod: save.rod, coins: save.coins, owned: save.rods.length }));
|
||||
ok('куплено удилище', rodAfter.rod === 1 && rodAfter.coins === 9750 && rodAfter.owned === 2, JSON.stringify(rodAfter));
|
||||
@@ -130,8 +168,8 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
await page.click('#tabJournal');
|
||||
await sleep(100);
|
||||
const jtxt = await page.$eval('#shopBody', el => el.textContent);
|
||||
ok('дневник рендерится', jtxt.includes('Бросков') && jtxt.includes('🪙'));
|
||||
ok('дневник показывает пойманную рыбу', jtxt.includes('Карась') || jtxt.includes('Окунь') || jtxt.includes('Лещ') || jtxt.includes('Карп') || jtxt.includes('Щука') || jtxt.includes('Сом') || jtxt.includes('Осётр') || jtxt.includes('Золотая рыбка'));
|
||||
ok('дневник рендерится', jtxt.includes('Бросков') && jtxt.includes('🪙/кг'));
|
||||
ok('дневник показывает пойманную рыбу с макс. весом', (jtxt.includes('Карась') || jtxt.includes('Окунь') || jtxt.includes('Лещ') || jtxt.includes('Карп') || jtxt.includes('Щука') || jtxt.includes('Сом') || jtxt.includes('Осётр') || jtxt.includes('Золотая рыбка')) && jtxt.includes('макс '));
|
||||
await page.screenshot({ path: 'shot-journal.png' });
|
||||
await page.click('#btnCloseShop');
|
||||
ok('магазин закрыт', await page.$eval('#shop', el => el.classList.contains('hidden')));
|
||||
@@ -140,7 +178,7 @@ const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
await page.reload({ waitUntil: 'load' });
|
||||
await sleep(500);
|
||||
const saved = await page.evaluate(() => ({ coins: save.coins, rod: save.rod, lure: save.lure, casts: save.casts }));
|
||||
ok('сохранение после reload', saved.coins === 9600 && saved.rod === 1 && saved.lure === 1 && saved.casts === 3, JSON.stringify(saved));
|
||||
ok('сохранение после reload', saved.coins === 9600 && saved.rod === 1 && saved.lure === 1 && saved.casts === 5, JSON.stringify(saved));
|
||||
|
||||
// --- 9. Ошибки консоли ---
|
||||
ok('нет ошибок JS (pageerror)', pageErrors.length === 0, pageErrors.join(' | '));
|
||||
|
||||