<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<meta name="viewport" content="width=device-width,initial-scale=1"/>
<title>3D Car Drift Arena — Vehicle Select (Images)</title>
<style>
:root { --ui-bg: rgba(0,0,0,0.35); --accent: #ff6600; }
html,body { height:100%; margin:0; background:#111; font-family:Inter,Arial,Helvetica,sans-serif; overflow:hidden; }
canvas#gameCanvas { display:block; width:100vw; height:100vh; background:#000; }
/* Top UI */
#ui { position:absolute; left:14px; top:14px; z-index:60; color:#fff; background:var(--ui-bg); padding:10px 12px; border-radius:8px; font-size:14px; }
#speedDisplayBox { margin-top:6px; font-size:13px; color:#ddd; }
#driftIndicator { position:absolute; right:14px; top:14px; z-index:60; color:var(--accent); font-weight:800; font-size:18px; display:none; text-shadow:0 2px 6px rgba(0,0,0,0.6); }
/* Bottom UI */
#instructions { position:absolute; left:50%; transform:translateX(-50%); bottom:14px; z-index:60; color:#ddd; background:var(--ui-bg); padding:8px 12px; border-radius:8px; font-size:12px; }
/* Minimap hint */
#minimapHint { position:absolute; left:14px; bottom:14px; z-index:60; color:#ddd; font-size:11px; pointer-events:none; }
/* Selection overlay */
#menuOverlay {
position:absolute; inset:0; z-index:200; display:flex; align-items:center; justify-content:center;
background: linear-gradient(180deg, rgba(0,0,0,0.6), rgba(0,0,0,0.3));
backdrop-filter: blur(6px);
}
#menuCard {
width:920px; max-width:95vw; background: linear-gradient(180deg,#121212,#151515); border-radius:12px; padding:18px; box-shadow:0 10px 40px rgba(0,0,0,0.8); color:#fff;
display:flex; gap:18px; align-items:stretch;
}
#vehicleGrid { width:480px; display:grid; grid-template-columns: repeat(2, 1fr); gap:12px; }
.vehicleTile {
background:#0f0f0f; border-radius:8px; padding:6px; cursor:pointer; border:2px solid transparent; transition:transform .14s ease, border-color .12s ease;
display:flex; flex-direction:column; align-items:center; gap:6px;
}
.vehicleTile img { width:100%; height:160px; object-fit:cover; border-radius:6px; display:block; }
.vehicleTile:hover { transform: translateY(-6px); border-color: rgba(255,255,255,0.06); }
.vehicleTile.selected { border-color: var(--accent); box-shadow:0 8px 26px rgba(255,102,0,0.08); transform:translateY(-4px); }
#vehicleInfo { flex:1; min-width:320px; display:flex; flex-direction:column; gap:12px; }
#vehiclePreview { height:220px; background:linear-gradient(180deg,#101010,#0a0a0a); border-radius:8px; display:flex; align-items:center; justify-content:center; }
#vehiclePreview canvas { width:100%; height:100%; display:block; }
.statRow { display:flex; justify-content:space-between; font-size:14px; color:#ddd; }
.statName { color:#aaa; }
#startRow { display:flex; justify-content:space-between; align-items:center; gap:12px; }
#startBtn { background:linear-gradient(90deg,var(--accent),#ff944d); padding:10px 18px; border-radius:8px; color:#111; font-weight:700; cursor:pointer; border:none; box-shadow:0 6px 22px rgba(255,102,0,0.14); }
#cancelBtn { background:transparent; padding:8px 12px; border-radius:8px; color:#ddd; border:1px solid rgba(255,255,255,0.04); cursor:pointer; }
/* small label */
.vehicleLabel { font-weight:700; font-size:15px; color:#fff; }
/* responsive */
@media (max-width:880px){
#menuCard { flex-direction:column; width:94vw; padding:12px; }
#vehicleGrid { width:100%; grid-template-columns:repeat(2,1fr); }
#vehiclePreview { height:180px; }
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="ui">
<div>Speed: <span id="speed">0</span> km/h</div>
<div id="speedDisplayBox">Drift Angle: <span id="driftAngle">0</span>° — Height: <span id="height">0.00</span> m</div>
</div>
<div id="driftIndicator">DRIFT!</div>
<div id="instructions">WASD / Arrow keys • Space = HAND-BRAKE (Arcade)</div>
<div id="minimapHint">Minimap: Top-down (bottom-left)</div>
<!-- VEHICLE SELECTION MENU -->
<div id="menuOverlay">
<div id="menuCard" role="dialog" aria-label="Vehicle Selection">
<div id="vehicleGrid">
<!-- Four tiles: use uploaded image for one tile; others are placeholders (picsum) -->
<div class="vehicleTile" data-key="sports" id="tile-sports">
<img src="/mnt/data/74176d45-998a-4e42-897a-07f73c28e168.png" alt="Sports Car"/>
<div class="vehicleLabel">Sports Car</div>
<div class="vehicleDesc">Fast & light</div>
</div>
<div class="vehicleTile" data-key="muscle" id="tile-muscle">
<img src="https://picsum.photos/seed/musclecar/640/360" alt="Muscle Car"/>
<div class="vehicleLabel">Muscle Car</div>
<div class="vehicleDesc">Heavy & torquey</div>
</div>
<div class="vehicleTile" data-key="rally" id="tile-rally">
<img src="https://picsum.photos/seed/rallycar/640/360" alt="Rally Car"/>
<div class="vehicleLabel">Rally Car</div>
<div class="vehicleDesc">Balanced handling</div>
</div>
<div class="vehicleTile" data-key="drift" id="tile-drift">
<img src="https://picsum.photos/seed/driftcar/640/360" alt="Drift Car"/>
<div class="vehicleLabel">Drift Car</div>
<div class="vehicleDesc">High-angle slides</div>
</div>
</div>
<div id="vehicleInfo">
<div id="vehiclePreview">
<!-- small rotating placeholder 3D preview canvas will be drawn from Three.js (same renderer is used but preview uses an offscreen camera) -->
<canvas id="previewCanvas" width="420" height="220"></canvas>
</div>
<div class="statRow"><div class="statName">Name</div><div id="statName">Sports Car</div></div>
<div class="statRow"><div class="statName">Style</div><div id="statStyle">Fast, light</div></div>
<div class="statRow"><div class="statName">Top Speed</div><div id="statTop">36 m/s</div></div>
<div class="statRow"><div class="statName">Acceleration</div><div id="statAccel">16</div></div>
<div class="statRow"><div class="statName">Drift</div><div id="statDrift">High</div></div>
<div class="statRow"><div class="statName">Brake Force</div><div id="statBrake">28</div></div>
<div id="startRow">
<div>
<button id="startBtn">Start Game</button>
<button id="cancelBtn">Cancel</button>
</div>
<div style="font-size:12px;color:#aaa">Select a vehicle and click Start</div>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/three@0.152.0/build/three.min.js"></script>
<script>
(function(){
/* -------------------------
VEHICLE CONFIG / SELECTION
------------------------- */
const vehicles = {
sports: {
name: 'Sports Car',
style: 'Fast, light',
color: 0xff2e2e,
stats: { maxSpeed: 36, acceleration: 16, deceleration: 8, brakeForce: 28, driftBase: 0.35 }
},
muscle: {
name: 'Muscle Car',
style: 'Heavy, torquey',
color: 0xff8c2e,
stats: { maxSpeed: 30, acceleration: 12, deceleration: 10, brakeForce: 32, driftBase: 0.22 }
},
rally: {
name: 'Rally Car',
style: 'Balanced',
color: 0x2ec0ff,
stats: { maxSpeed: 32, acceleration: 14.5, deceleration: 9, brakeForce: 26, driftBase: 0.28 }
},
drift: {
name: 'Drift Car',
style: 'High angle',
color: 0xa32eff,
stats: { maxSpeed: 30, acceleration: 12.5, deceleration: 7, brakeForce: 24, driftBase: 0.55 }
}
};
// default selection
let selectedKey = 'sports';
// DOM references
const tileEls = {
sports: document.getElementById('tile-sports'),
muscle: document.getElementById('tile-muscle'),
rally: document.getElementById('tile-rally'),
drift: document.getElementById('tile-drift')
};
const statName = document.getElementById('statName');
const statStyle = document.getElementById('statStyle');
const statTop = document.getElementById('statTop');
const statAccel = document.getElementById('statAccel');
const statDrift = document.getElementById('statDrift');
const statBrake = document.getElementById('statBrake');
const startBtn = document.getElementById('startBtn');
const cancelBtn = document.getElementById('cancelBtn');
const menuOverlay = document.getElementById('menuOverlay');
const previewCanvas = document.getElementById('previewCanvas');
// highlight default
function updateSelectionUI(){
Object.keys(tileEls).forEach(k => {
tileEls[k].classList.toggle('selected', k === selectedKey);
});
const v = vehicles[selectedKey];
statName.textContent = v.name;
statStyle.textContent = v.style;
statTop.textContent = v.stats.maxSpeed + ' m/s';
statAccel.textContent = v.stats.acceleration.toFixed(1);
statDrift.textContent = (v.stats.driftBase > 0.45 ? 'Very High' : v.stats.driftBase > 0.3 ? 'High' : 'Medium');
statBrake.textContent = v.stats.brakeForce;
}
Object.keys(tileEls).forEach(k => {
tileEls[k].addEventListener('click', ()=> { selectedKey = k; updateSelectionUI(); });
});
updateSelectionUI();
// Cancel hides menu and starts with default selected car (useful for dev)
cancelBtn.addEventListener('click', ()=> { menuOverlay.style.display = 'none'; startGameWith(selectedKey); });
/* -----------------------------------------
PREVIEW: small rotating 3D preview of chosen
----------------------------------------- */
// We'll create a small local THREE renderer for the preview canvas
const previewRenderer = new THREE.WebGLRenderer({ canvas: previewCanvas, antialias: true, alpha: true });
previewRenderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
previewRenderer.setSize(previewCanvas.width, previewCanvas.height, false);
const previewScene = new THREE.Scene();
const previewCam = new THREE.PerspectiveCamera(50, previewCanvas.width/previewCanvas.height, 0.1, 1000);
previewCam.position.set(0, 3.5, 6);
previewCam.lookAt(0,0,0);
const pl = new THREE.HemisphereLight(0xffffff, 0x444444, 0.9); previewScene.add(pl);
const dl = new THREE.DirectionalLight(0xffffff, 0.7); dl.position.set(5,8,5); previewScene.add(dl);
// platform
const platform = new THREE.Mesh(new THREE.CylinderGeometry(2.2,2.2,0.15,32), new THREE.MeshPhongMaterial({ color: 0x111111, shininess:10 }));
platform.rotation.x = 0; platform.position.y = -0.5; previewScene.add(platform);
let previewCarMesh = null;
function makePreviewCar(color){
if (previewCarMesh){ previewScene.remove(previewCarMesh); previewCarMesh.traverse(c=> c.dispose && c.dispose()); previewCarMesh = null; }
const g = new THREE.Group();
// simple stylized car: body + roof + 4 wheels
const body = new THREE.Mesh(new THREE.BoxGeometry(2,0.6,3.4), new THREE.MeshPhongMaterial({ color }));
body.position.y = 0.35; g.add(body);
const roof = new THREE.Mesh(new THREE.BoxGeometry(1.4,0.5,1.4), new THREE.MeshPhongMaterial({ color: new THREE.Color(color).offsetHSL(0,-0.1,0.06) }));
roof.position.set(0,0.85,-0.12); g.add(roof);
const wheelMat = new THREE.MeshPhongMaterial({ color:0x111111 });
const wheelGeo = new THREE.CylinderGeometry(0.28,0.28,0.4,16);
const positions = [[-0.9,0.18,1.35],[0.9,0.18,1.35],[-0.9,0.18,-1.2],[0.9,0.18,-1.2]];
positions.forEach(p => { const w = new THREE.Mesh(wheelGeo, wheelMat); w.rotation.z = Math.PI/2; w.position.set(...p); g.add(w); });
previewCarMesh = g; previewScene.add(g);
}
// animate preview
let prevTime = performance.now();
function animatePreview(){
const now = performance.now();
const dt = (now - prevTime) / 1000; prevTime = now;
const v = vehicles[selectedKey];
if (!previewCarMesh) makePreviewCar(v.color);
previewCarMesh.rotation.y += 0.7 * dt;
previewRenderer.render(previewScene, previewCam);
requestAnimationFrame(animatePreview);
}
animatePreview();
// when selection changes, update preview color and stats
function refreshPreview(){
const v = vehicles[selectedKey];
makePreviewCar(v.color);
updateSelectionUI();
}
// attach small observer for selection changes
Object.keys(tileEls).forEach(k => tileEls[k].addEventListener('click', ()=> setTimeout(refreshPreview, 60)));
/* --------------------------
MAIN GAME (Three.js world)
-------------------------- */
// main renderer & scene use the single canvas 'gameCanvas'
const canvas = document.getElementById('gameCanvas');
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio || 1, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.FogExp2(0x87CEEB, 0.0015);
// main camera
const camera = new THREE.PerspectiveCamera(62, window.innerWidth/window.innerHeight, 0.1, 1000);
camera.position.set(0,8,16);
// lights
const hemi = new THREE.HemisphereLight(0xffffff, 0x444444, 0.7);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xffffff, 1.0);
sun.position.set(50,80,50);
sun.castShadow = true;
scene.add(sun);
// ground
const ground = new THREE.Mesh(new THREE.PlaneGeometry(400,400), new THREE.MeshLambertMaterial({ color: 0x202020 }));
ground.rotation.x = -Math.PI/2; ground.receiveShadow = true; scene.add(ground);
// subtle grid
const grid = new THREE.GridHelper(200, 40, 0x101010, 0x080808); grid.material.opacity = 0.06; grid.material.transparent = true; scene.add(grid);
// obstacles and ramps
const obstacles = [], obstacleMeshes = [];
(function createObstacles(){
const mat = new THREE.MeshLambertMaterial({ color: 0xff4444 });
const defs = [
{ pos:[15,1,15], size:[3,2,3] },
{ pos:[-15,1,-15], size:[3,2,3] },
{ pos:[20,1,-10], size:[2,3,2] },
{ pos:[-20,1,10], size:[2,3,2] },
{ pos:[0,1.5,0], size:[4,3,4] }
];
defs.forEach(d => {
const m = new THREE.Mesh(new THREE.BoxGeometry(...d.size), mat);
m.position.set(...d.pos); m.castShadow = true; m.receiveShadow = true; scene.add(m);
obstacles.push({ mesh:m, halfSize: new THREE.Vector3(d.size[0]/2,d.size[1]/2,d.size[2]/2) });
obstacleMeshes.push(m);
});
})();
const rampMeshes = [], ramps = [];
(function createRamps(){
const mat = new THREE.MeshLambertMaterial({ color: 0x777777 });
const defs = [
{ pos:[20,0.05,18], rotX:-Math.PI/10, rotY:-Math.PI/6, size:[8,0.5,10] },
{ pos:[-20,0.05,-18], rotX:-Math.PI/10, rotY:Math.PI/6, size:[8,0.5,10] },
{ pos:[8,0.05,-22], rotX:-Math.PI/12, rotY:Math.PI/10, size:[6,0.5,8] }
];
defs.forEach(d => {
const m = new THREE.Mesh(new THREE.BoxGeometry(...d.size), mat);
m.position.set(...d.pos); m.rotation.set(d.rotX, d.rotY, 0); m.castShadow=true; m.receiveShadow=true;
scene.add(m); rampMeshes.push(m); ramps.push({ mesh:m, size: new THREE.Vector3(d.size[0], d.size[1], d.size[2]) });
});
})();
// create arena walls
(function createWalls(){
const mat = new THREE.MeshLambertMaterial({ color: 0x8B4513 });
const arenaSize = 40, wallHeight = 3, thickness = 1;
const def = [
{ pos:[0,wallHeight/2,arenaSize], size:[arenaSize*2,wallHeight,thickness] },
{ pos:[0,wallHeight/2,-arenaSize], size:[arenaSize*2,wallHeight,thickness] },
{ pos:[arenaSize,wallHeight/2,0], size:[thickness,wallHeight,arenaSize*2] },
{ pos:[-arenaSize,wallHeight/2,0], size:[thickness,wallHeight,arenaSize*2] }
];
def.forEach(w => { const m = new THREE.Mesh(new THREE.BoxGeometry(...w.size), mat); m.position.set(...w.pos); m.castShadow=true; scene.add(m); });
})();
// Player car visual (will be replaced with a colored stylized model per selection)
let playerCar = null;
function makePlayerCar(color){
if (playerCar){ scene.remove(playerCar); playerCar = null; }
const group = new THREE.Group();
const body = new THREE.Mesh(new THREE.BoxGeometry(2,0.8,4), new THREE.MeshPhongMaterial({ color }));
body.position.y = 0.6; body.castShadow = true; group.add(body);
const roof = new THREE.Mesh(new THREE.BoxGeometry(1.6,0.6,2), new THREE.MeshPhongMaterial({ color: new THREE.Color(color).offsetHSL(0,-0.08,0.03) }));
roof.position.set(0,1.05,-0.4); roof.castShadow = true; group.add(roof);
const wheelGeo = new THREE.CylinderGeometry(0.38,0.38,0.35,16);
const wheelMat = new THREE.MeshPhongMaterial({ color:0x111111 });
const wheelPos = [[-1.15,0.35,1.45],[1.15,0.35,1.45],[-1.15,0.35,-1.45],[1.15,0.35,-1.45]];
wheelPos.forEach(p => { const w = new THREE.Mesh(wheelGeo, wheelMat); w.rotation.z=Math.PI/2; w.position.set(...p); w.castShadow=true; group.add(w); });
scene.add(group); playerCar = group;
}
/* -------------------------
PHYSICS / GAME STATE
------------------------- */
const physics = {
position: new THREE.Vector3(0,0,0),
velocity: new THREE.Vector3(0,0,0),
rotation: 0,
steering: 0,
speed: 0,
maxSpeed: 36,
acceleration: 16,
deceleration: 8,
brakeForce: 28,
driftFactor: 0,
isDrifting: false,
angularVelocity: 0,
airborne: false,
airVelY: 0,
pitchVel: 0,
rollVel: 0
};
// Input
const keys = {};
window.addEventListener('keydown', e => { keys[e.code]=true; if (e.key) keys[e.key.toLowerCase()] = true; }, {passive:true});
window.addEventListener('keyup', e => { keys[e.code]=false; if (e.key) keys[e.key.toLowerCase()] = false; }, {passive:true});
// helpers
function forwardVector(yaw){ return new THREE.Vector3(Math.sin(yaw),0,Math.cos(yaw)); }
function rightVector(yaw){ return new THREE.Vector3(Math.cos(yaw),0,-Math.sin(yaw)); }
// raycaster used for ramp/ground detection
const downRay = new THREE.Raycaster(); downRay.far = 6;
// particles (sparks) pool
const maxParticles = 700;
const pp = new Float32Array(maxParticles*3);
const pc = new Float32Array(maxParticles*3);
const ps = new Float32Array(maxParticles);
const pl = new Float32Array(maxParticles);
const pv = new Array(maxParticles).fill().map(()=> new THREE.Vector3());
const pal = new Array(maxParticles).fill(false);
const pGeo = new THREE.BufferGeometry();
pGeo.setAttribute('position', new THREE.BufferAttribute(pp, 3));
pGeo.setAttribute('color', new THREE.BufferAttribute(pc, 3));
pGeo.setAttribute('size', new THREE.BufferAttribute(ps, 1));
const spriteTex = (function(){ const s=64; const c=document.createElement('canvas'); c.width=c.height=s; const ctx=c.getContext('2d'); const g=ctx.createRadialGradient(s/2,s/2,0,s/2,s/2,s/2); g.addColorStop(0,'rgba(255,255,255,1)'); g.addColorStop(0.15,'rgba(255,230,180,1)'); g.addColorStop(0.35,'rgba(255,120,30,0.9)'); g.addColorStop(1,'rgba(0,0,0,0)'); ctx.fillStyle=g; ctx.fillRect(0,0,s,s); return new THREE.CanvasTexture(c); })();
const pMat = new THREE.PointsMaterial({ size:0.18, map: spriteTex, vertexColors:true, transparent:true, blending:THREE.AdditiveBlending, depthWrite:false });
const particles = new THREE.Points(pGeo, pMat); scene.add(particles);
function emitParticle(pos, vel, life, color, size=0.16){
for (let i=0;i<maxParticles;i++){
if (!pal[i]){ pal[i]=true; pp[i*3]=pos.x; pp[i*3+1]=pos.y; pp[i*3+2]=pos.z; pc[i*3]=color.r; pc[i*3+1]=color.g; pc[i*3+2]=color.b; ps[i]=size; pl[i]=life; pv[i].copy(vel); pGeo.attributes.position.needsUpdate=true; pGeo.attributes.color.needsUpdate=true; pGeo.attributes.size.needsUpdate=true; return; }
}
}
function emitDriftSparks(){
const rearLeft = new THREE.Vector3(-1.15,0,-1.45);
const rearRight = new THREE.Vector3(1.15,0,-1.45);
[rearLeft,rearRight].forEach(local => {
const w = local.clone(); const c = Math.cos(physics.rotation), s = Math.sin(physics.rotation);
const x = w.x*c - w.z*s, z = w.x*s + w.z*c; w.x = x + physics.position.x; w.z = z + physics.position.z; w.y = 0.25 + Math.random()*0.12;
const base = forwardVector(physics.rotation).clone().multiplyScalar(-0.6 - Math.random()*0.6);
const jitter = new THREE.Vector3((Math.random()-0.5)*1.2, Math.random()*0.6, (Math.random()-0.5)*1.2);
const v = base.add(jitter).multiplyScalar(1 + Math.abs(physics.speed)/16);
const col = new THREE.Color(0xffb86b).lerp(new THREE.Color(0xff3300), Math.random()*0.7);
const cnt = 4 + Math.floor(Math.random()*5);
for (let i=0;i<cnt;i++) emitParticle(w.clone(), v.clone().add(new THREE.Vector3((Math.random()-0.5)*0.8, Math.random()*0.6, (Math.random()-0.5)*0.8)), 0.45 + Math.random()*0.35, col, 0.12 + Math.random()*0.18);
});
}
function emitCollisionBurst(pos){
const c1=new THREE.Color(0xffffff), c2=new THREE.Color(0xffcf66), n=40+Math.floor(Math.random()*40);
for (let i=0;i<n;i++){ const dir=new THREE.Vector3((Math.random()-0.5), Math.random()*0.8, (Math.random()-0.5)).normalize(); const sp=3+Math.random()*6; emitParticle(pos.clone(), dir.multiplyScalar(sp), 0.5+Math.random()*0.6, c1.clone().lerp(c2, Math.random()), 0.16+Math.random()*0.25); }
}
function emitLandingBurst(pos, intensity=1.0){
const c1=new THREE.Color(0xffa86b), c2=new THREE.Color(0xffffff), n=Math.min(80, Math.floor(10 + intensity*50));
for (let i=0;i<n;i++){ const dir=new THREE.Vector3((Math.random()-0.5), Math.random()*0.9, (Math.random()-0.5)).normalize(); const sp=2 + Math.random()*6*intensity; emitParticle(pos.clone(), dir.multiplyScalar(sp), 0.5 + Math.random()*0.6, c1.clone().lerp(c2, Math.random()), 0.12 + Math.random()*0.2); }
}
function updateParticles(dt){
let upd=false;
for (let i=0;i<maxParticles;i++){
if (!pal[i]) continue;
pl[i] -= dt;
if (pl[i] <= 0){ pal[i]=false; pp[i*3]=pp[i*3+1]=pp[i*3+2]=0; pc[i*3]=pc[i*3+1]=pc[i*3+2]=0; ps[i]=0; upd=true; continue; }
pv[i].y -= 9.8 * dt * 0.3;
pv[i].multiplyScalar(1 - Math.min(0.95, dt*2.0));
pp[i*3] += pv[i].x * dt; pp[i*3+1] += pv[i].y * dt; pp[i*3+2] += pv[i].z * dt;
ps[i] *= 0.995;
pc[i*3] *= 0.998; pc[i*3+1] *= 0.998; pc[i*3+2] *= 0.998;
upd=true;
}
if (upd){ pGeo.attributes.position.needsUpdate=true; pGeo.attributes.color.needsUpdate=true; pGeo.attributes.size.needsUpdate=true; }
}
// physics helpers
function boxIntersectsPoint(center, half, point){ return Math.abs(point.x - center.x) <= half.x + 0.9 && Math.abs(point.y - center.y) <= half.y + 0.9 && Math.abs(point.z - center.z) <= half.z + 0.9; }
// ramp launch (hybrid tuning)
function attemptRampLaunch(){
const frontOffset = 1.9;
const frontWorld = physics.position.clone().add(forwardVector(physics.rotation).multiplyScalar(frontOffset));
const rayOrigin = frontWorld.clone(); rayOrigin.y += 1.2;
downRay.set(rayOrigin, new THREE.Vector3(0,-1,0));
const hits = downRay.intersectObjects(rampMeshes, true);
if (!hits.length) return;
const hit = hits[0];
const rampNormal = hit.face ? hit.face.normal.clone().applyMatrix3(new THREE.Matrix3().getNormalMatrix(hit.object.matrixWorld)).normalize() : new THREE.Vector3(0,1,0);
if (rampNormal.y <= 0.25) return;
const verticalDist = rayOrigin.y - hit.point.y;
const approachDot = forwardVector(physics.rotation).dot(rampNormal.clone().negate());
if (verticalDist < 1.0 && approachDot > 0.12 && Math.abs(physics.speed) > Math.min(8, physics.maxSpeed*0.25)){
const forwardMag = physics.speed;
const forwardOnPlane = forwardVector(physics.rotation).clone().projectOnPlane(rampNormal).normalize();
const along = forwardOnPlane.clone().multiplyScalar(forwardMag * 0.75);
const steepnessFactor = Math.max(0.0, 1 - rampNormal.y);
const normalPush = rampNormal.clone().multiplyScalar(Math.min(10, forwardMag * (0.35 + steepnessFactor * 0.9)));
const launchVel = along.clone().add(normalPush.clone().setY(0));
const airVelY = Math.max(2.0, Math.min(10, normalPush.y + Math.abs(physics.speed) * 0.06));
physics.airborne = true;
physics.airVelY = airVelY;
physics.velocity.copy(launchVel);
physics.speed = physics.velocity.length();
physics.position.y = hit.point.y + 0.06;
physics.pitchVel = -0.6 * Math.sign(physics.speed) * (0.6 + Math.random()*0.4);
physics.rollVel = (Math.random() - 0.5) * 0.5;
physics.position.add(forwardVector(physics.rotation).multiplyScalar(0.12));
emitDriftSparks();
}
}
// HB3: Arcade handbrake
let handbrakeHeld = false;
function startHandbrake(){
handbrakeHeld = true;
physics.driftFactor = Math.min(1.2, physics.driftFactor + 0.9);
physics.isDrifting = true;
const impulse = 1.8 * Math.sign(physics.steering || 1) * (Math.abs(physics.speed)/Math.max(1, physics.maxSpeed));
physics.angularVelocity += impulse * 0.8;
const lateral = rightVector(physics.rotation).clone().multiplyScalar(physics.speed * 0.9 * (Math.random()*0.6 + 0.7));
physics.velocity.add(lateral);
emitDriftSparks();
}
function endHandbrake(){
handbrakeHeld = false;
physics.driftFactor = Math.min(1.0, physics.driftFactor * 0.7);
}
window.addEventListener('keydown', e => { if (e.code === 'Space' || e.key === ' ') { if (!handbrakeHeld) startHandbrake(); } }, {passive:true});
window.addEventListener('keyup', e => { if (e.code === 'Space' || e.key === ' ') { endHandbrake(); } }, {passive:true});
// camera chase params
const chase = { offsetZ: 12, offsetY: 7, lookAtY: 1.5 };
let cameraShake = 0;
function applyCameraShake(v){ cameraShake = Math.max(cameraShake, v); }
// minimap cam (orthographic top-down)
const miniSize = 40;
const miniAspect = 240/160;
const miniCam = new THREE.OrthographicCamera(-miniSize*miniAspect, miniSize*miniAspect, miniSize, -miniSize, 0.1, 200);
miniCam.position.set(0,90,0);
miniCam.up.set(0,0,-1);
miniCam.lookAt(new THREE.Vector3(0,0,0));
// UI elements
const speedEl = document.getElementById('speed');
const driftEl = document.getElementById('driftAngle');
const heightEl = document.getElementById('height');
const driftIndicator = document.getElementById('driftIndicator');
const speedDisplay = document.getElementById('speedDisplayBox');
// window resize
window.addEventListener('resize', ()=> { renderer.setSize(window.innerWidth, window.innerHeight); camera.aspect = window.innerWidth/window.innerHeight; camera.updateProjectionMatrix(); });
/* ------------------------------
game start / vehicle spawn
------------------------------ */
function startGameWith(key){
// load selected vehicle stats into physics
const v = vehicles[key];
physics.maxSpeed = v.stats.maxSpeed;
physics.acceleration = v.stats.acceleration;
physics.deceleration = v.stats.deceleration;
physics.brakeForce = v.stats.brakeForce;
physics.driftFactor = v.stats.driftBase * 0.0; // start neutral, increases with handbrake
physics.isDrifting = false;
// create player car with chosen color
makePlayerCar(v.color);
// hide menu
menuOverlay.style.display = 'none';
// set spawn position (center)
physics.position.set(0, 0, 0);
physics.speed = 0;
physics.rotation = Math.PI; // face -Z initially
// small camera snap
camera.position.set(0, 8, 16);
}
// connect start button
startBtn.addEventListener('click', ()=> { startGameWith(selectedKey); });
/* -------------------------
main loop & update steps
------------------------- */
const clock = new THREE.Clock();
function updatePhysics(dt){
// airborne
if (physics.airborne){
const steerEffect = 0.25;
const steerInput = ((keys['a']||keys['KeyA']||keys['ArrowLeft'])? -1:0) + ((keys['d']||keys['KeyD']||keys['ArrowRight'])? 1:0);
physics.steering += (steerInput - physics.steering) * Math.min(1, dt * 6);
physics.angularVelocity += (-physics.steering * physics.steeringSpeed * steerEffect * dt * 1.6);
physics.airVelY -= 9.8 * dt;
physics.position.y += physics.airVelY * dt;
const fwd = forwardVector(physics.rotation);
physics.velocity.copy(fwd.clone().multiplyScalar(physics.speed * 0.995));
physics.position.add(physics.velocity.clone().multiplyScalar(dt));
physics.pitchVel *= 0.994; physics.rollVel *= 0.994;
if (playerCar){ playerCar.rotation.x += physics.pitchVel * dt; playerCar.rotation.z += physics.rollVel * dt; }
// landing detection
const rayFrom = physics.position.clone(); rayFrom.y += 1.6;
downRay.set(rayFrom, new THREE.Vector3(0,-1,0));
const candidates = [ground, ...rampMeshes, ...obstacleMeshes];
const hits = downRay.intersectObjects(candidates, true);
if (hits.length > 0){
const hit = hits[0];
if (physics.position.y - hit.point.y <= 0.28){
const impact = Math.abs(physics.airVelY);
physics.position.y = hit.point.y;
physics.airborne = false; physics.airVelY = 0;
physics.speed *= Math.max(0.5, 1 - impact * 0.08);
emitLandingBurst(physics.position.clone(), Math.min(1.6, impact/12));
applyCameraShake(Math.min(1.2, impact/12));
if (playerCar){ playerCar.rotation.x = 0; playerCar.rotation.z = 0; }
}
}
return;
}
// ground controls
let steerInput = 0;
if (keys['a'] || keys['KeyA'] || keys['ArrowLeft']) steerInput -= 1;
if (keys['d'] || keys['KeyD'] || keys['ArrowRight']) steerInput += 1;
physics.steering += (steerInput - physics.steering) * Math.min(1, dt * 10);
const throttle = (keys['w'] || keys['KeyW'] || keys['ArrowUp']) ? 1 : 0;
const brake = (keys['s'] || keys['KeyS'] || keys['ArrowDown']) ? 1 : 0;
if (throttle) physics.speed += physics.acceleration * throttle * dt;
else if (brake) physics.speed -= physics.brakeForce * dt;
else {
const drag = physics.deceleration * dt;
if (physics.speed > 0) physics.speed = Math.max(0, physics.speed - drag);
else physics.speed = Math.min(0, physics.speed + drag);
}
physics.speed = Math.max(-physics.maxSpeed*0.5, Math.min(physics.maxSpeed, physics.speed));
if (!handbrakeHeld){
physics.driftFactor = Math.max(0, physics.driftFactor - dt * 1.6);
physics.isDrifting = physics.driftFactor > 0.08;
} else physics.isDrifting = true;
const speedFactor = Math.abs(physics.speed) / physics.maxSpeed;
const baseTurn = -physics.steering * physics.steeringSpeed * (0.8 + speedFactor * 1.2);
if (physics.isDrifting){
physics.angularVelocity += baseTurn * (1 + physics.driftFactor * 2.4) * dt * 52;
physics.angularVelocity *= (1 - Math.min(0.9, 0.7 * dt));
} else {
physics.angularVelocity = baseTurn * dt * 60;
}
const fwd = forwardVector(physics.rotation);
const rgt = rightVector(physics.rotation);
const forwardVel = fwd.clone().multiplyScalar(physics.speed);
let lateralVel = new THREE.Vector3(0,0,0);
if (physics.isDrifting){
const lateralStrength = (handbrakeHeld ? 1.2 : 0.6);
lateralVel = rgt.clone().multiplyScalar(physics.speed * physics.driftFactor * lateralStrength * (physics.speed >= 0 ? 1 : -1));
}
physics.velocity.copy(forwardVel).add(lateralVel);
physics.position.add(physics.velocity.clone().multiplyScalar(dt));
physics.rotation += physics.angularVelocity * dt;
if (physics.isDrifting && Math.abs(physics.speed) > 8 && Math.random() < 0.5) emitDriftSparks();
attemptRampLaunch();
// boundaries & obstacles
const boundary = 38;
let hitWall = false;
if (physics.position.x > boundary){ physics.position.x = boundary; physics.speed *= 0.35; hitWall = true; }
else if (physics.position.x < -boundary){ physics.position.x = -boundary; physics.speed *= 0.35; hitWall = true; }
if (physics.position.z > boundary){ physics.position.z = boundary; physics.speed *= 0.35; hitWall = true; }
else if (physics.position.z < -boundary){ physics.position.z = -boundary; physics.speed *= 0.35; hitWall = true; }
if (hitWall && Math.abs(physics.speed) > 6) emitCollisionBurst(physics.position.clone());
for (let i=0;i<obstacles.length;i++){
const ob = obstacles[i];
if (boxIntersectsPoint(ob.mesh.position, ob.halfSize, physics.position)){
const push = forwardVector(physics.rotation).clone().multiplyScalar(-1.6);
physics.position.add(push); physics.speed *= 0.35; emitCollisionBurst(physics.position.clone());
}
}
// place car y via raycast
const rayOrigin = physics.position.clone(); rayOrigin.y += 1.4;
downRay.set(rayOrigin, new THREE.Vector3(0,-1,0));
const candidates = [ground, ...rampMeshes, ...obstacleMeshes];
const hits = downRay.intersectObjects(candidates, true);
if (hits.length > 0) physics.position.y = hits[0].point.y + 0.01;
else physics.position.y = 0;
// update model and wheels
if (playerCar){
playerCar.position.copy(physics.position); playerCar.rotation.y = physics.rotation;
playerCar.rotation.x += (0 - playerCar.rotation.x) * Math.min(1, dt * 4);
playerCar.rotation.z += (0 - playerCar.rotation.z) * Math.min(1, dt * 4);
// wheels rotation (approx)
playerCar.traverse(c => {
if (c.geometry && c.geometry.type === 'CylinderGeometry') c.rotation.x += physics.speed * 3 * dt;
});
}
}
function updateCamera(dt){
const fwd = forwardVector(physics.rotation);
const desired = physics.position.clone().add(fwd.clone().multiplyScalar(-chase.offsetZ));
desired.y += chase.offsetY;
camera.position.lerp(desired, Math.min(1, dt * 4));
if (cameraShake > 0){
const s = cameraShake * 0.6;
camera.position.x += (Math.random()*2 - 1) * s;
camera.position.y += (Math.random()*2 - 1) * s * 0.4;
camera.position.z += (Math.random()*2 - 1) * s * 0.4;
cameraShake = Math.max(0, cameraShake - dt * 2.2);
}
const look = physics.position.clone(); look.y += chase.lookAtY;
camera.lookAt(look);
}
function updateUI(){
const kmh = Math.round(Math.abs(physics.speed) * 3.6);
document.getElementById('speed').textContent = kmh;
document.getElementById('driftAngle').textContent = Math.round(Math.atan2(Math.abs(physics.velocity.dot(rightVector(physics.rotation))), Math.max(0.0001, Math.abs(physics.velocity.dot(forwardVector(physics.rotation))))) * 180/Math.PI);
document.getElementById('height').textContent = physics.position.y.toFixed(2);
driftIndicator.style.display = physics.isDrifting && Math.abs(physics.speed) > 6 ? 'block' : 'none';
}
// minimap rendering into bottom-left without a frame
function renderMiniMap(){
const cssW = 240, cssH = 160;
const DPR = Math.max(1, window.devicePixelRatio || 1);
const w = Math.floor(cssW * DPR), h = Math.floor(cssH * DPR);
const x = Math.floor(8 * DPR), y = Math.floor(8 * DPR);
const canvasH = renderer.domElement.height;
// miniCam follow
miniCam.position.set(physics.position.x, 90, physics.position.z);
miniCam.lookAt(new THREE.Vector3(physics.position.x, 0, physics.position.z));
miniCam.up.set(0,0,-1); miniCam.updateProjectionMatrix();
renderer.clearDepth();
renderer.setScissorTest(true);
renderer.setViewport(x, canvasH - y - h, w, h);
renderer.setScissor(x, canvasH - y - h, w, h);
// darken region and render
renderer.setClearColor(0x0a0a0a, 0.25);
renderer.clearColor();
renderer.render(scene, miniCam);
renderer.setScissorTest(false);
renderer.setViewport(0, 0, renderer.domElement.width, renderer.domElement.height);
renderer.setClearColor(scene.background);
}
// main loop
const mainClock = new THREE.Clock();
function animate(){
requestAnimationFrame(animate);
const dt = Math.min(0.05, mainClock.getDelta());
updatePhysics(dt);
updateCamera(dt);
updateParticles(dt);
updateUI();
renderer.render(scene, camera);
renderMiniMap();
}
// updateParticles wrapper
function updateParticles(dt){
updateParticles._impl ? updateParticles._impl(dt) : (updateParticles._impl = (dt) => {
let upd=false;
for (let i=0;i<maxParticles;i++){
if (!pal[i]) continue;
pl[i] -= dt;
if (pl[i] <= 0){ pal[i]=false; pp[i*3]=pp[i*3+1]=pp[i*3+2]=0; pc[i*3]=pc[i*3+1]=pc[i*3+2]=0; ps[i]=0; upd=true; continue; }
pv[i].y -= 9.8 * dt * 0.3;
pv[i].multiplyScalar(1 - Math.min(0.95, dt*2.0));
pp[i*3] += pv[i].x * dt; pp[i*3+1] += pv[i].y * dt; pp[i*3+2] += pv[i].z * dt;
ps[i] *= 0.995; pc[i*3] *= 0.998; pc[i*3+1] *= 0.998; pc[i*3+2] *= 0.998;
upd=true;
}
if (upd){ pGeo.attributes.position.needsUpdate=true; pGeo.attributes.color.needsUpdate=true; pGeo.attributes.size.needsUpdate=true; }
}, updateParticles._impl(dt));
}
// start render loop after user selects and presses Start (menuOverlay hides)
// but we also start animation while menu is up so preview is active
animate();
// expose for debugging
window.__carPhysics = physics;
window.__vehicles = vehicles;
})();
</script>
</body>
</html>
check the code and the select functionalu is not working and image of the car mentioned doesnt macth the model