METRO VELOCITY
by CyberFalcon701608 lines38.4 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.0,user-scalable=no">
<title>METRO VELOCITY</title>
<style>
*{box-sizing:border-box}
html,body{margin:0;width:100%;height:100%;overflow:hidden;background:#08111b;font-family:Arial,Helvetica,sans-serif}
body{touch-action:none}
#game{position:fixed;inset:0;width:100%;height:100%;display:block}
#hud{position:fixed;inset:0;pointer-events:none;color:#fff}
.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:95px;padding:8px 13px;border-radius:12px;
background:rgba(7,15,25,.62);border:1px solid rgba(255,255,255,.15);
backdrop-filter:blur(8px);box-shadow:0 5px 20px #0005;
font-weight:bold;text-shadow:0 2px 3px #000
}
#actions{display:flex;gap:7px;pointer-events:auto}
.uiButton{
border:1px solid #ffffff33;border-radius:12px;background:#101c2bdb;
color:white;padding:10px 14px;font-size:16px;font-weight:bold;cursor:pointer
}
.uiButton:hover{background:#19304a}
#powerStatus{
position:absolute;top:77px;left:50%;transform:translateX(-50%);
padding:7px 14px;border-radius:20px;background:#071321b8;
font-weight:bold;text-shadow:0 2px 3px #000
}
#mobile{
position:absolute;left:0;right:0;bottom:15px;
display:none;justify-content:space-between;padding:0 18px;
pointer-events:auto
}
.pad{display:flex;gap:8px}
.mobileBtn{
width:58px;height:58px;border-radius:50%;
border:1px solid #ffffff40;background:#071321b5;color:#fff;
font-size:23px;font-weight:bold;backdrop-filter:blur(5px)
}
#overlay{
position:fixed;inset:0;display:flex;align-items:center;justify-content:center;
background:linear-gradient(#02081288,#020812cc);
backdrop-filter:blur(7px);z-index:20
}
.panel{
width:min(92vw,540px);padding:34px;border-radius:25px;text-align:center;
background:linear-gradient(145deg,#132538f5,#07111ef7);
border:1px solid #ffffff25;box-shadow:0 25px 80px #000b
}
.panel h1{
margin:0;font-size:48px;letter-spacing:4px;
background:linear-gradient(90deg,#64d9ff,#a98cff);
color:transparent;background-clip:text;-webkit-background-clip:text
}
.panel h2{margin:0 0 10px;font-size:30px}
.sub{color:#9fb4c9;margin:8px 0 25px}
.mainBtn{
width:100%;padding:15px;border:0;border-radius:13px;
background:linear-gradient(90deg,#168dff,#805cff);
color:white;font-size:19px;font-weight:bold;cursor:pointer
}
.mainBtn:hover{filter:brightness(1.15)}
.menuRow{display:flex;gap:10px;margin-top:10px}
.menuRow button{flex:1}
.controls{
margin-top:23px;color:#b8c8d9;line-height:1.8;font-size:14px
}
.hidden{display:none!important}
.setting{
display:flex;justify-content:space-between;align-items:center;
padding:12px 0;border-bottom:1px solid #ffffff15;color:#d8e4ef
}
select{
background:#101c2b;color:white;border:1px solid #ffffff30;
border-radius:8px;padding:7px
}
#loading{
position:fixed;inset:0;background:#06101b;display:flex;
align-items:center;justify-content:center;color:white;z-index:50;
font-size:18px
}
@media(max-width:700px){
.stat{min-width:auto;padding:7px 9px;font-size:13px}
#actions .uiButton{padding:9px}
#mobile{display:flex}
.panel{padding:25px}
.panel h1{font-size:37px}
}
</style>
</head>
<body>
<div id="loading">Loading Metro Velocity...</div>
<canvas id="game"></canvas>
<div id="hud">
<div class="top">
<div class="stats">
<div class="stat">SCORE <span id="score">0</span></div>
<div class="stat">DIST <span id="distance">0</span> m</div>
<div class="stat">COINS <span id="coins">0</span></div>
<div class="stat">BEST <span id="best">0</span></div>
</div>
<div id="actions">
<button class="uiButton" id="pauseBtn">Ⅱ</button>
<button class="uiButton" id="soundBtn">🔊</button>
<button class="uiButton" id="settingsBtn">⚙</button>
</div>
</div>
<div id="powerStatus"></div>
<div id="mobile">
<div class="pad">
<button class="mobileBtn" id="leftBtn">←</button>
<button class="mobileBtn" id="rightBtn">→</button>
</div>
<div class="pad">
<button class="mobileBtn" id="slideBtn">↓</button>
<button class="mobileBtn" id="jumpBtn">↑</button>
</div>
</div>
</div>
<div id="overlay">
<div class="panel" id="mainPanel">
<h1>METRO VELOCITY</h1>
<p class="sub">A high-speed run through the city.</p>
<p>BEST RUN: <b id="menuBest">0</b></p>
<button class="mainBtn" id="playBtn">START RUN</button>
<div class="controls">
<b>Keyboard</b><br>
A / D or ← / → — Change lane<br>
W / Space / ↑ — Jump<br>
S / ↓ — Slide<br>
P — Pause
</div>
</div>
<div class="panel hidden" id="pausePanel">
<h2>PAUSED</h2>
<p class="sub">The city is waiting.</p>
<button class="mainBtn" id="resumeBtn">RESUME</button>
</div>
<div class="panel hidden" id="overPanel">
<h2>RUN COMPLETE</h2>
<p class="sub">You made it this far.</p>
<p>SCORE: <b id="finalScore">0</b></p>
<p>DISTANCE: <b id="finalDistance">0</b> m</p>
<p>COINS: <b id="finalCoins">0</b></p>
<p>BEST: <b id="finalBest">0</b></p>
<button class="mainBtn" id="restartBtn">RUN AGAIN</button>
</div>
<div class="panel hidden" id="settingsPanel">
<h2>SETTINGS</h2>
<div class="setting">
<span>Graphics Quality</span>
<select id="quality">
<option value="low">Low</option>
<option value="medium" selected>Medium</option>
<option value="high">High</option>
</select>
</div>
<div class="setting">
<span>Sound</span>
<button class="uiButton" id="settingsSound">ON</button>
</div>
<br>
<button class="mainBtn" id="closeSettings">DONE</button>
</div>
</div>
<script type="module">
import * as THREE from
"https://cdn.jsdelivr.net/npm/three@0.170.0/build/three.module.js";
/* =========================================================
METRO VELOCITY
Complete procedural 3D endless runner
========================================================= */
const canvas=document.getElementById("game");
let renderer;
try{
renderer=new THREE.WebGLRenderer({
canvas,
antialias:true,
powerPreference:"high-performance"
});
}catch(e){
document.getElementById("loading").textContent="WebGL is not supported by this browser.";
throw e;
}
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.outputColorSpace=THREE.SRGBColorSpace;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.15;
const scene=new THREE.Scene();
scene.fog=new THREE.FogExp2(0x8295a8,.006);
const camera=new THREE.PerspectiveCamera(
63,innerWidth/innerHeight,.1,650
);
camera.position.set(0,5.2,10);
const clock=new THREE.Clock();
let quality="medium";
let soundOn=true;
let state="menu";
let score=0;
let distance=0;
let coins=0;
let highScore=Number(localStorage.getItem("metroVelocityHigh")||0);
let speed=18;
let baseSpeed=18;
let elapsed=0;
let spawnDistance=0;
let sceneryDistance=0;
let lane=0;
let targetLane=0;
let jumpY=0;
let verticalVelocity=0;
let slideTimer=0;
let wasGrounded=true;
let magnetTimer=0;
let shieldTimer=0;
let boostTimer=0;
let shake=0;
let environmentTime=0;
const laneWidth=3.2;
const trackWidth=11;
const segmentLength=24;
const objects=[];
const activeObjects=[];
const activeScenery=[];
const particles=[];
const materials={};
function mat(color,roughness=.7,metalness=.05){
return new THREE.MeshStandardMaterial({
color,
roughness,
metalness
});
}
materials.asphalt=mat(0x252a30,.88);
materials.sidewalk=mat(0x777b7d,.92);
materials.roadLine=mat(0xdce5e8,.5);
materials.concrete=mat(0x8c9093,.9);
materials.glass=mat(0x25465a,.2,.55);
materials.darkGlass=mat(0x111c25,.18,.7);
materials.tree=mat(0x1c542d,.9);
materials.trunk=mat(0x503b2a,.95);
materials.metal=mat(0x626a70,.35,.8);
materials.yellow=mat(0xffc928,.45,.25);
materials.red=mat(0xd7353d,.55,.2);
materials.blue=mat(0x2788c7,.5,.2);
materials.white=mat(0xe7e9e6,.5);
materials.orange=mat(0xf47c32,.5,.15);
const white=0xffffff;
const hemi=new THREE.HemisphereLight(0xdceaff,0x26323b,2.1);
scene.add(hemi);
const sun=new THREE.DirectionalLight(0xfff0d0,3.0);
sun.position.set(-70,100,80);
sun.castShadow=true;
sun.shadow.mapSize.set(2048,2048);
sun.shadow.camera.left=-80;
sun.shadow.camera.right=80;
sun.shadow.camera.top=80;
sun.shadow.camera.bottom=-80;
scene.add(sun);
const ambient=new THREE.AmbientLight(0x506070,.35);
scene.add(ambient);
/* ---------- texture generators ---------- */
function canvasTexture(draw,w=256,h=256){
const c=document.createElement("canvas");
c.width=w;c.height=h;
const x=c.getContext("2d");
draw(x,w,h);
const t=new THREE.CanvasTexture(c);
t.colorSpace=THREE.SRGBColorSpace;
t.wrapS=t.wrapT=THREE.RepeatWrapping;
return t;
}
const asphaltTexture=canvasTexture((c,w,h)=>{
c.fillStyle="#292d31";
c.fillRect(0,0,w,h);
for(let i=0;i<1800;i++){
const v=30+Math.random()*35;
c.fillStyle=`rgb(${v},${v+2},${v+3})`;
c.fillRect(Math.random()*w,Math.random()*h,1+Math.random()*2,1+Math.random()*2);
}
},512,512);
asphaltTexture.repeat.set(2,8);
materials.asphalt.map=asphaltTexture;
const concreteTexture=canvasTexture((c,w,h)=>{
c.fillStyle="#7b7d7d";
c.fillRect(0,0,w,h);
for(let i=0;i<900;i++){
const v=80+Math.random()*45;
c.fillStyle=`rgb(${v},${v},${v})`;
c.fillRect(Math.random()*w,Math.random()*h,1+Math.random()*5,1+Math.random()*2);
}
},256,256);
materials.sidewalk.map=concreteTexture;
const buildingTexture=canvasTexture((c,w,h)=>{
const grad=c.createLinearGradient(0,0,w,h);
grad.addColorStop(0,"#77818a");
grad.addColorStop(1,"#343d46");
c.fillStyle=grad;c.fillRect(0,0,w,h);
for(let y=10;y<h;y+=30){
for(let x=8;x<w;x+=27){
const lit=Math.random()>.38;
c.fillStyle=lit?"#c9d4d2":"#1c2830";
c.fillRect(x,y,14,18);
if(lit){
c.fillStyle="#eef2d0";
c.fillRect(x+2,y+2,10,14);
}
}
}
},256,512);
buildingTexture.repeat.set(1,1);
const grassTexture=canvasTexture((c,w,h)=>{
c.fillStyle="#274d2e";c.fillRect(0,0,w,h);
for(let i=0;i<1600;i++){
c.fillStyle=Math.random()>.5?"#356438":"#1e4027";
c.fillRect(Math.random()*w,Math.random()*h,1,3+Math.random()*5);
}
},256,256);
grassTexture.repeat.set(5,20);
/* ---------- basic geometry helpers ---------- */
function box(w,h,d,material){
const m=new THREE.Mesh(new THREE.BoxGeometry(w,h,d),material);
m.castShadow=true;m.receiveShadow=true;
return m;
}
function cyl(r,h,material,segments=12){
const m=new THREE.Mesh(new THREE.CylinderGeometry(r,r,h,segments),material);
m.castShadow=true;m.receiveShadow=true;
return m;
}
function addMesh(parent,m,x,y,z){
m.position.set(x,y,z);
parent.add(m);
return m;
}
/* ---------- player ---------- */
const player=new THREE.Group();
player.position.set(0,0,5);
scene.add(player);
const body=new THREE.Group();
player.add(body);
const skin=mat(0xc88665,.75);
const shirt=mat(0x197bc0,.62);
const pants=mat(0x202b39,.8);
const shoe=mat(0x202124,.65);
const hair=mat(0x202020,.7);
const torso=box(.95,1.35,.55,shirt);
torso.position.y=2.55;
body.add(torso);
const neck=cyl(.16,.22,skin,12);
neck.position.y=3.28;
body.add(neck);
const head=new THREE.Mesh(
new THREE.SphereGeometry(.43,20,16),
skin
);
head.position.y=3.72;
head.castShadow=true;
body.add(head);
const hairMesh=new THREE.Mesh(
new THREE.SphereGeometry(.445,20,12,0,Math.PI*2,0,Math.PI*.55),
hair
);
hairMesh.position.y=3.82;
hairMesh.scale.set(1,1,.95);
body.add(hairMesh);
const armL=new THREE.Group();
const armR=new THREE.Group();
armL.position.set(-.62,2.9,0);
armR.position.set(.62,2.9,0);
body.add(armL,armR);
const upperArmL=box(.25,.9,.27,shirt);
upperArmL.position.y=-.45;
armL.add(upperArmL);
const upperArmR=upperArmL.clone();
armR.add(upperArmR);
const handL=cyl(.17,.3,skin);
handL.position.y=-.98;
armL.add(handL);
const handR=handL.clone();
armR.add(handR);
const legL=new THREE.Group();
const legR=new THREE.Group();
legL.position.set(-.28,1.9,0);
legR.position.set(.28,1.9,0);
body.add(legL,legR);
const thighL=box(.31,.85,pants);
thighL.position.y=-.43;
legL.add(thighL);
const thighR=thighL.clone();
legR.add(thighR);
const shoeL=box(.38,.24,.75,shoe);
shoeL.position.set(0,-.93,.16);
legL.add(shoeL);
const shoeR=shoeL.clone();
legR.add(shoeR);
const shieldVisual=new THREE.Mesh(
new THREE.SphereGeometry(2.25,24,16),
new THREE.MeshBasicMaterial({
color:0x40c9ff,
transparent:true,
opacity:.13,
wireframe:true
})
);
shieldVisual.position.y=2.15;
shieldVisual.visible=false;
player.add(shieldVisual);
/* ---------- city ---------- */
const worldGroup=new THREE.Group();
scene.add(worldGroup);
const roadSegments=[];
const sceneryPool=[];
const segmentCount=30;
function createRoadSegment(){
const g=new THREE.Group();
const road=box(trackWidth,.18,segmentLength,materials.asphalt);
road.position.y=-.12;
g.add(road);
const leftSide=box(4,.28,segmentLength,materials.sidewalk);
leftSide.position.set(-7.5,.02,0);
g.add(leftSide);
const rightSide=leftSide.clone();
rightSide.position.x=7.5;
g.add(rightSide);
const grassL=box(12,.15,segmentLength, new THREE.MeshStandardMaterial({
map:grassTexture,roughness:.95
}));
grassL.position.set(-15,-.05,0);
g.add(grassL);
const grassR=grassL.clone();
grassR.position.x=15;
g.add(grassR);
for(let l=1;l<3;l++){
const line=box(.09,.025,segmentLength*.72,materials.roadLine);
line.position.set((l-1.5)*laneWidth+laneWidth/2,.015,0);
line.material=materials.roadLine;
g.add(line);
}
return g;
}
for(let i=0;i<segmentCount;i++){
const s=createRoadSegment();
s.position.z=-i*segmentLength;
worldGroup.add(s);
roadSegments.push(s);
}
/* ---------- city objects ---------- */
function building(){
const g=new THREE.Group();
const w=5+Math.random()*9;
const h=10+Math.random()*30;
const d=8+Math.random()*12;
const b=box(w,h,d,new THREE.MeshStandardMaterial({
map:buildingTexture,
roughness:.72,
metalness:.08
}));
b.position.y=h/2;
g.add(b);
if(Math.random()<.5){
const roof=box(w+.2,.4,d+.2,materials.concrete);
roof.position.y=h+.2;
g.add(roof);
}
if(Math.random()<.3){
const antenna=cyl(.08,4,materials.metal);
antenna.position.y=h+2;
g.add(antenna);
}
return g;
}
function streetLight(){
const g=new THREE.Group();
const pole=cyl(.09,5.3,materials.metal,10);
pole.position.y=2.65;
g.add(pole);
const arm=box(1.4,.09,.09,materials.metal);
arm.position.set(.65,5.15,0);
g.add(arm);
const lamp=new THREE.Mesh(
new THREE.SphereGeometry(.18,10,8),
new THREE.MeshStandardMaterial({
color:0xffe6a0,emissive:0xffb12b,emissiveIntensity:2
})
);
lamp.position.set(1.28,5.05,0);
g.add(lamp);
return g;
}
function tree(){
const g=new THREE.Group();
const trunk=cyl(.28,2.5,materials.trunk,10);
trunk.position.y=1.25;
g.add(trunk);
const crown=new THREE.Mesh(
new THREE.IcosahedronGeometry(1.6,1),
materials.tree
);
crown.position.y=3.1;
crown.scale.set(1.05,1.3,1.05);
crown.castShadow=true;
g.add(crown);
return g;
}
function car(){
const g=new THREE.Group();
const colors=[0x233d57,0x9d2833,0xc6a32a,0xe4e7e9,0x242a30];
const color=colors[Math.floor(Math.random()*colors.length)];
const base=box(1.75,.5,3.5,mat(color,.38,.25));
base.position.y=.55;
g.add(base);
const cabin=box(1.45,.62,1.65,materials.darkGlass);
cabin.position.set(0,1.02,.1);
g.add(cabin);
const wheelMat=mat(0x151719,.65);
for(const x of [-.95,.95]){
for(const z of [-1.05,1.05]){
const wh=new THREE.Mesh(
new THREE.CylinderGeometry(.38,.38,.18,16),
wheelMat
);
wh.rotation.z=Math.PI/2;
wh.position.set(x,.42,z);
g.add(wh);
}
}
return g;
}
function sign(){
const g=new THREE.Group();
const pole=cyl(.06,3.5,materials.metal);
pole.position.y=1.75;
g.add(pole);
const board=box(2.3,1,.12,mat(0x174f72,.55,.2));
board.position.y=3.2;
g.add(board);
return g;
}
function populateScenery(){
for(let i=0;i<65;i++){
const z=-20-Math.random()*650;
const side=Math.random()<.5?-1:1;
let o;
const r=Math.random();
if(r<.42)o=building();
else if(r<.64)o=tree();
else if(r<.84)o=streetLight();
else if(r<.94)o=car();
else o=sign();
o.position.z=z;
o.position.x=side*(10+Math.random()*17);
if(o.type==="Group"){
const scale=.7+Math.random()*1.1;
o.scale.setScalar(scale);
}
worldGroup.add(o);
activeScenery.push(o);
}
}
populateScenery();
/* ---------- obstacle/pickup creation ---------- */
const pool=[];
function createObject(){
const g=new THREE.Group();
g.userData.kind="";
g.visible=false;
scene.add(g);
pool.push(g);
return g;
}
for(let i=0;i<100;i++)createObject();
function getObject(){
for(const o of pool){
if(!o.visible)return o;
}
return createObject();
}
function clearGroup(g){
while(g.children.length)g.remove(g.children[0]);
}
function setupObject(type,laneIndex,z){
const g=getObject();
clearGroup(g);
g.visible=true;
g.userData.kind=type;
g.userData.lane=laneIndex;
g.userData.hit=false;
g.position.set(laneIndex*laneWidth,z,0);
if(type==="crate"){
const c=box(2.1,1.9,2.1,materials.orange);
c.position.y=.95;
g.add(c);
const stripe=box(2.15,.16,2.15,materials.yellow);
stripe.position.y=1.25;
g.add(stripe);
}
if(type==="barrier"){
const base=box(2.6,1.15,.65,materials.red);
base.position.y=.58;
g.add(base);
const a=box(.22,1.8,.22,materials.yellow);
a.position.set(-1.05,1.35,0);
g.add(a);
const b=a.clone();
b.position.x=1.05;
g.add(b);
const rail=box(2.4,.18,.75,materials.yellow);
rail.position.y=1.25;
g.add(rail);
}
if(type==="overhead"){
const left=box(.28,2.7,.35,materials.metal);
left.position.set(-1.05,1.35,0);
g.add(left);
const right=left.clone();
right.position.x=1.05;
g.add(right);
const top=box(2.5,.3,.5,materials.red);
top.position.y=2.5;
g.add(top);
}
if(type==="train"){
const t=box(2.7,3.4,6.2,materials.blue);
t.position.y=1.7;
g.add(t);
const windows=box(2.45,1.25,2.7,materials.darkGlass);
windows.position.set(0,2.25,-.2);
g.add(windows);
const light=box(2.5,.22,.15,materials.yellow);
light.position.set(0,.9,3.1);
g.add(light);
}
if(type==="coin"){
const coin=new THREE.Mesh(
new THREE.CylinderGeometry(.46,.46,.13,20),
new THREE.MeshStandardMaterial({
color:0xffd22e,
metalness:.8,
roughness:.22,
emissive:0x6e4200,
emissiveIntensity:.18
})
);
coin.rotation.x=Math.PI/2;
coin.position.y=1.7;
coin.castShadow=true;
g.add(coin);
g.userData.spin=coin;
}
if(type==="magnet"||type==="shield"||type==="boost"){
const colors={
magnet:0xd54dff,
shield:0x3bc7ff,
boost:0xff7043
};
const orb=new THREE.Mesh(
new THREE.IcosahedronGeometry(.65,2),
new THREE.MeshStandardMaterial({
color:colors[type],
emissive:colors[type],
emissiveIntensity:.9,
roughness:.2,
metalness:.35
})
);
orb.position.y=1.7;
g.add(orb);
g.userData.orb=orb;
}
activeObjects.push(g);
return g;
}
/* ---------- spawning ---------- */
function spawnCoinLine(l,count=5,start=-70,spacing=4){
for(let i=0;i<count;i++){
setupObject("coin",l,start-i*spacing);
}
}
function spawnPattern(){
const d=Math.min(1,distance/2500);
const r=Math.random();
if(r<.24){
const blocked=Math.floor(Math.random()*3);
setupObject(Math.random()<.5?"crate":"barrier",blocked,-90);
const safe=[0,1,2].filter(x=>x!==blocked)[
Math.floor(Math.random()*2)
];
spawnCoinLine(safe,5,-105,4);
}
else if(r<.42){
const l=Math.floor(Math.random()*3);
spawnCoinLine(l,7,-90,4);
}
else if(r<.58){
const l=Math.floor(Math.random()*3);
setupObject("overhead",l,-95);
spawnCoinLine((l+1)%3,5,-112,5);
}
else if(r<.70){
const l=Math.floor(Math.random()*3);
setupObject("train",l,-100);
spawnCoinLine((l+2)%3,6,-115,5);
}
else if(r<.83){
const l=Math.floor(Math.random()*3);
const powers=["magnet","shield","boost"];
setupObject(powers[Math.floor(Math.random()*3)],l,-90);
spawnCoinLine(l,5,-105,4);
}
else{
const safe=Math.floor(Math.random()*3);
for(let l=0;l<3;l++){
if(l!==safe){
setupObject("crate",l,-95);
}
}
spawnCoinLine(safe,8,-110,4);
}
if(d>.45&&Math.random()<.35){
const l=Math.floor(Math.random()*3);
setupObject("crate",l,-145);
}
}
/* ---------- particles ---------- */
function particle(x,y,z,color=0xffd83d){
const p=new THREE.Mesh(
new THREE.SphereGeometry(.055,6,6),
new THREE.MeshBasicMaterial({color,transparent:true})
);
p.position.set(x,y,z);
p.userData.life=.55;
p.userData.v=new THREE.Vector3(
(Math.random()-.5)*3,
Math.random()*4+1,
(Math.random()-.5)*3
);
scene.add(p);
particles.push(p);
}
function burst(x,y,z,color,n=14){
for(let i=0;i<n;i++)particle(x,y,z,color);
}
/* ---------- audio ---------- */
let audioCtx=null;
function audioInit(){
if(!soundOn)return;
if(!audioCtx){
const AC=window.AudioContext||window.webkitAudioContext;
if(!AC)return;
audioCtx=new AC();
}
if(audioCtx.state==="suspended")audioCtx.resume();
}
function tone(freq,duration,type="sine",volume=.045){
if(!soundOn)return;
audioInit();
if(!audioCtx)return;
const o=audioCtx.createOscillator();
const g=audioCtx.createGain();
o.type=type;
o.frequency.setValueAtTime(freq,audioCtx.currentTime);
g.gain.setValueAtTime(volume,audioCtx.currentTime);
g.gain.exponentialRampToValueAtTime(
.001,audioCtx.currentTime+duration
);
o.connect(g);
g.connect(audioCtx.destination);
o.start();
o.stop(audioCtx.currentTime+duration);
}
function coinSound(){
tone(720,.07,"sine",.055);
setTimeout(()=>tone(1000,.08,"sine",.035),45);
}
function jumpSound(){tone(430,.13,"triangle",.045)}
function powerSound(){
tone(500,.12,"sine",.05);
setTimeout(()=>tone(850,.18,"sine",.04),80);
}
function crashSound(){
tone(90,.35,"sawtooth",.08);
setTimeout(()=>tone(55,.4,"square",.04),100);
}
/* ---------- game controls ---------- */
function changeLane(dir){
if(state!=="playing")return;
targetLane=Math.max(-1,Math.min(1,targetLane+dir));
tone(dir>0?360:280,.045,"square",.025);
}
function jump(){
if(state!=="playing")return;
if(jumpY<=.02&&slideTimer<=0){
verticalVelocity=12.5;
wasGrounded=false;
jumpSound();
}
}
function slide(){
if(state!=="playing")return;
if(jumpY<.08){
slideTimer=.72;
tone(180,.08,"triangle",.025);
}
}
function pause(){
if(state==="playing"){
state="paused";
showPanel("pause");
}else if(state==="paused"){
state="playing";
hidePanels();
clock.getDelta();
}
}
addEventListener("keydown",e=>{
const k=e.key.toLowerCase();
if(["arrowleft","arrowright","arrowup","arrowdown"," "].includes(k))
e.preventDefault();
if(k==="arrowleft"||k==="a")changeLane(-1);
if(k==="arrowright"||k==="d")changeLane(1);
if(k==="arrowup"||k==="w"||k===" ")jump();
if(k==="arrowdown"||k==="s")slide();
if(k==="p")pause();
});
document.getElementById("leftBtn").onpointerdown=()=>changeLane(-1);
document.getElementById("rightBtn").onpointerdown=()=>changeLane(1);
document.getElementById("jumpBtn").onpointerdown=()=>jump();
document.getElementById("slideBtn").onpointerdown=()=>slide();
let touchX=0,touchY=0;
canvas.addEventListener("pointerdown",e=>{
if(e.pointerType==="touch"){
touchX=e.clientX;
touchY=e.clientY;
}
});
canvas.addEventListener("pointerup",e=>{
if(e.pointerType!=="touch")return;
const dx=e.clientX-touchX;
const dy=e.clientY-touchY;
if(Math.max(Math.abs(dx),Math.abs(dy))<30)return;
if(Math.abs(dx)>Math.abs(dy)){
changeLane(dx>0?1:-1);
}else{
if(dy<0)jump();
else slide();
}
});
/* ---------- UI ---------- */
const overlay=document.getElementById("overlay");
const panels=[
document.getElementById("mainPanel"),
document.getElementById("pausePanel"),
document.getElementById("overPanel"),
document.getElementById("settingsPanel")
];
function showPanel(name){
overlay.classList.remove("hidden");
panels.forEach(p=>p.classList.add("hidden"));
if(name==="menu")document.getElementById("mainPanel").classList.remove("hidden");
if(name==="pause")document.getElementById("pausePanel").classList.remove("hidden");
if(name==="over")document.getElementById("overPanel").classList.remove("hidden");
if(name==="settings")document.getElementById("settingsPanel").classList.remove("hidden");
}
function hidePanels(){
overlay.classList.add("hidden");
}
document.getElementById("playBtn").onclick=startGame;
document.getElementById("restartBtn").onclick=startGame;
document.getElementById("pauseBtn").onclick=pause;
document.getElementById("resumeBtn").onclick=pause;
document.getElementById("soundBtn").onclick=()=>{
soundOn=!soundOn;
document.getElementById("soundBtn").textContent=soundOn?"🔊":"🔇";
document.getElementById("settingsSound").textContent=soundOn?"ON":"OFF";
if(soundOn)audioInit();
};
document.getElementById("settingsBtn").onclick=()=>{
if(state==="playing"){
state="paused";
}
showPanel("settings");
};
document.getElementById("closeSettings").onclick=()=>{
if(state==="paused"){
state="playing";
hidePanels();
}else{
showPanel("menu");
}
};
document.getElementById("settingsSound").onclick=()=>{
soundOn=!soundOn;
document.getElementById("settingsSound").textContent=soundOn?"ON":"OFF";
document.getElementById("soundBtn").textContent=soundOn?"🔊":"🔇";
};
document.getElementById("quality").onchange=e=>{
quality=e.target.value;
applyQuality();
};
/* ---------- quality ---------- */
function applyQuality(){
const high=quality==="high";
const low=quality==="low";
renderer.setPixelRatio(
Math.min(devicePixelRatio,low?1:high?2:1.5)
);
renderer.shadowMap.enabled=!low;
sun.shadow.mapSize.set(
low?512:high?2048:1024,
low?512:high?2048:1024
);
scene.fog.density=low?.009:quality==="medium"?.006:.0048;
}
/* ---------- game state ---------- */
function resetObjects(){
for(const o of activeObjects)o.visible=false;
activeObjects.length=0;
}
function resetScenery(){
for(const o of activeScenery)worldGroup.remove(o);
activeScenery.length=0;
populateScenery();
}
function startGame(){
audioInit();
score=0;
distance=0;
coins=0;
elapsed=0;
spawnDistance=0;
sceneryDistance=0;
speed=18;
baseSpeed=18;
lane=0;
targetLane=0;
jumpY=0;
verticalVelocity=0;
slideTimer=0;
magnetTimer=0;
shieldTimer=0;
boostTimer=0;
shake=0;
environmentTime=0;
resetObjects();
resetScenery();
player.position.set(0,0,5);
player.rotation.set(0,0,0);
for(const p of particles)scene.remove(p);
particles.length=0;
state="playing";
hidePanels();
tone(300,.1,"triangle",.04);
tone(500,.12,"triangle",.035);
}
function gameOver(){
if(state!=="playing")return;
state="over";
crashSound();
const final=Math.floor(score);
if(final>highScore){
highScore=final;
localStorage.setItem("metroVelocityHigh",highScore);
}
document.getElementById("finalScore").textContent=final;
document.getElementById("finalDistance").textContent=Math.floor(distance);
document.getElementById("finalCoins").textContent=coins;
document.getElementById("finalBest").textContent=highScore;
document.getElementById("menuBest").textContent=highScore;
document.getElementById("best").textContent=highScore;
showPanel("over");
}
/* ---------- collisions ---------- */
function playerBox(){
const height=slideTimer>0?1.7:3.8;
const bottom=jumpY;
return {
minX:player.position.x-.55,
maxX:player.position.x+.55,
minY:bottom,
maxY:bottom+height,
minZ:player.position.z-1,
maxZ:player.position.z+1
};
}
function objectBounds(o){
const p=o.position;
if(o.userData.kind==="coin"||
o.userData.kind==="magnet"||
o.userData.kind==="shield"||
o.userData.kind==="boost"){
return {
minX:p.x-.8,maxX:p.x+.8,
minY:1,maxY:2.5,
minZ:p.z-.8,maxZ:p.z+.8
};
}
if(o.userData.kind==="crate"||
o.userData.kind==="barrier"){
return {
minX:p.x-1.35,maxX:p.x+1.35,
minY:0,maxY:o.userData.kind==="crate"?2:1.5,
minZ:p.z-1.1,maxZ:p.z+1.1
};
}
if(o.userData.kind==="overhead"){
return {
minX:p.x-1.3,maxX:p.x+1.3,
minY:0,maxY:2.7,
minZ:p.z-.8,maxZ:p.z+.8
};
}
return {
minX:p.x-1.45,maxX:p.x+1.45,
minY:0,maxY:3.5,
minZ:p.z-3.2,maxZ:p.z+3.2
};
}
function intersects(a,b){
return a.minX<b.maxX&&a.maxX>b.minX&&
a.minY<b.maxY&&a.maxY>b.minY&&
a.minZ<b.maxZ&&a.maxZ>b.minZ;
}
/* ---------- object updates ---------- */
function updateObjects(dt){
const pbox=playerBox();
for(let i=activeObjects.length-1;i>=0;i--){
const o=activeObjects[i];
o.position.z+=speed*dt;
if(o.userData.spin){
o.userData.spin.rotation.z+=dt*7;
o.userData.spin.rotation.y+=dt*3;
}
if(o.userData.orb){
o.userData.orb.rotation.x+=dt*2;
o.userData.orb.rotation.y+=dt*3;
o.position.y=1.65+Math.sin(environmentTime*4+o.id)*.18;
}
/* Magnet attraction */
if(
magnetTimer>0 &&
o.userData.kind==="coin" &&
o.position.z>0 &&
o.position.z<12
){
const dx=player.position.x-o.position.x;
if(Math.abs(dx)<8){
o.position.x+=dx*dt*8;
o.position.y+=(2.1-o.position.y)*dt*5;
}
}
const bounds=objectBounds(o);
if(intersects(pbox,bounds)){
const kind=o.userData.kind;
if(kind==="coin"){
coins++;
score+=100;
coinSound();
burst(o.position.x,o.position.y,o.position.z,0xffd52f,12);
o.visible=false;
activeObjects.splice(i,1);
continue;
}
if(kind==="magnet"||kind==="shield"||kind==="boost"){
if(kind==="magnet")magnetTimer=9;
if(kind==="shield")shieldTimer=10;
if(kind==="boost")boostTimer=6;
powerSound();
burst(
o.position.x,o.position.y,o.position.z,
kind==="shield"?0x39cfff:
kind==="magnet"?0xd65cff:0xff7040,
22
);
o.visible=false;
activeObjects.splice(i,1);
continue;
}
/* Jump clears low obstacles */
if((kind==="crate"||kind==="barrier")&&jumpY>1.45){
continue;
}
/* Slide clears overhead bar */
if(kind==="overhead"&&slideTimer>0){
continue;
}
if(shieldTimer>0){
shieldTimer=0;
shake=.25;
burst(o.position.x,2,o.position.z,0x54dfff,25);
tone(170,.15,"sawtooth",.05);
o.visible=false;
activeObjects.splice(i,1);
continue;
}
gameOver();
return;
}
if(o.position.z>15){
o.visible=false;
activeObjects.splice(i,1);
}
}
}
/* ---------- scenery movement ---------- */
function updateScenery(dt){
for(const o of activeScenery){
o.position.z+=speed*dt;
if(o.position.z>40){
o.position.z=-650-Math.random()*250;
o.position.x=(Math.random()<.5?-1:1)*(10+Math.random()*17);
}
}
for(const s of roadSegments){
s.position.z+=speed*dt;
if(s.position.z>segmentLength){
let min=Infinity;
for(const r of roadSegments)min=Math.min(min,r.position.z);
s.position.z=min-segmentLength;
}
}
}
/* ---------- player animation ---------- */
function updatePlayer(dt){
const previousJump=jumpY;
if(jumpY>0||verticalVelocity>0){
jumpY+=verticalVelocity*dt;
verticalVelocity-=30*dt;
if(jumpY<=0){
jumpY=0;
verticalVelocity=0;
if(!wasGrounded){
tone(100,.06,"triangle",.025);
wasGrounded=true;
}
}
}
if(jumpY>0.05)wasGrounded=false;
if(slideTimer>0){
slideTimer-=dt;
body.rotation.x=THREE.MathUtils.lerp(body.rotation.x,-.8,dt*12);
body.position.y=THREE.MathUtils.lerp(body.position.y,-.7,dt*12);
}else{
body.rotation.x=THREE.MathUtils.lerp(body.rotation.x,0,dt*12);
body.position.y=THREE.MathUtils.lerp(body.position.y,0,dt*12);
}
const desiredX=targetLane*laneWidth;
player.position.x=THREE.MathUtils.lerp(
player.position.x,desiredX,1-Math.pow(.001,dt)
);
const run=Math.sin(environmentTime*speed*.55);
const runFast=Math.sin(environmentTime*speed*.55+Math.PI);
if(slideTimer<=0){
legL.rotation.x=run*.75;
legR.rotation.x=runFast*.75;
armL.rotation.x=runFast*.65;
armR.rotation.x=run*.65;
armL.rotation.z=-.08;
armR.rotation.z=.08;
}else{
legL.rotation.x=.9;
legR.rotation.x=.9;
armL.rotation.x=-.4;
armR.rotation.x=-.4;
}
player.position.y=jumpY;
player.rotation.z=THREE.MathUtils.lerp(
player.rotation.z,
(desiredX-player.position.x)*-.06,
dt*8
);
if(previousJump>0&&jumpY===0){
burst(player.position.x,.2,player.position.z,0xffffff,5);
}
}
/* ---------- camera ---------- */
function updateCamera(dt){
const targetX=player.position.x*.28;
camera.position.x=THREE.MathUtils.lerp(
camera.position.x,targetX,dt*5
);
camera.position.y=THREE.MathUtils.lerp(
camera.position.y,
5.1+jumpY*.15,
dt*5
);
camera.position.z=THREE.MathUtils.lerp(
camera.position.z,10,dt*4
);
camera.lookAt(
player.position.x*.12,
1.9+jumpY*.2,
-10
);
if(shake>0){
camera.position.x+=(Math.random()-.5)*shake;
camera.position.y+=(Math.random()-.5)*shake;
shake=Math.max(0,shake-dt*1.5);
}
}
/* ---------- atmosphere ---------- */
function updateEnvironment(dt){
environmentTime+=dt;
const cycle=(environmentTime/100)%1;
const sunAngle=cycle*Math.PI*2;
sun.position.x=Math.cos(sunAngle)*100;
sun.position.y=45+Math.sin(sunAngle)*75;
sun.position.z=60;
const daylight=Math.max(.15,Math.sin(sunAngle)*.5+.55);
sun.intensity=1.1+daylight*2.4;
hemi.intensity=.8+daylight*1.5;
ambient.intensity=.2+daylight*.3;
const skyTop=new THREE.Color().setHSL(
.58,.32,.12+daylight*.34
);
scene.background=skyTop;
scene.fog.color.copy(
new THREE.Color().setHSL(.58,.25,.18+daylight*.28)
);
}
/* ---------- HUD ---------- */
function updateHUD(){
document.getElementById("score").textContent=Math.floor(score);
document.getElementById("distance").textContent=Math.floor(distance);
document.getElementById("coins").textContent=coins;
document.getElementById("best").textContent=Math.max(
highScore,Math.floor(score)
);
let text=[];
if(magnetTimer>0)
text.push("🧲 MAGNET "+magnetTimer.toFixed(1));
if(shieldTimer>0)
text.push("🛡 SHIELD "+shieldTimer.toFixed(1));
if(boostTimer>0)
text.push("⚡ BOOST "+boostTimer.toFixed(1));
document.getElementById("powerStatus").textContent=text.join(" ");
}
/* ---------- main update ---------- */
function update(dt){
elapsed+=dt;
const difficulty=Math.min(1,distance/3000);
baseSpeed=18+difficulty*13+Math.min(10,distance/1800);
speed=baseSpeed+(boostTimer>0?10:0);
distance+=speed*dt;
score+=speed*dt*2.3;
spawnDistance-=speed*dt;
if(spawnDistance<=0){
spawnPattern();
const interval=Math.max(
17,
31-distance*.003
);
spawnDistance=interval*(.75+Math.random()*.3);
}
magnetTimer=Math.max(0,magnetTimer-dt);
shieldTimer=Math.max(0,shieldTimer-dt);
boostTimer=Math.max(0,boostTimer-dt);
shieldVisual.visible=shieldTimer>0;
updatePlayer(dt);
updateObjects(dt);
updateScenery(dt);
updateEnvironment(dt);
updateCamera(dt);
updateParticles(dt);
updateHUD();
}
/* ---------- particle update ---------- */
function updateParticles(dt){
for(let i=particles.length-1;i>=0;i--){
const p=particles[i];
p.position.addScaledVector(p.userData.v,dt);
p.userData.v.y-=9*dt;
p.userData.life-=dt;
p.material.opacity=Math.max(0,p.userData.life/.55);
if(p.userData.life<=0){
scene.remove(p);
particles.splice(i,1);
}
}
}
/* ---------- rendering ---------- */
function render(){
renderer.render(scene,camera);
}
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),.05);
if(state==="playing"){
update(dt);
}else{
environmentTime+=dt*.15;
updateEnvironment(dt*.15);
}
render();
}
window.addEventListener("resize",()=>{
camera.aspect=innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
applyQuality();
});
/* ---------- initialization ---------- */
applyQuality();
document.getElementById("menuBest").textContent=highScore;
document.getElementById("best").textContent=highScore;
document.getElementById("settingsSound").textContent="ON";
scene.background=new THREE.Color(0x718ba4);
setTimeout(()=>{
document.getElementById("loading").classList.add("hidden");
showPanel("menu");
},500);
animate();
</script>
</body>
</html>Game Source: METRO VELOCITY
Creator: CyberFalcon70
Libraries: three
Complexity: complex (1608 lines, 38.4 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: metro-velocity-cyberfalcon70" to link back to the original. Then publish at arcadelab.ai/publish.