Added kidney_labe and Cyste_kid
This commit is contained in:
266
Cyst_Kid/js/game.js
Normal file
266
Cyst_Kid/js/game.js
Normal file
@@ -0,0 +1,266 @@
|
||||
/* ========================================
|
||||
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());
|
||||
Reference in New Issue
Block a user