TinyHTMLGame1 / index.html
awacke1's picture
Update index.html
995d988 verified
raw
history blame
56.2 kB
<!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 {
margin-top: 20px;
font-size: 14px;
}
.sidebar .character-sheet div, .sidebar .skills-sheet 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="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>
<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 currentTrack = 'Phelps Island Drift';
let lastGatePass = 0;
let isJumping = false;
let jumpCooldown = 0;
// 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) {
const type = shipTypes[typeIndex];
const ship = new THREE.Group();
const bodyGeometry = new THREE.BoxGeometry(type.scale.x, type.scale.y, type.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, 0.2, 0.5, 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) * type.scale.x * 0.8,
0,
(i < 2 ? 0.5 : -0.5) * type.scale.z * 0.8
);
thruster.rotation.x = Math.PI / 2;
ship.add(thruster);
}
} else {
const jetpackGeometry = new THREE.CylinderGeometry(0.3, 0.3, 0.8, 8);
const jetpack = new THREE.Mesh(jetpackGeometry, new THREE.MeshStandardMaterial({ color: 0xaaaaaa }));
jetpack.position.set(0, -0.5, 0);
ship.add(jetpack);
}
ship.position.y = 10;
ship.castShadow = true;
ship.userData = {
type: type.name,
maxSpeed: type.speed,
acceleration: type.accel,
thrusters: Array(4).fill(type.maxHP),
body: { front: type.maxHP, back: type.maxHP, left: type.maxHP, right: type.maxHP },
active: true,
speed: 0,
currentWaypoint: 0,
lastGatePass: 0
};
scene.add(ship);
return ship;
}
// Initialize player
function initPlayer() {
const typeIndex = Math.floor(Math.random() * shipTypes.length);
playerShip = createShip(typeIndex, true);
playerData = playerShip.userData;
// Reset skills
Object.keys(skills).forEach(skill => skills[skill].level = 0);
updateCharacterSheet();
updateSkillsSheet();
}
// 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);
// Particle system for bullets
const bullets = [];
function createBullet(position, direction) {
const geometry = new THREE.SphereGeometry(0.2, 8, 8);
const material = new THREE.MeshBasicMaterial({ color: 0xffff00 });
const bullet = new THREE.Mesh(geometry, material);
bullet.position.copy(position);
bullet.userData = {
direction: direction.clone(),
lifetime: 120,
damage: skills.shotPower.effect(skills.shotPower.level)
};
scene.add(bullet);
bullets.push(bullet);
}
// 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) {
const geometry = new THREE.BoxGeometry(width, height, depth);
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 / 2, z);
building.castShadow = true;
building.receiveShadow = true;
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', 'straight'], 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;
document.addEventListener('mousedown', (event) => {
if (event.button === 0 && canShoot && gameState === 'racing' && playerData.active) {
canShoot = false;
setTimeout(() => canShoot = true, shotCooldown);
const direction = new THREE.Vector3();
camera.getWorldDirection(direction);
const position = playerShip.position.clone().add(direction.clone().multiplyScalar(2));
const count = skills.shotCount.effect(skills.shotCount.level);
for (let i = 0; i < count; i++) {
const offset = new THREE.Vector3((Math.random() - 0.5) * 0.1, (Math.random() - 0.5) * 0.1, 0);
createBullet(position.clone().add(offset), direction);
}
}
});
// 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();
}
checkShipStatus(ship);
}
function checkShipStatus(ship) {
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 === playerShip) {
document.getElementById('status').textContent = 'Status: Destroyed';
endRace();
}
}
}
// 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();
}
scene.remove(pickup);
pickups.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;
}
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));
}
}
});
}
// Bullet update
function updateBullets() {
const raycaster = new THREE.Raycaster();
for (let i = bullets.length - 1; i >= 0; i--) {
const bullet = bullets[i];
bullet.userData.lifetime--;
if (bullet.userData.lifetime <= 0) {
scene.remove(bullet);
bullets.splice(i, 1);
continue;
}
bullet.position.add(bullet.userData.direction.clone().multiplyScalar(1));
raycaster.set(bullet.position, bullet.userData.direction);
const intersects = raycaster.intersectObjects([...aiShips, playerShip].filter(s => s.userData.active));
if (intersects.length > 0) {
const hitShip = intersects[0].object.parent;
applyDamage(hitShip, bullet.userData.damage, bullet.position);
scene.remove(bullet);
bullets.splice(i, 1);
}
}
}
// 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));
pickups.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;
document.getElementById('time').textContent = `Time: 0`;
document.getElementById('status').textContent = `Status: Active`;
updateCharacterSheet();
updateSkillsSheet();
}
// Restart race
function restartRace() {
scene.remove(playerShip);
bullets.forEach(b => scene.remove(b));
explosions.forEach(e => scene.remove(e));
pickups.forEach(p => scene.remove(p));
bullets.length = 0;
explosions.length = 0;
pickups.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`);
}
// 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();
updateBullets();
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>