HighlanderDrivingGame / index.html
awacke1's picture
Update index.html
ee7f7b2 verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Isle of Mull Ferry Driving Simulator</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
}
#info {
position: absolute;
top: 10px;
width: 100%;
text-align: center;
color: white;
background-color: rgba(0,0,0,0.5);
padding: 10px;
z-index: 100;
}
#timer {
position: absolute;
top: 10px;
right: 10px;
color: white;
background-color: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 5px;
z-index: 100;
}
#message {
position: absolute;
bottom: 20px;
width: 100%;
text-align: center;
color: white;
background-color: rgba(0,0,0,0.7);
padding: 10px;
font-size: 18px;
z-index: 100;
transition: opacity 0.5s;
opacity: 0;
}
#speedometer {
position: absolute;
bottom: 10px;
left: 10px;
color: white;
background-color: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 5px;
z-index: 100;
}
#score {
position: absolute;
top: 10px;
left: 10px;
color: white;
background-color: rgba(0,0,0,0.5);
padding: 5px 10px;
border-radius: 5px;
z-index: 100;
}
#gameOver {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0,0,0,0.8);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
z-index: 200;
display: none;
}
button {
background-color: #4CAF50;
border: none;
color: white;
padding: 10px 20px;
text-align: center;
text-decoration: none;
display: inline-block;
font-size: 16px;
margin: 10px 2px;
cursor: pointer;
border-radius: 5px;
}
#instructions {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0,0,0,0.8);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
z-index: 300;
max-width: 600px;
}
.highlight {
color: #ffcc00;
font-weight: bold;
}
/* Hide specific error messages only */
.error-message {
display: none !important;
}
/* Make sure Three.js canvas is visible */
canvas {
display: block !important;
}
</style>
</head>
<body>
<div id="info">Isle of Mull Ferry Driving Simulator</div>
<div id="timer">Time: 0:00</div>
<div id="speedometer">Speed: 0 mph</div>
<div id="score">Score: 0</div>
<div id="message"></div>
<div id="rearView" style="position: absolute; top: 10px; left: 50%; transform: translateX(-50%); width: 200px; height: 40px; background-color: rgba(0,0,0,0.5); border: 2px solid #333; border-radius: 5px; z-index: 100;"></div>
<div id="healthBar" style="position: absolute; bottom: 50px; left: 10px; width: 200px; height: 20px; background-color: rgba(0,0,0,0.5); border: 1px solid #fff; z-index: 100;">
<div id="health" style="width: 100%; height: 100%; background-color: #00ff00;"></div>
</div>
<div id="airTime" style="position: absolute; bottom: 80px; left: 10px; color: white; background-color: rgba(0,0,0,0.5); padding: 5px 10px; border-radius: 5px; z-index: 100;">Air Time: 0.0s</div>
<div id="gameOver">
<h2 id="gameOverTitle">Game Over</h2>
<p id="gameOverText"></p>
<button id="restartButton">Try Again</button>
</div>
<div id="instructions">
<h2>Welcome to the Isle of Mull Ferry Driving Simulator!</h2>
<p>You're driving to catch the ferry to the Isle of Mull in Scotland. Practice safe driving on American-style roads.</p>
<h3>How to Play:</h3>
<ul style="text-align: left;">
<li>Drive on the <span class="highlight">right side</span> of the road</li>
<li>Use <span class="highlight">W/S</span> or <span class="highlight">↑/↓</span> to accelerate/brake</li>
<li>Use <span class="highlight">A/D</span> or <span class="highlight">←/→</span> to steer</li>
<li>Press <span class="highlight">SPACE</span> or <span class="highlight">J</span> to jump (when driving fast)</li>
<li>Watch for oncoming traffic and stop at <span class="highlight">passing places</span> to let them through</li>
<li>Be courteous to cars behind you by using passing places</li>
<li>Cross <span class="highlight">one-lane bridges</span> carefully - yield to oncoming traffic</li>
<li>Watch your <span class="highlight">rear view mirror</span> for cars flashing to overtake</li>
<li>Look for <span class="highlight">jump ramps</span> to perform jumps and earn bonus points!</li>
<li>Avoid potholes and collisions - watch your damage meter!</li>
<li>Earn <span class="highlight">points</span> for courteous driving and jump tricks</li>
<li>Reach the ferry in under 2 minutes!</li>
</ul>
<p><span class="highlight">Passing Place Etiquette:</span> If a car is coming toward you, stop at a passing place on YOUR side of the road. Don't cross to the other side unless necessary.</p>
<button id="startButton">Start Driving</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Game variables
let scene, camera, renderer, clock;
let playerCar, road, terrain;
let aiCars = [];
let passingPlaces = [];
let bridges = [];
let clouds = [];
let oceanPlane;
let jumpRamps = []; // Array to store jump ramps
let ferryObject; // For the ferry mesh
let gameStarted = false;
let gameOver = false;
let gameTime = 0;
let speed = 0;
const roadWidth = 5;
let maxSpeed = 45;
let acceleration = 0.2;
let deceleration = 0.3;
let braking = 0.5;
let steering = 0.02;
let roadLength = 2000;
let ferryPosition = roadLength - 100;
let potholes = [];
let score = 0;
let playerHealth = 100;
let healthRegenRate = 2; // Health points per second when not taking damage
let lastDamageTime = 0;
let goodStopsInARow = 0;
let carsBehind = [];
// Physics variables
let gravity = 0.25;
let velocity = new THREE.Vector3(0, 0, 0);
let isGrounded = true;
let airTime = 0;
let jumpForce = 0;
let jumpSpeed = 0; // Track speed when jumping
let lastRoadY = 0;
let suspensionCompression = 0;
let suspensionStrength = 0.3;
let suspensionDamping = 0.8;
const keys = {
ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false,
w: false, a: false, s: false, d: false,
' ': false, j: false
};
function init() {
const bodyChildNodes = document.body.childNodes;
for (let i = bodyChildNodes.length - 1; i >= 0; i--) {
const node = bodyChildNodes[i];
if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '' &&
(node.textContent.includes('function') || node.textContent.includes('var') || node.textContent.includes('let') || node.textContent.includes('const'))) {
node.textContent = '';
}
}
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000); // Increased far plane
camera.position.set(0, 5, -10);
camera.lookAt(0, 0, 10);
renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.domElement.id = 'game-canvas';
document.body.appendChild(renderer.domElement);
const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);
directionalLight.position.set(100, 150, 75);
directionalLight.castShadow = true;
directionalLight.shadow.mapSize.width = 2048;
directionalLight.shadow.mapSize.height = 2048;
directionalLight.shadow.camera.near = 0.5;
directionalLight.shadow.camera.far = 500;
directionalLight.shadow.camera.left = -100;
directionalLight.shadow.camera.right = 100;
directionalLight.shadow.camera.top = 100;
directionalLight.shadow.camera.bottom = -100;
scene.add(directionalLight);
createTerrain();
createRoad();
createPlayerCar();
createAICars();
createPassingPlaces();
createBridges();
createPotholes();
createOcean();
createClouds();
createJumpRamps(); // Add jump ramps
createFerry(); // Add the ferry
clock = new THREE.Clock();
window.addEventListener('resize', onWindowResize);
window.addEventListener('keydown', onKeyDown);
window.addEventListener('keyup', onKeyUp);
document.getElementById('startButton').addEventListener('click', startGame);
document.getElementById('restartButton').addEventListener('click', restartGame);
}
function getRoadPropertiesAtZ(worldZPos) {
const localZ = worldZPos - (roadLength / 2);
const roadCurve = Math.sin(localZ * 0.005) * 15;
// Gradually reduce elevation variation as we approach the ferry (coastal approach)
const distanceToFerry = Math.max(0, ferryPosition - worldZPos);
const coastalFactor = Math.min(1, distanceToFerry / 400); // Start flattening 400 units before ferry
const elevationAmplitude = coastalFactor * 0.5;
// Lift the entire road by 2 units to ensure it's always above water
const baseRoadElevation = 2;
const roadY = baseRoadElevation + (Math.sin(localZ * 0.01) * 5 + Math.sin(localZ * 0.03) * 2) * elevationAmplitude;
return { roadY, roadCurve };
}
function createTerrain() {
const terrainWidth = 1000;
const terrainSegmentsX = 150;
const terrainSegmentsZ = 300;
const groundGeometry = new THREE.PlaneGeometry(terrainWidth, roadLength, terrainSegmentsX, terrainSegmentsZ);
groundGeometry.rotateX(-Math.PI / 2);
const vertices = groundGeometry.attributes.position;
for (let i = 0; i < vertices.count; i++) {
const x_local = vertices.getX(i);
const z_local = vertices.getZ(i);
const worldZ = z_local + roadLength / 2;
const { roadY: actualRoadY, roadCurve: actualRoadCurve } = getRoadPropertiesAtZ(worldZ);
let height = actualRoadY;
const distFromRoadCenter = Math.abs(x_local - actualRoadCurve);
const roadEdgeBuffer = 5;
// Calculate coastal approach factor - mountains recede as we near the ferry
const distanceToFerry = Math.max(0, ferryPosition - worldZ);
const coastalTransition = Math.min(1, distanceToFerry / 600); // Start transition 600 units before ferry
const mountainHeightFactor = coastalTransition;
// Also create asymmetric coastal effect - mountains more on one side
const ferryApproachFactor = 1 - Math.max(0, Math.min(1, (ferryPosition - worldZ) / 800));
const coastalAsymmetry = ferryApproachFactor * Math.max(0, 1 - Math.abs(x_local + actualRoadCurve) / 200);
if (distFromRoadCenter > (roadWidth / 2 + roadEdgeBuffer)) {
const mountainBaseHeight = 10 + Math.abs(Math.sin(z_local * 0.001 + x_local * 0.005)) * 20;
const mountainDetail = Math.sin(z_local * 0.02 + x_local * 0.03) * 15 + Math.cos(z_local * 0.015) * 10;
let mountainOffset = mountainBaseHeight + mountainDetail;
const riseFactor = Math.min((distFromRoadCenter - (roadWidth / 2 + roadEdgeBuffer)) * 0.2, 1.0) + 0.5;
mountainOffset *= riseFactor;
const glenFactor = 0.6 + (Math.sin(z_local * 0.004) * 0.5 + 0.5) * 0.4;
mountainOffset *= glenFactor;
// Apply coastal factors to reduce mountain height near ferry
mountainOffset *= mountainHeightFactor;
mountainOffset *= (1 - coastalAsymmetry * 0.7); // Reduce mountains more on ocean side
height += Math.max(0, mountainOffset);
}
else if (distFromRoadCenter > roadWidth / 2) {
// Create natural water channels/glens beside the road
const channelDepth = (distFromRoadCenter - roadWidth/2) * 0.8;
const channelVariation = Math.sin(z_local * 0.02) * 0.5 + Math.cos(z_local * 0.015) * 0.3;
height -= channelDepth + channelVariation;
// Ensure channels don't go too deep and create nice glen-like depressions
height = Math.max(height, -1.5); // Keep above water level but create natural channels
}
// Ensure terrain near ferry is at reasonable coastal elevation
if (worldZ > ferryPosition - 200) {
const coastalBlend = (worldZ - (ferryPosition - 200)) / 200;
const targetCoastalHeight = 0.5; // Slightly above sea level for coastal approach
height = height * (1 - coastalBlend) + targetCoastalHeight * coastalBlend;
}
vertices.setY(i, height);
}
groundGeometry.attributes.position.needsUpdate = true;
groundGeometry.computeVertexNormals();
const groundMaterial = new THREE.MeshStandardMaterial({
color: 0x365E36, flatShading: true, roughness: 0.9, metalness: 0.1
});
terrain = new THREE.Mesh(groundGeometry, groundMaterial);
terrain.position.z = roadLength / 2;
terrain.receiveShadow = true;
scene.add(terrain);
}
function createRoad() {
const roadGeometry = new THREE.PlaneGeometry(roadWidth, roadLength, 1, 200);
roadGeometry.rotateX(-Math.PI / 2);
const vertices = roadGeometry.attributes.position;
for (let i = 0; i < vertices.count; i++) {
const localZ = vertices.getZ(i);
const worldZ = localZ + roadLength / 2;
const curve_val = Math.sin(localZ * 0.005) * 15;
vertices.setX(i, roadGeometry.attributes.position.getX(i) + curve_val);
// Use the updated getRoadPropertiesAtZ function for consistent elevation
const { roadY: elevation_val } = getRoadPropertiesAtZ(worldZ);
vertices.setY(i, elevation_val + 0.1); // Lift road surface slightly above base terrain
}
roadGeometry.attributes.position.needsUpdate = true;
roadGeometry.computeVertexNormals();
const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 });
road = new THREE.Mesh(roadGeometry, roadMaterial);
road.position.z = roadLength / 2;
road.receiveShadow = true;
scene.add(road);
// Create thicker road base to prevent water clipping
const roadBaseGeometry = new THREE.BoxGeometry(roadWidth, 0.5, roadLength);
const roadBaseMaterial = new THREE.MeshStandardMaterial({ color: 0x444444, roughness: 0.9 });
const roadBase = new THREE.Mesh(roadBaseGeometry, roadBaseMaterial);
// Position road base properly
const roadBaseVertices = roadBaseGeometry.attributes.position;
for (let i = 0; i < roadBaseVertices.count; i++) {
const localZ = roadBaseVertices.getZ(i);
const worldZ = localZ + roadLength / 2;
const { roadY } = getRoadPropertiesAtZ(worldZ);
roadBaseVertices.setY(i, roadBaseVertices.getY(i) + roadY - 0.2); // Position base below road surface
}
roadBaseGeometry.attributes.position.needsUpdate = true;
roadBase.position.z = roadLength / 2;
roadBase.receiveShadow = true;
scene.add(roadBase);
const centerLineGeometry = new THREE.PlaneGeometry(0.1, roadLength, 1, 200);
centerLineGeometry.rotateX(-Math.PI / 2);
const lineVertices = centerLineGeometry.attributes.position;
for (let i = 0; i < lineVertices.count; i++) {
const localZ = lineVertices.getZ(i);
const worldZ = localZ + roadLength / 2;
const curve_val = Math.sin(localZ * 0.005) * 15;
lineVertices.setX(i, lineVertices.getX(i) + curve_val);
const { roadY: elevation_val } = getRoadPropertiesAtZ(worldZ);
lineVertices.setY(i, elevation_val + 0.12);
}
centerLineGeometry.attributes.position.needsUpdate = true;
centerLineGeometry.computeVertexNormals();
const centerLineMaterial = new THREE.MeshStandardMaterial({ color: 0xFFFFFF });
const centerLine = new THREE.Mesh(centerLineGeometry, centerLineMaterial);
centerLine.position.z = roadLength / 2;
scene.add(centerLine);
}
function createOcean() {
const oceanSize = 4000;
const oceanGeometry = new THREE.PlaneGeometry(oceanSize, oceanSize);
const oceanMaterial = new THREE.MeshStandardMaterial({
color: 0x0077be, transparent: true, opacity: 0.85, roughness: 0.3, metalness: 0.1,
});
oceanPlane = new THREE.Mesh(oceanGeometry, oceanMaterial);
oceanPlane.rotation.x = -Math.PI / 2;
// Position ocean at a consistent level that's always below the road
// The minimum road elevation with the new coastal approach will be around -1.5
const oceanLevel = -3; // Well below minimum road elevation
const ferryRoadProps = getRoadPropertiesAtZ(ferryPosition);
oceanPlane.position.set(ferryRoadProps.roadCurve, oceanLevel, ferryPosition + 50);
oceanPlane.receiveShadow = true;
scene.add(oceanPlane);
}
function createClouds() {
const cloudMaterial = new THREE.MeshBasicMaterial({
color: 0xffffff, transparent: true, opacity: 0.6, depthWrite: false
});
const numClouds = 15;
const skyHeight = 150;
const skyDepthRange = 1000;
const skyWidthRange = 2000;
for (let i = 0; i < numClouds; i++) {
const cloudWidth = 100 + Math.random() * 200;
const cloudHeight = 50 + Math.random() * 100;
const cloudGeometry = new THREE.PlaneGeometry(cloudWidth, cloudHeight);
const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
cloud.position.set(
(Math.random() - 0.5) * skyWidthRange,
skyHeight + (Math.random() - 0.5) * 50,
(Math.random() * skyDepthRange) - skyDepthRange / 4
);
cloud.rotation.y = (Math.random() - 0.5) * 0.5;
// cloud.lookAt(camera.position); // Initial orientation
cloud.userData.speed = 0.5 + Math.random() * 1;
clouds.push(cloud);
scene.add(cloud);
}
}
function updateClouds(delta) {
const wrapAroundX = 2200;
clouds.forEach(cloud => {
cloud.position.x += cloud.userData.speed * delta * 5;
if (cloud.position.x > wrapAroundX / 2) {
cloud.position.x = -wrapAroundX / 2;
cloud.position.z = (Math.random() * 1000) - 500 + playerCar.position.z; // Re-position relative to player Z
}
// Make clouds face the general direction of the camera's Z but not directly lookAt
const targetZ = camera.position.z + 500; // A point far in front of camera
const direction = new THREE.Vector3(cloud.position.x, cloud.position.y, targetZ);
cloud.lookAt(direction);
});
}
function createPlayerCar() {
const bodyGroup = new THREE.Group();
scene.add(bodyGroup);
playerCar = bodyGroup;
const bodyGeometry = new THREE.BoxGeometry(2, 1, 4);
bodyGeometry.translate(0, 0.5, 0);
const carMaterial = new THREE.MeshStandardMaterial({ color: 0x3366FF, roughness: 0.5, metalness: 0.7 });
const carBody = new THREE.Mesh(bodyGeometry, carMaterial);
carBody.castShadow = true;
bodyGroup.add(carBody);
const windshieldGeometry = new THREE.CylinderGeometry(1, 1, 1.8, 16, 1, false, 0, Math.PI);
windshieldGeometry.rotateZ(Math.PI / 2); windshieldGeometry.rotateY(Math.PI / 2);
windshieldGeometry.scale(1, 0.4, 0.8); windshieldGeometry.translate(0, 1.1, 0.5);
const windshieldMaterial = new THREE.MeshStandardMaterial({ color: 0xAACCFF, transparent: true, opacity: 0.7, roughness: 0.1, metalness: 0.2 });
const windshield = new THREE.Mesh(windshieldGeometry, windshieldMaterial);
bodyGroup.add(windshield);
const frontBumperGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2, 16, 1, false, -Math.PI/2, Math.PI);
frontBumperGeometry.rotateZ(Math.PI / 2); frontBumperGeometry.scale(1, 0.5, 0.5); frontBumperGeometry.translate(0, 0.5, 2);
const bumperMaterial = new THREE.MeshStandardMaterial({ color: 0x2255DD, roughness: 0.7, metalness: 0.3 });
const frontBumper = new THREE.Mesh(frontBumperGeometry, bumperMaterial);
frontBumper.castShadow = true; bodyGroup.add(frontBumper);
const rearBumperGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2, 16, 1, false, Math.PI/2, Math.PI);
rearBumperGeometry.rotateZ(Math.PI / 2); rearBumperGeometry.scale(1, 0.5, 0.5); rearBumperGeometry.translate(0, 0.5, -2);
const rearBumper = new THREE.Mesh(rearBumperGeometry, bumperMaterial);
rearBumper.castShadow = true; bodyGroup.add(rearBumper);
const wheelGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.3, 24);
wheelGeometry.rotateZ(Math.PI / 2);
const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9, metalness: 0.2 });
const hubCapGeometry = new THREE.CircleGeometry(0.3, 16);
const hubCapMaterial = new THREE.MeshStandardMaterial({ color: 0xCCCCCC, roughness: 0.5, metalness: 0.8 });
const wheelsInfo = [
{ x: -1, z: 1.5, hubCapX: 0.16, hubCapRotY: Math.PI / 2 }, { x: 1, z: 1.5, hubCapX: -0.16, hubCapRotY: -Math.PI / 2 },
{ x: -1, z: -1.5, hubCapX: 0.16, hubCapRotY: Math.PI / 2 }, { x: 1, z: -1.5, hubCapX: -0.16, hubCapRotY: -Math.PI / 2 }
];
wheelsInfo.forEach(info => {
const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
wheel.position.set(info.x, 0.5, info.z); wheel.isWheel = true;
const hubCap = new THREE.Mesh(hubCapGeometry, hubCapMaterial);
hubCap.position.set(info.hubCapX, 0, 0); hubCap.rotation.y = info.hubCapRotY;
wheel.add(hubCap); bodyGroup.add(wheel);
});
const springMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
function createSpring(x, z_offset) {
const springGroup = new THREE.Group(); const coilCount = 5; const coilHeight = 0.08;
for (let i = 0; i < coilCount; i++) {
const coil = new THREE.Mesh(new THREE.TorusGeometry(0.15, 0.03, 8, 16), springMaterial);
coil.position.y = i * coilHeight; springGroup.add(coil);
}
springGroup.position.set(x, 0.2, z_offset); return springGroup;
}
bodyGroup.add(createSpring(-0.8, 1.5)); bodyGroup.add(createSpring(0.8, 1.5));
bodyGroup.add(createSpring(-0.8, -1.5)); bodyGroup.add(createSpring(0.8, -1.5));
const mirrorBase = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.1, 0.1), carMaterial);
mirrorBase.position.set(0, 1.3, -0.5); bodyGroup.add(mirrorBase);
const mirrorGeo = new THREE.CylinderGeometry(0.1, 0.1, 1.8, 16, 1, true, 0, Math.PI);
mirrorGeo.rotateX(Math.PI / 2); mirrorGeo.translate(0, 0.1, 0);
const mirror = new THREE.Mesh(mirrorGeo, new THREE.MeshStandardMaterial({ color: 0x444444, roughness: 0.3, metalness: 0.8 }));
mirror.position.set(0, 1.3, -0.5); bodyGroup.add(mirror);
const particlesCount = 50; const particlesGeometry = new THREE.BufferGeometry();
const posArray = new Float32Array(particlesCount * 3); const sizeArray = new Float32Array(particlesCount);
for (let i = 0; i < particlesCount; i++) {
posArray[i*3]=0; posArray[i*3+1]=0; posArray[i*3+2]=0; sizeArray[i]=Math.random()*0.2;
}
particlesGeometry.setAttribute('position', new THREE.BufferAttribute(posArray,3));
particlesGeometry.setAttribute('size', new THREE.BufferAttribute(sizeArray,1));
const particlesMaterial = new THREE.PointsMaterial({color:0xCCCCCC,size:0.1,sizeAttenuation:true,transparent:true,opacity:0.5});
const particles = new THREE.Points(particlesGeometry, particlesMaterial);
particles.visible = false; bodyGroup.add(particles); bodyGroup.particles = particles;
const initialRoadProps = getRoadPropertiesAtZ(0);
bodyGroup.position.set(initialRoadProps.roadCurve, 1 + initialRoadProps.roadY, 0);
}
function createAICars() {
for (let i = 0; i < 5; i++) {
createAICar(300 + i * 350, true);
}
for (let i = 0; i < 2; i++) {
createAICar(-100 - i * 150, false);
}
}
function createAICar(worldZPosition, isOncoming) {
const carColors = [0xCC0000,0x00CC00,0xCCCC00,0xCCCCCC,0x9900CC];
const car = new THREE.Mesh(new THREE.BoxGeometry(1.8,1,3.8), new THREE.MeshStandardMaterial({color:carColors[Math.floor(Math.random()*carColors.length)]}));
car.castShadow = true;
const {roadY,roadCurve} = getRoadPropertiesAtZ(worldZPosition);
// SWITCHED: For right-side driving, oncoming cars should be on player's left side
const laneXOffset = isOncoming ? (-roadWidth/4 - 0.25) : (roadWidth/4 + 0.25);
car.position.set(roadCurve + laneXOffset, 1+roadY, worldZPosition);
car.rotation.y = isOncoming ? Math.PI : 0;
scene.add(car);
const headlightMat = new THREE.MeshBasicMaterial({color:0xFFFFFF});
const leftHeadlight = new THREE.Mesh(new THREE.SphereGeometry(0.2,8,8), headlightMat);
leftHeadlight.position.set(-0.7,0.3,1.9); car.add(leftHeadlight);
const rightHeadlight = new THREE.Mesh(new THREE.SphereGeometry(0.2,8,8), headlightMat);
rightHeadlight.position.set(0.7,0.3,1.9); car.add(rightHeadlight);
aiCars.push({
mesh:car,
speed:isOncoming?8:10,
baseSpeed:isOncoming?8:10, // Store original speed
isOncoming:isOncoming,
honking:false,
waiting:false,
initialPositionZ:worldZPosition,
flashing:false,
waitingAtPassingPlace:false,
flashingLights:false,
leftHeadlight:leftHeadlight,
rightHeadlight:rightHeadlight,
politeness:Math.random()*0.8+0.2,
avoidingPlayer:false, // New property for collision avoidance
reactionDistance:30 + Math.random() * 20 // Variable reaction distance
});
}
function createPassingPlaces() {
for (let i = 0; i < 15; i++) createPassingPlace(80 + i * 130);
}
function createPassingPlace(worldZPosition) {
const side = (passingPlaces.length%2===0)?-1:1;
const {roadY,roadCurve} = getRoadPropertiesAtZ(worldZPosition);
const passingLength=20, passingWidth=6, taperLength=12;
const mainPassingGroup = new THREE.Group();
const groupXOffset = side * (roadWidth/2 + passingWidth/2);
mainPassingGroup.position.set(roadCurve+groupXOffset, roadY+0.01, worldZPosition);
scene.add(mainPassingGroup);
const passingMaterial = new THREE.MeshStandardMaterial({color:0x555555});
const mainGeom = createRoundedRectGeometry(passingWidth,passingLength,1.5);
mainGeom.rotateX(-Math.PI/2);
const mainMesh = new THREE.Mesh(mainGeom, passingMaterial);
mainPassingGroup.add(mainMesh);
const entranceTaperGeom = createSmoothTaperGeometry(taperLength,passingWidth,true,10);
entranceTaperGeom.rotateX(-Math.PI/2);
const entranceTaper = new THREE.Mesh(entranceTaperGeom, passingMaterial);
entranceTaper.position.set(0,0,-passingLength/2-taperLength/2);
if(side<0) entranceTaper.rotation.z=Math.PI;
mainPassingGroup.add(entranceTaper);
const exitTaperGeom = createSmoothTaperGeometry(taperLength,passingWidth,false,10);
exitTaperGeom.rotateX(-Math.PI/2);
const exitTaper = new THREE.Mesh(exitTaperGeom, passingMaterial);
exitTaper.position.set(0,0,passingLength/2+taperLength/2);
if(side<0) exitTaper.rotation.z=Math.PI;
mainPassingGroup.add(exitTaper);
const sign = new THREE.Mesh(new THREE.BoxGeometry(0.5,2,0.1), new THREE.MeshStandardMaterial({color:0xFFFFFF}));
const signXOffset = side * (roadWidth/2 + passingWidth + 1);
sign.position.set(roadCurve+signXOffset, 1+roadY, worldZPosition);
scene.add(sign);
const signGraphic = new THREE.Mesh(new THREE.CircleGeometry(0.3,16), new THREE.MeshBasicMaterial({color:0x000000}));
signGraphic.position.set(0,0.5,0.06);
sign.add(signGraphic);
passingPlaces.push({position:worldZPosition,side:side,width:passingWidth,length:passingLength+taperLength*2,mesh:mainMesh,group:mainPassingGroup,entranceTaper:entranceTaper,exitTaper:exitTaper,worldXCenter:roadCurve+groupXOffset});
}
function createRoundedRectGeometry(width,length,radius){
const s=new THREE.Shape(); const x=-width/2,y=-length/2;
s.moveTo(x,y+radius); s.lineTo(x,y+length-radius); s.quadraticCurveTo(x,y+length,x+radius,y+length);
s.lineTo(x+width-radius,y+length); s.quadraticCurveTo(x+width,y+length,x+width,y+length-radius);
s.lineTo(x+width,y+radius); s.quadraticCurveTo(x+width,y,x+width-radius,y);
s.lineTo(x+radius,y); s.quadraticCurveTo(x,y,x,y+radius);
return new THREE.ShapeGeometry(s,16);
}
function createSmoothTaperGeometry(length,baseWidth,isEntrance,segments){
const g=new THREE.PlaneGeometry(baseWidth,length,1,segments); const p=g.attributes.position;
for(let i=0;i<=segments;i++){
const t=i/segments; let wf=isEntrance?easeInOut(t):1-easeInOut(t);
const cw=baseWidth*wf; const lidx=i*2,ridx=i*2+1;
p.setX(lidx,-cw/2); p.setX(ridx,cw/2);
}
p.needsUpdate=true; g.computeVertexNormals(); return g;
}
function createTaperedGeometry(length,startWidth,endWidth,segments){
const piw=Math.max(startWidth,endWidth); const g=new THREE.PlaneGeometry(piw,length,1,segments);
const p=g.attributes.position;
for(let i=0;i<=segments;i++){
const t=i/segments; const et=easeInOut(t); const cw=startWidth+(endWidth-startWidth)*et;
const lidx=i*2,ridx=i*2+1;
p.setX(lidx,-cw/2); p.setX(ridx,cw/2);
}
p.needsUpdate=true; g.computeVertexNormals(); return g;
}
function createBridges() {
for (let i = 0; i < 5; i++) createBridge(300 + i * 350);
}
function createBridge(worldZPosition) {
const {roadY,roadCurve}=getRoadPropertiesAtZ(worldZPosition);
const bridgeW=6,bridgeL=20,approachL=15,taperSegs=10,approachRoadW=12,deckH=1;
const bridgeGeom = new THREE.BoxGeometry(bridgeW,deckH,bridgeL);
const bridgeMat = new THREE.MeshStandardMaterial({color:0x888888});
const bridge = new THREE.Mesh(bridgeGeom,bridgeMat);
bridge.position.set(roadCurve,roadY+deckH/2,worldZPosition);
bridge.castShadow=true; bridge.receiveShadow=true; scene.add(bridge);
const railW=0.5,railH=1; const railGeom=new THREE.BoxGeometry(railW,railH,bridgeL);
const railMat = new THREE.MeshStandardMaterial({color:0x444444});
const lRail=new THREE.Mesh(railGeom,railMat); lRail.position.set(-bridgeW/2+railW/2,railH/2,0); bridge.add(lRail);
const rRail=new THREE.Mesh(railGeom,railMat); rRail.position.set(bridgeW/2-railW/2,railH/2,0); bridge.add(rRail);
const approachMat=new THREE.MeshStandardMaterial({color:0x555555});
const nAppGeom=createTaperedGeometry(approachL,approachRoadW,bridgeW,taperSegs);
nAppGeom.rotateX(-Math.PI/2);
const nApp=new THREE.Mesh(nAppGeom,approachMat);
nApp.position.set(roadCurve,roadY+0.02,worldZPosition-bridgeL/2-approachL/2);
nApp.receiveShadow=true; scene.add(nApp);
const sAppGeom=createTaperedGeometry(approachL,bridgeW,approachRoadW,taperSegs);
sAppGeom.rotateX(-Math.PI/2);
const sApp=new THREE.Mesh(sAppGeom,approachMat);
sApp.position.set(roadCurve,roadY+0.02,worldZPosition+bridgeL/2+approachL/2);
sApp.receiveShadow=true; scene.add(sApp);
const sign=new THREE.Mesh(new THREE.BoxGeometry(0.5,2,0.1),new THREE.MeshStandardMaterial({color:0xFFFFFF}));
sign.position.set(roadCurve-(approachRoadW/2+2),1+roadY,worldZPosition-bridgeL/2-approachL-5); scene.add(sign);
const signGraphic=new THREE.Mesh(new THREE.PlaneGeometry(0.4,0.4),new THREE.MeshBasicMaterial({color:0x000000}));
signGraphic.position.set(0,0.5,0.06); sign.add(signGraphic);
bridges.push({position:worldZPosition,width:bridgeW,length:bridgeL,approachLength:approachL,mesh:bridge,northApproach:nApp,southApproach:sApp,carsWaiting:[]});
}
function easeInOut(t){return t<0.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;}
function createPotholes() {
for (let i = 0; i < 10; i++) createPothole(200 + i * 180 + Math.random()*40);
}
function createPothole(worldZPosition) {
const {roadY,roadCurve}=getRoadPropertiesAtZ(worldZPosition);
const xOff=-roadWidth/4+(Math.random()-0.5)*(roadWidth/2-1);
const potholeGeom=new THREE.CircleGeometry(0.5+Math.random()*0.3,8);
potholeGeom.rotateX(-Math.PI/2);
const pothole=new THREE.Mesh(potholeGeom,new THREE.MeshStandardMaterial({color:0x111111,roughness:0.9}));
pothole.position.set(roadCurve+xOff,roadY+0.01,worldZPosition); scene.add(pothole);
potholes.push({mesh:pothole,positionZ:worldZPosition,positionX:roadCurve+xOff,hit:false});
}
function createJumpRamps() {
const numRamps = 5;
const rampLength = 10;
const rampWidth = 4;
const rampHeight = 2; // Height at the peak of the ramp
for (let i = 0; i < numRamps; i++) {
const worldZPosition = 250 + i * (roadLength / (numRamps + 1)) + (Math.random() - 0.5) * 100; // Distribute ramps
const { roadY, roadCurve } = getRoadPropertiesAtZ(worldZPosition);
// Create ramp geometry (a wedge)
const shape = new THREE.Shape();
shape.moveTo(-rampWidth / 2, 0);
shape.lineTo(rampWidth / 2, 0);
shape.lineTo(rampWidth / 2, rampHeight); // This point defines the peak
shape.lineTo(-rampWidth / 2, rampHeight * 0.3); // Lower front part of ramp for smoother entry
shape.closePath();
const extrudeSettings = { depth: rampLength, bevelEnabled: false };
const rampGeometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
// Rotate and position the ramp
rampGeometry.rotateY(Math.PI / 2); // Rotate so length is along Z
rampGeometry.translate(0, 0, -rampLength / 2); // Center it
const rampMaterial = new THREE.MeshStandardMaterial({ color: 0x777777, roughness: 0.6 });
const ramp = new THREE.Mesh(rampGeometry, rampMaterial);
// Place ramp on the road, slightly to one side or centered
const xOffset = (Math.random() - 0.5) * (roadWidth - rampWidth) * 0.5;
ramp.position.set(roadCurve + xOffset, roadY + 0.05, worldZPosition); // +0.05 to be slightly above road
ramp.castShadow = true;
ramp.receiveShadow = true;
scene.add(ramp);
jumpRamps.push({ mesh: ramp, worldZ: worldZPosition, length: rampLength, width: rampWidth, height: rampHeight, used: false });
}
}
function createFerry() {
ferryObject = new THREE.Group();
const { roadY, roadCurve } = getRoadPropertiesAtZ(ferryPosition);
// Ferry Deck
const deckWidth = 20;
const deckLength = 40;
const deckHeight = 2;
const deckGeometry = new THREE.BoxGeometry(deckWidth, deckHeight, deckLength);
const deckMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.7 });
const deck = new THREE.Mesh(deckGeometry, deckMaterial);
deck.position.y = deckHeight / 2;
deck.receiveShadow = true;
deck.castShadow = true;
ferryObject.add(deck);
// Superstructure (Cabin)
const cabinWidth = 10;
const cabinLength = 15;
const cabinHeight = 8;
const cabinGeometry = new THREE.BoxGeometry(cabinWidth, cabinHeight, cabinLength);
const cabinMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.8 });
const cabin = new THREE.Mesh(cabinGeometry, cabinMaterial);
cabin.position.set(0, deckHeight + cabinHeight / 2, -deckLength / 4); // Position on deck towards the rear
cabin.castShadow = true;
ferryObject.add(cabin);
// Funnel
const funnelRadius = 1.5;
const funnelHeight = 7;
const funnelGeometry = new THREE.CylinderGeometry(funnelRadius, funnelRadius * 0.8, funnelHeight, 16);
const funnelMaterial = new THREE.MeshStandardMaterial({ color: 0x555555 });
const funnel = new THREE.Mesh(funnelGeometry, funnelMaterial);
funnel.position.set(0, deckHeight + cabinHeight + funnelHeight / 2 - 2, cabinLength / 3);
funnel.castShadow = true;
ferryObject.add(funnel);
// Position ferry at ocean level (-3) + deck height, so it floats properly
const oceanLevel = -3;
ferryObject.position.set(roadCurve, oceanLevel + deckHeight/2, ferryPosition + deckLength/2 + 5);
ferryObject.rotation.y = Math.PI / 2; // Sideways to the road
scene.add(ferryObject);
}
function startGame() {
document.getElementById('instructions').style.display = 'none';
gameStarted = true;
clock.start();
animate();
clearErrors();
}
function restartGame() {
gameOver = false; gameTime = 0; speed = 0; score = 0; playerHealth = 100;
goodStopsInARow = 0; isGrounded = true; airTime = 0; velocity.set(0,0,0);
suspensionCompression = 0; lastDamageTime = 0; // Reset damage timer
document.getElementById('score').textContent = `Score: ${score}`;
document.getElementById('health').style.width = '100%';
document.getElementById('health').style.backgroundColor = '#00ff00';
const startPos = getRoadPropertiesAtZ(0);
playerCar.position.set(startPos.roadCurve, 1 + startPos.roadY, 0);
playerCar.rotation.set(0,0,0);
aiCars.forEach(ai => {
const {roadY:rY, roadCurve:rC} = getRoadPropertiesAtZ(ai.initialPositionZ);
// SWITCHED: Updated for right-side driving
const lo = ai.isOncoming ? (-roadWidth/4-0.25) : (roadWidth/4+0.25);
ai.mesh.position.set(rC+lo, 1+rY, ai.initialPositionZ);
ai.mesh.rotation.y = ai.isOncoming ? Math.PI : 0;
Object.assign(ai, {waiting:false,honking:false,waitingAtPassingPlace:false,flashingLights:false,isOvertaking:false,avoidingPlayer:false});
ai.speed = ai.baseSpeed; // Reset to base speed
if(ai.leftHeadlight) ai.leftHeadlight.material.color.setHex(0xFFFFFF);
if(ai.rightHeadlight) ai.rightHeadlight.material.color.setHex(0xFFFFFF);
});
potholes.forEach(p => p.hit = false);
jumpRamps.forEach(r => r.used = false); // Reset used state of ramps
document.getElementById('rearView').innerHTML = '';
document.getElementById('gameOver').style.display = 'none';
document.getElementById('instructions').style.display = 'none';
clock.stop(); clock.start(); gameStarted = true;
}
function animate() {
if (gameOver && !gameStarted) { renderer.render(scene, camera); requestAnimationFrame(animate); return; }
if (!gameStarted && gameOver) { renderer.render(scene, camera); requestAnimationFrame(animate); return; }
if (!gameStarted) return;
requestAnimationFrame(animate);
const delta = clock.getDelta();
if (!gameOver) update(delta);
renderer.render(scene, camera);
if (Math.random() < 0.05) clearErrors();
}
function update(delta) {
gameTime += delta; updateTimer(); updateClouds(delta);
if (playerCar.position.z >= ferryPosition) { endGame(true); return; }
if (gameTime > 120) { showMessage("Time's up! You missed the ferry!", 5); endGame(false, "Time's up!"); return; }
updatePlayerCar(delta); updateAICars(delta); checkCollisions(); updateCamera();
updateHealthRegen(delta); // Add health regeneration
if (!isGrounded) {
document.getElementById('airTime').textContent = `Air Time: ${airTime.toFixed(1)}s`;
document.getElementById('airTime').style.display = 'block';
} else { document.getElementById('airTime').style.display = 'none'; }
updateParticleEffects(delta);
}
function updateParticleEffects(delta) {
if (playerCar.particles) {
const particles = playerCar.particles; const particlePositions = particles.geometry.attributes.position;
const showParticles = (isGrounded && Math.abs(speed)>15 && (keys.ArrowLeft||keys.ArrowRight||keys.a||keys.d)) ||
(!isGrounded && Math.abs(velocity.y)<0.1 && airTime>0.1 && playerCar.position.y < lastRoadY+1.5) ||
(isGrounded && Math.abs(speed)>25 && (keys.ArrowUp||keys.w));
if (showParticles) {
particles.visible = true;
for (let i=0; i<particlePositions.count; i++) {
const wheelIdx=(Math.floor(Math.random()*2)+2), wheelX=(wheelIdx===2)?-1:1, wheelZ=-1.5;
const xOff=(Math.random()-0.5)*0.5, yOff=Math.random()*0.1-0.2, zOff=(Math.random()-0.5)*0.5-0.3;
particlePositions.setXYZ(i,wheelX+xOff,yOff,wheelZ+zOff);
}
particlePositions.needsUpdate = true;
} else { particles.visible = false; }
}
}
function updatePlayerCar(delta) {
updateCarPhysics(delta);
if((keys.ArrowUp||keys.w)&&!gameOver) speed+=acceleration*(isGrounded?1.2:0.3);
else if((keys.ArrowDown||keys.s)&&!gameOver) speed-=braking*(isGrounded?1.2:0.3);
else { if(isGrounded)speed*=0.98; else speed*=0.995; if(Math.abs(speed)<0.05)speed=0; }
if((keys[' ']||keys.j)&&isGrounded&&Math.abs(speed)>10){
// Speed-dependent jump height
jumpSpeed = Math.abs(speed); // Store speed when jumping
jumpForce = 0.8 + jumpSpeed * 0.04 + suspensionCompression * 6; // Reduced base jump
velocity.y = jumpForce;
isGrounded = false;
showMessage("Jumping!", 1);
}
speed=Math.max(-maxSpeed/2,Math.min(maxSpeed,speed));
document.getElementById('speedometer').textContent=`Speed: ${Math.abs(Math.round(speed))} mph`;
const actualMoveSpeed=speed*delta*2.5;
const steeringInput=(keys.ArrowLeft||keys.a)?1:(keys.ArrowRight||keys.d)?-1:0;
if(steeringInput!==0&&Math.abs(speed)>0.1){
const steerEff=isGrounded?1.0:0.3; const turnRate=steering*steerEff*Math.abs(speed/maxSpeed)*2.0;
playerCar.rotation.y+=steeringInput*turnRate*Math.sign(speed);
}
playerCar.position.x+=Math.sin(playerCar.rotation.y)*actualMoveSpeed;
playerCar.position.z+=Math.cos(playerCar.rotation.y)*actualMoveSpeed;
if(!isGrounded){
const airTilt=Math.min(Math.max(velocity.y*0.1,-0.3),0.3);
playerCar.rotation.x=airTilt;
playerCar.rotation.z+=steeringInput*speed*0.0005;
playerCar.rotation.z=Math.max(-0.3,Math.min(0.3,playerCar.rotation.z));
}else{playerCar.rotation.x*=0.8; playerCar.rotation.z*=0.8;}
const {roadY:currentRoadY,roadCurve:currentRoadCurve}=getRoadPropertiesAtZ(playerCar.position.z);
if(isGrounded) playerCar.position.y=1+currentRoadY+suspensionCompression;
if(isGrounded){
const latOff=playerCar.position.x-currentRoadCurve; const maxOff=roadWidth/2+0.5;
if(Math.abs(latOff)>maxOff){
playerCar.position.x-=Math.sign(latOff)*0.1*Math.abs(latOff-maxOff);
if(Math.abs(latOff)>maxOff+1.0)speed*=0.95;
}
}
}
function updateCarPhysics(delta) {
const {roadY: groundHeightAtCar}=getRoadPropertiesAtZ(playerCar.position.z);
const carEffectiveRadius=0.5;
// Check for jump ramp interaction
let onRamp = false;
jumpRamps.forEach(ramp => {
const distToRampZ = Math.abs(playerCar.position.z - ramp.mesh.position.z);
const distToRampX = Math.abs(playerCar.position.x - ramp.mesh.position.x);
if (distToRampZ < ramp.length / 2 && distToRampX < ramp.width / 2 && playerCar.position.y < ramp.mesh.position.y + ramp.height + carEffectiveRadius) {
onRamp = true;
if (!ramp.used && isGrounded) { // Only trigger jump once and if grounded
jumpSpeed = Math.abs(speed); // Store speed when hitting ramp
velocity.y = ramp.height * 1.2 + jumpSpeed * 0.08; // Speed-dependent ramp jump
isGrounded = false;
ramp.used = true; // Mark ramp as used for this jump
showMessage("Ramp Jump!", 2);
score += 150; // Bonus points for ramp jump
document.getElementById('score').textContent = `Score: ${score}`;
setTimeout(() => { ramp.used = false; }, 3000); // Allow reuse after a delay
}
}
});
if (isGrounded && !onRamp) { // Don't apply ground physics if on ramp and about to jump
const elevationChange = groundHeightAtCar - lastRoadY;
suspensionCompression = -elevationChange * suspensionStrength;
suspensionCompression = Math.max(-0.3, Math.min(0.3, suspensionCompression));
playerCar.position.y = groundHeightAtCar + carEffectiveRadius + suspensionCompression;
velocity.y = 0;
} else {
velocity.y -= gravity * delta * 20;
playerCar.position.y += velocity.y * delta * 5;
airTime += delta;
if (playerCar.position.y <= groundHeightAtCar + carEffectiveRadius && velocity.y < 0 && !onRamp) { // Land only if not on ramp
playerCar.position.y = groundHeightAtCar + carEffectiveRadius;
isGrounded = true;
const impactForce = Math.abs(velocity.y);
velocity.y = 0;
// Check for successful landing bonuses
checkLandingBonus(impactForce);
airTime = 0;
// Speed-dependent landing damage
const speedFactor = Math.max(0.3, jumpSpeed / 45); // Reduce damage for slower jumps
const damageThreshold = 0.4 + (jumpSpeed / 45); // Higher threshold for faster jumps
if (impactForce > damageThreshold) {
const damage = Math.floor(impactForce * 5 * speedFactor); // Much reduced damage
decreaseHealth(damage, `Hard landing!`);
suspensionCompression = Math.min(impactForce*0.2,0.4);
} else {
suspensionCompression = Math.min(impactForce*0.1,0.1);
}
jumpSpeed = 0; // Reset jump speed after landing
}
}
lastRoadY = groundHeightAtCar;
playerCar.children.forEach(c=>{if(c.isWheel)c.position.y=0.5+suspensionCompression*0.5;});
}
function updateAICars(delta) {
carsBehind = [];
aiCars.forEach(ai => {
const carM=ai.mesh;
// NEW: Collision avoidance logic
const distToPlayer = playerCar.position.distanceTo(carM.position);
let targetSpeed = ai.baseSpeed;
ai.avoidingPlayer = false;
if (ai.isOncoming) {
// Oncoming cars should slow down when approaching player head-on
if (distToPlayer < ai.reactionDistance &&
Math.abs(carM.position.x - playerCar.position.x) < roadWidth * 0.8) {
ai.avoidingPlayer = true;
const slowdownFactor = Math.max(0.2, distToPlayer / ai.reactionDistance);
targetSpeed = ai.baseSpeed * slowdownFactor;
}
} else {
// Cars behind player should slow down when getting too close
const zDiff = playerCar.position.z - carM.position.z;
if (zDiff > 0 && zDiff < ai.reactionDistance &&
Math.abs(carM.position.x - playerCar.position.x) < roadWidth * 0.6) {
ai.avoidingPlayer = true;
const slowdownFactor = Math.max(0.3, zDiff / ai.reactionDistance);
targetSpeed = Math.min(ai.baseSpeed * slowdownFactor, Math.abs(speed) * 0.8);
}
}
// Smoothly adjust speed towards target
ai.speed += (targetSpeed - ai.speed) * delta * 2;
let curSpd=ai.waiting||ai.waitingAtPassingPlace?0:ai.speed;
const moveDist=curSpd*(ai.isOncoming?-1:1)*delta*2.5; carM.position.z+=moveDist;
const {roadY,roadCurve}=getRoadPropertiesAtZ(carM.position.z);
// SWITCHED: Updated for right-side driving
let tLaneXOff=ai.isOncoming?(-roadWidth/4):(-roadWidth/4);
if(ai.isOvertaking)tLaneXOff=ai.isOncoming?(roadWidth/4):(-roadWidth/4);
const tX=roadCurve+tLaneXOff; carM.position.x+=(tX-carM.position.x)*0.1; carM.position.y=1+roadY;
if(ai.isOncoming){
const dToP=playerCar.position.distanceTo(carM.position);
if(dToP<40&&!ai.waitingAtPassingPlace){
let pYield=isPlayerInPassingPlaceForOncoming(ai);
if(!pYield&&ai.politeness>0.5){
let canAIPull=false;
passingPlaces.forEach(pp=>{if(pp.side===1&&Math.abs(carM.position.z-pp.position)<pp.length/2+10){canAIPull=true;ai.waitingAtPassingPlace=true;ai.flashingLights=true;}});
if(!canAIPull)ai.waiting=true;
}
}else if((ai.waiting||ai.waitingAtPassingPlace)&&dToP>50){ai.waiting=false;ai.waitingAtPassingPlace=false;ai.flashingLights=false;}
}else{
const distBPlayer=playerCar.position.z-carM.position.z;
if(distBPlayer>5&&distBPlayer<30&&speed<ai.speed*0.8&&!ai.isOvertaking){
let canOvertake=false;
passingPlaces.forEach(pp=>{if(Math.abs(playerCar.position.z-pp.position)<pp.length/2&&playerCar.position.x*pp.side<0&&Math.abs(speed)<5){if(pp.side===-1&&playerCar.position.x<getRoadPropertiesAtZ(playerCar.position.z).roadCurve)canOvertake=true; if(pp.side===1&&playerCar.position.x>getRoadPropertiesAtZ(playerCar.position.z).roadCurve)canOvertake=true;}});
if(canOvertake){ai.isOvertaking=true;ai.flashingLights=false;setTimeout(()=>{ai.isOvertaking=false;},5000);}
else if(!ai.waitingAtPassingPlace)ai.flashingLights=true;
}else if(ai.flashingLights&&distBPlayer>50)ai.flashingLights=false;
}
if(ai.flashingLights){const t=Date.now()*0.005; const fs=Math.sin(t*5)>0; if(ai.leftHeadlight)ai.leftHeadlight.material.color.setHex(fs?0xFFFF00:0xFFFFFF); if(ai.rightHeadlight)ai.rightHeadlight.material.color.setHex(fs?0xFFFF00:0xFFFFFF);}
else{if(ai.leftHeadlight)ai.leftHeadlight.material.color.setHex(0xFFFFFF); if(ai.rightHeadlight)ai.rightHeadlight.material.color.setHex(0xFFFFFF);}
if(!ai.isOncoming&&carM.position.z<playerCar.position.z&&carM.position.z>playerCar.position.z-50)carsBehind.push(ai);
if(Math.abs(carM.position.z-(roadLength/2))>roadLength/2+100){
const iZ=ai.initialPositionZ; const{roadY:iRY,roadCurve:iRC}=getRoadPropertiesAtZ(iZ);
// SWITCHED: Updated for right-side driving
const lo=ai.isOncoming?(-roadWidth/4-0.25):(roadWidth/4+0.25);
carM.position.set(iRC+lo,1+iRY,iZ);
Object.assign(ai,{waiting:false,waitingAtPassingPlace:false,isOvertaking:false,flashingLights:false,avoidingPlayer:false});
ai.speed = ai.baseSpeed; // Reset speed when respawning
}
});
updateRearViewMirror();
}
function isPlayerInPassingPlaceForOncoming(oncomingAICar) {
const pZ=playerCar.position.z,pX=playerCar.position.x; const{roadCurve:pRC}=getRoadPropertiesAtZ(pZ);
for(const pp of passingPlaces){
if(Math.abs(pZ-pp.position)<pp.length/2){
// SWITCHED: Updated for right-side driving - player should pull to the right
if(pp.side===1){if(pX>pRC+roadWidth/4&&Math.abs(speed)<5)return true;}
}
}return false;
}
function awardPointsForGoodStop(){goodStopsInARow++;let pts=0;if(goodStopsInARow===1)pts=100;else if(goodStopsInARow===2)pts=500;else if(goodStopsInARow>=3)pts=1000;if(pts>0){score+=pts;document.getElementById('score').textContent=`Score: ${score}`;showMessage(`+${pts} points! ${goodStopsInARow} good stops!`,3);}}
function updateRearViewMirror(){
const rvEl=document.getElementById('rearView'); rvEl.innerHTML='';
carsBehind.sort((a,b)=>(playerCar.position.z-a.mesh.position.z)-(playerCar.position.z-b.mesh.position.z));
for(let i=0;i<Math.min(carsBehind.length,3);i++){
const cd=carsBehind[i]; const dist=playerCar.position.z-cd.mesh.position.z;
const ind=document.createElement('div'); ind.style.position='absolute';
ind.style.width=`${Math.max(5,30-dist*0.5)}px`; ind.style.height=`${Math.max(3,20-dist*0.3)}px`;
ind.style.backgroundColor=cd.flashingLights?(Math.sin(Date.now()*0.01)>0?'#ffff00':'#cc0000'):'#cc0000';
ind.style.borderRadius='3px';
ind.style.left=`${(rvEl.offsetWidth/2)-(parseFloat(ind.style.width)/2)+(i-Math.floor(Math.min(carsBehind.length,3)/2))*35}px`;
ind.style.bottom=`${5+Math.max(0,20-dist*0.8)}px`; ind.style.zIndex=50-Math.floor(dist);
rvEl.appendChild(ind);
}
}
function checkCollisions() {
const playerBox = new THREE.Box3().setFromObject(playerCar);
potholes.forEach(pd=>{if(!pd.hit){const dist=playerCar.position.distanceTo(pd.mesh.position);if(dist<1.5){pd.hit=true;const oSpd=Math.abs(speed);speed*=0.6;decreaseHealth(15,"Hit a pothole!");if(oSpd>20){showMessage("Flat tire!",3);decreaseHealth(25,"Flat Tire!");}}}});
aiCars.forEach(aiD=>{
const aiB=new THREE.Box3().setFromObject(aiD.mesh);
if(playerBox.intersectsBox(aiB)){
// NEW: Determine collision type and adjust damage accordingly
let damage = 35; // Base damage
let collisionType = "Collision!";
// Check if it's a rear collision (player hitting AI from behind or AI hitting player from behind)
const playerToAI = aiD.mesh.position.z - playerCar.position.z;
const playerSpeed = Math.abs(speed);
if (Math.abs(playerToAI) > 2) { // Not a side collision
if ((playerToAI > 0 && speed > 0) || (playerToAI < 0 && speed < 0)) {
// Rear collision - much less damage
damage = Math.max(8, Math.floor(damage * 0.3)); // 70% damage reduction
collisionType = "Rear collision!";
}
}
// Speed-based damage adjustment
const speedFactor = Math.max(0.5, playerSpeed / 30);
damage = Math.floor(damage * speedFactor);
decreaseHealth(damage, collisionType);
playerCar.position.z-=Math.sign(speed)*2.5;
speed*=0.1;
aiD.waiting=true;
setTimeout(()=>{if(aiD)aiD.waiting=false;},2500);
}
});
}
function checkLandingBonus(impactForce) {
// Award points for successful jumps and landings
if (jumpSpeed > 0) {
let bonusPoints = 0;
if (airTime > 1.0) bonusPoints += 100;
if (airTime > 2.0) bonusPoints += 200;
if (jumpSpeed > 30) bonusPoints += 150;
if (impactForce < 0.5) bonusPoints += 100; // Smooth landing bonus
if (bonusPoints > 0) {
score += bonusPoints;
document.getElementById('score').textContent = `Score: ${score}`;
showMessage(`Landing bonus: +${bonusPoints}!`, 2);
}
}
}
function updateHealthRegen(delta) {
// Regenerate health slowly if no recent damage (after 3 seconds of no damage)
if (gameTime - lastDamageTime > 3 && playerHealth < 100) {
playerHealth = Math.min(100, playerHealth + healthRegenRate * delta);
document.getElementById('health').style.width = `${playerHealth}%`;
// Update health bar color
if (playerHealth < 30) document.getElementById('health').style.backgroundColor = '#ff0000';
else if (playerHealth < 60) document.getElementById('health').style.backgroundColor = '#ffff00';
else document.getElementById('health').style.backgroundColor = '#00ff00';
}
}
function decreaseHealth(amount,message){
playerHealth-=amount;
playerHealth=Math.max(0,playerHealth);
lastDamageTime = gameTime; // Track when damage occurred
document.getElementById('health').style.width=`${playerHealth}%`;
if(playerHealth<30)document.getElementById('health').style.backgroundColor='#ff0000';
else if(playerHealth<60)document.getElementById('health').style.backgroundColor='#ffff00';
else document.getElementById('health').style.backgroundColor='#00ff00';
if(message)showMessage(message,3);
if(playerHealth<=0&&!gameOver)endGame(false,"Car too damaged!");
}
function updateCamera(){const cH=3.8,cD=9,laD=18;const tCP=new THREE.Vector3();tCP.set(0,cH,-cD);tCP.applyMatrix4(playerCar.matrixWorld);const tLA=new THREE.Vector3();tLA.set(0,1.2,laD);tLA.applyMatrix4(playerCar.matrixWorld);camera.position.lerp(tCP,0.08);camera.lookAt(tLA);}
function updateTimer(){const m=Math.floor(gameTime/60),s=Math.floor(gameTime%60);document.getElementById('timer').textContent=`Time: ${m}:${s<10?'0':''}${s}`;}
function showMessage(text,duration){const msgEl=document.getElementById('message');msgEl.textContent=text;msgEl.style.opacity=1;setTimeout(()=>{msgEl.style.opacity=0;},duration*1000);clearErrors();}
function clearErrors(){document.querySelectorAll('.error-message').forEach(el=>el.style.display='none');const bCN=document.body.childNodes;for(let i=bCN.length-1;i>=0;i--){const n=bCN[i];if(n.nodeType===Node.TEXT_NODE&&n.parentElement===document.body&&n.textContent.trim()!==''){if(n.textContent.includes('function')||n.textContent.includes('var')||n.textContent.includes('error'))n.textContent='';}}}
function endGame(success,customMessage){if(gameOver)return;gameOver=true;gameStarted=false;const goEl=document.getElementById('gameOver'),goT=document.getElementById('gameOverTitle'),goTxt=document.getElementById('gameOverText');if(success){goT.textContent="Success!";goTxt.innerHTML=`Made it in ${Math.floor(gameTime/60)}:${Math.floor(gameTime%60)<10?'0':''}${Math.floor(gameTime%60)}!<br><br>Score: ${score}<br>Condition: ${playerHealth}%`;}else{goT.textContent="Game Over";goTxt.innerHTML=(customMessage||"Didn't make it.")+`<br><br>Score: ${score}`;}goEl.style.display='block';}
function onWindowResize(){camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);}
function onKeyDown(e){if(keys.hasOwnProperty(e.key.toLowerCase()))keys[e.key.toLowerCase()]=true;else if(e.key.startsWith('Arrow'))keys[e.key]=true;else if(e.code==='Space')keys[' ']=true;}
function onKeyUp(e){if(keys.hasOwnProperty(e.key.toLowerCase()))keys[e.key.toLowerCase()]=false;else if(e.key.startsWith('Arrow'))keys[e.key]=false;else if(e.code==='Space')keys[' ']=false;}
init();
</script>
</body>
</html>