🎮ArcadeLab

RUN! Monster Chase

by BraveMaker51
568 lines24.8 KB
▶ Play
<!DOCTYPE html>

<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>RUN! Monster Chase</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Creepster&family=Rajdhani:wght@400;700&display=swap');
  * { margin:0; padding:0; box-sizing:border-box; }
  body { background:#000; overflow:hidden; font-family:'Rajdhani',sans-serif; user-select:none; }
  canvas { display:block; }
  #ui {
    position:fixed; top:0; left:0; right:0; z-index:10;
    display:flex; justify-content:space-between; align-items:flex-start;
    padding:16px 24px; pointer-events:none;
    background:linear-gradient(to bottom, rgba(0,0,0,0.7), transparent);
  }
  #score-display { font-family:'Creepster',cursive; color:#ff3366; font-size:2rem; text-shadow:0 0 20px #ff003380; letter-spacing:2px; }
  #lives-display { font-family:'Creepster',cursive; color:#cc44ff; font-size:1.8rem; }
  #minimap { position:fixed; bottom:16px; right:16px; z-index:10; border:1px solid #44004480; border-radius:4px; opacity:0.75; }
  #rage-bar-wrap { position:fixed; bottom:16px; left:50%; transform:translateX(-50%); z-index:10; display:flex; flex-direction:column; align-items:center; gap:4px; pointer-events:none; }
  #rage-label { color:#ff3344; font-size:0.75rem; letter-spacing:3px; text-transform:uppercase; opacity:0.8; }
  #rage-bar-bg { width:180px; height:8px; background:#1a001a; border-radius:4px; overflow:hidden; border:1px solid #44002280; }
  #rage-bar { height:100%; width:0%; background:linear-gradient(90deg,#440000,#ff0000); border-radius:4px; transition:width 0.4s; box-shadow:0 0 8px #ff000080; }
  #overlay { position:fixed; inset:0; z-index:20; display:flex; flex-direction:column; align-items:center; justify-content:center; background:radial-gradient(ellipse, #12000a 0%, #030003 100%); transition:opacity 0.5s; }
  #overlay h1 { font-family:'Creepster',cursive; font-size:clamp(3rem,9vw,8rem); color:#cc0022; text-shadow:0 0 40px #ff000090, 0 0 80px #ff000040, 0 5px 0 #600010; letter-spacing:8px; animation:flicker 4s infinite; margin-bottom:8px; }
  #overlay p { color:#aa66aa; font-size:1.2rem; margin-bottom:36px; letter-spacing:3px; text-transform:uppercase; opacity:0.8; }
  #start-btn { font-family:'Creepster',cursive; font-size:1.8rem; letter-spacing:4px; color:#fff; background:linear-gradient(135deg,#770099,#bb0033); border:none; padding:14px 48px; border-radius:4px; cursor:pointer; box-shadow:0 0 30px #aa003380; transition:transform 0.1s, box-shadow 0.2s; pointer-events:all; }
  #start-btn:hover { transform:scale(1.05); box-shadow:0 0 50px #cc004480; }
  #start-btn:active { transform:scale(0.97); }
  #monster-preview { margin-bottom:24px; animation:bob 2s ease-in-out infinite; }
  #final-score { font-family:'Creepster',cursive; font-size:2rem; color:#ffaa00; text-shadow:0 0 20px #ffaa0080; margin-bottom:20px; display:none; }
  #instructions { position:fixed; bottom:60px; left:50%; transform:translateX(-50%); z-index:10; color:#553355; font-size:0.8rem; letter-spacing:2px; text-transform:uppercase; pointer-events:none; white-space:nowrap; }
  @keyframes flicker { 0%,100%{opacity:1} 92%{opacity:1} 93%{opacity:0.5} 94%{opacity:1} 96%{opacity:0.6} 97%{opacity:1} }
  @keyframes bob { 0%,100%{transform:translateY(0) scale(1)} 50%{transform:translateY(-14px) scale(1.05)} }
</style>
</head>
<body>
<div id="ui">
  <div id="score-display">SCORE: 0</div>
  <div id="lives-display">❤️❤️❤️</div>
</div>
<div id="rage-bar-wrap">
  <div id="rage-label">⚠ MONSTER RAGE</div>
  <div id="rage-bar-bg"><div id="rage-bar"></div></div>
</div>
<canvas id="minimap" width="120" height="80"></canvas>
<div id="instructions">WASD / Arrow Keys — Collect crystals — Don't get caught</div>
<canvas id="c"></canvas>

<div id="overlay">
  <canvas id="monster-preview" width="120" height="120"></canvas>
  <h1>RUN!</h1>
  <p id="tagline">IT IS ALWAYS HUNGRY...</p>
  <div id="final-score"></div>
  <button id="start-btn">BEGIN YOUR DOOM</button>
