// Helper Functions /** * Utility function to check if the airplane collides with the ground or a building. */ function checkCollision(airplane, buildingBoxes) { // Ground collision if (airplane.position.y <= 0) { return true; } // Building collisions for (const box of buildingBoxes) { if ( airplane.position.x > box.min.x && airplane.position.x < box.max.x && airplane.position.y > box.min.y && airplane.position.y < box.max.y && airplane.position.z > box.min.z && airplane.position.z < box.max.z ) { return true; } } return false; } /** * Utility function to update the camera position and orientation to follow the airplane. */ function updateCamera(camera, airplane) { camera.position.set( airplane.position.x, airplane.position.y + 5, airplane.position.z - 10 ); camera.lookAt(airplane.position); } /** * Utility function to update the distance UI element. */ function updateDistanceDisplay(airplane, distanceElement) { const horizontalDistance = Math.sqrt( airplane.position.x ** 2 + airplane.position.z ** 2 ); distanceElement.innerText = `Distance: ${horizontalDistance.toFixed(2)}`; } // Scene Setup const scene = new THREE.Scene(); // Create sunset gradient background const canvas = document.createElement('canvas'); canvas.width = 1; canvas.height = 256; const context = canvas.getContext('2d'); const gradient = context.createLinearGradient(0, 256, 0, 0); gradient.addColorStop(0, '#FF4500'); // Orange-red at the bottom (horizon) gradient.addColorStop(0.4, '#4169E1'); // Royal blue in the middle gradient.addColorStop(1, '#000000'); // Black at the top context.fillStyle = gradient; context.fillRect(0, 0, 1, 256); const texture = new THREE.CanvasTexture(canvas); // Center the texture so rotation pivots around its middle texture.center.set(0.5, 0.5); scene.background = texture; // Create stars in the night sky function createStars() { const starsCount = 1000; const starsGeometry = new THREE.BufferGeometry(); const starPositions = new Float32Array(starsCount * 3); for (let i = 0; i < starsCount; i++) { const i3 = i * 3; // Generate stars in a large hemisphere above the scene const radius = 500; const theta = Math.random() * Math.PI * 2; const phi = Math.random() * Math.PI * 0.65; // Limit to upper hemisphere starPositions[i3] = radius * Math.sin(phi) * Math.cos(theta); starPositions[i3 + 1] = radius * Math.cos(phi) + 100; // Lift up a bit starPositions[i3 + 2] = radius * Math.sin(phi) * Math.sin(theta); } starsGeometry.setAttribute( "position", new THREE.BufferAttribute(starPositions, 3) ); const starsMaterial = new THREE.PointsMaterial({ color: 0xffffff, size: 1, sizeAttenuation: false, }); const stars = new THREE.Points(starsGeometry, starsMaterial); scene.add(stars); } createStars(); // Camera Setup const camera = new THREE.PerspectiveCamera( 75, window.innerWidth / window.innerHeight, 0.1, 1000 ); // Renderer Setup const renderer = new THREE.WebGLRenderer(); renderer.setSize(window.innerWidth, window.innerHeight); renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap; document.body.appendChild(renderer.domElement); // Lighting const light = new THREE.DirectionalLight(0xfff0dd, 1.5); // Warm sunlight color light.position.set(-100, 200, -50); // More realistic sun angle light.castShadow = true; light.shadow.mapSize.width = 512; light.shadow.mapSize.height = 512; light.shadow.camera.near = 0.1; light.shadow.camera.far = 500; light.shadow.camera.left = -50; light.shadow.camera.right = 50; light.shadow.camera.top = 50; light.shadow.camera.bottom = -50; light.shadow.bias = -0.0001; const ambientLight = new THREE.AmbientLight(0x6688cc, 0.4); // Subtle blue sky light scene.add(ambientLight); scene.add(light); // Paper Airplane function createPaperAirplane() { // Create a group to hold all paper airplane parts const airplaneGroup = new THREE.Group(); // Main body/fuselage (triangle) const bodyShape = new THREE.Shape(); bodyShape.moveTo(0, 0); // Nose bodyShape.lineTo(-0.2, 0.5); // Left mid fold bodyShape.lineTo(-0.4, 1.0); // Left back corner bodyShape.lineTo(0.4, 1.0); // Right back corner bodyShape.lineTo(0.2, 0.5); // Right mid fold bodyShape.lineTo(0, 0); // Back to nose const bodyGeometry = new THREE.ExtrudeGeometry(bodyShape, { depth: 0.03, bevelEnabled: false }); const paperMaterial = new THREE.MeshLambertMaterial({ color: 0xf0f0f0, side: THREE.DoubleSide, }); const body = new THREE.Mesh(bodyGeometry, paperMaterial); body.castShadow = true; body.receiveShadow = true; // Left wing (triangle extending outward) const leftWingShape = new THREE.Shape(); leftWingShape.moveTo(0, 0.2); // Front connection to body leftWingShape.lineTo(-0.3, 0.8); // Back connection to body leftWingShape.lineTo(-0.7, 0.5); // Wing tip leftWingShape.lineTo(0, 0.2); // Back to start const leftWingGeometry = new THREE.ExtrudeGeometry(leftWingShape, { depth: 0.02, bevelEnabled: false }); const leftWing = new THREE.Mesh(leftWingGeometry, paperMaterial); leftWing.castShadow = true; leftWing.receiveShadow = true; leftWing.position.y = 0.01; // Slight offset to prevent z-fighting leftWing.rotation.x = 0.2; // Right wing (triangle extending outward) const rightWingShape = new THREE.Shape(); rightWingShape.moveTo(0, 0.2); // Front connection to body rightWingShape.lineTo(0.3, 0.8); // Back connection to body rightWingShape.lineTo(0.7, 0.5); // Wing tip rightWingShape.lineTo(0, 0.2); // Back to start const rightWingGeometry = new THREE.ExtrudeGeometry(rightWingShape, { depth: 0.02, bevelEnabled: false }); const rightWing = new THREE.Mesh(rightWingGeometry, paperMaterial); rightWing.castShadow = true; rightWing.receiveShadow = true; rightWing.position.y = 0.02; // Slight offset to prevent z-fighting rightWing.rotation.x = 0.2; // Add center fold line for realism const foldLineGeometry = new THREE.BufferGeometry(); const foldLinePoints = [ new THREE.Vector3(0, 0.03, 0), // Slightly above nose new THREE.Vector3(0, 0.03, 1.0), // Slightly above back ]; foldLineGeometry.setFromPoints(foldLinePoints); const foldLineMaterial = new THREE.LineBasicMaterial({ color: 0xdddddd }); const foldLine = new THREE.Line(foldLineGeometry, foldLineMaterial); foldLine.position.z = 0.026; // Slightly above the extruded body // Add all parts to the group airplaneGroup.add(body); airplaneGroup.add(leftWing); airplaneGroup.add(rightWing); airplaneGroup.add(foldLine); // Rotate and position airplaneGroup.rotation.order = "ZXY"; airplaneGroup.rotation.x = -Math.PI / 2; // Rotate to face forward return airplaneGroup; } const airplane = createPaperAirplane(); airplane.position.set(0, 10.1, 0); scene.add(airplane); // Cityscape Environment const buildingGeometry = new THREE.BoxGeometry(1, 1, 1); // Realistic building colors const buildingColors = [ 0x8c8c8c, // Concrete gray 0x9c5b3c, // Brick red-brown 0x5a7d9e, // Steel blue 0xbcbcbc, // Light gray 0x4a4a4a, // Dark gray ]; const buildings = []; const buildingBoxes = []; // Window parameters const floorHeight = 1; const windowWidth = 0.2; const windowHeight = 0.3; const horizontalSpacingMin = 0.1; const epsilon = 0.01; // Create two window materials - dark and lit const darkWindowMaterial = new THREE.MeshLambertMaterial({ color: 0x0a1a2a, transparent: true, opacity: 0.5, }); // Dark blue glass const litWindowMaterial = new THREE.MeshLambertMaterial({ color: 0xffeb3b, transparent: true, opacity: 0.8, emissive: 0xffeb3b, emissiveIntensity: 0.5, }); // Yellow lit windows function createBuilding(x, z, height, width) { const colorIndex = Math.floor(Math.random() * buildingColors.length); const buildingMaterial = new THREE.MeshLambertMaterial({ color: buildingColors[colorIndex], }); const building = new THREE.Mesh(buildingGeometry, buildingMaterial); building.scale.set(width, height, width); building.position.set(x, height / 2, z); building.castShadow = true; building.receiveShadow = true; scene.add(building); buildings.push(building); buildingBoxes.push({ min: new THREE.Vector3(x - width / 2, 0, z - width / 2), max: new THREE.Vector3(x + width / 2, height, z + width / 2), }); // Add windows if building is sizable const numFloors = Math.floor(height / floorHeight); if (numFloors > 0) { const n_horizontal = Math.floor( (width + horizontalSpacingMin) / (windowWidth + horizontalSpacingMin) ); if (n_horizontal > 0) { const spacing_horizontal = (width - n_horizontal * windowWidth) / (n_horizontal + 1); const faces = [ { normal: new THREE.Vector3(0, 0, 1), offset: width / 2 + epsilon, rotationY: 0, }, // Front { normal: new THREE.Vector3(0, 0, -1), offset: -width / 2 - epsilon, rotationY: Math.PI, }, // Back { normal: new THREE.Vector3(-1, 0, 0), offset: -width / 2 - epsilon, rotationY: -Math.PI / 2, }, // Left { normal: new THREE.Vector3(1, 0, 0), offset: width / 2 + epsilon, rotationY: Math.PI / 2, }, // Right ]; // Create a merged BufferGeometry for all windows const windowCount = numFloors * n_horizontal * faces.length; const positions = new Float32Array(windowCount * 12); // 4 vertices * 3 coords per window const indices = new Uint16Array(windowCount * 6); // 2 triangles * 3 indices per window let posIndex = 0; let idxIndex = 0; let vertexOffset = 0; for (const face of faces) { const { offset, rotationY } = face; const rotationMatrix = new THREE.Matrix4().makeRotationY(rotationY); for (let k = 0; k < numFloors; k++) { const y = (k + 0.5) * floorHeight; for (let m = 0; m < n_horizontal; m++) { let x_local, z_local; if (face.normal.x !== 0) { // Left or right face z_local = z - width / 2 + spacing_horizontal + m * (windowWidth + spacing_horizontal) + windowWidth / 2; x_local = x + offset; } else { // Front or back face x_local = x - width / 2 + spacing_horizontal + m * (windowWidth + spacing_horizontal) + windowWidth / 2; z_local = z + offset; } const windowPos = new THREE.Vector3(x_local, y, z_local); // Define the four vertices of the window plane const halfW = windowWidth / 2; const halfH = windowHeight / 2; const vertices = [ new THREE.Vector3(-halfW, -halfH, 0), new THREE.Vector3(halfW, -halfH, 0), new THREE.Vector3(halfW, halfH, 0), new THREE.Vector3(-halfW, halfH, 0), ]; // Apply rotation and translation vertices.forEach((v) => { v.applyMatrix4(rotationMatrix); v.add(windowPos); }); // Add positions positions[posIndex++] = vertices[0].x; positions[posIndex++] = vertices[0].y; positions[posIndex++] = vertices[0].z; positions[posIndex++] = vertices[1].x; positions[posIndex++] = vertices[1].y; positions[posIndex++] = vertices[1].z; positions[posIndex++] = vertices[2].x; positions[posIndex++] = vertices[2].y; positions[posIndex++] = vertices[2].z; positions[posIndex++] = vertices[3].x; positions[posIndex++] = vertices[3].y; positions[posIndex++] = vertices[3].z; // Add indices (two triangles per quad) indices[idxIndex++] = vertexOffset + 0; indices[idxIndex++] = vertexOffset + 1; indices[idxIndex++] = vertexOffset + 2; indices[idxIndex++] = vertexOffset + 0; indices[idxIndex++] = vertexOffset + 2; indices[idxIndex++] = vertexOffset + 3; vertexOffset += 4; } } } // Create and populate BufferGeometry const mergedWindowGeometry = new THREE.BufferGeometry(); mergedWindowGeometry.setAttribute( "position", new THREE.BufferAttribute(positions, 3) ); mergedWindowGeometry.setIndex(new THREE.BufferAttribute(indices, 1)); // Append window positions to global arrays for (let i = 0; i < positions.length; i += 12) { if (Math.random() < 0.2) { // 20% chance to be lit allLitWindowPositions.push(...positions.slice(i, i + 12)); } else { allDarkWindowPositions.push(...positions.slice(i, i + 12)); } } } } return building; } function createGlobe(x, y, z) { // Create the main sphere const geometry = new THREE.SphereGeometry(1.0, 16, 16); const material = new THREE.MeshBasicMaterial({ color: 0x00ff00, }); const globe = new THREE.Mesh(geometry, material); globe.position.set(x, y, z); // Create outer glow sphere const glowGeometry = new THREE.SphereGeometry(1.3, 16, 16); const glowMaterial = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.3, side: THREE.BackSide }); const glow = new THREE.Mesh(glowGeometry, glowMaterial); globe.add(glow); // Create second outer glow for more intensity const glow2Geometry = new THREE.SphereGeometry(1.6, 16, 16); const glow2Material = new THREE.MeshBasicMaterial({ color: 0x00ff00, transparent: true, opacity: 0.15, side: THREE.BackSide }); const glow2 = new THREE.Mesh(glow2Geometry, glow2Material); globe.add(glow2); // Add a pulsing glow effect globe.userData.pulsePhase = Math.random() * Math.PI * 2; // Random starting phase globe.userData.glowLayers = [glow, glow2]; // Store references to glow layers scene.add(globe); return globe; } // Generate more buildings // Arrays for merging windows let allDarkWindowPositions = []; let allLitWindowPositions = []; for (let z = 20; z < 700; z += 5) { // Start at z=20 instead of z=10 to double the gap for (let x = -60; x <= 60; x += 5) { let placeBuilding = Math.random() > 0.3; let height, width; // Calculate base height first const baseHeight = Math.random() * 15 + 5; // Normal height range: 5 to 20 // 2% chance for a super tall building (30% higher) if (Math.random() < 0.02) { height = baseHeight * 1.3; // 30% higher than normal buildings width = Math.random() * 4 + 3; // Wider base for tall buildings } else { height = baseHeight; width = Math.random() * 3 + 1; } if (placeBuilding) { const offsetX = (Math.random() - 0.5) * 2; const offsetZ = (Math.random() - 0.5) * 2; createBuilding(x + offsetX, z + offsetZ, height, width); } } } // Starting building for takeoff const startingBuilding = createBuilding(0, 0, 10, 2); startingBuilding.material.color.set(0x0000ff); // Create boost recharge globes let globes = []; function initGlobes() { // Remove existing globes globes.forEach((globe) => scene.remove(globe)); globes = []; // Define globe z-positions const globe_z_positions = []; for (let z = 40; z <= 680; z += 20) { globe_z_positions.push(z); } // Place globes relative to buildings globe_z_positions.forEach((z) => { const nearbyBuildings = buildings.filter( (b) => b.position.z >= z - 10 && b.position.z <= z + 10 ); if (nearbyBuildings.length > 0) { const randomBuilding = nearbyBuildings[Math.floor(Math.random() * nearbyBuildings.length)]; const offsetX = (Math.random() - 0.5) * 2; const offsetZ = (Math.random() - 0.5) * 2; const globeX = randomBuilding.position.x + offsetX; const globeZ = randomBuilding.position.z + offsetZ; const globeY = randomBuilding.position.y + randomBuilding.scale.y / 2 + 5; const globe = createGlobe(globeX, globeY, globeZ); globes.push(globe); } }); } // Initialize globes after buildings are created initGlobes(); // Create merged window meshes const darkGeometry = new THREE.BufferGeometry(); const darkPositionsArray = new Float32Array(allDarkWindowPositions); darkGeometry.setAttribute( "position", new THREE.BufferAttribute(darkPositionsArray, 3) ); const numDarkWindows = allDarkWindowPositions.length / 12; const darkIndices = []; for (let i = 0; i < numDarkWindows; i++) { const offset = i * 4; darkIndices.push( offset, offset + 1, offset + 2, offset, offset + 2, offset + 3 ); } darkGeometry.setIndex(darkIndices); const darkWindowsMesh = new THREE.Mesh(darkGeometry, darkWindowMaterial); darkWindowsMesh.receiveShadow = true; scene.add(darkWindowsMesh); // Create lit windows mesh const litGeometry = new THREE.BufferGeometry(); const litPositionsArray = new Float32Array(allLitWindowPositions); litGeometry.setAttribute( "position", new THREE.BufferAttribute(litPositionsArray, 3) ); const numLitWindows = allLitWindowPositions.length / 12; const litIndices = []; for (let i = 0; i < numLitWindows; i++) { const offset = i * 4; litIndices.push( offset, offset + 1, offset + 2, offset, offset + 2, offset + 3 ); } litGeometry.setIndex(litIndices); const litWindowsMesh = new THREE.Mesh(litGeometry, litWindowMaterial); litWindowsMesh.receiveShadow = true; scene.add(litWindowsMesh); // Ground Plane with texture const groundGeometry = new THREE.PlaneGeometry(2000, 2000, 100, 100); const groundCanvas = document.createElement('canvas'); groundCanvas.width = 1024; groundCanvas.height = 1024; const groundContext = groundCanvas.getContext('2d'); // Fill with dark base color groundContext.fillStyle = '#111111'; groundContext.fillRect(0, 0, 1024, 1024); // Draw grid pattern groundContext.strokeStyle = '#333333'; groundContext.lineWidth = 1; // Draw major grid lines const majorGridSize = 64; groundContext.beginPath(); for (let i = 0; i <= 1024; i += majorGridSize) { groundContext.moveTo(i, 0); groundContext.lineTo(i, 1024); groundContext.moveTo(0, i); groundContext.lineTo(1024, i); } groundContext.stroke(); // Draw minor grid lines groundContext.strokeStyle = '#222222'; groundContext.lineWidth = 0.5; const minorGridSize = 16; groundContext.beginPath(); for (let i = 0; i <= 1024; i += minorGridSize) { if (i % majorGridSize !== 0) { // Skip where major lines already exist groundContext.moveTo(i, 0); groundContext.lineTo(i, 1024); groundContext.moveTo(0, i); groundContext.lineTo(1024, i); } } groundContext.stroke(); // Add radial gradient for fade-out effect const groundGradient = groundContext.createRadialGradient(512, 512, 0, 512, 512, 700); groundGradient.addColorStop(0, 'rgba(0, 0, 0, 0)'); groundGradient.addColorStop(0.7, 'rgba(0, 0, 0, 0.3)'); groundGradient.addColorStop(1, 'rgba(0, 0, 0, 0.9)'); groundContext.fillStyle = groundGradient; groundContext.fillRect(0, 0, 1024, 1024); const groundTexture = new THREE.CanvasTexture(groundCanvas); groundTexture.wrapS = THREE.RepeatWrapping; groundTexture.wrapT = THREE.RepeatWrapping; groundTexture.repeat.set(4, 4); const groundMaterial = new THREE.MeshLambertMaterial({ map: groundTexture, transparent: true, opacity: 0.9 }); const ground = new THREE.Mesh(groundGeometry, groundMaterial); ground.rotation.x = -Math.PI / 2; ground.position.y = 0; ground.receiveShadow = true; scene.add(ground); // Trail system for the airplane const trailLength = 50; // Number of points in the trail const trailPositions = new Float32Array(trailLength * 3); const trailGeometry = new THREE.BufferGeometry(); trailGeometry.setAttribute( "position", new THREE.BufferAttribute(trailPositions, 3) ); // Create gradient trail material const trailMaterial = new THREE.LineBasicMaterial({ color: 0x88ccff, transparent: true, opacity: 0.7, vertexColors: true, linewidth: 1, }); // Add vertex colors for gradient effect const trailColors = new Float32Array(trailLength * 3); for (let i = 0; i < trailLength; i++) { // Create a gradient from light blue to darker blue const intensity = 1 - i / trailLength; trailColors[i * 3] = 0.4 * intensity; // R (less red for blue color) trailColors[i * 3 + 1] = 0.7 * intensity; // G (medium green for cyan/blue) trailColors[i * 3 + 2] = 1.0 * intensity; // B (full blue) } trailGeometry.setAttribute("color", new THREE.BufferAttribute(trailColors, 3)); const trail = new THREE.Line(trailGeometry, trailMaterial); scene.add(trail); // Function to update the trail positions function updateTrail(newPosition) { // Shift all positions one slot back for (let i = trailLength - 1; i > 0; i--) { trailPositions[i * 3] = trailPositions[(i - 1) * 3]; trailPositions[i * 3 + 1] = trailPositions[(i - 1) * 3 + 1]; trailPositions[i * 3 + 2] = trailPositions[(i - 1) * 3 + 2]; } // Add the new position at the front trailPositions[0] = newPosition.x; trailPositions[1] = newPosition.y; trailPositions[2] = newPosition.z; // Update the geometry trailGeometry.attributes.position.needsUpdate = true; } // Game State and Physics Variables let gameState = "aiming"; let velocity = new THREE.Vector3(0, 0, 0); const gravity = 2.5; // Increased gravity from 1 to 2.5 const acceleration = new THREE.Vector3(0, -gravity, 0); // Power charging variables let isCharging = false; let currentPower = 0; const maxPower = 10; const powerIncreaseRate = 20; // units per second (4x faster) // Launch controls window.addEventListener("keydown", (event) => { if (event.code === "Space") { if (gameState === "aiming") { event.preventDefault(); isCharging = true; } else if (gameState === "ended") { event.preventDefault(); resetGame(); } } else if (event.key === "ArrowDown") { if (gameState === "flying") { event.preventDefault(); isBoosting = true; } } else if (event.key === "ArrowLeft") { leftPressed = true; } else if (event.key === "ArrowRight") { rightPressed = true; } }); window.addEventListener("keyup", (event) => { if (event.code === "Space") { if (gameState === "aiming") { event.preventDefault(); isCharging = false; launchAirplane(); } else if (gameState === "ended") { event.preventDefault(); resetGame(); } } else if (event.key === "ArrowDown") { if (gameState === "flying") { event.preventDefault(); isBoosting = false; } } else if (event.key === "Escape") { // Restart the game instantly when Escape key is pressed event.preventDefault(); resetGame(); } }); window.addEventListener( "touchstart", (event) => { if (gameState === "aiming") { event.preventDefault(); isCharging = true; } else if (gameState === "flying") { event.preventDefault(); // For touch screens, we'll still allow touch to boost since there's no down arrow isBoosting = true; } }, { passive: false } ); window.addEventListener( "touchend", (event) => { if (gameState === "aiming") { event.preventDefault(); isCharging = false; launchAirplane(); } else if (gameState === "flying") { event.preventDefault(); isBoosting = false; } else if (gameState === "ended") { event.preventDefault(); resetGame(); } }, { passive: false } ); function launchAirplane() { const pitchAngle = Math.PI / 4; // 45 degrees const initialVelocity = new THREE.Vector3( 0, Math.sin(pitchAngle) * currentPower, Math.cos(pitchAngle) * currentPower ); velocity.copy(initialVelocity); gameState = "flying"; currentPower = 0; } // Steering Controls let leftPressed = false; let rightPressed = false; let upPressed = false; let downPressed = false; const steeringForce = 5; const diveForce = 8; // Force applied when diving let currentTilt = 0; let currentPitch = 0; // Track pitch angle // Boost variables const maxBoostPower = 100; let boostPower = maxBoostPower; const boostConsumptionRate = 20; // units per second const boostForce = 6.0; // increased from 4.5 to 6.0 for more powerful boost let isBoosting = false; window.addEventListener("keydown", (event) => { if (event.key === "ArrowLeft") leftPressed = true; else if (event.key === "ArrowRight") rightPressed = true; else if (event.key === "ArrowUp") upPressed = true; else if (event.key === "ArrowDown") downPressed = true; }); window.addEventListener("keyup", (event) => { if (event.key === "ArrowLeft") leftPressed = false; else if (event.key === "ArrowRight") rightPressed = false; else if (event.key === "ArrowUp") upPressed = false; else if (event.key === "ArrowDown") downPressed = false; }); // Background music setup const backgroundMusic = document.getElementById('backgroundMusic'); backgroundMusic.volume = 0.5; // Set volume to 50% // Function to start background music function startBackgroundMusic() { backgroundMusic.play().catch(error => { console.log("Audio playback failed:", error); }); } // Try to start music on page load document.addEventListener('DOMContentLoaded', () => { // Modern browsers require user interaction before playing audio document.addEventListener('click', startBackgroundMusic, { once: true }); document.addEventListener('keydown', startBackgroundMusic, { once: true }); document.addEventListener('touchstart', startBackgroundMusic, { once: true }); }); // Animation Loop const clock = new THREE.Clock(); function animate() { requestAnimationFrame(animate); const delta = clock.getDelta(); // Update UI elements if (gameState === "aiming") { // Always show the power gauge in aiming state document.getElementById("powerGauge").style.display = "block"; if (isCharging) { currentPower += powerIncreaseRate * delta; if (currentPower > maxPower) currentPower = maxPower; const powerPercentage = (currentPower / maxPower) * 100; document.getElementById("powerBar").style.width = powerPercentage + "%"; } else { document.getElementById("powerBar").style.width = "0%"; } document.getElementById("boostGauge").style.display = "none"; } else if (gameState === "flying") { document.getElementById("powerGauge").style.display = "none"; document.getElementById("boostGauge").style.display = "block"; const boostPercentage = (boostPower / maxBoostPower) * 100; document.getElementById("boostBar").style.width = boostPercentage + "%"; } else if (gameState === "ended") { document.getElementById("powerGauge").style.display = "none"; document.getElementById("boostGauge").style.display = "none"; } if (gameState === "flying") { velocity.add(acceleration.clone().multiplyScalar(delta)); if (upPressed) { // Dive downward when up arrow is pressed velocity.y -= diveForce * delta; // Tilt the airplane's nose down when diving currentPitch = THREE.MathUtils.lerp(currentPitch, 0.3, 0.1); // Gradually tilt nose down } else if (isBoosting && boostPower > 0) { velocity.y += boostForce * delta; boostPower -= boostConsumptionRate * delta; if (boostPower < 0) boostPower = 0; // Tilt the airplane's nose up slightly when boosting currentPitch = THREE.MathUtils.lerp(currentPitch, -0.2, 0.1); // Gradually tilt nose up } else { // Return to normal orientation when not boosting or diving currentPitch = THREE.MathUtils.lerp(currentPitch, 0, 0.1); // Gradually return to neutral } // Apply the current pitch to the airplane airplane.rotation.x = -Math.PI / 2 + currentPitch; // Create a modified velocity vector with doubled forward (z) speed const modifiedVelocity = velocity.clone(); modifiedVelocity.z *= 2; // Double the forward speed airplane.position.add(modifiedVelocity.multiplyScalar(delta)); // Update the trail with the current airplane position updateTrail(airplane.position); if (leftPressed) velocity.x += steeringForce * delta; // Inverted: left key moves right if (rightPressed) velocity.x -= steeringForce * delta; // Inverted: right key moves left let targetTilt = 0; if (leftPressed) targetTilt = -Math.PI / 6; // Inverted: negative tilt for left arrow else if (rightPressed) targetTilt = Math.PI / 6; // Inverted: positive tilt for right arrow currentTilt = THREE.MathUtils.lerp(currentTilt, targetTilt, 0.1); airplane.rotation.z = currentTilt; const collided = checkCollision(airplane, buildingBoxes); // Check for globe collection globes = globes.filter((globe) => { const distance = airplane.position.distanceTo(globe.position); if (distance < 3.0) { // Increased from 2.0 to 3.0 for larger hitbox (1.5x) // Collected a globe - recharge boost boostPower = maxBoostPower; scene.remove(globe); // Play ping sound const pingSound = document.getElementById('pingSound'); pingSound.volume = 0.25; // Set volume to 25% (half of the default 0.5) pingSound.currentTime = 0; // Reset sound to beginning pingSound.play().catch(error => { console.log("Ping sound playback failed:", error); }); // Add visual feedback console.log("Globe collected! Boost recharged."); return false; } // Animate globe pulsing globe.userData.pulsePhase += delta * 2; const scale = 1 + 0.1 * Math.sin(globe.userData.pulsePhase); globe.scale.set(scale, scale, scale); // Animate glow layers const glowScale = 1 + 0.2 * Math.sin(globe.userData.pulsePhase + Math.PI/4); const glowOpacity = 0.3 + 0.1 * Math.sin(globe.userData.pulsePhase); if (globe.userData.glowLayers) { globe.userData.glowLayers[0].scale.set(glowScale, glowScale, glowScale); globe.userData.glowLayers[0].material.opacity = glowOpacity; const glow2Scale = 1 + 0.15 * Math.sin(globe.userData.pulsePhase + Math.PI/2); const glow2Opacity = 0.15 + 0.05 * Math.sin(globe.userData.pulsePhase + Math.PI/3); globe.userData.glowLayers[1].scale.set(glow2Scale, glow2Scale, glow2Scale); globe.userData.glowLayers[1].material.opacity = glow2Opacity; } return true; }); if (collided) { gameState = "ended"; const finalHorizontalDistance = Math.sqrt( airplane.position.x ** 2 + airplane.position.z ** 2 ); document.getElementById( "finalScore" ).innerText = `Final Distance: ${finalHorizontalDistance.toFixed(2)}`; document.getElementById("finalScore").style.display = "block"; document.getElementById("restart").style.display = "block"; document.getElementById("spaceToRestart").style.display = "block"; } } // Camera follows airplane correctly camera.position.set( airplane.position.x, airplane.position.y + 5, airplane.position.z - 10 ); camera.lookAt(airplane.position); updateDistanceDisplay(airplane, document.getElementById("distance")); // Calculate distance and rotate background const totalCityDistance = 700; // Adjust if needed const horizontalDistance = Math.sqrt( airplane.position.x ** 2 + airplane.position.z ** 2 ); const ratio = Math.min(1, horizontalDistance / totalCityDistance); texture.rotation = ratio * (Math.PI * 0.5); // Up to 90° rotation texture.needsUpdate = true; renderer.render(scene, camera); } animate(); // Reset Game Function function resetGame() { airplane.position.set(0, 10.1, 0); velocity.set(0, 0, 0); currentTilt = 0; currentPitch = 0; airplane.rotation.z = 0; airplane.rotation.x = -Math.PI / 2; // Reset pitch to default gameState = "aiming"; currentPower = 0; boostPower = maxBoostPower; // Make sure music is playing if (backgroundMusic.paused) { backgroundMusic.play().catch(error => { console.log("Audio playback failed:", error); }); } // Clear the trail when resetting the game for (let i = 0; i < trailLength * 3; i++) { trailPositions[i] = 0; } trailGeometry.attributes.position.needsUpdate = true; document.getElementById("powerGauge").style.display = "block"; // Show power gauge on reset document.getElementById("finalScore").style.display = "none"; document.getElementById("restart").style.display = "none"; document.getElementById("spaceToRestart").style.display = "none"; // Reset globes initGlobes(); } document.getElementById("restart").addEventListener("click", resetGame); // Handle Window Resize window.addEventListener("resize", () => { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); });