/
veranda
/
TypeGame
Обзор
Документация
Войти
/
veranda
/
TypeGame
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
Безопасность
main
game-engine.js
228 строк
57 KB
veranda
Обновить игры и карту приключений
09 авг 2026, 00:59
09 авг 2026, 00:59
47e89c0
Код
Авторство
О чём код?
(() => { "use strict"; const A="assets/game-items/"; const assetNames=["cat","dog","fox","lion","bear","tiger","rabbit","monkey","dolphin","crab","octopus","fish","shell","shark","anchor","leaf","tree","snake","banana","flower","parrot","dino","football","goal","hockey","pirate-ship","cannon","treasure-chest","pearl","shell-launcher"]; const images={}; assetNames.forEach(n=>{const i=new Image();i.src=A+n+".png";images[n]=i;}); const flightParrot=new Image();flightParrot.src="assets/PixVerse_V6_Transition_540P___-ezgif.com-effects.gif";images["parrot-flight"]=flightParrot; for(let frame=0;frame<20;frame++){const image=new Image();image.src=`assets/parrot-flight-frames/frame-${String(frame).padStart(2,"0")}.png`;images[`parrot-flight-${frame}`]=image;} const jungleFlightBg=new Image();jungleFlightBg.src="assets/jungle-flight-premium.png";images["jungle-flight-bg"]=jungleFlightBg; const seaFloorBg=new Image();seaFloorBg.src="assets/sea-floor-premium.png";images["sea-floor-bg"]=seaFloorBg; [["dino-world","assets/dino-scanner-sanctuary.png"],["dino-crystal","assets/dino-energy-crystal.png"],["sport-world","assets/sport-3d.webp"],["pirate-world","assets/pirate-3d.webp"],["dino-blaster","assets/dino-rescue-blaster.png"]].forEach(([key,src])=>{const image=new Image();image.src=src;images[key]=image;}); ["cute","water","wild","exotic"].forEach(id=>{const image=new Image();image.src=`assets/merge-${id}-premium.png`;images[`merge-bg-${id}`]=image;}); for(let n=1;n<=31;n++){const key=`new-${n}`,i=new Image();i.src=`assets/new-animals/${n}.png`;images[key]=i;} const mergeAnimalSets=[ {id:"cute",name:"Милые зверята",animals:[["new-7","rabbit","кролика"],["new-12","hamster","хомяка"],["new-9","squirrel","белку"],["new-5","koala","коалу"],["new-17","panda","панду"],["new-3","elephant","слона"]]}, {id:"water",name:"Водный мир",animals:[["new-11","frog","лягушку"],["new-14","octopus","осьминога"],["new-1","dolphin","дельфина"],["new-30","turtle","черепаху"],["new-10","penguin","пингвина"],["new-2","whale","кита"]]}, {id:"wild",name:"Дикие животные",animals:[["new-8","hedgehog","ежа"],["new-16","raccoon","енота"],["new-18","wolf","волка"],["new-19","zebra","зебру"],["new-6","giraffe","жирафа"],["new-20","hippo","бегемота"]]}, {id:"exotic",name:"Экзотические",animals:[["new-22","gecko","геккона"],["new-13","chameleon","хамелеона"],["new-25","armadillo","броненосца"],["new-23","sloth","ленивца"],["new-31","kangaroo","кенгуру"],["new-29","flamingo","фламинго"]]} ]; const clamp=(v,a,b)=>Math.max(a,Math.min(b,v)),dist=(a,b)=>Math.hypot(a.x-b.x,a.y-b.y),pick=a=>a[Math.floor(Math.random()*a.length)],colourValues={red:"#e53935",blue:"#2878e3",green:"#35a853",yellow:"#ffd632",black:"#20242b",white:"#ffffff",orange:"#f28c20",purple:"#854ec7",pink:"#f174ad",brown:"#86502d"}; class Arcade { constructor(host,id,hooks={}){this.host=host;this.id=id;this.hooks=hooks;this.running=true;this.last=performance.now();this.score=0;this.actions=0;this.pointer={x:0,y:0,down:false};this.keys={};this.clean=[];this.build();this.init();this.frame=requestAnimationFrame(t=>this.loop(t));} build(){this.host.innerHTML=`<div class="arcade-shell"><div class="arcade-hud"><span class="objective"></span><span class="mission-progress"></span></div><button class="sound-toggle" type="button" aria-label="Выключить звуки">🔊</button><canvas class="arcade-canvas" tabindex="0" aria-label="Игровое поле"></canvas><button class="merge-generator" type="button" hidden>🐾 Создать зверька</button><div class="arcade-help"></div></div>`;this.canvas=this.host.querySelector("canvas");this.ctx=this.canvas.getContext("2d");this.objective=this.host.querySelector(".objective");this.progress=this.host.querySelector(".mission-progress");this.help=this.host.querySelector(".arcade-help");this.generator=this.host.querySelector(".merge-generator");this.soundButton=this.host.querySelector(".sound-toggle");this.soundOn=this.hooks.soundEnabled!==false;this.soundButton.textContent=this.soundOn?"🔊":"🔇";this.soundButton.onclick=()=>{this.soundOn=!this.soundOn;this.soundButton.textContent=this.soundOn?"🔊":"🔇";this.soundButton.setAttribute("aria-label",this.soundOn?"Выключить звуки":"Включить звуки");if(this.soundOn)this.sound("move");};this.resize();const listen=(target,type,fn,opts)=>{target.addEventListener(type,fn,opts);this.clean.push(()=>target.removeEventListener(type,fn,opts));};const pos=e=>{const r=this.canvas.getBoundingClientRect(),p=e.touches?e.touches[0]:e;this.pointer.x=(p.clientX-r.left)*this.canvas.width/r.width;this.pointer.y=(p.clientY-r.top)*this.canvas.height/r.height;};const down=e=>{pos(e);this.pointer.down=true;this.canvas.focus();if(e.pointerId!==undefined)this.canvas.setPointerCapture?.(e.pointerId);this.onDown(e);};const move=e=>{pos(e);this.onMove(e);};const up=e=>{pos(e);this.pointer.down=false;this.onUp(e);if(e.pointerId!==undefined&&this.canvas.hasPointerCapture?.(e.pointerId))this.canvas.releasePointerCapture(e.pointerId);};const keydown=e=>{if(document.activeElement!==this.canvas)return;this.keys[e.key]=true;this.onKey(e);};const keyup=e=>this.keys[e.key]=false;const visibility=()=>document.hidden?this.pause():this.resume();listen(this.canvas,"pointerdown",down);listen(this.canvas,"pointermove",move);listen(this.canvas,"pointerup",up);listen(this.canvas,"pointercancel",up);listen(window,"keydown",keydown);listen(window,"keyup",keyup);listen(window,"resize",()=>this.resize());listen(document,"visibilitychange",visibility);} resize(){const r=this.host.getBoundingClientRect(),oldW=this.canvas.width||0,oldH=this.canvas.height||0,w=Math.max(320,Math.round(r.width||900)),available=Math.max(300,innerHeight-165),h=Math.max(300,Math.round(Math.min(r.height||available,available)));this.canvas.width=w;this.canvas.height=h;if(oldW&&oldH&&this.bubbles){const sx=w/oldW,sy=h/oldH,sr=Math.min(sx,sy);this.bubbles.forEach(b=>{b.x*=sx;b.y*=sy;b.r=Math.max(16,b.r*sr);});if(this.shot){this.shot.x*=sx;this.shot.y*=sy;this.shot.r=Math.max(16,this.shot.r*sr);}}} init(){({animals:()=>this.initMerge(),sea:()=>this.initBubble(),jungle:()=>this.initRunner(),dino:()=>this.initBlaster(),sport:()=>this.initHockey(),pirate:()=>this.initPirate()}[this.id]||(()=>{}))();} loop(t){if(this.destroyed)return;if(!this.running){this.last=t;this.frame=requestAnimationFrame(n=>this.loop(n));return;}const dt=Math.min(.032,(t-this.last)/1000);this.last=t;this.update(dt);this.draw();this.frame=requestAnimationFrame(n=>this.loop(n));} update(dt){this.time=(this.time||0)+dt;({animals:()=>this.updateMerge(dt),sea:()=>this.updateBubble(dt),jungle:()=>this.updateRunner(dt),dino:()=>this.updateBlaster(dt),sport:()=>this.updateHockey(dt),pirate:()=>this.updatePirate(dt)}[this.id]||(()=>{}))();} draw(){const c=this.ctx;c.clearRect(0,0,this.canvas.width,this.canvas.height);({animals:()=>this.drawMerge(),sea:()=>this.drawBubble(),jungle:()=>this.drawRunner(),dino:()=>this.drawBlaster(),sport:()=>this.drawHockey(),pirate:()=>this.drawPirate()}[this.id]||(()=>{}))();this.drawFeedback();} pause(){this.running=false;} resume(){if(document.hidden||document.getElementById("modal"))return;this.running=true;this.last=performance.now();} destroy(){this.destroyed=true;cancelAnimationFrame(this.frame);this.running=false;this.clean.splice(0).forEach(fn=>fn());} sound(name){if(!this.soundOn)return;try{const AC=window.AudioContext||window.webkitAudioContext;this.audio=this.audio||new AC();const ctx=this.audio,now=ctx.currentTime,patterns={move:[[330,.045]],launch:[[240,.04],[390,.07]],correct:[[520,.07],[660,.08],[820,.11]],wrong:[[180,.11],[135,.16]],coin:[[740,.05],[980,.1]],bonus:[[440,.06],[660,.08],[880,.13]],hit:[[110,.12]],finish:[[392,.1],[523,.12],[659,.14],[784,.22]]},notes=patterns[name]||patterns.move;notes.forEach(([freq,dur],i)=>{const o=ctx.createOscillator(),g=ctx.createGain(),start=now+i*.07;o.type=name==="wrong"||name==="hit"?"square":"sine";o.frequency.setValueAtTime(freq,start);g.gain.setValueAtTime(.0001,start);g.gain.exponentialRampToValueAtTime(.055,start+.01);g.gain.exponentialRampToValueAtTime(.0001,start+dur);o.connect(g).connect(ctx.destination);o.start(start);o.stop(start+dur+.02);});}catch{}} success(label="Correct!",notify=true){this.score+=10;this.actions++;this.flash={text:label,good:true,t:1};this.sound("correct");if(notify)this.hooks.onSuccess?.(label);} error(label="Try again",notify=true){this.flash={text:label,good:false,t:1};this.sound("wrong");if(notify)this.hooks.onError?.(label);} drawFeedback(){const c=this.ctx;if(this.id==="dino"){if(this.portalFlash>0){c.save();c.globalAlpha=this.portalFlash*.72;c.strokeStyle="#72f8ff";c.lineWidth=10;c.shadowBlur=35;c.shadowColor="#6cf7ff";c.beginPath();c.arc(this.canvas.width/2,this.canvas.height*.2,70+(1-this.portalFlash)*85,0,Math.PI*2);c.stroke();c.restore();}this.dinoParticles?.forEach(p=>{c.save();c.globalAlpha=clamp(p.t,0,1);c.fillStyle=p.t>.65?"#fff37a":"#58efff";c.shadowBlur=18;c.shadowColor="#66f5ff";c.beginPath();c.arc(p.x,p.y,p.r,0,Math.PI*2);c.fill();c.restore();});}if(!this.flash)return;const f=this.flash,alpha=clamp(f.t,0,1);c.save();c.globalAlpha=alpha;this.roundRect(this.canvas.width/2-170,this.canvas.height/2-42,340,84,22,f.good?"#e4ffd7":"#ffe1dc",f.good?"#65b943":"#e25a4a");this.text(f.text,this.canvas.width/2,this.canvas.height/2,25,f.good?"#276b20":"#8d2822");c.restore();f.t-=document.body.classList.contains("reduced")?.12:.035;if(f.t<=0)this.flash=null;} bg(top="#77ddec",bottom="#0a8296"){const g=this.ctx.createLinearGradient(0,0,0,this.canvas.height);g.addColorStop(0,top);g.addColorStop(1,bottom);this.ctx.fillStyle=g;this.ctx.fillRect(0,0,this.canvas.width,this.canvas.height);} text(s,x,y,size=24,color="#17364c",align="center"){const c=this.ctx;c.font=`900 ${size}px Trebuchet MS,Arial`;c.textAlign=align;c.textBaseline="middle";c.fillStyle=color;c.fillText(s,x,y);} image(name,x,y,w,h=w){const im=images[name];if(im?.complete)this.ctx.drawImage(im,x-w/2,y-h/2,w,h);} cover(name,shade="rgba(0,24,38,.18)",focusY=.5){const im=images[name],c=this.ctx,w=this.canvas.width,h=this.canvas.height;if(im?.complete&&im.naturalWidth){const scale=Math.max(w/im.naturalWidth,h/im.naturalHeight),sw=w/scale,sh=h/scale,sourceY=(im.naturalHeight-sh)*clamp(focusY,0,1);c.drawImage(im,(im.naturalWidth-sw)/2,sourceY,sw,sh,0,0,w,h);}else this.bg();if(shade){c.fillStyle=shade;c.fillRect(0,0,w,h);}} roundRect(x,y,w,h,r,fill,stroke){const c=this.ctx;c.beginPath();c.roundRect(x,y,w,h,r);c.fillStyle=fill;c.fill();if(stroke){c.strokeStyle=stroke;c.lineWidth=3;c.stroke();}} hitCircle(p,o,r=o.r||30){return Math.hypot(p.x-o.x,p.y-o.y)<=r;} onDown(e){({animals:()=>this.mergeDown(),sea:()=>this.bubbleAim(),jungle:()=>this.runnerLift(true),dino:()=>this.blasterShoot(),sport:()=>this.hockeyDown(),pirate:()=>this.pirateDown()}[this.id]||(()=>{}))();} onMove(){if(this.id==="sea")this.bubbleAim();if(this.id==="sport")this.hockeyMove();if(this.id==="pirate")this.pirateAim();} onUp(){if(this.id==="jungle")this.runnerLift(false);if(this.id==="pirate")this.pirateFire();} onKey(e){if(this.id==="jungle"&&[" ","ArrowUp"].includes(e.key)){e.preventDefault();this.runnerLift(true);}if(this.id==="dino"&&e.key===" "){e.preventDefault();this.blasterShoot();}if(this.id==="pirate"&&e.key===" "){e.preventDefault();this.pointer.down?this.pirateFire():this.pirateDown();}} // MERGE — persistent board, real species evolution lines. initMerge(){const config=mergeAnimalSets.find(x=>x.id===this.hooks.mergeSet)||mergeAnimalSets[0];this.mergeTheme=config.id;this.mergeChain=config.animals;this.mergeTargetRank=5;this.merge=[];this.mergeUid=0;this.mergeSelected=null;this.mergeWon=false;this.objective.textContent=`${config.name} · собери ${this.mergeChain[5][2]}`;this.help.textContent="Нажми на одного зверька, затем на такого же.";this.generator.hidden=false;this.generator.onclick=()=>this.generateMergeAnimal();for(let i=0;i<5;i++)this.generateMergeAnimal(true);this.hooks.onProgress?.(0,5,this.mergeChain[5][1]);} mergeDown(){const g=this.mergeGeom(),cell=this.cellAt(this.pointer,g),item=this.merge.find(o=>o.cell===cell);if(!item){this.mergeSelected=null;return;}if(this.mergeSelected===item){this.mergeSelected=null;return;}if(this.mergeSelected){const selected=this.mergeSelected;this.mergeSelected=null;if(item.rank===selected.rank&&item.rank<this.mergeChain.length-1)this.mergePair(selected,item,item.cell);else this.error("Выбери такого же зверька");return;}this.mergeSelected=item;} mergePair(a,target,to){const rank=a.rank+1;this.merge=this.merge.filter(x=>x!==a&&x!==target);const merged={uid:++this.mergeUid,rank,cell:to};this.merge.push(merged);this.mergeSelected=null;this.success("Новый зверёк!");this.hooks.onProgress?.(rank,this.mergeTargetRank,this.mergeChain[5][1]);if(rank===this.mergeTargetRank&&!this.mergeWon){this.mergeWon=true;this.generator.disabled=true;this.objective.textContent="Цель достигнута!";this.hooks.onComplete?.();}} cellAt(p,g){const col=Math.floor((p.x-g.x)/g.s),row=Math.floor((p.y-g.y)/g.s);return col>=0&&col<5&&row>=0&&row<4?row*5+col:-1;} mergeGeom(){const s=Math.min(125,(this.canvas.width-80)/5,(this.canvas.height-125)/4),w=s*5;return{x:(this.canvas.width-w)/2,y:105,s};} updateMerge(){const top=Math.max(0,...this.merge.map(x=>x.rank));this.progress.textContent=`Открыто видов: ${top+1}/${this.mergeChain.length} · Слияний: ${this.actions}`;} generateMergeAnimal(quiet=false){const empty=Array.from({length:20},(_,i)=>i).filter(i=>!this.merge.some(x=>x.cell===i));if(!empty.length){if(!quiet)this.error("На поле нет свободных клеток");return;}const rank=Math.random()<.86?0:1;this.merge.push({uid:++this.mergeUid,rank,cell:pick(empty)});if(!quiet){this.flash={text:"Новый зверёк!",good:true,t:.65};this.sound("bonus");this.hooks.onGenerate?.();}} drawMerge(){this.bg("#ffe9a8","#c9863d");const c=this.ctx,g=this.mergeGeom(),chainY=54,step=Math.min(82,(this.canvas.width-120)/6);this.mergeChain.forEach((animal,i)=>{const x=this.canvas.width/2-step*2.5+i*step;this.image(animal[0],x,chainY,52);if(i<5)this.text("→",x+step/2,chainY,22,"#743817");});this.roundRect(g.x-15,g.y-15,g.s*5+30,g.s*4+30,24,"#6c3518","#d79648");for(let i=0;i<20;i++){const x=g.x+(i%5)*g.s,y=g.y+Math.floor(i/5)*g.s;this.roundRect(x+5,y+5,g.s-10,g.s-10,16,"rgba(255,244,199,.13)","rgba(255,226,151,.25)");}this.merge.forEach(o=>this.drawMergeItem(o,g));} drawMergeItem(o,g){const x=g.x+(o.cell%5)*g.s+g.s/2,y=g.y+Math.floor(o.cell/5)*g.s+g.s/2,selected=this.mergeSelected===o;this.roundRect(x-g.s*.42,y-g.s*.42,g.s*.84,g.s*.84,16,selected?"#fff0a0":"#fff4c9",selected?"#ffb000":"#fff");this.image(this.mergeChain[o.rank][0],x,y,g.s*.68);} // Premium themed merge boards for all four animal collections. drawMerge(){ const c=this.ctx,w=this.canvas.width,h=this.canvas.height,g=this.mergeGeom(),theme=this.mergeTheme||"cute",bg=images[`merge-bg-${theme}`],themes={ cute:{frame:"#7b4726",board:"rgba(255,231,188,.56)",cell:"rgba(255,252,229,.42)",edge:"#ffd98a",glow:"#fff2b5"}, water:{frame:"#07577b",board:"rgba(4,86,120,.52)",cell:"rgba(220,250,255,.40)",edge:"#8df3ff",glow:"#c9fbff"}, wild:{frame:"#563817",board:"rgba(65,62,28,.54)",cell:"rgba(255,226,145,.38)",edge:"#f4c45b",glow:"#ffe6a0"}, exotic:{frame:"#183f31",board:"rgba(13,65,49,.55)",cell:"rgba(224,247,204,.40)",edge:"#7ee29a",glow:"#c8ffbd"} },p=themes[theme]; if(bg?.complete&&bg.naturalWidth){const scale=Math.max(w/bg.naturalWidth,h/bg.naturalHeight),sw=w/scale,sh=h/scale;c.drawImage(bg,(bg.naturalWidth-sw)/2,(bg.naturalHeight-sh)/2,sw,sh,0,0,w,h);}else this.bg("#d7e99b","#4f8b56"); c.fillStyle=theme==="water"?"rgba(0,65,103,.08)":"rgba(24,31,17,.10)";c.fillRect(0,0,w,h); c.save();c.globalAlpha=.55;for(let i=0;i<10;i++){const x=(i*149+(this.time*13)%(w+160))-80,y=40+(i*71)%(h-80);c.beginPath();c.arc(x,y,2+i%3*2,0,Math.PI*2);c.fillStyle=p.glow;c.fill();}c.restore(); const chainW=Math.min(620,w-80),chainX=(w-chainW)/2;this.roundRect(chainX,12,chainW,76,24,"rgba(255,252,231,.88)",p.edge); const step=Math.min(82,(chainW-70)/6),chainY=50;this.mergeChain.forEach((animal,i)=>{const x=w/2-step*2.5+i*step;c.save();c.shadowBlur=12;c.shadowColor=p.glow;this.image(animal[0],x,chainY,50);c.restore();if(i<5)this.text("›",x+step/2,chainY,28,p.frame);}); c.save();c.shadowBlur=30;c.shadowColor="rgba(0,20,18,.48)";this.roundRect(g.x-22,g.y-22,g.s*5+44,g.s*4+44,30,p.frame,p.edge);c.shadowBlur=0;this.roundRect(g.x-13,g.y-13,g.s*5+26,g.s*4+26,24,p.board,p.glow); for(let i=0;i<20;i++){const x=g.x+(i%5)*g.s,y=g.y+Math.floor(i/5)*g.s,grad=c.createLinearGradient(x,y,x+g.s,y+g.s);grad.addColorStop(0,p.cell);grad.addColorStop(1,theme==="water"?"rgba(93,207,232,.30)":theme==="exotic"?"rgba(80,145,94,.30)":theme==="wild"?"rgba(152,104,39,.32)":"rgba(238,173,110,.30)");this.roundRect(x+6,y+6,g.s-12,g.s-12,18,grad,p.edge);} c.restore();this.merge.forEach(o=>this.drawMergeItem(o,g)); } drawMergeItem(o,g){ const c=this.ctx,x=g.x+(o.cell%5)*g.s+g.s/2,y=g.y+Math.floor(o.cell/5)*g.s+g.s/2,selected=this.mergeSelected===o,theme=this.mergeTheme||"cute",edges={cute:"#fff0b5",water:"#bffaff",wild:"#ffe08a",exotic:"#baffb8"},edge=selected?"#ffd229":edges[theme]; c.save();c.shadowBlur=selected?25:13;c.shadowColor=selected?"#ffd229":edge;this.roundRect(x-g.s*.39,y-g.s*.39,g.s*.78,g.s*.78,18,selected?"rgba(255,246,170,.97)":"rgba(255,255,245,.91)",edge);c.shadowBlur=0;c.fillStyle="rgba(255,255,255,.48)";c.beginPath();c.ellipse(x-g.s*.12,y-g.s*.18,g.s*.18,g.s*.08,-.4,0,Math.PI*2);c.fill();this.image(this.mergeChain[o.rank][0],x,y,g.s*.66);c.restore(); } // BUBBLE SHOOTER — aim, flight, wall bounce, hex attachment and cluster pop. initBubble(){this.bubbleWords=[{word:"crab",art:"crab"},{word:"fish",art:"fish"},{word:"shark",art:"shark"},{word:"shell",art:"shell"},{word:"octopus",art:"octopus"}];this.bubbles=[];const r=clamp(this.canvas.width/36,19,28),cx=this.canvas.width/2,cy=48,maxRadius=Math.min(this.canvas.width*.43,this.canvas.height*.4),rings=this.canvas.height<520?3:4;for(let ring=0;ring<rings;ring++){const radius=maxRadius-ring*r*2.05,count=Math.max(7,Math.floor(Math.PI*radius/(r*2.03)));for(let i=0;i<count;i++){const angle=Math.PI*(.07+.86*(i/(count-1))),x=cx+Math.cos(angle)*radius,y=cy+Math.sin(angle)*radius;this.bubbles.push({x,y,r,type:(ring*2+i)%5,anchor:ring===0});}}this.bubbleRows=rings;this.shot=null;this.aim=-Math.PI/2;this.nextType=0;this.afterType=1;this.objective.textContent="Собери 8 групп и освободи дельфина";this.help.textContent="Наведи и нажми, чтобы запустить. Соедини 3 одинаковых картинки.";} bubbleAim(){const dx=this.pointer.x-this.canvas.width/2,dy=this.pointer.y-(this.canvas.height-58);this.aim=clamp(Math.atan2(dy,dx),-Math.PI+.25,-.25);if(this.pointer.down&&!this.shot){const s=620;this.shot={x:this.canvas.width/2,y:this.canvas.height-78,vx:Math.cos(this.aim)*s,vy:Math.sin(this.aim)*s,r:26,type:this.nextType};this.nextType=this.afterType;this.afterType=Math.floor(Math.random()*5);this.sound("launch");}} updateBubble(dt){if(!this.shot)return;const s=this.shot;s.x+=s.vx*dt;s.y+=s.vy*dt;if(s.x<s.r||s.x>this.canvas.width-s.r){s.vx*=-1;s.x=clamp(s.x,s.r,this.canvas.width-s.r);}const hit=s.y<s.r+20||this.bubbles.find(b=>dist(s,b)<s.r+b.r-5);if(hit){s.vx=s.vy=0;this.bubbles.push(s);this.resolveBubble(s);this.shot=null;}} resolveBubble(s){const near=(a,b)=>dist(a,b)<68;const group=[],q=[s],seen=new Set([s]);while(q.length){const a=q.pop();group.push(a);this.bubbles.forEach(b=>{if(!seen.has(b)&&b.type===s.type&&near(a,b)){seen.add(b);q.push(b);}});}if(group.length>=3){this.bubbles=this.bubbles.filter(b=>!seen.has(b));this.success(this.bubbleWords[s.type].word);this.dropLoose();}else this.error("Build a group of three");this.progress.textContent=`Matches: ${this.actions}/8`;} dropLoose(){const fixed=new Set(this.bubbles.filter(b=>b.anchor)),q=[...fixed];while(q.length){const a=q.pop();this.bubbles.forEach(b=>{if(!fixed.has(b)&&dist(a,b)<68){fixed.add(b);q.push(b);}});}const loose=this.bubbles.filter(b=>!fixed.has(b));this.bubbles=this.bubbles.filter(b=>fixed.has(b));if(loose.length)this.score+=loose.length*2;} drawBubble(){this.bg("#74dff0","#087c91");const c=this.ctx;this.bubbles.forEach(b=>{const grad=c.createRadialGradient(b.x-10,b.y-12,2,b.x,b.y,b.r);grad.addColorStop(0,"#f4ffff");grad.addColorStop(.4,["#69cfe8","#f28472","#9c79e2","#f6c45c","#70d38b"][b.type]);grad.addColorStop(1,"#146b80");c.beginPath();c.arc(b.x,b.y,b.r,0,7);c.fillStyle=grad;c.fill();c.strokeStyle="#eaffff";c.lineWidth=3;c.stroke();this.image(this.bubbleWords[b.type].art,b.x,b.y,b.r*1.25);});if(this.shot)this.image(this.bubbleWords[this.shot.type].art,this.shot.x,this.shot.y,48);const x=this.canvas.width/2,launchY=this.canvas.height-78;this.image("shell-launcher",x-24,launchY+49,126,146);this.image(this.bubbleWords[this.nextType].art,x,launchY,48);c.setLineDash([8,10]);c.strokeStyle="rgba(255,255,255,.72)";c.lineWidth=3;c.beginPath();c.moveTo(x,launchY);c.lineTo(x+Math.cos(this.aim)*180,launchY+Math.sin(this.aim)*180);c.stroke();c.setLineDash([]);this.roundRect(this.canvas.width-128,this.canvas.height-112,106,86,16,"rgba(255,252,226,.9)","#fff");this.text("NEXT",this.canvas.width-75,this.canvas.height-95,13);this.image(this.bubbleWords[this.afterType].art,this.canvas.width-75,this.canvas.height-60,48);} // Premium underwater rendering for the semicircular reef. drawBubble(){ const c=this.ctx,w=this.canvas.width,h=this.canvas.height,bg=images["sea-floor-bg"]; if(bg?.complete&&bg.naturalWidth){const scale=Math.max(w/bg.naturalWidth,h/bg.naturalHeight),sw=w/scale,sh=h/scale,sx=(bg.naturalWidth-sw)/2,sy=(bg.naturalHeight-sh)*.46;c.drawImage(bg,sx,sy,sw,sh,0,0,w,h);}else this.bg("#63e3f5","#076d91"); c.fillStyle="rgba(4,76,119,.08)";c.fillRect(0,0,w,h); c.save();c.globalAlpha=.42;for(let i=0;i<12;i++){const x=(i*137+(this.time*18)%(w+140))-70,y=h-((i*83+this.time*34)%(h+80));c.beginPath();c.arc(x,y,4+i%3*3,0,Math.PI*2);c.strokeStyle="#dffcff";c.lineWidth=2;c.stroke();}c.restore(); this.bubbles.forEach(b=>{const grad=c.createRadialGradient(b.x-b.r*.34,b.y-b.r*.4,2,b.x,b.y,b.r);grad.addColorStop(0,"#ffffff");grad.addColorStop(.25,"rgba(255,255,255,.82)");grad.addColorStop(.52,["#42c9ec","#ff7e68","#9a72ef","#ffc44f","#55d88b"][b.type]);grad.addColorStop(1,"#075f87");c.save();c.shadowBlur=12;c.shadowColor="rgba(192,251,255,.72)";c.beginPath();c.arc(b.x,b.y,b.r,0,Math.PI*2);c.fillStyle=grad;c.fill();c.strokeStyle="#eaffff";c.lineWidth=3;c.stroke();c.shadowBlur=0;this.image(this.bubbleWords[b.type].art,b.x,b.y,b.r*1.24);c.restore();}); if(this.shot)this.image(this.bubbleWords[this.shot.type].art,this.shot.x,this.shot.y,48); const x=w/2,launchY=h-78;this.image("shell-launcher",x-24,launchY+49,126,146);this.image(this.bubbleWords[this.nextType].art,x,launchY,48); c.setLineDash([8,10]);c.strokeStyle="rgba(255,255,255,.82)";c.lineWidth=3;c.beginPath();c.moveTo(x,launchY);c.lineTo(x+Math.cos(this.aim)*190,launchY+Math.sin(this.aim)*190);c.stroke();c.setLineDash([]); this.roundRect(w-128,h-112,106,86,16,"rgba(255,252,226,.92)","#fff");this.text("NEXT",w-75,h-95,13);this.image(this.bubbleWords[this.afterType].art,w-75,h-60,48); } // JUNGLE RUNNER — quiz or spelling, three lanes, obstacles and bonuses. initRunner(){const mode=this.hooks.runnerMode||"quiz";this.runner={mode,lane:1,x:Math.max(95,this.canvas.width*.14),speed:clamp(this.canvas.width*.2,170,235),objects:[],phase:"ready",wait:.35,done:0,total:mode==="word"?8:12,lives:3,combo:0,bestCombo:0,coins:0,stage:0,target:"JUNGLE",pos:0,lastCorrectLane:-1,finished:false};this.help.textContent=mode==="word"?"Собирай буквы по порядку · стрелки, свайп или касание дорожки":"Выбери дорожку с верным ответом · уклоняйся от препятствий · доберись до храма";this.spawnRunnerQuiz();} runnerMove(dir){const r=this.runner,old=r.lane;if(typeof dir!=="number"){const lane=Math.floor(this.pointer.y/(this.canvas.height/3));r.lane=clamp(lane,0,2);}else r.lane=clamp(r.lane+dir,0,2);if(old!==r.lane)this.sound("move");} runnerOptions(options,answer){const unique=[...new Set(options.map(String))];while(unique.length<3)unique.push(pick(["river","stone","flower","cloud"]));const shuffled=unique.slice(0,3).sort(()=>Math.random()-.5);if(!shuffled.includes(String(answer)))shuffled[Math.floor(Math.random()*3)]=String(answer);return shuffled;} spawnRunnerQuiz(){const r=this.runner;if(r.finished)return;r.phase="quiz";let prompt,answer,options;if(r.mode==="word"){answer=r.target[r.pos];prompt=`Собери слово: ${r.target.split("").map((x,i)=>i<r.pos?x:"_").join(" ")}`;const traps="ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").filter(x=>x!==answer).sort(()=>Math.random()-.5).slice(0,2);options=[answer,...traps];}else{const task=this.hooks.getTask?.()||{prompt:"Which animal can fly?",options:["parrot","elephant","tiger"],answer:"parrot"};prompt=task.prompt;answer=task.answer;options=task.options;}options=this.runnerOptions(options,answer);let correctLane=options.indexOf(String(answer));if(correctLane===r.lastCorrectLane&&Math.random()<.7){const swap=(correctLane+1+Math.floor(Math.random()*2))%3;[options[correctLane],options[swap]]=[options[swap],options[correctLane]];correctLane=swap;}r.lastCorrectLane=correctLane;r.answer=String(answer);r.prompt=prompt;r.objects=options.map((label,lane)=>({kind:"answer",x:this.canvas.width+130,lane,label,correct:label===String(answer),done:false}));this.objective.textContent=prompt;} spawnRunnerInterlude(){const r=this.runner,blocked=Math.floor(Math.random()*3),bonus=(blocked+1+Math.floor(Math.random()*2))%3;r.phase="interlude";r.objects=[{kind:"obstacle",type:pick(["vine","stone","snake"]),x:this.canvas.width+90,lane:blocked,done:false},{kind:"bonus",type:pick(["coin","banana","heart","shield"]),x:this.canvas.width+250,lane:bonus,done:false}];this.objective.textContent=pick(["Уклонись от лианы!","Собери бонус!","Путь через джунгли продолжается…"]);} runnerAnswer(selected){const r=this.runner,good=selected.correct;if(good){r.combo++;r.bestCombo=Math.max(r.bestCombo,r.combo);r.coins+=r.combo>=5?20:10;this.success(r.combo>=5?"SUPER FLIGHT! +20":"Верно! +10",false);if(r.mode==="word"){r.pos++;if(r.pos>=r.target.length){r.target=pick(["PARROT","MONKEY","BANANA","FLOWER"]);r.pos=0;}}}else{r.lives=Math.max(0,r.lives-1);r.combo=0;this.error(`Ответ: ${r.answer}`,false);}r.done++;r.objects.forEach(o=>o.done=true);this.hooks.onRunnerAnswer?.(good,r);if(r.done>=r.total||r.lives<=0){r.finished=true;this.objective.textContent=r.lives>0?"TEMPLE REACHED!":"Попугай отдохнул и долетел до храма!";this.sound("finish");setTimeout(()=>this.hooks.onComplete?.(),900);return;}r.phase="pause";r.wait=.7;} updateRunner(dt){const r=this.runner;if(r.finished)return;r.wait-=dt;if(r.phase==="pause"&&r.wait<=0){this.spawnRunnerInterlude();return;}if(r.phase==="interlude"&&!r.objects.length){r.phase="ready";r.wait=.35;}if(r.phase==="ready"&&r.wait<=0){this.spawnRunnerQuiz();return;}r.objects.forEach(o=>o.x-=r.speed*dt);const near=r.objects.filter(o=>!o.done&&Math.abs(o.x-r.x)<48);if(r.phase==="quiz"&&near.length){const selected=near.find(o=>o.lane===r.lane);if(selected)this.runnerAnswer(selected);}else if(r.phase==="interlude"){near.forEach(o=>{o.done=true;if(o.lane!==r.lane)return;if(o.kind==="obstacle"){r.lives=Math.max(0,r.lives-1);r.combo=0;this.flash={text:"Осторожно! −1 жизнь",good:false,t:.75};this.sound("hit");}else{if(o.type==="heart")r.lives=Math.min(3,r.lives+1);else r.coins+=o.type==="coin"?5:10;this.flash={text:o.type==="heart"?"+1 жизнь":"Бонус!",good:true,t:.65};this.sound(o.type==="coin"?"coin":"bonus");}});}r.objects=r.objects.filter(o=>o.x>-150&&!o.done);if(r.phase==="interlude"&&!r.objects.length){r.phase="ready";r.wait=.35;}r.stage=Math.min(5,Math.floor(r.done/2.1));this.progress.textContent=`${"❤️".repeat(r.lives)} · Комбо ${r.combo} · До храма ${Math.max(0,(r.total-r.done)*90)} м`;} drawRunner(){const r=this.runner,c=this.ctx,laneH=this.canvas.height/3,stages=[["#8ee38a","#176b43"],["#55bd64","#0d5538"],["#67d8c5","#146d67"],["#7dd7e8","#246285"],["#98bd79","#4a6140"],["#f4cd74","#765027"]],palette=stages[r.stage];this.bg(palette[0],palette[1]);c.globalAlpha=.22;for(let x=(this.time*90)%130-130;x<this.canvas.width;x+=130){this.image("tree",x,70,115);this.image("leaf",x+55,this.canvas.height-55,95);}c.globalAlpha=1;for(let i=1;i<3;i++){c.strokeStyle="rgba(255,247,186,.48)";c.lineWidth=3;c.setLineDash([18,16]);c.beginPath();c.moveTo(0,i*laneH);c.lineTo(this.canvas.width,i*laneH);c.stroke();}c.setLineDash([]);if(r.stage>=4)this.text("🛕",this.canvas.width-75,75,64,"#fff");const py=(r.lane+.5)*laneH+Math.sin(this.time*5)*5;c.save();c.translate(r.x,py);c.rotate(Math.sin(this.time*4)*.035);c.scale(-1,1);this.image("parrot",0,0,clamp(laneH*.62,72,112));c.restore();r.objects.forEach(o=>{const y=(o.lane+.5)*laneH;if(o.kind==="answer"){const width=clamp(110+o.label.length*7,132,230),font=clamp(25-o.label.length*.55,16,25);this.roundRect(o.x-width/2,y-39,width,78,18,"rgba(255,248,207,.96)","#fff4b0");c.strokeStyle="#70451e";c.lineWidth=4;c.stroke();this.text(o.label,o.x,y,font,"#173b4d");}else if(o.kind==="obstacle"){this.text({vine:"🌿",stone:"🪨",snake:"🐍"}[o.type],o.x,y,56,"#fff");}else{if(o.type==="coin")this.image("coin",o.x,y,54);else if(o.type==="banana")this.image("banana",o.x,y,62);else this.text(o.type==="heart"?"❤️":"🛡️",o.x,y,48,"#fff");}});this.roundRect(16,this.canvas.height-46,Math.min(330,this.canvas.width*.38),28,13,"rgba(4,45,37,.72)");this.roundRect(20,this.canvas.height-42,(Math.min(322,this.canvas.width*.38-8))*r.done/r.total,20,10,"#ffd34e");this.text(`${["Вход","Густые джунгли","Река","Водопад","Руины","Храм"][r.stage]} · ${r.done}/${r.total}`,25,this.canvas.height-31,14,"#fff","left");} // FLYING RUNNER — free vertical flight, answer challenges and vine corridors. initRunner(){ const mode=this.hooks.runnerMode||"quiz"; this.spellingWords=[["TIGER","тигр","🐯"],["PARROT","попугай","🦜"],["MONKEY","обезьяна","🐒"],["BANANA","банан","🍌"],["FLOWER","цветок","🌺"]]; this.runner={mode,x:Math.max(105,this.canvas.width*.16),y:this.canvas.height*.5,vy:0,holding:false,speed:165,objects:[],phase:"ready",wait:.45,done:0,total:10,lives:3,combo:0,bestCombo:0,coins:0,shield:0,invulnerable:0,stage:0,wordIndex:0,pos:0,finished:false}; this.help.textContent="Удерживай — вверх · отпусти — вниз"; setTimeout(()=>{if(this.help)this.help.textContent="";},5500); const release=e=>{if([" ","ArrowUp"].includes(e.key))this.runnerLift(false);};window.addEventListener("keyup",release);this.clean.push(()=>window.removeEventListener("keyup",release)); this.spawnRunnerChallenge(); } runnerLift(active){const r=this.runner;if(!r)return;r.holding=active;if(active){r.vy=Math.min(r.vy,-105);this.sound("move");}} runnerOptions(options,answer){const unique=[...new Set(options.map(String))];while(unique.length<3)unique.push(pick(["river","stone","flower","cloud"]));const shuffled=unique.slice(0,3).sort(()=>Math.random()-.5);if(!shuffled.includes(String(answer)))shuffled[Math.floor(Math.random()*3)]=String(answer);return shuffled;} spawnRunnerChallenge(){ const r=this.runner;if(r.finished)return;r.phase="challenge"; let prompt,answer,options; if(r.mode==="word"){ const [word,hint,icon]=this.spellingWords[r.wordIndex%this.spellingWords.length],built=word.split("").map((x,i)=>i<r.pos?x:"_").join(" "); answer=word[r.pos];prompt=`${icon} ${word} — «${hint}» · ${built}`; options=[answer,..."ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").filter(x=>x!==answer).sort(()=>Math.random()-.5).slice(0,2)]; }else{ const task=this.hooks.getTask?.()||{prompt:"Which animal can fly?",options:["parrot","elephant","tiger"],answer:"parrot"}; prompt=task.prompt;answer=task.answer;options=task.options; } options=this.runnerOptions(options,answer); const h=this.canvas.height,levels=[h*.22,h*.5,h*.78].sort(()=>Math.random()-.5),baseX=this.canvas.width+r.speed*3.2; r.answer=String(answer); r.objects=options.map((label,i)=>({kind:"answer",x:baseX+i*235,y:clamp(levels[i],82,h-76),label,correct:label===String(answer),w:clamp(108+String(label).length*7,128,210),done:false})); this.objective.textContent=prompt; } spawnRunnerFlight(){ const r=this.runner,h=this.canvas.height,gapH=clamp(h*.4,190,250),gapY=clamp(r.y+(Math.random()-.5)*140,gapH/2+35,h-gapH/2-35),x=this.canvas.width+100; r.phase="flight";r.objects=[{kind:"vines",x,gapY,gapH,done:false}]; for(let i=0;i<10;i++){const special=i===4||i===9,type=special?pick(["star","heart","shield"]):"coin";r.objects.push({kind:"bonus",type,x:x-250+i*96,y:clamp(gapY+Math.sin(i*.72)*58,72,h-72),done:false});} this.objective.textContent=pick(["Пролети между лианами","Собери бонусы по пути","Путь к храму продолжается"]); } runnerAnswer(selected){ const r=this.runner,good=!!selected?.correct; if(good){ r.combo++;r.bestCombo=Math.max(r.bestCombo,r.combo);r.coins+=r.combo>=5?20:10;this.success(r.combo>=5?"SUPER FLIGHT! +20":"CORRECT! +10",false); if(r.mode==="word"){const [word]=this.spellingWords[r.wordIndex%this.spellingWords.length];r.pos++;if(r.pos>=word.length){r.wordIndex++;r.pos=0;}} }else{ if(r.shield)r.shield=0;else r.lives=Math.max(0,r.lives-1); r.combo=0;r.invulnerable=1.2;this.error(`Правильный ответ: ${r.answer}`,false); } r.done++;r.objects.forEach(o=>o.done=true);this.hooks.onRunnerAnswer?.(good,r); if(r.done>=r.total||r.lives<=0){r.finished=true;this.objective.textContent="TEMPLE REACHED!";this.sound("finish");setTimeout(()=>this.hooks.onComplete?.(),1000);return;} r.phase="safe";r.wait=1.05; } runnerHit(){ const r=this.runner;if(r.invulnerable>0)return;r.invulnerable=1.3; if(r.shield){r.shield=0;this.flash={text:"Щит защитил!",good:true,t:.7};this.sound("bonus");return;} r.lives=Math.max(0,r.lives-1);r.combo=0;this.flash={text:"Осторожно! −1 сердце",good:false,t:.75};this.sound("hit"); } updateRunner(dt){ const r=this.runner;if(r.finished)return; r.invulnerable=Math.max(0,r.invulnerable-dt);r.speed=clamp(this.canvas.width*.11+r.done*2,105,155); if(r.holding)r.vy=Math.max(-270,r.vy-720*dt);else r.vy=Math.min(235,r.vy+430*dt); r.y=clamp(r.y+r.vy*dt,58,this.canvas.height-58);if(r.y<=58||r.y>=this.canvas.height-58)r.vy*=.35; r.wait-=dt;if(r.phase==="safe"&&r.wait<=0){this.spawnRunnerFlight();return;}if(r.phase==="ready"&&r.wait<=0){this.spawnRunnerChallenge();return;} r.objects.forEach(o=>o.x-=r.speed*dt); if(r.phase==="challenge"){ const hit=r.objects.find(o=>!o.done&&Math.abs(o.x-r.x)<o.w/2+27&&Math.abs(o.y-r.y)<58); if(hit)this.runnerAnswer(hit);else if(r.objects.length&&r.objects.every(o=>o.x<r.x-150))this.runnerAnswer(null); }else if(r.phase==="flight"){ r.objects.forEach(o=>{ if(o.done)return; if(o.kind==="vines"&&Math.abs(o.x-r.x)<38&&(r.y<o.gapY-o.gapH/2+25||r.y>o.gapY+o.gapH/2-25))this.runnerHit(); if(o.kind==="bonus"&&Math.abs(o.x-r.x)<44&&Math.abs(o.y-r.y)<46){o.done=true;if(o.type==="heart")r.lives=Math.min(3,r.lives+1);else if(o.type==="shield")r.shield=1;else r.coins+=o.type==="star"?10:5;this.flash={text:o.type==="heart"?"+1 сердце":o.type==="shield"?"Щит!":"Бонус!",good:true,t:.55};this.sound(o.type==="coin"?"coin":"bonus");} }); } r.objects=r.objects.filter(o=>o.x>-180&&!o.done);if(r.phase==="flight"&&!r.objects.length){r.phase="ready";r.wait=.75;} r.stage=Math.min(5,Math.floor(r.done/2));this.progress.textContent=`${"❤️".repeat(r.lives)}${r.shield?" 🛡️":""} · 🪙 ${r.coins}${r.combo>=2?` · 🔥 x${r.combo}`:""} · ${r.done}/${r.total}`; } drawRunner(){ const r=this.runner,c=this.ctx,w=this.canvas.width,h=this.canvas.height,bg=images["jungle-flight-bg"]; if(bg?.complete&&bg.naturalWidth){const scale=Math.max(w/bg.naturalWidth,h/bg.naturalHeight),sw=w/scale,sh=h/scale,sx=(bg.naturalWidth-sw)*(.38+r.stage*.035),sy=(bg.naturalHeight-sh)*.48;c.drawImage(bg,sx,sy,sw,sh,0,0,w,h);}else this.bg("#69c98d","#0b604c"); const tint=["rgba(21,115,70,.03)","rgba(0,67,38,.14)","rgba(0,126,150,.11)","rgba(70,150,190,.12)","rgba(91,72,37,.12)","rgba(255,188,57,.08)"][r.stage];c.fillStyle=tint;c.fillRect(0,0,w,h); c.save();c.globalAlpha=.58;for(let i=0;i<6;i++){const x=(i*293-(this.time*r.speed*.48)%1758+1758)%1758-140;this.image(i%3?"leaf":"flower",x,i%2?h-38:38,95+i%2*30);}c.restore(); r.objects.forEach(o=>{ if(o.kind==="answer"){ c.save();c.shadowBlur=18;c.shadowColor="rgba(7,35,24,.46)";this.roundRect(o.x-o.w/2-5,o.y-40,o.w+10,80,18,"#5d3219","#ffd66b");c.shadowBlur=0;this.roundRect(o.x-o.w/2,o.y-35,o.w,70,14,"#f4c66a","#fff0a6");const wood=c.createLinearGradient(o.x-o.w/2,o.y-32,o.x+o.w/2,o.y+32);wood.addColorStop(0,"#fff4c5");wood.addColorStop(.5,"#f5d98f");wood.addColorStop(1,"#dca94f");this.roundRect(o.x-o.w/2+6,o.y-29,o.w-12,58,11,wood,"#9c652c");c.fillStyle="rgba(255,255,255,.4)";c.fillRect(o.x-o.w/2+15,o.y-23,o.w-30,3);this.image("leaf",o.x-o.w/2+4,o.y-30,25);this.image("leaf",o.x+o.w/2-4,o.y+30,25);this.text(o.label,o.x,o.y,clamp(23-String(o.label).length*.4,15,23),"#19394a");c.restore(); }else if(o.kind==="vines"){ const top=o.gapY-o.gapH/2,bottom=o.gapY+o.gapH/2;c.strokeStyle="#276b2e";c.lineWidth=20;c.lineCap="round";c.beginPath();c.moveTo(o.x,0);c.quadraticCurveTo(o.x+18,top*.55,o.x,top);c.moveTo(o.x,h);c.quadraticCurveTo(o.x-18,bottom+(h-bottom)*.45,o.x,bottom);c.stroke();for(let y=35;y<top;y+=55)this.image("leaf",o.x+18,y,40);for(let y=h-35;y>bottom;y-=55)this.image("leaf",o.x-18,y,40); }else if(o.kind==="bonus"){const colours={coin:["#ffd84d","#a96308"],star:["#b978ff","#582390"],heart:["#ff697d","#9d1736"],shield:["#55d7ff","#126d9b"]},symbols={coin:"★",star:"✦",heart:"♥",shield:"◆"},palette=colours[o.type]||colours.coin;c.save();c.shadowBlur=28;c.shadowColor=palette[0];c.beginPath();c.arc(o.x,o.y,38,0,Math.PI*2);c.fillStyle=palette[0];c.fill();c.strokeStyle="#fff7c9";c.lineWidth=5;c.stroke();c.shadowBlur=0;c.beginPath();c.arc(o.x,o.y,28,0,Math.PI*2);c.fillStyle=palette[1];c.fill();this.text(symbols[o.type]||"★",o.x,o.y+1,34,"#fff9d2");c.restore();} }); c.save();c.globalAlpha=r.invulnerable>0&&Math.floor(this.time*12)%2?0.35:1;c.translate(r.x,r.y);c.rotate(clamp(r.vy/700,-.22,.22));this.image(`parrot-flight-${Math.floor(this.time*10)%20}`,0,0,clamp(h*.29,128,178),clamp(h*.22,96,132));c.restore(); const mapW=Math.min(310,w*.42);this.roundRect(18,h-38,mapW,22,11,"rgba(3,48,39,.72)");this.roundRect(21,h-35,(mapW-6)*r.done/r.total,16,8,"#ffd34e");this.text(`${r.done}/${r.total} 🦜 ━ 🌴 ━ 🛕`,27,h-27,13,"#fff","left"); } // DINO SCANNER — research ancient answer crystals with a smoothly pivoting scanner. initBlaster(){this.wave=0;this.dinoCombo=0;this.dinoParticles=[];this.portalFlash=0;this.targets=[];this.scanBeam=null;this.scanLocked=false;this.blasterReadyAt=0;this.blasterReading=performance.now()+1800;this.turret={x:this.canvas.width/2,y:this.canvas.height-58,angle:0,targetAngle:0};this.spawnWave();this.help.textContent="Наведи исследовательский сканер на кристалл с правильным ответом";} task3(fallback){const raw=this.hooks.getTask?.()||fallback,answer=String(raw.answer),wrong=[...new Set((raw.options||[]).map(String).filter(x=>x!==answer))];while(wrong.length<2)wrong.push(`— ${wrong.length+1}`);return{prompt:raw.prompt,answer,optionStyle:raw.optionStyle||null,options:[answer,...wrong.slice(0,2)].sort(()=>Math.random()-.5)};} spawnWave(){if(this.destroyed)return;const q=this.task3({prompt:"I ___ seven.",options:["am","is","are"],answer:"am"}),w=this.canvas.width,h=this.canvas.height,r=clamp(Math.min(w,h)*.075,44,60),xs=[w*.23,w*.5,w*.77];this.blasterTask=q;this.objective.textContent=q.prompt;this.blasterReading=performance.now()+1800;this.scanLocked=false;this.targets=q.options.map((answer,i)=>({x:i===1?w/2:(i? w+120:-120),baseX:xs[i],baseY:clamp(h*.3+(i%2)*h*.1,145,h*.52),y:h*.2,r,phase:i*2.1,enter:0,answer,colour:q.optionStyle==="colour"?colourValues[answer]:null,correct:answer===q.answer,pulse:0}));} blasterShoot(){if(performance.now()<this.blasterReading||performance.now()<this.blasterReadyAt||this.scanLocked||!this.targets.length)return;const target=this.targets.find(t=>dist(this.pointer,t)<t.r*1.15);if(!target)return;this.blasterReadyAt=performance.now()+650;this.scanLocked=true;this.turret.targetAngle=clamp(Math.atan2(target.y-(this.turret.y-88),target.x-this.turret.x)+Math.PI/2,-.68,.68);this.scanBeam={target,t:.58,resolved:false};this.sound("launch");} updateBlaster(dt){this.turret.x=this.canvas.width/2;this.turret.y=this.canvas.height*.86-3;const aimX=this.scanBeam?.target.x||(this.pointer.x||this.turret.x),aimY=this.scanBeam?.target.y||(this.pointer.y||120);this.turret.targetAngle=clamp(Math.atan2(aimY-(this.turret.y-88),aimX-this.turret.x)+Math.PI/2,-.68,.68);this.turret.angle+=(this.turret.targetAngle-this.turret.angle)*Math.min(1,dt*7);this.targets.forEach(t=>{t.enter=Math.min(1,t.enter+dt*2.25);const tx=t.baseX+Math.sin(this.time*.9+t.phase)*32,ty=t.baseY+Math.sin(this.time*1.55+t.phase)*18;t.x+=(tx-t.x)*Math.min(1,dt*(3+t.enter*4));t.y+=(ty-t.y)*Math.min(1,dt*6);t.pulse=Math.max(0,t.pulse-dt);});this.dinoParticles.forEach(p=>{p.x+=p.vx*dt;p.y+=p.vy*dt;p.vy+=35*dt;p.t-=dt;});this.dinoParticles=this.dinoParticles.filter(p=>p.t>0);this.portalFlash=Math.max(0,this.portalFlash-dt*1.5);if(this.scanBeam){this.scanBeam.t-=dt;const t=this.scanBeam.target;if(!this.scanBeam.resolved&&this.scanBeam.t<.28){this.scanBeam.resolved=true;if(t.correct){this.dinoCombo++;this.portalFlash=1;for(let i=0;i<38;i++)this.dinoParticles.push({x:t.x,y:t.y,vx:(Math.random()-.5)*310,vy:(Math.random()-.7)*260,t:.65+Math.random()*.55,r:2+Math.random()*5});t.pulse=1;this.success(this.dinoCombo>=3?`Энергосерия ×${this.dinoCombo}!`:"Кристалл активирован!");this.wave++;this.targets=[];if(this.wave<10)setTimeout(()=>this.spawnWave(),750);}else{this.dinoCombo=0;t.pulse=-1;this.error(`Верный кристалл: ${this.blasterTask.answer}`);this.scanLocked=false;}}if(this.scanBeam.t<=0)this.scanBeam=null;}this.progress.textContent=`Долина: ${Math.min(this.wave,10)}/10${this.dinoCombo>=2?` · Серия ×${this.dinoCombo}`:""}`;} drawBlaster(){this.cover("dino-world","rgba(8,39,29,.15)",0);const c=this.ctx,w=this.canvas.width,h=this.canvas.height,scanner=images["dino-blaster"];c.fillStyle="rgba(18,53,37,.08)";c.fillRect(0,0,w,h);for(let i=0;i<10;i++){const x=38+i*Math.min(34,(w-76)/10),active=i<this.wave;c.beginPath();c.arc(x,h-30,9,0,7);c.fillStyle=active?"#64f6ff":"rgba(255,255,255,.36)";c.shadowBlur=active?15:0;c.shadowColor="#73f8ff";c.fill();}c.shadowBlur=0;this.targets.forEach(t=>{c.save();c.translate(t.x,t.y);const glow=t.pulse>0?"#fff66b":t.pulse<0?"#ff745e":"#64f4ff";c.shadowBlur=28;c.shadowColor=glow;const g=c.createLinearGradient(-t.r,-t.r,t.r,t.r);g.addColorStop(0,t.colour||"#c9ffff");g.addColorStop(.45,t.colour||"#42d9e9");g.addColorStop(1,t.colour||"#197cae");c.beginPath();c.moveTo(0,-t.r);c.lineTo(t.r*.72,-t.r*.34);c.lineTo(t.r*.58,t.r*.62);c.lineTo(0,t.r);c.lineTo(-t.r*.58,t.r*.62);c.lineTo(-t.r*.72,-t.r*.34);c.closePath();c.fillStyle=g;c.fill();c.strokeStyle="#e9ffff";c.lineWidth=5;c.stroke();c.shadowBlur=0;c.beginPath();c.moveTo(0,-t.r*.82);c.lineTo(-t.r*.24,t.r*.56);c.moveTo(0,-t.r*.82);c.lineTo(t.r*.28,t.r*.54);c.strokeStyle="rgba(255,255,255,.48)";c.lineWidth=3;c.stroke();this.roundRect(-t.r*.96,-16,t.r*1.92,38,12,t.colour||"rgba(255,253,224,.95)","#174d68");if(!t.colour)this.text(t.answer,0,3,clamp(24-String(t.answer).length*.45,15,23),"#17384b");c.restore();});if(this.scanBeam){const t=this.scanBeam.target,ox=this.turret.x+Math.sin(this.turret.angle)*106,oy=h-54-Math.cos(this.turret.angle)*106,beam=c.createLinearGradient(ox,oy,t.x,t.y);beam.addColorStop(0,"rgba(255,247,116,.96)");beam.addColorStop(1,"rgba(88,246,255,.35)");c.save();c.strokeStyle=beam;c.lineWidth=8;c.shadowBlur=25;c.shadowColor="#6df6ff";c.beginPath();c.moveTo(ox,oy);c.lineTo(t.x,t.y);c.stroke();c.restore();}if(scanner?.complete&&scanner.naturalWidth){const sw=scanner.naturalWidth,sh=scanner.naturalHeight;c.save();c.shadowBlur=22;c.shadowColor="#54efff";c.drawImage(scanner,sw*.15,sh*.72,sw*.7,sh*.28,this.turret.x-78,h-61,156,61);c.translate(this.turret.x,h-48);c.rotate(this.turret.angle);c.drawImage(scanner,sw*.18,0,sw*.64,sh*.78,-57,-154,114,154);c.restore();}if(this.pointer.x){c.strokeStyle="rgba(230,255,255,.9)";c.lineWidth=3;c.beginPath();c.arc(this.pointer.x,this.pointer.y,20,0,7);c.moveTo(this.pointer.x-29,this.pointer.y);c.lineTo(this.pointer.x-10,this.pointer.y);c.moveTo(this.pointer.x+10,this.pointer.y);c.lineTo(this.pointer.x+29,this.pointer.y);c.moveTo(this.pointer.x,this.pointer.y-29);c.lineTo(this.pointer.x,this.pointer.y-10);c.moveTo(this.pointer.x,this.pointer.y+10);c.lineTo(this.pointer.x,this.pointer.y+29);c.stroke();}} // Final scanner rendering: generated 3D crystals and device aligned to the sanctuary pedestal. drawBlaster(){this.cover("dino-world","rgba(8,39,29,.13)",0);const c=this.ctx,w=this.canvas.width,h=this.canvas.height,scanner=images["dino-blaster"],crystal=images["dino-crystal"],deviceBottom=h*.86;c.fillStyle="rgba(9,36,35,.06)";c.fillRect(0,0,w,h);for(let i=0;i<10;i++){const x=38+i*Math.min(34,(w-76)/10),active=i<this.wave;c.beginPath();c.arc(x,h-30,9,0,7);c.fillStyle=active?"#64f6ff":"rgba(255,255,255,.36)";c.shadowBlur=active?15:0;c.shadowColor="#73f8ff";c.fill();}c.shadowBlur=0;this.targets.forEach(t=>{c.save();c.translate(t.x,t.y);const scale=.82+.18*t.enter,glow=t.pulse>0?"#fff66b":t.pulse<0?"#ff745e":"#64f4ff";c.scale(scale,scale);c.shadowBlur=30;c.shadowColor=glow;if(crystal?.complete&&crystal.naturalWidth)c.drawImage(crystal,-t.r*.83,-t.r*1.22,t.r*1.66,t.r*2.44);c.shadowBlur=0;this.roundRect(-t.r*.98,-18,t.r*1.96,42,13,t.colour||"rgba(255,253,224,.96)",t.pulse<0?"#e35d4d":"#174d68");if(!t.colour)this.text(t.answer,0,3,clamp(24-String(t.answer).length*.45,15,23),"#17384b");c.restore();});if(this.scanBeam){const t=this.scanBeam.target,ox=this.turret.x+Math.sin(this.turret.angle)*106,oy=deviceBottom-54-Math.cos(this.turret.angle)*106,beam=c.createLinearGradient(ox,oy,t.x,t.y);beam.addColorStop(0,"rgba(255,247,116,.98)");beam.addColorStop(1,"rgba(88,246,255,.35)");c.save();c.strokeStyle=beam;c.lineWidth=8;c.shadowBlur=25;c.shadowColor="#6df6ff";c.beginPath();c.moveTo(ox,oy);c.lineTo(t.x,t.y);c.stroke();c.globalAlpha=.4;c.lineWidth=18;c.stroke();c.restore();}if(scanner?.complete&&scanner.naturalWidth){const sw=scanner.naturalWidth,sh=scanner.naturalHeight;c.save();c.shadowBlur=22;c.shadowColor="#54efff";c.drawImage(scanner,sw*.15,sh*.72,sw*.7,sh*.28,this.turret.x-78,deviceBottom-61,156,61);c.translate(this.turret.x,deviceBottom-48);c.rotate(this.turret.angle);c.drawImage(scanner,sw*.18,0,sw*.64,sh*.78,-57,-154,114,154);c.restore();}if(this.pointer.x){c.strokeStyle="rgba(230,255,255,.9)";c.lineWidth=3;c.beginPath();c.arc(this.pointer.x,this.pointer.y,20,0,7);c.moveTo(this.pointer.x-29,this.pointer.y);c.lineTo(this.pointer.x-10,this.pointer.y);c.moveTo(this.pointer.x+10,this.pointer.y);c.lineTo(this.pointer.x+29,this.pointer.y);c.moveTo(this.pointer.x,this.pointer.y-29);c.lineTo(this.pointer.x,this.pointer.y-10);c.moveTo(this.pointer.x,this.pointer.y+10);c.lineTo(this.pointer.x,this.pointer.y+29);c.stroke();}} // WORD CUP — three real goal zones and predictable child-friendly physics. initHockey(){this.hq=0;this.hockeyLocked=false;this.newHockeyTask();this.puck={x:this.canvas.width/2,y:this.canvas.height*.62,vx:0,vy:0,r:clamp(this.canvas.width*.018,15,21)};this.mallet={x:this.canvas.width/2,y:this.canvas.height-70,px:this.canvas.width/2,py:this.canvas.height-70,r:clamp(this.canvas.width*.038,30,42)};this.help.textContent="Ударь шайбу в ворота с правильным ответом";} newHockeyTask(){if(this.destroyed)return;const t=this.task3({prompt:"She ___ to school every day.",options:["goes","go","going"],answer:"goes"});this.hockeyTask={prompt:t.prompt,answers:t.options,answer:t.answer,optionStyle:t.optionStyle};this.objective.textContent=t.prompt;this.hockeyLocked=false;this.hockeyReadUntil=performance.now()+1700;} hockeyDown(){this.hockeyMove();} hockeyMove(){if(!this.mallet)return;this.mallet.px=this.mallet.x;this.mallet.py=this.mallet.y;this.mallet.x=clamp(this.pointer.x,this.mallet.r+16,this.canvas.width-this.mallet.r-16);this.mallet.y=clamp(this.pointer.y,this.canvas.height*.52,this.canvas.height-this.mallet.r-12);} resetPuck(next=false){this.puck.x=this.canvas.width/2;this.puck.y=this.canvas.height*.62;this.puck.vx=0;this.puck.vy=0;if(next&&this.hq<8)setTimeout(()=>this.newHockeyTask(),650);else if(!next)this.hockeyLocked=false;} updateHockey(dt){const p=this.puck,m=this.mallet,w=this.canvas.width,h=this.canvas.height,goalY=92;p.x+=p.vx*dt;p.y+=p.vy*dt;const drag=Math.pow(.994,dt*60);p.vx*=drag;p.vy*=drag;if(p.x<p.r+12||p.x>w-p.r-12){p.vx*=-.94;p.x=clamp(p.x,p.r+12,w-p.r-12);this.sound("hit");}if(p.y>h-p.r-10){p.vy=-Math.abs(p.vy)*.9;p.y=h-p.r-10;}if(!this.hockeyLocked&&performance.now()>this.hockeyReadUntil&&dist(p,m)<p.r+m.r){const dx=p.x-m.x,dy=p.y-m.y,n=Math.hypot(dx,dy)||1,motion=Math.hypot(m.x-m.px,m.y-m.py),s=clamp(motion*25+320,320,680);p.vx=dx/n*s;p.vy=Math.min(-180,dy/n*s);p.x=m.x+dx/n*(p.r+m.r+2);p.y=m.y+dy/n*(p.r+m.r+2);this.sound("hit");}if(!this.hockeyLocked&&p.y<goalY){const index=clamp(Math.floor(p.x/(w/3)),0,2);this.hockeyLocked=true;if(this.hockeyTask.answers[index]===this.hockeyTask.answer){this.success("Гол!");this.hq++;this.resetPuck(true);}else{this.error(`Верный ответ: ${this.hockeyTask.answer}`);this.resetPuck(false);}}if(Math.hypot(p.vx,p.vy)<8&&p.y<h*.55&&performance.now()>this.hockeyReadUntil+6500)this.resetPuck(false);this.progress.textContent=`Голы: ${Math.min(this.hq,8)}/8${this.hq>=3?" · Звёздная серия!":""}`;} drawHockey(){this.cover("sport-world","rgba(2,28,64,.38)");const c=this.ctx,w=this.canvas.width,h=this.canvas.height,top=92;c.save();const ice=c.createLinearGradient(0,top,0,h);ice.addColorStop(0,"rgba(209,249,255,.86)");ice.addColorStop(1,"rgba(91,202,226,.78)");this.roundRect(12,top-8,w-24,h-top-4,30,ice,"#eaffff");c.strokeStyle="rgba(255,255,255,.82)";c.lineWidth=5;c.beginPath();c.moveTo(16,h*.52);c.lineTo(w-16,h*.52);c.stroke();c.beginPath();c.arc(w/2,h*.67,58,0,7);c.stroke();const colours=[["#ffbf35","#8b3f12"],["#66e2ff","#145c98"],["#d087ff","#5a2790"]];for(let i=0;i<3;i++){const x=i*w/3+8,gw=w/3-16,active=this.hockeyLocked&&this.hockeyTask.answers[i]===this.hockeyTask.answer;c.shadowBlur=active?30:12;c.shadowColor=colours[i][0];this.roundRect(x,15,gw,76,18,active?"#fff8a8":"rgba(250,253,255,.94)",colours[i][0]);c.shadowBlur=0;this.text(this.hockeyTask.answers[i],x+gw/2,53,clamp(24-String(this.hockeyTask.answers[i]).length*.45,14,22),"#17384c");c.fillStyle=colours[i][1];c.fillRect(x+8,88,gw-16,10);}c.beginPath();c.arc(this.puck.x,this.puck.y,this.puck.r,0,7);const pg=c.createRadialGradient(this.puck.x-6,this.puck.y-7,2,this.puck.x,this.puck.y,this.puck.r);pg.addColorStop(0,"#8cecff");pg.addColorStop(.35,"#274d6c");pg.addColorStop(1,"#0b1725");c.fillStyle=pg;c.shadowBlur=16;c.shadowColor="#79eaff";c.fill();c.shadowBlur=0;c.beginPath();c.arc(this.mallet.x,this.mallet.y,this.mallet.r,0,7);const mg=c.createRadialGradient(this.mallet.x-10,this.mallet.y-12,3,this.mallet.x,this.mallet.y,this.mallet.r);mg.addColorStop(0,"#fff4b5");mg.addColorStop(.35,"#ff7745");mg.addColorStop(1,"#a32132");c.fillStyle=mg;c.fill();c.strokeStyle="#fff1b7";c.lineWidth=6;c.stroke();if(this.puck.vx===0&&this.puck.vy===0&&performance.now()>this.hockeyReadUntil){c.setLineDash([8,10]);c.strokeStyle="rgba(255,255,255,.8)";c.lineWidth=4;c.beginPath();c.moveTo(this.puck.x,this.puck.y-25);c.lineTo(this.puck.x,this.puck.y-125);c.stroke();c.setLineDash([]);}c.restore();} // TREASURE BAY — common learning tasks, fair wind and ballistic aiming. initPirate(){this.pq=0;this.wind=(Math.random()-.5)*12;this.charge=0;this.ball=null;this.misses=0;this.cannon={x:clamp(this.canvas.width*.12,72,120),y:this.canvas.height-82,angle:-.72};this.makeShips();this.help.textContent="Наведи пушку, удерживай для силы и отпусти — попади в верный ответ";} makeShips(){if(this.destroyed)return;const q=this.task3({prompt:"Choose the correct word.",options:["map","mep","mat"],answer:"map"}),w=this.canvas.width,h=this.canvas.height,spots=[[.43,.39],[.66,.31],[.84,.43]];this.pirateTask=q;this.objective.textContent=q.prompt;this.wind=(Math.random()-.5)*12;this.ships=q.options.map((answer,i)=>({x:w*spots[i][0],y:clamp(h*spots[i][1],150,h*.5),w:clamp(w*.14,105,145),h:88,answer,colour:q.optionStyle==="colour"?colourValues[answer]:null,correct:answer===q.answer,shield:0,bob:i*1.9}));this.pirateReadUntil=performance.now()+1800;} pirateAim(){if(!this.pointer.down&&!this.ball){const dx=this.pointer.x-this.cannon.x,dy=this.pointer.y-this.cannon.y;this.cannon.angle=clamp(Math.atan2(dy,dx),-1.32,-.18);}} pirateDown(){if(!this.ball&&performance.now()>this.pirateReadUntil){this.charging=true;this.charge=Math.max(this.charge,28);}} pirateFire(){if(!this.charging||this.ball)return;this.charging=false;const speed=430+this.charge*3.5;this.ball={x:this.cannon.x+Math.cos(this.cannon.angle)*68,y:this.cannon.y+Math.sin(this.cannon.angle)*68,vx:Math.cos(this.cannon.angle)*speed,vy:Math.sin(this.cannon.angle)*speed,r:12};this.charge=0;this.smoke=.5;this.sound("launch");} updatePirate(dt){if(this.charging)this.charge=Math.min(100,this.charge+48*dt);this.ships.forEach(s=>{s.drawY=s.y+Math.sin(this.time*1.4+s.bob)*5;s.shield=Math.max(0,s.shield-dt);});if(this.ball){const b=this.ball;b.vx+=this.wind*dt;b.vy+=340*dt;b.x+=b.vx*dt;b.y+=b.vy*dt;for(const s of this.ships)if(!b.dead&&b.x>s.x-s.w*.52&&b.x<s.x+s.w*.52&&b.y>(s.drawY||s.y)-s.h*.48&&b.y<(s.drawY||s.y)+s.h*.58){b.dead=true;if(s.correct){this.success("Карта найдена!");this.pq++;this.misses=0;this.ships=[];if(this.pq<8)setTimeout(()=>this.makeShips(),850);}else{this.error(`Верный ответ: ${this.pirateTask.answer}`);s.shield=.9;}break;}if(b.y>this.canvas.height-22||b.x>this.canvas.width+35||b.x<-35){b.dead=true;this.misses++;this.splash={x:clamp(b.x,20,this.canvas.width-20),y:Math.min(b.y,this.canvas.height-24),t:.7};}if(b.dead)this.ball=null;}if(this.splash)this.splash.t-=dt;this.progress.textContent=`Карта сокровищ: ${Math.min(this.pq,8)}/8 · Ветер ${this.wind>=0?"→":"←"} ${Math.round(Math.abs(this.wind))}`;} drawPirate(){this.cover("pirate-world","rgba(5,33,48,.16)");const c=this.ctx,w=this.canvas.width,h=this.canvas.height;c.fillStyle="rgba(3,88,115,.32)";c.fillRect(0,h*.52,w,h*.48);for(let i=0;i<8;i++){const x=w-28-i*28,y=h-31;c.beginPath();c.arc(x,y,9,0,7);c.fillStyle=i<this.pq?"#ffd94b":"rgba(255,255,255,.35)";c.fill();}this.ships.forEach(s=>{const y=s.drawY||s.y;c.save();c.shadowBlur=18;c.shadowColor="rgba(0,25,35,.55)";this.image("pirate-ship",s.x,y,s.w,s.h);c.shadowBlur=0;this.roundRect(s.x-s.w*.48,y-s.h*.58,s.w*.96,42,12,s.shield?"#bff8ff":"rgba(255,246,204,.96)",s.shield?"#5ce9ff":"#8b531e");this.text(s.answer,s.x,y-s.h*.58+21,clamp(22-String(s.answer).length*.4,14,20),"#17394a");if(s.shield){c.beginPath();c.arc(s.x,y,s.w*.58,0,7);c.strokeStyle="#7af2ff";c.lineWidth=8;c.stroke();}c.restore();});c.save();c.translate(this.cannon.x,this.cannon.y);c.rotate(this.cannon.angle);c.shadowBlur=14;c.shadowColor="#ffc14a";this.image("cannon",43,0,112,82);c.restore();const speed=430+this.charge*3.5;c.strokeStyle="rgba(255,255,222,.8)";c.lineWidth=4;c.setLineDash([7,10]);c.beginPath();c.moveTo(this.cannon.x,this.cannon.y);const guide=this.misses>=2?.78:.52;for(let t=0;t<guide;t+=.045){const x=this.cannon.x+Math.cos(this.cannon.angle)*speed*t+.5*this.wind*t*t,y=this.cannon.y+Math.sin(this.cannon.angle)*speed*t+.5*340*t*t;c.lineTo(x,y);}c.stroke();c.setLineDash([]);if(this.ball){c.beginPath();c.arc(this.ball.x,this.ball.y,this.ball.r,0,7);c.fillStyle="#202535";c.shadowBlur=10;c.shadowColor="#ffcc65";c.fill();c.shadowBlur=0;}if(this.splash?.t>0){c.strokeStyle="#eaffff";c.lineWidth=7;c.beginPath();c.arc(this.splash.x,this.splash.y,50*(1-this.splash.t),Math.PI,Math.PI*2);c.stroke();}this.roundRect(20,22,210,32,14,"rgba(0,31,44,.72)","#dffcff");this.roundRect(25,27,this.charge*2,22,10,"#ffd447");this.text("СИЛА",125,38,14,"#fff");this.roundRect(w-180,20,150,42,15,"rgba(255,248,211,.92)","#d49a35");this.text(`Ветер ${this.wind>=0?"→":"←"} ${Math.round(Math.abs(this.wind))}`,w-105,41,17,"#633b16");} } let active=null; window.GameArcade={mount(host,id,hooks){active?.destroy();active=new Arcade(host,id,hooks);return active;},pause(){active?.pause();},resume(){active?.resume();},destroy(){active?.destroy();active=null;},get active(){return active;}}; })();