</div>

<script>
const canvas = document.getElementById('c');
const ctx = canvas.getContext('2d');
const miniCanvas = document.getElementById('minimap');
const miniCtx = miniCanvas.getContext('2d');
const overlay = document.getElementById('overlay');
const scoreEl = document.getElementById('score-display');
const livesEl = document.getElementById('lives-display');
const startBtn = document.getElementById('start-btn');
const finalScoreEl = document.getElementById('final-score');
const taglineEl = document.getElementById('tagline');
const rageBar = document.getElementById('rage-bar');
const titleEl = document.querySelector('#overlay h1');
const previewCanvas = document.getElementById('monster-preview');
const previewCtx = previewCanvas.getContext('2d');

let VW, VH;
function resize(){ VW = canvas.width = window.innerWidth; VH = canvas.height = window.innerHeight; }
resize();
window.addEventListener('resize', resize);

const TILE = 48;
const MAP_COLS = 36;
const MAP_ROWS = 26;
const WORLD_W = MAP_COLS * TILE;
const WORLD_H = MAP_ROWS * TILE;
let cam = { x:0, y:0 };

const keys = {};
window.addEventListener('keydown', e => {
  keys[e.key] = true;
  if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key)) e.preventDefault();
});
window.addEventListener('keyup', e => { keys[e.key] = false; });
let touchDir = {x:0,y:0}, touchId=null, touchSX=0, touchSY=0;
canvas.addEventListener('touchstart', e=>{ const t=e.touches[0]; touchId=t.identifier; touchSX=t.clientX; touchSY=t.clientY; },{passive:true});
canvas.addEventListener('touchmove', e=>{
  for(let t of e.touches) if(t.identifier===touchId){
    const dx=t.clientX-touchSX,dy=t.clientY-touchSY,len=Math.hypot(dx,dy)||1,d=Math.min(len,70)/70;
    touchDir.x=(dx/len)*d; touchDir.y=(dy/len)*d;
  }
  e.preventDefault();
},{passive:false});
canvas.addEventListener('touchend',()=>{touchDir.x=0;touchDir.y=0;},{passive:true});

let state='menu';
let score=0,lives=3,frame=0;
let gems=[],particles=[],bloodDrops=[],footprints=[];
let shake={mag:0,t:0,x:0,y:0};
let flashRed=0;

const PLAYER_SPEED=3.8;
let player={x:0,y:0,vx:0,vy:0,r:14,angle:0,invincible:0,trail:[]};
let monster={
  x:0,y:0,vx:0,vy:0,r:28,speed:1.5,
  wobble:0,rage:0,phase:0,roarTimer:300,
  tentacleAngles:Array.from({length:6},(_,i)=>i*(Math.PI*2/6)),
  eyePulse:0,slobber:0,lastFootX:0,lastFootY:0
};
let walls=[];

function buildMap(){
  walls=[];
  for(let c=0;c<MAP_COLS;c++){
    walls.push({x:c*TILE,y:0,w:TILE,h:TILE});
    walls.push({x:c*TILE,y:(MAP_ROWS-1)*TILE,w:TILE,h:TILE});
  }
  for(let r=1;r<MAP_ROWS-1;r++){
    walls.push({x:0,y:r*TILE,w:TILE,h:TILE});
    walls.push({x:(MAP_COLS-1)*TILE,y:r*TILE,w:TILE,h:TILE});
  }
  const grid=Array.from({length:MAP_ROWS},()=>new Array(MAP_COLS).fill(0));
  const clusters=[
    [3,2,2,3],[7,2,3,2],[13,2,2,4],[18,2,4,2],[24,2,3,3],[29,2,2,4],
    [3,7,3,2],[8,6,2,4],[12,7,4,2],[18,6,2,3],[22,7,3,2],[27,6,2,4],[31,7,3,2],
    [2,12,2,4],[6,12,3,2],[11,12,2,3],[15,11,4,2],[20,12,2,4],[25,12,3,2],[30,12,2,3],
    [3,17,2,3],[7,17,4,2],[13,17,2,3],[18,17,3,2],[23,17,2,4],[28,17,2,3],[32,17,2,3],
    [4,21,3,2],[9,21,2,3],[15,21,4,2],[20,21,2,2],[25,21,3,2],[30,21,2,3],
    [16,10,4,6],
  ];
  for(let [c,r,w,h] of clusters){
    for(let dr=0;dr<h;dr++) for(let dc=0;dc<w;dc++){
      const rr=r+dr,cc=c+dc;
      if(rr>0&&rr<MAP_ROWS-1&&cc>0&&cc<MAP_COLS-1) grid[rr][cc]=1;
    }
  }
  for(let r=1;r<MAP_ROWS-1;r++) for(let c=1;c<MAP_COLS-1;c++){
    if(grid[r][c]===1) walls.push({x:c*TILE,y:r*TILE,w:TILE,h:TILE});
  }
}

