Spaces:
Running
Running
Update index.html
Browse files- index.html +236 -65
index.html
CHANGED
@@ -54,11 +54,11 @@
|
|
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 {
|
@@ -93,8 +93,9 @@
|
|
93 |
</head>
|
94 |
<body>
|
95 |
<div class="ui-container" id="race-ui">
|
96 |
-
<h2>Starship Circuit Commander
|
97 |
<div id="time">Time: 0</div>
|
|
|
98 |
<div id="status">Status: Active</div>
|
99 |
</div>
|
100 |
<div class="sidebar" id="sidebar">
|
@@ -176,6 +177,10 @@
|
|
176 |
<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>
|
177 |
<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>
|
178 |
</div>
|
|
|
|
|
|
|
|
|
179 |
</div>
|
180 |
<div class="gallery" id="gallery">
|
181 |
<h2>Lake Minnetonka Gallery</h2>
|
@@ -191,10 +196,12 @@
|
|
191 |
// Game state
|
192 |
let gameState = 'racing';
|
193 |
let raceTime = 0;
|
|
|
194 |
let currentTrack = 'Phelps Island Drift';
|
195 |
let lastGatePass = 0;
|
196 |
let isJumping = false;
|
197 |
let jumpCooldown = 0;
|
|
|
198 |
|
199 |
// Scene setup
|
200 |
const scene = new THREE.Scene();
|
@@ -262,10 +269,15 @@
|
|
262 |
// Player ship
|
263 |
let playerShip = null;
|
264 |
let playerData = null;
|
265 |
-
function createShip(typeIndex, isPlayer) {
|
266 |
const type = shipTypes[typeIndex];
|
267 |
const ship = new THREE.Group();
|
268 |
-
const
|
|
|
|
|
|
|
|
|
|
|
269 |
const bodyMaterial = new THREE.MeshStandardMaterial({
|
270 |
color: isPlayer ? 0x00ff00 : 0xff0000,
|
271 |
metalness: 0.8,
|
@@ -274,38 +286,43 @@
|
|
274 |
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
275 |
ship.add(body);
|
276 |
if (type.name !== 'Rocketman') {
|
277 |
-
const thrusterGeometry = new THREE.CylinderGeometry(0.2, 0.2, 0.5, 8);
|
278 |
const thrusterMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa });
|
279 |
for (let i = 0; i < 4; i++) {
|
280 |
const thruster = new THREE.Mesh(thrusterGeometry, thrusterMaterial);
|
281 |
thruster.position.set(
|
282 |
-
(i % 2 === 0 ? 0.5 : -0.5) *
|
283 |
0,
|
284 |
-
(i < 2 ? 0.5 : -0.5) *
|
285 |
);
|
286 |
thruster.rotation.x = Math.PI / 2;
|
287 |
ship.add(thruster);
|
288 |
}
|
289 |
} else {
|
290 |
-
const jetpackGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.8, 8);
|
291 |
const jetpack = new THREE.Mesh(jetpackGeometry, new THREE.MeshStandardMaterial({ color: 0xaaaaaa }));
|
292 |
-
jetpack.position.set(0, -0.5, 0);
|
293 |
ship.add(jetpack);
|
294 |
}
|
295 |
ship.position.y = 10;
|
296 |
ship.castShadow = true;
|
|
|
297 |
ship.userData = {
|
298 |
type: type.name,
|
299 |
maxSpeed: type.speed,
|
300 |
acceleration: type.accel,
|
301 |
-
thrusters: Array(4).fill(
|
302 |
-
body: { front:
|
303 |
active: true,
|
304 |
speed: 0,
|
305 |
currentWaypoint: 0,
|
306 |
-
lastGatePass: 0
|
|
|
|
|
|
|
307 |
};
|
308 |
scene.add(ship);
|
|
|
309 |
return ship;
|
310 |
}
|
311 |
|
@@ -314,10 +331,12 @@
|
|
314 |
const typeIndex = Math.floor(Math.random() * shipTypes.length);
|
315 |
playerShip = createShip(typeIndex, true);
|
316 |
playerData = playerShip.userData;
|
317 |
-
// Reset skills
|
318 |
Object.keys(skills).forEach(skill => skills[skill].level = 0);
|
|
|
|
|
319 |
updateCharacterSheet();
|
320 |
updateSkillsSheet();
|
|
|
321 |
}
|
322 |
|
323 |
// AI opponents
|
@@ -333,20 +352,51 @@
|
|
333 |
const startGate = new THREE.Mesh(gateGeometry, gateMaterial);
|
334 |
scene.add(startGate);
|
335 |
|
336 |
-
//
|
337 |
-
const
|
338 |
-
function
|
339 |
-
const geometry = new THREE.
|
340 |
-
const material = new THREE.
|
341 |
-
const
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
346 |
damage: skills.shotPower.effect(skills.shotPower.level)
|
347 |
};
|
348 |
-
scene.add(
|
349 |
-
|
|
|
350 |
}
|
351 |
|
352 |
// Particle system for explosions
|
@@ -389,17 +439,25 @@
|
|
389 |
}
|
390 |
|
391 |
// Building and column generation
|
392 |
-
function createBuilding(x, z, width, depth, height) {
|
393 |
-
const geometry = new THREE.BoxGeometry(width, height, depth);
|
394 |
const material = new THREE.MeshStandardMaterial({
|
395 |
color: Math.random() * 0xffffff,
|
396 |
roughness: 0.7,
|
397 |
metalness: 0.3
|
398 |
});
|
399 |
const building = new THREE.Mesh(geometry, material);
|
400 |
-
building.position.set(x, height / 2, z);
|
401 |
building.castShadow = true;
|
402 |
building.receiveShadow = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
403 |
return building;
|
404 |
}
|
405 |
|
@@ -625,7 +683,7 @@
|
|
625 |
'Miami Beach Boulevard': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
|
626 |
'Florida Keys Canal Cruise': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
|
627 |
'Orlando Theme Park Tour': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
|
628 |
-
'Tampa Bay Finance District Drift': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', '
|
629 |
'Jacksonville River City Run': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
|
630 |
'Fort Lauderdale Cruise Port Circuit': { segments: ['straight', 'straight', 'straight', 'gap', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
|
631 |
'Key West Sunset Sprint': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
|
@@ -671,21 +729,50 @@
|
|
671 |
// Shooting
|
672 |
let canShoot = true;
|
673 |
let shotCooldown = 200;
|
|
|
|
|
674 |
document.addEventListener('mousedown', (event) => {
|
675 |
if (event.button === 0 && canShoot && gameState === 'racing' && playerData.active) {
|
676 |
canShoot = false;
|
677 |
setTimeout(() => canShoot = true, shotCooldown);
|
678 |
-
|
679 |
-
|
680 |
-
|
681 |
-
const
|
682 |
-
|
683 |
-
|
684 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
685 |
}
|
686 |
}
|
687 |
});
|
688 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
689 |
// Update character sheet
|
690 |
function updateCharacterSheet() {
|
691 |
document.getElementById('ship-type').textContent = `Type: ${playerData.type}`;
|
@@ -742,24 +829,81 @@
|
|
742 |
createExplosion(hitPosition);
|
743 |
if (ship === playerShip) {
|
744 |
updateCharacterSheet();
|
|
|
745 |
}
|
746 |
checkShipStatus(ship);
|
747 |
}
|
748 |
|
749 |
-
function checkShipStatus(ship) {
|
750 |
const totalHP = ship.userData.thrusters.reduce((a, b) => a + b, 0) +
|
751 |
Object.values(ship.userData.body).reduce((a, b) => a + b, 0);
|
752 |
if (totalHP <= 0) {
|
753 |
ship.userData.active = false;
|
754 |
ship.visible = false;
|
755 |
createExplosion(ship.position);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
756 |
if (ship === playerShip) {
|
757 |
document.getElementById('status').textContent = 'Status: Destroyed';
|
|
|
758 |
endRace();
|
759 |
}
|
760 |
}
|
761 |
}
|
762 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
763 |
// Pickup handling
|
764 |
function updatePickups() {
|
765 |
for (let i = pickups.length - 1; i >= 0; i--) {
|
@@ -769,6 +913,7 @@
|
|
769 |
if (skill.level < skill.maxLevel) {
|
770 |
skill.level++;
|
771 |
updateSkillsSheet();
|
|
|
772 |
}
|
773 |
scene.remove(pickup);
|
774 |
pickups.splice(i, 1);
|
@@ -776,6 +921,48 @@
|
|
776 |
}
|
777 |
}
|
778 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
779 |
// Player movement
|
780 |
let rotation = 0;
|
781 |
let bankAngle = 0;
|
@@ -798,6 +985,7 @@
|
|
798 |
isJumping = true;
|
799 |
jumpHeight = 5;
|
800 |
jumpCooldown = 60;
|
|
|
801 |
}
|
802 |
if (isJumping) {
|
803 |
jumpHeight -= 0.2;
|
@@ -853,29 +1041,6 @@
|
|
853 |
});
|
854 |
}
|
855 |
|
856 |
-
// Bullet update
|
857 |
-
function updateBullets() {
|
858 |
-
const raycaster = new THREE.Raycaster();
|
859 |
-
for (let i = bullets.length - 1; i >= 0; i--) {
|
860 |
-
const bullet = bullets[i];
|
861 |
-
bullet.userData.lifetime--;
|
862 |
-
if (bullet.userData.lifetime <= 0) {
|
863 |
-
scene.remove(bullet);
|
864 |
-
bullets.splice(i, 1);
|
865 |
-
continue;
|
866 |
-
}
|
867 |
-
bullet.position.add(bullet.userData.direction.clone().multiplyScalar(1));
|
868 |
-
raycaster.set(bullet.position, bullet.userData.direction);
|
869 |
-
const intersects = raycaster.intersectObjects([...aiShips, playerShip].filter(s => s.userData.active));
|
870 |
-
if (intersects.length > 0) {
|
871 |
-
const hitShip = intersects[0].object.parent;
|
872 |
-
applyDamage(hitShip, bullet.userData.damage, bullet.position);
|
873 |
-
scene.remove(bullet);
|
874 |
-
bullets.splice(i, 1);
|
875 |
-
}
|
876 |
-
}
|
877 |
-
}
|
878 |
-
|
879 |
// Explosion update
|
880 |
const explosions = [];
|
881 |
function updateExplosions() {
|
@@ -924,7 +1089,9 @@
|
|
924 |
currentTrack = trackName;
|
925 |
if (currentTrackGroup) scene.remove(currentTrackGroup);
|
926 |
pickups.forEach(p => scene.remove(p));
|
|
|
927 |
pickups.length = 0;
|
|
|
928 |
const { trackGroup, waypoints: newWaypoints, startPosition } = generateTrack(trackConfigs[trackName]);
|
929 |
currentTrackGroup = trackGroup;
|
930 |
waypoints = newWaypoints;
|
@@ -948,21 +1115,24 @@
|
|
948 |
startGate.position.y = 12;
|
949 |
startGate.rotation.y = Math.atan2(waypoints[1].x - waypoints[0].x, waypoints[1].z - waypoints[0].z);
|
950 |
raceTime = 0;
|
|
|
951 |
document.getElementById('time').textContent = `Time: 0`;
|
|
|
952 |
document.getElementById('status').textContent = `Status: Active`;
|
953 |
updateCharacterSheet();
|
954 |
updateSkillsSheet();
|
|
|
955 |
}
|
956 |
|
957 |
// Restart race
|
958 |
function restartRace() {
|
959 |
scene.remove(playerShip);
|
960 |
-
bullets.forEach(b => scene.remove(b));
|
961 |
explosions.forEach(e => scene.remove(e));
|
962 |
pickups.forEach(p => scene.remove(p));
|
963 |
-
|
964 |
explosions.length = 0;
|
965 |
pickups.length = 0;
|
|
|
966 |
initPlayer();
|
967 |
startRace(currentTrack);
|
968 |
}
|
@@ -971,7 +1141,8 @@
|
|
971 |
function endRace() {
|
972 |
gameState = 'menu';
|
973 |
const activeShips = aiShips.filter(s => s.userData.active).length + (playerData.active ? 1 : 0);
|
974 |
-
alert(`Race Over! Ships Remaining: ${activeShips}, Time: ${raceTime.toFixed(2)}s`);
|
|
|
975 |
}
|
976 |
|
977 |
// Gallery
|
@@ -988,7 +1159,7 @@
|
|
988 |
if (gameState === 'racing') {
|
989 |
updatePlayer();
|
990 |
updateAI();
|
991 |
-
|
992 |
updateExplosions();
|
993 |
raceTime += 1 / 60;
|
994 |
document.getElementById('time').textContent = `Time: ${raceTime.toFixed(2)}`;
|
|
|
54 |
.sidebar button:hover {
|
55 |
background: #45a049;
|
56 |
}
|
57 |
+
.sidebar .character-sheet, .sidebar .skills-sheet, .sidebar .leaderboard {
|
58 |
margin-top: 20px;
|
59 |
font-size: 14px;
|
60 |
}
|
61 |
+
.sidebar .character-sheet div, .sidebar .skills-sheet div, .sidebar .leaderboard div {
|
62 |
margin: 5px 0;
|
63 |
}
|
64 |
.gallery {
|
|
|
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">
|
|
|
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 class="leaderboard" id="leaderboard">
|
181 |
+
<h3>Leaderboard 📊</h3>
|
182 |
+
<div id="logDisplay">Loading...</div>
|
183 |
+
</div>
|
184 |
</div>
|
185 |
<div class="gallery" id="gallery">
|
186 |
<h2>Lake Minnetonka Gallery</h2>
|
|
|
196 |
// Game state
|
197 |
let gameState = 'racing';
|
198 |
let raceTime = 0;
|
199 |
+
let score = 0;
|
200 |
let currentTrack = 'Phelps Island Drift';
|
201 |
let lastGatePass = 0;
|
202 |
let isJumping = false;
|
203 |
let jumpCooldown = 0;
|
204 |
+
let playerName = prompt('Enter your name:') || 'Player';
|
205 |
|
206 |
// Scene setup
|
207 |
const scene = new THREE.Scene();
|
|
|
269 |
// Player ship
|
270 |
let playerShip = null;
|
271 |
let playerData = null;
|
272 |
+
function createShip(typeIndex, isPlayer, scaleMultiplier = 1, splitCount = 0) {
|
273 |
const type = shipTypes[typeIndex];
|
274 |
const ship = new THREE.Group();
|
275 |
+
const scale = {
|
276 |
+
x: type.scale.x * scaleMultiplier,
|
277 |
+
y: type.scale.y * scaleMultiplier,
|
278 |
+
z: type.scale.z * scaleMultiplier
|
279 |
+
};
|
280 |
+
const bodyGeometry = new THREE.BoxGeometry(scale.x, scale.y, scale.z);
|
281 |
const bodyMaterial = new THREE.MeshStandardMaterial({
|
282 |
color: isPlayer ? 0x00ff00 : 0xff0000,
|
283 |
metalness: 0.8,
|
|
|
286 |
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
|
287 |
ship.add(body);
|
288 |
if (type.name !== 'Rocketman') {
|
289 |
+
const thrusterGeometry = new THREE.CylinderGeometry(0.2 * scaleMultiplier, 0.2 * scaleMultiplier, 0.5 * scaleMultiplier, 8);
|
290 |
const thrusterMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa });
|
291 |
for (let i = 0; i < 4; i++) {
|
292 |
const thruster = new THREE.Mesh(thrusterGeometry, thrusterMaterial);
|
293 |
thruster.position.set(
|
294 |
+
(i % 2 === 0 ? 0.5 : -0.5) * scale.x * 0.8,
|
295 |
0,
|
296 |
+
(i < 2 ? 0.5 : -0.5) * scale.z * 0.8
|
297 |
);
|
298 |
thruster.rotation.x = Math.PI / 2;
|
299 |
ship.add(thruster);
|
300 |
}
|
301 |
} else {
|
302 |
+
const jetpackGeometry = new THREE.CylinderGeometry(0.3 * scaleMultiplier, 0.3 * scaleMultiplier, 0.8 * scaleMultiplier, 8);
|
303 |
const jetpack = new THREE.Mesh(jetpackGeometry, new THREE.MeshStandardMaterial({ color: 0xaaaaaa }));
|
304 |
+
jetpack.position.set(0, -0.5 * scaleMultiplier, 0);
|
305 |
ship.add(jetpack);
|
306 |
}
|
307 |
ship.position.y = 10;
|
308 |
ship.castShadow = true;
|
309 |
+
const maxHP = type.maxHP * scaleMultiplier;
|
310 |
ship.userData = {
|
311 |
type: type.name,
|
312 |
maxSpeed: type.speed,
|
313 |
acceleration: type.accel,
|
314 |
+
thrusters: Array(4).fill(maxHP),
|
315 |
+
body: { front: maxHP, back: maxHP, left: maxHP, right: maxHP },
|
316 |
active: true,
|
317 |
speed: 0,
|
318 |
currentWaypoint: 0,
|
319 |
+
lastGatePass: 0,
|
320 |
+
isPlayer: isPlayer,
|
321 |
+
splitCount: splitCount,
|
322 |
+
typeIndex: typeIndex
|
323 |
};
|
324 |
scene.add(ship);
|
325 |
+
if (!isPlayer) aiShips.push(ship);
|
326 |
return ship;
|
327 |
}
|
328 |
|
|
|
331 |
const typeIndex = Math.floor(Math.random() * shipTypes.length);
|
332 |
playerShip = createShip(typeIndex, true);
|
333 |
playerData = playerShip.userData;
|
|
|
334 |
Object.keys(skills).forEach(skill => skills[skill].level = 0);
|
335 |
+
score = 0;
|
336 |
+
document.getElementById('score').textContent = `Score: ${score}`;
|
337 |
updateCharacterSheet();
|
338 |
updateSkillsSheet();
|
339 |
+
updateLeaderboard();
|
340 |
}
|
341 |
|
342 |
// AI opponents
|
|
|
352 |
const startGate = new THREE.Mesh(gateGeometry, gateMaterial);
|
353 |
scene.add(startGate);
|
354 |
|
355 |
+
// Missile system
|
356 |
+
const missiles = [];
|
357 |
+
function createMissile(position, target) {
|
358 |
+
const geometry = new THREE.ConeGeometry(0.2, 0.5, 8);
|
359 |
+
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
|
360 |
+
const missile = new THREE.Mesh(geometry, material);
|
361 |
+
missile.position.copy(position);
|
362 |
+
missile.rotation.x = Math.PI / 2;
|
363 |
+
missile.castShadow = true;
|
364 |
+
|
365 |
+
// Flame trail
|
366 |
+
const flameCount = 10;
|
367 |
+
const flameGeometry = new THREE.BufferGeometry();
|
368 |
+
const flamePositions = new Float32Array(flameCount * 3);
|
369 |
+
const flameColors = new Float32Array(flameCount * 3);
|
370 |
+
for (let i = 0; i < flameCount; i++) {
|
371 |
+
const i3 = i * 3;
|
372 |
+
flamePositions[i3] = 0;
|
373 |
+
flamePositions[i3 + 1] = 0;
|
374 |
+
flamePositions[i3 + 2] = 0;
|
375 |
+
flameColors[i3] = 1;
|
376 |
+
flameColors[i3 + 1] = 0;
|
377 |
+
flameColors[i3 + 2] = 0;
|
378 |
+
}
|
379 |
+
flameGeometry.setAttribute('position', new THREE.BufferAttribute(flamePositions, 3));
|
380 |
+
flameGeometry.setAttribute('color', new THREE.BufferAttribute(flameColors, 3));
|
381 |
+
const flameMaterial = new THREE.PointsMaterial({
|
382 |
+
size: 0.2,
|
383 |
+
vertexColors: true,
|
384 |
+
transparent: true,
|
385 |
+
opacity: 0.5
|
386 |
+
});
|
387 |
+
const flame = new THREE.Points(flameGeometry, flameMaterial);
|
388 |
+
flame.position.z = -0.3;
|
389 |
+
missile.add(flame);
|
390 |
+
|
391 |
+
missile.userData = {
|
392 |
+
target: target,
|
393 |
+
speed: 1,
|
394 |
+
lifetime: 300,
|
395 |
damage: skills.shotPower.effect(skills.shotPower.level)
|
396 |
};
|
397 |
+
scene.add(missile);
|
398 |
+
missiles.push(missile);
|
399 |
+
return missile;
|
400 |
}
|
401 |
|
402 |
// Particle system for explosions
|
|
|
439 |
}
|
440 |
|
441 |
// Building and column generation
|
442 |
+
function createBuilding(x, z, width, depth, height, scaleMultiplier = 1, splitCount = 0) {
|
443 |
+
const geometry = new THREE.BoxGeometry(width * scaleMultiplier, height * scaleMultiplier, depth * scaleMultiplier);
|
444 |
const material = new THREE.MeshStandardMaterial({
|
445 |
color: Math.random() * 0xffffff,
|
446 |
roughness: 0.7,
|
447 |
metalness: 0.3
|
448 |
});
|
449 |
const building = new THREE.Mesh(geometry, material);
|
450 |
+
building.position.set(x, (height * scaleMultiplier) / 2, z);
|
451 |
building.castShadow = true;
|
452 |
building.receiveShadow = true;
|
453 |
+
building.userData = {
|
454 |
+
splitCount: splitCount,
|
455 |
+
maxHP: 10 * scaleMultiplier,
|
456 |
+
currentHP: 10 * scaleMultiplier,
|
457 |
+
width: width,
|
458 |
+
depth: depth,
|
459 |
+
height: height
|
460 |
+
};
|
461 |
return building;
|
462 |
}
|
463 |
|
|
|
683 |
'Miami Beach Boulevard': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
|
684 |
'Florida Keys Canal Cruise': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
|
685 |
'Orlando Theme Park Tour': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
|
686 |
+
'Tampa Bay Finance District Drift': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'standard'], length: 50, streetWidth: 32, buildingHeight: 45 },
|
687 |
'Jacksonville River City Run': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
|
688 |
'Fort Lauderdale Cruise Port Circuit': { segments: ['straight', 'straight', 'straight', 'gap', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
|
689 |
'Key West Sunset Sprint': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
|
|
|
729 |
// Shooting
|
730 |
let canShoot = true;
|
731 |
let shotCooldown = 200;
|
732 |
+
const raycaster = new THREE.Raycaster();
|
733 |
+
const mouse = new THREE.Vector2();
|
734 |
document.addEventListener('mousedown', (event) => {
|
735 |
if (event.button === 0 && canShoot && gameState === 'racing' && playerData.active) {
|
736 |
canShoot = false;
|
737 |
setTimeout(() => canShoot = true, shotCooldown);
|
738 |
+
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
739 |
+
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
740 |
+
raycaster.setFromCamera(mouse, camera);
|
741 |
+
const targets = [...aiShips, ...(currentTrackGroup ? currentTrackGroup.children.filter(c => c.isMesh && c.geometry.type === 'BoxGeometry') : [])].filter(t => t !== playerShip && (!t.userData.isPlayer || !t.userData.active));
|
742 |
+
const intersects = raycaster.intersectObjects(targets);
|
743 |
+
if (intersects.length > 0) {
|
744 |
+
const target = intersects[0].object;
|
745 |
+
const position = playerShip.position.clone().add(new THREE.Vector3(0, 0, -2));
|
746 |
+
const count = skills.shotCount.effect(skills.shotCount.level);
|
747 |
+
for (let i = 0; i < count; i++) {
|
748 |
+
createMissile(position.clone().add(new THREE.Vector3((Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.5, 0)), target);
|
749 |
+
}
|
750 |
+
logAction(playerName, score, 'Shot Fired');
|
751 |
}
|
752 |
}
|
753 |
});
|
754 |
|
755 |
+
// Leaderboard logging
|
756 |
+
function logAction(name, points, action) {
|
757 |
+
const logEntry = {
|
758 |
+
player: name,
|
759 |
+
score: points,
|
760 |
+
action: action,
|
761 |
+
timestamp: new Date().toISOString()
|
762 |
+
};
|
763 |
+
let logs = JSON.parse(localStorage.getItem('gameLogs') || '[]');
|
764 |
+
logs.push(logEntry);
|
765 |
+
localStorage.setItem('gameLogs', JSON.stringify(logs));
|
766 |
+
updateLeaderboard();
|
767 |
+
}
|
768 |
+
|
769 |
+
function updateLeaderboard() {
|
770 |
+
let logs = JSON.parse(localStorage.getItem('gameLogs') || '[]');
|
771 |
+
document.getElementById('logDisplay').innerHTML = logs.slice(-5).map(log =>
|
772 |
+
`${log.player} (${log.score} pts): ${log.action} at ${new Date(log.timestamp).toLocaleString()}`
|
773 |
+
).join('<br>') || 'No history!';
|
774 |
+
}
|
775 |
+
|
776 |
// Update character sheet
|
777 |
function updateCharacterSheet() {
|
778 |
document.getElementById('ship-type').textContent = `Type: ${playerData.type}`;
|
|
|
829 |
createExplosion(hitPosition);
|
830 |
if (ship === playerShip) {
|
831 |
updateCharacterSheet();
|
832 |
+
logAction(playerName, score, 'Ship Damaged');
|
833 |
}
|
834 |
checkShipStatus(ship);
|
835 |
}
|
836 |
|
837 |
+
function checkShipStatus(ship, hitPosition) {
|
838 |
const totalHP = ship.userData.thrusters.reduce((a, b) => a + b, 0) +
|
839 |
Object.values(ship.userData.body).reduce((a, b) => a + b, 0);
|
840 |
if (totalHP <= 0) {
|
841 |
ship.userData.active = false;
|
842 |
ship.visible = false;
|
843 |
createExplosion(ship.position);
|
844 |
+
if (ship.userData.splitCount < 3) {
|
845 |
+
const newScale = 0.5;
|
846 |
+
const offsets = [
|
847 |
+
new THREE.Vector3(1, 0, 1),
|
848 |
+
new THREE.Vector3(-1, 0, -1)
|
849 |
+
];
|
850 |
+
offsets.forEach(offset => {
|
851 |
+
const newShip = createShip(
|
852 |
+
ship.userData.typeIndex,
|
853 |
+
false,
|
854 |
+
newScale,
|
855 |
+
ship.userData.splitCount + 1
|
856 |
+
);
|
857 |
+
newShip.position.copy(ship.position).add(offset.multiplyScalar(2));
|
858 |
+
newShip.userData.currentWaypoint = ship.userData.currentWaypoint;
|
859 |
+
});
|
860 |
+
}
|
861 |
+
if (!ship.userData.isPlayer) {
|
862 |
+
const index = aiShips.indexOf(ship);
|
863 |
+
if (index > -1) aiShips.splice(index, 1);
|
864 |
+
score += 100;
|
865 |
+
document.getElementById('score').textContent = `Score: ${score}`;
|
866 |
+
logAction(playerName, score, 'Enemy Destroyed');
|
867 |
+
}
|
868 |
if (ship === playerShip) {
|
869 |
document.getElementById('status').textContent = 'Status: Destroyed';
|
870 |
+
logAction(playerName, score, 'Player Destroyed');
|
871 |
endRace();
|
872 |
}
|
873 |
}
|
874 |
}
|
875 |
|
876 |
+
// Building damage
|
877 |
+
function applyBuildingDamage(building, damage, hitPosition) {
|
878 |
+
building.userData.currentHP -= damage;
|
879 |
+
createExplosion(hitPosition);
|
880 |
+
if (building.userData.currentHP <= 0) {
|
881 |
+
currentTrackGroup.remove(building);
|
882 |
+
if (building.userData.splitCount < 3) {
|
883 |
+
const newScale = 0.5;
|
884 |
+
const offsets = [
|
885 |
+
new THREE.Vector3(5, 0, 5),
|
886 |
+
new THREE.Vector3(-5, 0, -5)
|
887 |
+
];
|
888 |
+
offsets.forEach(offset => {
|
889 |
+
const newBuilding = createBuilding(
|
890 |
+
building.position.x + offset.x,
|
891 |
+
building.position.z + offset.z,
|
892 |
+
building.userData.width,
|
893 |
+
building.userData.depth,
|
894 |
+
building.userData.height,
|
895 |
+
newScale,
|
896 |
+
building.userData.splitCount + 1
|
897 |
+
);
|
898 |
+
currentTrackGroup.add(newBuilding);
|
899 |
+
});
|
900 |
+
}
|
901 |
+
score += 100;
|
902 |
+
document.getElementById('score').textContent = `Score: ${score}`;
|
903 |
+
logAction(playerName, score, 'Building Destroyed');
|
904 |
+
}
|
905 |
+
}
|
906 |
+
|
907 |
// Pickup handling
|
908 |
function updatePickups() {
|
909 |
for (let i = pickups.length - 1; i >= 0; i--) {
|
|
|
913 |
if (skill.level < skill.maxLevel) {
|
914 |
skill.level++;
|
915 |
updateSkillsSheet();
|
916 |
+
logAction(playerName, score, `Picked ${pickup.userData.skill}`);
|
917 |
}
|
918 |
scene.remove(pickup);
|
919 |
pickups.splice(i, 1);
|
|
|
921 |
}
|
922 |
}
|
923 |
|
924 |
+
// Missile update
|
925 |
+
function updateMissiles() {
|
926 |
+
for (let i = missiles.length - 1; i >= 0; i--) {
|
927 |
+
const missile = missiles[i];
|
928 |
+
missile.userData.lifetime--;
|
929 |
+
if (missile.userData.lifetime <= 0 || !missile.userData.target || (!missile.userData.target.userData?.active && !missile.userData.target.isMesh)) {
|
930 |
+
scene.remove(missile);
|
931 |
+
missiles.splice(i, 1);
|
932 |
+
continue;
|
933 |
+
}
|
934 |
+
const targetPos = missile.userData.target.isMesh ? missile.userData.target.position : missile.userData.target.position;
|
935 |
+
const direction = targetPos.clone().sub(missile.position).normalize();
|
936 |
+
missile.position.add(direction.multiplyScalar(missile.userData.speed));
|
937 |
+
missile.lookAt(targetPos);
|
938 |
+
missile.rotation.x += Math.PI / 2;
|
939 |
+
|
940 |
+
// Update flame trail
|
941 |
+
const flame = missile.children[0];
|
942 |
+
const positions = flame.geometry.attributes.position.array;
|
943 |
+
for (let j = 0; j < positions.length / 3; j++) {
|
944 |
+
const j3 = j * 3;
|
945 |
+
positions[j3] = (Math.random() - 0.5) * 0.1;
|
946 |
+
positions[j3 + 1] = (Math.random() - 0.5) * 0.1;
|
947 |
+
positions[j3 + 2] = -0.3 - Math.random() * 0.2;
|
948 |
+
}
|
949 |
+
flame.geometry.attributes.position.needsUpdate = true;
|
950 |
+
|
951 |
+
// Check collision
|
952 |
+
const missileBox = new THREE.Box3().setFromObject(missile);
|
953 |
+
const targetBox = new THREE.Box3().setFromObject(missile.userData.target);
|
954 |
+
if (missileBox.intersectsBox(targetBox)) {
|
955 |
+
if (missile.userData.target.isMesh && missile.userData.target.geometry.type === 'BoxGeometry') {
|
956 |
+
applyBuildingDamage(missile.userData.target, missile.userData.damage, missile.position);
|
957 |
+
} else {
|
958 |
+
applyDamage(missile.userData.target, missile.userData.damage, missile.position);
|
959 |
+
}
|
960 |
+
scene.remove(missile);
|
961 |
+
missiles.splice(i, 1);
|
962 |
+
}
|
963 |
+
}
|
964 |
+
}
|
965 |
+
|
966 |
// Player movement
|
967 |
let rotation = 0;
|
968 |
let bankAngle = 0;
|
|
|
985 |
isJumping = true;
|
986 |
jumpHeight = 5;
|
987 |
jumpCooldown = 60;
|
988 |
+
logAction(playerName, score, 'Jumped');
|
989 |
}
|
990 |
if (isJumping) {
|
991 |
jumpHeight -= 0.2;
|
|
|
1041 |
});
|
1042 |
}
|
1043 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1044 |
// Explosion update
|
1045 |
const explosions = [];
|
1046 |
function updateExplosions() {
|
|
|
1089 |
currentTrack = trackName;
|
1090 |
if (currentTrackGroup) scene.remove(currentTrackGroup);
|
1091 |
pickups.forEach(p => scene.remove(p));
|
1092 |
+
missiles.forEach(m => scene.remove(m));
|
1093 |
pickups.length = 0;
|
1094 |
+
missiles.length = 0;
|
1095 |
const { trackGroup, waypoints: newWaypoints, startPosition } = generateTrack(trackConfigs[trackName]);
|
1096 |
currentTrackGroup = trackGroup;
|
1097 |
waypoints = newWaypoints;
|
|
|
1115 |
startGate.position.y = 12;
|
1116 |
startGate.rotation.y = Math.atan2(waypoints[1].x - waypoints[0].x, waypoints[1].z - waypoints[0].z);
|
1117 |
raceTime = 0;
|
1118 |
+
score = 0;
|
1119 |
document.getElementById('time').textContent = `Time: 0`;
|
1120 |
+
document.getElementById('score').textContent = `Score: ${score}`;
|
1121 |
document.getElementById('status').textContent = `Status: Active`;
|
1122 |
updateCharacterSheet();
|
1123 |
updateSkillsSheet();
|
1124 |
+
logAction(playerName, score, `Started ${trackName}`);
|
1125 |
}
|
1126 |
|
1127 |
// Restart race
|
1128 |
function restartRace() {
|
1129 |
scene.remove(playerShip);
|
|
|
1130 |
explosions.forEach(e => scene.remove(e));
|
1131 |
pickups.forEach(p => scene.remove(p));
|
1132 |
+
missiles.forEach(m => scene.remove(m));
|
1133 |
explosions.length = 0;
|
1134 |
pickups.length = 0;
|
1135 |
+
missiles.length = 0;
|
1136 |
initPlayer();
|
1137 |
startRace(currentTrack);
|
1138 |
}
|
|
|
1141 |
function endRace() {
|
1142 |
gameState = 'menu';
|
1143 |
const activeShips = aiShips.filter(s => s.userData.active).length + (playerData.active ? 1 : 0);
|
1144 |
+
alert(`Race Over! Ships Remaining: ${activeShips}, Time: ${raceTime.toFixed(2)}s, Score: ${score}`);
|
1145 |
+
logAction(playerName, score, 'Race Ended');
|
1146 |
}
|
1147 |
|
1148 |
// Gallery
|
|
|
1159 |
if (gameState === 'racing') {
|
1160 |
updatePlayer();
|
1161 |
updateAI();
|
1162 |
+
updateMissiles();
|
1163 |
updateExplosions();
|
1164 |
raceTime += 1 / 60;
|
1165 |
document.getElementById('time').textContent = `Time: ${raceTime.toFixed(2)}`;
|