Spaces:
Running
Running
Update index.html
Browse files- index.html +1221 -19
index.html
CHANGED
@@ -1,19 +1,1221 @@
|
|
1 |
-
<!
|
2 |
-
<html>
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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>
|