function wallAt(px,py,r=16){
  for(let w of walls) if(px+r>w.x&&px-r<w.x+w.w&&py+r>w.y&&py-r<w.y+w.h) return true;
  return false;
}
function resolveWalls(obj){
  for(let w of walls){
    const cx=Math.max(w.x,Math.min(obj.x,w.x+w.w));
    const cy=Math.max(w.y,Math.min(obj.y,w.y+w.h));
    const dx=obj.x-cx,dy=obj.y-cy,d=Math.hypot(dx,dy);
    if(d<obj.r&&d>0){
      const nx=dx/d,ny=dy/d,pen=obj.r-d;
      obj.x+=nx*pen; obj.y+=ny*pen;
      const dot=obj.vx*nx+obj.vy*ny;
      if(dot<0){obj.vx-=dot*nx;obj.vy-=dot*ny;}
    }
  }
}

function spawnGem(){
  let gx,gy,tries=0;
  do{
    gx=(Math.floor(Math.random()*(MAP_COLS-2))+1)*TILE+TILE/2;
    gy=(Math.floor(Math.random()*(MAP_ROWS-2))+1)*TILE+TILE/2;
    tries++;
  } while(tries<40&&(wallAt(gx,gy,20)||Math.hypot(gx-player.x,gy-player.y)<120||Math.hypot(gx-monster.x,gy-monster.y)<100));
  gems.push({x:gx,y:gy,r:11,t:Math.random()*Math.PI*2,collected:false});
}

function burst(x,y,color,n=14){
  for(let i=0;i<n;i++){
    const a=Math.random()*Math.PI*2,s=2+Math.random()*5;
    particles.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s,life:1,color,r:3+Math.random()*4});
  }
}
function bloodBurst(x,y,n=20){
  for(let i=0;i<n;i++){
    const a=Math.random()*Math.PI*2,s=1+Math.random()*6;
    bloodDrops.push({x,y,vx:Math.cos(a)*s,vy:Math.sin(a)*s-2,life:1,r:2+Math.random()*4,splat:false});
  }
}

function startGame(){
  score=0;lives=3;frame=0;gems=[];particles=[];bloodDrops=[];footprints=[];
  buildMap();
  player.x=WORLD_W/2;player.y=WORLD_H/2;
  player.vx=0;player.vy=0;player.invincible=0;player.trail=[];
  monster.x=TILE*2;monster.y=TILE*2;
  monster.vx=0;monster.vy=0;monster.speed=1.5;monster.rage=0;monster.roarTimer=300;
  monster.phase=0;monster.eyePulse=0;monster.slobber=0;
  for(let i=0;i<8;i++) spawnGem();
  updateUI();
  overlay.style.opacity='0';
  setTimeout(()=>overlay.style.display='none',500);
  state='play';
}
startBtn.addEventListener('click',startGame);

function gameOver(){
  state='over';
  bloodBurst(player.x,player.y,40);
  flashRed=1;
  triggerShake(25,50);
  setTimeout(()=>{
    overlay.style.display='flex';overlay.style.opacity='1';
    titleEl.textContent='DEVOURED.';titleEl.style.color='#cc0000';
    taglineEl.textContent='It tasted your fear...';
    finalScoreEl.style.display='block';finalScoreEl.textContent='You scored: '+score;
    startBtn.textContent='FLEE AGAIN';
  },900);
}

function triggerShake(mag,dur){shake.mag=mag;shake.t=dur;}
function updateUI(){
  scoreEl.textContent='SCORE: '+score;
  livesEl.textContent='❤️'.repeat(Math.max(0,lives));
  rageBar.style.width=Math.min(100,monster.rage*100)+'%';
}

function loop(){
  requestAnimationFrame(loop);
  frame++;
  if(shake.t>0){shake.t--;const m=shake.mag*(shake.t/50);shake.x=(Math.random()-.5)*m;shake.y=(Math.random()-.5)*m;}
  else{shake.x=0;shake.y=0;}
  flashRed=Math.max(0,flashRed-0.03);
  ctx.save();
  ctx.translate(shake.x,shake.y);
  ctx.clearRect(-60,-60,VW+120,VH+120);
  if(state==='play') update();
  drawWorld();
  drawFootprints();
  drawBloodDrops();
  drawGems();
  drawParticles();
  if(state==='play'||state==='over'){drawPlayer();drawMonster();}
  drawDarkness();
  if(flashRed>0){ctx.fillStyle=`rgba(180,0,0,${flashRed*0.35})`;ctx.fillRect(-60,-60,VW+120,VH+120);}
  ctx.restore();
  drawMinimap();
  if(state==='menu') drawPreviewMonster();
}

