| |
| |
| |
| |
| function checkCollision(airplane, buildingBoxes) { |
| |
| if (airplane.position.y <= 0) { |
| return true; |
| } |
| |
| 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; |
| } |
|
|
| |
| |
| |
| function updateCamera(camera, airplane) { |
| camera.position.set( |
| airplane.position.x, |
| airplane.position.y + 5, |
| airplane.position.z - 10 |
| ); |
| camera.lookAt(airplane.position); |
| } |
|
|
| |
| |
| |
| function updateDistanceDisplay(airplane, distanceElement) { |
| const horizontalDistance = Math.sqrt( |
| airplane.position.x ** 2 + airplane.position.z ** 2 |
| ); |
| distanceElement.innerText = `Distance: ${horizontalDistance.toFixed(2)}`; |
| } |
|
|
| |
| const scene = new THREE.Scene(); |
| |
| 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'); |
| gradient.addColorStop(0.4, '#4169E1'); |
| gradient.addColorStop(1, '#000000'); |
| context.fillStyle = gradient; |
| context.fillRect(0, 0, 1, 256); |
| const texture = new THREE.CanvasTexture(canvas); |
| |
| texture.center.set(0.5, 0.5); |
| scene.background = texture; |
|
|
| |
| 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; |
| |
| const radius = 500; |
| const theta = Math.random() * Math.PI * 2; |
| const phi = Math.random() * Math.PI * 0.65; |
|
|
| starPositions[i3] = radius * Math.sin(phi) * Math.cos(theta); |
| starPositions[i3 + 1] = radius * Math.cos(phi) + 100; |
| 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(); |
|
|
| |
| const camera = new THREE.PerspectiveCamera( |
| 75, |
| window.innerWidth / window.innerHeight, |
| 0.1, |
| 1000 |
| ); |
|
|
| |
| 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); |
|
|
| |
| const light = new THREE.DirectionalLight(0xfff0dd, 1.5); |
| light.position.set(-100, 200, -50); |
| 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); |
| scene.add(ambientLight); |
| scene.add(light); |
|
|
| |
| function createPaperAirplane() { |
| |
| const airplaneGroup = new THREE.Group(); |
|
|
| |
| const bodyShape = new THREE.Shape(); |
| bodyShape.moveTo(0, 0); |
| bodyShape.lineTo(-0.2, 0.5); |
| bodyShape.lineTo(-0.4, 1.0); |
| bodyShape.lineTo(0.4, 1.0); |
| bodyShape.lineTo(0.2, 0.5); |
| bodyShape.lineTo(0, 0); |
|
|
| 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; |
|
|
| |
| const leftWingShape = new THREE.Shape(); |
| leftWingShape.moveTo(0, 0.2); |
| leftWingShape.lineTo(-0.3, 0.8); |
| leftWingShape.lineTo(-0.7, 0.5); |
| leftWingShape.lineTo(0, 0.2); |
|
|
| 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; |
| leftWing.rotation.x = 0.2; |
|
|
| |
| const rightWingShape = new THREE.Shape(); |
| rightWingShape.moveTo(0, 0.2); |
| rightWingShape.lineTo(0.3, 0.8); |
| rightWingShape.lineTo(0.7, 0.5); |
| rightWingShape.lineTo(0, 0.2); |
|
|
| 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; |
| rightWing.rotation.x = 0.2; |
|
|
| |
| const foldLineGeometry = new THREE.BufferGeometry(); |
| const foldLinePoints = [ |
| new THREE.Vector3(0, 0.03, 0), |
| new THREE.Vector3(0, 0.03, 1.0), |
| ]; |
| foldLineGeometry.setFromPoints(foldLinePoints); |
| const foldLineMaterial = new THREE.LineBasicMaterial({ color: 0xdddddd }); |
| const foldLine = new THREE.Line(foldLineGeometry, foldLineMaterial); |
| foldLine.position.z = 0.026; |
|
|
| |
| airplaneGroup.add(body); |
| airplaneGroup.add(leftWing); |
| airplaneGroup.add(rightWing); |
| airplaneGroup.add(foldLine); |
|
|
| |
| airplaneGroup.rotation.order = "ZXY"; |
| airplaneGroup.rotation.x = -Math.PI / 2; |
|
|
| return airplaneGroup; |
| } |
|
|
| const airplane = createPaperAirplane(); |
| airplane.position.set(0, 10.1, 0); |
| scene.add(airplane); |
|
|
| |
| const buildingGeometry = new THREE.BoxGeometry(1, 1, 1); |
|
|
| |
| const buildingColors = [ |
| 0x8c8c8c, |
| 0x9c5b3c, |
| 0x5a7d9e, |
| 0xbcbcbc, |
| 0x4a4a4a, |
| ]; |
| const buildings = []; |
| const buildingBoxes = []; |
|
|
| |
| const floorHeight = 1; |
| const windowWidth = 0.2; |
| const windowHeight = 0.3; |
| const horizontalSpacingMin = 0.1; |
| const epsilon = 0.01; |
| |
| const darkWindowMaterial = new THREE.MeshLambertMaterial({ |
| color: 0x0a1a2a, |
| transparent: true, |
| opacity: 0.5, |
| }); |
| const litWindowMaterial = new THREE.MeshLambertMaterial({ |
| color: 0xffeb3b, |
| transparent: true, |
| opacity: 0.8, |
| emissive: 0xffeb3b, |
| emissiveIntensity: 0.5, |
| }); |
|
|
| 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), |
| }); |
|
|
| |
| 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, |
| }, |
| { |
| normal: new THREE.Vector3(0, 0, -1), |
| offset: -width / 2 - epsilon, |
| rotationY: Math.PI, |
| }, |
| { |
| normal: new THREE.Vector3(-1, 0, 0), |
| offset: -width / 2 - epsilon, |
| rotationY: -Math.PI / 2, |
| }, |
| { |
| normal: new THREE.Vector3(1, 0, 0), |
| offset: width / 2 + epsilon, |
| rotationY: Math.PI / 2, |
| }, |
| ]; |
|
|
| |
| const windowCount = numFloors * n_horizontal * faces.length; |
| const positions = new Float32Array(windowCount * 12); |
| const indices = new Uint16Array(windowCount * 6); |
|
|
| 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) { |
| |
| z_local = |
| z - |
| width / 2 + |
| spacing_horizontal + |
| m * (windowWidth + spacing_horizontal) + |
| windowWidth / 2; |
| x_local = x + offset; |
| } else { |
| |
| 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); |
|
|
| |
| 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), |
| ]; |
|
|
| |
| vertices.forEach((v) => { |
| v.applyMatrix4(rotationMatrix); |
| v.add(windowPos); |
| }); |
|
|
| |
| 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; |
|
|
| |
| 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; |
| } |
| } |
| } |
|
|
| |
| const mergedWindowGeometry = new THREE.BufferGeometry(); |
| mergedWindowGeometry.setAttribute( |
| "position", |
| new THREE.BufferAttribute(positions, 3) |
| ); |
| mergedWindowGeometry.setIndex(new THREE.BufferAttribute(indices, 1)); |
| |
| for (let i = 0; i < positions.length; i += 12) { |
| if (Math.random() < 0.2) { |
| |
| allLitWindowPositions.push(...positions.slice(i, i + 12)); |
| } else { |
| allDarkWindowPositions.push(...positions.slice(i, i + 12)); |
| } |
| } |
| } |
| } |
| return building; |
| } |
|
|
| function createGlobe(x, y, z) { |
| |
| 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); |
| |
| |
| 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); |
| |
| |
| 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); |
| |
| |
| globe.userData.pulsePhase = Math.random() * Math.PI * 2; |
| globe.userData.glowLayers = [glow, glow2]; |
| |
| scene.add(globe); |
| return globe; |
| } |
|
|
| |
| |
| let allDarkWindowPositions = []; |
| let allLitWindowPositions = []; |
|
|
| for (let z = 20; z < 700; z += 5) { |
| |
| for (let x = -60; x <= 60; x += 5) { |
| let placeBuilding = Math.random() > 0.3; |
| let height, width; |
|
|
| |
| const baseHeight = Math.random() * 15 + 5; |
|
|
| |
| if (Math.random() < 0.02) { |
| height = baseHeight * 1.3; |
| width = Math.random() * 4 + 3; |
| } 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); |
| } |
| } |
| } |
|
|
| |
| const startingBuilding = createBuilding(0, 0, 10, 2); |
| startingBuilding.material.color.set(0x0000ff); |
|
|
| |
| let globes = []; |
|
|
| function initGlobes() { |
| |
| globes.forEach((globe) => scene.remove(globe)); |
| globes = []; |
|
|
| |
| const globe_z_positions = []; |
| for (let z = 40; z <= 680; z += 20) { |
| globe_z_positions.push(z); |
| } |
|
|
| |
| 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); |
| } |
| }); |
| } |
|
|
| |
| initGlobes(); |
|
|
| |
| 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); |
|
|
| |
| 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); |
|
|
| |
| 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'); |
|
|
| |
| groundContext.fillStyle = '#111111'; |
| groundContext.fillRect(0, 0, 1024, 1024); |
|
|
| |
| groundContext.strokeStyle = '#333333'; |
| groundContext.lineWidth = 1; |
|
|
| |
| 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(); |
|
|
| |
| groundContext.strokeStyle = '#222222'; |
| groundContext.lineWidth = 0.5; |
| const minorGridSize = 16; |
| groundContext.beginPath(); |
| for (let i = 0; i <= 1024; i += minorGridSize) { |
| if (i % majorGridSize !== 0) { |
| groundContext.moveTo(i, 0); |
| groundContext.lineTo(i, 1024); |
| groundContext.moveTo(0, i); |
| groundContext.lineTo(1024, i); |
| } |
| } |
| groundContext.stroke(); |
|
|
| |
| 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); |
|
|
| |
| const trailLength = 50; |
| const trailPositions = new Float32Array(trailLength * 3); |
| const trailGeometry = new THREE.BufferGeometry(); |
| trailGeometry.setAttribute( |
| "position", |
| new THREE.BufferAttribute(trailPositions, 3) |
| ); |
|
|
| |
| const trailMaterial = new THREE.LineBasicMaterial({ |
| color: 0x88ccff, |
| transparent: true, |
| opacity: 0.7, |
| vertexColors: true, |
| linewidth: 1, |
| }); |
|
|
| |
| const trailColors = new Float32Array(trailLength * 3); |
| for (let i = 0; i < trailLength; i++) { |
| |
| const intensity = 1 - i / trailLength; |
| trailColors[i * 3] = 0.4 * intensity; |
| trailColors[i * 3 + 1] = 0.7 * intensity; |
| trailColors[i * 3 + 2] = 1.0 * intensity; |
| } |
| trailGeometry.setAttribute("color", new THREE.BufferAttribute(trailColors, 3)); |
|
|
| const trail = new THREE.Line(trailGeometry, trailMaterial); |
| scene.add(trail); |
|
|
| |
| function updateTrail(newPosition) { |
| |
| 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]; |
| } |
|
|
| |
| trailPositions[0] = newPosition.x; |
| trailPositions[1] = newPosition.y; |
| trailPositions[2] = newPosition.z; |
|
|
| |
| trailGeometry.attributes.position.needsUpdate = true; |
| } |
|
|
| |
| let gameState = "aiming"; |
| let velocity = new THREE.Vector3(0, 0, 0); |
| const gravity = 2.5; |
| const acceleration = new THREE.Vector3(0, -gravity, 0); |
|
|
| |
| let isCharging = false; |
| let currentPower = 0; |
| const maxPower = 10; |
| const powerIncreaseRate = 20; |
|
|
| |
| 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") { |
| |
| event.preventDefault(); |
| resetGame(); |
| } |
| }); |
|
|
| window.addEventListener( |
| "touchstart", |
| (event) => { |
| if (gameState === "aiming") { |
| event.preventDefault(); |
| isCharging = true; |
| } else if (gameState === "flying") { |
| event.preventDefault(); |
| |
| 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; |
| const initialVelocity = new THREE.Vector3( |
| 0, |
| Math.sin(pitchAngle) * currentPower, |
| Math.cos(pitchAngle) * currentPower |
| ); |
| velocity.copy(initialVelocity); |
| gameState = "flying"; |
| currentPower = 0; |
| } |
|
|
| |
| let leftPressed = false; |
| let rightPressed = false; |
| let upPressed = false; |
| let downPressed = false; |
| const steeringForce = 5; |
| const diveForce = 8; |
| let currentTilt = 0; |
| let currentPitch = 0; |
|
|
| |
| const maxBoostPower = 100; |
| let boostPower = maxBoostPower; |
| const boostConsumptionRate = 20; |
| const boostForce = 6.0; |
| 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; |
| }); |
|
|
| |
| const backgroundMusic = document.getElementById('backgroundMusic'); |
| backgroundMusic.volume = 0.5; |
|
|
| |
| function startBackgroundMusic() { |
| backgroundMusic.play().catch(error => { |
| console.log("Audio playback failed:", error); |
| }); |
| } |
|
|
| |
| document.addEventListener('DOMContentLoaded', () => { |
| |
| document.addEventListener('click', startBackgroundMusic, { once: true }); |
| document.addEventListener('keydown', startBackgroundMusic, { once: true }); |
| document.addEventListener('touchstart', startBackgroundMusic, { once: true }); |
| }); |
|
|
| |
| const clock = new THREE.Clock(); |
|
|
| function animate() { |
| requestAnimationFrame(animate); |
| const delta = clock.getDelta(); |
|
|
| |
| if (gameState === "aiming") { |
| |
| 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) { |
| |
| velocity.y -= diveForce * delta; |
| |
| |
| currentPitch = THREE.MathUtils.lerp(currentPitch, 0.3, 0.1); |
| } else if (isBoosting && boostPower > 0) { |
| velocity.y += boostForce * delta; |
| boostPower -= boostConsumptionRate * delta; |
| if (boostPower < 0) boostPower = 0; |
|
|
| |
| currentPitch = THREE.MathUtils.lerp(currentPitch, -0.2, 0.1); |
| } else { |
| |
| currentPitch = THREE.MathUtils.lerp(currentPitch, 0, 0.1); |
| } |
| |
| |
| airplane.rotation.x = -Math.PI / 2 + currentPitch; |
|
|
| |
| const modifiedVelocity = velocity.clone(); |
| modifiedVelocity.z *= 2; |
| airplane.position.add(modifiedVelocity.multiplyScalar(delta)); |
|
|
| |
| updateTrail(airplane.position); |
|
|
| if (leftPressed) velocity.x += steeringForce * delta; |
| if (rightPressed) velocity.x -= steeringForce * delta; |
|
|
| let targetTilt = 0; |
| if (leftPressed) targetTilt = -Math.PI / 6; |
| else if (rightPressed) targetTilt = Math.PI / 6; |
| currentTilt = THREE.MathUtils.lerp(currentTilt, targetTilt, 0.1); |
| airplane.rotation.z = currentTilt; |
|
|
| const collided = checkCollision(airplane, buildingBoxes); |
| |
| |
| globes = globes.filter((globe) => { |
| const distance = airplane.position.distanceTo(globe.position); |
| if (distance < 3.0) { |
| |
| boostPower = maxBoostPower; |
| scene.remove(globe); |
| |
| |
| const pingSound = document.getElementById('pingSound'); |
| pingSound.volume = 0.25; |
| pingSound.currentTime = 0; |
| pingSound.play().catch(error => { |
| console.log("Ping sound playback failed:", error); |
| }); |
| |
| |
| console.log("Globe collected! Boost recharged."); |
| return false; |
| } |
| |
| |
| globe.userData.pulsePhase += delta * 2; |
| const scale = 1 + 0.1 * Math.sin(globe.userData.pulsePhase); |
| globe.scale.set(scale, scale, scale); |
| |
| |
| 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.position.set( |
| airplane.position.x, |
| airplane.position.y + 5, |
| airplane.position.z - 10 |
| ); |
| camera.lookAt(airplane.position); |
|
|
| updateDistanceDisplay(airplane, document.getElementById("distance")); |
|
|
| |
| const totalCityDistance = 700; |
| 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); |
| texture.needsUpdate = true; |
|
|
| renderer.render(scene, camera); |
| } |
| animate(); |
|
|
| |
| 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; |
| gameState = "aiming"; |
| currentPower = 0; |
| boostPower = maxBoostPower; |
| |
| |
| if (backgroundMusic.paused) { |
| backgroundMusic.play().catch(error => { |
| console.log("Audio playback failed:", error); |
| }); |
| } |
|
|
| |
| for (let i = 0; i < trailLength * 3; i++) { |
| trailPositions[i] = 0; |
| } |
| trailGeometry.attributes.position.needsUpdate = true; |
|
|
| document.getElementById("powerGauge").style.display = "block"; |
| document.getElementById("finalScore").style.display = "none"; |
| document.getElementById("restart").style.display = "none"; |
| document.getElementById("spaceToRestart").style.display = "none"; |
| |
| |
| initGlobes(); |
| } |
|
|
| document.getElementById("restart").addEventListener("click", resetGame); |
|
|
| |
| window.addEventListener("resize", () => { |
| camera.aspect = window.innerWidth / window.innerHeight; |
| camera.updateProjectionMatrix(); |
| renderer.setSize(window.innerWidth, window.innerHeight); |
| }); |
|
|