Current directory: /home/klas4s23/domains/585455.klas4s23.mid-ica.nl/public_html/Gastenboek/uploads
// ========================================
// =============== CAR CLASS ==============
// ========================================
import * as THREE from 'three';
import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
import { CONFIG } from '../config.js';
export class Car {
constructor(scene, audioManager) {
this.scene = scene;
this.config = CONFIG.car;
this.audioManager = audioManager;
// Physics state
this.velocity = new THREE.Vector3();
this.acceleration = new THREE.Vector3();
this.position = new THREE.Vector3(0, 0, 0);
this.rotation = 0;
// Input state
this.keys = {
forward: false,
backward: false,
left: false,
right: false,
brake: false
};
// Control state
this.controlsEnabled = true;
// Visual elements
this.group = null;
this.wheels = [];
this.headlights = [];
this.headlightsOn = false;
this.taillightMaterial = null;
this.carModel = null; // Store the loaded model
this.modelLoaded = false;
// Stats
this.distanceTraveled = 0;
this.maxSpeedReached = 0;
this.create();
this.setupEventListeners();
}
async create() {
this.group = new THREE.Group();
// Load GLTF car model
await this.loadCarModel();
// Add fallback geometry if model fails to load
if (!this.modelLoaded) {
this.createFallbackCar();
}
// Set initial position (lifted above ground)
this.position.y = 1; // Lift car above ground
this.group.position.copy(this.position);
this.scene.add(this.group);
}
async loadCarModel() {
const loader = new GLTFLoader();
// Load car model from assets folder
const modelPath = './assets/models/car.glb';
try {
console.log('🚗 Loading car model...');
const gltf = await loader.loadAsync(modelPath);
this.carModel = gltf.scene;
// Scale the model (adjust if too big/small)
this.carModel.scale.set(1, 1, 1);
this.carModel.position.set(0, 0, 0); // Adjust height
// ROTATE 90 DEGREES to fix orientation (front faces forward)
this.carModel.rotation.y = -Math.PI / 2; // -90 degrees
// Enable shadows for all meshes
this.carModel.traverse((child) => {
if (child.isMesh) {
child.castShadow = true;
child.receiveShadow = true;
}
});
this.group.add(this.carModel);
this.modelLoaded = true;
console.log('✅ Car model loaded successfully!');
// Add custom lights to the GLTF model
this.addLightsToModel();
} catch (error) {
console.error('❌ Failed to load car model:', error);
console.log('⚠️ Using fallback car geometry');
this.modelLoaded = false;
}
}
addLightsToModel() {
// Get model bounds to position lights correctly
const bbox = new THREE.Box3().setFromObject(this.carModel);
const size = new THREE.Vector3();
bbox.getSize(size);
// Position lights relative to model size (lowered Y to "place them down")
const frontZ = size.z / 2 + 0.05; // slightly forward
const backZ = -size.z / 2 - 0.05; // slightly back
const sideX = size.x / 3;
const heightY = -Math.abs(size.y) / 7; // lowered below model center
// ===== HEADLIGHTS (Front) - Visual meshes only =====
const headlightGeometry = new THREE.BoxGeometry(0.3, 0.2, 0.1);
// Left headlight (starts off)
const headlightMaterialLeft = new THREE.MeshStandardMaterial({
color: 0x333333,
emissive: 0x000000,
emissiveIntensity: 0,
metalness: 0,
roughness: 0,
transparent: true,
opacity: 0
});
const headlightLeft = new THREE.Mesh(headlightGeometry, headlightMaterialLeft);
headlightLeft.position.set(-sideX, heightY, frontZ);
this.group.add(headlightLeft);
this.headlights.push(headlightLeft); // Store mesh for toggling
// Right headlight (starts off)
const headlightMaterialRight = new THREE.MeshStandardMaterial({
color: 0x333333,
emissive: 0x000000,
emissiveIntensity: 0,
metalness: 0,
roughness: 0,
transparent: true,
opacity: 0
});
const headlightRight = new THREE.Mesh(headlightGeometry, headlightMaterialRight);
headlightRight.position.set(sideX, heightY, frontZ);
this.group.add(headlightRight);
this.headlights.push(headlightRight); // Store mesh for toggling
// ===== TAIL LIGHTS (Back) - Always on, dim =====
const taillightGeometry = new THREE.BoxGeometry(0.25, 0.15, 0.1);
this.taillightMaterial = new THREE.MeshBasicMaterial({
color: 0xff0000,
transparent: true,
opacity: 0.4
});
this.taillightLeft = new THREE.Mesh(taillightGeometry, this.taillightMaterial);
this.taillightLeft.position.set(-sideX, heightY, backZ);
this.group.add(this.taillightLeft);
this.taillightRight = new THREE.Mesh(taillightGeometry, this.taillightMaterial.clone());
this.taillightRight.position.set(sideX, heightY, backZ);
this.group.add(this.taillightRight);
console.log('💡 Visual headlights and tail lights added to model');
}
setupEventListeners() {
window.addEventListener('keydown', (e) => this.onKeyDown(e));
window.addEventListener('keyup', (e) => this.onKeyUp(e));
}
onKeyDown(event) {
if (!this.controlsEnabled) return;
const key = event.key.toLowerCase();
switch(key) {
case 'w':
case 'arrowup':
this.keys.forward = true;
break;
case 's':
case 'arrowdown':
this.keys.backward = true;
break;
case 'a':
case 'arrowleft':
this.keys.left = true;
break;
case 'd':
case 'arrowright':
this.keys.right = true;
break;
case ' ':
this.keys.brake = true;
event.preventDefault();
break;
case 'l': // Toggle headlights
this.toggleHeadlights();
break;
case 'h': // Horn
if (this.audioManager) {
this.audioManager.playSound('horn');
}
break;
}
}
onKeyUp(event) {
if (!this.controlsEnabled) return;
const key = event.key.toLowerCase();
switch(key) {
case 'w':
case 'arrowup':
this.keys.forward = false;
break;
case 's':
case 'arrowdown':
this.keys.backward = false;
break;
case 'a':
case 'arrowleft':
this.keys.left = false;
break;
case 'd':
case 'arrowright':
this.keys.right = false;
break;
case ' ':
this.keys.brake = false;
break;
}
}
enableControls() {
this.controlsEnabled = true;
}
disableControls() {
this.controlsEnabled = false;
// Reset all keys
this.keys.forward = false;
this.keys.backward = false;
this.keys.left = false;
this.keys.right = false;
this.keys.brake = false;
}
toggleHeadlights() {
// Toggle headlights (works for both fallback and GLTF model)
if (this.headlights.length > 0) {
this.headlightsOn = !this.headlightsOn;
this.headlights.forEach(headlight => {
if (this.modelLoaded) {
// For GLTF model: Toggle emissive glow on meshes
if (headlight.material) {
if (this.headlightsOn) {
headlight.material.color.setHex(0xffeb3b);
headlight.material.emissive.setHex(0xffeb3b);
headlight.material.emissiveIntensity = 1.0;
} else {
headlight.material.color.setHex(0x333333);
headlight.material.emissive.setHex(0x000000);
headlight.material.emissiveIntensity = 0;
}
}
} else {
// For fallback car: Toggle spotlight intensity
const intensity = this.headlightsOn ? this.config.headlightIntensity : 0;
headlight.intensity = intensity;
}
});
}
}
updatePhysics() {
// Get car's forward and right vectors
// Forward is now POSITIVE Z (since camera is behind looking forward)
const forward = new THREE.Vector3(0, 0, 1).applyAxisAngle(
new THREE.Vector3(0, 1, 0),
this.rotation
);
// Reset acceleration each frame
this.acceleration.set(0, 0, 0);
// Forward acceleration with smooth ramp-up
if (this.keys.forward) {
const accelMultiplier = 1.0 + (this.velocity.length() / this.config.maxSpeed) * 0.5;
this.acceleration.add(forward.clone().multiplyScalar(this.config.acceleration * accelMultiplier));
}
// Reverse acceleration
if (this.keys.backward) {
const reverseForce = forward.clone().multiplyScalar(-this.config.acceleration * 0.7);
this.acceleration.add(reverseForce);
}
// Apply acceleration to velocity
this.velocity.add(this.acceleration);
// Improved friction and deceleration
const speed = this.velocity.length();
if (!this.keys.forward && !this.keys.backward) {
// Natural friction - stronger at higher speeds
const speedFactor = Math.min(speed / this.config.maxSpeed, 1.0);
const dynamicFriction = this.config.friction - (speedFactor * 0.08);
this.velocity.multiplyScalar(Math.max(dynamicFriction, 0.82));
// Stop completely at very low speeds to prevent sliding
if (speed < 0.001) {
this.velocity.set(0, 0, 0);
}
} else {
// Slight drag when accelerating
this.velocity.multiplyScalar(0.985);
}
if (this.audioManager) {
const speed = this.getSpeed();
this.audioManager.updateEngineSound(speed);
}
// Enhanced braking - more responsive with visual feedback
if (this.keys.brake) {
const brakeStrength = this.config.brakeFriction;
this.velocity.multiplyScalar(brakeStrength);
// Play brake sound
if (this.audioManager && speed > 5) {
this.audioManager.playSound('brake', 0.3);
}
// Update brake lights
this.updateBrakeLights(true);
// Immediate stop at very low speed while braking
if (speed < 0.005) {
this.velocity.set(0, 0, 0);
}
} else {
this.updateBrakeLights(false);
}
// Speed limits with smooth clamping
const maxSpeed = this.keys.backward ?
this.config.reverseSpeed : this.config.maxSpeed;
if (speed > maxSpeed) {
this.velocity.normalize().multiplyScalar(maxSpeed);
}
// Improved turning physics - variable turning radius based on speed
if (speed > this.config.minTurnSpeed) {
const speedRatio = speed / this.config.maxSpeed;
const turnAmount = this.config.turnSpeed *
(this.config.turnSpeedMultiplier + speedRatio *
(1 - this.config.turnSpeedMultiplier));
if (this.keys.left) {
this.rotation += turnAmount;
}
if (this.keys.right) {
this.rotation -= turnAmount;
}
}
// Update position
this.position.add(this.velocity);
// Track distance
this.distanceTraveled += this.velocity.length();
// Track max speed
const speedKmh = speed * 200;
if (speedKmh > this.maxSpeedReached) {
this.maxSpeedReached = speedKmh;
}
// Keep car within world bounds with bounce
this.handleWorldBoundaries();
// Animate wheels based on speed and steering
this.updateWheelAnimation(speed);
}
updateBrakeLights(braking) {
// Update brake lights for both fallback and GLTF model
if (this.taillightMaterial) {
const targetOpacity = braking ? 1.0 : 0.4;
this.taillightMaterial.opacity = THREE.MathUtils.lerp(
this.taillightMaterial.opacity,
targetOpacity,
0.2
);
// Also update the right taillight if it exists (for GLTF model)
if (this.taillightRight && this.taillightRight.material) {
this.taillightRight.material.opacity = THREE.MathUtils.lerp(
this.taillightRight.material.opacity,
targetOpacity,
0.2
);
}
}
}
updateWheelAnimation(speed) {
// Only animate if using fallback car (GLTF model has its own wheels)
if (this.wheels.length > 0 && speed > 0.01 && !this.modelLoaded) {
const wheelRotation = speed * 2.5;
this.wheels.forEach(wheel => {
wheel.rotation.x += wheelRotation;
});
}
}
handleWorldBoundaries() {
const halfSize = CONFIG.world.size / 2 - 5;
if (Math.abs(this.position.x) > halfSize) {
this.position.x = Math.sign(this.position.x) * halfSize;
this.velocity.x *= -this.config.boundaryBounce;
}
if (Math.abs(this.position.z) > halfSize) {
this.position.z = Math.sign(this.position.z) * halfSize;
this.velocity.z *= -this.config.boundaryBounce;
}
}
update() {
this.updatePhysics();
// Update 3D model position and rotation
this.group.position.copy(this.position);
this.group.rotation.y = this.rotation;
}
getSpeed() {
return this.velocity.length() * 200; // Convert to km/h
}
getPosition() {
return this.position.clone();
}
getRotation() {
return this.rotation;
}
getForwardVector() {
return new THREE.Vector3(0, 0, -1).applyAxisAngle(
new THREE.Vector3(0, 1, 0),
this.rotation
);
}
getGroup() {
return this.group;
}
getStats() {
return {
distanceTraveled: this.distanceTraveled,
maxSpeedReached: this.maxSpeedReached
};
}
reset() {
this.position.set(0, 0, 0);
this.velocity.set(0, 0, 0);
this.acceleration.set(0, 0, 0);
this.rotation = 0;
this.distanceTraveled = 0;
this.update();
}
dispose() {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
this.scene.remove(this.group);
}
}
export default Car;