TinyHTMLGame1 / index.html
awacke1's picture
Update index.html
662afab verified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Starship Circuit Commander</title>
<style>
body {
margin: 0;
overflow: hidden;
font-family: Arial, sans-serif;
}
canvas {
display: block;
}
.ui-container {
position: absolute;
top: 10px;
right: 10px;
color: white;
background-color: rgba(0, 0, 0, 0.5);
padding: 10px;
border-radius: 5px;
user-select: none;
}
.sidebar {
position: absolute;
top: 0;
left: 0;
width: 250px;
height: 100%;
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 10px;
overflow-y: auto;
border-radius: 0 5px 5px 0;
}
.sidebar h2, .sidebar h3 {
margin: 0 0 10px;
font-size: 18px;
}
.sidebar button {
display: block;
width: 100%;
margin: 5px 0;
padding: 8px;
background: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
}
.sidebar button:hover {
background: #45a049;
}
.sidebar .character-sheet, .sidebar .skills-sheet, .sidebar .leaderboard {
margin-top: 20px;
font-size: 14px;
}
.sidebar .character-sheet div, .sidebar .skills-sheet div, .sidebar .leaderboard div {
margin: 5px 0;
}
.gallery {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 20px;
border-radius: 10px;
text-align: center;
max-width: 600px;
display: none;
}
.gallery img {
max-width: 100%;
margin: 10px 0;
}
.skill-meter {
background: #333;
height: 10px;
border-radius: 5px;
overflow: hidden;
}
.skill-meter div {
background: #4CAF50;
height: 100%;
transition: width 0.3s;
}
</style>
</head>
<body>
<div class="ui-container" id="race-ui">
<h2>Starship Circuit Commander 🏁</h2>
<div id="time">Time: 0</div>
<div id="score">Score: 0</div>
<div id="status">Status: Active</div>
</div>
<div class="sidebar" id="sidebar">
<h2>Tracks</h2>
<h3>Minnesota</h3>
<button onclick="startRace('U of MN Twin Cities Track')">🎓 U of MN Twin Cities Track</button>
<button onclick="startRace('Phelps Island Drift')">🏝️ Phelps Island Drift</button>
<button onclick="startRace('Mound Overlook Circuit')">🏞️ Mound Overlook Circuit</button>
<button onclick="startRace('Lake Minnetonka Sprint')">🚣 Lake Minnetonka Sprint</button>
<button onclick="startRace('St. Paul Riverbend Run')">🛶 St. Paul Riverbend Run</button>
<button onclick="startRace('Cathedral Hill Climb')">⛪ Cathedral Hill Climb</button>
<button onclick="startRace('Downtown Minneapolis Skyway Loop')">🌆 Downtown Minneapolis Skyway Loop</button>
<button onclick="startRace('Nicollet Mall Straightaway')">🛍️ Nicollet Mall Straightaway</button>
<button onclick="startRace('Lake of the Isles Island Circuit')">🏝️ Lake of the Isles Island Circuit</button>
<button onclick="startRace('Minnehaha Falls Meander')">🌳 Minnehaha Falls Meander</button>
<h3>Wisconsin</h3>
<button onclick="startRace('Milwaukee Loop')">🛶 Milwaukee Loop</button>
<button onclick="startRace('Waukesha Speedway')">🏁 Waukesha Speedway</button>
<button onclick="startRace('Manitowoc Marina Run')">🌊 Manitowoc Marina Run</button>
<button onclick="startRace('Lake Winnebago Circuit')">🌾 Lake Winnebago Circuit</button>
<button onclick="startRace('Door County Coastal Cruise')">🌲 Door County Coastal Cruise</button>
<button onclick="startRace('Green Bay Bayfront Blitz')">🦌 Green Bay Bayfront Blitz</button>
<button onclick="startRace('Sturgeon Bay Ship Canal Sprint')">🚢 Sturgeon Bay Ship Canal Sprint</button>
<button onclick="startRace('Wisconsin Dells Rapids Run')">🎢 Wisconsin Dells Rapids Run</button>
<button onclick="startRace('Madison Capitol Circuit')">🏛️ Madison Capitol Circuit</button>
<button onclick="startRace('Superior North Shore Dash')">🌲 Superior North Shore Dash</button>
<h3>Texas</h3>
<button onclick="startRace('Houston Oil Refinery Rush')">⚙️ Houston Oil Refinery Rush</button>
<button onclick="startRace('Dallas Finance District Dash')">💼 Dallas Finance District Dash</button>
<button onclick="startRace('Austin Tech Corridor Cruise')">🎸 Austin Tech Corridor Cruise</button>
<button onclick="startRace('San Antonio Riverwalk Run')">🎺 San Antonio Riverwalk Run</button>
<button onclick="startRace('Corpus Christi Coastal Run')">⚓ Corpus Christi Coastal Run</button>
<button onclick="startRace('Fort Worth Stockyards Loop')">🐎 Fort Worth Stockyards Loop</button>
<button onclick="startRace('Galveston Port Channel Circuit')">🚢 Galveston Port Channel Circuit</button>
<button onclick="startRace('El Paso Border Trade Boulevard')">🌵 El Paso Border Trade Boulevard</button>
<button onclick="startRace('Lubbock Innovation Loop')">🌟 Lubbock Innovation Loop</button>
<button onclick="startRace('Midland Permian Basin Sprint')">⛽ Midland Permian Basin Sprint</button>
<h3>Florida</h3>
<button onclick="startRace('Clearwater Curve')">🏖️ Clearwater Curve</button>
<button onclick="startRace('Miami Beach Boulevard')">🌴 Miami Beach Boulevard</button>
<button onclick="startRace('Florida Keys Canal Cruise')">🚤 Florida Keys Canal Cruise</button>
<button onclick="startRace('Orlando Theme Park Tour')">🎢 Orlando Theme Park Tour</button>
<button onclick="startRace('Tampa Bay Finance District Drift')">💰 Tampa Bay Finance District Drift</button>
<button onclick="startRace('Jacksonville River City Run')">🏙️ Jacksonville River City Run</button>
<button onclick="startRace('Fort Lauderdale Cruise Port Circuit')">🚢 Fort Lauderdale Cruise Port Circuit</button>
<button onclick="startRace('Key West Sunset Sprint')">🌅 Key West Sunset Sprint</button>
<button onclick="startRace('St. Augustine Colonial Run')">🏰 St. Augustine Colonial Run</button>
<button onclick="startRace('Palm Beach Gold Coast Circuit')">💎 Palm Beach Gold Coast Circuit</button>
<h3>California</h3>
<button onclick="startRace('Venice Beach Boardwalk Circuit')">🏄 Venice Beach Boardwalk Circuit</button>
<button onclick="startRace('Los Angeles Star-Studded Speedway')">🌟 Los Angeles Star-Studded Speedway</button>
<button onclick="startRace('San Francisco Golden Gate Run')">🌉 San Francisco Golden Gate Run</button>
<button onclick="startRace('San Diego Coastal Drift')">🚤 San Diego Coastal Drift</button>
<button onclick="startRace('Anaheim Theme Park Loop')">🏰 Anaheim Theme Park Loop</button>
<button onclick="startRace('Santa Barbara Vineyard Tour')">🍇 Santa Barbara Vineyard Tour</button>
<button onclick="startRace('Palm Springs Desert Dash')">☀️ Palm Springs Desert Dash</button>
<button onclick="startRace('Santa Cruz Boardwalk Sprint')">🎡 Santa Cruz Boardwalk Sprint</button>
<button onclick="startRace('Monterey Bay Coastal Circuit')">🐋 Monterey Bay Coastal Circuit</button>
<button onclick="startRace('Carmel-by-the-Sea Scenic Route')">🏞️ Carmel-by-the-Sea Scenic Route</button>
<button onclick="startRace('Napa Valley Vineyard Ride')">🍷 Napa Valley Vineyard Ride</button>
<button onclick="showGallery()">📷 View Image Gallery</button>
<button onclick="restartRace()">🔄 Restart Race</button>
<div class="character-sheet" id="character-sheet">
<h3>Ship Status</h3>
<div id="ship-type">Type: Unknown</div>
<div id="thruster-1">Thruster 1: 0/0</div>
<div id="thruster-2">Thruster 2: 0/0</div>
<div id="thruster-3">Thruster 3: 0/0</div>
<div id="thruster-4">Thruster 4: 0/0</div>
<div id="body-front">Body Front: 0/0</div>
<div id="body-back">Body Back: 0/0</div>
<div id="body-left">Body Left: 0/0</div>
<div id="body-right">Body Right: 0/0</div>
</div>
<div class="skills-sheet" id="skills-sheet">
<h3>Skills</h3>
<div>Speed: <span id="skill-speed">0</span><div class="skill-meter"><div id="skill-speed-meter" style="width: 0%"></div></div></div>
<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>
<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>
<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>
</div>
<div class="leaderboard" id="leaderboard">
<h3>Leaderboard 📊</h3>
<div id="logDisplay">Loading...</div>
</div>
</div>
<div class="gallery" id="gallery">
<h2>Lake Minnetonka Gallery</h2>
<p>Images of the Lake Minnetonka area</p>
<img src="https://via.placeholder.com/400x200?text=Phelps+Island" alt="Phelps Island">
<img src="https://via.placeholder.com/400x200?text=Mound+Shoreline" alt="Mound Shoreline">
<img src="https://via.placeholder.com/400x200?text=Wayzata+Bay" alt="Wayzata Bay">
<button onclick="hideGallery()">Back to Menu</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// Game state
let gameState = 'racing';
let raceTime = 0;
let score = 0;
let currentTrack = 'Phelps Island Drift';
let lastGatePass = 0;
let isJumping = false;
let jumpCooldown = 0;
let playerName = prompt('Enter your name:') || 'Player';
// Scene setup
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
document.body.appendChild(renderer.domElement);
// Lights
const ambientLight = new THREE.AmbientLight(0xffffff, 0.5);
scene.add(ambientLight);
const directionalLight = new THREE.DirectionalLight(0xffffff, 0.8);
directionalLight.position.set(50, 50, 50);
directionalLight.castShadow = true;
scene.add(directionalLight);
// Ground plane
const groundGeometry = new THREE.PlaneGeometry(1000, 1000);
const groundMaterial = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8, metalness: 0.2 });
const ground = new THREE.Mesh(groundGeometry, groundMaterial);
ground.rotation.x = -Math.PI / 2;
ground.position.y = -0.1;
ground.receiveShadow = true;
scene.add(ground);
// Ship types
const shipTypes = [
{ name: 'Truck', scale: { x: 2, y: 1.5, z: 4 }, maxHP: 20, speed: 0.2, accel: 0.008 },
{ name: 'Subcompact', scale: { x: 1, y: 1, z: 2 }, maxHP: 10, speed: 0.3, accel: 0.01 },
{ name: 'Motorbike', scale: { x: 0.5, y: 0.5, z: 1.5 }, maxHP: 5, speed: 0.35, accel: 0.012 },
{ name: 'Rocketman', scale: { x: 0.5, y: 1, z: 0.5 }, maxHP: 3, speed: 0.4, accel: 0.015 }
];
// Skills
const skills = {
speed: { level: 0, maxLevel: 10, effect: level => 1 + level * 0.05 },
shotPower: { level: 0, maxLevel: 10, effect: level => 5 + level * 2 },
shotCount: { level: 0, maxLevel: 10, effect: level => 1 + Math.floor(level / 2) },
turnSpeed: { level: 0, maxLevel: 10, effect: level => 0.05 + level * 0.005 }
};
// Item pickups
const pickupTypes = [
{ skill: 'speed', color: 0x00ff00 },
{ skill: 'shotPower', color: 0xff0000 },
{ skill: 'shotCount', color: 0xffff00 },
{ skill: 'turnSpeed', color: 0x0000ff }
];
const pickups = [];
function createPickup(position) {
const geometry = new THREE.SphereGeometry(0.5, 16, 16);
const type = pickupTypes[Math.floor(Math.random() * pickupTypes.length)];
const material = new THREE.MeshStandardMaterial({ color: type.color });
const pickup = new THREE.Mesh(geometry, material);
pickup.position.copy(position);
pickup.userData = { skill: type.skill };
scene.add(pickup);
pickups.push(pickup);
return pickup;
}
// Player ship
let playerShip = null;
let playerData = null;
function createShip(typeIndex, isPlayer, scaleMultiplier = 1, splitCount = 0) {
const type = shipTypes[typeIndex];
const ship = new THREE.Group();
const scale = {
x: type.scale.x * scaleMultiplier,
y: type.scale.y * scaleMultiplier,
z: type.scale.z * scaleMultiplier
};
const bodyGeometry = new THREE.BoxGeometry(scale.x, scale.y, scale.z);
const bodyMaterial = new THREE.MeshStandardMaterial({
color: isPlayer ? 0x00ff00 : 0xff0000,
metalness: 0.8,
roughness: 0.2
});
const body = new THREE.Mesh(bodyGeometry, bodyMaterial);
ship.add(body);
if (type.name !== 'Rocketman') {
const thrusterGeometry = new THREE.CylinderGeometry(0.2 * scaleMultiplier, 0.2 * scaleMultiplier, 0.5 * scaleMultiplier, 8);
const thrusterMaterial = new THREE.MeshStandardMaterial({ color: 0xaaaaaa });
for (let i = 0; i < 4; i++) {
const thruster = new THREE.Mesh(thrusterGeometry, thrusterMaterial);
thruster.position.set(
(i % 2 === 0 ? 0.5 : -0.5) * scale.x * 0.8,
0,
(i < 2 ? 0.5 : -0.5) * scale.z * 0.8
);
thruster.rotation.x = Math.PI / 2;
ship.add(thruster);
}
} else {
const jetpackGeometry = new THREE.CylinderGeometry(0.3 * scaleMultiplier, 0.3 * scaleMultiplier, 0.8 * scaleMultiplier, 8);
const jetpack = new THREE.Mesh(jetpackGeometry, new THREE.MeshStandardMaterial({ color: 0xaaaaaa }));
jetpack.position.set(0, -0.5 * scaleMultiplier, 0);
ship.add(jetpack);
}
ship.position.y = 10;
ship.castShadow = true;
const maxHP = type.maxHP * scaleMultiplier;
ship.userData = {
type: type.name,
maxSpeed: type.speed,
acceleration: type.accel,
thrusters: Array(4).fill(maxHP),
body: { front: maxHP, back: maxHP, left: maxHP, right: maxHP },
active: true,
speed: 0,
currentWaypoint: 0,
lastGatePass: 0,
isPlayer: isPlayer,
splitCount: splitCount,
typeIndex: typeIndex
};
scene.add(ship);
if (!isPlayer) aiShips.push(ship);
return ship;
}
// Initialize player
function initPlayer() {
const typeIndex = Math.floor(Math.random() * shipTypes.length);
playerShip = createShip(typeIndex, true);
playerData = playerShip.userData;
Object.keys(skills).forEach(skill => skills[skill].level = 0);
score = 0;
document.getElementById('score').textContent = `Score: ${score}`;
updateCharacterSheet();
updateSkillsSheet();
updateLeaderboard();
}
// AI opponents
const aiShips = [];
for (let i = 0; i < 3; i++) {
const typeIndex = Math.floor(Math.random() * shipTypes.length);
aiShips.push(createShip(typeIndex, false));
}
// Start gate
const gateGeometry = new THREE.BoxGeometry(60, 12, 1);
const gateMaterial = new THREE.MeshBasicMaterial({ visible: false });
const startGate = new THREE.Mesh(gateGeometry, gateMaterial);
scene.add(startGate);
// Missile system
const missiles = [];
function createMissile(position, target) {
const geometry = new THREE.ConeGeometry(0.2, 0.5, 8);
const material = new THREE.MeshStandardMaterial({ color: 0xff0000 });
const missile = new THREE.Mesh(geometry, material);
missile.position.copy(position);
missile.rotation.x = Math.PI / 2;
missile.castShadow = true;
// Flame trail
const flameCount = 10;
const flameGeometry = new THREE.BufferGeometry();
const flamePositions = new Float32Array(flameCount * 3);
const flameColors = new Float32Array(flameCount * 3);
for (let i = 0; i < flameCount; i++) {
const i3 = i * 3;
flamePositions[i3] = 0;
flamePositions[i3 + 1] = 0;
flamePositions[i3 + 2] = 0;
flameColors[i3] = 1;
flameColors[i3 + 1] = 0;
flameColors[i3 + 2] = 0;
}
flameGeometry.setAttribute('position', new THREE.BufferAttribute(flamePositions, 3));
flameGeometry.setAttribute('color', new THREE.BufferAttribute(flameColors, 3));
const flameMaterial = new THREE.PointsMaterial({
size: 0.2,
vertexColors: true,
transparent: true,
opacity: 0.5
});
const flame = new THREE.Points(flameGeometry, flameMaterial);
flame.position.z = -0.3;
missile.add(flame);
missile.userData = {
target: target,
speed: 1,
lifetime: 300,
damage: skills.shotPower.effect(skills.shotPower.level)
};
scene.add(missile);
missiles.push(missile);
return missile;
}
// Particle system for explosions
function createExplosion(position) {
const particleCount = 50;
const geometry = new THREE.BufferGeometry();
const positions = new Float32Array(particleCount * 3);
const colors = new Float32Array(particleCount * 3);
for (let i = 0; i < particleCount; i++) {
const i3 = i * 3;
positions[i3] = position.x;
positions[i3 + 1] = position.y;
positions[i3 + 2] = position.z;
const color = Math.random() > 0.5 ? [1, 1, 0] : [1, 0, 0];
colors[i3] = color[0];
colors[i3 + 1] = color[1];
colors[i3 + 2] = color[2];
}
geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.PointsMaterial({
size: 0.5,
vertexColors: true,
transparent: true,
opacity: 0.8
});
const particles = new THREE.Points(geometry, material);
particles.userData = { lifetime: 60, velocities: [] };
for (let i = 0; i < particleCount; i++) {
particles.userData.velocities.push(
new THREE.Vector3(
(Math.random() - 0.5) * 0.2,
(Math.random() - 0.5) * 0.2,
(Math.random() - 0.5) * 0.2
)
);
}
scene.add(particles);
return particles;
}
// Building and column generation
function createBuilding(x, z, width, depth, height, scaleMultiplier = 1, splitCount = 0) {
const geometry = new THREE.BoxGeometry(width * scaleMultiplier, height * scaleMultiplier, depth * scaleMultiplier);
const material = new THREE.MeshStandardMaterial({
color: Math.random() * 0xffffff,
roughness: 0.7,
metalness: 0.3
});
const building = new THREE.Mesh(geometry, material);
building.position.set(x, (height * scaleMultiplier) / 2, z);
building.castShadow = true;
building.receiveShadow = true;
building.userData = {
splitCount: splitCount,
maxHP: 10 * scaleMultiplier,
currentHP: 10 * scaleMultiplier,
width: width,
depth: depth,
height: height
};
return building;
}
function createColumn(x, z, height) {
const geometry = new THREE.CylinderGeometry(1, 1, height, 8);
const material = new THREE.MeshStandardMaterial({ color: 0x666666, roughness: 0.8, metalness: 0.5 });
const column = new THREE.Mesh(geometry, material);
column.position.set(x, height / 2, z);
column.castShadow = true;
column.receiveShadow = true;
return column;
}
// Track segment generation
function createTrackSegment(type, inputPos, inputRot, params) {
const points = [];
const buildings = [];
const columns = [];
let length = params.length;
let width = params.streetWidth;
let height = params.buildingHeight;
let outputPos, outputRot;
if (type === 'straight') {
points.push(inputPos);
outputPos = inputPos.clone().add(new THREE.Vector3(0, 0, -length).applyEuler(inputRot));
points.push(outputPos);
outputRot = inputRot.clone();
for (let i = 0; i <= length; i += 10) {
const t = i / length;
const pos = inputPos.clone().lerp(outputPos, t);
buildings.push(createBuilding(pos.x + width / 2 + 5, pos.z, 10, length, height));
buildings.push(createBuilding(pos.x - width / 2 - 5, pos.z, 10, length, height));
if (i % 20 === 0) {
columns.push(createColumn(pos.x + width / 2, pos.z, height));
columns.push(createColumn(pos.x - width / 2, pos.z, height));
}
if (i % 30 === 0 && Math.random() < 0.3) {
createPickup(new THREE.Vector3(pos.x + (Math.random() - 0.5) * width / 2, 10, pos.z));
}
}
} else if (type === 'left') {
const center = inputPos.clone().add(new THREE.Vector3(length, 0, 0).applyEuler(inputRot));
for (let i = 0; i <= 16; i++) {
const angle = (i / 16) * Math.PI / 2;
const x = center.x - length * Math.cos(angle);
const z = center.z - length * Math.sin(angle);
points.push(new THREE.Vector3(x, inputPos.y, z));
if (i % 4 === 0) {
buildings.push(createBuilding(
x + Math.cos(angle) * (width / 2 + 5), z + Math.sin(angle) * (width / 2 + 5),
10, 10, height
));
buildings.push(createBuilding(
x - Math.cos(angle) * (width / 2 + 5), z - Math.sin(angle) * (width / 2 + 5),
10, 10, height
));
columns.push(createColumn(
x + Math.cos(angle) * width / 2, z + Math.sin(angle) * width / 2, height
));
columns.push(createColumn(
x - Math.cos(angle) * width / 2, z - Math.sin(angle) * width / 2, height
));
if (Math.random() < 0.3) {
createPickup(new THREE.Vector3(x + (Math.random() - 0.5) * width / 2, 10, z));
}
}
}
outputPos = points[points.length - 1];
outputRot = inputRot.clone();
outputRot.y += Math.PI / 2;
} else if (type === 'right') {
const center = inputPos.clone().add(new THREE.Vector3(-length, 0, 0).applyEuler(inputRot));
for (let i = 0; i <= 16; i++) {
const angle = (i / 16) * -Math.PI / 2;
const x = center.x - length * Math.cos(angle);
const z = center.z - length * Math.sin(angle);
points.push(new THREE.Vector3(x, inputPos.y, z));
if (i % 4 === 0) {
buildings.push(createBuilding(
x - Math.cos(angle) * (width / 2 + 5), z - Math.sin(angle) * (width / 2 + 5),
10, 10, height
));
buildings.push(createBuilding(
x + Math.cos(angle) * (width / 2 + 5), z + Math.sin(angle) * (width / 2 + 5),
10, 10, height
));
columns.push(createColumn(
x - Math.cos(angle) * width / 2, z - Math.sin(angle) * width / 2, height
));
columns.push(createColumn(
x + Math.cos(angle) * width / 2, z + Math.sin(angle) * width / 2, height
));
if (Math.random() < 0.3) {
createPickup(new THREE.Vector3(x + (Math.random() - 0.5) * width / 2, 10, z));
}
}
}
outputPos = points[points.length - 1];
outputRot = inputRot.clone();
outputRot.y -= Math.PI / 2;
} else if (type === 'up') {
points.push(inputPos);
outputPos = inputPos.clone().add(new THREE.Vector3(0, length, -length).applyEuler(inputRot));
points.push(outputPos);
outputRot = inputRot.clone();
outputRot.x += Math.PI / 4;
for (let i = 0; i <= length; i += 10) {
const t = i / length;
const pos = inputPos.clone().lerp(outputPos, t);
buildings.push(createBuilding(pos.x + width / 2 + 5, pos.z, 10, length, height));
buildings.push(createBuilding(pos.x - width / 2 - 5, pos.z, 10, length, height));
if (i % 20 === 0) {
columns.push(createColumn(pos.x + width / 2, pos.z, height));
columns.push(createColumn(pos.x - width / 2, pos.z, height));
}
if (i % 30 === 0 && Math.random() < 0.3) {
createPickup(new THREE.Vector3(pos.x + (Math.random() - 0.5) * width / 2, pos.z, pos.y));
}
}
} else if (type === 'down') {
points.push(inputPos);
outputPos = inputPos.clone().add(new THREE.Vector3(0, -length, -length).applyEuler(inputRot));
points.push(outputPos);
outputRot = inputRot.clone();
outputRot.x -= Math.PI / 4;
for (let i = 0; i <= length; i += 10) {
const t = i / length;
const pos = inputPos.clone().lerp(outputPos, t);
buildings.push(createBuilding(pos.x + width / 2 + 5, pos.z, 10, length, height));
buildings.push(createBuilding(pos.x - width / 2 - 5, pos.z, 10, length, height));
if (i % 20 === 0) {
columns.push(createColumn(pos.x + width / 2, pos.z, height));
columns.push(createColumn(pos.x - width / 2, pos.z, height));
}
if (i % 30 === 0 && Math.random() < 0.3) {
createPickup(new THREE.Vector3(pos.x + (Math.random() - 0.5) * width / 2, pos.z, pos.y));
}
}
} else if (type === 'gap') {
points.push(inputPos);
outputPos = inputPos.clone().add(new THREE.Vector3(0, 0, -length * 2).applyEuler(inputRot));
points.push(outputPos);
outputRot = inputRot.clone();
buildings.push(createBuilding(outputPos.x + width / 2 + 5, outputPos.z, 10, 10, height));
buildings.push(createBuilding(outputPos.x - width / 2 - 5, outputPos.z, 10, 10, height));
columns.push(createColumn(outputPos.x + width / 2, outputPos.z, height));
columns.push(createColumn(outputPos.x - width / 2, outputPos.z, height));
if (Math.random() < 0.3) {
createPickup(new THREE.Vector3(outputPos.x + (Math.random() - 0.5) * width / 2, 10, outputPos.z));
}
}
return { points, outputPos, outputRot, buildings, columns };
}
// Track generation
function generateTrack(params) {
const trackGroup = new THREE.Group();
const waypoints = [];
let currentPos = new THREE.Vector3(0, 10, 0);
let currentRot = new THREE.Euler(0, 0, 0);
const segmentTypes = params.segments;
segmentTypes.forEach(type => {
const segment = createTrackSegment(type, currentPos, currentRot, {
length: params.length,
streetWidth: params.streetWidth,
buildingHeight: params.buildingHeight
});
segment.buildings.forEach(building => trackGroup.add(building));
segment.columns.forEach(column => trackGroup.add(column));
segment.points.forEach(point => waypoints.push(point.clone()));
currentPos = segment.outputPos;
currentRot = segment.outputRot;
});
const finalSegment = createTrackSegment('straight', currentPos, currentRot, {
length: currentPos.distanceTo(new THREE.Vector3(0, 10, 0)),
streetWidth: params.streetWidth,
buildingHeight: params.buildingHeight
});
finalSegment.buildings.forEach(building => trackGroup.add(building));
finalSegment.columns.forEach(column => trackGroup.add(column));
finalSegment.points.forEach(point => waypoints.push(point.clone()));
return { trackGroup, waypoints, startPosition: new THREE.Vector3(0, 10, 0) };
}
// Track configurations
const trackConfigs = {
'U of MN Twin Cities Track': { segments: ['straight', 'right', 'straight', 'left', 'up', 'straight', 'down', 'gap', 'straight'], length: 50, streetWidth: 30, buildingHeight: 40 },
'Phelps Island Drift': { segments: ['straight', 'left', 'left', 'right', 'gap', 'straight', 'right', 'left', 'gap'], length: 45, streetWidth: 28, buildingHeight: 35 },
'Mound Overlook Circuit': { segments: ['straight', 'up', 'right', 'straight', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Lake Minnetonka Sprint': { segments: ['straight', 'left', 'right', 'gap', 'straight', 'up', 'down', 'left', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'St. Paul Riverbend Run': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
'Cathedral Hill Climb': { segments: ['up', 'straight', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Downtown Minneapolis Skyway Loop': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight', 'right'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Nicollet Mall Straightaway': { segments: ['straight', 'straight', 'straight', 'gap', 'straight', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
'Lake of the Isles Island Circuit': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight', 'right'], length: 45, streetWidth: 28, buildingHeight: 35 },
'Minnehaha Falls Meander': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight', 'left'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Milwaukee Loop': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Waukesha Speedway': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Manitowoc Marina Run': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
'Lake Winnebago Circuit': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Door County Coastal Cruise': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Green Bay Bayfront Blitz': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
'Sturgeon Bay Ship Canal Sprint': { segments: ['straight', 'straight', 'straight', 'gap', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
'Wisconsin Dells Rapids Run': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Madison Capitol Circuit': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Superior North Shore Dash': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Houston Oil Refinery Rush': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Dallas Finance District Dash': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Austin Tech Corridor Cruise': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
'San Antonio Riverwalk Run': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Corpus Christi Coastal Run': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Fort Worth Stockyards Loop': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
'Galveston Port Channel Circuit': { segments: ['straight', 'straight', 'straight', 'gap', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
'El Paso Border Trade Boulevard': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Lubbock Innovation Loop': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Midland Permian Basin Sprint': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Clearwater Curve': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Miami Beach Boulevard': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Florida Keys Canal Cruise': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
'Orlando Theme Park Tour': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Tampa Bay Finance District Drift': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'standard'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Jacksonville River City Run': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
'Fort Lauderdale Cruise Port Circuit': { segments: ['straight', 'straight', 'straight', 'gap', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
'Key West Sunset Sprint': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'St. Augustine Colonial Run': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Palm Beach Gold Coast Circuit': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Venice Beach Boardwalk Circuit': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Los Angeles Star-Studded Speedway': { segments: ['straight', 'right', 'straight', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'San Francisco Golden Gate Run': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
'San Diego Coastal Drift': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Anaheim Theme Park Loop': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Santa Barbara Vineyard Tour': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 },
'Palm Springs Desert Dash': { segments: ['straight', 'straight', 'straight', 'gap', 'straight'], length: 55, streetWidth: 35, buildingHeight: 50 },
'Santa Cruz Boardwalk Sprint': { segments: ['straight', 'left', 'up', 'right', 'down', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Monterey Bay Coastal Circuit': { segments: ['straight', 'right', 'left', 'straight', 'gap', 'right', 'straight'], length: 50, streetWidth: 32, buildingHeight: 45 },
'Carmel-by-the-Sea Scenic Route': { segments: ['straight', 'up', 'right', 'down', 'left', 'gap', 'straight'], length: 48, streetWidth: 30, buildingHeight: 40 },
'Napa Valley Vineyard Ride': { segments: ['straight', 'left', 'right', 'gap', 'left', 'straight'], length: 45, streetWidth: 28, buildingHeight: 35 }
};
let currentTrackGroup = null;
let waypoints = [];
// Controls
const keys = { w: false, a: false, s: false, d: false, space: false };
document.addEventListener('keydown', (event) => {
switch (event.key.toLowerCase()) {
case 'w': keys.w = true; break;
case 'a': keys.a = true; break;
case 's': keys.s = true; break;
case 'd': keys.d = true; break;
case ' ': keys.space = true; break;
}
});
document.addEventListener('keyup', (event) => {
switch (event.key.toLowerCase()) {
case 'w': keys.w = false; break;
case 'a': keys.a = false; break;
case 's': keys.s = false; break;
case 'd': keys.d = false; break;
case ' ': keys.space = false; break;
}
});
// Shooting
let canShoot = true;
let shotCooldown = 200;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
document.addEventListener('mousedown', (event) => {
if (event.button === 0 && canShoot && gameState === 'racing' && playerData.active) {
canShoot = false;
setTimeout(() => canShoot = true, shotCooldown);
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
raycaster.setFromCamera(mouse, camera);
const targets = [...aiShips, ...(currentTrackGroup ? currentTrackGroup.children.filter(c => c.isMesh && c.geometry.type === 'BoxGeometry') : [])].filter(t => t !== playerShip && (!t.userData.isPlayer || !t.userData.active));
const intersects = raycaster.intersectObjects(targets);
if (intersects.length > 0) {
const target = intersects[0].object;
const position = playerShip.position.clone().add(new THREE.Vector3(0, 0, -2));
const count = skills.shotCount.effect(skills.shotCount.level);
for (let i = 0; i < count; i++) {
createMissile(position.clone().add(new THREE.Vector3((Math.random() - 0.5) * 0.5, (Math.random() - 0.5) * 0.5, 0)), target);
}
logAction(playerName, score, 'Shot Fired');
}
}
});
// Leaderboard logging
function logAction(name, points, action) {
const logEntry = {
player: name,
score: points,
action: action,
timestamp: new Date().toISOString()
};
let logs = JSON.parse(localStorage.getItem('gameLogs') || '[]');
logs.push(logEntry);
localStorage.setItem('gameLogs', JSON.stringify(logs));
updateLeaderboard();
}
function updateLeaderboard() {
let logs = JSON.parse(localStorage.getItem('gameLogs') || '[]');
document.getElementById('logDisplay').innerHTML = logs.slice(-5).map(log =>
`${log.player} (${log.score} pts): ${log.action} at ${new Date(log.timestamp).toLocaleString()}`
).join('<br>') || 'No history!';
}
// Update character sheet
function updateCharacterSheet() {
document.getElementById('ship-type').textContent = `Type: ${playerData.type}`;
document.getElementById('thruster-1').textContent = `Thruster 1: ${playerData.thrusters[0]}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
document.getElementById('thruster-2').textContent = `Thruster 2: ${playerData.thrusters[1]}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
document.getElementById('thruster-3').textContent = `Thruster 3: ${playerData.thrusters[2]}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
document.getElementById('thruster-4').textContent = `Thruster 4: ${playerData.thrusters[3]}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
document.getElementById('body-front').textContent = `Body Front: ${playerData.body.front}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
document.getElementById('body-back').textContent = `Body Back: ${playerData.body.back}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
document.getElementById('body-left').textContent = `Body Left: ${playerData.body.left}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
document.getElementById('body-right').textContent = `Body Right: ${playerData.body.right}/${shipTypes.find(t => t.name === playerData.type).maxHP}`;
}
// Update skills sheet
function updateSkillsSheet() {
document.getElementById('skill-speed').textContent = skills.speed.level;
document.getElementById('skill-speed-meter').style.width = `${(skills.speed.level / skills.speed.maxLevel) * 100}%`;
document.getElementById('skill-shot-power').textContent = skills.shotPower.level;
document.getElementById('skill-shot-power-meter').style.width = `${(skills.shotPower.level / skills.shotPower.maxLevel) * 100}%`;
document.getElementById('skill-shot-count').textContent = skills.shotCount.level;
document.getElementById('skill-shot-count-meter').style.width = `${(skills.shotCount.level / skills.shotCount.maxLevel) * 100}%`;
document.getElementById('skill-turn-speed').textContent = skills.turnSpeed.level;
document.getElementById('skill-turn-speed-meter').style.width = `${(skills.turnSpeed.level / skills.turnSpeed.maxLevel) * 100}%`;
}
// Damage handling
function applyDamage(ship, damage, hitPosition) {
if (!ship.userData.active) return;
const direction = hitPosition.clone().sub(ship.position).normalize();
const absX = Math.abs(direction.x);
const absZ = Math.abs(direction.z);
let component;
if (absZ > absX) {
component = direction.z > 0 ? 'front' : 'back';
} else {
component = direction.x > 0 ? 'right' : 'left';
}
if (Math.random() < 0.4) {
const thrusterIndex = Math.floor(Math.random() * 4);
if (ship.userData.thrusters[thrusterIndex] > 0) {
ship.userData.thrusters[thrusterIndex] -= damage;
if (ship.userData.thrusters[thrusterIndex] <= 0) {
ship.userData.thrusters[thrusterIndex] = 0;
}
}
} else {
if (ship.userData.body[component] > 0) {
ship.userData.body[component] -= damage;
if (ship.userData.body[component] <= 0) {
ship.userData.body[component] = 0;
}
}
}
createExplosion(hitPosition);
if (ship === playerShip) {
updateCharacterSheet();
logAction(playerName, score, 'Ship Damaged');
}
checkShipStatus(ship);
}
function checkShipStatus(ship, hitPosition) {
const totalHP = ship.userData.thrusters.reduce((a, b) => a + b, 0) +
Object.values(ship.userData.body).reduce((a, b) => a + b, 0);
if (totalHP <= 0) {
ship.userData.active = false;
ship.visible = false;
createExplosion(ship.position);
if (ship.userData.splitCount < 3) {
const newScale = 0.5;
const offsets = [
new THREE.Vector3(1, 0, 1),
new THREE.Vector3(-1, 0, -1)
];
offsets.forEach(offset => {
const newShip = createShip(
ship.userData.typeIndex,
false,
newScale,
ship.userData.splitCount + 1
);
newShip.position.copy(ship.position).add(offset.multiplyScalar(2));
newShip.userData.currentWaypoint = ship.userData.currentWaypoint;
});
}
if (!ship.userData.isPlayer) {
const index = aiShips.indexOf(ship);
if (index > -1) aiShips.splice(index, 1);
score += 100;
document.getElementById('score').textContent = `Score: ${score}`;
logAction(playerName, score, 'Enemy Destroyed');
}
if (ship === playerShip) {
document.getElementById('status').textContent = 'Status: Destroyed';
logAction(playerName, score, 'Player Destroyed');
endRace();
}
}
}
// Building damage
function applyBuildingDamage(building, damage, hitPosition) {
building.userData.currentHP -= damage;
createExplosion(hitPosition);
if (building.userData.currentHP <= 0) {
currentTrackGroup.remove(building);
if (building.userData.splitCount < 3) {
const newScale = 0.5;
const offsets = [
new THREE.Vector3(5, 0, 5),
new THREE.Vector3(-5, 0, -5)
];
offsets.forEach(offset => {
const newBuilding = createBuilding(
building.position.x + offset.x,
building.position.z + offset.z,
building.userData.width,
building.userData.depth,
building.userData.height,
newScale,
building.userData.splitCount + 1
);
currentTrackGroup.add(newBuilding);
});
}
score += 100;
document.getElementById('score').textContent = `Score: ${score}`;
logAction(playerName, score, 'Building Destroyed');
}
}
// Pickup handling
function updatePickups() {
for (let i = pickups.length - 1; i >= 0; i--) {
const pickup = pickups[i];
if (playerShip.position.distanceTo(pickup.position) < 2 && playerData.active) {
const skill = skills[pickup.userData.skill];
if (skill.level < skill.maxLevel) {
skill.level++;
updateSkillsSheet();
logAction(playerName, score, `Picked ${pickup.userData.skill}`);
}
scene.remove(pickup);
pickups.splice(i, 1);
}
}
}
// Missile update
function updateMissiles() {
for (let i = missiles.length - 1; i >= 0; i--) {
const missile = missiles[i];
missile.userData.lifetime--;
if (missile.userData.lifetime <= 0 || !missile.userData.target || (!missile.userData.target.userData?.active && !missile.userData.target.isMesh)) {
scene.remove(missile);
missiles.splice(i, 1);
continue;
}
const targetPos = missile.userData.target.isMesh ? missile.userData.target.position : missile.userData.target.position;
const direction = targetPos.clone().sub(missile.position).normalize();
missile.position.add(direction.multiplyScalar(missile.userData.speed));
missile.lookAt(targetPos);
missile.rotation.x += Math.PI / 2;
// Update flame trail
const flame = missile.children[0];
const positions = flame.geometry.attributes.position.array;
for (let j = 0; j < positions.length / 3; j++) {
const j3 = j * 3;
positions[j3] = (Math.random() - 0.5) * 0.1;
positions[j3 + 1] = (Math.random() - 0.5) * 0.1;
positions[j3 + 2] = -0.3 - Math.random() * 0.2;
}
flame.geometry.attributes.position.needsUpdate = true;
// Check collision
const missileBox = new THREE.Box3().setFromObject(missile);
const targetBox = new THREE.Box3().setFromObject(missile.userData.target);
if (missileBox.intersectsBox(targetBox)) {
if (missile.userData.target.isMesh && missile.userData.target.geometry.type === 'BoxGeometry') {
applyBuildingDamage(missile.userData.target, missile.userData.damage, missile.position);
} else {
applyDamage(missile.userData.target, missile.userData.damage, missile.position);
}
scene.remove(missile);
missiles.splice(i, 1);
}
}
}
// Player movement
let rotation = 0;
let bankAngle = 0;
let jumpHeight = 0;
function updatePlayer() {
if (!playerData.active) return;
const activeThrusters = playerData.thrusters.filter(hp => hp > 0).length;
const speedMultiplier = skills.speed.effect(skills.speed.level);
const maxSpeed = playerData.maxSpeed * (activeThrusters / 4) * speedMultiplier;
const acceleration = playerData.acceleration * (activeThrusters / 4) * speedMultiplier;
const turnSpeed = skills.turnSpeed.effect(skills.turnSpeed.level);
if (keys.w) playerData.speed = Math.min(playerData.speed + acceleration, maxSpeed);
else if (keys.s) playerData.speed = Math.max(playerData.speed - acceleration, -maxSpeed / 2);
else playerData.speed *= 0.995;
if (keys.a) rotation += turnSpeed * (activeThrusters / 4);
if (keys.d) rotation -= turnSpeed * (activeThrusters / 4);
bankAngle = (keys.a ? -0.3 : 0) + (keys.d ? 0.3 : 0);
if (keys.space && !isJumping && jumpCooldown <= 0 && activeThrusters > 0) {
isJumping = true;
jumpHeight = 5;
jumpCooldown = 60;
logAction(playerName, score, 'Jumped');
}
if (isJumping) {
jumpHeight -= 0.2;
if (jumpHeight <= 0) {
jumpHeight = 0;
isJumping = false;
}
}
if (jumpCooldown > 0) jumpCooldown--;
playerShip.rotation.z = rotation;
playerShip.rotation.x = bankAngle;
const direction = new THREE.Vector3(0, 0, -1).applyAxisAngle(new THREE.Vector3(0, 1, 0), rotation);
playerShip.position.add(direction.multiplyScalar(playerData.speed));
playerShip.position.y = 10 + Math.sin(Date.now() * 0.005) * 0.2 + jumpHeight;
checkCollisions(playerShip);
checkColumnProximity(playerShip);
updatePickups();
camera.position.copy(playerShip.position).add(new THREE.Vector3(0, 5 + jumpHeight, 15).applyAxisAngle(new THREE.Vector3(0, 1, 0), rotation));
camera.lookAt(playerShip.position);
}
// Collision detection
function checkCollisions(ship) {
if (!currentTrackGroup || !ship.userData.active) return;
currentTrackGroup.traverse(child => {
if (child.isMesh && child.geometry.type !== 'CylinderGeometry') {
const shipBox = new THREE.Box3().setFromObject(ship);
const wallBox = new THREE.Box3().setFromObject(child);
if (shipBox.intersectsBox(wallBox) && !isJumping) {
const direction = ship.position.clone().sub(child.position).normalize();
ship.position.add(direction.multiplyScalar(0.5));
ship.userData.speed *= 0.8;
applyDamage(ship, 2, ship.position);
}
}
});
}
// Column proximity nudging
function checkColumnProximity(ship) {
if (!currentTrackGroup || !ship.userData.active) return;
currentTrackGroup.traverse(child => {
if (child.isMesh && child.geometry.type === 'CylinderGeometry') {
const shipPos = ship.position;
const columnPos = child.position;
const distance = shipPos.distanceTo(columnPos);
if (distance < 5) {
const direction = shipPos.clone().sub(columnPos).normalize();
ship.position.add(direction.multiplyScalar(0.2));
}
}
});
}
// Explosion update
const explosions = [];
function updateExplosions() {
for (let i = explosions.length - 1; i >= 0; i--) {
const explosion = explosions[i];
explosion.userData.lifetime--;
if (explosion.userData.lifetime <= 0) {
scene.remove(explosion);
explosions.splice(i, 1);
continue;
}
const positions = explosion.geometry.attributes.position.array;
for (let j = 0; j < positions.length / 3; j++) {
const j3 = j * 3;
positions[j3] += explosion.userData.velocities[j].x;
positions[j3 + 1] += explosion.userData.velocities[j].y;
positions[j3 + 2] += explosion.userData.velocities[j].z;
}
explosion.geometry.attributes.position.needsUpdate = true;
explosion.material.opacity = explosion.userData.lifetime / 60;
}
}
// AI movement
function updateAI() {
aiShips.forEach((ship, index) => {
if (!ship.userData.active) return;
const activeThrusters = ship.userData.thrusters.filter(hp => hp > 0).length;
const maxSpeed = ship.userData.maxSpeed * (activeThrusters / 4);
const target = waypoints[ship.userData.currentWaypoint];
const direction = target.clone().sub(ship.position).normalize();
ship.position.add(direction.multiplyScalar(maxSpeed));
ship.rotation.z = Math.atan2(direction.x, -direction.z);
ship.position.y = 10 + Math.sin(Date.now() * 0.005 + index) * 0.2;
if (ship.position.distanceTo(target) < 2) {
ship.userData.currentWaypoint = (ship.userData.currentWaypoint + 1) % waypoints.length;
}
checkCollisions(ship);
checkColumnProximity(ship);
});
}
// Race start
function startRace(trackName) {
gameState = 'racing';
currentTrack = trackName;
if (currentTrackGroup) scene.remove(currentTrackGroup);
pickups.forEach(p => scene.remove(p));
missiles.forEach(m => scene.remove(m));
pickups.length = 0;
missiles.length = 0;
const { trackGroup, waypoints: newWaypoints, startPosition } = generateTrack(trackConfigs[trackName]);
currentTrackGroup = trackGroup;
waypoints = newWaypoints;
scene.add(trackGroup);
playerShip.position.set(startPosition.x, 10, startPosition.z);
playerShip.rotation.z = Math.atan2(waypoints[1].x - waypoints[0].x, waypoints[1].z - waypoints[0].z);
playerData.speed = 0;
playerData.active = true;
playerShip.visible = true;
aiShips.forEach((ship, i) => {
ship.position.set(startPosition.x + (i + 1) * 5, 10, startPosition.z);
ship.userData.currentWaypoint = 0;
ship.userData.lastGatePass = 0;
ship.userData.active = true;
ship.visible = true;
const type = shipTypes.find(t => t.name === ship.userData.type);
ship.userData.thrusters = Array(4).fill(type.maxHP);
ship.userData.body = { front: type.maxHP, back: type.maxHP, left: type.maxHP, right: type.maxHP };
});
startGate.position.copy(startPosition);
startGate.position.y = 12;
startGate.rotation.y = Math.atan2(waypoints[1].x - waypoints[0].x, waypoints[1].z - waypoints[0].z);
raceTime = 0;
score = 0;
document.getElementById('time').textContent = `Time: 0`;
document.getElementById('score').textContent = `Score: ${score}`;
document.getElementById('status').textContent = `Status: Active`;
updateCharacterSheet();
updateSkillsSheet();
logAction(playerName, score, `Started ${trackName}`);
}
// Restart race
function restartRace() {
scene.remove(playerShip);
explosions.forEach(e => scene.remove(e));
pickups.forEach(p => scene.remove(p));
missiles.forEach(m => scene.remove(m));
explosions.length = 0;
pickups.length = 0;
missiles.length = 0;
initPlayer();
startRace(currentTrack);
}
// Race end
function endRace() {
gameState = 'menu';
const activeShips = aiShips.filter(s => s.userData.active).length + (playerData.active ? 1 : 0);
alert(`Race Over! Ships Remaining: ${activeShips}, Time: ${raceTime.toFixed(2)}s, Score: ${score}`);
logAction(playerName, score, 'Race Ended');
}
// Gallery
function showGallery() {
document.getElementById('gallery').style.display = 'block';
}
function hideGallery() {
document.getElementById('gallery').style.display = 'none';
}
// Game loop
function animate() {
requestAnimationFrame(animate);
if (gameState === 'racing') {
updatePlayer();
updateAI();
updateMissiles();
updateExplosions();
raceTime += 1 / 60;
document.getElementById('time').textContent = `Time: ${raceTime.toFixed(2)}`;
if (aiShips.every(s => !s.userData.active) && playerData.active) {
endRace();
}
}
renderer.render(scene, camera);
}
// Resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// Start
initPlayer();
startRace('Phelps Island Drift');
animate();
</script>
</body>
</html>