Velocity District 3D
by CyberFalcon701362 lines35.8 KB🛠️ Three.js (3D graphics)
<!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 ← / → Lane change<br>
W / SPACE / ↑ Jump<br>
S / ↓ Slide<br>
P 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.