function update(){
  let dx=0,dy=0;
  if(keys['ArrowLeft']||keys['a']||keys['A']) dx-=1;
  if(keys['ArrowRight']||keys['d']||keys['D']) dx+=1;
  if(keys['ArrowUp']||keys['w']||keys['W']) dy-=1;
  if(keys['ArrowDown']||keys['s']||keys['S']) dy+=1;
  dx+=touchDir.x;dy+=touchDir.y;
  const plen=Math.hypot(dx,dy)||1;
  if(dx||dy){player.vx=(dx/plen)*PLAYER_SPEED;player.vy=(dy/plen)*PLAYER_SPEED;}
  else{player.vx*=0.78;player.vy*=0.78;}
  if(Math.abs(player.vx)>0.1||Math.abs(player.vy)>0.1) player.angle=Math.atan2(player.vy,player.vx);
  player.x+=player.vx;player.y+=player.vy;
  player.x=Math.max(player.r,Math.min(WORLD_W-player.r,player.x));
  player.y=Math.max(player.r,Math.min(WORLD_H-player.r,player.y));
  resolveWalls(player);
  if(frame%3===0) player.trail.push({x:player.x,y:player.y,life:1});
  for(let t of player.trail) t.life-=0.08;
  player.trail=player.trail.filter(t=>t.life>0);
  const targetCX=player.x-VW/2,targetCY=player.y-VH/2;
  cam.x+=(targetCX-cam.x)*0.1;cam.y+=(targetCY-cam.y)*0.1;
  cam.x=Math.max(0,Math.min(WORLD_W-VW,cam.x));
  cam.y=Math.max(0,Math.min(WORLD_H-VH,cam.y));

  monster.phase+=0.06;monster.wobble+=0.05;monster.eyePulse+=0.08;monster.slobber+=0.04;
  const toPX=player.x-monster.x,toPY=player.y-monster.y;
  const toPDist=Math.hypot(toPX,toPY)||1;
  if(toPDist<200) monster.rage=Math.min(1,monster.rage+0.004);
  else if(toPDist<400) monster.rage=Math.min(1,monster.rage+0.001);
  else monster.rage=Math.max(0,monster.rage-0.002);
  const effectiveSpeed=monster.speed*(1+monster.rage*0.8);
  monster.vx+=(toPX/toPDist)*0.2*(1+monster.rage*0.5);
  monster.vy+=(toPY/toPDist)*0.2*(1+monster.rage*0.5);
  const ms=Math.hypot(monster.vx,monster.vy);
  if(ms>effectiveSpeed){monster.vx=(monster.vx/ms)*effectiveSpeed;monster.vy=(monster.vy/ms)*effectiveSpeed;}
  monster.x+=monster.vx;monster.y+=monster.vy;
  resolveWalls(monster);
  if(Math.hypot(monster.x-monster.lastFootX,monster.y-monster.lastFootY)>50){
    footprints.push({x:monster.x,y:monster.y,life:1,size:12+monster.rage*8});
    monster.lastFootX=monster.x;monster.lastFootY=monster.y;
  }
  for(let f of footprints) f.life-=0.005;
  footprints=footprints.filter(f=>f.life>0);
  monster.roarTimer--;
  if(monster.roarTimer<=0&&monster.rage>0.5){
    triggerShake(6+monster.rage*10,20);
    flashRed=0.3+monster.rage*0.3;
    monster.roarTimer=180+Math.random()*200;
    burst(monster.x,monster.y,'#ff0000',8);
  }
  monster.speed=1.5+Math.floor(score/60)*0.2;
  for(let i=0;i<monster.tentacleAngles.length;i++) monster.tentacleAngles[i]+=0.03*(i%2===0?1:-1)*(1+monster.rage*0.5);

  if(player.invincible<=0&&toPDist<player.r+monster.r-6){
    lives--;triggerShake(18,35);flashRed=0.8;
    bloodBurst(player.x,player.y,25);player.invincible=150;
    updateUI();
    if(lives<=0){gameOver();return;}
    const ang=Math.atan2(player.y-monster.y,player.x-monster.x);
    player.vx=Math.cos(ang)*10;player.vy=Math.sin(ang)*10;
  }
  if(player.invincible>0) player.invincible--;

  for(let g of gems){
    if(!g.collected&&Math.hypot(player.x-g.x,player.y-g.y)<player.r+g.r){
      g.collected=true;score+=10;burst(g.x,g.y,'#00ffcc',16);updateUI();
    }
    g.t+=0.04;
  }
  gems=gems.filter(g=>!g.collected);
  while(gems.length<6) spawnGem();
  for(let p of particles){p.x+=p.vx;p.y+=p.vy;p.vx*=0.92;p.vy*=0.92;p.life-=0.022;}
  particles=particles.filter(p=>p.life>0);
  for(let b of bloodDrops){
    if(!b.splat){b.x+=b.vx;b.y+=b.vy;b.vy+=0.3;b.life-=0.018;if(b.vy>0&&b.life<0.6)b.splat=true;}
    else b.life-=0.008;
  }
  bloodDrops=bloodDrops.filter(b=>b.life>0);
}

