// ========== 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; 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; 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+=SC_DOT;if(this.fr%4===0)this.snd.bite()} // Pick up med if(!this.hasMed){for(const m of this.meds){if(!m.eaten&&m.r===p.gr&&m.c===p.gc){ m.eaten=true;this.hasMed=true;this.snd.laser(); this.fx.push({x:p.x+TILE/2,y:p.y+TILE/2,co:'#EC4899',t:30,mx:30,tp:'glow'}); }}} // Deliver med to patient if(this.hasMed&&this.patient&&p.gr===this.patient.r&&p.gc===this.patient.c){ this._succChall(); } }, // -------- 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*GHOST_SCARED_MULT: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)