Thanks to visit codestin.com
Credit goes to www.scribd.com

0% found this document useful (0 votes)
27 views14 pages

Message

The document outlines the HTML structure and styling for a simple 3D racing game car selection interface. It includes a splash screen, car selection options with previews, and game information display. The JavaScript section initializes the game environment using Three.js, allowing users to select different car types and manage game controls.

Uploaded by

Fahmi
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
27 views14 pages

Message

The document outlines the HTML structure and styling for a simple 3D racing game car selection interface. It includes a splash screen, car selection options with previews, and game information display. The JavaScript section initializes the game environment using Three.js, allowing users to select different car types and manage game controls.

Uploaded by

Fahmi
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as TXT, PDF, TXT or read online on Scribd
You are on page 1/ 14

<!

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>

You might also like