🎮ArcadeLab

Scope Challenge

by BlazeGalaxy19
353 lines7.3 KB
▶ Play

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scope Challenge</title>
<style>
*{box-sizing:border-box}
body{
 margin:0;background:#101a27;color:white;
 font-family:Arial;text-align:center;overflow:hidden;
 touch-action:none;
}
#hud{
 height:48px;background:#172b40;
 display:flex;justify-content:space-around;
 align-items:center;font-weight:bold;
}
canvas{
 display:block;width:100%;height:calc(100dvh - 105px);
 background:#9bc8d0;touch-action:none;
}
#bottom{
 height:57px;background:#172b40;
 display:flex;align-items:center;justify-content:center;
 gap:15px;
}
button{padding:9px 18px;border:0;border-radius:8px;font-weight:bold}
</style>
</head>
<body>

<div id="hud">
 <span>🎯 SCORE: <b id="score">0</b></span>
 <span>🤖 TARGETS: <b id="hits">0</b></span>
 <span>🏆 BEST: <b id="best">0</b></span>
</div>

<canvas id="game"></canvas>

<div id="bottom">
 <span>HOLD AND DRAG TO AIM</span>
 <button onclick="restart()">RESTART</button>
</div>

<script>
const canvas=document.getElementById("game");
const ctx=canvas.getContext("2d");

let W=0,H=0;
let score=0,hits=0;
let best=Number(localStorage.scopeBest||0);
let targets=[];
let particles=[];
let aiming=false;
let pointer={x:0,y:0};
let lastShot=0;
let shotCooldown=350;
let messages=[];
let gameOver=false;

document.getElementById("best").textContent=best;

function resize(){
 const rect=canvas.getBoundingClientRect();
 const oldW=W||rect.width;
 const oldH=H||rect.height;
 W=rect.width;
 H=rect.height;
 canvas.width=Math.floor(W*devicePixelRatio);
 canvas.height=Math.floor(H*devicePixelRatio);
 ctx.setTransform(devicePixelRatio,0,0,devicePixelRatio,0,0);

 if(pointer.x===0){pointer.x=W/2;pointer.y=H/2}
 targets.forEach(t=>{
  t.x=t.x/oldW*W;
  t.y=t.y/oldH*H;
 });
}
window.addEventListener("resize",resize);

function makeTarget(){
 const size=0.75+Math.random()*.35;
 return {
  x:50+Math.random()*(W-100),
  y:H*.43+Math.random()*(H*.35),
  size,
  vx:(Math.random()>.5?1:-1)*(18+Math.random()*30),
  vy:(Math.random()-.5)*12,
  alive:true,
  color:["#f15b50","#e6a23c","#7b69df","#35a9a0"][Math.floor(Math.random()*4)]
 };
}

function spawnTargets(){
 while(targets.filter(t=>t.alive).length<7){
  targets.push(makeTarget());
 }
}

function updateHUD(){
 document.getElementById("score").textContent=score;
 document.getElementById("hits").textContent=hits;
 document.getElementById("best").textContent=best;
}

function drawBackground(){
 ctx.fillStyle="#a9d6dc";
 ctx.fillRect(0,0,W,H);

 // distant mountains
 ctx.fillStyle="#739ba0";
 ctx.beginPath();
 ctx.moveTo(0,H*.55);
 for(let x=0;x<=W;x+=30){
  ctx.lineTo(x,H*.40+Math.sin(x*.015)*35);
 }
 ctx.lineTo(W,H);ctx.lineTo(0,H);ctx.fill();

 // grassy field
 ctx.fillStyle="#477d4b";
 ctx.fillRect(0,H*.66,W,H*.34);

 // ground stripes
 ctx.fillStyle="#35663d";
 for(let i=0;i<W;i+=70){
  ctx.fillRect(i,H*.66,35,5);
 }

 // trees
 for(let i=30;i<W;i+=130){
  ctx.fillStyle="#654a34";
  ctx.fillRect(i,H*.47,10,H*.2);
  ctx.fillStyle="#286b43";
  ctx.beginPath();
  ctx.arc(i+5,H*.46,24,0,Math.PI*2);
  ctx.fill();
 }
}

function drawRobot(t){
 if(!t.alive)return;
 const s=t.size;
 const x=t.x,y=t.y;

 // legs
 ctx.fillStyle="#273b55";
 ctx.fillRect(x-12*s,y+24*s,9*s,21*s);
 ctx.fillRect(x+3*s,y+24*s,9*s,21*s);

 // body
 ctx.fillStyle=t.color;
 ctx.fillRect(x-21*s,y-2*s,42*s,30*s);

 // chest panel
 ctx.fillStyle="#d9e8e9";
 ctx.fillRect(x-10*s,y+6*s,20*s,10*s);

 // head
 ctx.fillStyle="#e8ecec";
 ctx.beginPath();
 ctx.arc(x,y-17*s,15*s,0,Math.PI*2);
 ctx.fill();

 // eyes
 ctx.fillStyle="#253d54";
 ctx.beginPath();
 ctx.arc(x-5*s,y-18*s,2.5*s,0,Math.PI*2);
 ctx.arc(x+5*s,y-18*s,2.5*s,0,Math.PI*2);
 ctx.fill();

 // head antenna
 ctx.strokeStyle="#273b55";
 ctx.lineWidth=3*s;
 ctx.beginPath();
 ctx.moveTo(x,y-31*s);
 ctx.lineTo(x,y-39*s);
 ctx.stroke();
 ctx.fillStyle="#ffdf4b";
 ctx.beginPath();
 ctx.arc(x,y-41*s,4*s,0,Math.PI*2);
 ctx.fill();
}

