🎮ArcadeLab

Velocity District 3D

by CyberFalcon70
1362 lines35.8 KB🛠️ Three.js (3D graphics)
▶ 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>Velocity District 3D</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#05070b;font-family:Arial,Helvetica,sans-serif}
canvas{display:block}
#hud{position:fixed;inset:0;pointer-events:none;color:#fff;z-index:5}
#top{
 position:absolute;top:14px;left:14px;right:14px;
 display:flex;justify-content:space-between;align-items:flex-start
}
.stats{display:flex;gap:8px;flex-wrap:wrap}
.stat{
 min-width:90px;padding:9px 13px;border-radius:12px;
 background:rgba(7,12,20,.72);border:1px solid rgba(255,255,255,.15);
 backdrop-filter:blur(8px);box-shadow:0 5px 20px rgba(0,0,0,.25);
 font-size:13px;color:#b9c8d8
}
.stat b{display:block;color:#fff;font-size:19px;margin-top:2px}
#pauseHUD,#muteHUD{
 pointer-events:auto;border:0;color:#fff;background:rgba(7,12,20,.78);
 border:1px solid rgba(255,255,255,.18);border-radius:11px;
 width:43px;height:43px;margin-left:6px;font-size:18px;cursor:pointer
}
#powerHUD{
 position:absolute;top:100px;left:50%;transform:translateX(-50%);
 background:rgba(5,10,18,.7);padding:7px 13px;border-radius:20px;
 font-size:13px;opacity:0;transition:.2s
}
#mobile{
 position:fixed;bottom:18px;left:0;right:0;display:none;
 justify-content:space-between;padding:0 18px;z-index:8;pointer-events:none
}
.mobileGroup{display:flex;gap:10px;pointer-events:auto}
.mbtn{
 width:62px;height:58px;border-radius:17px;border:1px solid rgba(255,255,255,.2);
 background:rgba(8,14,24,.65);color:#fff;font-size:23px;
 backdrop-filter:blur(8px);touch-action:none
}
#overlay{
 position:fixed;inset:0;z-index:20;display:flex;align-items:center;justify-content:center;
 background:rgba(3,6,12,.67);backdrop-filter:blur(9px);color:white
}
.panel{
 width:min(92vw,520px);padding:35px;border-radius:26px;text-align:center;
 background:linear-gradient(145deg,rgba(18,28,44,.98),rgba(5,10,18,.98));
 border:1px solid rgba(255,255,255,.16);
 box-shadow:0 30px 100px rgba(0,0,0,.65)
}
.logo{
 font-size:clamp(38px,8vw,66px);font-weight:900;letter-spacing:5px;
 margin:0;text-shadow:0 0 30px rgba(70,180,255,.35)
}
.sub{color:#9eb3c9;margin:8px 0 28px}
button.main{
 border:0;border-radius:14px;padding:14px 30px;color:#fff;
 font-size:18px;font-weight:800;background:#167be8;cursor:pointer;
 box-shadow:0 5px 0 #0753a5
}
button.main:active{transform:translateY(3px);box-shadow:0 2px 0 #0753a5}
.menuRow{display:flex;justify-content:center;gap:10px;margin-top:15px}
button.secondary{
 border:1px solid rgba(255,255,255,.15);border-radius:12px;
 padding:10px 16px;background:rgba(255,255,255,.07);color:#fff;cursor:pointer
}
.controls{margin-top:24px;color:#9baec0;font-size:13px;line-height:1.8}
.hidden{display:none!important}
.setting{
 display:flex;justify-content:space-between;align-items:center;
 padding:12px 0;border-bottom:1px solid rgba(255,255,255,.1);color:#dbe7f2
}
.quality{display:flex;gap:5px}
.quality button{
 background:rgba(255,255,255,.08);border:1px solid rgba(255,255,255,.12);
 color:white;border-radius:8px;padding:7px 9px
}
.quality button.active{background:#147ee8}
</style>
</head>
<body>

<div id="hud">
 <div id="top">
  <div class="stats">
   <div class="stat">SCORE<b id="score">0</b></div>
   <div class="stat">DISTANCE<b id="distance">0 m</b></div>
   <div class="stat">COINS<b id="coins">0</b></div>
   <div class="stat">BEST<b id="best">0</b></div>
  </div>
  <div>
   <button id="pauseHUD">Ⅱ</button>
   <button id="muteHUD">🔊</button>
  </div>
 </div>
 <div id="powerHUD"></div>
</div>

<div id="mobile">
 <div class="mobileGroup">
  <button class="mbtn" id="ml">‹</button>
  <button class="mbtn" id="mr">›</button>
 </div>
 <div class="mobileGroup">
  <button class="mbtn" id="mj">↑</button>
  <button class="mbtn" id="ms">↓</button>
 </div>
</div>

<div id="overlay">
 <div class="panel" id="mainPanel">
  <h1 class="logo">VELOCITY</h1>
  <h2 style="letter-spacing:4px;margin:0;color:#59bfff">DISTRICT</h2>
  <p class="sub">An endless urban night run.</p>
  <button class="main" id="play">START RUN</button>
  <div class="menuRow">
   <button class="secondary" id="settings">SETTINGS</button>
  </div>
  <div class="controls">
   A / D or ← / → &nbsp; Lane change<br>
   W / SPACE / ↑ &nbsp; Jump<br>
   S / ↓ &nbsp; Slide<br>
   P &nbsp; Pause
  </div>
 </div>

 <div class="panel hidden" id="pausePanel">
  <h1 class="logo" style="font-size:42px">PAUSED</h1>
  <p class="sub">The city is waiting.</p>
  <button class="main" id="resume">RESUME</button>
  <div class="menuRow">
   <button class="secondary" id="pauseSettings">SETTINGS</button>
   <button class="secondary" id="quit">QUIT RUN</button>
  </div>
 </div>

 <div class="panel hidden" id="overPanel">
  <h1 class="logo" style="font-size:42px">RUN OVER</h1>
  <p class="sub">You made it through the district.</p>
  <div style="font-size:20px;line-height:1.8">
   Score: <b id="finalScore">0</b><br>
   Distance: <b id="finalDistance">0 m</b><br>
   Coins: <b id="finalCoins">0</b><br>
   Best: <b id="finalBest">0</b>
  </div>
  <br>
  <button class="main" id="restart">RUN AGAIN</button>
  <div class="menuRow"><button class="secondary" id="overSettings">SETTINGS</button></div>
 </div>

 <div class="panel hidden" id="settingsPanel">
  <h1 style="margin-top:0">SETTINGS</h1>
  <div class="setting">
   <span>Graphics quality</span>
   <div class="quality">
    <button data-quality="low">Low</button>
    <button data-quality="medium">Medium</button>
    <button data-quality="high">High</button>
   </div>
  </div>
  <div class="setting">
   <span>Sound</span>
   <button class="secondary" id="soundSetting">ON</button>
  </div>
  <div class="setting">
   <span>Music</span>
   <button class="secondary" id="musicSetting">ON</button>
  </div>
  <br>
  <button class="secondary" id="back">BACK</button>
 </div>
</div>

<script type="module">
import * as THREE from "https://cdn.jsdelivr.net/npm/three@0.180.0/build/three.module.js";

const scene=new THREE.Scene();
scene.fog=new THREE.FogExp2(0x101827,0.014);

const camera=new THREE.PerspectiveCamera(65,innerWidth/innerHeight,.1,500);
camera.position.set(0,4.5,9);

const renderer=new THREE.WebGLRenderer({antialias:true,powerPreference:"high-performance"});
renderer.setSize(innerWidth,innerHeight);
renderer.setPixelRatio(Math.min(devicePixelRatio,1.75));
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.15;
document.body.insertBefore(renderer.domElement,document.body.firstChild);

const clock=new THREE.Clock();

let quality=localStorage.getItem("vd_quality")||"high";
let soundOn=localStorage.getItem("vd_sound")!=="off";
let musicOn=localStorage.getItem("vd_music")!=="off";

const lanes=[-2.35,0,2.35];
const PLAYER_Z=5;
let gameState="menu";
let score=0,coins=0,distance=0,best=Number(localStorage.getItem("vd_best")||0);
let speed=18;
let lane=1,targetLane=1;
let verticalVelocity=0;
let playerY=0;
let sliding=false;
let slideTime=0;
let spawnTime=0;
let sceneryTime=0;
let elapsed=0;
let dayTime=0;
let magnet=0,shield=0,boost=0;
let objects=[];
let scenery=[];
let particles=[];
let shake=0;
let runCycle=0;
let audioCtx=null;
let musicTimer=null;

const scoreEl=document.getElementById("score");
const distEl=document.getElementById("distance");
const coinEl=document.getElementById("coins");
const bestEl=document.getElementById("best");
bestEl.textContent=best;

const textures={};

function mat(color,rough=.7,metal=0){
 return new THREE.MeshStandardMaterial({color,roughness:rough,metalness:metal});
}

function box(w,h,d,color,rough=.7,metal=0){
 return new THREE.Mesh(new THREE.BoxGeometry(w,h,d),mat(color,rough,metal));
}

function cylinder(r1,r2,h,color,segments=12,rough=.7){
 return new THREE.Mesh(new THREE.CylinderGeometry(r1,r2,h,segments),mat(color,rough));
}

function add(mesh,parent=scene){
 parent.add(mesh);
 return mesh;
}

/* ---------- LIGHTING ---------- */

const hemi=new THREE.HemisphereLight(0x9cc9ff,0x161b20,1.8);
scene.add(hemi);

const moon=new THREE.DirectionalLight(0xb9d5ff,2.8);
moon.position.set(-30,50,25);
moon.castShadow=true;
moon.shadow.mapSize.set(1024,1024);
moon.shadow.camera.left=-50;
moon.shadow.camera.right=50;
moon.shadow.camera.top=50;
moon.shadow.camera.bottom=-50;
scene.add(moon);

const streetLightColor=0xffc77d;

/* ---------- SKY ---------- */

const skyGeo=new THREE.SphereGeometry(180,32,16);
const skyMat=new THREE.MeshBasicMaterial({color:0x101a31,side:THREE.BackSide});
const sky=new THREE.Mesh(skyGeo,skyMat);
scene.add(sky);

const stars=new THREE.Group();
scene.add(stars);
for(let i=0;i<180;i++){
 const p=new THREE.Mesh(
  new THREE.SphereGeometry(.08+Math.random()*.13,4,4),
  new THREE.MeshBasicMaterial({color:0xffffff})
 );
 p.position.set(
  (Math.random()-.5)*260,
  35+Math.random()*90,
  -30-Math.random()*170
 );
 stars.add(p);
}

/* ---------- ROAD ---------- */

const roadMat=mat(0x242932,.82,0);
const road=new THREE.Mesh(new THREE.PlaneGeometry(10,700),roadMat);
road.rotation.x=-Math.PI/2;
road.position.set(0,-.03,-300);
road.receiveShadow=true;
scene.add(road);

const sidewalkMat=mat(0x777d83,.9);
for(const x of [-6,6]){
 const s=new THREE.Mesh(new THREE.PlaneGeometry(2.3,700),sidewalkMat);
 s.rotation.x=-Math.PI/2;
 s.position.set(x,-.01,-300);
 s.receiveShadow=true;
 scene.add(s);
}

const lineMat=mat(0xf1f1df,.55);
for(const x of [-1.175,1.175]){
 const line=new THREE.Mesh(new THREE.PlaneGeometry(.07,700),lineMat);
 line.rotation.x=-Math.PI/2;
 line.position.set(x,.015,-300);
 scene.add(line);
}

function roadMark(){
 const m=new THREE.Mesh(new THREE.PlaneGeometry(.11,3.5),mat(0xe9e7d8,.5));
 m.rotation.x=-Math.PI/2;
 m.position.y=.025;
 m.position.z=-50;
 scene.add(m);
 return m;
}
for(let i=0;i<55;i++){
 const a=roadMark();
 a.position.z=8-i*12;
 scenery.push({mesh:a,type:"mark"});
}

/* ---------- PLAYER ---------- */

const player=new THREE.Group();
player.position.set(0,0,PLAYER_Z);
scene.add(player);

const body=new THREE.Group();
player.add(body);

const skin=mat(0xb87958,.72);
const shirt=mat(0x2469a8,.65);
const pants=mat(0x182333,.85);
const shoes=mat(0x15171c,.55);
const hair=mat(0x171313,.8);

const torso=box(.82,1.35,.48,0x2469a8,.65);
torso.position.y=1.65;
torso.castShadow=true;
body.add(torso);

const head=new THREE.Mesh(new THREE.SphereGeometry(.39,20,16),skin);
head.position.y=2.65;
head.castShadow=true;
body.add(head);

const hairCap=new THREE.Mesh(new THREE.SphereGeometry(.405,20,12,0,Math.PI*2,0,Math.PI*.48),hair);
hairCap.position.y=2.75;
body.add(hairCap);

const neck=cylinder(.16,.16,.18,0xb87958,12);
neck.position.y=2.27;
body.add(neck);

function limb(length,radius,material){
 const g=new THREE.Group();
 const m=cylinder(radius,radius,length,material,10);
 m.position.y=-length/2;
 m.castShadow=true;
 g.add(m);
 return g;
}

const leftArm=limb(1.15,.13,shirt);
const rightArm=limb(1.15,.13,shirt);
leftArm.position.set(-.57,2.08,0);
rightArm.position.set(.57,2.08,0);
body.add(leftArm,rightArm);

const leftLeg=limb(1.35,.17,pants);
const rightLeg=limb(1.35,.17,pants);
leftLeg.position.set(-.23,.98,0);
rightLeg.position.set(.23,.98,0);
body.add(leftLeg,rightLeg);

const leftShoe=box(.3,.17,.62,0x111318,.55);
const rightShoe=box(.3,.17,.62,0x111318,.55);
leftShoe.position.set(-.23,.12,-.12);
rightShoe.position.set(.23,.12,-.12);
body.add(leftShoe,rightShoe);

/* ---------- PLAYER ACCESSORIES ---------- */

const backpack=box(.62,.75,.22,0x10151e,.75);
backpack.position.set(0,1.55,.34);
body.add(backpack);

const shieldVisual=new THREE.Mesh(
 new THREE.SphereGeometry(1.15,24,16),
 new THREE.MeshBasicMaterial({color:0x38bdf8,transparent:true,opacity:.13,wireframe:true})
);
shieldVisual.visible=false;
player.add(shieldVisual);

/* ---------- CITY ---------- */

function building(x,z,w,h,d,color){
 const g=new THREE.Group();
 const b=box(w,h,d,color,.88);
 b.position.y=h/2;
 b.castShadow=true;b.receiveShadow=true;
 g.add(b);

 const windowMat=new THREE.MeshStandardMaterial({
  color:0xffd982,emissive:0x3d2910,emissiveIntensity:.5,
  roughness:.55
 });

 const rows=Math.floor(h/3);
 const cols=Math.max(1,Math.floor(w/2));
 for(let r=0;r<rows;r++){
  for(let c=0;c<cols;c++){
   if(Math.random()<.25)continue;
   const win=new THREE.Mesh(new THREE.PlaneGeometry(.48,.75),windowMat);
   win.position.set(
    -w/2+.8+c*(w-.9)/cols,
    1.3+r*2.5,
    -d/2-.012
   );
   g.add(win);
  }
 }
 g.position.set(x,0,z);
 scene.add(g);
 scenery.push({mesh:g,type:"building",speed:.045});
}

const buildingColors=[0x29313c,0x343c48,0x202a35,0x41444a,0x27333e];
for(let i=0;i<26;i++){
 const z=-i*25-15;
 const side=i%2?-1:1;
 building(
  side*(8+Math.random()*3),
  z,
  3+Math.random()*4,
  7+Math.random()*15,
  5+Math.random()*4,
  buildingColors[Math.floor(Math.random()*buildingColors.length)]
 );
}

/* ---------- STREETLIGHTS ---------- */

function streetLamp(x,z){
 const g=new THREE.Group();
 const pole=cylinder(.07,.09,5.5,0x30363c,8,.45);
 pole.position.y=2.75;
 pole.castShadow=true;
 g.add(pole);

 const arm=box(.75,.08,.08,0x30363c,.45);
 arm.position.set(x>0?-.32:.32,5.25,0);
 g.add(arm);

 const bulb=new THREE.Mesh(
  new THREE.SphereGeometry(.13,10,8),
  new THREE.MeshStandardMaterial({
   color:0xffe3a0,emissive:0xff9e35,emissiveIntensity:3
  })
 );
 bulb.position.set(x>0?-.68:.68,5.16,0);
 g.add(bulb);

 const light=new THREE.PointLight(streetLightColor,2.4,12,2);
 light.position.copy(bulb.position);
 g.add(light);

 g.position.set(x,0,z);
 scene.add(g);
 scenery.push({mesh:g,type:"lamp"});
}

for(let i=0;i<28;i++){
 streetLamp(-6.2,-i*24-5);
 streetLamp(6.2,-i*24-17);
}

/* ---------- TREES ---------- */

function tree(x,z){
 const g=new THREE.Group();
 const trunk=cylinder(.18,.24,2.2,0x5a3927,8);
 trunk.position.y=1.1;
 g.add(trunk);

 const leaves=mat(0x1c6a42,.95);
 for(let i=0;i<3;i++){
  const crown=new THREE.Mesh(new THREE.IcosahedronGeometry(1.1-i*.15,1),leaves);
  crown.position.y=2.2+i*.55;
  crown.position.x=(Math.random()-.5)*.35;
  crown.castShadow=true;
  g.add(crown);
 }
 g.position.set(x,0,z);
 scene.add(g);
 scenery.push({mesh:g,type:"tree"});
}

for(let i=0;i<20;i++){
 tree(-7.4,-i*34-8);
 tree(7.4,-i*34-24);
}

/* ---------- SIGNS ---------- */

function sign(x,z,text,color){
 const g=new THREE.Group();
 const pole=cylinder(.07,.07,2.8,0x555a60,8);
 pole.position.y=1.4;
 g.add(pole);
 const board=box(1.7,.65,.08,color,.55);
 board.position.y=2.8;
 g.add(board);

 const canvas=document.createElement("canvas");
 canvas.width=256;canvas.height=96;
 const c=canvas.getContext("2d");
 c.fillStyle="#10151d";c.fillRect(0,0,256,96);
 c.fillStyle="#ffffff";c.font="bold 32px Arial";
 c.textAlign="center";c.textBaseline="middle";c.fillText(text,128,48);
 const tex=new THREE.CanvasTexture(canvas);
 const face=new THREE.Mesh(
  new THREE.PlaneGeometry(1.55,.55),
  new THREE.MeshBasicMaterial({map:tex})
 );
 face.position.set(0,2.8,-.05);
 g.add(face);
 g.position.set(x,0,z);
 scene.add(g);
 scenery.push({mesh:g,type:"sign"});
}

sign(-5.9,-70,"NORTH",0x1769aa);
sign(5.9,-145,"CITY",0x9b3d58);
sign(-5.9,-225,"RIVER",0x287c66);

/* ---------- VEHICLES ---------- */

function car(z,laneIndex,police=false){
 const g=new THREE.Group();
 const base=box(1.75,.58,3.5,police?0x1b2636:0x6c2930,.48);
 base.position.y=.58;
 base.castShadow=true;
 g.add(base);

 const cabin=box(1.35,.6,1.65,police?0x263a55:0x33383f,.3);
 cabin.position.y=1.08;
 cabin.position.z=.05;
 cabin.castShadow=true;
 g.add(cabin);

 const glass=mat(0x172a3b,.15,.2);
 const windshield=box(1.18,.4,.06,0x172a3b,.15,.3);
 windshield.position.set(0,1.18,-.79);
 g.add(windshield);

 const wheels=[];
 for(const x of [-.91,.91])for(const zz of [-1.05,1.05]){
  const w=new THREE.Mesh(new THREE.CylinderGeometry(.29,.29,.18,12),mat(0x111214,.55));
  w.rotation.z=Math.PI/2;
  w.position.set(x,.38,zz);
  w.castShadow=true;
  g.add(w);wheels.push(w);
 }

 if(police){
  const light=box(.6,.12,.2,0x2244ff,.3);
  light.position.y=1.48;
  g.add(light);
 }

 g.position.set(lanes[laneIndex],0,z);
 scene.add(g);
 return g;
}

/* ---------- OBJECTS ---------- */

const objectGroup=new THREE.Group();
scene.add(objectGroup);

function makeObstacle(type,laneIndex,z){
 const g=new THREE.Group();

 if(type==="barrier"){
  const body=box(2,.8,.65,0xe0a22b,.55);
  body.position.y=.4;
  g.add(body);
  const stripe=box(2.05,.16,.68,0xe8e8e8,.5);
  stripe.position.y=.55;
  g.add(stripe);
  for(const x of [-.75,.75]){
   const leg=box(.16,.75,.16,0x33383c,.6);
   leg.position.set(x,.05,0);
   g.add(leg);
  }
 }

 if(type==="crate"){
  const b=box(1.8,1.8,1.8,0x8c522c,.8);
  b.position.y=.9;
  g.add(b);
  for(let i=-1;i<=1;i+=2){
   const plank=box(.16,1.95,1.9,0xc98243,.75);
   plank.rotation.z=i*.7;
   plank.position.y=.9;
   g.add(plank);
  }
 }

 if(type==="truck"){
  const body=box(2.2,2.1,4.5,0x33465c,.7);
  body.position.y=1.05;
  g.add(body);
  const cab=box(2.05,2.2,1.5,0x9b3c32,.6);
  cab.position.set(0,1.1,-1.45);
  g.add(cab);
  const wind=box(1.55,.55,.06,0x152a3b,.2,.2);
  wind.position.set(0,1.45,-2.22);
  g.add(wind);
 }

 if(type==="gate"){
  const left=box(.28,2.7,.35,0x31383d,.5);
  const right=left.clone();
  left.position.set(-1.05,1.35,0);
  right.position.set(1.05,1.35,0);
  g.add(left,right);
  const top=box(2.4,.3,.38,0xe14c4c,.45);
  top.position.y=2.45;
  g.add(top);
 }

 g.position.set(lanes[laneIndex],0,z);
 g.userData.type=type;
 g.userData.lane=laneIndex;
 objectGroup.add(g);
 objects.push(g);
}

function makeCoin(laneIndex,z){
 const g=new THREE.Group();
 const ring=new THREE.Mesh(
  new THREE.TorusGeometry(.34,.095,10,20),
  new THREE.MeshStandardMaterial({
   color:0xffcf39,metalness:.8,roughness:.2,
   emissive:0x6b4300,emissiveIntensity:.3
  })
 );
 ring.rotation.y=Math.PI/2;
 ring.position.y=1.25;
 ring.castShadow=true;
 g.add(ring);
 g.position.set(lanes[laneIndex],0,z);
 g.userData.type="coin";
 objectGroup.add(g);
 objects.push(g);
}

function makePower(laneIndex,z,type){
 const g=new THREE.Group();
 const colors={shield:0x45c9ff,magnet:0xff4c9c,boost:0x8c6cff};
 const orb=new THREE.Mesh(
  new THREE.IcosahedronGeometry(.48,1),
  new THREE.MeshStandardMaterial({
   color:colors[type],emissive:colors[type],emissiveIntensity:1.3,
   metalness:.4,roughness:.2
  })
 );
 orb.position.y=1.45;
 g.add(orb);

 const ring=new THREE.Mesh(
  new THREE.TorusGeometry(.7,.055,8,20),
  new THREE.MeshBasicMaterial({color:colors[type]})
 );
 ring.rotation.x=Math.PI/2;
 ring.position.y=1.45;
 g.add(ring);

 g.position.set(lanes[laneIndex],0,z);
 g.userData.type="power";
 g.userData.power=type;
 objectGroup.add(g);
 objects.push(g);
}

/* ---------- PARTICLES ---------- */

function particle(x,y,z,color){
 const m=new THREE.Mesh(
  new THREE.SphereGeometry(.055,6,6),
  new THREE.MeshBasicMaterial({color,transparent:true})
 );
 m.position.set(x,y,z);
 objectGroup.add(m);
 particles.push({mesh:m,life:.6,v:new THREE.Vector3(
  (Math.random()-.5)*5,
  Math.random()*5,
  (Math.random()-.5)*5
 )});
}

function burst(position,color=0xffd447){
 for(let i=0;i<15;i++)particle(position.x,position.y,position.z,color);
}

/* ---------- AUDIO ---------- */

function ensureAudio(){
 if(!soundOn)return;
 if(!audioCtx){
  audioCtx=new (window.AudioContext||window.webkitAudioContext)();
 }
 if(audioCtx.state==="suspended")audioCtx.resume();
}

function tone(freq,dur,type="sine",volume=.04){
 if(!soundOn)return;
 try{
  ensureAudio();
  const o=audioCtx.createOscillator();
  const g=audioCtx.createGain();
  o.type=type;o.frequency.value=freq;
  g.gain.setValueAtTime(volume,audioCtx.currentTime);
  g.gain.exponentialRampToValueAtTime(.001,audioCtx.currentTime+dur);
  o.connect(g);g.connect(audioCtx.destination);
  o.start();o.stop(audioCtx.currentTime+dur);
 }catch(e){}
}

function playMusic(){
 if(!musicOn||!soundOn||musicTimer)return;
 ensureAudio();
 const notes=[220,277.18,329.63,369.99,329.63,277.18,246.94,329.63];
 let n=0;
 musicTimer=setInterval(()=>{
  if(gameState==="playing"){
   tone(notes[n%notes.length],.13,"triangle",.012);
   n++;
  }
 },260);
}

function stopMusic(){
 if(musicTimer){clearInterval(musicTimer);musicTimer=null}
}

/* ---------- GAME STATE ---------- */

function showPanel(which){
 document.getElementById("overlay").style.display="flex";
 for(const id of ["mainPanel","pausePanel","overPanel","settingsPanel"])
  document.getElementById(id).classList.add("hidden");
 document.getElementById(which).classList.remove("hidden");
}

function hideOverlay(){
 document.getElementById("overlay").style.display="none";
}

function clearObjects(){
 for(const o of objects)objectGroup.remove(o);
 objects=[];
}

function startGame(){
 ensureAudio();
 clearObjects();
 score=0;coins=0;distance=0;speed=18;
 lane=1;targetLane=1;
 player.position.set(0,0,PLAYER_Z);
 playerY=0;verticalVelocity=0;
 sliding=false;slideTime=0;
 spawnTime=.3;
 elapsed=0;
 magnet=shield=boost=0;
 shake=0;
 gameState="playing";
 hideOverlay();
 updateHUD();
 playMusic();
 clock.start();
}

function gameOver(){
 if(gameState!=="playing")return;
 gameState="over";
 const final=Math.floor(score);
 if(final>best){
  best=final;
  localStorage.setItem("vd_best",best);
 }
 document.getElementById("finalScore").textContent=final;
 document.getElementById("finalDistance").textContent=Math.floor(distance)+" m";
 document.getElementById("finalCoins").textContent=coins;
 document.getElementById("finalBest").textContent=best;
 bestEl.textContent=best;
 tone(95,.45,"sawtooth",.06);
 stopMusic();
 showPanel("overPanel");
}

function pauseGame(){
 if(gameState==="playing"){
  gameState="paused";
  stopMusic();
  showPanel("pausePanel");
 }else if(gameState==="paused"){
  gameState="playing";
  hideOverlay();
  playMusic();
  clock.start();
 }
}

function quitGame(){
 gameState="menu";
 stopMusic();
 showPanel("mainPanel");
}

document.getElementById("play").onclick=startGame;
document.getElementById("restart").onclick=startGame;
document.getElementById("pauseHUD").onclick=pauseGame;
document.getElementById("resume").onclick=pauseGame;
document.getElementById("quit").onclick=quitGame;

document.getElementById("settings").onclick=()=>showPanel("settingsPanel");
document.getElementById("pauseSettings").onclick=()=>showPanel("settingsPanel");
document.getElementById("overSettings").onclick=()=>showPanel("settingsPanel");
document.getElementById("back").onclick=()=>{
 if(gameState==="paused")showPanel("pausePanel");
 else if(gameState==="over")showPanel("overPanel");
 else showPanel("mainPanel");
};

document.querySelectorAll("[data-quality]").forEach(b=>{
 b.onclick=()=>{
  quality=b.dataset.quality;
  localStorage.setItem("vd_quality",quality);
  applyQuality();
 };
});

document.getElementById("soundSetting").onclick=()=>{
 soundOn=!soundOn;
 localStorage.setItem("vd_sound",soundOn?"on":"off");
 document.getElementById("soundSetting").textContent=soundOn?"ON":"OFF";
 if(!soundOn)stopMusic(); else playMusic();
};

document.getElementById("musicSetting").onclick=()=>{
 musicOn=!musicOn;
 localStorage.setItem("vd_music",musicOn?"on":"off");
 document.getElementById("musicSetting").textContent=musicOn?"ON":"OFF";
 if(musicOn)playMusic();else stopMusic();
};

document.getElementById("muteHUD").onclick=()=>{
 soundOn=!soundOn;
 localStorage.setItem("vd_sound",soundOn?"on":"off");
 document.getElementById("muteHUD").textContent=soundOn?"🔊":"🔇";
 document.getElementById("soundSetting").textContent=soundOn?"ON":"OFF";
 if(soundOn)playMusic();else stopMusic();
};

function applyQuality(){
 const buttons=document.querySelectorAll("[data-quality]");
 buttons.forEach(b=>b.classList.toggle("active",b.dataset.quality===quality));

 if(quality==="low"){
  renderer.setPixelRatio(1);
  renderer.shadowMap.enabled=false;
  scene.fog.density=.02;
 }else if(quality==="medium"){
  renderer.setPixelRatio(Math.min(devicePixelRatio,1.25));
  renderer.shadowMap.enabled=true;
  moon.shadow.mapSize.set(512,512);
  scene.fog.density=.016;
 }else{
  renderer.setPixelRatio(Math.min(devicePixelRatio,1.75));
  renderer.shadowMap.enabled=true;
  moon.shadow.mapSize.set(1024,1024);
  scene.fog.density=.014;
 }
}
applyQuality();
document.getElementById("soundSetting").textContent=soundOn?"ON":"OFF";
document.getElementById("musicSetting").textContent=musicOn?"ON":"OFF";

/* ---------- CONTROLS ---------- */

function left(){
 if(gameState!=="playing")return;
 targetLane=Math.max(0,targetLane-1);
 tone(270,.045,"square",.018);
}
function right(){
 if(gameState!=="playing")return;
 targetLane=Math.min(2,targetLane+1);
 tone(330,.045,"square",.018);
}
function jump(){
 if(gameState!=="playing")return;
 if(playerY<=.01&&!sliding){
  verticalVelocity=11.5;
  tone(520,.1,"triangle",.035);
 }
}
function slide(){
 if(gameState!=="playing")return;
 if(playerY<.05){
  sliding=true;
  slideTime=.7;
  tone(170,.08,"triangle",.025);
 }
}

addEventListener("keydown",e=>{
 if(["ArrowLeft","ArrowRight","ArrowUp","ArrowDown"," "].includes(e.key))e.preventDefault();
 const k=e.key.toLowerCase();
 if(k==="a"||e.key==="ArrowLeft")left();
 if(k==="d"||e.key==="ArrowRight")right();
 if(k==="w"||e.key==="ArrowUp"||e.key===" ")jump();
 if(k==="s"||e.key==="ArrowDown")slide();
 if(k==="p")pauseGame();
});

function bindMobile(id,fn){
 const el=document.getElementById(id);
 ["touchstart","mousedown"].forEach(ev=>{
  el.addEventListener(ev,e=>{e.preventDefault();fn()},{passive:false});
 });
}
bindMobile("ml",left);bindMobile("mr",right);
bindMobile("mj",jump);bindMobile("ms",slide);

let touchX=0,touchY=0;
renderer.domElement.addEventListener("touchstart",e=>{
 const t=e.changedTouches[0];
 touchX=t.clientX;touchY=t.clientY;
},{passive:true});
renderer.domElement.addEventListener("touchend",e=>{
 const t=e.changedTouches[0];
 const dx=t.clientX-touchX,dy=t.clientY-touchY;
 if(Math.max(Math.abs(dx),Math.abs(dy))<30)return;
 if(Math.abs(dx)>Math.abs(dy))dx>0?right():left();
 else dy<0?jump():slide();
},{passive:true});

/* ---------- SPAWNING ---------- */

function randomLane(){return Math.floor(Math.random()*3)}

function spawnWave(){
 const difficulty=Math.min(1,distance/1800);
 const r=Math.random();

 if(r<.32){
  const blocked=randomLane();
  const types=["barrier","crate","truck"];
  makeObstacle(types[Math.floor(Math.random()*types.length)],blocked,-75);
  if(Math.random()<.75){
   const safe=[0,1,2].filter(x=>x!==blocked)[Math.floor(Math.random()*2)];
   for(let i=0;i<5;i++)makeCoin(safe,-84-i*7);
  }
 }else if(r<.53){
  const l=randomLane();
  for(let i=0;i<8;i++)makeCoin(l,-65-i*7);
 }else if(r<.68){
  const l=randomLane();
  makeObstacle("gate",l,-75);
 }else if(r<.80){
  const l=randomLane();
  makePower(l,-75,["shield","magnet","boost"][Math.floor(Math.random()*3)]);
 }else if(r<.91){
  const safe=randomLane();
  for(let l=0;l<3;l++){
   if(l!==safe)makeObstacle(Math.random()<.5?"barrier":"crate",l,-75);
  }
  for(let i=0;i<6;i++)makeCoin(safe,-85-i*7);
 }else{
  const l1=randomLane();
  let l2=randomLane();
  if(l2===l1)l2=(l2+1)%3;
  makeObstacle("truck",l1,-75);
  makeObstacle("barrier",l2,-93);
  for(let i=0;i<4;i++)makeCoin(3-l1-l2,-102-i*7);
 }

 if(difficulty>.6&&Math.random()<.35){
  const l=randomLane();
  makeCoin(l,-125);
 }
}

/* ---------- COLLISIONS ---------- */

function hitTest(o){
 const dz=Math.abs(o.position.z-player.position.z);
 if(dz>1.9)return false;
 const dx=Math.abs(o.position.x-player.position.x);
 if(dx>1.25)return false;
 return true;
}

function collect(o){
 const p=o.position.clone();
 p.y+=1.2;
 burst(p,0xffd447);
 coins++;
 score+=100;
 tone(760+Math.random()*180,.07,"sine",.035);
 objectGroup.remove(o);
 objects.splice(objects.indexOf(o),1);
}

function powerup(o){
 const type=o.userData.power;
 if(type==="shield")shield=9;
 if(type==="magnet")magnet=9;
 if(type==="boost")boost=6;
 const p=o.position.clone();p.y+=1;
 burst(p,type==="shield"?0x45c9ff:type==="magnet"?0xff4c9c:0x8c6cff);
 tone(650,.12,"sine",.045);
 objectGroup.remove(o);
 objects.splice(objects.indexOf(o),1);
}

/* ---------- GAME UPDATE ---------- */

function update(dt){
 elapsed+=dt;
 distance+=speed*dt;
 speed=18+Math.min(15,distance*.009);
 if(boost>0)speed+=9;

 score+=dt*(speed*3.0);

 targetLane=Math.max(0,Math.min(2,targetLane));
 const desiredX=lanes[targetLane];
 player.position.x+=
  (desiredX-player.position.x)*Math.min(1,dt*13);

 if(playerY>0||verticalVelocity>0){
  playerY+=verticalVelocity*dt;
  verticalVelocity-=28*dt;
  if(playerY<=0){
   playerY=0;
   verticalVelocity=0;
   tone(105,.045,"sine",.018);
  }
 }

 if(slideTime>0){
  slideTime-=dt;
  if(slideTime<=0)sliding=false;
 }

 magnet=Math.max(0,magnet-dt);
 shield=Math.max(0,shield-dt);
 boost=Math.max(0,boost-dt);

 player.position.y=playerY;

 animatePlayer(dt);

 spawnTime-=dt;
 const interval=Math.max(.38,.85-distance/4500);
 if(spawnTime<=0){
  spawnWave();
  spawnTime=interval*(.8+Math.random()*.25);
 }

 for(let i=objects.length-1;i>=0;i--){
  const o=objects[i];
  o.position.z+=speed*dt;

  if(o.userData.type==="coin"){
   o.rotation.y+=dt*5;
   o.position.y=1.25+Math.sin(elapsed*6+o.position.z)*.12;

   if(magnet>0){
    const dx=player.position.x-o.position.x;
    const dz=player.position.z-o.position.z;
    if(Math.abs(dz)<16&&Math.abs(dx)<7){
     o.position.x+=dx*Math.min(1,dt*5);
     o.position.z+=dz*Math.min(1,dt*5);
    }
   }

   if(hitTest(o)&&Math.abs(playerY-1.2)<1.7)collect(o);
  }else if(o.userData.type==="power"){
   o.rotation.y+=dt*2;
   o.children[1].rotation.z+=dt*2;
   if(hitTest(o)&&playerY<2)powerup(o);
  }else{
   if(hitTest(o)){
    let collision=true;
    const type=o.userData.type;

    if(type==="barrier"&&playerY>1.05)collision=false;
    if(type==="gate"&&sliding)collision=false;

    if(collision){
     if(shield>0){
      shield=0;
      burst(o.position,0x48d9ff);
      shake=.35;
      tone(140,.18,"sawtooth",.05);
      objectGroup.remove(o);
      objects.splice(i,1);
     }else{
      burst(player.position,0xff563d);
      shake=.65;
      gameOver();
      return;
     }
    }
   }
  }

  if(o.position.z>15){
   objectGroup.remove(o);
   objects.splice(i,1);
  }
 }

 updateScenery(dt);
 updateParticles(dt);
 updateCamera(dt);
 updateLighting(dt);
 updateHUD();
}

/* ---------- ANIMATION ---------- */

function animatePlayer(dt){
 runCycle+=dt*(7+speed*.13);

 const running=Math.sin(runCycle);
 const running2=Math.sin(runCycle+Math.PI);

 if(sliding){
  body.rotation.x=-.35;
  leftArm.rotation.x=-1;
  rightArm.rotation.x=-1;
  leftLeg.rotation.x=.5;
  rightLeg.rotation.x=-.5;
 }else{
  body.rotation.x=0;
  leftArm.rotation.x=running*.65;
  rightArm.rotation.x=running2*.65;
  leftLeg.rotation.x=running2*.75;
  rightLeg.rotation.x=running*.75;
 }

 player.rotation.y+=(targetLane-lane)*0.05;

 if(playerY>0){
  body.rotation.x=-.12;
 }
}

/* ---------- SCENERY UPDATE ---------- */

function updateScenery(dt){
 for(const s of scenery){
  s.mesh.position.z+=speed*dt*s.speed;

  if(s.mesh.position.z>25){
   if(s.type==="building"){
    s.mesh.position.z-=650;
   }else if(s.type==="lamp"){
    s.mesh.position.z-=672;
   }else if(s.type==="tree"){
    s.mesh.position.z-=680;
   }else if(s.type==="sign"){
    s.mesh.position.z-=500;
   }else if(s.type==="mark"){
    s.mesh.position.z-=660;
   }
  }
 }
}

/* ---------- CAMERA ---------- */

function updateCamera(dt){
 const targetX=player.position.x*.34;
 camera.position.x+=(targetX-camera.position.x)*Math.min(1,dt*5);

 const targetY=4.1+playerY*.18;
 camera.position.y+=(targetY-camera.position.y)*Math.min(1,dt*5);

 camera.position.z=9+Math.sin(elapsed*1.8)*.04;

 const look=new THREE.Vector3(
  player.position.x*.25,
  1.8+playerY*.2,
  player.position.z-10
 );
 camera.lookAt(look);

 if(shake>0){
  camera.position.x+=(Math.random()-.5)*shake;
  camera.position.y+=(Math.random()-.5)*shake;
  shake=Math.max(0,shake-dt);
 }
}

/* ---------- LIGHT / SKY ---------- */

function updateLighting(dt){
 dayTime=(dayTime+dt*.012)%1;
 const night=.5+.5*Math.sin(dayTime*Math.PI*2);

 const skyColor=new THREE.Color().lerpColors(
  new THREE.Color(0x263d61),
  new THREE.Color(0x070d1b),
  night
 );
 sky.material.color.copy(skyColor);

 hemi.intensity=1.15+night*.7;
 moon.intensity=1.2+night*2.0;

 if(night<.45){
  sky.material.color.lerp(new THREE.Color(0x6d8ca9),.25);
 }
}

/* ---------- PARTICLES UPDATE ---------- */

function updateParticles(dt){
 for(let i=particles.length-1;i>=0;i--){
  const p=particles[i];
  p.life-=dt;
  p.mesh.position.x+=p.v.x*dt;
  p.mesh.position.y+=p.v.y*dt;
  p.mesh.position.z+=p.v.z*dt;
  p.v.y-=9*dt;
  p.mesh.material.opacity=Math.max(0,p.life/.6);
  if(p.life<=0){
   objectGroup.remove(p.mesh);
   particles.splice(i,1);
  }
 }
}

/* ---------- HUD ---------- */

function updateHUD(){
 scoreEl.textContent=Math.floor(score);
 distEl.textContent=Math.floor(distance)+" m";
 coinEl.textContent=coins;
 bestEl.textContent=Math.max(best,Math.floor(score));

 const p=[];
 if(shield>0)p.push("🛡 SHIELD "+shield.toFixed(1));
 if(magnet>0)p.push("🧲 MAGNET "+magnet.toFixed(1));
 if(boost>0)p.push("⚡ BOOST "+boost.toFixed(1));

 const el=document.getElementById("powerHUD");
 el.textContent=p.join("   ");
 el.style.opacity=p.length?"1":"0";

 shieldVisual.visible=shield>0;
 shieldVisual.rotation.y+=.015;
}

/* ---------- RANDOM EVENTS ---------- */

let eventTimer=25;

function environmentalEvent(){
 if(gameState!=="playing")return;

 const r=Math.random();

 if(r<.34){
  /* passing car on a side lane */
  const l=randomLane();
  const c=car(-100,l,Math.random()<.25);
  scenery.push({mesh:c,type:"eventCar",speed:.75});
 }else if(r<.67){
  /* brief atmospheric flash */
  const old=hemi.intensity;
  hemi.intensity=4;
  setTimeout(()=>{hemi.intensity=old},120);
 }else{
  /* extra coin trail */
  const l=randomLane();
  for(let i=0;i<7;i++)makeCoin(l,-70-i*6);
 }

 eventTimer=18+Math.random()*20;
}

/* ---------- MAIN LOOP ---------- */

function animate(){
 requestAnimationFrame(animate);

 const dt=Math.min(clock.getDelta(),.045);

 if(gameState==="playing"){
  update(dt);
  eventTimer-=dt;
  if(eventTimer<=0)environmentalEvent();
 }

 renderer.render(scene,camera);
}
animate();

/* ---------- RESPONSIVE ---------- */

function resize(){
 camera.aspect=innerWidth/innerHeight;
 camera.updateProjectionMatrix();
 renderer.setSize(innerWidth,innerHeight);
 applyQuality();
 document.getElementById("mobile").style.display=
  innerWidth<850?"flex":"none";
}
addEventListener("resize",resize);
resize();

/* ---------- INITIAL UI ---------- */

document.getElementById("mobile").style.display=innerWidth<850?"flex":"none";

/* ---------- VISUAL QUALITY ---------- */

function refreshQualityButtons(){
 document.querySelectorAll("[data-quality]").forEach(b=>{
  b.classList.toggle("active",b.dataset.quality===quality);
 });
}
refreshQualityButtons();

/* ---------- INITIAL CAMERA ---------- */

camera.position.set(0,4.5,9);
camera.lookAt(0,1.7,-5);

/* ---------- PREVENT CONTEXT MENU ON MOBILE ---------- */

renderer.domElement.addEventListener("contextmenu",e=>e.preventDefault());

</script>
</body>
</html>

Game Source: Velocity District 3D

Creator: CyberFalcon70

Libraries: three

Complexity: complex (1362 lines, 35.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: velocity-district-3d-cyberfalcon70" to link back to the original. Then publish at arcadelab.ai/publish.