Spaces:
Running
Running
<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> |