// ========== MOVEMENT ========== // Extends Game.prototype — must be loaded after game.js Object.assign(Game.prototype, { // -------- ENTITY MOVEMENT SYSTEM -------- // Core idea: An entity is either AT a tile center (moving=false) // or IN TRANSIT to the next tile (moving=true). // When at center, pick a direction. Then move pixel-by-pixel toward the // next tile center in that direction. Once the center is reached or passed, // snap to it and set moving=false again. _moveEntity(ent, spd, pickDir){ if(!ent.moving){ const dir = pickDir(ent); if(dir){ ent.dir = dir; ent.moving = true; } return; } const dx = DC[ent.dir] * spd; const dy = DR[ent.dir] * spd; ent.x += dx; ent.y += dy; const tgtC = ent.gc + DC[ent.dir]; const tgtR = ent.gr + DR[ent.dir]; const tgtX = tgtC * TILE; const tgtY = tgtR * TILE + HUD_TOP; let reached = false; if(ent.dir==='right' && ent.x >= tgtX) reached=true; if(ent.dir==='left' && ent.x <= tgtX) reached=true; if(ent.dir==='down' && ent.y >= tgtY) reached=true; if(ent.dir==='up' && ent.y <= tgtY) reached=true; if(reached){ ent.gc = tgtC; ent.gr = tgtR; ent.x = tgtC * TILE; ent.y = tgtR * TILE + HUD_TOP; ent.moving = false; } }, // -------- PLAYER MOVEMENT -------- _movePl(wantDir){ const p = this.pl; const spd = p.spd; this._moveEntity(p, spd, (ent) => { // This function is called when player is at tile center. // Return the direction to move, or null to stay. // Try the queued direction (allows turning at the next opening) if(wantDir){ const nr = ent.gr + DR[wantDir]; const nc = ent.gc + DC[wantDir]; if(plOk(nr, nc)){ ent.lastFace = wantDir; return wantDir; } // Queued turn not yet possible — keep moving straight const nr2 = ent.gr + DR[ent.dir]; const nc2 = ent.gc + DC[ent.dir]; if(plOk(nr2, nc2)) return ent.dir; } // No input yet or fully blocked — stay put return null; }); // Handle tunnel wrap AFTER movement if(p.gr === T_ROW){ if(p.gc < 0){ p.gc = COLS-1; p.x = p.gc*TILE; p.gr = T_ROW; p.y = T_ROW*TILE+HUD_TOP; p.moving = false; this._onTun(); return; } if(p.gc >= COLS){ p.gc = 0; p.x = 0; p.gr = T_ROW; p.y = T_ROW*TILE+HUD_TOP; p.moving = false; this._onTun(); return; } } // Allow changing direction mid-transit if the player wants to reverse (180°) if(p.moving && wantDir && wantDir === OPP[p.dir]){ p.dir = wantDir; p.lastFace = wantDir; } // Allow turning at intersections mid-transit if close to grid on perpendicular axis if(p.moving && wantDir && wantDir !== p.dir && wantDir !== OPP[p.dir]){ const cx = p.gc * TILE; const cy = p.gr * TILE + HUD_TOP; const isHoriz = (p.dir==='left'||p.dir==='right'); const aligned = isHoriz ? Math.abs(p.y - cy) < 1 : Math.abs(p.x - cx) < 1; if(aligned){ const snapC = Math.round(p.x / TILE); const snapR = Math.round((p.y - HUD_TOP) / TILE); const nr = snapR + DR[wantDir]; const nc = snapC + DC[wantDir]; if(plOk(nr, nc)){ p.gc = snapC; p.gr = snapR; p.x = snapC * TILE; p.y = snapR * TILE + HUD_TOP; p.dir = wantDir; p.lastFace = wantDir; } } } if(wantDir) p.lastFace = wantDir; // Mouth anim if(p.moving){ p.mouth+=.15*p.mouthD;if(p.mouth>1){p.mouth=1;p.mouthD=-1}if(p.mouth<0){p.mouth=0;p.mouthD=1} } // Eat dots for(const d of this.dots)if(!d.e&&d.r===p.gr&&d.c===p.gc){d.e=true;this.eDots++;this.lsc++;if(this.fr%4===0)this.snd.bite()} // Eat cystine for(const cs of this.cysts)if(cs.v&&!cs.e&&cs.r===p.gr&&cs.c===p.gc){ cs.e=true;cs.v=false;this.ateSt=true;this.lsc+=10;this.snd.laser(); this.fx.push({x:p.x+TILE/2,y:p.y+TILE/2,co:'#FFD700',t:30,mx:30,tp:'glow'}); this.cysts.forEach(x=>{if(!x.e)x.v=false});this._sBio(); } // Eat bio for(let i=this.bios.length-1;i>=0;i--){const b=this.bios[i];if(b.r===p.gr&&b.c===p.gc){ this.bios.splice(i,1);this.ateBi=true;this.lsc+=10;this.snd.laser(); this.fx.push({x:p.x+TILE/2,y:p.y+TILE/2,co:'#FFD700',t:30,mx:30,tp:'glow'}); }} }, _onTun(){ if(this.ateSt&&this.ateBi&&!this.sup){ this._sSup();this.ateSt=false;this.ateBi=false;this.combD++;this.lsc+=20; } }, _sBio(){ const cs=[];for(let r=1;r5)cs.push({r,c}); if(cs.length)this.bios.push(cs[0|Math.random()*cs.length]); }, _sSup(){ this.sup=true;this.supT=SUPER_SEC*FPS;this.snd.laser(); this.pains.forEach(p=>p.scared=true); this.fx.push({x:this.pl.x+TILE/2,y:this.pl.y+TILE/2,co:CO.sup,t:40,mx:40,tp:'glow'}); }, _esup(){ this.sup=false; PH.forEach((h,i)=>{const p=this.pains[i];p.scared=false;p.eaten=false;p.inHouse=true;p.leaving=false;p.x=h.c*TILE;p.y=h.r*TILE+HUD_TOP;p.gc=h.c;p.gr=h.r;p.relT=i*60+30;p.moving=false}); if(this.combD!c.e);const pos=this._rp(rem.length,1,20);rem.forEach((cs,i)=>{if(pos[i]){cs.r=pos[i].r;cs.c=pos[i].c;cs.v=true}})} }, // -------- PAIN MOVEMENT -------- _movePains(){ for(const p of this.pains){ if(p.eaten)continue; p.pulse+=.06; // In house if(p.inHouse){ p.relT--; if(p.relT<=0){ p.inHouse=false;p.leaving=true; p.gc=13;p.gr=9;p.x=p.gc*TILE;p.y=p.gr*TILE+HUD_TOP; p.dir='up';p.moving=false; } continue; } // Leaving house — move upward through door to row 7 if(p.leaving){ p.y-=p.spd; const tgtY=7*TILE+HUD_TOP; if(p.y<=tgtY){ p.gr=7;p.gc=13;p.x=p.gc*TILE;p.y=p.gr*TILE+HUD_TOP; p.leaving=false;p.moving=false;p.dir='left'; } continue; } // Normal movement using the entity system const spd=p.scared?p.spd*.55:p.spd; this._moveEntity(p, spd, (ent)=>{ const valid=DIRS.filter(d=>{ const nr=ent.gr+DR[d], nc=ent.gc+DC[d]; return painOk(nr,nc); }); const fwd=valid.filter(d=>d!==OPP[ent.dir]); const ch=fwd.length>0?fwd:valid; if(ch.length===0)return null; return this._ppd(ent,ch); }); // Tunnel wrap if(p.gc<0){p.gc=COLS-1;p.x=p.gc*TILE;p.moving=false} if(p.gc>=COLS){p.gc=0;p.x=0;p.moving=false} } }, _ppd(pain,ch){ const pr=this.pl.gr,pc=this.pl.gc; const dist=d=>{const nr=pain.gr+DR[d],nc=pain.gc+DC[d];return Math.abs(nr-pr)+Math.abs(nc-pc)}; if(pain.scared)return ch.reduce((a,b)=>dist(a)>dist(b)?a:b); switch(pain.strat){ case'chase':return ch.reduce((a,b)=>dist(a){const nr=pain.gr+DR[d],nc=pain.gc+DC[d];return Math.abs(nr-ar)+Math.abs(nc-ac)};return ch.reduce((a,b)=>ad(a)dist(a)