awacke1 commited on
Commit
579a4d7
·
verified ·
1 Parent(s): 2b14cc4

Create index.html

Browse files
Files changed (1) hide show
  1. index.html +1055 -0
index.html ADDED
@@ -0,0 +1,1055 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Isle of Mull Ferry Driving Simulator</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ overflow: hidden;
11
+ font-family: Arial, sans-serif;
12
+ }
13
+ #info {
14
+ position: absolute;
15
+ top: 10px;
16
+ width: 100%;
17
+ text-align: center;
18
+ color: white;
19
+ background-color: rgba(0,0,0,0.5);
20
+ padding: 10px;
21
+ z-index: 100;
22
+ }
23
+ #timer {
24
+ position: absolute;
25
+ top: 10px;
26
+ right: 10px;
27
+ color: white;
28
+ background-color: rgba(0,0,0,0.5);
29
+ padding: 5px 10px;
30
+ border-radius: 5px;
31
+ z-index: 100;
32
+ }
33
+ #message {
34
+ position: absolute;
35
+ bottom: 20px;
36
+ width: 100%;
37
+ text-align: center;
38
+ color: white;
39
+ background-color: rgba(0,0,0,0.7);
40
+ padding: 10px;
41
+ font-size: 18px;
42
+ z-index: 100;
43
+ transition: opacity 0.5s;
44
+ opacity: 0;
45
+ }
46
+ #speedometer {
47
+ position: absolute;
48
+ bottom: 10px;
49
+ left: 10px;
50
+ color: white;
51
+ background-color: rgba(0,0,0,0.5);
52
+ padding: 5px 10px;
53
+ border-radius: 5px;
54
+ z-index: 100;
55
+ }
56
+ #score {
57
+ position: absolute;
58
+ top: 10px;
59
+ left: 10px;
60
+ color: white;
61
+ background-color: rgba(0,0,0,0.5);
62
+ padding: 5px 10px;
63
+ border-radius: 5px;
64
+ z-index: 100;
65
+ }
66
+ #gameOver {
67
+ position: absolute;
68
+ top: 50%;
69
+ left: 50%;
70
+ transform: translate(-50%, -50%);
71
+ background-color: rgba(0,0,0,0.8);
72
+ color: white;
73
+ padding: 20px;
74
+ border-radius: 10px;
75
+ text-align: center;
76
+ z-index: 200;
77
+ display: none;
78
+ }
79
+ button {
80
+ background-color: #4CAF50;
81
+ border: none;
82
+ color: white;
83
+ padding: 10px 20px;
84
+ text-align: center;
85
+ text-decoration: none;
86
+ display: inline-block;
87
+ font-size: 16px;
88
+ margin: 10px 2px;
89
+ cursor: pointer;
90
+ border-radius: 5px;
91
+ }
92
+ #instructions {
93
+ position: absolute;
94
+ top: 50%;
95
+ left: 50%;
96
+ transform: translate(-50%, -50%);
97
+ background-color: rgba(0,0,0,0.8);
98
+ color: white;
99
+ padding: 20px;
100
+ border-radius: 10px;
101
+ text-align: center;
102
+ z-index: 300;
103
+ max-width: 600px;
104
+ }
105
+ .highlight {
106
+ color: #ffcc00;
107
+ font-weight: bold;
108
+ }
109
+ /* Hide specific error messages only */
110
+ .error-message {
111
+ display: none !important;
112
+ }
113
+ /* Make sure Three.js canvas is visible */
114
+ canvas {
115
+ display: block !important;
116
+ }
117
+ </style>
118
+ </head>
119
+ <body>
120
+ <div id="info">Isle of Mull Ferry Driving Simulator</div>
121
+ <div id="timer">Time: 0:00</div>
122
+ <div id="speedometer">Speed: 0 mph</div>
123
+ <div id="score">Score: 0</div>
124
+ <div id="message"></div>
125
+ <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>
126
+ <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;">
127
+ <div id="health" style="width: 100%; height: 100%; background-color: #00ff00;"></div>
128
+ </div>
129
+ <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>
130
+ <div id="gameOver">
131
+ <h2 id="gameOverTitle">Game Over</h2>
132
+ <p id="gameOverText"></p>
133
+ <button id="restartButton">Try Again</button>
134
+ </div>
135
+ <div id="instructions">
136
+ <h2>Welcome to the Isle of Mull Ferry Driving Simulator!</h2>
137
+ <p>You're driving to catch the ferry to the Isle of Mull in Scotland. Practice safe driving on Scotland's unique single-track roads with passing places.</p>
138
+
139
+ <h3>How to Play:</h3>
140
+ <ul style="text-align: left;">
141
+ <li>Drive on the <span class="highlight">left side</span> of the road</li>
142
+ <li>Use <span class="highlight">W/S</span> or <span class="highlight">↑/↓</span> to accelerate/brake</li>
143
+ <li>Use <span class="highlight">A/D</span> or <span class="highlight">←/→</span> to steer</li>
144
+ <li>Press <span class="highlight">SPACE</span> or <span class="highlight">J</span> to jump (when driving fast)</li>
145
+ <li>Watch for oncoming traffic and stop at <span class="highlight">passing places</span> to let them through</li>
146
+ <li>Be courteous to cars behind you by using passing places</li>
147
+ <li>Cross <span class="highlight">one-lane bridges</span> carefully - yield to oncoming traffic</li>
148
+ <li>Watch your <span class="highlight">rear view mirror</span> for cars flashing to overtake</li>
149
+ <li>Look for <span class="highlight">jump ramps</span> to perform jumps and earn bonus points!</li>
150
+ <li>Avoid potholes and collisions - watch your damage meter!</li>
151
+ <li>Earn <span class="highlight">points</span> for courteous driving and jump tricks</li>
152
+ <li>Reach the ferry in under 2 minutes!</li>
153
+ </ul>
154
+
155
+ <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>
156
+
157
+ <button id="startButton">Start Driving</button>
158
+ </div>
159
+
160
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
161
+ <script>
162
+ // Game variables
163
+ let scene, camera, renderer, clock;
164
+ let playerCar, road, terrain;
165
+ let aiCars = [];
166
+ let passingPlaces = [];
167
+ let bridges = [];
168
+ let clouds = [];
169
+ let oceanPlane;
170
+ let jumpRamps = []; // Array to store jump ramps
171
+ let ferryObject; // For the ferry mesh
172
+
173
+ let gameStarted = false;
174
+ let gameOver = false;
175
+ let gameTime = 0;
176
+ let speed = 0;
177
+ const roadWidth = 5;
178
+ let maxSpeed = 45;
179
+ let acceleration = 0.2;
180
+ let deceleration = 0.3;
181
+ let braking = 0.5;
182
+ let steering = 0.02;
183
+ let roadLength = 2000;
184
+ let ferryPosition = roadLength - 100;
185
+ let potholes = [];
186
+ let score = 0;
187
+ let playerHealth = 100;
188
+ let healthRegenRate = 2; // Health points per second when not taking damage
189
+ let lastDamageTime = 0;
190
+ let goodStopsInARow = 0;
191
+ let carsBehind = [];
192
+
193
+ // Physics variables
194
+ let gravity = 0.25;
195
+ let velocity = new THREE.Vector3(0, 0, 0);
196
+ let isGrounded = true;
197
+ let airTime = 0;
198
+ let jumpForce = 0;
199
+ let jumpSpeed = 0; // Track speed when jumping
200
+ let lastRoadY = 0;
201
+ let suspensionCompression = 0;
202
+ let suspensionStrength = 0.3;
203
+ let suspensionDamping = 0.8;
204
+
205
+ const keys = {
206
+ ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false,
207
+ w: false, a: false, s: false, d: false,
208
+ ' ': false, j: false
209
+ };
210
+
211
+ function init() {
212
+ const bodyChildNodes = document.body.childNodes;
213
+ for (let i = bodyChildNodes.length - 1; i >= 0; i--) {
214
+ const node = bodyChildNodes[i];
215
+ if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '' &&
216
+ (node.textContent.includes('function') || node.textContent.includes('var') || node.textContent.includes('let') || node.textContent.includes('const'))) {
217
+ node.textContent = '';
218
+ }
219
+ }
220
+
221
+ scene = new THREE.Scene();
222
+ scene.background = new THREE.Color(0x87CEEB);
223
+
224
+ camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000); // Increased far plane
225
+ camera.position.set(0, 5, -10);
226
+ camera.lookAt(0, 0, 10);
227
+
228
+ renderer = new THREE.WebGLRenderer({ antialias: true });
229
+ renderer.setSize(window.innerWidth, window.innerHeight);
230
+ renderer.shadowMap.enabled = true;
231
+ renderer.domElement.id = 'game-canvas';
232
+ document.body.appendChild(renderer.domElement);
233
+
234
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
235
+ scene.add(ambientLight);
236
+
237
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);
238
+ directionalLight.position.set(100, 150, 75);
239
+ directionalLight.castShadow = true;
240
+ directionalLight.shadow.mapSize.width = 2048;
241
+ directionalLight.shadow.mapSize.height = 2048;
242
+ directionalLight.shadow.camera.near = 0.5;
243
+ directionalLight.shadow.camera.far = 500;
244
+ directionalLight.shadow.camera.left = -100;
245
+ directionalLight.shadow.camera.right = 100;
246
+ directionalLight.shadow.camera.top = 100;
247
+ directionalLight.shadow.camera.bottom = -100;
248
+ scene.add(directionalLight);
249
+
250
+ createTerrain();
251
+ createRoad();
252
+ createPlayerCar();
253
+ createAICars();
254
+ createPassingPlaces();
255
+ createBridges();
256
+ createPotholes();
257
+ createOcean();
258
+ createClouds();
259
+ createJumpRamps(); // Add jump ramps
260
+ createFerry(); // Add the ferry
261
+
262
+ clock = new THREE.Clock();
263
+
264
+ window.addEventListener('resize', onWindowResize);
265
+ window.addEventListener('keydown', onKeyDown);
266
+ window.addEventListener('keyup', onKeyUp);
267
+
268
+ document.getElementById('startButton').addEventListener('click', startGame);
269
+ document.getElementById('restartButton').addEventListener('click', restartGame);
270
+ }
271
+
272
+ function getRoadPropertiesAtZ(worldZPos) {
273
+ const localZ = worldZPos - (roadLength / 2);
274
+ const roadCurve = Math.sin(localZ * 0.005) * 15;
275
+
276
+ // Gradually reduce elevation variation as we approach the ferry (coastal approach)
277
+ const distanceToFerry = Math.max(0, ferryPosition - worldZPos);
278
+ const coastalFactor = Math.min(1, distanceToFerry / 400); // Start flattening 400 units before ferry
279
+ const elevationAmplitude = coastalFactor * 0.5;
280
+
281
+ // Lift the entire road by 2 units to ensure it's always above water
282
+ const baseRoadElevation = 2;
283
+ const roadY = baseRoadElevation + (Math.sin(localZ * 0.01) * 5 + Math.sin(localZ * 0.03) * 2) * elevationAmplitude;
284
+ return { roadY, roadCurve };
285
+ }
286
+
287
+ function createTerrain() {
288
+ const terrainWidth = 1000;
289
+ const terrainSegmentsX = 150;
290
+ const terrainSegmentsZ = 300;
291
+ const groundGeometry = new THREE.PlaneGeometry(terrainWidth, roadLength, terrainSegmentsX, terrainSegmentsZ);
292
+ groundGeometry.rotateX(-Math.PI / 2);
293
+
294
+ const vertices = groundGeometry.attributes.position;
295
+ for (let i = 0; i < vertices.count; i++) {
296
+ const x_local = vertices.getX(i);
297
+ const z_local = vertices.getZ(i);
298
+ const worldZ = z_local + roadLength / 2;
299
+ const { roadY: actualRoadY, roadCurve: actualRoadCurve } = getRoadPropertiesAtZ(worldZ);
300
+
301
+ let height = actualRoadY;
302
+ const distFromRoadCenter = Math.abs(x_local - actualRoadCurve);
303
+ const roadEdgeBuffer = 5;
304
+
305
+ // Calculate coastal approach factor - mountains recede as we near the ferry
306
+ const distanceToFerry = Math.max(0, ferryPosition - worldZ);
307
+ const coastalTransition = Math.min(1, distanceToFerry / 600); // Start transition 600 units before ferry
308
+ const mountainHeightFactor = coastalTransition;
309
+
310
+ // Also create asymmetric coastal effect - mountains more on one side
311
+ const ferryApproachFactor = 1 - Math.max(0, Math.min(1, (ferryPosition - worldZ) / 800));
312
+ const coastalAsymmetry = ferryApproachFactor * Math.max(0, 1 - Math.abs(x_local + actualRoadCurve) / 200);
313
+
314
+ if (distFromRoadCenter > (roadWidth / 2 + roadEdgeBuffer)) {
315
+ const mountainBaseHeight = 10 + Math.abs(Math.sin(z_local * 0.001 + x_local * 0.005)) * 20;
316
+ const mountainDetail = Math.sin(z_local * 0.02 + x_local * 0.03) * 15 + Math.cos(z_local * 0.015) * 10;
317
+ let mountainOffset = mountainBaseHeight + mountainDetail;
318
+ const riseFactor = Math.min((distFromRoadCenter - (roadWidth / 2 + roadEdgeBuffer)) * 0.2, 1.0) + 0.5;
319
+ mountainOffset *= riseFactor;
320
+ const glenFactor = 0.6 + (Math.sin(z_local * 0.004) * 0.5 + 0.5) * 0.4;
321
+ mountainOffset *= glenFactor;
322
+
323
+ // Apply coastal factors to reduce mountain height near ferry
324
+ mountainOffset *= mountainHeightFactor;
325
+ mountainOffset *= (1 - coastalAsymmetry * 0.7); // Reduce mountains more on ocean side
326
+
327
+ height += Math.max(0, mountainOffset);
328
+ }
329
+ else if (distFromRoadCenter > roadWidth / 2) {
330
+ // Create natural water channels/glens beside the road
331
+ const channelDepth = (distFromRoadCenter - roadWidth/2) * 0.8;
332
+ const channelVariation = Math.sin(z_local * 0.02) * 0.5 + Math.cos(z_local * 0.015) * 0.3;
333
+ height -= channelDepth + channelVariation;
334
+
335
+ // Ensure channels don't go too deep and create nice glen-like depressions
336
+ height = Math.max(height, -1.5); // Keep above water level but create natural channels
337
+ }
338
+
339
+ // Ensure terrain near ferry is at reasonable coastal elevation
340
+ if (worldZ > ferryPosition - 200) {
341
+ const coastalBlend = (worldZ - (ferryPosition - 200)) / 200;
342
+ const targetCoastalHeight = 0.5; // Slightly above sea level for coastal approach
343
+ height = height * (1 - coastalBlend) + targetCoastalHeight * coastalBlend;
344
+ }
345
+
346
+ vertices.setY(i, height);
347
+ }
348
+ groundGeometry.attributes.position.needsUpdate = true;
349
+ groundGeometry.computeVertexNormals();
350
+
351
+ const groundMaterial = new THREE.MeshStandardMaterial({
352
+ color: 0x365E36, flatShading: true, roughness: 0.9, metalness: 0.1
353
+ });
354
+
355
+ terrain = new THREE.Mesh(groundGeometry, groundMaterial);
356
+ terrain.position.z = roadLength / 2;
357
+ terrain.receiveShadow = true;
358
+ scene.add(terrain);
359
+ }
360
+
361
+ function createRoad() {
362
+ const roadGeometry = new THREE.PlaneGeometry(roadWidth, roadLength, 1, 200);
363
+ roadGeometry.rotateX(-Math.PI / 2);
364
+
365
+ const vertices = roadGeometry.attributes.position;
366
+ for (let i = 0; i < vertices.count; i++) {
367
+ const localZ = vertices.getZ(i);
368
+ const worldZ = localZ + roadLength / 2;
369
+ const curve_val = Math.sin(localZ * 0.005) * 15;
370
+ vertices.setX(i, roadGeometry.attributes.position.getX(i) + curve_val);
371
+
372
+ // Use the updated getRoadPropertiesAtZ function for consistent elevation
373
+ const { roadY: elevation_val } = getRoadPropertiesAtZ(worldZ);
374
+ vertices.setY(i, elevation_val);
375
+ }
376
+ roadGeometry.attributes.position.needsUpdate = true;
377
+ roadGeometry.computeVertexNormals();
378
+
379
+ const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 });
380
+ road = new THREE.Mesh(roadGeometry, roadMaterial);
381
+ road.position.z = roadLength / 2;
382
+ road.receiveShadow = true;
383
+ scene.add(road);
384
+
385
+ const centerLineGeometry = new THREE.PlaneGeometry(0.1, roadLength, 1, 200);
386
+ centerLineGeometry.rotateX(-Math.PI / 2);
387
+
388
+ const lineVertices = centerLineGeometry.attributes.position;
389
+ for (let i = 0; i < lineVertices.count; i++) {
390
+ const localZ = lineVertices.getZ(i);
391
+ const worldZ = localZ + roadLength / 2;
392
+ const curve_val = Math.sin(localZ * 0.005) * 15;
393
+ lineVertices.setX(i, lineVertices.getX(i) + curve_val);
394
+ const { roadY: elevation_val } = getRoadPropertiesAtZ(worldZ);
395
+ lineVertices.setY(i, elevation_val + 0.01);
396
+ }
397
+ centerLineGeometry.attributes.position.needsUpdate = true;
398
+ centerLineGeometry.computeVertexNormals();
399
+
400
+ const centerLineMaterial = new THREE.MeshStandardMaterial({ color: 0xFFFFFF });
401
+ const centerLine = new THREE.Mesh(centerLineGeometry, centerLineMaterial);
402
+ centerLine.position.z = roadLength / 2;
403
+ scene.add(centerLine);
404
+ }
405
+
406
+ function createOcean() {
407
+ const oceanSize = 4000;
408
+ const oceanGeometry = new THREE.PlaneGeometry(oceanSize, oceanSize);
409
+ const oceanMaterial = new THREE.MeshStandardMaterial({
410
+ color: 0x0077be, transparent: true, opacity: 0.85, roughness: 0.3, metalness: 0.1,
411
+ });
412
+ oceanPlane = new THREE.Mesh(oceanGeometry, oceanMaterial);
413
+ oceanPlane.rotation.x = -Math.PI / 2;
414
+
415
+ // Position ocean at a consistent level that's always below the road
416
+ // The minimum road elevation with the new coastal approach will be around -1.5
417
+ const oceanLevel = -3; // Well below minimum road elevation
418
+ const ferryRoadProps = getRoadPropertiesAtZ(ferryPosition);
419
+ oceanPlane.position.set(ferryRoadProps.roadCurve, oceanLevel, ferryPosition + 50);
420
+ oceanPlane.receiveShadow = true;
421
+ scene.add(oceanPlane);
422
+ }
423
+
424
+ function createClouds() {
425
+ const cloudMaterial = new THREE.MeshBasicMaterial({
426
+ color: 0xffffff, transparent: true, opacity: 0.6, depthWrite: false
427
+ });
428
+ const numClouds = 15;
429
+ const skyHeight = 150;
430
+ const skyDepthRange = 1000;
431
+ const skyWidthRange = 2000;
432
+
433
+ for (let i = 0; i < numClouds; i++) {
434
+ const cloudWidth = 100 + Math.random() * 200;
435
+ const cloudHeight = 50 + Math.random() * 100;
436
+ const cloudGeometry = new THREE.PlaneGeometry(cloudWidth, cloudHeight);
437
+ const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
438
+ cloud.position.set(
439
+ (Math.random() - 0.5) * skyWidthRange,
440
+ skyHeight + (Math.random() - 0.5) * 50,
441
+ (Math.random() * skyDepthRange) - skyDepthRange / 4
442
+ );
443
+ cloud.rotation.y = (Math.random() - 0.5) * 0.5;
444
+ // cloud.lookAt(camera.position); // Initial orientation
445
+ cloud.userData.speed = 0.5 + Math.random() * 1;
446
+ clouds.push(cloud);
447
+ scene.add(cloud);
448
+ }
449
+ }
450
+
451
+ function updateClouds(delta) {
452
+ const wrapAroundX = 2200;
453
+ clouds.forEach(cloud => {
454
+ cloud.position.x += cloud.userData.speed * delta * 5;
455
+ if (cloud.position.x > wrapAroundX / 2) {
456
+ cloud.position.x = -wrapAroundX / 2;
457
+ cloud.position.z = (Math.random() * 1000) - 500 + playerCar.position.z; // Re-position relative to player Z
458
+ }
459
+ // Make clouds face the general direction of the camera's Z but not directly lookAt
460
+ const targetZ = camera.position.z + 500; // A point far in front of camera
461
+ const direction = new THREE.Vector3(cloud.position.x, cloud.position.y, targetZ);
462
+ cloud.lookAt(direction);
463
+
464
+ });
465
+ }
466
+
467
+ function createPlayerCar() {
468
+ const bodyGroup = new THREE.Group();
469
+ scene.add(bodyGroup);
470
+ playerCar = bodyGroup;
471
+ const bodyGeometry = new THREE.BoxGeometry(2, 1, 4);
472
+ bodyGeometry.translate(0, 0.5, 0);
473
+ const carMaterial = new THREE.MeshStandardMaterial({ color: 0x3366FF, roughness: 0.5, metalness: 0.7 });
474
+ const carBody = new THREE.Mesh(bodyGeometry, carMaterial);
475
+ carBody.castShadow = true;
476
+ bodyGroup.add(carBody);
477
+ const windshieldGeometry = new THREE.CylinderGeometry(1, 1, 1.8, 16, 1, false, 0, Math.PI);
478
+ windshieldGeometry.rotateZ(Math.PI / 2); windshieldGeometry.rotateY(Math.PI / 2);
479
+ windshieldGeometry.scale(1, 0.4, 0.8); windshieldGeometry.translate(0, 1.1, 0.5);
480
+ const windshieldMaterial = new THREE.MeshStandardMaterial({ color: 0xAACCFF, transparent: true, opacity: 0.7, roughness: 0.1, metalness: 0.2 });
481
+ const windshield = new THREE.Mesh(windshieldGeometry, windshieldMaterial);
482
+ bodyGroup.add(windshield);
483
+ const frontBumperGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2, 16, 1, false, -Math.PI/2, Math.PI);
484
+ frontBumperGeometry.rotateZ(Math.PI / 2); frontBumperGeometry.scale(1, 0.5, 0.5); frontBumperGeometry.translate(0, 0.5, 2);
485
+ const bumperMaterial = new THREE.MeshStandardMaterial({ color: 0x2255DD, roughness: 0.7, metalness: 0.3 });
486
+ const frontBumper = new THREE.Mesh(frontBumperGeometry, bumperMaterial);
487
+ frontBumper.castShadow = true; bodyGroup.add(frontBumper);
488
+ const rearBumperGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2, 16, 1, false, Math.PI/2, Math.PI);
489
+ rearBumperGeometry.rotateZ(Math.PI / 2); rearBumperGeometry.scale(1, 0.5, 0.5); rearBumperGeometry.translate(0, 0.5, -2);
490
+ const rearBumper = new THREE.Mesh(rearBumperGeometry, bumperMaterial);
491
+ rearBumper.castShadow = true; bodyGroup.add(rearBumper);
492
+ const wheelGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.3, 24);
493
+ wheelGeometry.rotateZ(Math.PI / 2);
494
+ const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9, metalness: 0.2 });
495
+ const hubCapGeometry = new THREE.CircleGeometry(0.3, 16);
496
+ const hubCapMaterial = new THREE.MeshStandardMaterial({ color: 0xCCCCCC, roughness: 0.5, metalness: 0.8 });
497
+ const wheelsInfo = [
498
+ { x: -1, z: 1.5, hubCapX: 0.16, hubCapRotY: Math.PI / 2 }, { x: 1, z: 1.5, hubCapX: -0.16, hubCapRotY: -Math.PI / 2 },
499
+ { x: -1, z: -1.5, hubCapX: 0.16, hubCapRotY: Math.PI / 2 }, { x: 1, z: -1.5, hubCapX: -0.16, hubCapRotY: -Math.PI / 2 }
500
+ ];
501
+ wheelsInfo.forEach(info => {
502
+ const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
503
+ wheel.position.set(info.x, 0.5, info.z); wheel.isWheel = true;
504
+ const hubCap = new THREE.Mesh(hubCapGeometry, hubCapMaterial);
505
+ hubCap.position.set(info.hubCapX, 0, 0); hubCap.rotation.y = info.hubCapRotY;
506
+ wheel.add(hubCap); bodyGroup.add(wheel);
507
+ });
508
+ const springMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
509
+ function createSpring(x, z_offset) {
510
+ const springGroup = new THREE.Group(); const coilCount = 5; const coilHeight = 0.08;
511
+ for (let i = 0; i < coilCount; i++) {
512
+ const coil = new THREE.Mesh(new THREE.TorusGeometry(0.15, 0.03, 8, 16), springMaterial);
513
+ coil.position.y = i * coilHeight; springGroup.add(coil);
514
+ }
515
+ springGroup.position.set(x, 0.2, z_offset); return springGroup;
516
+ }
517
+ bodyGroup.add(createSpring(-0.8, 1.5)); bodyGroup.add(createSpring(0.8, 1.5));
518
+ bodyGroup.add(createSpring(-0.8, -1.5)); bodyGroup.add(createSpring(0.8, -1.5));
519
+ const mirrorBase = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.1, 0.1), carMaterial);
520
+ mirrorBase.position.set(0, 1.3, -0.5); bodyGroup.add(mirrorBase);
521
+ const mirrorGeo = new THREE.CylinderGeometry(0.1, 0.1, 1.8, 16, 1, true, 0, Math.PI);
522
+ mirrorGeo.rotateX(Math.PI / 2); mirrorGeo.translate(0, 0.1, 0);
523
+ const mirror = new THREE.Mesh(mirrorGeo, new THREE.MeshStandardMaterial({ color: 0x444444, roughness: 0.3, metalness: 0.8 }));
524
+ mirror.position.set(0, 1.3, -0.5); bodyGroup.add(mirror);
525
+ const particlesCount = 50; const particlesGeometry = new THREE.BufferGeometry();
526
+ const posArray = new Float32Array(particlesCount * 3); const sizeArray = new Float32Array(particlesCount);
527
+ for (let i = 0; i < particlesCount; i++) {
528
+ posArray[i*3]=0; posArray[i*3+1]=0; posArray[i*3+2]=0; sizeArray[i]=Math.random()*0.2;
529
+ }
530
+ particlesGeometry.setAttribute('position', new THREE.BufferAttribute(posArray,3));
531
+ particlesGeometry.setAttribute('size', new THREE.BufferAttribute(sizeArray,1));
532
+ const particlesMaterial = new THREE.PointsMaterial({color:0xCCCCCC,size:0.1,sizeAttenuation:true,transparent:true,opacity:0.5});
533
+ const particles = new THREE.Points(particlesGeometry, particlesMaterial);
534
+ particles.visible = false; bodyGroup.add(particles); bodyGroup.particles = particles;
535
+ const initialRoadProps = getRoadPropertiesAtZ(0);
536
+ bodyGroup.position.set(initialRoadProps.roadCurve, 1 + initialRoadProps.roadY, 0);
537
+ }
538
+
539
+ function createAICars() {
540
+ for (let i = 0; i < 5; i++) {
541
+ createAICar(300 + i * 350, true);
542
+ }
543
+ for (let i = 0; i < 2; i++) {
544
+ createAICar(-100 - i * 150, false);
545
+ }
546
+ }
547
+
548
+ function createAICar(worldZPosition, isOncoming) {
549
+ const carColors = [0xCC0000,0x00CC00,0xCCCC00,0xCCCCCC,0x9900CC];
550
+ const car = new THREE.Mesh(new THREE.BoxGeometry(1.8,1,3.8), new THREE.MeshStandardMaterial({color:carColors[Math.floor(Math.random()*carColors.length)]}));
551
+ car.castShadow = true;
552
+ const {roadY,roadCurve} = getRoadPropertiesAtZ(worldZPosition);
553
+ const laneXOffset = isOncoming ? (roadWidth/4 + 0.25) : (-roadWidth/4 - 0.25);
554
+ car.position.set(roadCurve+laneXOffset, 1+roadY, worldZPosition);
555
+ car.rotation.y = isOncoming ? Math.PI : 0;
556
+ scene.add(car);
557
+ const headlightMat = new THREE.MeshBasicMaterial({color:0xFFFFFF});
558
+ const leftHeadlight = new THREE.Mesh(new THREE.SphereGeometry(0.2,8,8), headlightMat);
559
+ leftHeadlight.position.set(-0.7,0.3,1.9); car.add(leftHeadlight);
560
+ const rightHeadlight = new THREE.Mesh(new THREE.SphereGeometry(0.2,8,8), headlightMat);
561
+ rightHeadlight.position.set(0.7,0.3,1.9); car.add(rightHeadlight);
562
+ aiCars.push({mesh:car,speed:isOncoming?8:10,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});
563
+ }
564
+
565
+ function createPassingPlaces() {
566
+ for (let i = 0; i < 15; i++) createPassingPlace(80 + i * 130);
567
+ }
568
+
569
+ function createPassingPlace(worldZPosition) {
570
+ const side = (passingPlaces.length%2===0)?-1:1;
571
+ const {roadY,roadCurve} = getRoadPropertiesAtZ(worldZPosition);
572
+ const passingLength=20, passingWidth=6, taperLength=12;
573
+ const mainPassingGroup = new THREE.Group();
574
+ const groupXOffset = side * (roadWidth/2 + passingWidth/2);
575
+ mainPassingGroup.position.set(roadCurve+groupXOffset, roadY+0.01, worldZPosition);
576
+ scene.add(mainPassingGroup);
577
+ const passingMaterial = new THREE.MeshStandardMaterial({color:0x555555});
578
+ const mainGeom = createRoundedRectGeometry(passingWidth,passingLength,1.5);
579
+ mainGeom.rotateX(-Math.PI/2);
580
+ const mainMesh = new THREE.Mesh(mainGeom, passingMaterial);
581
+ mainPassingGroup.add(mainMesh);
582
+ const entranceTaperGeom = createSmoothTaperGeometry(taperLength,passingWidth,true,10);
583
+ entranceTaperGeom.rotateX(-Math.PI/2);
584
+ const entranceTaper = new THREE.Mesh(entranceTaperGeom, passingMaterial);
585
+ entranceTaper.position.set(0,0,-passingLength/2-taperLength/2);
586
+ if(side<0) entranceTaper.rotation.z=Math.PI;
587
+ mainPassingGroup.add(entranceTaper);
588
+ const exitTaperGeom = createSmoothTaperGeometry(taperLength,passingWidth,false,10);
589
+ exitTaperGeom.rotateX(-Math.PI/2);
590
+ const exitTaper = new THREE.Mesh(exitTaperGeom, passingMaterial);
591
+ exitTaper.position.set(0,0,passingLength/2+taperLength/2);
592
+ if(side<0) exitTaper.rotation.z=Math.PI;
593
+ mainPassingGroup.add(exitTaper);
594
+ const sign = new THREE.Mesh(new THREE.BoxGeometry(0.5,2,0.1), new THREE.MeshStandardMaterial({color:0xFFFFFF}));
595
+ const signXOffset = side * (roadWidth/2 + passingWidth + 1);
596
+ sign.position.set(roadCurve+signXOffset, 1+roadY, worldZPosition);
597
+ scene.add(sign);
598
+ const signGraphic = new THREE.Mesh(new THREE.CircleGeometry(0.3,16), new THREE.MeshBasicMaterial({color:0x000000}));
599
+ signGraphic.position.set(0,0.5,0.06);
600
+ sign.add(signGraphic);
601
+ passingPlaces.push({position:worldZPosition,side:side,width:passingWidth,length:passingLength+taperLength*2,mesh:mainMesh,group:mainPassingGroup,entranceTaper:entranceTaper,exitTaper:exitTaper,worldXCenter:roadCurve+groupXOffset});
602
+ }
603
+
604
+ function createRoundedRectGeometry(width,length,radius){
605
+ const s=new THREE.Shape(); const x=-width/2,y=-length/2;
606
+ s.moveTo(x,y+radius); s.lineTo(x,y+length-radius); s.quadraticCurveTo(x,y+length,x+radius,y+length);
607
+ s.lineTo(x+width-radius,y+length); s.quadraticCurveTo(x+width,y+length,x+width,y+length-radius);
608
+ s.lineTo(x+width,y+radius); s.quadraticCurveTo(x+width,y,x+width-radius,y);
609
+ s.lineTo(x+radius,y); s.quadraticCurveTo(x,y,x,y+radius);
610
+ return new THREE.ShapeGeometry(s,16);
611
+ }
612
+
613
+ function createSmoothTaperGeometry(length,baseWidth,isEntrance,segments){
614
+ const g=new THREE.PlaneGeometry(baseWidth,length,1,segments); const p=g.attributes.position;
615
+ for(let i=0;i<=segments;i++){
616
+ const t=i/segments; let wf=isEntrance?easeInOut(t):1-easeInOut(t);
617
+ const cw=baseWidth*wf; const lidx=i*2,ridx=i*2+1;
618
+ p.setX(lidx,-cw/2); p.setX(ridx,cw/2);
619
+ }
620
+ p.needsUpdate=true; g.computeVertexNormals(); return g;
621
+ }
622
+
623
+ function createTaperedGeometry(length,startWidth,endWidth,segments){
624
+ const piw=Math.max(startWidth,endWidth); const g=new THREE.PlaneGeometry(piw,length,1,segments);
625
+ const p=g.attributes.position;
626
+ for(let i=0;i<=segments;i++){
627
+ const t=i/segments; const et=easeInOut(t); const cw=startWidth+(endWidth-startWidth)*et;
628
+ const lidx=i*2,ridx=i*2+1;
629
+ p.setX(lidx,-cw/2); p.setX(ridx,cw/2);
630
+ }
631
+ p.needsUpdate=true; g.computeVertexNormals(); return g;
632
+ }
633
+
634
+ function createBridges() {
635
+ for (let i = 0; i < 5; i++) createBridge(300 + i * 350);
636
+ }
637
+
638
+ function createBridge(worldZPosition) {
639
+ const {roadY,roadCurve}=getRoadPropertiesAtZ(worldZPosition);
640
+ const bridgeW=6,bridgeL=20,approachL=15,taperSegs=10,approachRoadW=12,deckH=1;
641
+ const bridgeGeom = new THREE.BoxGeometry(bridgeW,deckH,bridgeL);
642
+ const bridgeMat = new THREE.MeshStandardMaterial({color:0x888888});
643
+ const bridge = new THREE.Mesh(bridgeGeom,bridgeMat);
644
+ bridge.position.set(roadCurve,roadY+deckH/2,worldZPosition);
645
+ bridge.castShadow=true; bridge.receiveShadow=true; scene.add(bridge);
646
+ const railW=0.5,railH=1; const railGeom=new THREE.BoxGeometry(railW,railH,bridgeL);
647
+ const railMat = new THREE.MeshStandardMaterial({color:0x444444});
648
+ const lRail=new THREE.Mesh(railGeom,railMat); lRail.position.set(-bridgeW/2+railW/2,railH/2,0); bridge.add(lRail);
649
+ const rRail=new THREE.Mesh(railGeom,railMat); rRail.position.set(bridgeW/2-railW/2,railH/2,0); bridge.add(rRail);
650
+ const approachMat=new THREE.MeshStandardMaterial({color:0x555555});
651
+ const nAppGeom=createTaperedGeometry(approachL,approachRoadW,bridgeW,taperSegs);
652
+ nAppGeom.rotateX(-Math.PI/2);
653
+ const nApp=new THREE.Mesh(nAppGeom,approachMat);
654
+ nApp.position.set(roadCurve,roadY+0.02,worldZPosition-bridgeL/2-approachL/2);
655
+ nApp.receiveShadow=true; scene.add(nApp);
656
+ const sAppGeom=createTaperedGeometry(approachL,bridgeW,approachRoadW,taperSegs);
657
+ sAppGeom.rotateX(-Math.PI/2);
658
+ const sApp=new THREE.Mesh(sAppGeom,approachMat);
659
+ sApp.position.set(roadCurve,roadY+0.02,worldZPosition+bridgeL/2+approachL/2);
660
+ sApp.receiveShadow=true; scene.add(sApp);
661
+ const sign=new THREE.Mesh(new THREE.BoxGeometry(0.5,2,0.1),new THREE.MeshStandardMaterial({color:0xFFFFFF}));
662
+ sign.position.set(roadCurve-(approachRoadW/2+2),1+roadY,worldZPosition-bridgeL/2-approachL-5); scene.add(sign);
663
+ const signGraphic=new THREE.Mesh(new THREE.PlaneGeometry(0.4,0.4),new THREE.MeshBasicMaterial({color:0x000000}));
664
+ signGraphic.position.set(0,0.5,0.06); sign.add(signGraphic);
665
+ bridges.push({position:worldZPosition,width:bridgeW,length:bridgeL,approachLength:approachL,mesh:bridge,northApproach:nApp,southApproach:sApp,carsWaiting:[]});
666
+ }
667
+
668
+ function easeInOut(t){return t<0.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;}
669
+
670
+ function createPotholes() {
671
+ for (let i = 0; i < 10; i++) createPothole(200 + i * 180 + Math.random()*40);
672
+ }
673
+
674
+ function createPothole(worldZPosition) {
675
+ const {roadY,roadCurve}=getRoadPropertiesAtZ(worldZPosition);
676
+ const xOff=-roadWidth/4+(Math.random()-0.5)*(roadWidth/2-1);
677
+ const potholeGeom=new THREE.CircleGeometry(0.5+Math.random()*0.3,8);
678
+ potholeGeom.rotateX(-Math.PI/2);
679
+ const pothole=new THREE.Mesh(potholeGeom,new THREE.MeshStandardMaterial({color:0x111111,roughness:0.9}));
680
+ pothole.position.set(roadCurve+xOff,roadY+0.01,worldZPosition); scene.add(pothole);
681
+ potholes.push({mesh:pothole,positionZ:worldZPosition,positionX:roadCurve+xOff,hit:false});
682
+ }
683
+
684
+ function createJumpRamps() {
685
+ const numRamps = 5;
686
+ const rampLength = 10;
687
+ const rampWidth = 4;
688
+ const rampHeight = 2; // Height at the peak of the ramp
689
+
690
+ for (let i = 0; i < numRamps; i++) {
691
+ const worldZPosition = 250 + i * (roadLength / (numRamps + 1)) + (Math.random() - 0.5) * 100; // Distribute ramps
692
+ const { roadY, roadCurve } = getRoadPropertiesAtZ(worldZPosition);
693
+
694
+ // Create ramp geometry (a wedge)
695
+ const shape = new THREE.Shape();
696
+ shape.moveTo(-rampWidth / 2, 0);
697
+ shape.lineTo(rampWidth / 2, 0);
698
+ shape.lineTo(rampWidth / 2, rampHeight); // This point defines the peak
699
+ shape.lineTo(-rampWidth / 2, rampHeight * 0.3); // Lower front part of ramp for smoother entry
700
+ shape.closePath();
701
+
702
+ const extrudeSettings = { depth: rampLength, bevelEnabled: false };
703
+ const rampGeometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
704
+
705
+ // Rotate and position the ramp
706
+ rampGeometry.rotateY(Math.PI / 2); // Rotate so length is along Z
707
+ rampGeometry.translate(0, 0, -rampLength / 2); // Center it
708
+
709
+ const rampMaterial = new THREE.MeshStandardMaterial({ color: 0x777777, roughness: 0.6 });
710
+ const ramp = new THREE.Mesh(rampGeometry, rampMaterial);
711
+
712
+ // Place ramp on the road, slightly to one side or centered
713
+ const xOffset = (Math.random() - 0.5) * (roadWidth - rampWidth) * 0.5;
714
+ ramp.position.set(roadCurve + xOffset, roadY + 0.05, worldZPosition); // +0.05 to be slightly above road
715
+ ramp.castShadow = true;
716
+ ramp.receiveShadow = true;
717
+ scene.add(ramp);
718
+ jumpRamps.push({ mesh: ramp, worldZ: worldZPosition, length: rampLength, width: rampWidth, height: rampHeight, used: false });
719
+ }
720
+ }
721
+
722
+ function createFerry() {
723
+ ferryObject = new THREE.Group();
724
+ const { roadY, roadCurve } = getRoadPropertiesAtZ(ferryPosition);
725
+
726
+ // Ferry Deck
727
+ const deckWidth = 20;
728
+ const deckLength = 40;
729
+ const deckHeight = 2;
730
+ const deckGeometry = new THREE.BoxGeometry(deckWidth, deckHeight, deckLength);
731
+ const deckMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.7 });
732
+ const deck = new THREE.Mesh(deckGeometry, deckMaterial);
733
+ deck.position.y = deckHeight / 2;
734
+ deck.receiveShadow = true;
735
+ deck.castShadow = true;
736
+ ferryObject.add(deck);
737
+
738
+ // Superstructure (Cabin)
739
+ const cabinWidth = 10;
740
+ const cabinLength = 15;
741
+ const cabinHeight = 8;
742
+ const cabinGeometry = new THREE.BoxGeometry(cabinWidth, cabinHeight, cabinLength);
743
+ const cabinMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.8 });
744
+ const cabin = new THREE.Mesh(cabinGeometry, cabinMaterial);
745
+ cabin.position.set(0, deckHeight + cabinHeight / 2, -deckLength / 4); // Position on deck towards the rear
746
+ cabin.castShadow = true;
747
+ ferryObject.add(cabin);
748
+
749
+ // Funnel
750
+ const funnelRadius = 1.5;
751
+ const funnelHeight = 7;
752
+ const funnelGeometry = new THREE.CylinderGeometry(funnelRadius, funnelRadius * 0.8, funnelHeight, 16);
753
+ const funnelMaterial = new THREE.MeshStandardMaterial({ color: 0x555555 });
754
+ const funnel = new THREE.Mesh(funnelGeometry, funnelMaterial);
755
+ funnel.position.set(0, deckHeight + cabinHeight + funnelHeight / 2 - 2, cabinLength / 3);
756
+ funnel.castShadow = true;
757
+ ferryObject.add(funnel);
758
+
759
+ // Position ferry at ocean level (-3) + deck height, so it floats properly
760
+ const oceanLevel = -3;
761
+ ferryObject.position.set(roadCurve, oceanLevel + deckHeight/2, ferryPosition + deckLength/2 + 5);
762
+ ferryObject.rotation.y = Math.PI / 2; // Sideways to the road
763
+ scene.add(ferryObject);
764
+ }
765
+
766
+ function startGame() {
767
+ document.getElementById('instructions').style.display = 'none';
768
+ gameStarted = true;
769
+ clock.start();
770
+ animate();
771
+ clearErrors();
772
+ }
773
+
774
+ function restartGame() {
775
+ gameOver = false; gameTime = 0; speed = 0; score = 0; playerHealth = 100;
776
+ goodStopsInARow = 0; isGrounded = true; airTime = 0; velocity.set(0,0,0);
777
+ suspensionCompression = 0; lastDamageTime = 0; // Reset damage timer
778
+
779
+ document.getElementById('score').textContent = `Score: ${score}`;
780
+ document.getElementById('health').style.width = '100%';
781
+ document.getElementById('health').style.backgroundColor = '#00ff00';
782
+
783
+ const startPos = getRoadPropertiesAtZ(0);
784
+ playerCar.position.set(startPos.roadCurve, 1 + startPos.roadY, 0);
785
+ playerCar.rotation.set(0,0,0);
786
+
787
+ aiCars.forEach(ai => {
788
+ const {roadY:rY, roadCurve:rC} = getRoadPropertiesAtZ(ai.initialPositionZ);
789
+ const lo = ai.isOncoming ? (roadWidth/4+0.25) : (-roadWidth/4-0.25);
790
+ ai.mesh.position.set(rC+lo, 1+rY, ai.initialPositionZ);
791
+ ai.mesh.rotation.y = ai.isOncoming ? Math.PI : 0;
792
+ Object.assign(ai, {waiting:false,honking:false,waitingAtPassingPlace:false,flashingLights:false,isOvertaking:false});
793
+ if(ai.leftHeadlight) ai.leftHeadlight.material.color.setHex(0xFFFFFF);
794
+ if(ai.rightHeadlight) ai.rightHeadlight.material.color.setHex(0xFFFFFF);
795
+ });
796
+ potholes.forEach(p => p.hit = false);
797
+ jumpRamps.forEach(r => r.used = false); // Reset used state of ramps
798
+
799
+ document.getElementById('rearView').innerHTML = '';
800
+ document.getElementById('gameOver').style.display = 'none';
801
+ document.getElementById('instructions').style.display = 'none';
802
+
803
+ clock.stop(); clock.start(); gameStarted = true;
804
+ }
805
+
806
+ function animate() {
807
+ if (gameOver && !gameStarted) { renderer.render(scene, camera); requestAnimationFrame(animate); return; }
808
+ if (!gameStarted && gameOver) { renderer.render(scene, camera); requestAnimationFrame(animate); return; }
809
+ if (!gameStarted) return;
810
+
811
+ requestAnimationFrame(animate);
812
+ const delta = clock.getDelta();
813
+ if (!gameOver) update(delta);
814
+ renderer.render(scene, camera);
815
+ if (Math.random() < 0.05) clearErrors();
816
+ }
817
+
818
+ function update(delta) {
819
+ gameTime += delta; updateTimer(); updateClouds(delta);
820
+ if (playerCar.position.z >= ferryPosition) { endGame(true); return; }
821
+ if (gameTime > 120) { showMessage("Time's up! You missed the ferry!", 5); endGame(false, "Time's up!"); return; }
822
+
823
+ updatePlayerCar(delta); updateAICars(delta); checkCollisions(); updateCamera();
824
+ updateHealthRegen(delta); // Add health regeneration
825
+
826
+ if (!isGrounded) {
827
+ document.getElementById('airTime').textContent = `Air Time: ${airTime.toFixed(1)}s`;
828
+ document.getElementById('airTime').style.display = 'block';
829
+ } else { document.getElementById('airTime').style.display = 'none'; }
830
+ updateParticleEffects(delta);
831
+ }
832
+
833
+ function updateParticleEffects(delta) {
834
+ if (playerCar.particles) {
835
+ const particles = playerCar.particles; const particlePositions = particles.geometry.attributes.position;
836
+ const showParticles = (isGrounded && Math.abs(speed)>15 && (keys.ArrowLeft||keys.ArrowRight||keys.a||keys.d)) ||
837
+ (!isGrounded && Math.abs(velocity.y)<0.1 && airTime>0.1 && playerCar.position.y < lastRoadY+1.5) ||
838
+ (isGrounded && Math.abs(speed)>25 && (keys.ArrowUp||keys.w));
839
+ if (showParticles) {
840
+ particles.visible = true;
841
+ for (let i=0; i<particlePositions.count; i++) {
842
+ const wheelIdx=(Math.floor(Math.random()*2)+2), wheelX=(wheelIdx===2)?-1:1, wheelZ=-1.5;
843
+ const xOff=(Math.random()-0.5)*0.5, yOff=Math.random()*0.1-0.2, zOff=(Math.random()-0.5)*0.5-0.3;
844
+ particlePositions.setXYZ(i,wheelX+xOff,yOff,wheelZ+zOff);
845
+ }
846
+ particlePositions.needsUpdate = true;
847
+ } else { particles.visible = false; }
848
+ }
849
+ }
850
+
851
+ function updatePlayerCar(delta) {
852
+ updateCarPhysics(delta);
853
+ if((keys.ArrowUp||keys.w)&&!gameOver) speed+=acceleration*(isGrounded?1.2:0.3);
854
+ else if((keys.ArrowDown||keys.s)&&!gameOver) speed-=braking*(isGrounded?1.2:0.3);
855
+ else { if(isGrounded)speed*=0.98; else speed*=0.995; if(Math.abs(speed)<0.05)speed=0; }
856
+
857
+ if((keys[' ']||keys.j)&&isGrounded&&Math.abs(speed)>10){
858
+ jumpForce=1.2+Math.abs(speed)*0.06+suspensionCompression*6; // Increased base and scaling
859
+ velocity.y=jumpForce; isGrounded=false; showMessage("Jumping!",1);
860
+ }
861
+
862
+ speed=Math.max(-maxSpeed/2,Math.min(maxSpeed,speed));
863
+ document.getElementById('speedometer').textContent=`Speed: ${Math.abs(Math.round(speed))} mph`;
864
+ const actualMoveSpeed=speed*delta*2.5;
865
+ const steeringInput=(keys.ArrowLeft||keys.a)?1:(keys.ArrowRight||keys.d)?-1:0;
866
+ if(steeringInput!==0&&Math.abs(speed)>0.1){
867
+ const steerEff=isGrounded?1.0:0.3; const turnRate=steering*steerEff*Math.abs(speed/maxSpeed)*2.0;
868
+ playerCar.rotation.y+=steeringInput*turnRate*Math.sign(speed);
869
+ }
870
+ playerCar.position.x+=Math.sin(playerCar.rotation.y)*actualMoveSpeed;
871
+ playerCar.position.z+=Math.cos(playerCar.rotation.y)*actualMoveSpeed;
872
+ if(!isGrounded){
873
+ const airTilt=Math.min(Math.max(velocity.y*0.1,-0.3),0.3);
874
+ playerCar.rotation.x=airTilt;
875
+ playerCar.rotation.z+=steeringInput*speed*0.0005;
876
+ playerCar.rotation.z=Math.max(-0.3,Math.min(0.3,playerCar.rotation.z));
877
+ }else{playerCar.rotation.x*=0.8; playerCar.rotation.z*=0.8;}
878
+ const {roadY:currentRoadY,roadCurve:currentRoadCurve}=getRoadPropertiesAtZ(playerCar.position.z);
879
+ if(isGrounded) playerCar.position.y=1+currentRoadY+suspensionCompression;
880
+ if(isGrounded){
881
+ const latOff=playerCar.position.x-currentRoadCurve; const maxOff=roadWidth/2+0.5;
882
+ if(Math.abs(latOff)>maxOff){
883
+ playerCar.position.x-=Math.sign(latOff)*0.1*Math.abs(latOff-maxOff);
884
+ if(Math.abs(latOff)>maxOff+1.0)speed*=0.95;
885
+ }
886
+ }
887
+ }
888
+
889
+ function updateCarPhysics(delta) {
890
+ const {roadY: groundHeightAtCar}=getRoadPropertiesAtZ(playerCar.position.z);
891
+ const carEffectiveRadius=0.5;
892
+
893
+ // Check for jump ramp interaction
894
+ let onRamp = false;
895
+ jumpRamps.forEach(ramp => {
896
+ const distToRampZ = Math.abs(playerCar.position.z - ramp.mesh.position.z);
897
+ const distToRampX = Math.abs(playerCar.position.x - ramp.mesh.position.x);
898
+ if (distToRampZ < ramp.length / 2 && distToRampX < ramp.width / 2 && playerCar.position.y < ramp.mesh.position.y + ramp.height + carEffectiveRadius) {
899
+ onRamp = true;
900
+ if (!ramp.used && isGrounded) { // Only trigger jump once and if grounded
901
+ velocity.y = ramp.height * 1.5 + Math.abs(speed) * 0.1; // Ramp jump force
902
+ isGrounded = false;
903
+ ramp.used = true; // Mark ramp as used for this jump
904
+ showMessage("Ramp Jump!", 2);
905
+ score += 150; // Bonus points for ramp jump
906
+ document.getElementById('score').textContent = `Score: ${score}`;
907
+ setTimeout(() => { ramp.used = false; }, 3000); // Allow reuse after a delay
908
+ }
909
+ }
910
+ });
911
+
912
+
913
+ if (isGrounded && !onRamp) { // Don't apply ground physics if on ramp and about to jump
914
+ const elevationChange = groundHeightAtCar - lastRoadY;
915
+ suspensionCompression = -elevationChange * suspensionStrength;
916
+ suspensionCompression = Math.max(-0.3, Math.min(0.3, suspensionCompression));
917
+ playerCar.position.y = groundHeightAtCar + carEffectiveRadius + suspensionCompression;
918
+ velocity.y = 0;
919
+ } else {
920
+ velocity.y -= gravity * delta * 20;
921
+ playerCar.position.y += velocity.y * delta * 5;
922
+ airTime += delta;
923
+
924
+ if (playerCar.position.y <= groundHeightAtCar + carEffectiveRadius && velocity.y < 0 && !onRamp) { // Land only if not on ramp
925
+ playerCar.position.y = groundHeightAtCar + carEffectiveRadius;
926
+ isGrounded = true;
927
+ const impactForce = Math.abs(velocity.y);
928
+ velocity.y = 0; airTime = 0;
929
+ if (impactForce > 0.5) { // Increased threshold for damage
930
+ decreaseHealth(Math.floor(impactForce*8),`Hard landing!`); // Reduced damage multiplier
931
+ suspensionCompression = Math.min(impactForce*0.2,0.4);
932
+ } else { suspensionCompression = Math.min(impactForce*0.1,0.1); }
933
+ }
934
+ }
935
+ lastRoadY = groundHeightAtCar;
936
+ playerCar.children.forEach(c=>{if(c.isWheel)c.position.y=0.5+suspensionCompression*0.5;});
937
+ }
938
+
939
+ function updateAICars(delta) {
940
+ carsBehind = [];
941
+ aiCars.forEach(ai => {
942
+ const carM=ai.mesh; let curSpd=ai.waiting||ai.waitingAtPassingPlace?0:ai.speed;
943
+ const moveDist=curSpd*(ai.isOncoming?-1:1)*delta*2.5; carM.position.z+=moveDist;
944
+ const {roadY,roadCurve}=getRoadPropertiesAtZ(carM.position.z);
945
+ let tLaneXOff=ai.isOncoming?(roadWidth/4):(-roadWidth/4);
946
+ if(ai.isOvertaking)tLaneXOff=ai.isOncoming?(-roadWidth/4):(roadWidth/4);
947
+ const tX=roadCurve+tLaneXOff; carM.position.x+=(tX-carM.position.x)*0.1; carM.position.y=1+roadY;
948
+ if(ai.isOncoming){
949
+ const dToP=playerCar.position.distanceTo(carM.position);
950
+ if(dToP<40&&!ai.waitingAtPassingPlace){
951
+ let pYield=isPlayerInPassingPlaceForOncoming(ai);
952
+ if(!pYield&&ai.politeness>0.5){
953
+ let canAIPull=false;
954
+ 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;}});
955
+ if(!canAIPull)ai.waiting=true;
956
+ }
957
+ }else if((ai.waiting||ai.waitingAtPassingPlace)&&dToP>50){ai.waiting=false;ai.waitingAtPassingPlace=false;ai.flashingLights=false;}
958
+ }else{
959
+ const distBPlayer=playerCar.position.z-carM.position.z;
960
+ if(distBPlayer>5&&distBPlayer<30&&speed<ai.speed*0.8&&!ai.isOvertaking){
961
+ let canOvertake=false;
962
+ 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;}});
963
+ if(canOvertake){ai.isOvertaking=true;ai.flashingLights=false;setTimeout(()=>{ai.isOvertaking=false;},5000);}
964
+ else if(!ai.waitingAtPassingPlace)ai.flashingLights=true;
965
+ }else if(ai.flashingLights&&distBPlayer>50)ai.flashingLights=false;
966
+ }
967
+ 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);}
968
+ else{if(ai.leftHeadlight)ai.leftHeadlight.material.color.setHex(0xFFFFFF); if(ai.rightHeadlight)ai.rightHeadlight.material.color.setHex(0xFFFFFF);}
969
+ if(!ai.isOncoming&&carM.position.z<playerCar.position.z&&carM.position.z>playerCar.position.z-50)carsBehind.push(ai);
970
+ if(Math.abs(carM.position.z-(roadLength/2))>roadLength/2+100){
971
+ const iZ=ai.initialPositionZ; const{roadY:iRY,roadCurve:iRC}=getRoadPropertiesAtZ(iZ);
972
+ const lo=ai.isOncoming?(roadWidth/4+0.25):(-roadWidth/4-0.25);
973
+ carM.position.set(iRC+lo,1+iRY,iZ); Object.assign(ai,{waiting:false,waitingAtPassingPlace:false,isOvertaking:false,flashingLights:false});
974
+ }
975
+ });
976
+ updateRearViewMirror();
977
+ }
978
+
979
+ function isPlayerInPassingPlaceForOncoming(oncomingAICar) {
980
+ const pZ=playerCar.position.z,pX=playerCar.position.x; const{roadCurve:pRC}=getRoadPropertiesAtZ(pZ);
981
+ for(const pp of passingPlaces){
982
+ if(Math.abs(pZ-pp.position)<pp.length/2){
983
+ if(pp.side===-1){if(pX<pRC-roadWidth/4&&Math.abs(speed)<5)return true;}
984
+ }
985
+ }return false;
986
+ }
987
+
988
+ 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);}}
989
+
990
+ function updateRearViewMirror(){
991
+ const rvEl=document.getElementById('rearView'); rvEl.innerHTML='';
992
+ carsBehind.sort((a,b)=>(playerCar.position.z-a.mesh.position.z)-(playerCar.position.z-b.mesh.position.z));
993
+ for(let i=0;i<Math.min(carsBehind.length,3);i++){
994
+ const cd=carsBehind[i]; const dist=playerCar.position.z-cd.mesh.position.z;
995
+ const ind=document.createElement('div'); ind.style.position='absolute';
996
+ ind.style.width=`${Math.max(5,30-dist*0.5)}px`; ind.style.height=`${Math.max(3,20-dist*0.3)}px`;
997
+ ind.style.backgroundColor=cd.flashingLights?(Math.sin(Date.now()*0.01)>0?'#ffff00':'#cc0000'):'#cc0000';
998
+ ind.style.borderRadius='3px';
999
+ ind.style.left=`${(rvEl.offsetWidth/2)-(parseFloat(ind.style.width)/2)+(i-Math.floor(Math.min(carsBehind.length,3)/2))*35}px`;
1000
+ ind.style.bottom=`${5+Math.max(0,20-dist*0.8)}px`; ind.style.zIndex=50-Math.floor(dist);
1001
+ rvEl.appendChild(ind);
1002
+ }
1003
+ }
1004
+
1005
+ function checkCollisions() {
1006
+ const playerBox = new THREE.Box3().setFromObject(playerCar);
1007
+ 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!");}}}});
1008
+ aiCars.forEach(aiD=>{const aiB=new THREE.Box3().setFromObject(aiD.mesh);if(playerBox.intersectsBox(aiB)){decreaseHealth(35,"Collided!");playerCar.position.z-=Math.sign(speed)*2.5;speed*=0.1;aiD.waiting=true;setTimeout(()=>{if(aiD)aiD.waiting=false;},2500);}});
1009
+ }
1010
+
1011
+ function updateHealthRegen(delta) {
1012
+ // Regenerate health slowly if no recent damage (after 3 seconds of no damage)
1013
+ if (gameTime - lastDamageTime > 3 && playerHealth < 100) {
1014
+ playerHealth = Math.min(100, playerHealth + healthRegenRate * delta);
1015
+ document.getElementById('health').style.width = `${playerHealth}%`;
1016
+
1017
+ // Update health bar color
1018
+ if (playerHealth < 30) document.getElementById('health').style.backgroundColor = '#ff0000';
1019
+ else if (playerHealth < 60) document.getElementById('health').style.backgroundColor = '#ffff00';
1020
+ else document.getElementById('health').style.backgroundColor = '#00ff00';
1021
+ }
1022
+ }
1023
+
1024
+ function decreaseHealth(amount,message){
1025
+ playerHealth-=amount;
1026
+ playerHealth=Math.max(0,playerHealth);
1027
+ lastDamageTime = gameTime; // Track when damage occurred
1028
+
1029
+ document.getElementById('health').style.width=`${playerHealth}%`;
1030
+ if(playerHealth<30)document.getElementById('health').style.backgroundColor='#ff0000';
1031
+ else if(playerHealth<60)document.getElementById('health').style.backgroundColor='#ffff00';
1032
+ else document.getElementById('health').style.backgroundColor='#00ff00';
1033
+ if(message)showMessage(message,3);
1034
+ if(playerHealth<=0&&!gameOver)endGame(false,"Car too damaged!");
1035
+ }
1036
+
1037
+ 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);}
1038
+
1039
+ function updateTimer(){const m=Math.floor(gameTime/60),s=Math.floor(gameTime%60);document.getElementById('timer').textContent=`Time: ${m}:${s<10?'0':''}${s}`;}
1040
+
1041
+ function showMessage(text,duration){const msgEl=document.getElementById('message');msgEl.textContent=text;msgEl.style.opacity=1;setTimeout(()=>{msgEl.style.opacity=0;},duration*1000);clearErrors();}
1042
+
1043
+ 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='';}}}
1044
+
1045
+ 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';}
1046
+
1047
+ function onWindowResize(){camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);}
1048
+
1049
+ 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;}
1050
+ 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;}
1051
+
1052
+ init();
1053
+ </script>
1054
+ </body>
1055
+ </html>