Scope Challenge
by BlazeGalaxy19353 lines7.3 KB
<!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.