Files
2026-04-16 17:20:19 +02:00

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];
}
}
});