function drawWorld(){
  ctx.save();ctx.translate(-cam.x,-cam.y);
  const groundGrad=ctx.createRadialGradient(WORLD_W/2,WORLD_H/2,100,WORLD_W/2,WORLD_H/2,WORLD_W*0.7);
  groundGrad.addColorStop(0,'#0e0020');groundGrad.addColorStop(1,'#050008');
  ctx.fillStyle=groundGrad;ctx.fillRect(0,0,WORLD_W,WORLD_H);
  ctx.strokeStyle='rgba(40,0,60,0.3)';ctx.lineWidth=1;
  for(let c=0;c<=MAP_COLS;c++){ctx.beginPath();ctx.moveTo(c*TILE,0);ctx.lineTo(c*TILE,WORLD_H);ctx.stroke();}
  for(let r=0;r<=MAP_ROWS;r++){ctx.beginPath();ctx.moveTo(0,r*TILE);ctx.lineTo(WORLD_W,r*TILE);ctx.stroke();}
  for(let w of walls){
    ctx.save();
    ctx.shadowColor=`hsl(${280+monster.rage*60},100%,30%)`;ctx.shadowBlur=12+monster.rage*8;
    const wg=ctx.createLinearGradient(w.x,w.y,w.x+w.w,w.y+w.h);
    wg.addColorStop(0,`hsl(${270+monster.rage*40},50%,10%)`);
    wg.addColorStop(1,`hsl(${260+monster.rage*40},40%,7%)`);
    ctx.fillStyle=wg;ctx.fillRect(w.x,w.y,w.w,w.h);
    ctx.shadowBlur=0;
    ctx.strokeStyle=`rgba(${80+monster.rage*100},0,${120+monster.rage*80},0.5)`;
    ctx.lineWidth=1;ctx.strokeRect(w.x+0.5,w.y+0.5,w.w-1,w.h-1);
    ctx.restore();
  }
  const monRadGrad=ctx.createRadialGradient(monster.x,monster.y,0,monster.x,monster.y,200+monster.rage*150);
  monRadGrad.addColorStop(0,`rgba(180,0,0,${0.08+monster.rage*0.12})`);monRadGrad.addColorStop(1,'transparent');
  ctx.fillStyle=monRadGrad;ctx.fillRect(0,0,WORLD_W,WORLD_H);
  ctx.restore();
}

function drawFootprints(){
  ctx.save();ctx.translate(-cam.x,-cam.y);
  for(let f of footprints){
    ctx.globalAlpha=f.life*0.5;ctx.fillStyle='#440000';
    ctx.beginPath();ctx.ellipse(f.x,f.y,f.size*0.6,f.size,0,0,Math.PI*2);ctx.fill();
  }
  ctx.globalAlpha=1;ctx.restore();
}

function drawPlayer(){
  if(player.invincible>0&&Math.floor(player.invincible/6)%2===0) return;
  ctx.save();ctx.translate(-cam.x,-cam.y);
  for(let t of player.trail){
    ctx.globalAlpha=t.life*0.25;ctx.fillStyle='#4488ff';
    ctx.beginPath();ctx.arc(t.x,t.y,8*t.life,0,Math.PI*2);ctx.fill();
  }
  ctx.globalAlpha=1;
  ctx.translate(player.x,player.y);
  ctx.shadowColor='#4488ff';ctx.shadowBlur=player.invincible>0?30:15;
  ctx.strokeStyle='#aaccff';ctx.lineWidth=2.5;ctx.lineCap='round';
  ctx.beginPath();ctx.arc(0,-18,7,0,Math.PI*2);ctx.stroke();
  ctx.beginPath();ctx.moveTo(0,-11);ctx.lineTo(0,4);ctx.stroke();
  const ro=Math.sin(frame*0.3)*6;
  ctx.beginPath();ctx.moveTo(0,-5);ctx.lineTo(-10,2+ro);ctx.stroke();
  ctx.beginPath();ctx.moveTo(0,-5);ctx.lineTo(10,2-ro);ctx.stroke();
  ctx.beginPath();ctx.moveTo(0,4);ctx.lineTo(-8,16-ro*0.5);ctx.stroke();
  ctx.beginPath();ctx.moveTo(0,4);ctx.lineTo(8,16+ro*0.5);ctx.stroke();
  ctx.fillStyle='#fff';ctx.beginPath();ctx.arc(0,-18,3,0,Math.PI*2);ctx.fill();
  ctx.fillStyle='#000';ctx.beginPath();ctx.arc(0.5,-18,1.5,0,Math.PI*2);ctx.fill();
  ctx.restore();
}

