- один самодостаточный index.html (мобильное управление, магазин, дневник, сохранение в localStorage) - автотест геймплея в headless Chromium (tests/test.js, 21 проверка) - скриншоты состояния игры (screenshots/)
891 lines
35 KiB
HTML
891 lines
35 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
|
||
<meta name="theme-color" content="#0d3b66">
|
||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||
<title>Рыбалка</title>
|
||
<style>
|
||
*{margin:0;padding:0;box-sizing:border-box;-webkit-tap-highlight-color:transparent;user-select:none;-webkit-user-select:none}
|
||
html,body{width:100%;height:100%;overflow:hidden;background:#0d3b66;font-family:system-ui,-apple-system,'Segoe UI',Roboto,sans-serif}
|
||
#game{position:fixed;inset:0;touch-action:none;display:block}
|
||
button{font-family:inherit;cursor:pointer}
|
||
.hidden{display:none!important}
|
||
|
||
/* ---------- HUD ---------- */
|
||
#hud{position:fixed;top:0;left:0;right:0;display:flex;justify-content:space-between;align-items:center;
|
||
padding:calc(8px + env(safe-area-inset-top)) 10px 8px;z-index:10;pointer-events:none}
|
||
#coins{background:rgba(6,25,45,.55);padding:8px 14px;border-radius:999px;color:#ffe9a8;font-weight:700;
|
||
font-size:17px;border:1px solid rgba(255,255,255,.15)}
|
||
.hudbtns{display:flex;gap:8px;pointer-events:auto}
|
||
.hudbtns button{width:46px;height:46px;font-size:21px;border-radius:14px;
|
||
border:1px solid rgba(255,255,255,.15);background:rgba(6,25,45,.55);color:#fff}
|
||
.hudbtns button:active{background:rgba(30,60,95,.8)}
|
||
|
||
#btnRecall{position:fixed;right:12px;bottom:calc(16px + env(safe-area-inset-bottom));width:54px;height:54px;
|
||
border-radius:50%;font-size:24px;background:rgba(6,25,45,.65);color:#fff;
|
||
border:1px solid rgba(255,255,255,.2);z-index:10}
|
||
#btnRecall:active{background:rgba(30,60,95,.85)}
|
||
|
||
#toast{position:fixed;left:50%;bottom:calc(92px + env(safe-area-inset-bottom));transform:translateX(-50%);
|
||
background:rgba(6,25,45,.9);color:#fff;padding:10px 18px;border-radius:999px;font-size:15px;font-weight:600;
|
||
z-index:30;opacity:0;transition:opacity .25s;pointer-events:none;white-space:nowrap;
|
||
border:1px solid rgba(255,255,255,.15)}
|
||
|
||
/* ---------- Overlays ---------- */
|
||
.overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
|
||
background:rgba(6,20,38,.6);backdrop-filter:blur(3px);-webkit-backdrop-filter:blur(3px);z-index:20;padding:16px}
|
||
.panel{position:relative;background:linear-gradient(#14406b,#0d2f4e);border:2px solid #2e6da3;border-radius:20px;
|
||
padding:22px;width:min(92vw,370px);max-height:86vh;overflow-y:auto;color:#eaf4ff;text-align:center;
|
||
box-shadow:0 10px 40px rgba(0,0,0,.5)}
|
||
.title{font-size:34px;font-weight:800;letter-spacing:1px;margin-bottom:10px}
|
||
.panel p{font-size:14.5px;line-height:1.5;color:#bcd6ee;margin-bottom:4px}
|
||
.big{width:100%;margin-top:16px;padding:15px;font-size:19px;font-weight:800;color:#04263d;
|
||
background:linear-gradient(#ffd76e,#f5a623);border:none;border-radius:14px;box-shadow:0 4px 0 #b97708}
|
||
.big:active{transform:translateY(2px);box-shadow:0 2px 0 #b97708}
|
||
.panelhead{font-size:22px;font-weight:800;margin-bottom:4px}
|
||
#btnCloseShop{position:absolute;top:12px;right:12px;width:36px;height:36px;border-radius:50%;border:none;
|
||
background:rgba(255,255,255,.12);color:#fff;font-size:17px}
|
||
|
||
.tabs{display:flex;gap:8px;margin:10px 0 6px}
|
||
.tabs button{flex:1;padding:11px;border-radius:12px;border:none;background:rgba(255,255,255,.1);
|
||
color:#cfe4f7;font-weight:700;font-size:15px}
|
||
.tabs button.active{background:#f5a623;color:#04263d}
|
||
.section-title{font-size:12px;letter-spacing:1.5px;color:#8fb8dd;text-transform:uppercase;margin:14px 0 8px;text-align:left}
|
||
.item{display:flex;align-items:center;justify-content:space-between;gap:10px;background:rgba(255,255,255,.06);
|
||
border:1px solid rgba(255,255,255,.08);border-radius:12px;padding:10px 12px;margin-bottom:8px;text-align:left}
|
||
.itemname{font-weight:700;font-size:15px}
|
||
.itemdesc{font-size:12px;color:#9fc3e2;margin-top:2px;line-height:1.35}
|
||
.buybtn{min-width:96px;padding:10px 10px;border-radius:10px;border:none;font-weight:700;font-size:13px;
|
||
background:#2e6da3;color:#fff;white-space:nowrap;flex-shrink:0}
|
||
.buybtn:active{transform:scale(.96)}
|
||
.buybtn.owned{background:rgba(255,255,255,.14)}
|
||
.buybtn.current{background:#2ecc71;color:#04263d}
|
||
.buybtn:disabled{opacity:.45}
|
||
.statsline{font-size:13px;color:#9fc3e2;margin-bottom:6px}
|
||
.jrow{display:flex;align-items:center;gap:10px;padding:9px 10px;background:rgba(255,255,255,.05);
|
||
border-radius:10px;margin-bottom:6px;font-size:14px;text-align:left}
|
||
.jrow .je{font-size:22px;width:30px;text-align:center}
|
||
.jrow .jn{flex:1}
|
||
.jrow .jc{color:#9fc3e2;font-weight:600;font-size:13px}
|
||
.jrow.unknown .je,.jrow.unknown .jn{color:#5d7f9e}
|
||
|
||
#card .bigemoji{font-size:74px;margin:4px 0 2px;display:block;animation:bob 1.2s ease-in-out infinite}
|
||
@keyframes bob{0%,100%{transform:translateY(0)}50%{transform:translateY(-9px)}}
|
||
#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{animation:pop .3s ease}
|
||
@keyframes pop{from{transform:scale(.75);opacity:0}to{transform:scale(1);opacity:1}}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<canvas id="game"></canvas>
|
||
|
||
<div id="hud">
|
||
<div id="coins">🪙 0</div>
|
||
<div class="hudbtns">
|
||
<button id="btnShop">🛒</button>
|
||
<button id="btnMute">🔊</button>
|
||
</div>
|
||
</div>
|
||
<button id="btnRecall" class="hidden">↩</button>
|
||
<div id="toast"></div>
|
||
|
||
<!-- Меню -->
|
||
<div id="menu" class="overlay">
|
||
<div class="panel">
|
||
<div class="title">🎣 РЫБАЛКА</div>
|
||
<p>👆 <b>Держи палец</b> — заряжай бросок,<br>
|
||
<b>отпусти</b> — кидай удочку.<br>
|
||
Дальше бросок = реже и жирнее рыба!</p>
|
||
<p style="margin-top:8px">Видишь <b style="color:#ff6b5e">!</b> — быстро жми, чтобы зацепить.<br>
|
||
При вываживании: тяни, когда рыба отдыхает,<br>
|
||
отпусти, когда она <b>тянет</b> — иначе лопнет!</p>
|
||
<button id="btnPlay" class="big">Играть</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Магазин -->
|
||
<div id="shop" class="overlay hidden">
|
||
<div class="panel">
|
||
<button id="btnCloseShop">×</button>
|
||
<div class="panelhead">🛒 Лавка рыбака</div>
|
||
<div class="tabs">
|
||
<button id="tabTackle" class="active">Снасти</button>
|
||
<button id="tabJournal">Дневник</button>
|
||
</div>
|
||
<div id="shopBody"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Карточка улова -->
|
||
<div id="card" class="overlay hidden">
|
||
<div class="panel">
|
||
<div class="rarity" id="cardRarity">Обычная</div>
|
||
<span class="bigemoji" id="cardEmoji">🐟</span>
|
||
<div class="cname" id="cardName">Карась</div>
|
||
<div class="cval" id="cardVal">+10 🪙</div>
|
||
<button id="btnCardOk" class="big">Отлично!</button>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
'use strict';
|
||
/* =========================================================
|
||
ДАННЫЕ
|
||
========================================================= */
|
||
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},
|
||
];
|
||
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'},
|
||
];
|
||
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:'Лучшее, что есть. Легенды плывут к ней'},
|
||
];
|
||
|
||
/* =========================================================
|
||
СОХРАНЕНИЕ
|
||
========================================================= */
|
||
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 persist(){ try{ localStorage.setItem(SAVE_KEY,JSON.stringify(save)); }catch(e){} }
|
||
|
||
/* =========================================================
|
||
ХОЛСТ И РАЗМЕРЫ
|
||
========================================================= */
|
||
const canvas=document.getElementById('game');
|
||
const ctx=canvas.getContext('2d');
|
||
let W=0,H=0,DPR=1,waterY=0,boatX=0;
|
||
let tip={x:0,y:0};
|
||
function resize(){
|
||
DPR=Math.min(window.devicePixelRatio||1,2);
|
||
W=window.innerWidth; H=window.innerHeight;
|
||
canvas.width=W*DPR; canvas.height=H*DPR;
|
||
canvas.style.width=W+'px'; canvas.style.height=H+'px';
|
||
ctx.setTransform(DPR,0,0,DPR,0,0);
|
||
waterY=Math.round(H*0.26);
|
||
boatX=Math.round(W*0.20);
|
||
initClouds(); initSparkles();
|
||
}
|
||
window.addEventListener('resize',resize);
|
||
|
||
/* =========================================================
|
||
ЗВУК
|
||
========================================================= */
|
||
let AC=null;
|
||
function ac(){
|
||
if(!AC){ try{ AC=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){ return null; } }
|
||
if(AC&&AC.state==='suspended'){ AC.resume(); }
|
||
return AC;
|
||
}
|
||
function beep(t0,f,dur,type,vol){
|
||
const c=ac(); if(!c)return;
|
||
const o=c.createOscillator(),g=c.createGain();
|
||
o.type=type||'sine'; o.frequency.setValueAtTime(f,t0);
|
||
g.gain.setValueAtTime(vol,t0); g.gain.exponentialRampToValueAtTime(0.001,t0+dur);
|
||
o.connect(g); g.connect(c.destination); o.start(t0); o.stop(t0+dur+0.02);
|
||
}
|
||
function noiseBurst(t0,dur,vol,freq){
|
||
const c=ac(); if(!c)return;
|
||
const n=Math.floor(c.sampleRate*dur);
|
||
const buf=c.createBuffer(1,n,c.sampleRate),d=buf.getChannelData(0);
|
||
for(let i=0;i<n;i++) d[i]=(Math.random()*2-1)*(1-i/n);
|
||
const src=c.createBufferSource(); src.buffer=buf;
|
||
const f=c.createBiquadFilter(); f.type='lowpass'; f.frequency.value=freq;
|
||
const g=c.createGain(); g.gain.value=vol;
|
||
src.connect(f); f.connect(g); g.connect(c.destination); src.start(t0);
|
||
}
|
||
function sfx(name){
|
||
if(save.muted) return;
|
||
const c=ac(); if(!c) return;
|
||
const t=c.currentTime;
|
||
switch(name){
|
||
case 'click': beep(t,500,0.06,'triangle',0.15); break;
|
||
case 'cast': noiseBurst(t,0.22,0.15,2500); break;
|
||
case 'splash': noiseBurst(t,0.3,0.4,700); break;
|
||
case 'bite': beep(t,900,0.07,'square',0.2); beep(t+0.1,1200,0.09,'square',0.2); break;
|
||
case 'hook': beep(t,600,0.05,'triangle',0.25); beep(t+0.06,900,0.08,'triangle',0.25); break;
|
||
case 'tick': beep(t,260,0.03,'triangle',0.07); break;
|
||
case 'pull': beep(t,140,0.18,'sawtooth',0.12); break;
|
||
case 'snap': noiseBurst(t,0.25,0.5,3000); beep(t,180,0.2,'sawtooth',0.2); break;
|
||
case 'escape': beep(t,500,0.12,'sine',0.2); beep(t+0.12,350,0.16,'sine',0.18); break;
|
||
case 'coin': beep(t,1200,0.08,'sine',0.2); beep(t+0.09,1600,0.12,'sine',0.2); break;
|
||
case 'catch': [523,659,784,1047].forEach((f,i)=>beep(t+i*0.11,f,0.14,'triangle',0.22)); break;
|
||
case 'no': beep(t,220,0.12,'square',0.15); break;
|
||
}
|
||
}
|
||
function vib(p){ try{ if(navigator.vibrate) navigator.vibrate(p); }catch(e){} }
|
||
|
||
/* =========================================================
|
||
СОСТОЯНИЕ ИГРЫ
|
||
========================================================= */
|
||
let state='MENU'; // MENU IDLE CHARGE CAST WAIT APPROACH BITE REEL SNAP CARD
|
||
let uiOpen=true;
|
||
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 biteTimer=0, biteT=0, biteFish=null, rippleTimer=0;
|
||
let reel=null;
|
||
let snapT=0, shake=0;
|
||
let ambient=[]; // фоновые рыбы
|
||
let clouds=[], sparkles=[];
|
||
let particles=[], ripples=[];
|
||
|
||
const rod=()=>RODS[save.rod];
|
||
const lure=()=>LURES[save.lure];
|
||
const rand=(a,b)=>a+Math.random()*(b-a);
|
||
|
||
function initFish(){
|
||
ambient=[];
|
||
for(let i=0;i<9;i++){
|
||
ambient.push({
|
||
x:Math.random()*W, depth:rand(0.12,0.92), size:rand(8,22),
|
||
speed:rand(10,32), dir:Math.random()<0.5?1:-1, hue:rand(140,260), ph:rand(0,6.28)
|
||
});
|
||
}
|
||
}
|
||
function initClouds(){
|
||
clouds=[];
|
||
for(let i=0;i<4;i++){
|
||
clouds.push({x:Math.random()*W,y:rand(14,Math.max(20,waterY-50)),s:rand(0.6,1.2),v:rand(4,9)});
|
||
}
|
||
}
|
||
function initSparkles(){
|
||
sparkles=[];
|
||
for(let i=0;i<24;i++){
|
||
sparkles.push({x:Math.random()*W,y:waterY+rand(10,H-waterY-10),ph:rand(0,6.28)});
|
||
}
|
||
}
|
||
|
||
/* =========================================================
|
||
УТИЛИТЫ
|
||
========================================================= */
|
||
const $=id=>document.getElementById(id);
|
||
let toastTO=null;
|
||
function toast(msg){
|
||
const t=$('toast'); t.textContent=msg; t.style.opacity=1;
|
||
clearTimeout(toastTO); toastTO=setTimeout(()=>t.style.opacity=0,1600);
|
||
}
|
||
function updateHUD(){ $('coins').textContent='🪙 '+save.coins; $('btnMute').textContent=save.muted?'🔇':'🔊'; }
|
||
function splash(x,y,n,spread){
|
||
for(let i=0;i<n;i++){
|
||
particles.push({
|
||
x:x,y:y, vx:rand(-1,1)*(spread||90), vy:rand(-160,-40),
|
||
g:320, life:rand(0.35,0.7), max:0.7, size:rand(1.5,3.5),
|
||
color:Math.random()<0.5?'255,255,255':'160,215,255'
|
||
});
|
||
}
|
||
}
|
||
function addRipple(x,y,max){ ripples.push({x:x,y:y,r:4,max:max||36,a:0.6}); }
|
||
|
||
/* =========================================================
|
||
ЛОГИКА
|
||
========================================================= */
|
||
function pickSpecies(dist){
|
||
const l=lure();
|
||
let total=0;
|
||
const ws=SPECIES.map(s=>{
|
||
let w=s.weight;
|
||
if(dist<s.minDist) w*=0.06;
|
||
w*=1+l.rare*s.rarity;
|
||
total+=w; return w;
|
||
});
|
||
let r=Math.random()*total;
|
||
for(let i=0;i<SPECIES.length;i++){ r-=ws[i]; if(r<=0) return SPECIES[i]; }
|
||
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 startBite(){
|
||
const dist=Math.min(1,(castTo.x-(boatX+30))/Math.max(1,W-boatX-70));
|
||
const sp=pickSpecies(dist);
|
||
const fromLeft=Math.random()<0.5;
|
||
const d=Math.max(0.25,sp.minDist)*0.8+0.1;
|
||
biteFish={
|
||
x:fromLeft?-30:W+30, y:waterY+d*(H-waterY-40),
|
||
sx:fromLeft?-30:W+30, sy:waterY+d*(H-waterY-40),
|
||
dir:fromLeft?1:-1, sp:sp, t:0, mode:'in'
|
||
};
|
||
state='APPROACH';
|
||
}
|
||
function hook(){
|
||
const sp=biteFish.sp;
|
||
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};
|
||
}
|
||
function doCast(){
|
||
const p=Math.max(0.12,power);
|
||
const dist=Math.min(0.97,(0.10+0.80*p)*lure().cast);
|
||
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');
|
||
save.casts++; persist();
|
||
}
|
||
function recall(){
|
||
if(state==='WAIT'||state==='APPROACH'||state==='BITE'){
|
||
state='IDLE'; biteFish=null; sfx('click');
|
||
}
|
||
}
|
||
function updateReel(dt){
|
||
const r=rod(), sp=reel.sp, st=sp.strength;
|
||
reel.phaseT-=dt;
|
||
if(reel.phaseT<=0){
|
||
if(reel.phase==='calm'){
|
||
reel.phase='pull'; reel.phaseT=0.35+st*rand(0.3,0.9);
|
||
sfx('pull'); vib(40);
|
||
}else{
|
||
reel.phase='calm'; reel.phaseT=rand(0.7,1.6); vib(15);
|
||
}
|
||
}
|
||
const pulling=reel.phase==='pull';
|
||
if(reel.holding){
|
||
reel.d-=30*r.reel*dt;
|
||
reel.tension+=(pulling?22+st*55: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;
|
||
}
|
||
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('💥 Линия лопнула!');
|
||
}else if(reel.d<=0){
|
||
catchFish(sp);
|
||
}else if(reel.d>=reel.d0*1.4){
|
||
state='WAIT'; biteTimer=rand(3,7)/lure().bite;
|
||
sfx('escape'); toast('Рыба сорвалась…');
|
||
}
|
||
}
|
||
function catchFish(sp){
|
||
const val=Math.round(sp.value*(0.9+Math.random()*0.3));
|
||
save.coins+=val; save.earned+=val;
|
||
save.caught[sp.id]=(save.caught[sp.id]||0)+1;
|
||
persist(); updateHUD();
|
||
sfx('catch'); vib([40,60,40]);
|
||
splash(castTo.x,waterY,26,140);
|
||
for(let i=0;i<10;i++){
|
||
particles.push({x:castTo.x,y:waterY,vx:rand(-60,60),vy:rand(-220,-80),g:300,
|
||
life:rand(0.5,0.9),max:0.9,size:rand(2,4),color:'255,215,94'});
|
||
}
|
||
reel=null; state='CARD';
|
||
showCard(sp,val);
|
||
}
|
||
|
||
function updateGame(dt){
|
||
// фоновые рыбы
|
||
for(const f of ambient){
|
||
f.x+=f.dir*f.speed*dt;
|
||
if(f.dir>0&&f.x>W+40){ f.x=-40; f.depth=rand(0.12,0.92); }
|
||
if(f.dir<0&&f.x<-40){ f.x=W+40; f.depth=rand(0.12,0.92); }
|
||
}
|
||
switch(state){
|
||
case 'CHARGE':
|
||
charge+=dt;
|
||
power=1-Math.abs(1-((charge/0.9)%2));
|
||
break;
|
||
case 'CAST':{
|
||
castT+=dt/0.65;
|
||
if(castT>=1){
|
||
castT=1;
|
||
bobber={x:castTo.x,y:waterY-2};
|
||
state='WAIT';
|
||
biteTimer=rand(2.5,7)/lure().bite;
|
||
splash(castTo.x,waterY,14,100);
|
||
addRipple(castTo.x,waterY,40);
|
||
sfx('splash'); vib(10);
|
||
}else{
|
||
const t=castT;
|
||
bobber.x=castFrom.x+(castTo.x-castFrom.x)*t;
|
||
bobber.y=castFrom.y+(castTo.y-castFrom.y)*t-150*Math.sin(Math.PI*t);
|
||
}
|
||
break;
|
||
}
|
||
case 'WAIT':{
|
||
bobber.y=waterY-3+Math.sin(time*2)*1.5;
|
||
rippleTimer-=dt;
|
||
if(rippleTimer<=0){ addRipple(bobber.x,waterY+3,22); rippleTimer=1.6; }
|
||
if(biteFish&&biteFish.mode==='out') updateBiteFishOut(dt);
|
||
biteTimer-=dt;
|
||
if(biteTimer<=0&&!biteFish) startBite();
|
||
break;
|
||
}
|
||
case 'APPROACH':
|
||
updateBiteFishIn(dt);
|
||
if(biteFish){
|
||
if(biteFish.t>=1){
|
||
state='BITE';
|
||
biteT=biteWindow(biteFish.sp);
|
||
sfx('bite'); vib(60);
|
||
addRipple(castTo.x,waterY+3,30);
|
||
}
|
||
}
|
||
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);
|
||
if(biteT<=0){
|
||
if(biteFish) biteFish.mode='out';
|
||
state='WAIT'; biteTimer=rand(3,8)/lure().bite;
|
||
sfx('escape'); toast('Ушла… жми быстрее!');
|
||
}
|
||
break;
|
||
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);
|
||
}
|
||
break;
|
||
case 'SNAP':
|
||
snapT-=dt;
|
||
if(snapT<=0) state='IDLE';
|
||
break;
|
||
}
|
||
syncDom();
|
||
}
|
||
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;
|
||
}
|
||
function updateBiteFishOut(dt){
|
||
biteFish.t+=dt/0.6;
|
||
const dir=biteFish.x<W/2?-1:1;
|
||
biteFish.x+=dir*380*dt;
|
||
biteFish.y+=40*dt;
|
||
if(biteFish.t>=1||biteFish.x<-50||biteFish.x>W+50) biteFish=null;
|
||
}
|
||
function updateDecor(dt){
|
||
for(const c of clouds){ c.x+=c.v*dt; if(c.x>W+80) c.x=-80; }
|
||
for(let i=particles.length-1;i>=0;i--){
|
||
const p=particles[i];
|
||
p.life-=dt; p.vy+=p.g*dt; p.x+=p.vx*dt; p.y+=p.vy*dt;
|
||
if(p.life<=0||p.y>H) particles.splice(i,1);
|
||
}
|
||
for(let i=ripples.length-1;i>=0;i--){
|
||
const r=ripples[i];
|
||
r.r+=45*dt; r.a-=0.9*dt;
|
||
if(r.a<=0||r.r>r.max) ripples.splice(i,1);
|
||
}
|
||
if(shake>0) shake-=dt;
|
||
}
|
||
function syncDom(){
|
||
const showRecall=(state==='WAIT'||state==='APPROACH'||state==='BITE');
|
||
$('btnRecall').classList.toggle('hidden',!showRecall);
|
||
}
|
||
|
||
/* =========================================================
|
||
ОТРИСОВКА
|
||
========================================================= */
|
||
function rr(x,y,w,h,r){
|
||
ctx.beginPath();
|
||
ctx.moveTo(x+r,y);
|
||
ctx.arcTo(x+w,y,x+w,y+h,r);
|
||
ctx.arcTo(x+w,y+h,x,y+h,r);
|
||
ctx.arcTo(x,y+h,x,y,r);
|
||
ctx.arcTo(x,y,x+w,y,r);
|
||
ctx.closePath();
|
||
}
|
||
function drawSky(){
|
||
const g=ctx.createLinearGradient(0,0,0,waterY);
|
||
g.addColorStop(0,'#7ec8e3'); g.addColorStop(1,'#cfeef7');
|
||
ctx.fillStyle=g; ctx.fillRect(0,0,W,waterY);
|
||
// солнце
|
||
ctx.fillStyle='rgba(255,230,120,0.35)';
|
||
ctx.beginPath(); ctx.arc(W-52,48,34,0,7); ctx.fill();
|
||
ctx.fillStyle='#ffe27a';
|
||
ctx.beginPath(); ctx.arc(W-52,48,20,0,7); ctx.fill();
|
||
// облака
|
||
ctx.fillStyle='rgba(255,255,255,0.9)';
|
||
for(const c of clouds){
|
||
ctx.save(); ctx.translate(c.x,c.y); ctx.scale(c.s,c.s);
|
||
ctx.beginPath();
|
||
ctx.arc(0,0,14,0,7); ctx.arc(16,-6,12,0,7); ctx.arc(32,0,15,0,7); ctx.arc(16,6,13,0,7);
|
||
ctx.fill(); ctx.restore();
|
||
}
|
||
}
|
||
function drawWater(){
|
||
const g=ctx.createLinearGradient(0,waterY,0,H);
|
||
g.addColorStop(0,'#2f9bd8'); g.addColorStop(1,'#0a2f52');
|
||
ctx.fillStyle=g; ctx.fillRect(0,waterY,W,H-waterY);
|
||
// блики
|
||
for(const s of sparkles){
|
||
const a=Math.max(0,Math.sin(time*1.5+s.ph))*0.25;
|
||
if(a>0.02){ ctx.fillStyle='rgba(255,255,255,'+a.toFixed(3)+')'; ctx.fillRect(s.x,s.y,3,1.5); }
|
||
}
|
||
// волна на поверхности
|
||
ctx.strokeStyle='rgba(255,255,255,0.55)'; ctx.lineWidth=2;
|
||
ctx.beginPath();
|
||
for(let x=0;x<=W;x+=8){
|
||
const y=waterY+Math.sin(x*0.05+time*2)*2.5;
|
||
if(x===0) ctx.moveTo(x,y); else ctx.lineTo(x,y);
|
||
}
|
||
ctx.stroke();
|
||
}
|
||
function drawFishShape(x,y,size,dir,hue,alpha){
|
||
ctx.save(); ctx.translate(x,y); ctx.scale(dir,1);
|
||
ctx.fillStyle='hsla('+hue+',55%,38%,'+alpha+')';
|
||
ctx.beginPath(); ctx.ellipse(0,0,size,size*0.45,0,0,7); ctx.fill();
|
||
ctx.beginPath();
|
||
ctx.moveTo(-size*0.8,0); ctx.lineTo(-size*1.45,-size*0.42); ctx.lineTo(-size*1.45,size*0.42);
|
||
ctx.closePath(); ctx.fill();
|
||
ctx.restore();
|
||
}
|
||
function drawAmbientFish(){
|
||
for(const f of ambient){
|
||
const y=waterY+20+f.depth*(H-waterY-50)+Math.sin(time*2+f.ph)*2;
|
||
drawFishShape(f.x,y,f.size,f.dir,f.hue,0.22+0.2*(1-f.depth));
|
||
}
|
||
}
|
||
function drawBoat(){
|
||
const bx=boatX, by=waterY+Math.sin(time*1.5)*1.5;
|
||
// лодка
|
||
ctx.fillStyle='#b0622d';
|
||
ctx.beginPath();
|
||
ctx.moveTo(bx-32,by); ctx.lineTo(bx+32,by); ctx.lineTo(bx+21,by+15); ctx.lineTo(bx-21,by+15);
|
||
ctx.closePath(); ctx.fill();
|
||
ctx.fillStyle='#8a4519'; ctx.fillRect(bx-32,by,64,4);
|
||
// рыбак
|
||
ctx.fillStyle='#34495e';
|
||
ctx.fillRect(bx-9,by-18,12,12);
|
||
ctx.fillStyle='#e8b88a';
|
||
ctx.beginPath(); ctx.arc(bx-3,by-24,6.5,0,7); ctx.fill();
|
||
ctx.fillStyle='#c0392b';
|
||
ctx.beginPath(); ctx.arc(bx-3,by-27,6.5,Math.PI,0); ctx.fill();
|
||
// удочка
|
||
const swing=state==='CHARGE'?-power*0.35:0;
|
||
ctx.strokeStyle='#5d4023'; ctx.lineWidth=3; ctx.lineCap='round';
|
||
ctx.beginPath(); ctx.moveTo(bx+2,by-14);
|
||
const tx=bx+34+swing*20, ty=by-38-swing*14;
|
||
ctx.lineTo(tx,ty); ctx.stroke();
|
||
tip={x:tx,y:ty};
|
||
// флажок
|
||
ctx.strokeStyle='#5d4023'; ctx.lineWidth=2;
|
||
ctx.beginPath(); ctx.moveTo(bx-24,by); ctx.lineTo(bx-24,by-22); ctx.stroke();
|
||
ctx.fillStyle='#f5a623';
|
||
ctx.beginPath(); ctx.moveTo(bx-24,by-22); ctx.lineTo(bx-12,by-18); ctx.lineTo(bx-24,by-14); ctx.closePath(); ctx.fill();
|
||
}
|
||
function drawLineTo(x,y,sag){
|
||
ctx.strokeStyle='rgba(230,240,255,0.8)'; ctx.lineWidth=1.2;
|
||
ctx.beginPath(); ctx.moveTo(tip.x,tip.y);
|
||
ctx.quadraticCurveTo((tip.x+x)/2,(tip.y+y)/2+sag,x,y);
|
||
ctx.stroke();
|
||
}
|
||
function drawBobber(){
|
||
const x=bobber.x,y=bobber.y;
|
||
ctx.fillStyle='#fff';
|
||
ctx.beginPath(); ctx.arc(x,y,5.5,0,7); ctx.fill();
|
||
ctx.fillStyle='#e74c3c';
|
||
ctx.beginPath(); ctx.arc(x,y,5.5,Math.PI,0); ctx.fill();
|
||
ctx.strokeStyle='rgba(255,255,255,0.9)'; ctx.lineWidth=1.5;
|
||
ctx.beginPath(); ctx.arc(x,y,5.5,0,7); ctx.stroke();
|
||
}
|
||
function drawLineAndBobber(){
|
||
if(state==='IDLE'||state==='CHARGE'||state==='MENU'||state==='CARD'){
|
||
// болтается грузило
|
||
const a=state==='CHARGE'?Math.sin(time*10)*power*4:Math.sin(time*1.2)*1.5;
|
||
ctx.strokeStyle='rgba(230,240,255,0.8)'; ctx.lineWidth=1.2;
|
||
ctx.beginPath(); ctx.moveTo(tip.x,tip.y); ctx.lineTo(tip.x+a,tip.y+22); ctx.stroke();
|
||
ctx.fillStyle='#e74c3c';
|
||
ctx.beginPath(); ctx.arc(tip.x+a,tip.y+25,3.5,0,7); ctx.fill();
|
||
return;
|
||
}
|
||
if(state==='SNAP'){
|
||
ctx.strokeStyle='rgba(230,240,255,0.8)'; ctx.lineWidth=1.2;
|
||
ctx.beginPath(); ctx.moveTo(tip.x,tip.y);
|
||
ctx.lineTo(tip.x+10+Math.sin(time*20)*6,tip.y+26); ctx.stroke();
|
||
ctx.globalAlpha=Math.min(1,snapT*2);
|
||
ctx.font='34px system-ui'; ctx.textAlign='center';
|
||
ctx.fillText('💥',castTo.x,waterY-20);
|
||
ctx.globalAlpha=1;
|
||
return;
|
||
}
|
||
if(state==='CAST'){ drawLineTo(bobber.x,bobber.y,14); drawBobber(); return; }
|
||
const sag=state==='REEL'?(reel.phase==='pull'?4:12):18+Math.sin(time*2)*3;
|
||
drawLineTo(bobber.x,bobber.y,sag);
|
||
drawBobber();
|
||
if(state==='BITE'){
|
||
const yy=bobber.y-24+Math.sin(time*12)*2;
|
||
ctx.font='800 28px system-ui'; ctx.textAlign='center';
|
||
ctx.lineWidth=4; ctx.strokeStyle='#fff';
|
||
ctx.strokeText('!',bobber.x,yy);
|
||
ctx.fillStyle='#ff3b30';
|
||
ctx.fillText('!',bobber.x,yy);
|
||
}
|
||
}
|
||
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;
|
||
drawFishShape(x,y,10+sp.rarity*2.5,dir,sp.hue,0.75);
|
||
}
|
||
function drawParticles(){
|
||
for(const p of particles){
|
||
ctx.fillStyle='rgba('+p.color+','+(Math.max(0,p.life/p.max)).toFixed(3)+')';
|
||
ctx.beginPath(); ctx.arc(p.x,p.y,p.size,0,7); ctx.fill();
|
||
}
|
||
}
|
||
function drawRipples(){
|
||
for(const r of ripples){
|
||
ctx.strokeStyle='rgba(255,255,255,'+Math.max(0,r.a).toFixed(3)+')';
|
||
ctx.lineWidth=1.5;
|
||
ctx.beginPath(); ctx.ellipse(r.x,r.y,r.r,r.r*0.3,0,0,7); ctx.stroke();
|
||
}
|
||
}
|
||
function drawMeters(){
|
||
if(state!=='REEL'||!reel) return;
|
||
const r=rod(), sp=reel.sp;
|
||
const pulling=reel.phase==='pull';
|
||
// --- нагрузка (вертикально справа) ---
|
||
const bw=14, bh=H*0.36, bx=W-30, by=H*0.46-bh/2;
|
||
ctx.fillStyle='rgba(0,0,0,0.35)'; rr(bx,by,bw,bh,7); ctx.fill();
|
||
const ratio=Math.min(1,reel.tension/r.line);
|
||
const col=ratio<0.5?'#2ecc71':ratio<0.8?'#f1c40f':'#e74c3c';
|
||
if(ratio>0.01){
|
||
ctx.fillStyle=col;
|
||
const fh=bh*ratio;
|
||
rr(bx+2,by+bh-2-fh,bw-4,Math.max(2,fh-2),5); ctx.fill();
|
||
}
|
||
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();
|
||
ctx.font='16px system-ui'; ctx.textAlign='center';
|
||
ctx.fillText(pulling?'⚡':'💪',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();
|
||
// rem = доля оставшейся дистанции: полоска «сгорает» к лодке, рыбка на её конце
|
||
const rem=Math.max(0,Math.min(1,reel.d/reel.d0));
|
||
if(rem>0.01){
|
||
ctx.fillStyle='#3fa7e8';
|
||
rr(dx+2,dy+2,Math.max(2,(dw-4)*rem),dh-4,5); ctx.fill();
|
||
}
|
||
// рыбка-маркер на конце полоски
|
||
const fx=dx+2+(dw-4)*rem;
|
||
ctx.font='20px system-ui'; ctx.textAlign='center';
|
||
ctx.fillText('🐟',fx,dy-6+Math.sin(time*10)*1.5);
|
||
ctx.font='18px system-ui';
|
||
ctx.fillText('⛵',dx+4,dy+dh+18);
|
||
// статус
|
||
ctx.font='700 15px system-ui'; ctx.textAlign='center';
|
||
if(pulling){
|
||
ctx.fillStyle='#ff6b5e';
|
||
ctx.fillText('⚡ РЫБА ТЯНЕТ — ОТПУСТИ! ⚡',W/2,dy-30);
|
||
}else{
|
||
ctx.fillStyle='#7ef0a8';
|
||
ctx.fillText('Держи палец — втягивай!',W/2,dy-30);
|
||
}
|
||
}
|
||
function drawPowerBar(){
|
||
if(state!=='CHARGE') return;
|
||
const w=W*0.7,h=16,x=(W-w)/2,y=H-64;
|
||
ctx.fillStyle='rgba(0,0,0,0.4)'; rr(x,y,w,h,8); ctx.fill();
|
||
const g=ctx.createLinearGradient(x,0,x+w,0);
|
||
g.addColorStop(0,'#2ecc71'); g.addColorStop(0.6,'#f1c40f'); g.addColorStop(1,'#e74c3c');
|
||
ctx.fillStyle=g;
|
||
if(power>0.01){ rr(x+2,y+2,Math.max(2,(w-4)*power),h-4,6); ctx.fill(); }
|
||
ctx.font='700 14px system-ui'; ctx.textAlign='center';
|
||
ctx.fillStyle='#fff';
|
||
ctx.fillText('Отпусти — бросок!',W/2,y-10);
|
||
}
|
||
function drawHint(){
|
||
if(state!=='IDLE'||save.casts>=5) return;
|
||
const a=0.55+Math.sin(time*3)*0.25;
|
||
ctx.globalAlpha=a;
|
||
ctx.font='700 17px system-ui'; ctx.textAlign='center';
|
||
ctx.fillStyle='#fff';
|
||
ctx.fillText('👆 Держи палец — заряжай бросок',W/2,H-70);
|
||
ctx.globalAlpha=1;
|
||
}
|
||
function draw(){
|
||
ctx.clearRect(0,0,W,H);
|
||
ctx.save();
|
||
if(shake>0){
|
||
ctx.translate(rand(-1,1)*shake*8,rand(-1,1)*shake*8);
|
||
}
|
||
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);
|
||
else if(biteFish&&biteFish.mode==='out') drawFishShape(biteFish.x,biteFish.y,12+biteFish.sp.rarity*2.5,biteFish.dir,biteFish.sp.hue,0.5);
|
||
drawBoat();
|
||
drawLineAndBobber();
|
||
drawHookedFish();
|
||
drawRipples();
|
||
drawParticles();
|
||
drawMeters();
|
||
drawPowerBar();
|
||
drawHint();
|
||
ctx.restore();
|
||
}
|
||
|
||
/* =========================================================
|
||
ЦИКЛ
|
||
========================================================= */
|
||
let last=performance.now();
|
||
function frame(now){
|
||
const dt=Math.min(0.05,(now-last)/1000); last=now;
|
||
time+=dt;
|
||
updateDecor(dt);
|
||
if(!uiOpen) updateGame(dt);
|
||
draw();
|
||
requestAnimationFrame(frame);
|
||
}
|
||
|
||
/* =========================================================
|
||
ВВОД
|
||
========================================================= */
|
||
function onDown(){
|
||
ac();
|
||
if(uiOpen) return;
|
||
if(state==='IDLE'){ state='CHARGE'; charge=0; power=0; }
|
||
else if(state==='APPROACH'||state==='BITE'){ hook(); }
|
||
else if(state==='REEL'&&reel){ reel.holding=true; }
|
||
}
|
||
function onUp(){
|
||
if(state==='CHARGE') doCast();
|
||
else if(state==='REEL'&&reel) reel.holding=false;
|
||
}
|
||
canvas.addEventListener('pointerdown',e=>{ e.preventDefault(); onDown(); });
|
||
window.addEventListener('pointerup',onUp);
|
||
window.addEventListener('pointercancel',onUp);
|
||
document.addEventListener('contextmenu',e=>e.preventDefault());
|
||
|
||
/* =========================================================
|
||
UI: МАГАЗИН / ДНЕВНИК / КАРТОЧКА
|
||
========================================================= */
|
||
let shopTab='tackle';
|
||
function openShop(){ uiOpen=true; $('shop').classList.remove('hidden'); renderShop(); sfx('click'); }
|
||
function closeShop(){ uiOpen=false; $('shop').classList.add('hidden'); sfx('click'); }
|
||
function setTab(t){ shopTab=t; renderShop();
|
||
$('tabTackle').classList.toggle('active',t==='tackle');
|
||
$('tabJournal').classList.toggle('active',t==='journal');
|
||
}
|
||
function itemRow(item,i,type,owned,cur){
|
||
let btn;
|
||
if(cur) btn='<button class="buybtn current" disabled>✓ Выбрано</button>';
|
||
else if(owned) btn='<button class="buybtn owned" data-act="use" data-type="'+type+'" data-i="'+i+'">Выбрать</button>';
|
||
else btn='<button class="buybtn" data-act="buy" data-type="'+type+'" data-i="'+i+'">'+item.cost+' 🪙</button>';
|
||
return '<div class="item"><div><div class="itemname">'+item.name+'</div>'+
|
||
'<div class="itemdesc">'+item.desc+'</div></div>'+btn+'</div>';
|
||
}
|
||
function renderShop(){
|
||
const body=$('shopBody');
|
||
if(shopTab==='tackle'){
|
||
let h='<div class="section-title">Удилища</div>';
|
||
RODS.forEach((r,i)=>{ h+=itemRow(r,i,'rod',save.rods.indexOf(i)>=0,save.rod===i); });
|
||
h+='<div class="section-title">Приманки</div>';
|
||
LURES.forEach((l,i)=>{ h+=itemRow(l,i,'lure',save.lures.indexOf(i)>=0,save.lure===i); });
|
||
body.innerHTML=h;
|
||
}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){
|
||
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>';
|
||
}else{
|
||
h+='<div class="jrow unknown"><span class="je">❓</span><span class="jn">???</span>'+
|
||
'<span class="jc">не пойман</span></div>';
|
||
}
|
||
}
|
||
body.innerHTML=h;
|
||
}
|
||
}
|
||
function shopClick(e){
|
||
const b=e.target.closest('[data-act]');
|
||
if(!b) return;
|
||
const type=b.dataset.type, i=+b.dataset.i;
|
||
const list=type==='rod'?RODS:LURES;
|
||
const owned=type==='rod'?save.rods:save.lures;
|
||
const item=list[i];
|
||
if(b.dataset.act==='buy'){
|
||
if(save.coins<item.cost){ sfx('no'); toast('Не хватает монет 🪙'); return; }
|
||
save.coins-=item.cost; owned.push(i);
|
||
if(type==='rod') save.rod=i; else save.lure=i;
|
||
persist(); updateHUD(); sfx('coin'); renderShop();
|
||
toast('Куплено: '+item.name+'!');
|
||
}else{
|
||
if(type==='rod') save.rod=i; else save.lure=i;
|
||
persist(); updateHUD(); sfx('click'); renderShop();
|
||
}
|
||
}
|
||
function showCard(sp,val){
|
||
$('cardRarity').textContent=RARITY_NAMES[sp.rarity];
|
||
$('cardRarity').style.color=RARITY_COLORS[sp.rarity];
|
||
$('cardEmoji').textContent=sp.emoji;
|
||
$('cardName').textContent=sp.name;
|
||
$('cardVal').textContent='+'+val+' 🪙';
|
||
$('card').classList.remove('hidden');
|
||
}
|
||
|
||
/* =========================================================
|
||
КНОПКИ
|
||
========================================================= */
|
||
$('btnPlay').addEventListener('click',()=>{
|
||
ac(); sfx('click');
|
||
$('menu').classList.add('hidden');
|
||
uiOpen=false; state='IDLE';
|
||
});
|
||
$('btnShop').addEventListener('click',openShop);
|
||
$('btnCloseShop').addEventListener('click',closeShop);
|
||
$('tabTackle').addEventListener('click',()=>{ setTab('tackle'); sfx('click'); });
|
||
$('tabJournal').addEventListener('click',()=>{ setTab('journal'); sfx('click'); });
|
||
$('shopBody').addEventListener('click',shopClick);
|
||
$('btnMute').addEventListener('click',()=>{
|
||
save.muted=!save.muted; persist(); updateHUD(); sfx('click');
|
||
});
|
||
$('btnRecall').addEventListener('click',()=>{ recall(); });
|
||
$('btnCardOk').addEventListener('click',()=>{
|
||
sfx('click');
|
||
$('card').classList.add('hidden');
|
||
state='IDLE';
|
||
});
|
||
|
||
/* =========================================================
|
||
СТАРТ
|
||
========================================================= */
|
||
loadSave();
|
||
resize();
|
||
initFish();
|
||
updateHUD();
|
||
requestAnimationFrame(frame);
|
||
</script>
|
||
</body>
|
||
</html>
|