awacke1 commited on
Commit
768d876
·
verified ·
1 Parent(s): 227d9d9

Update index.html

Browse files
Files changed (1) hide show
  1. index.html +1221 -19
index.html CHANGED
@@ -1,19 +1,1221 @@
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>Starship Circuit Commander</title>
7
+ <style>
8
+ body {
9
+ margin: 0;
10
+ overflow: hidden;
11
+ font-family: Arial, sans-serif;
12
+ }
13
+ canvas {
14
+ display: block;
15
+ }
16
+ .ui-container {
17
+ position: absolute;
18
+ top: 10px;
19
+ right: 10px;
20
+ color: white;
21
+ background-color: rgba(0, 0, 0, 0.5);
22
+ padding: 10px;
23
+ border-radius: 5px;
24
+ user-select: none;
25
+ }
26
+ .sidebar {
27
+ position: absolute;
28
+ top: 0;
29
+ left: 0;
30
+ width: 250px;
31
+ height: 100%;
32
+ background-color: rgba(0, 0, 0, 0.8);
33
+ color: white;
34
+ padding: 10px;
35
+ overflow-y: auto;
36
+ border-radius: 0 5px 5px 0;
37
+ }
38
+ .sidebar h2, .sidebar h3 {
39
+ margin: 0 0 10px;
40
+ font-size: 18px;
41
+ }
42
+ .sidebar button {
43
+ display: block;
44
+ width: 100%;
45
+ margin: 5px 0;
46
+ padding: 8px;
47
+ background: #4CAF50;
48
+ color: white;
49
+ border: none;
50
+ border-radius: 5px;
51
+ cursor: pointer;
52
+ font-size: 14px;
53
+ }
54
+ .sidebar button:hover {
55
+ background: #45a049;
56
+ }
57
+ .sidebar .character-sheet, .sidebar .skills-sheet {
58
+ margin-top: 20px;
59
+ font-size: 14px;
60
+ }
61
+ .sidebar .character-sheet div, .sidebar .skills-sheet div {
62
+ margin: 5px 0;
63
+ }
64
+ .gallery {
65
+ position: absolute;
66
+ top: 50%;
67
+ left: 50%;
68
+ transform: translate(-50%, -50%);
69
+ background-color: rgba(0, 0, 0, 0.8);
70
+ color: white;
71
+ padding: 20px;
72
+ border-radius: 10px;
73
+ text-align: center;
74
+ max-width: 600px;
75
+ display: none;
76
+ }
77
+ .gallery img {
78
+ max-width: 100%;
79
+ margin: 10px 0;
80
+ }
81
+ .skill-meter {
82
+ background: #333;
83
+ height: 10px;
84
+ border-radius: 5px;
85
+ overflow: hidden;
86
+ }
87
+ .skill-meter div {
88
+ background: #4CAF50;
89
+ height: 100%;
90
+ transition: width 0.3s;
91
+ }
92
+ </style>
93
+ </head>
94
+ <body>
95
+ <div class="ui-container" id="race-ui">
96
+ <h2>Starship Circuit Commander</h2>
97
+ <div id="time">Time: 0</div>
98
+ <div id="score">Score: 0</div>
99
+ <div id="status">Status: Active</div>
100
+ </div>
101
+ <div class="sidebar" id="sidebar">
102
+ <h2>Tracks</h2>
103
+ <h3>Minnesota</h3>
104
+ <button onclick="startRace('U of MN Twin Cities Track')">🎓 U of MN Twin Cities Track</button>
105
+ <button onclick="startRace('Phelps Island Drift')">🏝️ Phelps Island Drift</button>
106
+ <button onclick="startRace('Mound Overlook Circuit')">🏞️ Mound Overlook Circuit</button>
107
+ <button onclick="startRace('Lake Minnetonka Sprint')">🚣 Lake Minnetonka Sprint</button>
108
+ <button onclick="startRace('St. Paul Riverbend Run')">🛶 St. Paul Riverbend Run</button>
109
+ <button onclick="startRace('Cathedral Hill Climb')">⛪ Cathedral Hill Climb</button>
110
+ <button onclick="startRace('Downtown Minneapolis Skyway Loop')">🌆 Downtown Minneapolis Skyway Loop</button>
111
+ <button onclick="startRace('Nicollet Mall Straightaway')">🛍️ Nicollet Mall Straightaway</button>
112
+ <button onclick="startRace('Lake of the Isles Island Circuit')">🏝️ Lake of the Isles Island Circuit</button>
113
+ <button onclick="startRace('Minnehaha Falls Meander')">🌳 Minnehaha Falls Meander</button>
114
+ <h3>Wisconsin</h3>
115
+ <button onclick="startRace('Milwaukee Loop')">🛶 Milwaukee Loop</button>
116
+ <button onclick="startRace('Waukesha Speedway')">🏁 Waukesha Speedway</button>
117
+ <button onclick="startRace('Manitowoc Marina Run')">🌊 Manitowoc Marina Run</button>
118
+ <button onclick="startRace('Lake Winnebago Circuit')">🌾 Lake Winnebago Circuit</button>
119
+ <button onclick="startRace('Door County Coastal Cruise')">🌲 Door County Coastal Cruise</button>
120
+ <button onclick="startRace('Green Bay Bayfront Blitz')">���� Green Bay Bayfront Blitz</button>
121
+ <button onclick="startRace('Sturgeon Bay Ship Canal Sprint')">🚢 Sturgeon Bay Ship Canal Sprint</button>
122
+ <button onclick="startRace('Wisconsin Dells Rapids Run')">🎢 Wisconsin Dells Rapids Run</button>
123
+ <button onclick="startRace('Madison Capitol Circuit')">🏛️ Madison Capitol Circuit</button>
124
+ <button onclick="startRace('Superior North Shore Dash')">🌲 Superior North Shore Dash</button>
125
+ <h3>Texas</h3>
126
+ <button onclick="startRace('Houston Oil Refinery Rush')">⚙️ Houston Oil Refinery Rush</button>
127
+ <button onclick="startRace('Dallas Finance District Dash')">💼 Dallas Finance District Dash</button>
128
+ <button onclick="startRace('Austin Tech Corridor Cruise')">🎸 Austin Tech Corridor Cruise</button>
129
+ <button onclick="startRace('San Antonio Riverwalk Run')">🎺 San Antonio Riverwalk Run</button>
130
+ <button onclick="startRace('Corpus Christi Coastal Run')">⚓ Corpus Christi Coastal Run</button>
131
+ <button onclick="startRace('Fort Worth Stockyards Loop')">🐎 Fort Worth Stockyards Loop</button>
132
+ <button onclick="startRace('Galveston Port Channel Circuit')">🚢 Galveston Port Channel Circuit</button>
133
+ <button onclick="startRace('El Paso Border Trade Boulevard')">🌵 El Paso Border Trade Boulevard</button>
134
+ <button onclick="startRace('Lubbock Innovation Loop')">🌟 Lubbock Innovation Loop</button>
135
+ <button onclick="startRace('Midland Permian Basin Sprint')">⛽ Midland Permian Basin Sprint</button>
136
+ <h3>Florida</h3>
137
+ <button onclick="startRace('Clearwater Curve')">🏖️ Clearwater Curve</button>
138
+ <button onclick="startRace('Miami Beach Boulevard')">🌴 Miami Beach Boulevard</button>
139
+ <button onclick="startRace('Florida Keys Canal Cruise')">🚤 Florida Keys Canal Cruise</button>
140
+ <button onclick="startRace('Orlando Theme Park Tour')">🎢 Orlando Theme Park Tour</button>
141
+ <button onclick="startRace('Tampa Bay Finance District Drift')">💰 Tampa Bay Finance District Drift</button>
142
+ <button onclick="startRace('Jacksonville River City Run')">🏙️ Jacksonville River City Run</button>
143
+ <button onclick="startRace('Fort Lauderdale Cruise Port Circuit')">🚢 Fort Lauderdale Cruise Port Circuit</button>
144
+ <button onclick="startRace('Key West Sunset Sprint')">🌅 Key West Sunset Sprint</button>
145
+ <button onclick="startRace('St. Augustine Colonial Run')">🏰 St. Augustine Colonial Run</button>
146
+ <button onclick="startRace('Palm Beach Gold Coast Circuit')">💎 Palm Beach Gold Coast Circuit</button>
147
+ <h3>California</h3>
148
+ <button onclick="startRace('Venice Beach Boardwalk Circuit')">🏄 Venice Beach Boardwalk Circuit</button>
149
+ <button onclick="startRace('Los Angeles Star-Studded Speedway')">🌟 Los Angeles Star-Studded Speedway</button>
150
+ <button onclick="startRace('San Francisco Golden Gate Run')">🌉 San Francisco Golden Gate Run</button>
151
+ <button onclick="startRace('San Diego Coastal Drift')">🚤 San Diego Coastal Drift</button>
152
+ <button onclick="startRace('Anaheim Theme Park Loop')">🏰 Anaheim Theme Park Loop</button>
153
+ <button onclick="startRace('Santa Barbara Vineyard Tour')">🍇 Santa Barbara Vineyard Tour</button>
154
+ <button onclick="startRace('Palm Springs Desert Dash')">☀️ Palm Springs Desert Dash</button>
155
+ <button onclick="startRace('Santa Cruz Boardwalk Sprint')">🎡 Santa Cruz Boardwalk Sprint</button>
156
+ <button onclick="startRace('Monterey Bay Coastal Circuit')">🐋 Monterey Bay Coastal Circuit</button>
157
+ <button onclick="startRace('Carmel-by-the-Sea Scenic Route')">🏞️ Carmel-by-the-Sea Scenic Route</button>
158
+ <button onclick="startRace('Napa Valley Vineyard Ride')">🍷 Napa Valley Vineyard Ride</button>
159
+ <button onclick="showGallery()">📷 View Image Gallery</button>
160
+ <button onclick="restartRace()">🔄 Restart Race</button>
161
+ <div class="character-sheet" id="character-sheet">
162
+ <h3>Ship Status</h3>
163
+ <div id="ship-type">Type: Unknown</div>
164
+ <div id="thruster-1">Thruster 1: 0/0</div>
165
+ <div id="thruster-2">Thruster 2: 0/0</div>
166
+ <div id="thruster-3">Thruster 3: 0/0</div>
167
+ <div id="thruster-4">Thruster 4: 0/0</div>
168
+ <div id="body-front">Body Front: 0/0</div>
169
+ <div id="body-back">Body Back: 0/0</div>
170
+ <div id="body-left">Body Left: 0/0</div>
171
+ <div id="body-right">Body Right: 0/0</div>
172
+ </div>
173
+ <div class="skills-sheet" id="skills-sheet">
174
+ <h3>Skills</h3>
175
+ <div>Speed: <span id="skill-speed">0</span><div class="skill-meter"><div id="skill-speed-meter" style="width: 0%"></div></div></div>
176
+ <div>Shot Power: <span id="skill-shot-power">0</span><div class="skill-meter"><div id="skill-shot-power-meter" style="width: 0%"></div></div></div>
177
+ <div>Shot Count: <span id="skill-shot-count">0</span><div class="skill-meter"><div id="skill-shot-count-meter" style="width: 0%"></div></div></div>
178
+ <div>Turn Speed: <span id="skill-turn-speed">0</span><div class="skill-meter"><div id="skill-turn-speed-meter" style="width: 0%"></div></div></div>
179
+ </div>
180
+ </div>
181
+ <div class="gallery" id="gallery">
182
+ <h2>Lake Minnetonka Gallery</h2>
183
+ <p>Images of the Lake Minnetonka area</p>
184
+ <img src="https://via.placeholder.com/400x200?text=Phelps+Island" alt="Phelps Island">
185
+ <img src="https://via.placeholder.com/400x200?text=Mound+Shoreline" alt="Mound Shoreline">
186
+ <img src="https://via.placeholder.com/400x200?text=Wayzata+Bay" alt="Wayzata Bay">
187
+ <button onclick="hideGallery()">Back to Menu</button>
188
+ </div>
189
+
190
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
191
+ <script>
192
+ // Game state
193
+ let gameState = 'racing';
194
+ let raceTime = 0;
195
+ let score = 0;
196
+ let currentTrack = 'Phelps Island Drift';
197
+ let lastGatePass = 0;
198
+ let isJumping = false;
199
+ let jumpCooldown = 0;
200
+
201
+ // Scene setup
202
+ const scene = new THREE.Scene();
203
+ scene.background = new THREE.Color(0x87CEEB);
204
+ const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
205
+ const renderer = new THREE.WebGLRenderer({ antialias: true });
206
+ renderer.setSize(window.innerWidth, window.innerHeight);
207
+ renderer.shadowMap.enabled = true;
208
+ document.body.appendChild(renderer.domElement);
209
+
210
+ // Lights
211
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
212
+ scene.add(ambientLight);
213
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
214
+ directionalLight.position.set(50, 50, 50);
215
+ directionalLight.castShadow = true;
216
+ scene.add(directionalLight);
217
+
218
+ // Ground plane
219
+ const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
220
+ const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8, metalness: 0.2 });
221
+ const ground = new THREE.Mesh(groundGeometry, groundMaterial);
222
+ ground.rotation.x = -Math.PI / 2;
223
+ ground.position.y = -0.1;
224
+ ground.receiveShadow = true;
225
+ scene.add(ground);
226
+
227
+ // Ship types
228
+ const shipTypes = [
229
+ { name: 'Truck', scale: { x: 2, y: 1.5, z: 4 }, maxHP: 20, speed: 0.2, accel: 0.008 },
230
+ { name: 'Subcompact', scale: { x: 1, y: 1, z: 2 }, maxHP: 10, speed: 0.3, accel: 0.01 },
231
+ { name: 'Motorbike', scale: { x: 0.5, y: 0.5, z: 1.5 }, maxHP: 5, speed: 0.35, accel: 0.012 },
232
+ { name: 'Rocketman', scale: { x: 0.5, y: 1, z: 0.5 }, maxHP: 3, speed: 0.4, accel: 0.015 }
233
+ ];
234
+
235
+ // Skills
236
+ const skills = {
237
+ speed: { level: 0, maxLevel: 10, effect: level => 1 + level * 0.05 },
238
+ shotPower: { level: 0, maxLevel: 10, effect: level => 5 + level * 2 },
239
+ shotCount: { level: 0, maxLevel: 10, effect: level => 1 + Math.floor(level / 2) },
240
+ turnSpeed: { level: 0, maxLevel: 10, effect: level => 0.05 + level * 0.005 }
241
+ };
242
+
243
+ // Item pickups
244
+ const pickupTypes = [
245
+ { skill: 'speed', color: 0x00ff00 },
246
+ { skill: 'shotPower', color: 0xff0000 },
247
+ { skill: 'shotCount', color: 0xffff00 },
248
+ { skill: 'turnSpeed', color: 0x0000ff }
249
+ ];
250
+ const pickups = [];
251
+
252
+ function createPickup(position) {
253
+ const geometry = new THREE.SphereGeometry(0.5, 16, 16);
254
+ const type = pickupTypes[Math.floor(Math.random() * pickupTypes.length)];
255
+ const material = new THREE.MeshStandardMaterial({ color: type.color });
256
+ const pickup = new THREE.Mesh(geometry, material);
257
+ pickup.position.copy(position);
258
+ pickup.userData = { skill: type.skill };
259
+ scene.add(pickup);
260
+ pickups.push(pickup);
261
+ return pickup;
262
+ }
263
+
264
+ // Player ship
265
+ let playerShip = null;
266
+ let playerData = null;
267
+ function createShip(typeIndex, isPlayer, scaleMultiplier = 1, splitCount = 0) {
268
+ const type = shipTypes[typeIndex];
269
+ const ship = new THREE.Group();
270
+ const scale = {
271
+ x: type.scale.x * scaleMultiplier,
272
+ y: type.scale.y * scaleMultiplier,
273
+ z: type.scale.z * scaleMultiplier
274
+ };
275
+ const bodyGeometry = new THREE.BoxGeometry(scale.x, scale.y, scale.z);
276
+ const bodyMaterial = new THREE.MeshStandardMaterial({
277
+ color: isPlayer ? 0x00ff00 : 0xff0000,
278
+ metalness: 0.8,
279
+ roughness: 0.2
280
+ });
281
+ const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
282
+ ship.add(body);
283
+ if (type.name !== 'Rocketman') {
284
+ const thrusterGeometry = new THREE.CylinderGeometry(0.2 * scaleMultiplier, 0.2 * scaleMultiplier, 0.5 * scaleMultiplier, 8);
285
+ const thrusterMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa });
286
+ for (let i = 0; i < 4; i++) {
287
+ const thruster = new THREE.Mesh(thrusterGeometry, thrusterMaterial);
288
+ thruster.position.set(
289
+ (i % 2 === 0 ? 0.5 : -0.5) * scale.x * 0.8,
290
+ 0,
291
+ (i < 2 ? 0.5 : -0.5) * scale.z * 0.8
292
+ );
293
+ thruster.rotation.x = Math.PI / 2;
294
+ ship.add(thruster);
295
+ }
296
+ } else {
297
+ const jetpackGeometry = new THREE.CylinderGeometry(0.3 * scaleMultiplier, 0.3 * scaleMultiplier, 0.8 * scaleMultiplier, 8);
298
+ const jetpack = new THREE.Mesh(jetpackGeometry, new THREE.MeshStandardMaterial({ color: 0xaaaaaa }));
299
+ jetpack.position.set(0, -0.5 * scaleMultiplier, 0);
300
+ ship.add(jetpack);
301
+ }
302
+ ship.position.y = 10;
303
+ ship.castShadow = true;
304
+ const maxHP = type.maxHP * scaleMultiplier;
305
+ ship.userData = {
306
+ type: type.name,
307
+ maxSpeed: type.speed,
308
+ acceleration: type.accel,
309
+ thrusters: Array(4).fill(maxHP),
310
+ body: { front: maxHP, back: maxHP, left: maxHP, right: maxHP },
311
+ active: true,
312
+ speed: 0,
313
+ currentWaypoint: 0,
314
+ lastGatePass: 0,
315
+ isPlayer: isPlayer,
316
+ splitCount: splitCount,
317
+ typeIndex: typeIndex
318
+ };
319
+ scene.add(ship);
320
+ if (!isPlayer) aiShips.push(ship);
321
+ return ship;
322
+ }
323
+
324
+ // Initialize player
325
+ function initPlayer() {
326
+ const typeIndex = Math.floor(Math.random() * shipTypes.length);
327
+ playerShip = createShip(typeIndex, true);
328
+ playerData = playerShip.userData;
329
+ Object.keys(skills).forEach(skill => skills[skill].level = 0);
330
+ score = 0;
331
+ document.getElementById('score').textContent = `Score: ${score}`;
332
+ updateCharacterSheet();
333
+ updateSkillsSheet();
334
+ }
335
+
336
+ // AI opponents
337
+ const aiShips = [];
338
+ for (let i = 0; i < 3; i++) {
339
+ const typeIndex = Math.floor(Math.random() * shipTypes.length);
340
+ aiShips.push(createShip(typeIndex, false));
341
+ }
342
+
343
+ // Start gate
344
+ const gateGeometry = new THREE.BoxGeometry(60, 12, 1);
345
+ const gateMaterial = new THREE.MeshBasicMaterial({ visible: false });
346
+ const startGate = new THREE.Mesh(gateGeometry, gateMaterial);
347
+ scene.add(startGate);
348
+
349
+ // Missile system
350
+ const missiles = [];
351
+ function createMissile(position, target) {
352
+ const geometry = new THREE.ConeGeometry(0.2, 0.5, 8);
353
+ const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
354
+ const missile = new THREE.Mesh(geometry, material);
355
+ missile.position.copy(position);
356
+ missile.rotation.x = Math.PI / 2;
357
+ missile.castShadow = true;
358
+
359
+ // Flame trail
360
+ const flameCount = 10;
361
+ const flameGeometry = new THREE.BufferGeometry();
362
+ const flamePositions = new Float32Array(flameCount * 3);
363
+ const flameColors = new Float32Array(flameCount * 3);
364
+ for (let i = 0; i < flameCount; i++) {
365
+ const i3 = i * 3;
366
+ flamePositions[i3] = 0;
367
+ flamePositions[i3 + 1] = 0;
368
+ flamePositions[i3 + 2] = 0;
369
+ flameColors[i3] = 1;
370
+ flameColors[i3 + 1] = 0;
371
+ flameColors[i3 + 2] = 0;
372
+ }
373
+ flameGeometry.setAttribute('position', new THREE.BufferAttribute(flamePositions, 3));
374
+ flameGeometry.setAttribute('color', new THREE.BufferAttribute(flameColors, 3));
375
+ const flameMaterial = new THREE.PointsMaterial({
376
+ size: 0.2,
377
+ vertexColors: true,
378
+ transparent: true,
379
+ opacity: 0.5
380
+ });
381
+ const flame = new THREE.Points(flameGeometry, flameMaterial);
382
+ flame.position.z = -0.3;
383
+ missile.add(flame);
384
+
385
+ missile.userData = {
386
+ target: target,
387
+ speed: 1,
388
+ lifetime: 300,
389
+ damage: skills.shotPower.effect(skills.shotPower.level)
390
+ };
391
+ scene.add(missile);
392
+ missiles.push(missile);
393
+ return missile;
394
+ }
395
+
396
+ // Particle system for explosions
397
+ function createExplosion(position, isCollision = false) {
398
+ const particleCount = 50;
399
+ const geometry = new THREE.BufferGeometry();
400
+ const positions = new Float32Array(particleCount * 3);
401
+ const colors = new Float32Array(particleCount * 3);
402
+ for (let i = 0; i < particleCount; i++) {
403
+ const i3 = i * 3;
404
+ positions[i3] = position.x;
405
+ positions[i3 + 1] = position.y;
406
+ positions[i3 + 2] = position.z;
407
+ const color = isCollision ? [0.5, 0.5, 0.5] : (Math.random() > 0.5 ? [1, 1, 0] : [1, 0, 0]);
408
+ colors[i3] = color[0];
409
+ colors[i3 + 1] = color[1];
410
+ colors[i3 + 2] = color[2];
411
+ }
412
+ geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
413
+ geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
414
+ const material = new THREE.PointsMaterial({
415
+ size: 0.5,
416
+ vertexColors: true,
417
+ transparent: true,
418
+ opacity: 0.8
419
+ });
420
+ const particles = new THREE.Points(geometry, material);
421
+ particles.userData = { lifetime: isCollision ? 30 : 60, velocities: [] };
422
+ for (let i = 0; i < particleCount; i++) {
423
+ particles.userData.velocities.push(
424
+ new THREE.Vector3(
425
+ (Math.random() - 0.5) * 0.2,
426
+ (Math.random() - 0.5) * 0.2,
427
+ (Math.random() - 0.5) * 0.2
428
+ )
429
+ );
430
+ }
431
+ scene.add(particles);
432
+ explosions.push(particles);
433
+ return particles;
434
+ }
435
+
436
+ // Building and column generation
437
+ function createBuilding(x, z, width, depth, height, scaleMultiplier = 1, splitCount = 0) {
438
+ const geometry = new THREE.BoxGeometry(width * scaleMultiplier, height * scaleMultiplier, depth * scaleMultiplier);
439
+ const material = new THREE.MeshStandardMaterial({
440
+ color: Math.random() * 0xffffff,
441
+ roughness: 0.7,
442
+ metalness: 0.3
443
+ });
444
+ const building = new THREE.Mesh(geometry, material);
445
+ building.position.set(x, (height * scaleMultiplier) / 2, z);
446
+ building.castShadow = true;
447
+ building.receiveShadow = true;
448
+ building.userData = {
449
+ splitCount: splitCount,
450
+ maxHP: 10 * scaleMultiplier,
451
+ currentHP: 10 * scaleMultiplier,
452
+ width: width,
453
+ depth: depth,
454
+ height: height,
455
+ isFalling: false,
456
+ fallVelocity: 0
457
+ };
458
+ return building;
459
+ }
460
+
461
+ function createColumn(x, z, height) {
462
+ const geometry = new THREE.CylinderGeometry(1, 1, height, 8);
463
+ const material = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.8, metalness: 0.5 });
464
+ const column = new THREE.Mesh(geometry, material);
465
+ column.position.set(x, height / 2, z);
466
+ column.castShadow = true;
467
+ column.receiveShadow = true;
468
+ return column;
469
+ }
470
+
471
+ // Track segment generation
472
+ function createTrackSegment(type, inputPos, inputRot, params) {
473
+ const points = [];
474
+ const buildings = [];
475
+ const columns = [];
476
+ let length = params.length;
477
+ let width = params.streetWidth;
478
+ let height = params.buildingHeight;
479
+ let outputPos, outputRot;
480
+
481
+ if (type === 'straight') {
482
+ points.push(inputPos);
483
+ outputPos = inputPos.clone().add(new THREE.Vector3(0, 0, -length).applyEuler(inputRot));
484
+ points.push(outputPos);
485
+ outputRot = inputRot.clone();
486
+ for (let i = 0; i <= length; i += 10) {
487
+ const t = i / length;
488
+ const pos = inputPos.clone().lerp(outputPos, t);
489
+ buildings.push(createBuilding(pos.x + width / 2 + 5, pos.z, 10, length, height));
490
+ buildings.push(createBuilding(pos.x - width / 2 - 5, pos.z, 10, length, height));
491
+ if (i % 20 === 0) {
492
+ columns.push(createColumn(pos.x + width / 2, pos.z, height));
493
+ columns.push(createColumn(pos.x - width / 2, pos.z, height));
494
+ }
495
+ if (i % 30 === 0 && Math.random() < 0.3) {
496
+ createPickup(new THREE.Vector3(pos.x + (Math.random() - 0.5) * width / 2, 10, pos.z));
497
+ }
498
+ }
499
+ } else if (type === 'left') {
500
+ const center = inputPos.clone().add(new THREE.Vector3(length, 0, 0).applyEuler(inputRot));
501
+ for (let i = 0; i <= 16; i++) {
502
+ const angle = (i / 16) * Math.PI / 2;
503
+ const x = center.x - length * Math.cos(angle);
504
+ const z = center.z - length * Math.sin(angle);
505
+ points.push(new THREE.Vector3(x, inputPos.y, z));
506
+ if (i % 4 === 0) {
507
+ buildings.push(createBuilding(
508
+ x + Math.cos(angle) * (width / 2 + 5), z + Math.sin(angle) * (width / 2 + 5),
509
+ 10, 10, height
510
+ ));
511
+ buildings.push(createBuilding(
512
+ x - Math.cos(angle) * (width / 2 + 5), z - Math.sin(angle) * (width / 2 + 5),
513
+ 10, 10, height
514
+ ));
515
+ columns.push(createColumn(
516
+ x + Math.cos(angle) * width / 2, z + Math.sin(angle) * width / 2, height
517
+ ));
518
+ columns.push(createColumn(
519
+ x - Math.cos(angle) * width / 2, z - Math.sin(angle) * width / 2, height
520
+ ));
521
+ if (Math.random() < 0.3) {
522
+ createPickup(new THREE.Vector3(x + (Math.random() - 0.5) * width / 2, 10, z));
523
+ }
524
+ }
525
+ }
526
+ outputPos = points[points.length - 1];
527
+ outputRot = inputRot.clone();
528
+ outputRot.y += Math.PI / 2;
529
+ } else if (type === 'right') {
530
+ const center = inputPos.clone().add(new THREE.Vector3(-length, 0, 0).applyEuler(inputRot));
531
+ for (let i = 0; i <= 16; i++) {
532
+ const angle = (i / 16) * -Math.PI / 2;
533
+ const x = center.x - length * Math.cos(angle);
534
+ const z = center.z - length * Math.sin(angle);
535
+ points.push(new THREE.Vector3(x, inputPos.y, z));
536
+ if (i % 4 === 0) {
537
+ buildings.push(createBuilding(
538
+ x - Math.cos(angle) * (width / 2 + 5), z - Math.sin(angle) * (width / 2 + 5),
539
+ 10, 10, height
540
+ ));
541
+ buildings.push(createBuilding(
542
+ x + Math.cos(angle) * (width / 2 + 5), z + Math.sin(angle) * (width / 2 + 5),
543
+ 10, 10, height
544
+ ));
545
+ columns.push(createColumn(
546
+ x - Math.cos(angle) * width / 2, z - Math.sin(angle) * width / 2, height
547
+ ));
548
+ columns.push(createColumn(
549
+ x + Math.cos(angle) * width / 2, z + Math.sin(angle) * width / 2, height
550
+ ));
551
+ if (Math.random() < 0.3) {
552
+ createPickup(new THREE.Vector3(x + (Math.random() - 0.5) * width / 2, 10, z));
553
+ }
554
+ }
555
+ }
556
+ outputPos = points[points.length - 1];
557
+ outputRot = inputRot.clone();
558
+ outputRot.y -= Math.PI / 2;
559
+ } else if (type === 'up') {
560
+ points.push(inputPos);
561
+ outputPos = inputPos.clone().add(new THREE.Vector3(0, length, -length).applyEuler(inputRot));
562
+ points.push(outputPos);
563
+ outputRot = inputRot.clone();
564
+ outputRot.x += Math.PI / 4;
565
+ for (let i = 0; i <= length; i += 10) {
566
+ const t = i / length;
567
+ const pos = inputPos.clone().lerp(outputPos, t);
568
+ buildings.push(createBuilding(pos.x + width / 2 + 5, pos.z, 10, length, height));
569
+ buildings.push(createBuilding(pos.x - width / 2 - 5, pos.z, 10, length, height));
570
+ if (i % 20 === 0) {
571
+ columns.push(createColumn(pos.x + width / 2, pos.z, height));
572
+ columns.push(createColumn(pos.x - width / 2, pos.z, height));
573
+ }
574
+ if (i % 30 === 0 && Math.random() < 0.3) {
575
+ createPickup(new THREE.Vector3(pos.x + (Math.random() - 0.5) * width / 2, pos.z, pos.y));
576
+ }
577
+ }
578
+ } else if (type === 'down') {
579
+ points.push(inputPos);
580
+ outputPos = inputPos.clone().add(new THREE.Vector3(0, -length, -length).applyEuler(inputRot));
581
+ points.push(outputPos);
582
+ outputRot = inputRot.clone();
583
+ outputRot.x -= Math.PI / 4;
584
+ for (let i = 0; i <= length; i += 10) {
585
+ const t = i / length;
586
+ const pos = inputPos.clone().lerp(outputPos, t);
587
+ buildings.push(createBuilding(pos.x + width / 2 + 5, pos.z, 10, length, height));
588
+ buildings.push(createBuilding(pos.x - width / 2 - 5, pos.z, 10, length, height));
589
+ if (i % 20 === 0) {
590
+ columns.push(createColumn(pos.x + width / 2, pos.z, height));
591
+ columns.push(createColumn(pos.x - width / 2, pos.z, height));
592
+ }
593
+ if (i % 30 === 0 && Math.random() < 0.3) {
594
+ createPickup(new THREE.Vector3(pos.x + (Math.random() - 0.5) * width / 2, pos.z, pos.y));
595
+ }
596
+ }
597
+ } else if (type === 'gap') {
598
+ points.push(inputPos);
599
+ outputPos = inputPos.clone().add(new THREE.Vector3(0, 0, -length * 2).applyEuler(inputRot));
600
+ points.push(outputPos);
601
+ outputRot = inputRot.clone();
602
+ buildings.push(createBuilding(outputPos.x + width / 2 + 5, outputPos.z, 10, 10, height));
603
+ buildings.push(createBuilding(outputPos.x - width / 2 - 5, outputPos.z, 10, 10, height));
604
+ columns.push(createColumn(outputPos.x + width / 2, outputPos.z, height));
605
+ columns.push(createColumn(outputPos.x - width / 2, outputPos.z, height));
606
+ if (Math.random() < 0.3) {
607
+ createPickup(new THREE.Vector3(outputPos.x + (Math.random() - 0.5) * width / 2, 10, outputPos.z));
608
+ }
609
+ }
610
+
611
+ return { points, outputPos, outputRot, buildings, columns };
612
+ }
613
+
614
+ // Track generation
615
+ function generateTrack(params) {
616
+ const trackGroup = new THREE.Group();
617
+ const waypoints = [];
618
+ let currentPos = new THREE.Vector3(0, 10, 0);
619
+ let currentRot = new THREE.Euler(0, 0, 0);
620
+ const segmentTypes = params.segments;
621
+
622
+ segmentTypes.forEach(type => {
623
+ const segment = createTrackSegment(type, currentPos, currentRot, {
624
+ length: params.length,
625
+ streetWidth: params.streetWidth,
626
+ buildingHeight: params.buildingHeight
627
+ });
628
+ segment.buildings.forEach(building => trackGroup.add(building));
629
+ segment.columns.forEach(column => trackGroup.add(column));
630
+ segment.points.forEach(point => waypoints.push(point.clone()));
631
+ currentPos = segment.outputPos;
632
+ currentRot = segment.outputRot;
633
+ });
634
+
635
+ const finalSegment = createTrackSegment('straight', currentPos, currentRot, {
636
+ length: currentPos.distanceTo(new THREE.Vector3(0, 10, 0)),
637
+ streetWidth: params.streetWidth,
638
+ buildingHeight: params.buildingHeight
639
+ });
640
+ finalSegment.buildings.forEach(building => trackGroup.add(building));
641
+ finalSegment.columns.forEach(column => trackGroup.add(column));
642
+ finalSegment.points.forEach(point => waypoints.push(point.clone()));
643
+
644
+ return { trackGroup, waypoints, startPosition: new THREE.Vector3(0, 10, 0) };
645
+ }
646
+
647
+ // Track configurations
648
+ const trackConfigs = {
649
+ 'U of MN Twin Cities Track': { segments: ['straight', 'right', 'straight', 'left', 'up', 'straight', 'down', 'gap', 'straight'], length: 50, streetWidth: 30, buildingHeight: 40 },
650
+ 'Phelps Island Drift': { segments: ['straight', 'left', 'left', 'right', 'gap', 'straight', 'right', 'left', 'gap'], length: 45, streetWidth: 28, buildingHeight: 35 },
651
+ 'Mound Overlook Circuit': { segments: ['straight', 'up', 'right', 'straight', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
652
+ 'Lake Minnetonka Sprint': { segments: ['straight', 'left', 'right', 'gap', 'straight', 'up', 'down', 'left', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
653
+ 'St. Paul Riverbend Run': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
654
+ 'Cathedral Hill Climb': { segments: ['up', 'straight', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
655
+ 'Downtown Minneapolis Skyway Loop': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight', 'right'], length: 50, streetWidth: 32, buildingHeight: 45 },
656
+ 'Nicollet Mall Straightaway': { segments: ['straight', 'straight', 'straight', 'gap', 'straight', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
657
+ 'Lake of the Isles Island Circuit': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight', 'right'], length: 45, streetWidth: 28, buildingHeight: 35 },
658
+ 'Minnehaha Falls Meander': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight', 'left'], length: 48, streetWidth: 30, buildingHeight: 40 },
659
+ 'Milwaukee Loop': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
660
+ 'Waukesha Speedway': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
661
+ 'Manitowoc Marina Run': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
662
+ 'Lake Winnebago Circuit': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
663
+ 'Door County Coastal Cruise': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
664
+ 'Green Bay Bayfront Blitz': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
665
+ 'Sturgeon Bay Ship Canal Sprint': { segments: ['straight', 'straight', 'straight', 'gap', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
666
+ 'Wisconsin Dells Rapids Run': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
667
+ 'Madison Capitol Circuit': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
668
+ 'Superior North Shore Dash': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
669
+ 'Houston Oil Refinery Rush': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
670
+ 'Dallas Finance District Dash': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
671
+ 'Austin Tech Corridor Cruise': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
672
+ 'San Antonio Riverwalk Run': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
673
+ 'Corpus Christi Coastal Run': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
674
+ 'Fort Worth Stockyards Loop': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
675
+ 'Galveston Port Channel Circuit': { segments: ['straight', 'straight', 'straight', 'gap', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
676
+ 'El Paso Border Trade Boulevard': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
677
+ 'Lubbock Innovation Loop': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
678
+ 'Midland Permian Basin Sprint': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
679
+ 'Clearwater Curve': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
680
+ 'Miami Beach Boulevard': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
681
+ 'Florida Keys Canal Cruise': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
682
+ 'Orlando Theme Park Tour': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
683
+ 'Tampa Bay Finance District Drift': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
684
+ 'Jacksonville River City Run': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
685
+ 'Fort Lauderdale Cruise Port Circuit': { segments: ['straight', 'straight', 'straight', 'gap', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
686
+ 'Key West Sunset Sprint': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
687
+ 'St. Augustine Colonial Run': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
688
+ 'Palm Beach Gold Coast Circuit': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
689
+ 'Venice Beach Boardwalk Circuit': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
690
+ 'Los Angeles Star-Studded Speedway': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
691
+ 'San Francisco Golden Gate Run': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
692
+ 'San Diego Coastal Drift': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
693
+ 'Anaheim Theme Park Loop': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
694
+ 'Santa Barbara Vineyard Tour': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
695
+ 'Palm Springs Desert Dash': { segments: ['straight', 'straight', 'straight', 'gap', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
696
+ 'Santa Cruz Boardwalk Sprint': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
697
+ 'Monterey Bay Coastal Circuit': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
698
+ 'Carmel-by-the-Sea Scenic Route': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
699
+ 'Napa Valley Vineyard Ride': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 }
700
+ };
701
+
702
+ let currentTrackGroup = null;
703
+ let waypoints = [];
704
+
705
+ // Controls
706
+ const keys = { w: false, a: false, s: false, d: false, space: false };
707
+ document.addEventListener('keydown', (event) => {
708
+ switch (event.key.toLowerCase()) {
709
+ case 'w': keys.w = true; break;
710
+ case 'a': keys.a = true; break;
711
+ case 's': keys.s = true; break;
712
+ case 'd': keys.d = true; break;
713
+ case ' ': keys.space = true; break;
714
+ }
715
+ });
716
+ document.addEventListener('keyup', (event) => {
717
+ switch (event.key.toLowerCase()) {
718
+ case 'w': keys.w = false; break;
719
+ case 'a': keys.a = false; break;
720
+ case 's': keys.s = false; break;
721
+ case 'd': keys.d = false; break;
722
+ case ' ': keys.space = false; break;
723
+ }
724
+ });
725
+
726
+ // Shooting
727
+ let canShoot = true;
728
+ let shotCooldown = 200;
729
+ const raycaster = new THREE.Raycaster();
730
+ const mouse = new THREE.Vector2();
731
+ document.addEventListener('mousedown', (event) => {
732
+ if (event.button === 0 && canShoot && gameState === 'racing' && playerData.active) {
733
+ canShoot = false;
734
+ setTimeout(() => canShoot = true, shotCooldown);
735
+ mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
736
+ mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
737
+ raycaster.setFromCamera(mouse, camera);
738
+ const targets = [...aiShips, ...(currentTrackGroup ? currentTrackGroup.children.filter(c => c.isMesh && c.geometry.type === 'BoxGeometry') : [])].filter(t => t !== playerShip && (!t.userData.isPlayer || !t.userData.active));
739
+ const intersects = raycaster.intersectObjects(targets);
740
+ if (intersects.length > 0) {
741
+ const target = intersects[0].object;
742
+ const position = playerShip.position.clone().add(new THREE.Vector3(0, 0, -2));
743
+ const count = skills.shotCount.effect(skills.shotCount.level);
744
+ for (let i = 0; i < count; i++) {
745
+ createMissile(position.clone().add(new THREE.Vector3((Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.5, 0)), target);
746
+ }
747
+ }
748
+ }
749
+ });
750
+
751
+ // Update character sheet
752
+ function updateCharacterSheet() {
753
+ document.getElementById('ship-type').textContent = `Type: ${playerData.type}`;
754
+ document.getElementById('thruster-1').textContent = `Thruster 1: ${playerData.thrusters[0]}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
755
+ document.getElementById('thruster-2').textContent = `Thruster 2: ${playerData.thrusters[1]}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
756
+ document.getElementById('thruster-3').textContent = `Thruster 3: ${playerData.thrusters[2]}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
757
+ document.getElementById('thruster-4').textContent = `Thruster 4: ${playerData.thrusters[3]}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
758
+ document.getElementById('body-front').textContent = `Body Front: ${playerData.body.front}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
759
+ document.getElementById('body-back').textContent = `Body Back: ${playerData.body.back}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
760
+ document.getElementById('body-left').textContent = `Body Left: ${playerData.body.left}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
761
+ document.getElementById('body-right').textContent = `Body Right: ${playerData.body.right}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
762
+ }
763
+
764
+ // Update skills sheet
765
+ function updateSkillsSheet() {
766
+ document.getElementById('skill-speed').textContent = skills.speed.level;
767
+ document.getElementById('skill-speed-meter').style.width = `${(skills.speed.level / skills.speed.maxLevel) * 100}%`;
768
+ document.getElementById('skill-shot-power').textContent = skills.shotPower.level;
769
+ document.getElementById('skill-shot-power-meter').style.width = `${(skills.shotPower.level / skills.shotPower.maxLevel) * 100}%`;
770
+ document.getElementById('skill-shot-count').textContent = skills.shotCount.level;
771
+ document.getElementById('skill-shot-count-meter').style.width = `${(skills.shotCount.level / skills.shotCount.maxLevel) * 100}%`;
772
+ document.getElementById('skill-turn-speed').textContent = skills.turnSpeed.level;
773
+ document.getElementById('skill-turn-speed-meter').style.width = `${(skills.turnSpeed.level / skills.turnSpeed.maxLevel) * 100}%`;
774
+ }
775
+
776
+ // Damage handling for ships
777
+ function applyDamage(ship, damage, hitPosition) {
778
+ if (!ship.userData.active) return;
779
+ const direction = hitPosition.clone().sub(ship.position).normalize();
780
+ const absX = Math.abs(direction.x);
781
+ const absZ = Math.abs(direction.z);
782
+ let component;
783
+ if (absZ > absX) {
784
+ component = direction.z > 0 ? 'front' : 'back';
785
+ } else {
786
+ component = direction.x > 0 ? 'right' : 'left';
787
+ }
788
+ if (Math.random() < 0.4) {
789
+ const thrusterIndex = Math.floor(Math.random() * 4);
790
+ if (ship.userData.thrusters[thrusterIndex] > 0) {
791
+ ship.userData.thrusters[thrusterIndex] -= damage;
792
+ if (ship.userData.thrusters[thrusterIndex] <= 0) {
793
+ ship.userData.thrusters[thrusterIndex] = 0;
794
+ }
795
+ }
796
+ } else {
797
+ if (ship.userData.body[component] > 0) {
798
+ ship.userData.body[component] -= damage;
799
+ if (ship.userData.body[component] <= 0) {
800
+ ship.userData.body[component] = 0;
801
+ }
802
+ }
803
+ }
804
+ createExplosion(hitPosition);
805
+ if (ship === playerShip) {
806
+ updateCharacterSheet();
807
+ }
808
+ checkShipStatus(ship, hitPosition);
809
+ }
810
+
811
+ function checkShipStatus(ship, hitPosition) {
812
+ const totalHP = ship.userData.thrusters.reduce((a, b) => a + b, 0) +
813
+ Object.values(ship.userData.body).reduce((a, b) => a + b, 0);
814
+ if (totalHP <= 0) {
815
+ ship.userData.active = false;
816
+ ship.visible = false;
817
+ createExplosion(ship.position);
818
+ if (ship.userData.splitCount < 3) {
819
+ const newScale = 0.5;
820
+ const offsets = [
821
+ new THREE.Vector3(1, 0, 1),
822
+ new THREE.Vector3(-1, 0, -1)
823
+ ];
824
+ offsets.forEach(offset => {
825
+ const newShip = createShip(
826
+ ship.userData.typeIndex,
827
+ false,
828
+ newScale,
829
+ ship.userData.splitCount + 1
830
+ );
831
+ newShip.position.copy(ship.position).add(offset.multiplyScalar(2));
832
+ newShip.userData.currentWaypoint = ship.userData.currentWaypoint;
833
+ });
834
+ }
835
+ if (!ship.userData.isPlayer) {
836
+ const index = aiShips.indexOf(ship);
837
+ if (index > -1) aiShips.splice(index, 1);
838
+ score += 100;
839
+ document.getElementById('score').textContent = `Score: ${score}`;
840
+ }
841
+ if (ship === playerShip) {
842
+ document.getElementById('status').textContent = 'Status: Destroyed';
843
+ endRace();
844
+ }
845
+ }
846
+ }
847
+
848
+ // Building damage
849
+ function applyBuildingDamage(building, damage, hitPosition) {
850
+ building.userData.currentHP -= damage;
851
+ createExplosion(hitPosition);
852
+ if (building.userData.currentHP <= 0) {
853
+ if (building.userData.splitCount < 3) {
854
+ const newScale = 0.5;
855
+ const hitLocal = building.worldToLocal(hitPosition.clone());
856
+ const width = building.userData.width * newScale;
857
+ const depth = building.userData.depth * newScale;
858
+ const height = building.userData.height * newScale;
859
+ // Determine split direction based on hit position
860
+ const absX = Math.abs(hitLocal.x);
861
+ const absZ = Math.abs(hitLocal.z);
862
+ const isXSplit = absX > absZ;
863
+ const splitSize = (isXSplit ? building.userData.width : building.userData.depth) / 3;
864
+ const remainingSize = splitSize * 2 * newScale;
865
+ const offsets = [
866
+ new THREE.Vector3(isXSplit ? splitSize : 0, 0, isXSplit ? 0 : splitSize).multiplyScalar(newScale),
867
+ new THREE.Vector3(isXSplit ? -splitSize : 0, 0, isXSplit ? 0 : -splitSize).multiplyScalar(newScale)
868
+ ];
869
+ offsets.forEach(offset => {
870
+ const newBuilding = createBuilding(
871
+ building.position.x + offset.x,
872
+ building.position.z + offset.z,
873
+ isXSplit ? remainingSize : width,
874
+ isXSplit ? depth : remainingSize,
875
+ height,
876
+ newScale,
877
+ building.userData.splitCount + 1
878
+ );
879
+ newBuilding.userData.isFalling = true;
880
+ newBuilding.userData.fallVelocity = 0;
881
+ currentTrackGroup.add(newBuilding);
882
+ });
883
+ score += 100;
884
+ document.getElementById('score').textContent = `Score: ${score}`;
885
+ }
886
+ currentTrackGroup.remove(building);
887
+ } else {
888
+ // Initiate falling if not already falling
889
+ if (!building.userData.isFalling) {
890
+ building.userData.isFalling = true;
891
+ building.userData.fallVelocity = 0;
892
+ }
893
+ }
894
+ }
895
+
896
+ // Update falling buildings
897
+ function updateFallingBuildings() {
898
+ if (!currentTrackGroup) return;
899
+ const buildings = currentTrackGroup.children.filter(c => c.isMesh && c.geometry.type === 'BoxGeometry');
900
+ for (let i = buildings.length - 1; i >= 0; i--) {
901
+ const building = buildings[i];
902
+ if (building.userData.isFalling) {
903
+ building.userData.fallVelocity += 0.1; // Gravity
904
+ building.position.y -= building.userData.fallVelocity / 60;
905
+ if (building.position.y <= building.userData.height * 0.5) {
906
+ building.position.y = building.userData.height * 0.5;
907
+ building.userData.currentHP -= 2; // Additional damage on impact
908
+ createExplosion(building.position);
909
+ if (building.userData.currentHP <= 0) {
910
+ if (building.userData.splitCount < 3) {
911
+ const newScale = 0.5;
912
+ const width = building.userData.width * newScale;
913
+ const depth = building.userData.depth * newScale;
914
+ const height = building.userData.height * newScale;
915
+ const offsets = [
916
+ new THREE.Vector3(5 * newScale, 0, 5 * newScale),
917
+ new THREE.Vector3(-5 * newScale, 0, -5 * newScale)
918
+ ];
919
+ offsets.forEach(offset => {
920
+ const newBuilding = createBuilding(
921
+ building.position.x + offset.x,
922
+ building.position.z + offset.z,
923
+ width,
924
+ depth,
925
+ height,
926
+ newScale,
927
+ building.userData.splitCount + 1
928
+ );
929
+ currentTrackGroup.add(newBuilding);
930
+ });
931
+ score += 100;
932
+ document.getElementById('score').textContent = `Score: ${score}`;
933
+ }
934
+ currentTrackGroup.remove(building);
935
+ }
936
+ }
937
+ }
938
+ }
939
+ }
940
+
941
+ // Pickup handling
942
+ function updatePickups() {
943
+ for (let i = pickups.length - 1; i >= 0; i--) {
944
+ const pickup = pickups[i];
945
+ if (playerShip.position.distanceTo(pickup.position) < 2 && playerData.active) {
946
+ const skill = skills[pickup.userData.skill];
947
+ if (skill.level < skill.maxLevel) {
948
+ skill.level++;
949
+ updateSkillsSheet();
950
+ }
951
+ scene.remove(pickup);
952
+ pickups.splice(i, 1);
953
+ }
954
+ }
955
+ }
956
+
957
+ // Missile update
958
+ function updateMissiles() {
959
+ for (let i = missiles.length - 1; i >= 0; i--) {
960
+ const missile = missiles[i];
961
+ missile.userData.lifetime--;
962
+ if (missile.userData.lifetime <= 0 || !missile.userData.target || (!missile.userData.target.userData?.active && !missile.userData.target.isMesh)) {
963
+ scene.remove(missile);
964
+ missiles.splice(i, 1);
965
+ continue;
966
+ }
967
+ const targetPos = missile.userData.target.isMesh ? missile.userData.target.position : missile.userData.target.position;
968
+ const direction = targetPos.clone().sub(missile.position).normalize();
969
+ missile.position.add(direction.multiplyScalar(missile.userData.speed));
970
+ missile.lookAt(targetPos);
971
+ missile.rotation.x += Math.PI / 2;
972
+
973
+ // Update flame trail
974
+ const flame = missile.children[0];
975
+ const positions = flame.geometry.attributes.position.array;
976
+ for (let j = 0; j < positions.length / 3; j++) {
977
+ const j3 = j * 3;
978
+ positions[j3] = (Math.random() - 0.5) * 0.1;
979
+ positions[j3 + 1] = (Math.random() - 0.5) * 0.1;
980
+ positions[j3 + 2] = -0.3 - Math.random() * 0.2;
981
+ }
982
+ flame.geometry.attributes.position.needsUpdate = true;
983
+
984
+ // Check collision
985
+ const missileBox = new THREE.Box3().setFromObject(missile);
986
+ const targetBox = new THREE.Box3().setFromObject(missile.userData.target);
987
+ if (missileBox.intersectsBox(targetBox)) {
988
+ if (missile.userData.target.isMesh && missile.userData.target.geometry.type === 'BoxGeometry') {
989
+ applyBuildingDamage(missile.userData.target, missile.userData.damage, missile.position);
990
+ } else {
991
+ applyDamage(missile.userData.target, missile.userData.damage, missile.position);
992
+ }
993
+ scene.remove(missile);
994
+ missiles.splice(i, 1);
995
+ }
996
+ }
997
+ }
998
+
999
+ // Player movement
1000
+ let rotation = 0;
1001
+ let bankAngle = 0;
1002
+ let jumpHeight = 0;
1003
+ function updatePlayer() {
1004
+ if (!playerData.active) return;
1005
+ const activeThrusters = playerData.thrusters.filter(hp => hp > 0).length;
1006
+ const speedMultiplier = skills.speed.effect(skills.speed.level);
1007
+ const maxSpeed = playerData.maxSpeed * (activeThrusters / 4) * speedMultiplier;
1008
+ const acceleration = playerData.acceleration * (activeThrusters / 4) * speedMultiplier;
1009
+ const turnSpeed = skills.turnSpeed.effect(skills.turnSpeed.level);
1010
+ if (keys.w) playerData.speed = Math.min(playerData.speed + acceleration, maxSpeed);
1011
+ else if (keys.s) playerData.speed = Math.max(playerData.speed - acceleration, -maxSpeed / 2);
1012
+ else playerData.speed *= 0.995;
1013
+ if (keys.a) rotation += turnSpeed * (activeThrusters / 4);
1014
+ if (keys.d) rotation -= turnSpeed * (activeThrusters / 4);
1015
+ bankAngle = (keys.a ? -0.3 : 0) + (keys.d ? 0.3 : 0);
1016
+
1017
+ if (keys.space && !isJumping && jumpCooldown <= 0 && activeThrusters > 0) {
1018
+ isJumping = true;
1019
+ jumpHeight = 5;
1020
+ jumpCooldown = 60;
1021
+ }
1022
+ if (isJumping) {
1023
+ jumpHeight -= 0.2;
1024
+ if (jumpHeight <= 0) {
1025
+ jumpHeight = 0;
1026
+ isJumping = false;
1027
+ }
1028
+ }
1029
+ if (jumpCooldown > 0) jumpCooldown--;
1030
+
1031
+ playerShip.rotation.z = rotation;
1032
+ playerShip.rotation.x = bankAngle;
1033
+ const direction = new THREE.Vector3(0, 0, -1).applyAxisAngle(new THREE.Vector3(0, 1, 0), rotation);
1034
+ playerShip.position.add(direction.multiplyScalar(playerData.speed));
1035
+ playerShip.position.y = 10 + Math.sin(Date.now() * 0.005) * 0.2 + jumpHeight;
1036
+ checkCollisions(playerShip);
1037
+ checkColumnProximity(playerShip);
1038
+ updatePickups();
1039
+ camera.position.copy(playerShip.position).add(new THREE.Vector3(0, 5 + jumpHeight, 15).applyAxisAngle(new THREE.Vector3(0, 1, 0), rotation));
1040
+ camera.lookAt(playerShip.position);
1041
+ }
1042
+
1043
+ // Collision detection
1044
+ function checkCollisions(ship) {
1045
+ if (!currentTrackGroup || !ship.userData.active) return;
1046
+ currentTrackGroup.traverse(child => {
1047
+ if (child.isMesh && child.geometry.type !== 'CylinderGeometry') {
1048
+ const shipBox = new THREE.Box3().setFromObject(ship);
1049
+ const wallBox = new THREE.Box3().setFromObject(child);
1050
+ if (shipBox.intersectsBox(wallBox) && !isJumping) {
1051
+ const direction = ship.position.clone().sub(child.position).normalize();
1052
+ ship.position.add(direction.multiplyScalar(0.5));
1053
+ ship.userData.speed *= 0.8;
1054
+ applyDamage(ship, 2, ship.position);
1055
+ createExplosion(ship.position, true); // Collision particles
1056
+ }
1057
+ }
1058
+ });
1059
+ }
1060
+
1061
+ // Column proximity nudging
1062
+ function checkColumnProximity(ship) {
1063
+ if (!currentTrackGroup || !ship.userData.active) return;
1064
+ currentTrackGroup.traverse(child => {
1065
+ if (child.isMesh && child.geometry.type === 'CylinderGeometry') {
1066
+ const shipPos = ship.position;
1067
+ const columnPos = child.position;
1068
+ const distance = shipPos.distanceTo(columnPos);
1069
+ if (distance < 5) {
1070
+ const direction = shipPos.clone().sub(columnPos).normalize();
1071
+ ship.position.add(direction.multiplyScalar(0.2));
1072
+ createExplosion(ship.position, true); // Collision particles
1073
+ }
1074
+ }
1075
+ });
1076
+ }
1077
+
1078
+ // Explosion update
1079
+ const explosions = [];
1080
+ function updateExplosions() {
1081
+ for (let i = explosions.length - 1; i >= 0; i--) {
1082
+ const explosion = explosions[i];
1083
+ explosion.userData.lifetime--;
1084
+ if (explosion.userData.lifetime <= 0) {
1085
+ scene.remove(explosion);
1086
+ explosions.splice(i, 1);
1087
+ continue;
1088
+ }
1089
+ const positions = explosion.geometry.attributes.position.array;
1090
+ for (let j = 0; j < positions.length / 3; j++) {
1091
+ const j3 = j * 3;
1092
+ positions[j3] += explosion.userData.velocities[j].x;
1093
+ positions[j3 + 1] += explosion.userData.velocities[j].y;
1094
+ positions[j3 + 2] += explosion.userData.velocities[j].z;
1095
+ }
1096
+ explosion.geometry.attributes.position.needsUpdate = true;
1097
+ explosion.material.opacity = explosion.userData.lifetime / (explosion.userData.lifetime <= 30 ? 30 : 60);
1098
+ }
1099
+ }
1100
+
1101
+ // AI movement
1102
+ function updateAI() {
1103
+ aiShips.forEach((ship, index) => {
1104
+ if (!ship.userData.active) return;
1105
+ const activeThrusters = ship.userData.thrusters.filter(hp => hp > 0).length;
1106
+ const maxSpeed = ship.userData.maxSpeed * (activeThrusters / 4);
1107
+ const target = waypoints[ship.userData.currentWaypoint];
1108
+ const direction = target.clone().sub(ship.position).normalize();
1109
+ ship.position.add(direction.multiplyScalar(maxSpeed));
1110
+ ship.rotation.z = Math.atan2(direction.x, -direction.z);
1111
+ ship.position.y = 10 + Math.sin(Date.now() * 0.005 + index) * 0.2;
1112
+ if (ship.position.distanceTo(target) < 2) {
1113
+ ship.userData.currentWaypoint = (ship.userData.currentWaypoint + 1) % waypoints.length;
1114
+ }
1115
+ checkCollisions(ship);
1116
+ checkColumnProximity(ship);
1117
+ });
1118
+ }
1119
+
1120
+ // Race start
1121
+ function startRace(trackName) {
1122
+ gameState = 'racing';
1123
+ currentTrack = trackName;
1124
+ if (currentTrackGroup) scene.remove(currentTrackGroup);
1125
+ pickups.forEach(p => scene.remove(p));
1126
+ missiles.forEach(m => scene.remove(m));
1127
+ explosions.forEach(e => scene.remove(e));
1128
+ pickups.length = 0;
1129
+ missiles.length = 0;
1130
+ explosions.length = 0;
1131
+ const { trackGroup, waypoints: newWaypoints, startPosition } = generateTrack(trackConfigs[trackName]);
1132
+ currentTrackGroup = trackGroup;
1133
+ waypoints = newWaypoints;
1134
+ scene.add(trackGroup);
1135
+ playerShip.position.set(startPosition.x, 10, startPosition.z);
1136
+ playerShip.rotation.z = Math.atan2(waypoints[1].x - waypoints[0].x, waypoints[1].z - waypoints[0].z);
1137
+ playerData.speed = 0;
1138
+ playerData.active = true;
1139
+ playerShip.visible = true;
1140
+ aiShips.forEach((ship, i) => {
1141
+ ship.position.set(startPosition.x + (i + 1) * 5, 10, startPosition.z);
1142
+ ship.userData.currentWaypoint = 0;
1143
+ ship.userData.lastGatePass = 0;
1144
+ ship.userData.active = true;
1145
+ ship.visible = true;
1146
+ const type = shipTypes.find(t => t.name === ship.userData.type);
1147
+ ship.userData.thrusters = Array(4).fill(type.maxHP);
1148
+ ship.userData.body = { front: type.maxHP, back: type.maxHP, left: type.maxHP, right: type.maxHP };
1149
+ });
1150
+ startGate.position.copy(startPosition);
1151
+ startGate.position.y = 12;
1152
+ startGate.rotation.y = Math.atan2(waypoints[1].x - waypoints[0].x, waypoints[1].z - waypoints[0].z);
1153
+ raceTime = 0;
1154
+ score = 0;
1155
+ document.getElementById('time').textContent = `Time: 0`;
1156
+ document.getElementById('score').textContent = `Score: ${score}`;
1157
+ document.getElementById('status').textContent = `Status: Active`;
1158
+ updateCharacterSheet();
1159
+ updateSkillsSheet();
1160
+ }
1161
+
1162
+ // Restart race
1163
+ function restartRace() {
1164
+ scene.remove(playerShip);
1165
+ explosions.forEach(e => scene.remove(e));
1166
+ pickups.forEach(p => scene.remove(p));
1167
+ missiles.forEach(m => scene.remove(m));
1168
+ explosions.length = 0;
1169
+ pickups.length = 0;
1170
+ missiles.length = 0;
1171
+ initPlayer();
1172
+ startRace(currentTrack);
1173
+ }
1174
+
1175
+ // Race end
1176
+ function endRace() {
1177
+ gameState = 'menu';
1178
+ const activeShips = aiShips.filter(s => s.userData.active).length + (playerData.active ? 1 : 0);
1179
+ alert(`Race Over! Ships Remaining: ${activeShips}, Time: ${raceTime.toFixed(2)}s, Score: ${score}`);
1180
+ }
1181
+
1182
+ // Gallery
1183
+ function showGallery() {
1184
+ document.getElementById('gallery').style.display = 'block';
1185
+ }
1186
+ function hideGallery() {
1187
+ document.getElementById('gallery').style.display = 'none';
1188
+ }
1189
+
1190
+ // Game loop
1191
+ function animate() {
1192
+ requestAnimationFrame(animate);
1193
+ if (gameState === 'racing') {
1194
+ updatePlayer();
1195
+ updateAI();
1196
+ updateMissiles();
1197
+ updateExplosions();
1198
+ updateFallingBuildings();
1199
+ raceTime += 1 / 60;
1200
+ document.getElementById('time').textContent = `Time: ${raceTime.toFixed(2)}`;
1201
+ if (aiShips.every(s => !s.userData.active) && playerData.active) {
1202
+ endRace();
1203
+ }
1204
+ }
1205
+ renderer.render(scene, camera);
1206
+ }
1207
+
1208
+ // Resize
1209
+ window.addEventListener('resize', () => {
1210
+ camera.aspect = window.innerWidth / window.innerHeight;
1211
+ camera.updateProjectionMatrix();
1212
+ renderer.setSize(window.innerWidth, window.innerHeight);
1213
+ });
1214
+
1215
+ // Start
1216
+ initPlayer();
1217
+ startRace('Phelps Island Drift');
1218
+ animate();
1219
+ </script>
1220
+ </body>
1221
+ </html>