function drawMonsterAt(cx,cy,scale,rage,ph,eyePulse,slobber,tentacleAngles,rctx){
  rctx.save();rctx.translate(cx,cy);rctx.scale(scale,scale);
  const breathe=Math.sin(ph)*4;
  const rageGlow=`hsl(${rage*20},100%,${30+rage*20}%)`;
  const aura=rctx.createRadialGradient(0,0,20,0,0,70+rage*30+breathe);
  aura.addColorStop(0,`rgba(150,0,0,${0.2+rage*0.3})`);
  aura.addColorStop(0.5,`rgba(80,0,20,${0.1+rage*0.15})`);
  aura.addColorStop(1,'transparent');
  rctx.fillStyle=aura;rctx.beginPath();rctx.arc(0,0,80+rage*30,0,Math.PI*2);rctx.fill();

  for(let i=0;i<tentacleAngles.length;i++){
    const ang=tentacleAngles[i],len=35+Math.sin(ph+i)*10+rage*15;
    rctx.save();rctx.rotate(ang);
    rctx.shadowColor=rageGlow;rctx.shadowBlur=8+rage*12;
    rctx.strokeStyle=`hsl(${300+i*10},${40+rage*40}%,${15+rage*10}%)`;
    rctx.lineWidth=6-i*0.3;rctx.lineCap='round';
    rctx.beginPath();rctx.moveTo(0,16);
    rctx.quadraticCurveTo(Math.sin(ph+i*1.5)*12,len*0.5,Math.cos(ph*0.7+i)*8,len);
    rctx.stroke();
    rctx.fillStyle=`hsl(0,${60+rage*40}%,${20+rage*20}%)`;
    rctx.beginPath();rctx.arc(Math.cos(ph*0.7+i)*8,len,4+rage*3,0,Math.PI*2);rctx.fill();
    rctx.restore();
  }

  rctx.shadowColor=rageGlow;rctx.shadowBlur=20+rage*25;
  const bodyGrad=rctx.createRadialGradient(-5,-5,2,0,0,28+breathe);
  bodyGrad.addColorStop(0,`hsl(${300+rage*20},${30+rage*30}%,${15+rage*15}%)`);
  bodyGrad.addColorStop(0.6,`hsl(${280+rage*30},${20+rage*20}%,${8+rage*10}%)`);
  bodyGrad.addColorStop(1,'hsl(260,10%,5%)');
  rctx.fillStyle=bodyGrad;rctx.beginPath();
  rctx.moveTo(0,-28-breathe);
  rctx.bezierCurveTo(22,-22,28+breathe,0,22,20);
  rctx.bezierCurveTo(10,30+breathe,-10,30+breathe,-22,20);
  rctx.bezierCurveTo(-28+breathe,0,-22,-22,0,-28-breathe);
  rctx.fill();

  rctx.fillStyle='rgba(0,0,0,0.4)';
  [[8,-10,5],[-10,-5,4],[4,8,3],[-6,12,3],[14,4,3],[-14,2,4]].forEach(([bx,by,br])=>{
    rctx.beginPath();rctx.arc(bx,by,br,0,Math.PI*2);rctx.fill();
  });

  rctx.save();
  const mouthOpen=0.3+rage*0.7+Math.sin(ph*2)*0.1;
  rctx.fillStyle='#1a0000';
  rctx.beginPath();rctx.ellipse(0,10+breathe*0.3,14+rage*6,8*mouthOpen,0,0,Math.PI*2);rctx.fill();
  rctx.fillStyle=`rgba(200,200,180,${0.7+rage*0.3})`;
  for(let t=-2;t<=2;t++){
    rctx.beginPath();rctx.moveTo(t*5.5,10+breathe*0.3-2);
    rctx.lineTo(t*5.5-3,10+breathe*0.3+7*mouthOpen);
    rctx.lineTo(t*5.5+3,10+breathe*0.3+7*mouthOpen);
    rctx.closePath();rctx.fill();
  }
  rctx.fillStyle=`rgba(180,180,160,${0.5+rage*0.3})`;
  for(let t=-1;t<=1;t++){
    rctx.beginPath();rctx.moveTo(t*7+3,10+breathe*0.3+6*mouthOpen);
    rctx.lineTo(t*7,10+breathe*0.3+13*mouthOpen);
    rctx.lineTo(t*7+6,10+breathe*0.3+13*mouthOpen);
    rctx.closePath();rctx.fill();
  }
  const slob=Math.sin(slobber);
  rctx.strokeStyle=`rgba(80,200,80,${0.4+rage*0.3})`;rctx.lineWidth=2;rctx.lineCap='round';
  rctx.beginPath();rctx.moveTo(-4,10+breathe*0.3+6*mouthOpen);rctx.lineTo(-4+slob*3,10+breathe*0.3+14*mouthOpen);rctx.stroke();
  rctx.beginPath();rctx.moveTo(5,10+breathe*0.3+5*mouthOpen);rctx.lineTo(5-slob*2,10+breathe*0.3+12*mouthOpen);rctx.stroke();
  rctx.restore();

  const eyePulseVal=0.5+Math.sin(eyePulse)*0.5;
  [[-10,-10],[10,-10]].forEach(([ex,ey],i)=>{
    rctx.fillStyle='rgba(0,0,0,0.8)';rctx.beginPath();rctx.arc(ex,ey+breathe*0.2,9,0,Math.PI*2);rctx.fill();
    rctx.shadowColor=`hsl(${i*15},100%,${50+eyePulseVal*30}%)`;rctx.shadowBlur=15+eyePulseVal*20+rage*20;
    rctx.fillStyle=`hsl(${i*15},100%,${50+eyePulseVal*30}%)`;
    rctx.beginPath();rctx.arc(ex,ey+breathe*0.2,6,0,Math.PI*2);rctx.fill();
    rctx.fillStyle='#000';rctx.beginPath();rctx.ellipse(ex,ey+breathe*0.2,2,5,0,0,Math.PI*2);rctx.fill();
    rctx.shadowBlur=0;rctx.fillStyle='rgba(255,255,255,0.8)';
    rctx.beginPath();rctx.arc(ex+2,ey+breathe*0.2-2,1.5,0,Math.PI*2);rctx.fill();
  });

  if(rage>0.3){
    const ea=(rage-0.3)/0.7;
    rctx.globalAlpha=ea;rctx.shadowColor='#ff0000';rctx.shadowBlur=25;
    rctx.fillStyle=`hsl(0,100%,${50+eyePulseVal*40}%)`;
    rctx.beginPath();rctx.arc(0,-22+breathe*0.3,5*ea,0,Math.PI*2);rctx.fill();
    rctx.fillStyle='#000';rctx.beginPath();rctx.ellipse(0,-22+breathe*0.3,1.5,4,0,0,Math.PI*2);rctx.fill();
    rctx.globalAlpha=1;
  }

  rctx.shadowColor=rageGlow;rctx.shadowBlur=8+rage*10;
  rctx.fillStyle=`hsl(${290+rage*30},${30+rage*30}%,${12+rage*12}%)`;
  [[-16,-22,6],[-6,-30,5],[6,-30,5],[16,-22,6],[0,-34,7]].forEach(([sx,sy,sh])=>{
    rctx.save();rctx.translate(sx,sy+breathe*0.4);
    rctx.beginPath();rctx.moveTo(0,-sh-rage*4);rctx.lineTo(-4,0);rctx.lineTo(4,0);rctx.closePath();rctx.fill();
    rctx.restore();
  });
  rctx.restore();
}

