🎮ArcadeLab

LIGHT OR DIE — Parkour

by MagicComet73
521 lines13.9 KB
▶ Play

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=no">
<title>LIGHT OR DIE — Parkour</title>
<style>
*{box-sizing:border-box}
body{margin:0;overflow:hidden;background:#080b18;color:white;font-family:Arial,sans-serif;touch-action:none;user-select:none}
canvas{display:block;width:100vw;height:100dvh;touch-action:none}
#hud{position:fixed;top:12px;left:12px;right:12px;display:flex;justify-content:space-between;align-items:flex-start;pointer-events:none}
.brand{font-weight:900;font-size:18px;letter-spacing:1px}
.info{font-size:12px;color:#bdc9e7;margin-top:6px;line-height:1.6}
#hud button{pointer-events:auto}
button{border:1px solid #ffffff25;border-radius:12px;padding:12px;background:#202942;color:white;font-weight:bold;font-size:14px;touch-action:none}
#switch{position:fixed;top:85px;left:50%;transform:translateX(-50%);border:2px solid #54dfff;background:#102c4a;color:#75e6ff;min-width:150px;font-size:15px}
#controls{position:fixed;bottom:14px;left:12px;right:12px;display:flex;justify-content:space-between;align-items:end;pointer-events:none}
.group{display:flex;gap:9px;pointer-events:auto}
.control{width:58px;height:58px;font-size:23px;background:#17243ded;border:1px solid #ffffff35}
#jump{width:82px;height:68px;background:#4b2c83;border-color:#b28bff;font-size:14px}
#switchMobile{width:76px;height:58px;font-size:12px;background:#12394b;border-color:#55dfff}
#overlay{position:fixed;inset:0;display:flex;align-items:center;justify-content:center;background:#050815d9;padding:20px}
.panel{width:100%;max-width:370px;background:#111a30;border:1px solid #344567;border-radius:22px;padding:26px;text-align:center;box-shadow:0 15px 70px #0008}
.panel h1{font-size:32px;margin:0 0 12px;letter-spacing:1px}
.panel p{color:#b9c7e3;line-height:1.6;font-size:14px}
.primary{width:100%;margin-top:12px;background:#40d9ff;color:#07111d;border:0;font-size:16px}
.small{font-size:11px;color:#8395b9;margin-top:14px}
.hidden{display:none!important}
@media(min-width:800px){
 #controls{max-width:650px;left:50%;transform:translateX(-50%)}
}
</style>
</head>
<body>
<canvas id="game"></canvas>

<div id="hud">
 <div>
  <div class="brand">⚡ LIGHT OR DIE</div>
  <div class="info">LEVEL <span id="level">1</span> · CHECKPOINT <span id="cp">START</span><br>MODE: <span id="mode">LIGHTS ON</span></div>
 </div>
 <button id="restart">↻ RESTART</button>
</div>

<button id="switch">🔵 LIGHTS ON</button>

<div id="controls">
 <div class="group">
  <button class="control" id="left">◀</button>
  <button class="control" id="right">▶</button>
 </div>
 <div class="group">
  <button id="switchMobile">SWITCH<br>LIGHTS</button>
  <button class="control" id="jump">JUMP ⤴</button>
 </div>
</div>

<div id="overlay">
 <div class="panel">
  <h1 style="color:#55ddff">LIGHT <span style="color:#ff67dc">OR DIE</span></h1>
  <p>Two paths. Two realities. Only one color is solid at a time.</p>
  <p>🔵 Lights ON = blue platforms<br>🩷 Lights OFF = pink platforms<br><br>Jump across the gaps and switch colors in mid-air. Reach the green portal to win!</p>
  <button class="primary" id="play">START PARKOUR</button>
  <div class="small">MOVE: ARROWS / A-D · JUMP: SPACE · SWITCH: SHIFT</div>
 </div>
</div>

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

let W=innerWidth,H=innerHeight,D=1;
function resize(){
 W=innerWidth;H=innerHeight;
 D=Math.min(devicePixelRatio||1,2);
 canvas.width=W*D;canvas.height=H*D;
 ctx.setTransform(D,0,0,D,0,0);
}
addEventListener("resize",resize);
resize();

const BLUE=0,PINK=1,NEUTRAL=2;
const COLORS={
 blue:"#42dfff",pink:"#ff63d8",neutral:"#9eafcc"
};

let level=1,world=[],player,camera=0,light=BLUE;
let running=false,paused=false,frame=0;
let checkpointX=65,checkpointReached=false;
let spawnX=65,spawnY=0;
let particles=[];
let keys={left:false,right:false};
let jumpQueued=false,deadTimer=0,notice="",noticeTime=0;
let levelLength=0;

function floorY(){return Math.min(H*.70,H-150)}
function addPlatform(x,y,w,type,extra={}){
 world.push({x,y,w,h:16,type,...extra});
}
function buildLevel(){
 world=[];
 const base=floorY();
 const spacing=Math.max(138,Math.min(158,W*.40));
 const count=12+Math.min(level,6);
 levelLength=180+count*spacing+100;

 // Safe starting platform
 addPlatform(0,base,190,NEUTRAL);
 addPlatform(190,base-25,82,BLUE);
 addPlatform(190,base-25,82,PINK);

 // Main route: alternating color paths and height
 for(let i=1;i<=count;i++){
  const x=190+i*spacing;
  const y=base-25-((i%3)*24);
  const type=i%2===1?PINK:BLUE;
  const w=100;

  addPlatform(x,y,w,type);

  // Alternate path at a different height
  if(i%2===0){
   addPlatform(x+8,y-58,w-10,PINK);
  }else{
   addPlatform(x+8,y-58,w-10,BLUE);
  }

  // Occasional low alternate platform
  if(i%3===0){
   addPlatform(x+spacing*.48,base-4,72,i%2===0?PINK:BLUE);
  }
 }

 // Permanent checkpoint island
 const cpX=190+Math.floor(count*.53)*spacing;
 checkpointX=cpX;
 addPlatform(cpX,base-12,120,NEUTRAL,{checkpoint:true});

 // Final safe runway and finish portal
 const finishX=levelLength-100;
 addPlatform(finishX-100,base-10,210,NEUTRAL,{finish:true});
 addPlatform(finishX+100,base-10,120,NEUTRAL,{finish:true});
 world.sort((a,b)=>a.x-b.x);

 checkpointReached=false;
 spawnX=65;
 spawnY=base-32;
}
function start(){
 level=1;
 light=BLUE;
 camera=0;
 checkpointX=65;
 checkpointReached=false;
 spawnX=65;
 particles=[];
 buildLevel();
 player={
  x:spawnX,y:spawnY,w:23,h:32,
  vx:0,vy:0,onGround:false,
  coyote:0,jumpLock:false
 };
 running=true;paused=false;deadTimer=0;
 $("overlay").classList.add("hidden");
 updateHud();
}
function updateHud(){
 $("level").textContent=level;
 $("cp").textContent=checkpointReached?"SAVED":"START";
 $("mode").textContent=light===BLUE?"LIGHTS ON":"LIGHTS OFF";
 $("switch").textContent=light===BLUE?"🔵 LIGHTS ON":"🩷 LIGHTS OFF";
 $("switch").style.borderColor=light===BLUE?COLORS.blue:COLORS.pink;
 $("switch").style.color=light===BLUE?COLORS.blue:COLORS.pink;
}
function toggleLight(){
 if(!running)return;
 light=light===BLUE?PINK:BLUE;
 updateHud();
 burst(player.x+player.w/2,player.y+player.h/2,light===BLUE?COLORS.blue:COLORS.pink,7);
}
function activePlatform(p){
 return p.type===NEUTRAL||p.type===light;
}
function respawn(){
 player.x=spawnX;
 player.y=spawnY;
 player.vx=0;
 player.vy=0;
 player.onGround=false;
 camera=Math.max(0,player.x-W*.30);
 deadTimer=0;
 notice="CHECKPOINT RESPAWN";
 noticeTime=65;
}
function die(){
 if(deadTimer>0)return;
 deadTimer=35;
 notice="YOU FELL!";
 noticeTime=35;
}
function burst(x,y,color,n=12){
 for(let i=0;i<n;i++){
  particles.push({
   x,y,vx:(Math.random()-.5)*5,
   vy:(Math.random()-.7)*5,
   life:25+Math.random()*20,color
  });
 }
}
function nextLevel(){
 level++;
 light=BLUE;
 buildLevel();
 player.x=65;
 player.y=spawnY;
 player.vx=0;
 player.vy=0;
 camera=0;
 updateHud();
 notice="LEVEL "+level+"!";
 noticeTime=90;
 burst(player.x,player.y,COLORS.blue,24);
}
function jump(){
 if(!running||deadTimer>0)return;
 if(player.onGround||player.coyote>0){
  player.vy=-10.8;
  player.onGround=false;
  player.coyote=0;
  burst(player.x+player.w/2,player.y+player.h,COLORS.blue,4);
 }
}
function overlap(a,b){
 return a.x<b.x+b.w&&a.x+a.w>b.x&&a.y<b.y+b.h&&a.y+a.h>b.y;
}
function update(){
 frame++;
 if(!running)return;

 if(deadTimer>0){
  deadTimer--;
  if(deadTimer===0)respawn();
  return;
 }

 // Smooth horizontal movement
 const accel=.68,friction=.80,maxSpeed=5.0;
 if(keys.left)player.vx-=accel;
 if(keys.right)player.vx+=accel;
 if(!keys.left&&!keys.right)player.vx*=friction;
 player.vx=Math.max(-maxSpeed,Math.min(maxSpeed,player.vx));

 // Gravity
 const oldY=player.y;
 player.vy=Math.min(player.vy+.48,12);
 player.x+=player.vx;
 player.y+=player.vy;
 player.onGround=false;

 // World boundaries
 player.x=Math.max(0,player.x);

 // Land only on currently active platforms.
 // Inactive colored platforms are intangible.
 if(player.vy>=0){
  for(const p of world){
   if(!activePlatform(p))continue;
   const wasAbove=oldY+player.h<=p.y+4;
   const nowCrossed=player.y+player.h>=p.y;
   const horizontal=player.x+player.w>p.x&&player.x<p.x+p.w;
   if(wasAbove&&nowCrossed&&horizontal){
    player.y=p.y-player.h;
    player.vy=0;
    player.onGround=true;
    if(p.checkpoint&&!checkpointReached){
     checkpointReached=true;
     spawnX=p.x+20;
     spawnY=p.y-player.h;
     updateHud();
     notice="CHECKPOINT SAVED!";
     noticeTime=85;
     burst(p.x+55,p.y,COLORS.blue,18);
    }
    if(p.finish&&player.x+player.w>=levelLength-75){
     nextLevel();
     return;
    }
   }
  }
 }
 if(player.onGround)player.coyote=7;
 else player.coyote=Math.max(0,player.coyote-1);

 // Check the finish region
 if(player.x>levelLength-65&&player.y<H*.9){
  nextLevel();
  return;
 }

 // Fall detection
 if(player.y>H+100)die();

 // Camera follows player
 const target=Math.max(0,player.x-W*.32);
 camera+=(target-camera)*.12;

 // Particles
 for(const q of particles){
  q.x+=q.vx;q.y+=q.vy;q.vy+=.12;q.life--;
 }
 particles=particles.filter(q=>q.life>0);
 if(noticeTime>0)noticeTime--;
}
function roundRect(x,y,w,h,r,color){
 ctx.fillStyle=color;
 ctx.beginPath();
 ctx.roundRect(x,y,w,h,r);
 ctx.fill();
}
function drawPlatform(p){
 const x=p.x-camera;
 if(x+p.w< -30||x>W+30)return;
 if(!activePlatform(p)){
  // Hidden path: invisible, intangible
  return;
 }
 const color=p.type===NEUTRAL?COLORS.neutral:
  p.type===BLUE?COLORS.blue:COLORS.pink;
 ctx.save();
 ctx.shadowBlur=p.type===NEUTRAL?4:15;
 ctx.shadowColor=color;
 roundRect(x,p.y,p.w,p.h,5,color);
 ctx.shadowBlur=0;
 roundRect(x,p.y+p.h-4,p.w,4,3,
  p.type===NEUTRAL?"#627594":
  p.type===BLUE?"#1589bf":"#bc309c");
 ctx.restore();
}
function drawPlayer(){
 const x=player.x-camera,y=player.y;
 const cx=x+player.w/2;
 ctx.save();
 ctx.shadowColor=light===BLUE?COLORS.blue:COLORS.pink;
 ctx.shadowBlur=14;

 // Head
 roundRect(cx-7,y,14,14,5,"#ffffff");
 // Eyes
 ctx.fillStyle="#111a30";
 ctx.fillRect(cx-4,y+5,2,3);
 ctx.fillRect(cx+2,y+5,2,3);
 // Body
 roundRect(cx-5,y+14,10,11,"#ffcf65");
 // Animated legs
 const stride=player.onGround?Math.sin(frame*.25)*5:2;
 ctx.strokeStyle="#ffffff";
 ctx.lineWidth=4;
 ctx.lineCap="round";
 ctx.beginPath();
 ctx.moveTo(cx-2,y+24);
 ctx.lineTo(cx-5+stride,y+31);
 ctx.moveTo(cx+2,y+24);
 ctx.lineTo(cx+5-stride,y+31);
 ctx.stroke();
 // Arms
 ctx.beginPath();
 ctx.moveTo(cx-4,y+16);
 ctx.lineTo(cx-9,y+21);
 ctx.moveTo(cx+4,y+16);
 ctx.lineTo(cx+9,y+21);
 ctx.stroke();
 ctx.restore();
}
function draw(){
 const g=ctx.createLinearGradient(0,0,0,H);
 g.addColorStop(0,"#080d20");
 g.addColorStop(1,"#1a2340");
 ctx.fillStyle=g;
 ctx.fillRect(0,0,W,H);

 // Stars
 for(let i=0;i<55;i++){
  const x=((i*113-camera*.13)%(W+20)+W+20)%(W+20);
  const y=(i*71)%Math.max(H,1);
  ctx.globalAlpha=.25+(i%3)*.2;
  ctx.fillStyle="#b9dfff";
  ctx.fillRect(x,y,2,2);
 }
 ctx.globalAlpha=1;

 // Far skyline
 for(let i=0;i<12;i++){
  const x=((i*110-camera*.22)%(W+120)+W+120)%(W+120)-60;
  ctx.fillStyle="#101a32";
  ctx.fillRect(x,H*.72-((i*31)%90),70,H);
 }

 // Platforms
 for(const p of world)drawPlatform(p);

 // Checkpoint flag
 const cp=world.find(p=>p.checkpoint);
 if(cp){
  const x=cp.x-camera+15;
  ctx.fillStyle="#e7edf8";
  ctx.fillRect(x,cp.y-43,3,31);
  ctx.fillStyle=checkpointReached?"#52f0a3":"#ffd166";
  ctx.beginPath();
  ctx.moveTo(x+3,cp.y-42);
  ctx.lineTo(x+23,cp.y-36);
  ctx.lineTo(x+3,cp.y-29);
  ctx.fill();
 }

 // Finish portal
 const fx=levelLength-45-camera;
 ctx.save();
 ctx.shadowBlur=22;
 ctx.shadowColor="#4dffac";
 ctx.strokeStyle="#4dffac";
 ctx.lineWidth=5;
 ctx.beginPath();
 ctx.ellipse(fx,floorY()-38,16,34,0,0,Math.PI*2);
 ctx.stroke();
 ctx.restore();
 ctx.fillStyle="#4dffac";
 ctx.font="bold 12px Arial";
 ctx.textAlign="center";
 ctx.fillText("FINISH",fx,floorY()-82);
 ctx.textAlign="left";

 drawPlayer();

 // Particles
 for(const q of particles){
  ctx.globalAlpha=Math.max(0,q.life/40);
  ctx.fillStyle=q.color;
  ctx.fillRect(q.x-camera,q.y,4,4);
 }
 ctx.globalAlpha=1;

 if(noticeTime>0){
  ctx.textAlign="center";
  ctx.font="bold 23px Arial";
  ctx.fillStyle=notice.includes("FELL")?"#ff6689":
    notice.includes("CHECKPOINT")?"#65ffb2":"#ffffff";
  ctx.shadowColor="#000";
  ctx.shadowBlur=8;
  ctx.fillText(notice,W/2,H*.27);
  ctx.shadowBlur=0;
  ctx.textAlign="left";
 }
}
function loop(){
 update();
 draw();
 requestAnimationFrame(loop);
}

// Mobile buttons: hold left/right to move.
function holdButton(id,key){
 const el=$(id);
 el.addEventListener("pointerdown",e=>{
  e.preventDefault();
  keys[key]=true;
  try{el.setPointerCapture(e.pointerId)}catch(_){}
 });
 const release=e=>{
  e.preventDefault();
  keys[key]=false;
 };
 el.addEventListener("pointerup",release);
 el.addEventListener("pointercancel",release);
 el.addEventListener("lostpointercapture",()=>keys[key]=false);
}
holdButton("left","left");
holdButton("right","right");

$("jump").addEventListener("pointerdown",e=>{
 e.preventDefault();
 jump();
});
$("switch").addEventListener("click",toggleLight);
$("switchMobile").addEventListener("pointerdown",e=>{
 e.preventDefault();
 toggleLight();
});
$("restart").addEventListener("click",()=>{
 if(running){
  buildLevel();
  player.x=checkpointReached?spawnX:65;
  player.y=checkpointReached?spawnY:floorY()-32;
  player.vx=0;player.vy=0;
  camera=Math.max(0,player.x-W*.32);
  deadTimer=0;
 }
});
$("play").addEventListener("click",start);

addEventListener("keydown",e=>{
 if(["ArrowLeft","ArrowRight","ArrowUp"," ","Shift","a","d","A","D"].includes(e.key))e.preventDefault();
 if(e.key==="ArrowLeft"||e.key==="a"||e.key==="A")keys.left=true;
 if(e.key==="ArrowRight"||e.key==="d"||e.key==="D")keys.right=true;
 if((e.key===" "||e.key==="ArrowUp")&&!e.repeat)jump();
 if(e.key==="Shift"&&!e.repeat)toggleLight();
});
addEventListener("keyup",e=>{
 if(e.key==="ArrowLeft"||e.key==="a"||e.key==="A")keys.left=false;
 if(e.key==="ArrowRight"||e.key==="d"||e.key==="D")keys.right=false;
});
addEventListener("blur",()=>{keys.left=false;keys.right=false});

loop();
})();
</script>
</body>
</html>

Game Source: LIGHT OR DIE — Parkour

Creator: MagicComet73

Libraries: none

Complexity: complex (521 lines, 13.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: light-or-die-parkour-magiccomet73" to link back to the original. Then publish at arcadelab.ai/publish.