awacke1 commited on
Commit
a6fd2b6
·
verified ·
1 Parent(s): 66bc588

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1009 -19
index.html CHANGED
@@ -1,19 +1,1009 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 goodStopsInARow = 0;
189
+ let carsBehind = [];
190
+
191
+ // Physics variables
192
+ let gravity = 0.25;
193
+ let velocity = new THREE.Vector3(0, 0, 0);
194
+ let isGrounded = true;
195
+ let airTime = 0;
196
+ let jumpForce = 0;
197
+ let lastRoadY = 0;
198
+ let suspensionCompression = 0;
199
+ let suspensionStrength = 0.3;
200
+ let suspensionDamping = 0.8;
201
+
202
+ const keys = {
203
+ ArrowUp: false, ArrowDown: false, ArrowLeft: false, ArrowRight: false,
204
+ w: false, a: false, s: false, d: false,
205
+ ' ': false, j: false
206
+ };
207
+
208
+ function init() {
209
+ const bodyChildNodes = document.body.childNodes;
210
+ for (let i = bodyChildNodes.length - 1; i >= 0; i--) {
211
+ const node = bodyChildNodes[i];
212
+ if (node.nodeType === Node.TEXT_NODE && node.textContent.trim() !== '' &&
213
+ (node.textContent.includes('function') || node.textContent.includes('var') || node.textContent.includes('let') || node.textContent.includes('const'))) {
214
+ node.textContent = '';
215
+ }
216
+ }
217
+
218
+ scene = new THREE.Scene();
219
+ scene.background = new THREE.Color(0x87CEEB);
220
+
221
+ camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 3000); // Increased far plane
222
+ camera.position.set(0, 5, -10);
223
+ camera.lookAt(0, 0, 10);
224
+
225
+ renderer = new THREE.WebGLRenderer({ antialias: true });
226
+ renderer.setSize(window.innerWidth, window.innerHeight);
227
+ renderer.shadowMap.enabled = true;
228
+ renderer.domElement.id = 'game-canvas';
229
+ document.body.appendChild(renderer.domElement);
230
+
231
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
232
+ scene.add(ambientLight);
233
+
234
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.9);
235
+ directionalLight.position.set(100, 150, 75);
236
+ directionalLight.castShadow = true;
237
+ directionalLight.shadow.mapSize.width = 2048;
238
+ directionalLight.shadow.mapSize.height = 2048;
239
+ directionalLight.shadow.camera.near = 0.5;
240
+ directionalLight.shadow.camera.far = 500;
241
+ directionalLight.shadow.camera.left = -100;
242
+ directionalLight.shadow.camera.right = 100;
243
+ directionalLight.shadow.camera.top = 100;
244
+ directionalLight.shadow.camera.bottom = -100;
245
+ scene.add(directionalLight);
246
+
247
+ createTerrain();
248
+ createRoad();
249
+ createPlayerCar();
250
+ createAICars();
251
+ createPassingPlaces();
252
+ createBridges();
253
+ createPotholes();
254
+ createOcean();
255
+ createClouds();
256
+ createJumpRamps(); // Add jump ramps
257
+ createFerry(); // Add the ferry
258
+
259
+ clock = new THREE.Clock();
260
+
261
+ window.addEventListener('resize', onWindowResize);
262
+ window.addEventListener('keydown', onKeyDown);
263
+ window.addEventListener('keyup', onKeyUp);
264
+
265
+ document.getElementById('startButton').addEventListener('click', startGame);
266
+ document.getElementById('restartButton').addEventListener('click', restartGame);
267
+ }
268
+
269
+ function getRoadPropertiesAtZ(worldZPos) {
270
+ const localZ = worldZPos - (roadLength / 2);
271
+ const roadCurve = Math.sin(localZ * 0.005) * 15;
272
+
273
+ // Gradually reduce elevation variation as we approach the ferry (coastal approach)
274
+ const distanceToFerry = Math.max(0, ferryPosition - worldZPos);
275
+ const coastalFactor = Math.min(1, distanceToFerry / 400); // Start flattening 400 units before ferry
276
+ const elevationAmplitude = coastalFactor * 0.5;
277
+
278
+ const roadY = (Math.sin(localZ * 0.01) * 5 + Math.sin(localZ * 0.03) * 2) * elevationAmplitude;
279
+ return { roadY, roadCurve };
280
+ }
281
+
282
+ function createTerrain() {
283
+ const terrainWidth = 1000;
284
+ const terrainSegmentsX = 150;
285
+ const terrainSegmentsZ = 300;
286
+ const groundGeometry = new THREE.PlaneGeometry(terrainWidth, roadLength, terrainSegmentsX, terrainSegmentsZ);
287
+ groundGeometry.rotateX(-Math.PI / 2);
288
+
289
+ const vertices = groundGeometry.attributes.position;
290
+ for (let i = 0; i < vertices.count; i++) {
291
+ const x_local = vertices.getX(i);
292
+ const z_local = vertices.getZ(i);
293
+ const worldZ = z_local + roadLength / 2;
294
+ const { roadY: actualRoadY, roadCurve: actualRoadCurve } = getRoadPropertiesAtZ(worldZ);
295
+
296
+ let height = actualRoadY;
297
+ const distFromRoadCenter = Math.abs(x_local - actualRoadCurve);
298
+ const roadEdgeBuffer = 5;
299
+
300
+ // Calculate coastal approach factor - mountains recede as we near the ferry
301
+ const distanceToFerry = Math.max(0, ferryPosition - worldZ);
302
+ const coastalTransition = Math.min(1, distanceToFerry / 600); // Start transition 600 units before ferry
303
+ const mountainHeightFactor = coastalTransition;
304
+
305
+ // Also create asymmetric coastal effect - mountains more on one side
306
+ const ferryApproachFactor = 1 - Math.max(0, Math.min(1, (ferryPosition - worldZ) / 800));
307
+ const coastalAsymmetry = ferryApproachFactor * Math.max(0, 1 - Math.abs(x_local + actualRoadCurve) / 200);
308
+
309
+ if (distFromRoadCenter > (roadWidth / 2 + roadEdgeBuffer)) {
310
+ const mountainBaseHeight = 10 + Math.abs(Math.sin(z_local * 0.001 + x_local * 0.005)) * 20;
311
+ const mountainDetail = Math.sin(z_local * 0.02 + x_local * 0.03) * 15 + Math.cos(z_local * 0.015) * 10;
312
+ let mountainOffset = mountainBaseHeight + mountainDetail;
313
+ const riseFactor = Math.min((distFromRoadCenter - (roadWidth / 2 + roadEdgeBuffer)) * 0.2, 1.0) + 0.5;
314
+ mountainOffset *= riseFactor;
315
+ const glenFactor = 0.6 + (Math.sin(z_local * 0.004) * 0.5 + 0.5) * 0.4;
316
+ mountainOffset *= glenFactor;
317
+
318
+ // Apply coastal factors to reduce mountain height near ferry
319
+ mountainOffset *= mountainHeightFactor;
320
+ mountainOffset *= (1 - coastalAsymmetry * 0.7); // Reduce mountains more on ocean side
321
+
322
+ height += Math.max(0, mountainOffset);
323
+ }
324
+ else if (distFromRoadCenter > roadWidth / 2) {
325
+ height -= (distFromRoadCenter - roadWidth/2) * 0.5;
326
+ }
327
+
328
+ // Ensure terrain near ferry is at reasonable coastal elevation
329
+ if (worldZ > ferryPosition - 200) {
330
+ const coastalBlend = (worldZ - (ferryPosition - 200)) / 200;
331
+ const targetCoastalHeight = -1; // Slightly below sea level for realism
332
+ height = height * (1 - coastalBlend) + targetCoastalHeight * coastalBlend;
333
+ }
334
+
335
+ vertices.setY(i, height);
336
+ }
337
+ groundGeometry.attributes.position.needsUpdate = true;
338
+ groundGeometry.computeVertexNormals();
339
+
340
+ const groundMaterial = new THREE.MeshStandardMaterial({
341
+ color: 0x365E36, flatShading: true, roughness: 0.9, metalness: 0.1
342
+ });
343
+
344
+ terrain = new THREE.Mesh(groundGeometry, groundMaterial);
345
+ terrain.position.z = roadLength / 2;
346
+ terrain.receiveShadow = true;
347
+ scene.add(terrain);
348
+ }
349
+
350
+ function createRoad() {
351
+ const roadGeometry = new THREE.PlaneGeometry(roadWidth, roadLength, 1, 200);
352
+ roadGeometry.rotateX(-Math.PI / 2);
353
+
354
+ const vertices = roadGeometry.attributes.position;
355
+ for (let i = 0; i < vertices.count; i++) {
356
+ const localZ = vertices.getZ(i);
357
+ const curve_val = Math.sin(localZ * 0.005) * 15;
358
+ vertices.setX(i, roadGeometry.attributes.position.getX(i) + curve_val);
359
+ const elevation_val = (Math.sin(localZ * 0.01) * 5 + Math.sin(localZ * 0.03) * 2) * 0.5;
360
+ vertices.setY(i, elevation_val);
361
+ }
362
+ roadGeometry.attributes.position.needsUpdate = true;
363
+ roadGeometry.computeVertexNormals();
364
+
365
+ const roadMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 });
366
+ road = new THREE.Mesh(roadGeometry, roadMaterial);
367
+ road.position.z = roadLength / 2;
368
+ road.receiveShadow = true;
369
+ scene.add(road);
370
+
371
+ const centerLineGeometry = new THREE.PlaneGeometry(0.1, roadLength, 1, 200);
372
+ centerLineGeometry.rotateX(-Math.PI / 2);
373
+
374
+ const lineVertices = centerLineGeometry.attributes.position;
375
+ for (let i = 0; i < lineVertices.count; i++) {
376
+ const localZ = lineVertices.getZ(i);
377
+ const curve_val = Math.sin(localZ * 0.005) * 15;
378
+ lineVertices.setX(i, lineVertices.getX(i) + curve_val);
379
+ const elevation_val = (Math.sin(localZ * 0.01) * 5 + Math.sin(localZ * 0.03) * 2) * 0.5;
380
+ lineVertices.setY(i, elevation_val + 0.01);
381
+ }
382
+ centerLineGeometry.attributes.position.needsUpdate = true;
383
+ centerLineGeometry.computeVertexNormals();
384
+
385
+ const centerLineMaterial = new THREE.MeshStandardMaterial({ color: 0xFFFFFF });
386
+ const centerLine = new THREE.Mesh(centerLineGeometry, centerLineMaterial);
387
+ centerLine.position.z = roadLength / 2;
388
+ scene.add(centerLine);
389
+ }
390
+
391
+ function createOcean() {
392
+ const oceanSize = 4000;
393
+ const oceanGeometry = new THREE.PlaneGeometry(oceanSize, oceanSize);
394
+ const oceanMaterial = new THREE.MeshStandardMaterial({
395
+ color: 0x0077be, transparent: true, opacity: 0.85, roughness: 0.3, metalness: 0.1,
396
+ });
397
+ oceanPlane = new THREE.Mesh(oceanGeometry, oceanMaterial);
398
+ oceanPlane.rotation.x = -Math.PI / 2;
399
+ const ferryRoadProps = getRoadPropertiesAtZ(ferryPosition);
400
+ oceanPlane.position.set(ferryRoadProps.roadCurve, ferryRoadProps.roadY - 2, ferryPosition + 50);
401
+ oceanPlane.receiveShadow = true;
402
+ scene.add(oceanPlane);
403
+ }
404
+
405
+ function createClouds() {
406
+ const cloudMaterial = new THREE.MeshBasicMaterial({
407
+ color: 0xffffff, transparent: true, opacity: 0.6, depthWrite: false
408
+ });
409
+ const numClouds = 15;
410
+ const skyHeight = 150;
411
+ const skyDepthRange = 1000;
412
+ const skyWidthRange = 2000;
413
+
414
+ for (let i = 0; i < numClouds; i++) {
415
+ const cloudWidth = 100 + Math.random() * 200;
416
+ const cloudHeight = 50 + Math.random() * 100;
417
+ const cloudGeometry = new THREE.PlaneGeometry(cloudWidth, cloudHeight);
418
+ const cloud = new THREE.Mesh(cloudGeometry, cloudMaterial);
419
+ cloud.position.set(
420
+ (Math.random() - 0.5) * skyWidthRange,
421
+ skyHeight + (Math.random() - 0.5) * 50,
422
+ (Math.random() * skyDepthRange) - skyDepthRange / 4
423
+ );
424
+ cloud.rotation.y = (Math.random() - 0.5) * 0.5;
425
+ // cloud.lookAt(camera.position); // Initial orientation
426
+ cloud.userData.speed = 0.5 + Math.random() * 1;
427
+ clouds.push(cloud);
428
+ scene.add(cloud);
429
+ }
430
+ }
431
+
432
+ function updateClouds(delta) {
433
+ const wrapAroundX = 2200;
434
+ clouds.forEach(cloud => {
435
+ cloud.position.x += cloud.userData.speed * delta * 5;
436
+ if (cloud.position.x > wrapAroundX / 2) {
437
+ cloud.position.x = -wrapAroundX / 2;
438
+ cloud.position.z = (Math.random() * 1000) - 500 + playerCar.position.z; // Re-position relative to player Z
439
+ }
440
+ // Make clouds face the general direction of the camera's Z but not directly lookAt
441
+ const targetZ = camera.position.z + 500; // A point far in front of camera
442
+ const direction = new THREE.Vector3(cloud.position.x, cloud.position.y, targetZ);
443
+ cloud.lookAt(direction);
444
+
445
+ });
446
+ }
447
+
448
+ function createPlayerCar() {
449
+ const bodyGroup = new THREE.Group();
450
+ scene.add(bodyGroup);
451
+ playerCar = bodyGroup;
452
+ const bodyGeometry = new THREE.BoxGeometry(2, 1, 4);
453
+ bodyGeometry.translate(0, 0.5, 0);
454
+ const carMaterial = new THREE.MeshStandardMaterial({ color: 0x3366FF, roughness: 0.5, metalness: 0.7 });
455
+ const carBody = new THREE.Mesh(bodyGeometry, carMaterial);
456
+ carBody.castShadow = true;
457
+ bodyGroup.add(carBody);
458
+ const windshieldGeometry = new THREE.CylinderGeometry(1, 1, 1.8, 16, 1, false, 0, Math.PI);
459
+ windshieldGeometry.rotateZ(Math.PI / 2); windshieldGeometry.rotateY(Math.PI / 2);
460
+ windshieldGeometry.scale(1, 0.4, 0.8); windshieldGeometry.translate(0, 1.1, 0.5);
461
+ const windshieldMaterial = new THREE.MeshStandardMaterial({ color: 0xAACCFF, transparent: true, opacity: 0.7, roughness: 0.1, metalness: 0.2 });
462
+ const windshield = new THREE.Mesh(windshieldGeometry, windshieldMaterial);
463
+ bodyGroup.add(windshield);
464
+ const frontBumperGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2, 16, 1, false, -Math.PI/2, Math.PI);
465
+ frontBumperGeometry.rotateZ(Math.PI / 2); frontBumperGeometry.scale(1, 0.5, 0.5); frontBumperGeometry.translate(0, 0.5, 2);
466
+ const bumperMaterial = new THREE.MeshStandardMaterial({ color: 0x2255DD, roughness: 0.7, metalness: 0.3 });
467
+ const frontBumper = new THREE.Mesh(frontBumperGeometry, bumperMaterial);
468
+ frontBumper.castShadow = true; bodyGroup.add(frontBumper);
469
+ const rearBumperGeometry = new THREE.CylinderGeometry(0.5, 0.5, 2, 16, 1, false, Math.PI/2, Math.PI);
470
+ rearBumperGeometry.rotateZ(Math.PI / 2); rearBumperGeometry.scale(1, 0.5, 0.5); rearBumperGeometry.translate(0, 0.5, -2);
471
+ const rearBumper = new THREE.Mesh(rearBumperGeometry, bumperMaterial);
472
+ rearBumper.castShadow = true; bodyGroup.add(rearBumper);
473
+ const wheelGeometry = new THREE.CylinderGeometry(0.5, 0.5, 0.3, 24);
474
+ wheelGeometry.rotateZ(Math.PI / 2);
475
+ const wheelMaterial = new THREE.MeshStandardMaterial({ color: 0x111111, roughness: 0.9, metalness: 0.2 });
476
+ const hubCapGeometry = new THREE.CircleGeometry(0.3, 16);
477
+ const hubCapMaterial = new THREE.MeshStandardMaterial({ color: 0xCCCCCC, roughness: 0.5, metalness: 0.8 });
478
+ const wheelsInfo = [
479
+ { x: -1, z: 1.5, hubCapX: 0.16, hubCapRotY: Math.PI / 2 }, { x: 1, z: 1.5, hubCapX: -0.16, hubCapRotY: -Math.PI / 2 },
480
+ { x: -1, z: -1.5, hubCapX: 0.16, hubCapRotY: Math.PI / 2 }, { x: 1, z: -1.5, hubCapX: -0.16, hubCapRotY: -Math.PI / 2 }
481
+ ];
482
+ wheelsInfo.forEach(info => {
483
+ const wheel = new THREE.Mesh(wheelGeometry, wheelMaterial);
484
+ wheel.position.set(info.x, 0.5, info.z); wheel.isWheel = true;
485
+ const hubCap = new THREE.Mesh(hubCapGeometry, hubCapMaterial);
486
+ hubCap.position.set(info.hubCapX, 0, 0); hubCap.rotation.y = info.hubCapRotY;
487
+ wheel.add(hubCap); bodyGroup.add(wheel);
488
+ });
489
+ const springMaterial = new THREE.MeshStandardMaterial({ color: 0x888888 });
490
+ function createSpring(x, z_offset) {
491
+ const springGroup = new THREE.Group(); const coilCount = 5; const coilHeight = 0.08;
492
+ for (let i = 0; i < coilCount; i++) {
493
+ const coil = new THREE.Mesh(new THREE.TorusGeometry(0.15, 0.03, 8, 16), springMaterial);
494
+ coil.position.y = i * coilHeight; springGroup.add(coil);
495
+ }
496
+ springGroup.position.set(x, 0.2, z_offset); return springGroup;
497
+ }
498
+ bodyGroup.add(createSpring(-0.8, 1.5)); bodyGroup.add(createSpring(0.8, 1.5));
499
+ bodyGroup.add(createSpring(-0.8, -1.5)); bodyGroup.add(createSpring(0.8, -1.5));
500
+ const mirrorBase = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.1, 0.1), carMaterial);
501
+ mirrorBase.position.set(0, 1.3, -0.5); bodyGroup.add(mirrorBase);
502
+ const mirrorGeo = new THREE.CylinderGeometry(0.1, 0.1, 1.8, 16, 1, true, 0, Math.PI);
503
+ mirrorGeo.rotateX(Math.PI / 2); mirrorGeo.translate(0, 0.1, 0);
504
+ const mirror = new THREE.Mesh(mirrorGeo, new THREE.MeshStandardMaterial({ color: 0x444444, roughness: 0.3, metalness: 0.8 }));
505
+ mirror.position.set(0, 1.3, -0.5); bodyGroup.add(mirror);
506
+ const particlesCount = 50; const particlesGeometry = new THREE.BufferGeometry();
507
+ const posArray = new Float32Array(particlesCount * 3); const sizeArray = new Float32Array(particlesCount);
508
+ for (let i = 0; i < particlesCount; i++) {
509
+ posArray[i*3]=0; posArray[i*3+1]=0; posArray[i*3+2]=0; sizeArray[i]=Math.random()*0.2;
510
+ }
511
+ particlesGeometry.setAttribute('position', new THREE.BufferAttribute(posArray,3));
512
+ particlesGeometry.setAttribute('size', new THREE.BufferAttribute(sizeArray,1));
513
+ const particlesMaterial = new THREE.PointsMaterial({color:0xCCCCCC,size:0.1,sizeAttenuation:true,transparent:true,opacity:0.5});
514
+ const particles = new THREE.Points(particlesGeometry, particlesMaterial);
515
+ particles.visible = false; bodyGroup.add(particles); bodyGroup.particles = particles;
516
+ const initialRoadProps = getRoadPropertiesAtZ(0);
517
+ bodyGroup.position.set(initialRoadProps.roadCurve, 1 + initialRoadProps.roadY, 0);
518
+ }
519
+
520
+ function createAICars() {
521
+ for (let i = 0; i < 5; i++) {
522
+ createAICar(300 + i * 350, true);
523
+ }
524
+ for (let i = 0; i < 2; i++) {
525
+ createAICar(-100 - i * 150, false);
526
+ }
527
+ }
528
+
529
+ function createAICar(worldZPosition, isOncoming) {
530
+ const carColors = [0xCC0000,0x00CC00,0xCCCC00,0xCCCCCC,0x9900CC];
531
+ const car = new THREE.Mesh(new THREE.BoxGeometry(1.8,1,3.8), new THREE.MeshStandardMaterial({color:carColors[Math.floor(Math.random()*carColors.length)]}));
532
+ car.castShadow = true;
533
+ const {roadY,roadCurve} = getRoadPropertiesAtZ(worldZPosition);
534
+ const laneXOffset = isOncoming ? (roadWidth/4 + 0.25) : (-roadWidth/4 - 0.25);
535
+ car.position.set(roadCurve+laneXOffset, 1+roadY, worldZPosition);
536
+ car.rotation.y = isOncoming ? Math.PI : 0;
537
+ scene.add(car);
538
+ const headlightMat = new THREE.MeshBasicMaterial({color:0xFFFFFF});
539
+ const leftHeadlight = new THREE.Mesh(new THREE.SphereGeometry(0.2,8,8), headlightMat);
540
+ leftHeadlight.position.set(-0.7,0.3,1.9); car.add(leftHeadlight);
541
+ const rightHeadlight = new THREE.Mesh(new THREE.SphereGeometry(0.2,8,8), headlightMat);
542
+ rightHeadlight.position.set(0.7,0.3,1.9); car.add(rightHeadlight);
543
+ 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});
544
+ }
545
+
546
+ function createPassingPlaces() {
547
+ for (let i = 0; i < 15; i++) createPassingPlace(80 + i * 130);
548
+ }
549
+
550
+ function createPassingPlace(worldZPosition) {
551
+ const side = (passingPlaces.length%2===0)?-1:1;
552
+ const {roadY,roadCurve} = getRoadPropertiesAtZ(worldZPosition);
553
+ const passingLength=20, passingWidth=6, taperLength=12;
554
+ const mainPassingGroup = new THREE.Group();
555
+ const groupXOffset = side * (roadWidth/2 + passingWidth/2);
556
+ mainPassingGroup.position.set(roadCurve+groupXOffset, roadY+0.01, worldZPosition);
557
+ scene.add(mainPassingGroup);
558
+ const passingMaterial = new THREE.MeshStandardMaterial({color:0x555555});
559
+ const mainGeom = createRoundedRectGeometry(passingWidth,passingLength,1.5);
560
+ mainGeom.rotateX(-Math.PI/2);
561
+ const mainMesh = new THREE.Mesh(mainGeom, passingMaterial);
562
+ mainPassingGroup.add(mainMesh);
563
+ const entranceTaperGeom = createSmoothTaperGeometry(taperLength,passingWidth,true,10);
564
+ entranceTaperGeom.rotateX(-Math.PI/2);
565
+ const entranceTaper = new THREE.Mesh(entranceTaperGeom, passingMaterial);
566
+ entranceTaper.position.set(0,0,-passingLength/2-taperLength/2);
567
+ if(side<0) entranceTaper.rotation.z=Math.PI;
568
+ mainPassingGroup.add(entranceTaper);
569
+ const exitTaperGeom = createSmoothTaperGeometry(taperLength,passingWidth,false,10);
570
+ exitTaperGeom.rotateX(-Math.PI/2);
571
+ const exitTaper = new THREE.Mesh(exitTaperGeom, passingMaterial);
572
+ exitTaper.position.set(0,0,passingLength/2+taperLength/2);
573
+ if(side<0) exitTaper.rotation.z=Math.PI;
574
+ mainPassingGroup.add(exitTaper);
575
+ const sign = new THREE.Mesh(new THREE.BoxGeometry(0.5,2,0.1), new THREE.MeshStandardMaterial({color:0xFFFFFF}));
576
+ const signXOffset = side * (roadWidth/2 + passingWidth + 1);
577
+ sign.position.set(roadCurve+signXOffset, 1+roadY, worldZPosition);
578
+ scene.add(sign);
579
+ const signGraphic = new THREE.Mesh(new THREE.CircleGeometry(0.3,16), new THREE.MeshBasicMaterial({color:0x000000}));
580
+ signGraphic.position.set(0,0.5,0.06);
581
+ sign.add(signGraphic);
582
+ passingPlaces.push({position:worldZPosition,side:side,width:passingWidth,length:passingLength+taperLength*2,mesh:mainMesh,group:mainPassingGroup,entranceTaper:entranceTaper,exitTaper:exitTaper,worldXCenter:roadCurve+groupXOffset});
583
+ }
584
+
585
+ function createRoundedRectGeometry(width,length,radius){
586
+ const s=new THREE.Shape(); const x=-width/2,y=-length/2;
587
+ s.moveTo(x,y+radius); s.lineTo(x,y+length-radius); s.quadraticCurveTo(x,y+length,x+radius,y+length);
588
+ s.lineTo(x+width-radius,y+length); s.quadraticCurveTo(x+width,y+length,x+width,y+length-radius);
589
+ s.lineTo(x+width,y+radius); s.quadraticCurveTo(x+width,y,x+width-radius,y);
590
+ s.lineTo(x+radius,y); s.quadraticCurveTo(x,y,x,y+radius);
591
+ return new THREE.ShapeGeometry(s,16);
592
+ }
593
+
594
+ function createSmoothTaperGeometry(length,baseWidth,isEntrance,segments){
595
+ const g=new THREE.PlaneGeometry(baseWidth,length,1,segments); const p=g.attributes.position;
596
+ for(let i=0;i<=segments;i++){
597
+ const t=i/segments; let wf=isEntrance?easeInOut(t):1-easeInOut(t);
598
+ const cw=baseWidth*wf; const lidx=i*2,ridx=i*2+1;
599
+ p.setX(lidx,-cw/2); p.setX(ridx,cw/2);
600
+ }
601
+ p.needsUpdate=true; g.computeVertexNormals(); return g;
602
+ }
603
+
604
+ function createTaperedGeometry(length,startWidth,endWidth,segments){
605
+ const piw=Math.max(startWidth,endWidth); const g=new THREE.PlaneGeometry(piw,length,1,segments);
606
+ const p=g.attributes.position;
607
+ for(let i=0;i<=segments;i++){
608
+ const t=i/segments; const et=easeInOut(t); const cw=startWidth+(endWidth-startWidth)*et;
609
+ const lidx=i*2,ridx=i*2+1;
610
+ p.setX(lidx,-cw/2); p.setX(ridx,cw/2);
611
+ }
612
+ p.needsUpdate=true; g.computeVertexNormals(); return g;
613
+ }
614
+
615
+ function createBridges() {
616
+ for (let i = 0; i < 5; i++) createBridge(300 + i * 350);
617
+ }
618
+
619
+ function createBridge(worldZPosition) {
620
+ const {roadY,roadCurve}=getRoadPropertiesAtZ(worldZPosition);
621
+ const bridgeW=6,bridgeL=20,approachL=15,taperSegs=10,approachRoadW=12,deckH=1;
622
+ const bridgeGeom = new THREE.BoxGeometry(bridgeW,deckH,bridgeL);
623
+ const bridgeMat = new THREE.MeshStandardMaterial({color:0x888888});
624
+ const bridge = new THREE.Mesh(bridgeGeom,bridgeMat);
625
+ bridge.position.set(roadCurve,roadY+deckH/2,worldZPosition);
626
+ bridge.castShadow=true; bridge.receiveShadow=true; scene.add(bridge);
627
+ const railW=0.5,railH=1; const railGeom=new THREE.BoxGeometry(railW,railH,bridgeL);
628
+ const railMat = new THREE.MeshStandardMaterial({color:0x444444});
629
+ const lRail=new THREE.Mesh(railGeom,railMat); lRail.position.set(-bridgeW/2+railW/2,railH/2,0); bridge.add(lRail);
630
+ const rRail=new THREE.Mesh(railGeom,railMat); rRail.position.set(bridgeW/2-railW/2,railH/2,0); bridge.add(rRail);
631
+ const approachMat=new THREE.MeshStandardMaterial({color:0x555555});
632
+ const nAppGeom=createTaperedGeometry(approachL,approachRoadW,bridgeW,taperSegs);
633
+ nAppGeom.rotateX(-Math.PI/2);
634
+ const nApp=new THREE.Mesh(nAppGeom,approachMat);
635
+ nApp.position.set(roadCurve,roadY+0.02,worldZPosition-bridgeL/2-approachL/2);
636
+ nApp.receiveShadow=true; scene.add(nApp);
637
+ const sAppGeom=createTaperedGeometry(approachL,bridgeW,approachRoadW,taperSegs);
638
+ sAppGeom.rotateX(-Math.PI/2);
639
+ const sApp=new THREE.Mesh(sAppGeom,approachMat);
640
+ sApp.position.set(roadCurve,roadY+0.02,worldZPosition+bridgeL/2+approachL/2);
641
+ sApp.receiveShadow=true; scene.add(sApp);
642
+ const sign=new THREE.Mesh(new THREE.BoxGeometry(0.5,2,0.1),new THREE.MeshStandardMaterial({color:0xFFFFFF}));
643
+ sign.position.set(roadCurve-(approachRoadW/2+2),1+roadY,worldZPosition-bridgeL/2-approachL-5); scene.add(sign);
644
+ const signGraphic=new THREE.Mesh(new THREE.PlaneGeometry(0.4,0.4),new THREE.MeshBasicMaterial({color:0x000000}));
645
+ signGraphic.position.set(0,0.5,0.06); sign.add(signGraphic);
646
+ bridges.push({position:worldZPosition,width:bridgeW,length:bridgeL,approachLength:approachL,mesh:bridge,northApproach:nApp,southApproach:sApp,carsWaiting:[]});
647
+ }
648
+
649
+ function easeInOut(t){return t<0.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;}
650
+
651
+ function createPotholes() {
652
+ for (let i = 0; i < 10; i++) createPothole(200 + i * 180 + Math.random()*40);
653
+ }
654
+
655
+ function createPothole(worldZPosition) {
656
+ const {roadY,roadCurve}=getRoadPropertiesAtZ(worldZPosition);
657
+ const xOff=-roadWidth/4+(Math.random()-0.5)*(roadWidth/2-1);
658
+ const potholeGeom=new THREE.CircleGeometry(0.5+Math.random()*0.3,8);
659
+ potholeGeom.rotateX(-Math.PI/2);
660
+ const pothole=new THREE.Mesh(potholeGeom,new THREE.MeshStandardMaterial({color:0x111111,roughness:0.9}));
661
+ pothole.position.set(roadCurve+xOff,roadY+0.01,worldZPosition); scene.add(pothole);
662
+ potholes.push({mesh:pothole,positionZ:worldZPosition,positionX:roadCurve+xOff,hit:false});
663
+ }
664
+
665
+ function createJumpRamps() {
666
+ const numRamps = 5;
667
+ const rampLength = 10;
668
+ const rampWidth = 4;
669
+ const rampHeight = 2; // Height at the peak of the ramp
670
+
671
+ for (let i = 0; i < numRamps; i++) {
672
+ const worldZPosition = 250 + i * (roadLength / (numRamps + 1)) + (Math.random() - 0.5) * 100; // Distribute ramps
673
+ const { roadY, roadCurve } = getRoadPropertiesAtZ(worldZPosition);
674
+
675
+ // Create ramp geometry (a wedge)
676
+ const shape = new THREE.Shape();
677
+ shape.moveTo(-rampWidth / 2, 0);
678
+ shape.lineTo(rampWidth / 2, 0);
679
+ shape.lineTo(rampWidth / 2, rampHeight); // This point defines the peak
680
+ shape.lineTo(-rampWidth / 2, rampHeight * 0.3); // Lower front part of ramp for smoother entry
681
+ shape.closePath();
682
+
683
+ const extrudeSettings = { depth: rampLength, bevelEnabled: false };
684
+ const rampGeometry = new THREE.ExtrudeGeometry(shape, extrudeSettings);
685
+
686
+ // Rotate and position the ramp
687
+ rampGeometry.rotateY(Math.PI / 2); // Rotate so length is along Z
688
+ rampGeometry.translate(0, 0, -rampLength / 2); // Center it
689
+
690
+ const rampMaterial = new THREE.MeshStandardMaterial({ color: 0x777777, roughness: 0.6 });
691
+ const ramp = new THREE.Mesh(rampGeometry, rampMaterial);
692
+
693
+ // Place ramp on the road, slightly to one side or centered
694
+ const xOffset = (Math.random() - 0.5) * (roadWidth - rampWidth) * 0.5;
695
+ ramp.position.set(roadCurve + xOffset, roadY + 0.05, worldZPosition); // +0.05 to be slightly above road
696
+ ramp.castShadow = true;
697
+ ramp.receiveShadow = true;
698
+ scene.add(ramp);
699
+ jumpRamps.push({ mesh: ramp, worldZ: worldZPosition, length: rampLength, width: rampWidth, height: rampHeight, used: false });
700
+ }
701
+ }
702
+
703
+ function createFerry() {
704
+ ferryObject = new THREE.Group();
705
+ const { roadY, roadCurve } = getRoadPropertiesAtZ(ferryPosition);
706
+
707
+ // Ferry Deck
708
+ const deckWidth = 20;
709
+ const deckLength = 40;
710
+ const deckHeight = 2;
711
+ const deckGeometry = new THREE.BoxGeometry(deckWidth, deckHeight, deckLength);
712
+ const deckMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa, roughness: 0.7 });
713
+ const deck = new THREE.Mesh(deckGeometry, deckMaterial);
714
+ deck.position.y = deckHeight / 2; // Sit on the water level (which is roadY - 2)
715
+ deck.receiveShadow = true;
716
+ deck.castShadow = true;
717
+ ferryObject.add(deck);
718
+
719
+ // Superstructure (Cabin)
720
+ const cabinWidth = 10;
721
+ const cabinLength = 15;
722
+ const cabinHeight = 8;
723
+ const cabinGeometry = new THREE.BoxGeometry(cabinWidth, cabinHeight, cabinLength);
724
+ const cabinMaterial = new THREE.MeshStandardMaterial({ color: 0xffffff, roughness: 0.8 });
725
+ const cabin = new THREE.Mesh(cabinGeometry, cabinMaterial);
726
+ cabin.position.set(0, deckHeight + cabinHeight / 2, -deckLength / 4); // Position on deck towards the rear
727
+ cabin.castShadow = true;
728
+ ferryObject.add(cabin);
729
+
730
+ // Funnel
731
+ const funnelRadius = 1.5;
732
+ const funnelHeight = 7;
733
+ const funnelGeometry = new THREE.CylinderGeometry(funnelRadius, funnelRadius * 0.8, funnelHeight, 16);
734
+ const funnelMaterial = new THREE.MeshStandardMaterial({ color: 0x555555 });
735
+ const funnel = new THREE.Mesh(funnelGeometry, funnelMaterial);
736
+ funnel.position.set(0, deckHeight + cabinHeight + funnelHeight / 2 - 2, cabinLength / 3);
737
+ funnel.castShadow = true;
738
+ ferryObject.add(funnel);
739
+
740
+ ferryObject.position.set(roadCurve, roadY - 1, ferryPosition + deckLength/2 + 5); // Position ferry at destination
741
+ ferryObject.rotation.y = Math.PI / 2; // Sideways to the road
742
+ scene.add(ferryObject);
743
+ }
744
+
745
+ function startGame() {
746
+ document.getElementById('instructions').style.display = 'none';
747
+ gameStarted = true;
748
+ clock.start();
749
+ animate();
750
+ clearErrors();
751
+ }
752
+
753
+ function restartGame() {
754
+ gameOver = false; gameTime = 0; speed = 0; score = 0; playerHealth = 100;
755
+ goodStopsInARow = 0; isGrounded = true; airTime = 0; velocity.set(0,0,0);
756
+ suspensionCompression = 0;
757
+
758
+ document.getElementById('score').textContent = `Score: ${score}`;
759
+ document.getElementById('health').style.width = '100%';
760
+ document.getElementById('health').style.backgroundColor = '#00ff00';
761
+
762
+ const startPos = getRoadPropertiesAtZ(0);
763
+ playerCar.position.set(startPos.roadCurve, 1 + startPos.roadY, 0);
764
+ playerCar.rotation.set(0,0,0);
765
+
766
+ aiCars.forEach(ai => {
767
+ const {roadY:rY, roadCurve:rC} = getRoadPropertiesAtZ(ai.initialPositionZ);
768
+ const lo = ai.isOncoming ? (roadWidth/4+0.25) : (-roadWidth/4-0.25);
769
+ ai.mesh.position.set(rC+lo, 1+rY, ai.initialPositionZ);
770
+ ai.mesh.rotation.y = ai.isOncoming ? Math.PI : 0;
771
+ Object.assign(ai, {waiting:false,honking:false,waitingAtPassingPlace:false,flashingLights:false,isOvertaking:false});
772
+ if(ai.leftHeadlight) ai.leftHeadlight.material.color.setHex(0xFFFFFF);
773
+ if(ai.rightHeadlight) ai.rightHeadlight.material.color.setHex(0xFFFFFF);
774
+ });
775
+ potholes.forEach(p => p.hit = false);
776
+ jumpRamps.forEach(r => r.used = false); // Reset used state of ramps
777
+
778
+ document.getElementById('rearView').innerHTML = '';
779
+ document.getElementById('gameOver').style.display = 'none';
780
+ document.getElementById('instructions').style.display = 'none';
781
+
782
+ clock.stop(); clock.start(); gameStarted = true;
783
+ }
784
+
785
+ function animate() {
786
+ if (gameOver && !gameStarted) { renderer.render(scene, camera); requestAnimationFrame(animate); return; }
787
+ if (!gameStarted && gameOver) { renderer.render(scene, camera); requestAnimationFrame(animate); return; }
788
+ if (!gameStarted) return;
789
+
790
+ requestAnimationFrame(animate);
791
+ const delta = clock.getDelta();
792
+ if (!gameOver) update(delta);
793
+ renderer.render(scene, camera);
794
+ if (Math.random() < 0.05) clearErrors();
795
+ }
796
+
797
+ function update(delta) {
798
+ gameTime += delta; updateTimer(); updateClouds(delta);
799
+ if (playerCar.position.z >= ferryPosition) { endGame(true); return; }
800
+ if (gameTime > 120) { showMessage("Time's up! You missed the ferry!", 5); endGame(false, "Time's up!"); return; }
801
+
802
+ updatePlayerCar(delta); updateAICars(delta); checkCollisions(); updateCamera();
803
+
804
+ if (!isGrounded) {
805
+ document.getElementById('airTime').textContent = `Air Time: ${airTime.toFixed(1)}s`;
806
+ document.getElementById('airTime').style.display = 'block';
807
+ } else { document.getElementById('airTime').style.display = 'none'; }
808
+ updateParticleEffects(delta);
809
+ }
810
+
811
+ function updateParticleEffects(delta) {
812
+ if (playerCar.particles) {
813
+ const particles = playerCar.particles; const particlePositions = particles.geometry.attributes.position;
814
+ const showParticles = (isGrounded && Math.abs(speed)>15 && (keys.ArrowLeft||keys.ArrowRight||keys.a||keys.d)) ||
815
+ (!isGrounded && Math.abs(velocity.y)<0.1 && airTime>0.1 && playerCar.position.y < lastRoadY+1.5) ||
816
+ (isGrounded && Math.abs(speed)>25 && (keys.ArrowUp||keys.w));
817
+ if (showParticles) {
818
+ particles.visible = true;
819
+ for (let i=0; i<particlePositions.count; i++) {
820
+ const wheelIdx=(Math.floor(Math.random()*2)+2), wheelX=(wheelIdx===2)?-1:1, wheelZ=-1.5;
821
+ const xOff=(Math.random()-0.5)*0.5, yOff=Math.random()*0.1-0.2, zOff=(Math.random()-0.5)*0.5-0.3;
822
+ particlePositions.setXYZ(i,wheelX+xOff,yOff,wheelZ+zOff);
823
+ }
824
+ particlePositions.needsUpdate = true;
825
+ } else { particles.visible = false; }
826
+ }
827
+ }
828
+
829
+ function updatePlayerCar(delta) {
830
+ updateCarPhysics(delta);
831
+ if((keys.ArrowUp||keys.w)&&!gameOver) speed+=acceleration*(isGrounded?1.2:0.3);
832
+ else if((keys.ArrowDown||keys.s)&&!gameOver) speed-=braking*(isGrounded?1.2:0.3);
833
+ else { if(isGrounded)speed*=0.98; else speed*=0.995; if(Math.abs(speed)<0.05)speed=0; }
834
+
835
+ if((keys[' ']||keys.j)&&isGrounded&&Math.abs(speed)>10){
836
+ jumpForce=1.2+Math.abs(speed)*0.06+suspensionCompression*6; // Increased base and scaling
837
+ velocity.y=jumpForce; isGrounded=false; showMessage("Jumping!",1);
838
+ }
839
+
840
+ speed=Math.max(-maxSpeed/2,Math.min(maxSpeed,speed));
841
+ document.getElementById('speedometer').textContent=`Speed: ${Math.abs(Math.round(speed))} mph`;
842
+ const actualMoveSpeed=speed*delta*2.5;
843
+ const steeringInput=(keys.ArrowLeft||keys.a)?1:(keys.ArrowRight||keys.d)?-1:0;
844
+ if(steeringInput!==0&&Math.abs(speed)>0.1){
845
+ const steerEff=isGrounded?1.0:0.3; const turnRate=steering*steerEff*Math.abs(speed/maxSpeed)*2.0;
846
+ playerCar.rotation.y+=steeringInput*turnRate*Math.sign(speed);
847
+ }
848
+ playerCar.position.x+=Math.sin(playerCar.rotation.y)*actualMoveSpeed;
849
+ playerCar.position.z+=Math.cos(playerCar.rotation.y)*actualMoveSpeed;
850
+ if(!isGrounded){
851
+ const airTilt=Math.min(Math.max(velocity.y*0.1,-0.3),0.3);
852
+ playerCar.rotation.x=airTilt;
853
+ playerCar.rotation.z+=steeringInput*speed*0.0005;
854
+ playerCar.rotation.z=Math.max(-0.3,Math.min(0.3,playerCar.rotation.z));
855
+ }else{playerCar.rotation.x*=0.8; playerCar.rotation.z*=0.8;}
856
+ const {roadY:currentRoadY,roadCurve:currentRoadCurve}=getRoadPropertiesAtZ(playerCar.position.z);
857
+ if(isGrounded) playerCar.position.y=1+currentRoadY+suspensionCompression;
858
+ if(isGrounded){
859
+ const latOff=playerCar.position.x-currentRoadCurve; const maxOff=roadWidth/2+0.5;
860
+ if(Math.abs(latOff)>maxOff){
861
+ playerCar.position.x-=Math.sign(latOff)*0.1*Math.abs(latOff-maxOff);
862
+ if(Math.abs(latOff)>maxOff+1.0)speed*=0.95;
863
+ }
864
+ }
865
+ }
866
+
867
+ function updateCarPhysics(delta) {
868
+ const {roadY: groundHeightAtCar}=getRoadPropertiesAtZ(playerCar.position.z);
869
+ const carEffectiveRadius=0.5;
870
+
871
+ // Check for jump ramp interaction
872
+ let onRamp = false;
873
+ jumpRamps.forEach(ramp => {
874
+ const distToRampZ = Math.abs(playerCar.position.z - ramp.mesh.position.z);
875
+ const distToRampX = Math.abs(playerCar.position.x - ramp.mesh.position.x);
876
+ if (distToRampZ < ramp.length / 2 && distToRampX < ramp.width / 2 && playerCar.position.y < ramp.mesh.position.y + ramp.height + carEffectiveRadius) {
877
+ onRamp = true;
878
+ if (!ramp.used && isGrounded) { // Only trigger jump once and if grounded
879
+ velocity.y = ramp.height * 1.5 + Math.abs(speed) * 0.1; // Ramp jump force
880
+ isGrounded = false;
881
+ ramp.used = true; // Mark ramp as used for this jump
882
+ showMessage("Ramp Jump!", 2);
883
+ score += 150; // Bonus points for ramp jump
884
+ document.getElementById('score').textContent = `Score: ${score}`;
885
+ setTimeout(() => { ramp.used = false; }, 3000); // Allow reuse after a delay
886
+ }
887
+ }
888
+ });
889
+
890
+
891
+ if (isGrounded && !onRamp) { // Don't apply ground physics if on ramp and about to jump
892
+ const elevationChange = groundHeightAtCar - lastRoadY;
893
+ suspensionCompression = -elevationChange * suspensionStrength;
894
+ suspensionCompression = Math.max(-0.3, Math.min(0.3, suspensionCompression));
895
+ playerCar.position.y = groundHeightAtCar + carEffectiveRadius + suspensionCompression;
896
+ velocity.y = 0;
897
+ } else {
898
+ velocity.y -= gravity * delta * 20;
899
+ playerCar.position.y += velocity.y * delta * 5;
900
+ airTime += delta;
901
+
902
+ if (playerCar.position.y <= groundHeightAtCar + carEffectiveRadius && velocity.y < 0 && !onRamp) { // Land only if not on ramp
903
+ playerCar.position.y = groundHeightAtCar + carEffectiveRadius;
904
+ isGrounded = true;
905
+ const impactForce = Math.abs(velocity.y);
906
+ velocity.y = 0; airTime = 0;
907
+ if (impactForce > 0.3) {
908
+ decreaseHealth(Math.floor(impactForce*15),`Hard landing!`);
909
+ suspensionCompression = Math.min(impactForce*0.2,0.4);
910
+ } else { suspensionCompression = Math.min(impactForce*0.1,0.1); }
911
+ }
912
+ }
913
+ lastRoadY = groundHeightAtCar;
914
+ playerCar.children.forEach(c=>{if(c.isWheel)c.position.y=0.5+suspensionCompression*0.5;});
915
+ }
916
+
917
+ function updateAICars(delta) {
918
+ carsBehind = [];
919
+ aiCars.forEach(ai => {
920
+ const carM=ai.mesh; let curSpd=ai.waiting||ai.waitingAtPassingPlace?0:ai.speed;
921
+ const moveDist=curSpd*(ai.isOncoming?-1:1)*delta*2.5; carM.position.z+=moveDist;
922
+ const {roadY,roadCurve}=getRoadPropertiesAtZ(carM.position.z);
923
+ let tLaneXOff=ai.isOncoming?(roadWidth/4):(-roadWidth/4);
924
+ if(ai.isOvertaking)tLaneXOff=ai.isOncoming?(-roadWidth/4):(roadWidth/4);
925
+ const tX=roadCurve+tLaneXOff; carM.position.x+=(tX-carM.position.x)*0.1; carM.position.y=1+roadY;
926
+ if(ai.isOncoming){
927
+ const dToP=playerCar.position.distanceTo(carM.position);
928
+ if(dToP<40&&!ai.waitingAtPassingPlace){
929
+ let pYield=isPlayerInPassingPlaceForOncoming(ai);
930
+ if(!pYield&&ai.politeness>0.5){
931
+ let canAIPull=false;
932
+ 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;}});
933
+ if(!canAIPull)ai.waiting=true;
934
+ }
935
+ }else if((ai.waiting||ai.waitingAtPassingPlace)&&dToP>50){ai.waiting=false;ai.waitingAtPassingPlace=false;ai.flashingLights=false;}
936
+ }else{
937
+ const distBPlayer=playerCar.position.z-carM.position.z;
938
+ if(distBPlayer>5&&distBPlayer<30&&speed<ai.speed*0.8&&!ai.isOvertaking){
939
+ let canOvertake=false;
940
+ 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;}});
941
+ if(canOvertake){ai.isOvertaking=true;ai.flashingLights=false;setTimeout(()=>{ai.isOvertaking=false;},5000);}
942
+ else if(!ai.waitingAtPassingPlace)ai.flashingLights=true;
943
+ }else if(ai.flashingLights&&distBPlayer>50)ai.flashingLights=false;
944
+ }
945
+ 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);}
946
+ else{if(ai.leftHeadlight)ai.leftHeadlight.material.color.setHex(0xFFFFFF); if(ai.rightHeadlight)ai.rightHeadlight.material.color.setHex(0xFFFFFF);}
947
+ if(!ai.isOncoming&&carM.position.z<playerCar.position.z&&carM.position.z>playerCar.position.z-50)carsBehind.push(ai);
948
+ if(Math.abs(carM.position.z-(roadLength/2))>roadLength/2+100){
949
+ const iZ=ai.initialPositionZ; const{roadY:iRY,roadCurve:iRC}=getRoadPropertiesAtZ(iZ);
950
+ const lo=ai.isOncoming?(roadWidth/4+0.25):(-roadWidth/4-0.25);
951
+ carM.position.set(iRC+lo,1+iRY,iZ); Object.assign(ai,{waiting:false,waitingAtPassingPlace:false,isOvertaking:false,flashingLights:false});
952
+ }
953
+ });
954
+ updateRearViewMirror();
955
+ }
956
+
957
+ function isPlayerInPassingPlaceForOncoming(oncomingAICar) {
958
+ const pZ=playerCar.position.z,pX=playerCar.position.x; const{roadCurve:pRC}=getRoadPropertiesAtZ(pZ);
959
+ for(const pp of passingPlaces){
960
+ if(Math.abs(pZ-pp.position)<pp.length/2){
961
+ if(pp.side===-1){if(pX<pRC-roadWidth/4&&Math.abs(speed)<5)return true;}
962
+ }
963
+ }return false;
964
+ }
965
+
966
+ 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);}}
967
+
968
+ function updateRearViewMirror(){
969
+ const rvEl=document.getElementById('rearView'); rvEl.innerHTML='';
970
+ carsBehind.sort((a,b)=>(playerCar.position.z-a.mesh.position.z)-(playerCar.position.z-b.mesh.position.z));
971
+ for(let i=0;i<Math.min(carsBehind.length,3);i++){
972
+ const cd=carsBehind[i]; const dist=playerCar.position.z-cd.mesh.position.z;
973
+ const ind=document.createElement('div'); ind.style.position='absolute';
974
+ ind.style.width=`${Math.max(5,30-dist*0.5)}px`; ind.style.height=`${Math.max(3,20-dist*0.3)}px`;
975
+ ind.style.backgroundColor=cd.flashingLights?(Math.sin(Date.now()*0.01)>0?'#ffff00':'#cc0000'):'#cc0000';
976
+ ind.style.borderRadius='3px';
977
+ ind.style.left=`${(rvEl.offsetWidth/2)-(parseFloat(ind.style.width)/2)+(i-Math.floor(Math.min(carsBehind.length,3)/2))*35}px`;
978
+ ind.style.bottom=`${5+Math.max(0,20-dist*0.8)}px`; ind.style.zIndex=50-Math.floor(dist);
979
+ rvEl.appendChild(ind);
980
+ }
981
+ }
982
+
983
+ function checkCollisions() {
984
+ const playerBox = new THREE.Box3().setFromObject(playerCar);
985
+ 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!");}}}});
986
+ 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);}});
987
+ }
988
+
989
+ function decreaseHealth(amount,message){playerHealth-=amount;playerHealth=Math.max(0,playerHealth);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!");}
990
+
991
+ 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);}
992
+
993
+ function updateTimer(){const m=Math.floor(gameTime/60),s=Math.floor(gameTime%60);document.getElementById('timer').textContent=`Time: ${m}:${s<10?'0':''}${s}`;}
994
+
995
+ function showMessage(text,duration){const msgEl=document.getElementById('message');msgEl.textContent=text;msgEl.style.opacity=1;setTimeout(()=>{msgEl.style.opacity=0;},duration*1000);clearErrors();}
996
+
997
+ 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='';}}}
998
+
999
+ 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';}
1000
+
1001
+ function onWindowResize(){camera.aspect=window.innerWidth/window.innerHeight;camera.updateProjectionMatrix();renderer.setSize(window.innerWidth,window.innerHeight);}
1002
+
1003
+ 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;}
1004
+ 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;}
1005
+
1006
+ init();
1007
+ </script>
1008
+ </body>
1009
+ </html>