function drawMonster(){
  ctx.save();ctx.translate(-cam.x,-cam.y);
  drawMonsterAt(monster.x,monster.y,1+monster.rage*0.3,monster.rage,monster.phase,monster.eyePulse,monster.slobber,monster.tentacleAngles,ctx);
  ctx.restore();
}

function drawGems(){
  ctx.save();ctx.translate(-cam.x,-cam.y);
  for(let g of gems){
    if(g.collected) continue;
    ctx.save();ctx.translate(g.x,g.y+Math.sin(g.t)*5);
    ctx.shadowColor='#00ffcc';ctx.shadowBlur=18+Math.sin(g.t*2)*8;
    ctx.fillStyle='#00ffcc';
    ctx.beginPath();ctx.moveTo(0,-13);ctx.lineTo(9,-3);ctx.lineTo(6,10);ctx.lineTo(-6,10);ctx.lineTo(-9,-3);ctx.closePath();ctx.fill();
    ctx.fillStyle='rgba(255,255,255,0.5)';
    ctx.beginPath();ctx.moveTo(0,-13);ctx.lineTo(-9,-3);ctx.lineTo(0,-2);ctx.closePath();ctx.fill();
    ctx.restore();
  }
  ctx.restore();
}

function drawParticles(){
  ctx.save();ctx.translate(-cam.x,-cam.y);
  for(let p of particles){
    ctx.globalAlpha=p.life;ctx.shadowColor=p.color;ctx.shadowBlur=10;ctx.fillStyle=p.color;
    ctx.beginPath();ctx.arc(p.x,p.y,p.r*p.life,0,Math.PI*2);ctx.fill();
  }
  ctx.globalAlpha=1;ctx.restore();
}