function drawScope(){
 const x=pointer.x,y=pointer.y;
 const radius=Math.min(W,H)*.115;

 // transparent scope area
 ctx.save();
 ctx.beginPath();
 ctx.arc(x,y,radius,0,Math.PI*2);
 ctx.clip();
 ctx.fillStyle="rgba(220,250,255,.12)";
 ctx.fillRect(x-radius,y-radius,radius*2,radius*2);
 ctx.restore();

 // scope ring
 ctx.strokeStyle="#f7f7f7";
 ctx.lineWidth=3;
 ctx.beginPath();
 ctx.arc(x,y,radius,0,Math.PI*2);
 ctx.stroke();

 // crosshair
 ctx.strokeStyle="#ff4545";
 ctx.lineWidth=2;
 ctx.beginPath();
 ctx.moveTo(x-radius-12,y);
 ctx.lineTo(x+radius+12,y);
 ctx.moveTo(x,y-radius-12);
 ctx.lineTo(x,y+radius+12);
 ctx.stroke();

 ctx.fillStyle="#ff4545";
 ctx.beginPath();
 ctx.arc(x,y,3,0,Math.PI*2);
 ctx.fill();

 // small scope marks
 ctx.strokeStyle="white";
 ctx.lineWidth=2;
 for(let a=0;a<360;a+=90){
  const rad=a*Math.PI/180;
  ctx.beginPath();
  ctx.moveTo(x+Math.cos(rad)*(radius-8),y+Math.sin(rad)*(radius-8));
  ctx.lineTo(x+Math.cos(rad)*(radius-2),y+Math.sin(rad)*(radius-2));
  ctx.stroke();
 }
}

function addMessage(text,x,y){
 messages.push({text,x,y,time:1});
}

function shoot(){
 const now=performance.now();
 if(now-lastShot<shotCooldown)return;
 lastShot=now;

 let target=null;
 let hitType="";
 let closest=Infinity;

 for(const t of targets){
  if(!t.alive)continue;
  const s=t.size;

  const headX=t.x;
  const headY=t.y-17*s;
  const bodyX=t.x;
  const bodyY=t.y+12*s;

  const headDist=Math.hypot(pointer.x-headX,pointer.y-headY);
  const bodyDist=Math.hypot(pointer.x-bodyX,pointer.y-bodyY);

  if(headDist<15*s&&headDist<closest){
   target=t;hitType="HEAD";closest=headDist;
  }else if(bodyDist<23*s&&bodyDist<closest){
   target=t;hitType="BODY";closest=bodyDist;
  }
 }

 if(target){
  target.alive=false;
  hits++;
  const points=hitType==="HEAD"?789:123;
  score+=points;
  if(score>best){
   best=score;
   localStorage.scopeBest=best;
  }
  addMessage(hitType+" +"+points,target.x,target.y-45);
  updateHUD();
 }else{
  addMessage("MISS",pointer.x,pointer.y-35);
 }
}

function update(dt){
 for(const t of targets){
  if(!t.alive)continue;
  t.x+=t.vx*dt;
  t.y+=t.vy*dt;

  if(t.x<35||t.x>W-35)t.vx*=-1;
  if(t.y<H*.40||t.y>H*.78)t.vy*=-1;
 }
 spawnTargets();

 for(const m of messages)m.time-=dt;
 messages=messages.filter(m=>m.time>0);
}

function drawMessages(){
 ctx.textAlign="center";
 ctx.font="bold 22px Arial";
 for(const m of messages){
  ctx.globalAlpha=Math.min(1,m.time);
  ctx.fillStyle=m.text.includes("HEAD")?"#ffe34c":
   m.text.includes("BODY")?"#6aff9a":"white";
  ctx.fillText(m.text,m.x,m.y-(1-m.time)*35);
 }
 ctx.globalAlpha=1;
}

let previous=performance.now();

function frame(now){
 const dt=Math.min((now-previous)/1000,.04);
 previous=now;

 update(dt);
 drawBackground();
 targets.forEach(drawRobot);
 drawScope();
 drawMessages();

 if(aiming)shoot();
 requestAnimationFrame(frame);
}

function movePointer(e){
 const rect=canvas.getBoundingClientRect();
 pointer.x=Math.max(0,Math.min(W,e.clientX-rect.left));
 pointer.y=Math.max(0,Math.min(H,e.clientY-rect.top));
}

canvas.addEventListener("pointerdown",e=>{
 aiming=true;
 canvas.setPointerCapture(e.pointerId);
 movePointer(e);
 shoot();
});
canvas.addEventListener("pointermove",e=>{
 if(aiming)movePointer(e);
});
canvas.addEventListener("pointerup",()=>aiming=false);
canvas.addEventListener("pointercancel",()=>aiming=false);

function restart(){
 score=0;hits=0;targets=[];messages=[];
 updateHUD();spawnTargets();
}

resize();
restart();
requestAnimationFrame(frame);
</script>
</body>
</html>

Game Source: Scope Challenge

Creator: BlazeGalaxy19

Libraries: none

Complexity: complex (353 lines, 7.3 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: scope-challenge-blazegalaxy19" to link back to the original. Then publish at arcadelab.ai/publish.