188 lines
6.4 KiB
JavaScript
188 lines
6.4 KiB
JavaScript
// ========== 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)<dist(b)?a:b);
|
|
case'ambush':{const pdir=this.pl.dir||this.pl.lastFace||'right';const ar=pr+(DR[pdir]||0)*4,ac=pc+(DC[pdir]||0)*4;const ad=d=>{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)<ad(b)?a:b)}
|
|
case'patrol':{if(Math.abs(pain.gr-pr)+Math.abs(pain.gc-pc)<8)return ch.reduce((a,b)=>dist(a)<dist(b)?a:b);return ch[0|Math.random()*ch.length]}
|
|
default:return ch[0|Math.random()*ch.length];
|
|
}
|
|
}
|
|
|
|
});
|