/* ======================================== CYST-KID — Game Core ======================================== */ class Game{ constructor(){ this.cv=document.getElementById('gameCanvas'); this.cx=this.cv.getContext('2d'); this.snd=new Snd(); this.cv.width=CW;this.cv.height=CH; this.state='start'; this.lv=1;this.lives=3;this.sc=0;this.lsc=0; this.lt=0;this.ld=0;this.tt=0;this.td=0; this.dots=[];this.nDots=0;this.eDots=0; this.pl=null;this.pains=[];this.cysts=[];this.bios=[]; this.ateSt=false;this.ateBi=false;this.combD=0;this.combN=1; this.sup=false;this.supT=0; this.fx=[];this.fr=0;this.lastT=0;this.acc=0; // Input this.ks={up:false,down:false,left:false,right:false,shift:false}; this.swD=null; this.rank=JSON.parse(localStorage.getItem('ckr3')||'[]'); this._inp();this._ui();this._rsz(); window.addEventListener('resize',()=>this._rsz()); this._rf=this._loop.bind(this);requestAnimationFrame(this._rf); } _rsz(){ const s=Math.min(window.innerWidth*.95/CW,window.innerHeight*.95/CH,2.5); this.cv.style.width=CW*s+'px';this.cv.style.height=CH*s+'px'; } // ---------- INPUT ---------- _inp(){ window.addEventListener('keydown',e=>{ let h=true; switch(e.code){ case'ArrowUp':case'KeyW':this.ks.up=true;break; case'ArrowDown':case'KeyS':this.ks.down=true;break; case'ArrowLeft':case'KeyA':this.ks.left=true;break; case'ArrowRight':case'KeyD':this.ks.right=true;break; case'ShiftLeft':case'ShiftRight':this.ks.shift=true;break; case'Escape':this._esc();break; case'Enter':case'NumpadEnter':this._ent();break; default:h=false; } if(h)e.preventDefault(); }); window.addEventListener('keyup',e=>{ switch(e.code){ case'ArrowUp':case'KeyW':this.ks.up=false;break; case'ArrowDown':case'KeyS':this.ks.down=false;break; case'ArrowLeft':case'KeyA':this.ks.left=false;break; case'ArrowRight':case'KeyD':this.ks.right=false;break; case'ShiftLeft':case'ShiftRight':this.ks.shift=false;break; } }); let tx=0,ty=0; this.cv.addEventListener('touchstart',e=>{e.preventDefault();tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:false}); this.cv.addEventListener('touchmove',e=>{e.preventDefault();const dx=e.touches[0].clientX-tx,dy=e.touches[0].clientY-ty;if(Math.abs(dx)+Math.abs(dy)<12)return;this.swD=Math.abs(dx)>Math.abs(dy)?(dx>0?'right':'left'):(dy>0?'down':'up');tx=e.touches[0].clientX;ty=e.touches[0].clientY},{passive:false}); this._gpi=null; window.addEventListener('gamepadconnected',e=>{this._gpi=e.gamepad.index}); window.addEventListener('gamepaddisconnected',e=>{if(this._gpi===e.gamepad.index)this._gpi=null}); // Virtual joystick — global touch tracking so dragging outside the circle keeps working const base=document.getElementById('joystick-base'); const knob=document.getElementById('joystick-knob'); if(base&&knob){ const MAX=39; // max knob travel px const DEAD=10; // deadzone px let joyId=null, cx=0, cy=0; // active touch id and fixed center coords const moveKnob=(dx,dy)=>{ const dist=Math.hypot(dx,dy); const scale=Math.min(dist,MAX)/(dist||1); knob.style.transform=`translate(calc(-50% + ${dx*scale}px), calc(-50% + ${dy*scale}px))`; }; const applyDir=(dx,dy)=>{ this.ks.up=this.ks.down=this.ks.left=this.ks.right=false; if(Math.hypot(dx,dy)Math.abs(dy)){this.ks[dx>0?'right':'left']=true} else{this.ks[dy>0?'down':'up']=true} }; const reset=()=>{ joyId=null; this.ks.up=this.ks.down=this.ks.left=this.ks.right=false; knob.style.transform='translate(-50%, -50%)'; }; // touchstart only on the base — anchors the joystick center base.addEventListener('touchstart',e=>{ e.preventDefault(); if(joyId!==null)return; // ignore second finger const t=e.changedTouches[0]; joyId=t.identifier; const r=base.getBoundingClientRect(); cx=r.left+r.width/2; cy=r.top+r.height/2; const dx=t.clientX-cx, dy=t.clientY-cy; moveKnob(dx,dy); applyDir(dx,dy); },{passive:false}); // move and end tracked globally so dragging outside the element keeps working document.addEventListener('touchmove',e=>{ if(joyId===null)return; const t=Array.from(e.changedTouches).find(x=>x.identifier===joyId); if(!t)return; e.preventDefault(); const dx=t.clientX-cx, dy=t.clientY-cy; moveKnob(dx,dy); applyDir(dx,dy); },{passive:false}); document.addEventListener('touchend',e=>{ if(joyId===null)return; if(Array.from(e.changedTouches).some(x=>x.identifier===joyId))reset(); }); document.addEventListener('touchcancel',e=>{ if(joyId===null)return; if(Array.from(e.changedTouches).some(x=>x.identifier===joyId))reset(); }); } } _gp(){if(this._gpi===null)return{};const g=navigator.getGamepads()[this._gpi];if(!g)return{};let d=null;const z=.3;if(Math.abs(g.axes[0])>Math.abs(g.axes[1])){if(g.axes[0]<-z)d='left';else if(g.axes[0]>z)d='right'}else{if(g.axes[1]<-z)d='up';else if(g.axes[1]>z)d='down'}if(g.buttons[0]?.pressed)this._ent();if(g.buttons[8]?.pressed)this._esc();return{dir:d,sprint:g.buttons[5]?.pressed||g.buttons[7]?.pressed}} // Which direction key is currently held? _dir(){ let d=null; if(this.ks.up)d='up'; if(this.ks.down)d='down'; if(this.ks.left)d='left'; if(this.ks.right)d='right'; if(this.swD){d=this.swD;this.swD=null} const gp=this._gp(); if(gp.dir)d=gp.dir; return d; } // ---------- UI BUTTON BINDINGS ---------- _ui(){ document.getElementById('startBtn').onclick=()=>{this.snd.init();this._start()}; document.getElementById('nextLevelBtn').onclick=()=>this._ent(); document.getElementById('restartBtn').onclick=()=>{this.sc=0;this._start()}; document.getElementById('submitScoreBtn').onclick=()=>this._sub(); document.getElementById('playAgainBtn').onclick=()=>this._ov('start'); } _esc(){if(this.state==='playing'){this.sc=0;this.lsc=0;this.state='start';this._ov('start')}} _ent(){ if(this.state==='lvlDone'){this.lv++;this._init();this.state='playing';this._ov(null)} else if(this.state==='over'){this.sc=0;this._start()} else if(this.state==='start'){this.snd.init();this._start()} } // ---------- OVERLAY / SCREEN MANAGER ---------- _ov(n){ ['start-screen','level-complete-screen','game-over-screen','win-screen'].forEach(id=>document.getElementById(id).style.display='none'); if(n==='start') this._showStart(); if(n==='lvlDone') this._showLvlDone(); if(n==='over') this._showGameOver(); if(n==='win') this._showWin(); } // ---------- LIFECYCLE ---------- _init(){ this.dots=[];this.nDots=0;this.eDots=0; for(let r=0;rthis.pains.push({ x:h.c*TILE, y:h.r*TILE+HUD_TOP, gc:h.c, gr:h.r, dir:'up', moving:false, spd:1.2+this.lv*0.2, strat:st[i], scared:false, eaten:false, inHouse:true, leaving:false, relT:i*80+50, pulse:Math.random()*6.28 })); this.cysts=[]; this._rp(this.lv,1,8).forEach(p=>this.cysts.push({r:p.r,c:p.c,e:false,v:true})); this.bios=[]; } _rp(n,mr,xr){ const cs=[];for(let r=mr;r<=xr;r++)for(let c=1;c0;i--){const j=0|Math.random()*(i+1);[cs[i],cs[j]]=[cs[j],cs[i]]} const o=[];for(const p of cs){if(o.length>=n)break;if(o.every(q=>Math.abs(q.r-p.r)+Math.abs(q.c-p.c)>=4))o.push(p)}return o; } // -------- MAIN LOOP -------- _loop(ts){ if(!this.lastT)this.lastT=ts; const dt=ts-this.lastT;this.lastT=ts; if(this.state==='playing'){this.acc+=dt;let s=0;while(this.acc>=TICK&&s<5){this._upd();this.acc-=TICK;s++}if(this.acc>TICK*5)this.acc=0} this._draw();requestAnimationFrame(this._rf); } // -------- UPDATE -------- _upd(){ this.fr++;this.lt++; const liveDir=this._dir(); if(liveDir) this.qDir=liveDir; const wantDir=this.qDir; const base=BASE_SPD+(this.lv-1)*0.2; const gp=this._gp(); const sprint=this.ks.shift||(gp.sprint||false); this.pl.spd=this.sup?base*2:(sprint?base*2:base); this._movePl(wantDir); this._movePains(); this._coll(); if(this.sup){this.supT--;if(this.supT<=0)this._esup()} this.fx=this.fx.filter(f=>{f.t--;return f.t>0}); this._chkDone(); } // -------- COLLISIONS -------- _coll(){ for(const p of this.pains){ if(p.eaten||p.inHouse||p.leaving)continue; if(Math.abs(this.pl.x-p.x)+Math.abs(this.pl.y-p.y)this._ov('over'),600)} else{ this.pl.x=PL0.c*TILE;this.pl.y=PL0.r*TILE+HUD_TOP;this.pl.gc=PL0.c;this.pl.gr=PL0.r;this.pl.dir='right';this.pl.moving=false; this.qDir=null;this.swD=null;this.ks.up=this.ks.down=this.ks.left=this.ks.right=false; PH.forEach((h,i)=>{const p=this.pains[i];p.x=h.c*TILE;p.y=h.r*TILE+HUD_TOP;p.gc=h.c;p.gr=h.r;p.inHouse=true;p.leaving=false;p.eaten=false;p.relT=i*90+60;p.moving=false}); } } _chkDone(){ if(this.eDots>=this.nDots&&this.cysts.every(c=>c.e)&&this.combD>=this.combN){ this.sc+=this.lsc;this.tt+=this.lt;this.td+=this.ld;this.snd.lvlUp(); if(this.lv>=3){this.state='win';setTimeout(()=>this._ov('win'),800)} else{this.state='lvlDone';setTimeout(()=>this._ov('lvlDone'),800)} } } } window.addEventListener('load',()=>new Game());