<!
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>
<!-- [Link] library -->
<script
src="[Link]
<!-- OrbitControls Add-on -->
<script
src="[Link]
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 = [Link]("splash-screen");
const carSelectScreen = [Link]("car-select-screen");
const gameInfoUI = [Link]("info");
const carOptions = [Link](".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 = [Link] / 6;
const steerLerpFactor = 8;
// --- INITIALIZATION (Called AFTER car selection) ---
function init(carType) {
// Set background back to sky blue for the game
[Link] = "#77b5fe";
clock = new [Link](); // Initialize clock here
// Scene, Camera, Renderer
scene = new [Link]();
[Link] = new [Link](0x77b5fe);
[Link] = new [Link](0x77b5fe, 50, 150);
camera = new [Link](
75,
[Link] / [Link],
0.1,
1000
);
renderer = new [Link]({ antialias: true });
[Link]([Link], [Link]);
[Link] = true;
[Link] = [Link];
// *** Append renderer canvas to body NOW ***
[Link]([Link]);
// Lighting
const ambientLight = new [Link](0xffffff, 0.7);
[Link](ambientLight);
const directionalLight = new [Link](0xffffff, 1.0);
[Link](70, 60, 40);
[Link] = true;
[Link] = 2048;
[Link] = 2048;
[Link] = 0.5;
[Link] = 200;
[Link] = -80;
[Link] = 80;
[Link] = 80;
[Link] = -80;
[Link] = -0.0005;
[Link](directionalLight);
// Ground
const groundGeometry = new [Link](200, 200);
const groundMaterial = new [Link]({
color: 0x448844,
side: [Link],
roughness: 0.8,
metalness: 0.2,
});
ground = new [Link](groundGeometry, groundMaterial);
[Link].x = -[Link] / 2;
[Link] = true;
[Link](ground);
// --- CREATE SELECTED CAR ---
car = createCar(carType); // Pass the selected type
[Link](0, [Link] || 0.35, 0);
[Link](car);
// --- SETUP ORBIT CONTROLS ---
orbitControls = new [Link](camera, [Link]);
const initialTarget = [Link]().add(cameraLookAtOffset);
[Link](initialTarget);
const initialCameraPos = initialTarget
.clone()
.add([Link]().applyQuaternion([Link]));
[Link](initialCameraPos);
[Link] = true;
[Link] = 0.1;
[Link] = 4;
[Link] = 40;
[Link] = [Link] / 2 - 0.05;
[Link]();
// Boundaries
createBoundary(-50, 0, 100, 1);
createBoundary(50, 0, 100, 1);
createBoundary(0, -50, 1, 100);
createBoundary(0, 50, 1, 100);
// Show Game UI
[Link] = "block";
// Event Listeners for Game
[Link]("keydown", handleKeyDown);
[Link]("keyup", handleKeyUp);
[Link]("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 [Link]();
[Link] = [];
[Link] = [];
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 [Link]({
color: 0x111111,
roughness: 0.8,
metalness: 0.1,
});
const defaultGlassMaterial = new [Link]({
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 [Link](1.6, 0.8, 3.0);
bodyMat = new [Link]({
color: carColor,
roughness: 0.6,
metalness: 0.4,
});
bodyMesh = new [Link](bodyGeo, bodyMat);
[Link].y = 0.4; // Higher base
[Link](bodyMesh);
// Cabin (Taller, more forward)
cabinGeo = new [Link](1.2, 0.9, 1.4);
cabinMat = new [Link]({
color: cabinColor,
roughness: 0.7,
});
cabinMesh = new [Link](cabinGeo, cabinMat);
[Link].y = 0.8 + 0.45; // Position on top of body
[Link].z = 0.1; // More forward placement
[Link](cabinMesh);
// Windshield (More upright)
windshieldGeo = new [Link](1.1, 0.7, 0.1);
windshieldMesh = new [Link](
windshieldGeo,
defaultGlassMaterial
);
[Link].y = 0.8 + 0.45;
[Link].z = 1.4 / 2 + 0.1 + 0.1 / 2; // Front of cabin
[Link].x = [Link] / 18; // Less slant
[Link](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 [Link](1.5, 0.4, 3.2);
bodyMat = new [Link]({
color: carColor,
roughness: 0.3,
metalness: 0.7,
});
bodyMesh = new [Link](bodyGeo, bodyMat);
[Link].y = 0.2; // Lower base
[Link](bodyMesh);
// Cabin (Sleek, further back)
cabinGeo = new [Link](0.8, 0.5, 1.5); // Longer cabin
cabinMat = new [Link]({
color: cabinColor,
roughness: 0.5,
});
cabinMesh = new [Link](cabinGeo, cabinMat);
[Link].y = 0.4 + 0.25; // Top of body + half height
[Link].z = -0.5; // Further back
[Link](cabinMesh);
// Windshield (Very slanted)
windshieldGeo = new [Link](0.75, 0.4, 0.1);
windshieldMesh = new [Link](
windshieldGeo,
defaultGlassMaterial
);
[Link].y = 0.4 + 0.25;
[Link].z = 1.5 / 2 - 0.5 + 0.1 / 2; // Front of cabin
[Link].x = [Link] / 5; // More slant
[Link](windshieldMesh);
// Simple Spoiler
const spoilerGeo = new [Link](1.4, 0.1, 0.3);
const spoilerMat = new [Link]({
color: carColor,
roughness: 0.4,
metalness: 0.6,
});
const spoilerMesh = new [Link](spoilerGeo, spoilerMat);
[Link](0, 0.6, -1.5); // Behind body
[Link].x = -[Link] / 12; // Slight downward angle
[Link](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 [Link](1.0, 0.3, 1.8);
bodyMat = new [Link]({
color: carColor,
roughness: 0.7,
metalness: 0.3,
});
bodyMesh = new [Link](bodyGeo, bodyMat);
[Link].y = 0.15; // Very low
[Link](bodyMesh);
// "Cabin" (Roll cage elements - simplified with boxes)
const cageMat = new [Link]({
color: cabinColor,
roughness: 0.5,
});
const pillarGeo = new [Link](0.1, 0.8, 0.1);
// Front pillars
const pillarFL = new [Link](pillarGeo, cageMat);
[Link](-0.45, 0.15 + 0.4, 0.4);
[Link](pillarFL);
const pillarFR = new [Link](pillarGeo, cageMat);
[Link](0.45, 0.15 + 0.4, 0.4);
[Link](pillarFR);
// Rear pillars
const pillarRL = new [Link](pillarGeo, cageMat);
[Link](-0.45, 0.15 + 0.4, -0.6);
[Link](pillarRL);
const pillarRR = new [Link](pillarGeo, cageMat);
[Link](0.45, 0.15 + 0.4, -0.6);
[Link](pillarRR);
// Top bars
const topBarGeo = new [Link](1.0, 0.1, 0.1);
const topBarL = new [Link](topBarGeo, cageMat);
[Link](-0.45, 0.15 + 0.8, -0.1);
[Link](topBarL);
const topBarR = new [Link](topBarGeo, cageMat);
[Link](0.45, 0.15 + 0.8, -0.1);
[Link](topBarR);
const topBarCrossGeo = new [Link](0.1, 0.1, 1.1);
const topBarF = new [Link](topBarCrossGeo, cageMat);
[Link](0, 0.15 + 0.8, 0.4);
[Link](topBarF);
const topBarRe = new [Link](topBarCrossGeo, cageMat);
[Link](0, 0.15 + 0.8, -0.6);
[Link](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 [Link](1.3, 0.5, 2.8);
bodyMat = new [Link]({
color: carColor,
roughness: 0.4,
metalness: 0.6,
});
bodyMesh = new [Link](bodyGeo, bodyMat);
[Link].y = 0.25;
[Link](bodyMesh);
// Original Cabin
cabinGeo = new [Link](0.9, 0.6, 1.2);
cabinMat = new [Link]({
color: cabinColor,
roughness: 0.6,
metalness: 0.4,
});
cabinMesh = new [Link](cabinGeo, cabinMat);
[Link].y = 0.5 + 0.3;
[Link].z = -0.4;
[Link](cabinMesh);
// Original Windshield
windshieldGeo = new [Link](0.85, 0.4, 0.1);
windshieldMesh = new [Link](
windshieldGeo,
defaultGlassMaterial
);
[Link].y = 0.5 + 0.3;
[Link].z = 1.2 / 2 - 0.4 + 0.1 / 2;
[Link].x = [Link] / 9;
[Link](windshieldMesh);
break;
}
// --- Common Car Setup (Applies to all types) ---
// Make sure body and cabin cast shadows if they exist
if (bodyMesh) [Link] = true;
if (cabinMesh) [Link] = true;
// Set final wheel properties based on selected type
[Link] = wheelRadius;
wheelY = wheelRadius; // Position wheels relative to ground
// Wheels (Geometry/Material/Placement - uses variables set in switch)
const wheelGeometry = new [Link](
wheelRadius,
wheelRadius,
wheelThickness,
wheelSegments
);
const wheelMaterial = new [Link]({
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 },
];
[Link]((posData) => {
const wheelMesh = new [Link](wheelGeometry, wheelMaterial);
[Link](posData.x, posData.y, posData.z);
[Link] = "YXZ";
[Link].z = [Link] / 2; // Stand upright
[Link] = 0; // Initialize roll accumulator
[Link] = true;
[Link](wheelMesh);
[Link](wheelMesh);
if ([Link]) {
[Link](wheelMesh);
}
});
return carGroup;
}
function createBoundary(x, z, width, depth) {
/* Unchanged */
const boundaryGeo = new [Link](width, 2, depth);
const boundaryMat = new [Link]({
color: 0x777777,
roughness: 0.9,
metalness: 0.1,
});
const boundary = new [Link](boundaryGeo, boundaryMat);
[Link](x, 1, z);
[Link] = true;
[Link](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 ([Link]) {
case "w":
case "ArrowUp":
[Link] = true;
break;
case "s":
case "ArrowDown":
[Link] = true;
break;
case "a":
case "ArrowLeft":
[Link] = true;
break;
case "d":
case "ArrowRight":
[Link] = true;
break;
}
}
function handleKeyUp(event) {
switch ([Link]) {
case "w":
case "ArrowUp":
[Link] = false;
break;
case "s":
case "ArrowDown":
[Link] = false;
break;
case "a":
case "ArrowLeft":
[Link] = false;
break;
case "d":
case "ArrowRight":
[Link] = false;
break;
}
}
function onWindowResize() {
if (camera && renderer) {
// Check if they exist (game might not be initialized yet)
[Link] = [Link] / [Link];
[Link]();
[Link]([Link], [Link]);
}
}
// --- 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 ([Link]) {
acceleration = [Link];
} else if ([Link]) {
acceleration =
[Link] > 0.1
? -[Link]
: -[Link] * 0.7;
}
[Link] += acceleration * deltaTime;
if (![Link] && ![Link]) {
const frictionDirection = [Link]([Link]);
if (frictionDirection !== 0) {
const speedChange = [Link] * deltaTime;
[Link] -= frictionDirection * speedChange;
if ([Link]([Link]) !== frictionDirection) {
[Link] = 0;
}
}
}
[Link] = [Link](
-[Link] * 0.5,
[Link]([Link], [Link])
);
// Car Body Steering
let bodySteeringInput = 0;
if ([Link]) {
bodySteeringInput = [Link];
}
if ([Link]) {
bodySteeringInput = -[Link];
}
const speedFactor = [Link](
1,
[Link]([Link]) / ([Link] * 0.5) + 0.3
);
const bodySteeringAmount = bodySteeringInput * speedFactor * deltaTime;
if (
[Link]([Link]) > 0.01 ||
[Link] ||
[Link]
) {
[Link].y +=
bodySteeringAmount * ([Link] >= 0 ? 1 : -1);
}
// Movement
const localMoveZ = [Link] * deltaTime;
[Link](localMoveZ);
// Wheel Animations
if ([Link] && [Link]) {
const distanceMoved = localMoveZ;
const deltaWheelRotation = distanceMoved / [Link];
let targetSteerAngle = 0;
if ([Link]) {
targetSteerAngle = maxSteerAngle;
} else if ([Link]) {
targetSteerAngle = -maxSteerAngle;
}
[Link]((wheel) => {
const isFront = [Link](wheel);
[Link] -= deltaWheelRotation;
if (isFront) {
[Link].y = [Link](
[Link].y,
targetSteerAngle,
steerLerpFactor * deltaTime
);
} else {
[Link].y = 0;
}
[Link].x = [Link];
});
}
// Update UI
if ([Link] === "block") {
// Only update if visible
[Link]("speed").textContent = [Link](
[Link]
).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 = [Link]().add(cameraLookAtOffset);
[Link](targetPosition, 5 * deltaTime);
const desiredCameraPosition = [Link]
.clone()
.add([Link]().applyQuaternion([Link]));
const distanceToTarget = [Link](
[Link]
);
const desiredDistance = [Link]();
const distanceThreshold = 0.1;
if (
[Link](distanceToTarget - desiredDistance) <
distanceThreshold * desiredDistance
) {
[Link](desiredCameraPosition, 4 * deltaTime);
}
}
// --- ANIMATION LOOP ---
function animate() {
// Only run if game objects exist (init has been called)
if (!renderer || !scene || !camera || !car) {
[Link]("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 = [Link]();
updateCar(deltaTime);
updateCamera(deltaTime);
[Link](); // MUST be called after camera/target updates
[Link](scene, camera);
}
// --- UI Flow & Game Start ---
// 1. Show Splash Screen
[Link] = 1;
[Link] = "none";
[Link] = "none";
// 2. After a delay, hide splash, show car select
setTimeout(() => {
[Link] = 0;
// Use another timeout to ensure display:none happens *after* fade
setTimeout(() => {
[Link] = "none";
[Link] = "flex"; // Show car select screen
}, 500); // Match CSS transition duration
}, 2000); // Splash screen duration (2 seconds)
// 3. Handle Car Selection Clicks
[Link]((button) => {
[Link]("click", () => {
selectedCarType = [Link]("data-car-type");
[Link]("Selected car:", selectedCarType);
// Hide selection screen
[Link] = "none";
// --- Initialize and start the game ---
try {
init(selectedCarType); // Pass selection to init
} catch (error) {
[Link]("Error initializing game:", error);
// Optional: Show an error message to the user
[Link] =
'<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>