🎮ArcadeLab

Shadow Quest

by MegaNinja77
315 lines9.9 KB
▶ Play
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Shadow Quest</title>
<style>
*{box-sizing:border-box}
body{margin:0;background:#111827;color:#f8f0cf;font-family:monospace;text-align:center}
h1{color:#ffd66b;margin:14px 0 4px;font-size:24px}
p{margin:5px}
#hud{display:flex;justify-content:center;gap:18px;flex-wrap:wrap;padding:10px;font-size:14px}
canvas{display:block;width:min(96vw,640px);height:auto;margin:auto;border:4px solid #7d83a8;image-rendering:pixelated;background:#203d38}
#controls{display:grid;grid-template-columns:repeat(3,54px);gap:5px;justify-content:center;margin:12px auto}
button{background:#39445f;color:white;border:2px solid #858db0;border-radius:8px;padding:12px;font:bold 17px monospace;touch-action:manipulation}
button:active{background:#7786bd}
#attack{background:#973f53}
#restart{font-size:14px;background:#76622f}
.help{color:#c5c9d9;font-size:12px;padding:0 12px 15px}
</style>
</head>
<body>
<h1>⚔ SHADOW QUEST ⚔</h1>
<p>THE LOST KINGDOM</p>
<div id="hud">
 <span>❤️ <b id="hp">5</b></span>
 <span>💎 <b id="crystals">0</b>/5</span>
 <span>👾 <b id="kills">0</b></span>
</div>
<canvas id="game" width="320" height="320"></canvas>
<div id="controls">
 <span></span><button data-key="up">▲</button><span></span>
 <button data-key="left">◀</button><button data-key="down">▼</button><button data-key="right">▶</button>
 <span></span><button id="attack">⚔</button><span></span>
</div>
<button id="restart">RESTART QUEST</button>
<p class="help">Move: WASD / arrows · Attack: SPACE · Collect 5 crystals to open the portal!</p>

<script>
const c=document.getElementById('game'),ctx=c.getContext('2d');
ctx.imageSmoothingEnabled=false;
const W=320,H=320,T=16;
const keys={};
let hero, enemies, gems, obstacles, portal, hp, kills, won, lost, swing, invuln;
let last=0, lastAttack=0, elapsed=0;

function rnd(n){return Math.floor(Math.random()*n)}
function dist(a,b){return Math.hypot(a.x-b.x,a.y-b.y)}
function makeWorld(){
 obstacles=[];
 // Tree trunks and stones around a walkable forest
 for(let i=0;i<70;i++){
  let x=8+rnd(304),y=8+rnd(304);
  if(Math.hypot(x-160,y-160)<45)continue;
  obstacles.push({x,y,type:Math.random()<.75?'tree':'rock'});
 }
 gems=[];
 while(gems.length<5){
  let g={x:16+rnd(288),y:16+rnd(288),phase:Math.random()*6};
  if(Math.hypot(g.x-160,g.y-160)>40 &&
     !obstacles.some(o=>Math.hypot(o.x-g.x,o.y-g.y)<15) &&
     !gems.some(a=>dist(a,g)<24)) gems.push(g);
 }
 enemies=[
  {x:45,y:65,hp:2,speed:22},
  {x:270,y:65,hp:2,speed:25},
  {x:45,y:265,hp:2,speed:24},
  {x:270,y:260,hp:3,speed:20},
  {x:160,y:55,hp:3,speed:28}
 ];
 portal={x:160,y:160};
}
function reset(){
 hero={x:160,y:220,dir:0};
 hp=5;kills=0;won=false;lost=false;swing=0;invuln=0;
 elapsed=0;last=0;lastAttack=0;
 makeWorld();updateHud();requestAnimationFrame(loop);
}
function updateHud(){
 document.getElementById('hp').textContent=hp;
 document.getElementById('crystals').textContent=5-gems.length;
 document.getElementById('kills').textContent=kills;
}
function hitObstacle(x,y){
 return obstacles.some(o=>{
  let r=o.type==='tree'?10:7;
  return Math.abs(x-o.x)<r+4&&Math.abs(y-o.y)<r+4;
 });
}
function moveEntity(e,dx,dy){
 let nx=Math.max(6,Math.min(314,e.x+dx));
 if(!hitObstacle(nx,e.y))e.x=nx;
 let ny=Math.max(6,Math.min(314,e.y+dy));
 if(!hitObstacle(e.x,ny))e.y=ny;
}
function attack(){
 if(won||lost||elapsed-lastAttack<260)return;
 lastAttack=elapsed;swing=180;
 let found=false;
 for(let e of enemies){
  if(dist(hero,e)<29){
   let vx=e.x-hero.x,vy=e.y-hero.y;
   if(vx*Math.cos(hero.dir)+vy*Math.sin(hero.dir)>-7){
    e.hp--;found=true;
    e.x+=Math.cos(hero.dir)*7;e.y+=Math.sin(hero.dir)*7;
   }
  }
 }
 enemies=enemies.filter(e=>{
  if(e.hp<=0){kills++;return false}
  return true;
 });
 updateHud();
}
function rect(x,y,w,h,color){
 ctx.fillStyle=color;ctx.fillRect(Math.round(x),Math.round(y),w,h);
}
function pixel(x,y,color,size=2){
 rect(x-size/2,y-size/2,size,size,color);
}
function drawTree(o){
 let x=o.x,y=o.y;
 // shadow
 rect(x-9,y+5,18,4,'#172b2b');
 // trunk
 rect(x-3,y-2,6,13,'#795037');
 rect(x-5,y+4,10,3,'#563a32');
 // layered leafy canopy
 rect(x-8,y-12,16,12,'#174c3b');
 rect(x-11,y-8,22,7,'#236548');
 rect(x-7,y-15,14,5,'#318153');
 pixel(x-4,y-10,'#66a85d',3);
 pixel(x+4,y-6,'#4d9a55',3);
 pixel(x,y-14,'#4d9a55',3);
}
function drawRock(o){
 let x=o.x,y=o.y;
 rect(x-7,y-3,14,8,'#586b72');
 rect(x-4,y-7,9,5,'#7c8b91');
 pixel(x-2,y-5,'#b1b9b4',3);
 rect(x-6,y+4,12,2,'#364a50');
}
function drawKnight(){
 let x=Math.round(hero.x),y=Math.round(hero.y);
 // shadow
 rect(x-7,y+7,14,3,'#172522');
 // boots and legs
 rect(x-6,y+3,4,5,'#3d3344');
 rect(x+2,y+3,4,5,'#3d3344');
 rect(x-7,y+7,5,2,'#a8b7bc');
 rect(x+2,y+7,5,2,'#a8b7bc');
 // cape
 rect(x-7,y-5,14,10,'#963c46');
 rect(x-5,y+2,10,4,'#652c3c');
 pixel(x,y+3,'#e35c53',3);
 // armor
 rect(x-6,y-7,12,11,'#718b9b');
 rect(x-4,y-6,8,8,'#a9c0c6');
 rect(x-2,y-4,4,5,'#e2d9bd');
 // helmet
 rect(x-6,y-13,12,8,'#a6b6b8');
 rect(x-4,y-16,8,4,'#cad4cd');
 rect(x-7,y-11,14,3,'#647e8b');
 // face visor
 rect(x-4,y-10,8,3,'#283542');
 rect(x-3,y-9,2,1,'#f9d978');
 rect(x+2,y-9,2,1,'#f9d978');
 // helmet plume
 rect(x-1,y-19,3,4,'#b94955');
 rect(x-4,y-17,4,2,'#e35c53');
 // sword pointed in facing direction
 let sx=x+Math.cos(hero.dir)*12,sy=y+Math.sin(hero.dir)*12;
 rect(sx-1,sy-1,3,3,'#f3e8bd');
 pixel(sx,sy,'#ffffff',2);
 if(swing>0){
  ctx.strokeStyle='#f8f1cf';ctx.lineWidth=3;
  ctx.beginPath();
  ctx.arc(x,y,21,hero.dir-.7,hero.dir+.7);
  ctx.stroke();
 }
}
function drawSlime(e){
 let x=Math.round(e.x),y=Math.round(e.y);
 rect(x-8,y+5,16,3,'#172b2b');
 rect(x-8,y-3,16,9,'#602e8a');
 rect(x-6,y-7,12,6,'#8c4bd0');
 rect(x-3,y-10,6,4,'#a56de2');
 rect(x-5,y+1,10,5,'#7440a9');
 pixel(x-4,y-2,'#ffffff',3);pixel(x+4,y-2,'#ffffff',3);
 pixel(x-4,y-2,'#281a3d',1);pixel(x+4,y-2,'#281a3d',1);
 pixel(x,y+2,'#f6c1ff',2);
 if(e.hp===3){
  pixel(x,y-12,'#f8d66b',3);
 }
}
function drawGem(g){
 let y=g.y+Math.sin(elapsed/180+g.phase)*2,x=g.x;
 pixel(x,y-5,'#d5ffff',3);
 rect(x-3,y-3,6,7,'#54e7ed');
 rect(x-5,y-1,10,3,'#3a9ddd');
 pixel(x,y-1,'#ffffff',2);
}
function drawPortal(){
 let open=gems.length===0;
 let x=portal.x,y=portal.y;
 // stone ring
 ctx.strokeStyle=open?'#e6c76b':'#65747e';ctx.lineWidth=5;
 ctx.beginPath();ctx.ellipse(x,y,13,18,0,0,Math.PI*2);ctx.stroke();
 ctx.fillStyle=open?'#62d7ee':'#263442';
 ctx.beginPath();ctx.ellipse(x,y,9,14,0,0,Math.PI*2);ctx.fill();
 if(open){
  pixel(x-3,y-5,'#d9ffff',3);
  pixel(x+4,y+3,'#a6f4ff',3);
 }
}
function draw(){
 // forest ground
 ctx.fillStyle='#294b3e';ctx.fillRect(0,0,W,H);
 for(let y=4;y<H;y+=16)for(let x=4;x<W;x+=16){
  pixel(x+(y%3),y,'#365943',2);
  if((x*3+y*7)%11===0)pixel(x+4,y+5,'#68804a',2);
 }
 // old stone path
 ctx.fillStyle='#465650';
 for(let i=0;i<9;i++){
  let x=144+(i%3)*12,y=80+i*19;
  rect(x,y,9,7,'#53665e');
 }
 drawPortal();
 gems.forEach(drawGem);
 // Draw objects sorted by y for depth
 let all=obstacles.slice().sort((a,b)=>a.y-b.y);
 for(let o of all){
  if(o.type==='tree')drawTree(o);else drawRock(o);
 }
 enemies.forEach(drawSlime);
 drawKnight();
 // edge vignette
 ctx.fillStyle='rgba(10,16,25,.22)';
 rect(0,0,W,3,'#17262b');rect(0,H-3,W,3,'#17262b');
 if(won||lost){
  ctx.fillStyle='rgba(10,15,25,.86)';rect(24,108,272,100,'#111827');
  ctx.textAlign='center';
  ctx.fillStyle=won?'#ffe17b':'#ff8b9b';
  ctx.font='bold 18px monospace';
  ctx.fillText(won?'KINGDOM RESTORED!':'YOU HAVE FALLEN',160,139);
  ctx.fillStyle='#f5f1df';ctx.font='11px monospace';
  ctx.fillText(won?'The forest is free.':'Crystals collected: '+(5-gems.length),160,160);
  ctx.fillText('Press RESTART to try again',160,184);
 }
}
function loop(ts){
 if(!last)last=ts;
 let dt=Math.min((ts-last)/1000,.04);last=ts;
 if(!won&&!lost){
  elapsed+=dt*1000;
  if(keys.ArrowUp||keys.w){moveEntity(hero,0,-85*dt);hero.dir=-Math.PI/2}
  if(keys.ArrowDown||keys.s){moveEntity(hero,0,85*dt);hero.dir=Math.PI/2}
  if(keys.ArrowLeft||keys.a){moveEntity(hero,-85*dt,0);hero.dir=Math.PI}
  if(keys.ArrowRight||keys.d){moveEntity(hero,85*dt,0);hero.dir=0}
  swing=Math.max(0,swing-dt*1000);
  invuln=Math.max(0,invuln-dt*1000);
  for(let e of enemies){
   let d=dist(hero,e);
   if(d>12){
    let dx=(hero.x-e.x)/d,dy=(hero.y-e.y)/d;
    let oldX=e.x,oldY=e.y;
    moveEntity(e,dx*e.speed*dt,dy*e.speed*dt);
    // Keep monsters from walking through trees
    if(oldX===e.x&&oldY===e.y){e.x+=dy*e.speed*dt;e.y-=dx*e.speed*dt}
   }
   if(d<16&&invuln<=0){
    hp--;invuln=1000;updateHud();
    if(hp<=0)lost=true;
   }
  }
  for(let i=gems.length-1;i>=0;i--){
   if(dist(hero,gems[i])<14){
    gems.splice(i,1);updateHud();
   }
  }
  if(gems.length===0&&dist(hero,portal)<22){
   won=true;
  }
 }
 draw();
 requestAnimationFrame(loop);
}
document.addEventListener('keydown',e=>{
 if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight',' '].includes(e.key))e.preventDefault();
 keys[e.key]=true;keys[e.key.toLowerCase()]=true;
 if(e.code==='Space')attack();
});
document.addEventListener('keyup',e=>{
 keys[e.key]=false;keys[e.key.toLowerCase()]=false;
});
document.querySelectorAll('[data-key]').forEach(b=>{
 let k={up:'ArrowUp',down:'ArrowDown',left:'ArrowLeft',right:'ArrowRight'}[b.dataset.key];
 const down=e=>{e.preventDefault();keys[k]=true;keys[k.toLowerCase()]=true};
 const up=e=>{e.preventDefault();keys[k]=false;keys[k.toLowerCase()]=false};
 b.addEventListener('pointerdown',down);
 b.addEventListener('pointerup',up);
 b.addEventListener('pointerleave',up);
 b.addEventListener('pointercancel',up);
});
document.getElementById('attack').addEventListener('pointerdown',e=>{
 e.preventDefault();attack();
});
document.getElementById('restart').addEventListener('click',reset);
reset();
</script>
</body>
</html>

Game Source: Shadow Quest

Creator: MegaNinja77

Libraries: none

Complexity: complex (315 lines, 9.9 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: shadow-quest-meganinja77" to link back to the original. Then publish at arcadelab.ai/publish.