<!
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Simple 3D Racer V7 - Car Selection</title>
<!-- Changed Title -->
<style>
body {
margin: 0;
overflow: hidden;
background-color: #333; /* Darker background initially */
color: white;
font-family: Arial, sans-serif;
}
canvas {
display: block; /* Will be managed by JS */
}
/* --- UI Screens --- */
.overlay {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
z-index: 100; /* High z-index */
}
#splash-screen {
background-color: #222; /* Dark splash */
font-size: 2em;
transition: opacity 0.5s ease-out; /* Fade out effect */
}
#car-select-screen {
background-color: rgba(0, 0, 50, 0.8); /* Semi-transparent blue */
display: none; /* Hidden initially */
padding: 20px;
box-sizing: border-box;
}
#car-select-screen h2 {
margin-bottom: 30px;
}
#car-options {
display: flex;
justify-content: center;
gap: 30px; /* Space between options */
flex-wrap: wrap; /* Allow wrapping on smaller screens */
}
.car-option {
padding: 15px 25px;
border: 2px solid white;
border-radius: 5px;
cursor: pointer;
transition: background-color 0.3s, transform 0.2s;
background-color: rgba(255, 255, 255, 0.1);
min-width: 120px; /* Ensure buttons have some width */
}
.car-option:hover {
background-color: rgba(255, 255, 255, 0.3);
transform: scale(1.05);
}
/* Style previews (simple colored boxes for now) */
.car-preview {
width: 50px;
height: 25px;
margin: 0 auto 10px auto; /* Center preview above text */
border: 1px solid #ccc;
}
.preview-red {
background-color: #a52523;
}
.preview-blue {
background-color: #2355a5;
}
.preview-green {
background-color: #23a55e;
}
.preview-yellow {
background-color: #d4c831;
}
#info {
/* Game UI */
position: absolute;
top: 10px;
left: 10px;
color: white;
font-family: Arial, sans-serif;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px 10px;
border-radius: 3px;
z-index: 10;
display: none; /* Hide until game starts */
}
#info::after {
content: "\A Use Mouse Drag to Orbit Camera";
white-space: pre;
display: block;
margin-top: 4px;
font-size: 0.9em;
}
</style>
</head>
<body>
<!-- UI Screens -->
<div id="splash-screen" class="overlay">
<p>Loading Racer...</p>
</div>
<div id="car-select-screen" class="overlay">
<h2>Select Your Car</h2>
<div id="car-options">
<div class="car-option" data-car-type="racer">
<div class="car-preview preview-red"></div>
Red Racer
</div>
<div class="car-option" data-car-type="truck">
<div class="car-preview preview-blue"></div>
Blue Truck
</div>
<div class="car-option" data-car-type="sporty">
<div class="car-preview preview-green"></div>
Green Sporty
</div>
<div class="car-option" data-car-type="buggy">
<div class="car-preview preview-yellow"></div>
Yellow Buggy
</div>
</div>
</div>
<!-- Game Info Display -->
<div id="info">
Use WASD or Arrow Keys to drive.<br />
Speed: <span id="speed">0.00</span>
</div>
<!-- Three.js library -->
<script
src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<!-- OrbitControls Add-on -->
<script
src="https://unpkg.com/
[email protected]/examples/js/controls/OrbitControls.js"></
script>
<script>
// --- Global Scope Variables ---
let scene, camera, renderer, car, ground, clock, orbitControls;
let selectedCarType = "racer"; // Default car type
// Game state / UI elements
const splashScreen = document.getElementById("splash-screen");
const carSelectScreen = document.getElementById("car-select-screen");
const gameInfoUI = document.getElementById("info");
const carOptions = document.querySelectorAll(".car-option");
// --- GAME VARIABLES ---
const carSpeed = {
current: 0,
max: 15,
acceleration: 8,
braking: 15,
friction: 3,
handling: 1.5,
};
const controls = {
forward: false,
backward: false,
left: false,
right: false,
};
const cameraFollowOffset = new THREE.Vector3(0, 5, -10);
const cameraLookAtOffset = new THREE.Vector3(0, 1.0, 0);
const maxSteerAngle = Math.PI / 6;
const steerLerpFactor = 8;
// --- INITIALIZATION (Called AFTER car selection) ---
function init(carType) {
// Set background back to sky blue for the game
document.body.style.backgroundColor = "#77b5fe";
clock = new THREE.Clock(); // Initialize clock here
// Scene, Camera, Renderer
scene = new THREE.Scene();
scene.background = new THREE.Color(0x77b5fe);
scene.fog = new THREE.Fog(0x77b5fe, 50, 150);
camera = new THREE.PerspectiveCamera(
75,
window.innerWidth / window.innerHeight,
0.1,
1000
);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// *** Append renderer canvas to body NOW ***
document.body.appendChild(renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 1.0);
directionalLight.position.set(70, 60, 40);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 200;
directionalLight.shadow.camera.left = -80;
directionalLight.shadow.camera.right = 80;
directionalLight.shadow.camera.top = 80;
directionalLight.shadow.camera.bottom = -80;
directionalLight.shadow.bias = -0.0005;
scene.add(directionalLight);
// Ground
const groundGeometry = new THREE.PlaneGeometry(200, 200);
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x448844,
side: THREE.DoubleSide,
roughness: 0.8,
metalness: 0.2,
});
ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.receiveShadow = true;
scene.add(ground);
// --- CREATE SELECTED CAR ---
car = createCar(carType); // Pass the selected type
car.position.set(0, car.userData.wheelRadius || 0.35, 0);
scene.add(car);
// --- SETUP ORBIT CONTROLS ---
orbitControls = new THREE.OrbitControls(camera, renderer.domElement);
const initialTarget = car.position.clone().add(cameraLookAtOffset);
orbitControls.target.copy(initialTarget);
const initialCameraPos = initialTarget
.clone()
.add(cameraFollowOffset.clone().applyQuaternion(car.quaternion));
camera.position.copy(initialCameraPos);
orbitControls.enableDamping = true;
orbitControls.dampingFactor = 0.1;
orbitControls.minDistance = 4;
orbitControls.maxDistance = 40;
orbitControls.maxPolarAngle = Math.PI / 2 - 0.05;
orbitControls.update();
// Boundaries
createBoundary(-50, 0, 100, 1);
createBoundary(50, 0, 100, 1);
createBoundary(0, -50, 1, 100);
createBoundary(0, 50, 1, 100);
// Show Game UI
gameInfoUI.style.display = "block";
// Event Listeners for Game
window.addEventListener("keydown", handleKeyDown);
window.addEventListener("keyup", handleKeyUp);
window.addEventListener("resize", onWindowResize);
// Start the game loop
animate();
}
// --- CAR CREATION (Handles different types) ---
function createCar(type = "racer") {
// Default to racer if type is missing
const carGroup = new THREE.Group();
carGroup.userData.wheels = [];
carGroup.userData.frontWheels = [];
let carColor, cabinColor, wheelColor, glassColor;
let bodyGeo, bodyMat, bodyMesh;
let cabinGeo, cabinMat, cabinMesh;
let windshieldGeo, windshieldMat, windshieldMesh;
let wheelRadius = 0.35,
wheelThickness = 0.25; // Default dimensions
let wheelY, wheelXOffset, wheelZOffset;
const wheelSegments = 16;
glassColor = 0x6699cc; // Common glass color
// Define base materials
const defaultWheelMaterial = new THREE.MeshStandardMaterial({
color: 0x111111,
roughness: 0.8,
metalness: 0.1,
});
const defaultGlassMaterial = new THREE.MeshPhysicalMaterial({
color: glassColor,
roughness: 0.1,
metalness: 0.0,
transmission: 0.9,
transparent: true,
opacity: 0.6,
});
// --- Car Type Specific Logic ---
switch (type) {
case "truck":
carColor = 0x2355a5; // Blue
cabinColor = 0xcccccc; // Light grey
wheelColor = 0x222222; // Darker wheels
wheelRadius = 0.45; // Larger wheels
wheelThickness = 0.3;
wheelXOffset = 0.7;
wheelZOffset = 1.1;
// Body (Blockier)
bodyGeo = new THREE.BoxGeometry(1.6, 0.8, 3.0);
bodyMat = new THREE.MeshStandardMaterial({
color: carColor,
roughness: 0.6,
metalness: 0.4,
});
bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
bodyMesh.position.y = 0.4; // Higher base
carGroup.add(bodyMesh);
// Cabin (Taller, more forward)
cabinGeo = new THREE.BoxGeometry(1.2, 0.9, 1.4);
cabinMat = new THREE.MeshStandardMaterial({
color: cabinColor,
roughness: 0.7,
});
cabinMesh = new THREE.Mesh(cabinGeo, cabinMat);
cabinMesh.position.y = 0.8 + 0.45; // Position on top of body
cabinMesh.position.z = 0.1; // More forward placement
carGroup.add(cabinMesh);
// Windshield (More upright)
windshieldGeo = new THREE.BoxGeometry(1.1, 0.7, 0.1);
windshieldMesh = new THREE.Mesh(
windshieldGeo,
defaultGlassMaterial
);
windshieldMesh.position.y = 0.8 + 0.45;
windshieldMesh.position.z = 1.4 / 2 + 0.1 + 0.1 / 2; // Front of cabin
windshieldMesh.rotation.x = Math.PI / 18; // Less slant
carGroup.add(windshieldMesh);
break;
case "sporty":
carColor = 0x23a55e; // Green
cabinColor = 0xdddddd;
wheelColor = 0x1a1a1a;
wheelRadius = 0.3; // Smaller wheels
wheelThickness = 0.28;
wheelXOffset = 0.7; // Wider stance
wheelZOffset = 1.0;
// Body (Lower, wider)
bodyGeo = new THREE.BoxGeometry(1.5, 0.4, 3.2);
bodyMat = new THREE.MeshStandardMaterial({
color: carColor,
roughness: 0.3,
metalness: 0.7,
});
bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
bodyMesh.position.y = 0.2; // Lower base
carGroup.add(bodyMesh);
// Cabin (Sleek, further back)
cabinGeo = new THREE.BoxGeometry(0.8, 0.5, 1.5); // Longer cabin
cabinMat = new THREE.MeshStandardMaterial({
color: cabinColor,
roughness: 0.5,
});
cabinMesh = new THREE.Mesh(cabinGeo, cabinMat);
cabinMesh.position.y = 0.4 + 0.25; // Top of body + half height
cabinMesh.position.z = -0.5; // Further back
carGroup.add(cabinMesh);
// Windshield (Very slanted)
windshieldGeo = new THREE.BoxGeometry(0.75, 0.4, 0.1);
windshieldMesh = new THREE.Mesh(
windshieldGeo,
defaultGlassMaterial
);
windshieldMesh.position.y = 0.4 + 0.25;
windshieldMesh.position.z = 1.5 / 2 - 0.5 + 0.1 / 2; // Front of cabin
windshieldMesh.rotation.x = Math.PI / 5; // More slant
carGroup.add(windshieldMesh);
// Simple Spoiler
const spoilerGeo = new THREE.BoxGeometry(1.4, 0.1, 0.3);
const spoilerMat = new THREE.MeshStandardMaterial({
color: carColor,
roughness: 0.4,
metalness: 0.6,
});
const spoilerMesh = new THREE.Mesh(spoilerGeo, spoilerMat);
spoilerMesh.position.set(0, 0.6, -1.5); // Behind body
spoilerMesh.rotation.x = -Math.PI / 12; // Slight downward angle
carGroup.add(spoilerMesh);
break;
case "buggy":
carColor = 0xd4c831; // Yellow
cabinColor = 0x555555; // Darker frame elements
wheelColor = 0x333333;
wheelRadius = 0.4; // Medium-large wheels
wheelThickness = 0.35; // Thicker wheels
wheelXOffset = 0.75; // Wide stance
wheelZOffset = 0.7; // Shorter wheelbase
// Body (Minimal chassis)
bodyGeo = new THREE.BoxGeometry(1.0, 0.3, 1.8);
bodyMat = new THREE.MeshStandardMaterial({
color: carColor,
roughness: 0.7,
metalness: 0.3,
});
bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
bodyMesh.position.y = 0.15; // Very low
carGroup.add(bodyMesh);
// "Cabin" (Roll cage elements - simplified with boxes)
const cageMat = new THREE.MeshStandardMaterial({
color: cabinColor,
roughness: 0.5,
});
const pillarGeo = new THREE.BoxGeometry(0.1, 0.8, 0.1);
// Front pillars
const pillarFL = new THREE.Mesh(pillarGeo, cageMat);
pillarFL.position.set(-0.45, 0.15 + 0.4, 0.4);
carGroup.add(pillarFL);
const pillarFR = new THREE.Mesh(pillarGeo, cageMat);
pillarFR.position.set(0.45, 0.15 + 0.4, 0.4);
carGroup.add(pillarFR);
// Rear pillars
const pillarRL = new THREE.Mesh(pillarGeo, cageMat);
pillarRL.position.set(-0.45, 0.15 + 0.4, -0.6);
carGroup.add(pillarRL);
const pillarRR = new THREE.Mesh(pillarGeo, cageMat);
pillarRR.position.set(0.45, 0.15 + 0.4, -0.6);
carGroup.add(pillarRR);
// Top bars
const topBarGeo = new THREE.BoxGeometry(1.0, 0.1, 0.1);
const topBarL = new THREE.Mesh(topBarGeo, cageMat);
topBarL.position.set(-0.45, 0.15 + 0.8, -0.1);
carGroup.add(topBarL);
const topBarR = new THREE.Mesh(topBarGeo, cageMat);
topBarR.position.set(0.45, 0.15 + 0.8, -0.1);
carGroup.add(topBarR);
const topBarCrossGeo = new THREE.BoxGeometry(0.1, 0.1, 1.1);
const topBarF = new THREE.Mesh(topBarCrossGeo, cageMat);
topBarF.position.set(0, 0.15 + 0.8, 0.4);
carGroup.add(topBarF);
const topBarRe = new THREE.Mesh(topBarCrossGeo, cageMat);
topBarRe.position.set(0, 0.15 + 0.8, -0.6);
carGroup.add(topBarRe);
// No windshield for buggy
break;
case "racer": // Default / Explicit Racer
default:
carColor = 0xa52523; // Red
cabinColor = 0xffffff;
wheelColor = 0x111111;
// Use default wheel dimensions set earlier
wheelRadius = 0.35;
wheelThickness = 0.25;
wheelXOffset = 0.65;
wheelZOffset = 0.9;
// Original Body
bodyGeo = new THREE.BoxGeometry(1.3, 0.5, 2.8);
bodyMat = new THREE.MeshStandardMaterial({
color: carColor,
roughness: 0.4,
metalness: 0.6,
});
bodyMesh = new THREE.Mesh(bodyGeo, bodyMat);
bodyMesh.position.y = 0.25;
carGroup.add(bodyMesh);
// Original Cabin
cabinGeo = new THREE.BoxGeometry(0.9, 0.6, 1.2);
cabinMat = new THREE.MeshStandardMaterial({
color: cabinColor,
roughness: 0.6,
metalness: 0.4,
});
cabinMesh = new THREE.Mesh(cabinGeo, cabinMat);
cabinMesh.position.y = 0.5 + 0.3;
cabinMesh.position.z = -0.4;
carGroup.add(cabinMesh);
// Original Windshield
windshieldGeo = new THREE.BoxGeometry(0.85, 0.4, 0.1);
windshieldMesh = new THREE.Mesh(
windshieldGeo,
defaultGlassMaterial
);
windshieldMesh.position.y = 0.5 + 0.3;
windshieldMesh.position.z = 1.2 / 2 - 0.4 + 0.1 / 2;
windshieldMesh.rotation.x = Math.PI / 9;
carGroup.add(windshieldMesh);
break;
}
// --- Common Car Setup (Applies to all types) ---
// Make sure body and cabin cast shadows if they exist
if (bodyMesh) bodyMesh.castShadow = true;
if (cabinMesh) cabinMesh.castShadow = true;
// Set final wheel properties based on selected type
carGroup.userData.wheelRadius = wheelRadius;
wheelY = wheelRadius; // Position wheels relative to ground
// Wheels (Geometry/Material/Placement - uses variables set in switch)
const wheelGeometry = new THREE.CylinderGeometry(
wheelRadius,
wheelRadius,
wheelThickness,
wheelSegments
);
const wheelMaterial = new THREE.MeshStandardMaterial({
color: wheelColor || 0x111111,
roughness: 0.8,
metalness: 0.1,
}); // Use specific or default
const finalWheelPositions = [
{ x: -wheelXOffset, y: wheelY, z: wheelZOffset, isFront: true },
{ x: wheelXOffset, y: wheelY, z: wheelZOffset, isFront: true },
{ x: -wheelXOffset, y: wheelY, z: -wheelZOffset, isFront: false },
{ x: wheelXOffset, y: wheelY, z: -wheelZOffset, isFront: false },
];
finalWheelPositions.forEach((posData) => {
const wheelMesh = new THREE.Mesh(wheelGeometry, wheelMaterial);
wheelMesh.position.set(posData.x, posData.y, posData.z);
wheelMesh.rotation.order = "YXZ";
wheelMesh.rotation.z = Math.PI / 2; // Stand upright
wheelMesh.userData.totalRoll = 0; // Initialize roll accumulator
wheelMesh.castShadow = true;
carGroup.add(wheelMesh);
carGroup.userData.wheels.push(wheelMesh);
if (posData.isFront) {
carGroup.userData.frontWheels.push(wheelMesh);
}
});
return carGroup;
}
function createBoundary(x, z, width, depth) {
/* Unchanged */
const boundaryGeo = new THREE.BoxGeometry(width, 2, depth);
const boundaryMat = new THREE.MeshStandardMaterial({
color: 0x777777,
roughness: 0.9,
metalness: 0.1,
});
const boundary = new THREE.Mesh(boundaryGeo, boundaryMat);
boundary.position.set(x, 1, z);
boundary.receiveShadow = true;
scene.add(boundary);
}
// --- EVENT HANDLERS for GAMEPLAY --- (Unchanged)
function handleKeyDown(event) {
/* ... same as before ... */
}
function handleKeyUp(event) {
/* ... same as before ... */
}
function onWindowResize() {
/* ... same as before ... */
}
// (Code for keydown/keyup/resize is identical to previous version, omitted
here for brevity)
function handleKeyDown(event) {
switch (event.key) {
case "w":
case "ArrowUp":
controls.forward = true;
break;
case "s":
case "ArrowDown":
controls.backward = true;
break;
case "a":
case "ArrowLeft":
controls.left = true;
break;
case "d":
case "ArrowRight":
controls.right = true;
break;
}
}
function handleKeyUp(event) {
switch (event.key) {
case "w":
case "ArrowUp":
controls.forward = false;
break;
case "s":
case "ArrowDown":
controls.backward = false;
break;
case "a":
case "ArrowLeft":
controls.left = false;
break;
case "d":
case "ArrowRight":
controls.right = false;
break;
}
}
function onWindowResize() {
if (camera && renderer) {
// Check if they exist (game might not be initialized yet)
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
}
// --- GAME LOGIC UPDATE --- (Unchanged logic, uses the created 'car')
function updateCar(deltaTime) {
/* ... same as before ... */
}
// (Code for updateCar is identical to previous version, omitted here for
brevity)
function updateCar(deltaTime) {
// Physics
let acceleration = 0;
if (controls.forward) {
acceleration = carSpeed.acceleration;
} else if (controls.backward) {
acceleration =
carSpeed.current > 0.1
? -carSpeed.braking
: -carSpeed.acceleration * 0.7;
}
carSpeed.current += acceleration * deltaTime;
if (!controls.forward && !controls.backward) {
const frictionDirection = Math.sign(carSpeed.current);
if (frictionDirection !== 0) {
const speedChange = carSpeed.friction * deltaTime;
carSpeed.current -= frictionDirection * speedChange;
if (Math.sign(carSpeed.current) !== frictionDirection) {
carSpeed.current = 0;
}
}
}
carSpeed.current = Math.max(
-carSpeed.max * 0.5,
Math.min(carSpeed.max, carSpeed.current)
);
// Car Body Steering
let bodySteeringInput = 0;
if (controls.left) {
bodySteeringInput = carSpeed.handling;
}
if (controls.right) {
bodySteeringInput = -carSpeed.handling;
}
const speedFactor = Math.min(
1,
Math.abs(carSpeed.current) / (carSpeed.max * 0.5) + 0.3
);
const bodySteeringAmount = bodySteeringInput * speedFactor * deltaTime;
if (
Math.abs(carSpeed.current) > 0.01 ||
controls.forward ||
controls.backward
) {
car.rotation.y +=
bodySteeringAmount * (carSpeed.current >= 0 ? 1 : -1);
}
// Movement
const localMoveZ = carSpeed.current * deltaTime;
car.translateZ(localMoveZ);
// Wheel Animations
if (car.userData.wheels && car.userData.wheelRadius) {
const distanceMoved = localMoveZ;
const deltaWheelRotation = distanceMoved / car.userData.wheelRadius;
let targetSteerAngle = 0;
if (controls.left) {
targetSteerAngle = maxSteerAngle;
} else if (controls.right) {
targetSteerAngle = -maxSteerAngle;
}
car.userData.wheels.forEach((wheel) => {
const isFront = car.userData.frontWheels.includes(wheel);
wheel.userData.totalRoll -= deltaWheelRotation;
if (isFront) {
wheel.rotation.y = THREE.MathUtils.lerp(
wheel.rotation.y,
targetSteerAngle,
steerLerpFactor * deltaTime
);
} else {
wheel.rotation.y = 0;
}
wheel.rotation.x = wheel.userData.totalRoll;
});
}
// Update UI
if (gameInfoUI.style.display === "block") {
// Only update if visible
document.getElementById("speed").textContent = Math.abs(
carSpeed.current
).toFixed(2);
}
}
// --- CAMERA UPDATE --- (Unchanged logic)
function updateCamera(deltaTime) {
/* ... same as before ... */
}
// (Code for updateCamera is identical to previous version, omitted here for
brevity)
function updateCamera(deltaTime) {
if (!orbitControls || !car) return; // Safety check if called before init
finishes
const targetPosition = car.position.clone().add(cameraLookAtOffset);
orbitControls.target.lerp(targetPosition, 5 * deltaTime);
const desiredCameraPosition = car.position
.clone()
.add(cameraFollowOffset.clone().applyQuaternion(car.quaternion));
const distanceToTarget = camera.position.distanceTo(
orbitControls.target
);
const desiredDistance = cameraFollowOffset.length();
const distanceThreshold = 0.1;
if (
Math.abs(distanceToTarget - desiredDistance) <
distanceThreshold * desiredDistance
) {
camera.position.lerp(desiredCameraPosition, 4 * deltaTime);
}
}
// --- ANIMATION LOOP ---
function animate() {
// Only run if game objects exist (init has been called)
if (!renderer || !scene || !camera || !car) {
console.warn("Game not fully initialized, skipping animation frame.");
return; // Exit if game isn't ready
}
requestAnimationFrame(animate); // Request next frame *conditionally* or
always? Always is safer.
// Let's request always, but logic inside handles if ready.
// requestAnimationFrame(animate);
const deltaTime = clock.getDelta();
updateCar(deltaTime);
updateCamera(deltaTime);
orbitControls.update(); // MUST be called after camera/target updates
renderer.render(scene, camera);
}
// --- UI Flow & Game Start ---
// 1. Show Splash Screen
splashScreen.style.opacity = 1;
carSelectScreen.style.display = "none";
gameInfoUI.style.display = "none";
// 2. After a delay, hide splash, show car select
setTimeout(() => {
splashScreen.style.opacity = 0;
// Use another timeout to ensure display:none happens *after* fade
setTimeout(() => {
splashScreen.style.display = "none";
carSelectScreen.style.display = "flex"; // Show car select screen
}, 500); // Match CSS transition duration
}, 2000); // Splash screen duration (2 seconds)
// 3. Handle Car Selection Clicks
carOptions.forEach((button) => {
button.addEventListener("click", () => {
selectedCarType = button.getAttribute("data-car-type");
console.log("Selected car:", selectedCarType);
// Hide selection screen
carSelectScreen.style.display = "none";
// --- Initialize and start the game ---
try {
init(selectedCarType); // Pass selection to init
} catch (error) {
console.error("Error initializing game:", error);
// Optional: Show an error message to the user
document.body.innerHTML =
'<p style="color:red; padding: 20px;">Error loading game. Please
check console.</p>';
}
});
});
// NOTE: We DO NOT call init() or animate() directly here anymore.
// init() is called via the car selection button click.
// animate() is called at the end of init().
</script>
</body>
</html>