function drawBloodDrops(){
  ctx.save();ctx.translate(-cam.x,-cam.y);
  for(let b of bloodDrops){
    ctx.globalAlpha=b.life*0.9;ctx.fillStyle=b.splat?'#330000':'#cc0000';
    ctx.shadowColor='#ff0000';ctx.shadowBlur=b.splat?0:5;
    ctx.beginPath();
    if(b.splat) ctx.ellipse(b.x,b.y,b.r*1.5,b.r*0.5,0,0,Math.PI*2);
    else ctx.arc(b.x,b.y,b.r*b.life,0,Math.PI*2);
    ctx.fill();
  }
  ctx.globalAlpha=1;ctx.restore();
}

function drawDarkness(){
  const px=player.x-cam.x,py=player.y-cam.y;
  const mx=monster.x-cam.x,my=monster.y-cam.y;
  const darkGrad=ctx.createRadialGradient(px,py,60,px,py,280);
  darkGrad.addColorStop(0,'transparent');darkGrad.addColorStop(0.5,'rgba(0,0,0,0.3)');darkGrad.addColorStop(1,'rgba(0,0,0,0.75)');
  ctx.fillStyle=darkGrad;ctx.fillRect(0,0,VW,VH);
  const monGrad=ctx.createRadialGradient(mx,my,0,mx,my,100+monster.rage*80);
  monGrad.addColorStop(0,`rgba(180,0,0,${0.15+monster.rage*0.2})`);monGrad.addColorStop(1,'transparent');
  ctx.fillStyle=monGrad;ctx.fillRect(0,0,VW,VH);
}

function drawMinimap(){
  if(state!=='play') return;
  const mw=miniCanvas.width,mh=miniCanvas.height;
  const sx=mw/WORLD_W,sy=mh/WORLD_H;
  miniCtx.clearRect(0,0,mw,mh);
  miniCtx.fillStyle='#0a000f';miniCtx.fillRect(0,0,mw,mh);
  miniCtx.fillStyle='#2a0044';
  for(let w of walls) miniCtx.fillRect(w.x*sx,w.y*sy,w.w*sx,w.h*sy);
  miniCtx.fillStyle='#00ffcc';
  for(let g of gems) if(!g.collected){miniCtx.beginPath();miniCtx.arc(g.x*sx,g.y*sy,2,0,Math.PI*2);miniCtx.fill();}
  miniCtx.fillStyle='#4488ff';miniCtx.beginPath();miniCtx.arc(player.x*sx,player.y*sy,3,0,Math.PI*2);miniCtx.fill();
  miniCtx.fillStyle='#ff0000';miniCtx.beginPath();miniCtx.arc(monster.x*sx,monster.y*sy,4,0,Math.PI*2);miniCtx.fill();
  miniCtx.strokeStyle='rgba(255,255,255,0.2)';miniCtx.lineWidth=1;
  miniCtx.strokeRect(cam.x*sx,cam.y*sy,VW*sx,VH*sy);
}

let previewPhase=0;
const previewTentacles=Array.from({length:6},(_,i)=>i*(Math.PI*2/6));
function drawPreviewMonster(){
  previewPhase+=0.04;
  for(let i=0;i<previewTentacles.length;i++) previewTentacles[i]+=0.03*(i%2===0?1:-1);
  previewCtx.clearRect(0,0,120,120);
  const pr=0.5+Math.sin(previewPhase*0.5)*0.4;
  drawMonsterAt(60,60,1.1,pr,previewPhase,previewPhase*1.2,previewPhase*0.8,previewTentacles,previewCtx);
}

loop();
</script>

</body>
</html>

Game Source: RUN! Monster Chase

Creator: BraveMaker51

Libraries: none

Complexity: complex (568 lines, 24.8 KB)

The full source code is displayed above on this page.

Remix Instructions

To remix this game, copy the source code above and modify it. Add a ARCADELAB header at the top with "remix_of: run-monster-chase-bravemaker51" to link back to the original. Then publish at arcadelab.ai/publish.