267 lines
11 KiB
JavaScript
267 lines
11 KiB
JavaScript
/* ========================================
|
|
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)<DEAD)return;
|
|
if(Math.abs(dx)>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;r<ROWS;r++)for(let c=0;c<COLS;c++)if(MAP[r][c]===0){this.dots.push({r,c,e:false});this.nDots++}
|
|
this.lsc=0;this.lt=0;this.ld=0;this.sup=false;this.supT=0;
|
|
this.ateSt=false;this.ateBi=false;this.combD=0;this.combN=this.lv;
|
|
this.fx=[];this.lives=3;this.qDir=null;
|
|
|
|
this.pl={
|
|
x:PL0.c*TILE, y:PL0.r*TILE+HUD_TOP,
|
|
gc:PL0.c, gr:PL0.r,
|
|
dir:'right', moving:false,
|
|
spd:BASE_SPD, mouth:0, mouthD:1,
|
|
lastFace:'right'
|
|
};
|
|
|
|
this.pains=[];
|
|
const st=['chase','ambush','patrol','random'];
|
|
PH.forEach((h,i)=>this.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;c<COLS-1;c++)if(isPath(r,c))cs.push({r,c});
|
|
for(let i=cs.length-1;i>0;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)<TILE*.8){
|
|
if(this.sup&&p.scared){p.eaten=true;this.lsc+=5;this.snd.bite();this.fx.push({x:p.x+TILE/2,y:p.y+TILE/2,co:'#00ff88',t:20,mx:20,tp:'burst'})}
|
|
else if(!this.sup){this._die();return}
|
|
}
|
|
}
|
|
}
|
|
|
|
_die(){
|
|
this.lives--;this.snd.boom();
|
|
this.fx.push({x:this.pl.x+TILE/2,y:this.pl.y+TILE/2,co:'#FF4444',t:40,mx:40,tp:'exp'});
|
|
if(this.lives<=0){this.lsc=0;this.state='over';this.snd.over();setTimeout(()=>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());
|