anycoder-5e9ef7d6 / index.html
User-2468's picture
Upload folder using huggingface_hub
87dee83 verified
Raw History Blame Contribute Delete
40.4 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>FORZA HORIZON - WebGL Racing</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Rajdhani:wght@300;500;700&display=swap');
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
:root {
--primary: #ff3e3e;
--secondary: #3eff3e;
--accent: #3e8eff;
--dark: #0a0a0f;
--light: #f0f0f5;
--glass: rgba(255, 255, 255, 0.05);
--glass-border: rgba(255, 255, 255, 0.1);
}
body {
overflow: hidden;
background: var(--dark);
font-family: 'Rajdhani', sans-serif;
color: var(--light);
}
#gameCanvas {
display: block;
width: 100vw;
height: 100vh;
}
/* Loading Screen */
#loadingScreen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #0a0a0f 0%, #1a1a2e 100%);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 1000;
transition: opacity 0.8s ease;
}
#loadingScreen.hidden {
opacity: 0;
pointer-events: none;
}
.loading-title {
font-family: 'Orbitron', sans-serif;
font-size: clamp(2rem, 6vw, 4rem);
font-weight: 900;
background: linear-gradient(90deg, var(--primary), var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 0 60px rgba(255, 62, 62, 0.5);
margin-bottom: 2rem;
letter-spacing: 0.2em;
}
.loading-bar-container {
width: 300px;
height: 6px;
background: var(--glass);
border-radius: 3px;
overflow: hidden;
border: 1px solid var(--glass-border);
}
.loading-bar {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--accent));
width: 0%;
transition: width 0.3s ease;
box-shadow: 0 0 20px var(--primary);
}
.loading-text {
margin-top: 1rem;
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 0.3em;
}
/* HUD */
#hud {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 100;
}
.hud-top {
display: flex;
justify-content: space-between;
padding: 1.5rem 2rem;
background: linear-gradient(180deg, rgba(0,0,0,0.6) 0%, transparent 100%);
}
.brand {
font-family: 'Orbitron', sans-serif;
font-weight: 900;
font-size: 1.2rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.brand-icon {
width: 30px;
height: 30px;
background: linear-gradient(135deg, var(--primary), var(--accent));
clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
}
.speed-display {
text-align: right;
}
.speed-value {
font-family: 'Orbitron', sans-serif;
font-size: clamp(2rem, 5vw, 3.5rem);
font-weight: 700;
color: var(--light);
line-height: 1;
text-shadow: 0 0 20px rgba(255, 255, 255, 0.3);
}
.speed-unit {
font-size: 0.9rem;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 0.2em;
}
.hud-bottom {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 2rem;
background: linear-gradient(0deg, rgba(0,0,0,0.7) 0%, transparent 100%);
display: flex;
justify-content: space-between;
align-items: flex-end;
}
.lap-info {
background: var(--glass);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 8px;
padding: 1rem 1.5rem;
}
.lap-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 0.2em;
margin-bottom: 0.25rem;
}
.lap-value {
font-family: 'Orbitron', sans-serif;
font-size: 1.5rem;
font-weight: 700;
color: var(--accent);
}
.time-display {
text-align: center;
}
.time-label {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 0.2em;
}
.time-value {
font-family: 'Orbitron', sans-serif;
font-size: 2rem;
font-weight: 700;
color: var(--secondary);
text-shadow: 0 0 15px rgba(62, 255, 62, 0.5);
}
.gear-indicator {
display: flex;
gap: 0.25rem;
margin-top: 0.5rem;
}
.gear {
width: 24px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Orbitron', sans-serif;
font-weight: 700;
font-size: 0.9rem;
background: var(--glass);
border: 1px solid var(--glass-border);
border-radius: 4px;
transition: all 0.15s ease;
}
.gear.active {
background: linear-gradient(135deg, var(--primary), var(--accent));
border-color: transparent;
box-shadow: 0 0 15px rgba(255, 62, 62, 0.5);
}
/* Mini Map */
.mini-map {
width: 150px;
height: 150px;
background: var(--glass);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 8px;
position: relative;
overflow: hidden;
}
.mini-map canvas {
width: 100%;
height: 100%;
}
/* Controls Panel */
#controls {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
background: var(--glass);
backdrop-filter: blur(10px);
border: 1px solid var(--glass-border);
border-radius: 12px;
padding: 1rem 2rem;
display: flex;
gap: 1.5rem;
pointer-events: auto;
z-index: 100;
}
.control-key {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
}
.key {
width: 45px;
height: 45px;
background: linear-gradient(180deg, #2a2a35 0%, #1a1a25 100%);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-family: 'Orbitron', sans-serif;
font-weight: 700;
font-size: 0.8rem;
color: var(--light);
transition: all 0.1s ease;
}
.key.active {
background: linear-gradient(180deg, var(--primary), #cc2020);
box-shadow: 0 0 20px rgba(255, 62, 62, 0.5);
transform: scale(0.95);
}
.key-label {
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.4);
letter-spacing: 0.1em;
}
/* Start Screen */
#startScreen {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(10, 10, 15, 0.95);
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
z-index: 500;
transition: opacity 0.5s ease;
}
#startScreen.hidden {
opacity: 0;
pointer-events: none;
}
.start-title {
font-family: 'Orbitron', sans-serif;
font-size: clamp(3rem, 10vw, 6rem);
font-weight: 900;
background: linear-gradient(90deg, var(--primary), #ff8c00, var(--accent));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
margin-bottom: 1rem;
animation: titlePulse 2s ease-in-out infinite;
}
@keyframes titlePulse {
0%, 100% { filter: brightness(1); }
50% { filter: brightness(1.2); }
}
.start-subtitle {
font-size: 1.2rem;
color: rgba(255, 255, 255, 0.5);
letter-spacing: 0.5em;
margin-bottom: 3rem;
}
.start-btn {
font-family: 'Orbitron', sans-serif;
font-size: 1.2rem;
font-weight: 700;
padding: 1rem 3rem;
background: linear-gradient(135deg, var(--primary), var(--accent));
border: none;
border-radius: 8px;
color: white;
cursor: pointer;
letter-spacing: 0.2em;
transition: all 0.3s ease;
box-shadow: 0 0 30px rgba(255, 62, 62, 0.3);
}
.start-btn:hover {
transform: scale(1.05);
box-shadow: 0 0 50px rgba(255, 62, 62, 0.5);
}
/* Shaders */
.vignette {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
background: radial-gradient(ellipse at center, transparent 40%, rgba(0,0,0,0.6) 100%);
z-index: 50;
}
/* Responsive */
@media (max-width: 768px) {
.hud-bottom {
flex-direction: column;
gap: 1rem;
align-items: center;
}
.mini-map {
display: none;
}
.controls {
bottom: 1rem;
padding: 0.75rem 1rem;
gap: 1rem;
}
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div class="vignette"></div>
<!-- Loading Screen -->
<div id="loadingScreen">
<div class="loading-title">FORZA HORIZON</div>
<div class="loading-bar-container">
<div class="loading-bar" id="loadingBar"></div>
</div>
<div class="loading-text">INITIALIZING ENGINE...</div>
</div>
<!-- Start Screen -->
<div id="startScreen" class="hidden">
<div class="start-title">FORZA HORIZON</div>
<div class="start-subtitle">WEBGL RACING</div>
<button class="start-btn" id="startBtn">START RACE</button>
</div>
<!-- HUD -->
<div id="hud">
<div class="hud-top">
<div class="brand">
<div class="brand-icon"></div>
<span>FORZA HORIZON</span>
</div>
<div class="speed-display">
<div class="speed-value" id="speedValue">0</div>
<div class="speed-unit">KM/H</div>
</div>
</div>
<div class="hud-bottom">
<div class="lap-info">
<div class="lap-label">LAP</div>
<div class="lap-value" id="lapValue">1/3</div>
</div>
<div class="time-display">
<div class="time-label">TIME</div>
<div class="time-value" id="timeValue">00:00.000</div>
<div class="gear-indicator" id="gearIndicator"></div>
</div>
<div class="mini-map">
<canvas id="miniMapCanvas"></canvas>
</div>
</div>
</div>
<!-- Controls -->
<div id="controls">
<div class="control-key">
<div class="key" id="keyW">W</div>
<div class="key-label">ACCEL</div>
</div>
<div class="control-key">
<div class="key" id="keyA">A</div>
<div class="key-label">LEFT</div>
</div>
<div class="control-key">
<div class="key" id="keyS">S</div>
<div class="key-label">BRAKE</div>
</div>
<div class="control-key">
<div class="key" id="keyD">D</div>
<div class="key-label">RIGHT</div>
</div>
</div>
<!-- Vertex Shader -->
<script id="vertexShader" type="x-shader/x-vertex">
varying vec3 vNormal;
varying vec3 vPosition;
varying vec2 vUv;
void main() {
vNormal = normalize(normalMatrix * normal);
vPosition = (modelMatrix * vec4(position, 1.0)).xyz;
vUv = uv;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
</script>
<!-- Fragment Shader - Car -->
<script id="carFragmentShader" type="x-shader/x-fragment">
precision highp float;
uniform vec3 uColor;
uniform vec3 uLightPosition;
uniform vec3 uCameraPosition;
uniform float uTime;
varying vec3 vNormal;
varying vec3 vPosition;
varying vec2 vUv;
void main() {
vec3 normal = normalize(vNormal);
vec3 lightDir = normalize(uLightPosition - vPosition);
vec3 viewDir = normalize(uCameraPosition - vPosition);
vec3 halfDir = normalize(lightDir + viewDir);
// Ambient
float ambient = 0.15;
// Diffuse
float diff = max(dot(normal, lightDir), 0.0);
// Specular
float spec = pow(max(dot(normal, halfDir), 0.0), 64.0);
// Fresnel
float fresnel = pow(1.0 - max(dot(normal, viewDir), 0.0), 3.0);
vec3 color = uColor * (ambient + diff * 0.8) + vec3(1.0) * spec * 0.6 + vec3(0.3, 0.5, 1.0) * fresnel * 0.3;
// Metallic shine
color += vec3(0.1) * sin(uTime * 2.0);
gl_FragColor = vec4(color, 1.0);
}
</script>
<!-- Fragment Shader - Ground -->
<script id="groundFragmentShader" type="x-shader/x-fragment">
precision highp float;
uniform vec3 uColor1;
uniform vec3 uColor2;
uniform float uTime;
varying vec3 vNormal;
varying vec3 vPosition;
varying vec2 vUv;
float hash(vec2 p) {
return fract(sin(dot(p, vec2(127.1, 311.7))) * 43758.5453);
}
float noise(vec2 p) {
vec2 i = floor(p);
vec2 f = fract(p);
f = f * f * (3.0 - 2.0 * f);
float a = hash(i);
float b = hash(i + vec2(1.0, 0.0));
float c = hash(i + vec2(0.0, 1.0));
float d = hash(i + vec2(1.0, 1.0));
return mix(mix(a, b, f.x), mix(c, d, f.x), f.y);
}
void main() {
vec3 normal = normalize(vNormal);
// Grid pattern
vec2 grid = fract(vPosition.xz * 0.1);
float gridLine = step(0.95, grid.x) + step(0.95, grid.y);
gridLine = min(gridLine, 1.0);
// Noise for texture
float n = noise(vPosition.xz * 0.5) * 0.2;
// Distance fade
float dist = length(vPosition.xz) * 0.005;
float fade = 1.0 - clamp(dist, 0.0, 1.0);
vec3 color = mix(uColor1, uColor2, gridLine * 0.3 + n);
color *= fade;
// Fog
float fog = 1.0 - exp(-dist * 2.0);
color = mix(color, vec3(0.1, 0.1, 0.15), fog * 0.7);
gl_FragColor = vec4(color, 1.0);
}
</script>
<!-- Sky Shader -->
<script id="skyFragmentShader" type="x-shader/x-fragment">
precision highp float;
uniform float uTime;
varying vec3 vPosition;
void main() {
vec3 dir = normalize(vPosition);
// Gradient sky
float y = dir.y * 0.5 + 0.5;
vec3 skyColor = mix(vec3(0.1, 0.15, 0.3), vec3(0.02, 0.05, 0.15), y);
// Stars
vec2 starUv = dir.xz / (dir.y + 0.001) * 10.0;
float stars = step(0.998, fract(sin(dot(floor(starUv * 50.0), vec2(12.9898, 78.233))) * 43758.5453));
stars *= smoothstep(0.3, 0.8, dir.y);
skyColor += vec3(stars) * 0.8;
// Horizon glow
float horizonGlow = exp(-abs(dir.y) * 5.0);
skyColor += vec3(0.3, 0.1, 0.2) * horizonGlow * 0.3;
gl_FragColor = vec4(skyColor, 1.0);
}
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Game State
const GameState = {
LOADING: 'loading',
START: 'start',
PLAYING: 'playing',
PAUSED: 'paused',
FINISHED: 'finished'
};
let currentState = GameState.LOADING;
// Three.js Setup
let scene, camera, renderer, clock;
let car, carBody, wheels = [];
let track;
let gameTime = 0;
let lapTime = 0;
let currentLap = 1;
let totalLaps = 3;
// Physics
const carPhysics = {
position: new THREE.Vector3(0, 0.5, 0),
velocity: new THREE.Vector3(),
rotation: 0,
speed: 0,
maxSpeed: 120,
acceleration: 40,
braking: 60,
friction: 10,
turnSpeed: 2.5,
wheelAngle: 0
};
// Controls
const keys = {
forward: false,
backward: false,
left: false,
right: false
};
// Track Data
let trackPoints = [];
let checkpoints = [];
let currentCheckpoint = 0;
// Mini Map
let miniMapCanvas, miniMapCtx;
// Initialize
async function init() {
updateLoadingBar(10);
// Scene
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a0a15, 0.008);
// Camera
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
camera.position.set(0, 5, -10);
// Renderer
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('gameCanvas'),
antialias: true,
powerPreference: 'high-performance'
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
updateLoadingBar(30);
// Clock
clock = new THREE.Clock();
// Create World
createSky();
updateLoadingBar(40);
createGround();
updateLoadingBar(50);
createTrack();
updateLoadingBar(70);
createCar();
updateLoadingBar(90);
createLights();
// Mini Map
setupMiniMap();
// Gear Indicator
setupGearIndicator();
updateLoadingBar(100);
// Event Listeners
setupEventListeners();
// Start Game Loop
setTimeout(() => {
document.getElementById('loadingScreen').classList.add('hidden');
document.getElementById('startScreen').classList.remove('hidden');
currentState = GameState.START;
}, 500);
animate();
}
function updateLoadingBar(percent) {
document.getElementById('loadingBar').style.width = percent + '%';
}
// Sky
function createSky() {
const skyGeo = new THREE.SphereGeometry(500, 32, 32);
const skyMat = new THREE.ShaderMaterial({
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('skyFragmentShader').textContent,
uniforms: {
uTime: { value: 0 }
},
side: THREE.BackSide
});
const sky = new THREE.Mesh(skyGeo, skyMat);
scene.add(sky);
}
// Ground
function createGround() {
const groundGeo = new THREE.PlaneGeometry(1000, 1000, 100, 100);
const groundMat = new THREE.ShaderMaterial({
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('groundFragmentShader').textContent,
uniforms: {
uColor1: { value: new THREE.Color(0x1a1a2e) },
uColor2: { value: new THREE.Color(0x16213e) },
uTime: { value: 0 }
}
});
const ground = new THREE.Mesh(groundGeo, groundMat);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
}
// Track Generation
function createTrack() {
// Generate track points in a loop
const trackRadius = 80;
const numPoints = 32;
for (let i = 0; i < numPoints; i++) {
const angle = (i / numPoints) * Math.PI * 2;
const wobble = Math.sin(angle * 3) * 20 + Math.cos(angle * 2) * 15;
const r = trackRadius + wobble;
trackPoints.push(new THREE.Vector3(
Math.cos(angle) * r,
0,
Math.sin(angle) * r
));
}
// Create track mesh
const trackShape = new THREE.Shape();
const trackWidth = 12;
// Track surface
const trackGeo = new THREE.BufferGeometry();
const vertices = [];
const normals = [];
const uvs = [];
for (let i = 0; i < trackPoints.length; i++) {
const p1 = trackPoints[i];
const p2 = trackPoints[(i + 1) % trackPoints.length];
const dir = new THREE.Vector3().subVectors(p2, p1).normalize();
const perp = new THREE.Vector3(-dir.z, 0, dir.x);
const p1Left = new THREE.Vector3().addVectors(p1, perp.clone().multiplyScalar(trackWidth / 2));
const p1Right = new THREE.Vector3().addVectors(p1, perp.clone().multiplyScalar(-trackWidth / 2));
const p2Left = new THREE.Vector3().addVectors(p2, perp.clone().multiplyScalar(trackWidth / 2));
const p2Right = new THREE.Vector3().addVectors(p2, perp.clone().multiplyScalar(-trackWidth / 2));
// Triangle 1
vertices.push(p1Left.x, 0.02, p1Left.z);
vertices.push(p1Right.x, 0.02, p1Right.z);
vertices.push(p2Left.x, 0.02, p2Left.z);
// Triangle 2
vertices.push(p1Right.x, 0.02, p1Right.z);
vertices.push(p2Right.x, 0.02, p2Right.z);
vertices.push(p2Left.x, 0.02, p2Left.z);
for (let j = 0; j < 6; j++) {
normals.push(0, 1, 0);
uvs.push(0, 0);
}
}
trackGeo.setAttribute('position', new THREE.Float32BufferAttribute(vertices, 3));
trackGeo.setAttribute('normal', new THREE.Float32BufferAttribute(normals, 3));
trackGeo.setAttribute('uv', new THREE.Float32BufferAttribute(uvs, 2));
const trackMat = new THREE.MeshStandardMaterial({
color: 0x2a2a35,
roughness: 0.8,
metalness: 0.2
});
track = new THREE.Mesh(trackGeo, trackMat);
track.receiveShadow = true;
scene.add(track);
// Track borders
createTrackBorders();
// Checkpoints
createCheckpoints();
}
function createTrackBorders() {
const borderRadius = 0.3;
const borderHeight = 0.5;
const innerBorderPoints = [];
const outerBorderPoints = [];
const trackWidth = 12;
for (let i = 0; i < trackPoints.length; i++) {
const p1 = trackPoints[i];
const p2 = trackPoints[(i + 1) % trackPoints.length];
const dir = new THREE.Vector3().subVectors(p2, p1).normalize();
const perp = new THREE.Vector3(-dir.z, 0, dir.x);
innerBorderPoints.push(new THREE.Vector3().addVectors(p1, perp.clone().multiplyScalar(trackWidth / 2 - 0.5)));
outerBorderPoints.push(new THREE.Vector3().addVectors(p1, perp.clone().multiplyScalar(-trackWidth / 2 + 0.5)));
}
// Create curb meshes
const curbMat = new THREE.MeshStandardMaterial({ color: 0xff3e3e });
const curbMat2 = new THREE.MeshStandardMaterial({ color: 0xf0f0f0 });
// Inner curb
for (let i = 0; i < innerBorderPoints.length; i++) {
const p1 = innerBorderPoints[i];
const p2 = innerBorderPoints[(i + 1) % innerBorderPoints.length];
const curbGeo = new THREE.BoxGeometry(0.5, borderHeight, p1.distanceTo(p2) + 0.5);
const curb = new THREE.Mesh(curbGeo, i % 2 === 0 ? curbMat : curbMat2);
curb.position.copy(p1).add(p2).multiplyScalar(0.5);
curb.position.y = borderHeight / 2;
curb.lookAt(p2);
curb.rotateY(Math.PI / 2);
scene.add(curb);
}
// Outer curb
for (let i = 0; i < outerBorderPoints.length; i++) {
const p1 = outerBorderPoints[i];
const p2 = outerBorderPoints[(i + 1) % outerBorderPoints.length];
const curbGeo = new THREE.BoxGeometry(0.5, borderHeight, p1.distanceTo(p2) + 0.5);
const curb = new THREE.Mesh(curbGeo, i % 2 === 0 ? curbMat : curbMat2);
curb.position.copy(p1).add(p2).multiplyScalar(0.5);
curb.position.y = borderHeight / 2;
curb.lookAt(p2);
curb.rotateY(Math.PI / 2);
scene.add(curb);
}
}
function createCheckpoints() {
const numCheckpoints = 8;
for (let i = 0; i < numCheckpoints; i++) {
const idx = Math.floor((i / numCheckpoints) * trackPoints.length);
const p1 = trackPoints[idx];
const p2 = trackPoints[(idx + 2) % trackPoints.length];
const center = new THREE.Vector3().addVectors(p1, p2).multiplyScalar(0.5);
const dir = new THREE.Vector3().subVectors(p2, p1).normalize();
checkpoints.push({
position: center,
direction: dir,
passed: false
});
}
}
// Car
function createCar() {
car = new THREE.Group();
// Car body shader material
const carMat = new THREE.ShaderMaterial({
vertexShader: document.getElementById('vertexShader').textContent,
fragmentShader: document.getElementById('carFragmentShader').textContent,
uniforms: {
uColor: { value: new THREE.Color(0xff3e3e) },
uLightPosition: { value: new THREE.Vector3(50, 100, 50) },
uCameraPosition: { value: new THREE.Vector3() },
uTime: { value: 0 }
}
});
// Main body
const bodyGeo = new THREE.BoxGeometry(2, 0.6, 4);
carBody = new THREE.Mesh(bodyGeo, carMat);
carBody.position.y = 0.5;
carBody.castShadow = true;
car.add(carBody);
// Cabin
const cabinGeo = new THREE.BoxGeometry(1.6, 0.5, 1.8);
const cabinMat = new THREE.MeshStandardMaterial({
color: 0x111111,
metalness: 0.9,
roughness: 0.1
});
const cabin = new THREE.Mesh(cabinGeo, cabinMat);
cabin.position.set(0, 1, -0.2);
cabin.castShadow = true;
car.add(cabin);
// Hood
const hoodGeo = new THREE.BoxGeometry(1.8, 0.2, 1.2);
const hood = new THREE.Mesh(hoodGeo, carMat);
hood.position.set(0, 0.8, 1.2);
hood.castShadow = true;
car.add(hood);
// Spoiler
const spoilerGeo = new THREE.BoxGeometry(1.8, 0.1, 0.3);
const spoiler = new THREE.Mesh(spoilerGeo, carMat);
spoiler.position.set(0, 1.1, -1.8);
spoiler.castShadow = true;
car.add(spoiler);
// Spoiler stands
const standGeo = new THREE.BoxGeometry(0.1, 0.3, 0.1);
const stand1 = new THREE.Mesh(standGeo, carMat);
stand1.position.set(0.7, 0.95, -1.8);
car.add(stand1);
const stand2 = new THREE.Mesh(standGeo, carMat);
stand2.position.set(-0.7, 0.95, -1.8);
car.add(stand2);
// Wheels
const wheelGeo = new THREE.CylinderGeometry(0.35, 0.35, 0.25, 16);
const wheelMat = new THREE.MeshStandardMaterial({
color: 0x1a1a1a,
metalness: 0.3,
roughness: 0.7
});
const wheelPositions = [
{ x: -1, y: 0.35, z: 1.2 },
{ x: 1, y: 0.35, z: 1.2 },
{ x: -1, y: 0.35, z: -1.2 },
{ x: 1, y: 0.35, z: -1.2 }
];
wheelPositions.forEach(pos => {
const wheel = new THREE.Mesh(wheelGeo, wheelMat);
wheel.rotation.z = Math.PI / 2;
wheel.position.set(pos.x, pos.y, pos.z);
wheel.castShadow = true;
wheels.push(wheel);
car.add(wheel);
// Rim
const rimGeo = new THREE.CylinderGeometry(0.2, 0.2, 0.26, 6);
const rimMat = new THREE.MeshStandardMaterial({
color: 0xcccccc,
metalness: 0.9,
roughness: 0.2
});
const rim = new THREE.Mesh(rimGeo, rimMat);
rim.rotation.z = Math.PI / 2;
rim.position.set(pos.x, pos.y, pos.z);
car.add(rim);
});
// Headlights
const headlightGeo = new THREE.SphereGeometry(0.15, 8, 8);
const headlightMat = new THREE.MeshBasicMaterial({ color: 0xffffcc });
const hl1 = new THREE.Mesh(headlightGeo, headlightMat);
hl1.position.set(0.6, 0.5, 2);
car.add(hl1);
const hl2 = new THREE.Mesh(headlightGeo, headlightMat);
hl2.position.set(-0.6, 0.5, 2);
car.add(hl2);
// Taillights
const taillightMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
const tl1 = new THREE.Mesh(headlightGeo, taillightMat);
tl1.position.set(0.6, 0.5, -2);
car.add(tl1);
const tl2 = new THREE.Mesh(headlightGeo, taillightMat);
tl2.position.set(-0.6, 0.5, -2);
car.add(tl2);
// Position car
car.position.copy(trackPoints[0]);
car.position.y = 0;
// Face correct direction
const firstDir = new THREE.Vector3().subVectors(trackPoints[1], trackPoints[0]).normalize();
car.rotation.y = Math.atan2(firstDir.x, firstDir.z);
carPhysics.position.copy(car.position);
carPhysics.rotation = car.rotation.y;
scene.add(car);
}
// Lights
function createLights() {
// Ambient
const ambient = new THREE.AmbientLight(0x404060, 0.5);
scene.add(ambient);
// Main directional light (sun)
const sun = new THREE.DirectionalLight(0xffeedd, 1);
sun.position.set(50, 100, 50);
sun.castShadow = true;
sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;
sun.shadow.camera.near = 0.5;
sun.shadow.camera.far = 500;
sun.shadow.camera.left = -100;
sun.shadow.camera.right = 100;
sun.shadow.camera.top = 100;
sun.shadow.camera.bottom = -100;
scene.add(sun);
// Fill light
const fill = new THREE.DirectionalLight(0x8888ff, 0.3);
fill.position.set(-50, 50, -50);
scene.add(fill);
// Hemisphere
const hemi = new THREE.HemisphereLight(0x606080, 0x404040, 0.5);
scene.add(hemi);
}
// Mini Map
function setupMiniMap() {
miniMapCanvas = document.getElementById('miniMapCanvas');
miniMapCanvas.width = 150;
miniMapCanvas.height = 150;
miniMapCtx = miniMapCanvas.getContext('2d');
}
function updateMiniMap() {
const ctx = miniMapCtx;
const w = miniMapCanvas.width;
const h = miniMapCanvas.height;
const scale = 0.8;
const offsetX = w / 2;
const offsetY = h / 2;
// Clear
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillRect(0, 0, w, h);
// Track
ctx.strokeStyle = '#2a2a35';
ctx.lineWidth = 8;
ctx.beginPath();
for (let i = 0; i < trackPoints.length; i++) {
const x = (trackPoints[i].x - carPhysics.position.x) * scale + offsetX;
const y = (trackPoints[i].z - carPhysics.position.z) * scale + offsetY;
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.closePath();
ctx.stroke();
// Car position
ctx.fillStyle = '#ff3e3e';
ctx.beginPath();
ctx.arc(offsetX, offsetY, 4, 0, Math.PI * 2);
ctx.fill();
// Direction indicator
ctx.strokeStyle = '#ff3e3e';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(offsetX, offsetY);
ctx.lineTo(
offsetX + Math.sin(carPhysics.rotation) * 8,
offsetY - Math.cos(carPhysics.rotation) * 8
);
ctx.stroke();
}
// Gear Indicator
function setupGearIndicator() {
const container = document.getElementById('gearIndicator');
for (let i = 1; i <= 6; i++) {
const gear = document.createElement('div');
gear.className = 'gear';
gear.textContent = i;
gear.dataset.gear = i;
container.appendChild(gear);
}
}
function updateGearIndicator() {
const speed = carPhysics.speed;
let gear;
if (speed < 10) gear = 1;
else if (speed < 25) gear = 2;
else if (speed < 45) gear = 3;
else if (speed < 70) gear = 4;
else if (speed < 95) gear = 5;
else gear = 6;
document.querySelectorAll('.gear').forEach(g => {
g.classList.toggle('active', parseInt(g.dataset.gear) === gear);
});
}
// Event Listeners
function setupEventListeners() {
// Keyboard
document.addEventListener('keydown', (e) => {
if (currentState !== GameState.PLAYING) return;
switch(e.key.toLowerCase()) {
case 'w':
case 'arrowup':
keys.forward = true;
document.getElementById('keyW').classList.add('active');
break;
case 's':
case 'arrowdown':
keys.backward = true;
document.getElementById('keyS').classList.add('active');
break;
case 'a':
case 'arrowleft':
keys.left = true;
document.getElementById('keyA').classList.add('active');
break;
case 'd':
case 'arrowright':
keys.right = true;
document.getElementById('keyD').classList.add('active');
break;
}
});
document.addEventListener('keyup', (e) => {
switch(e.key.toLowerCase()) {
case 'w':
case 'arrowup':
keys.forward = false;
document.getElementById('keyW').classList.remove('active');
break;
case 's':
case 'arrowdown':
keys.backward = false;
document.getElementById('keyS').classList.remove('active');
break;