import * as THREE from 'three'; import { FlightModel } from './flight-model.js'; import { Terrain } from './terrain.js'; import { Airport } from './airport.js'; import { Aircraft } from './aircraft.js'; import { HUD } from './hud.js'; import { UI } from './ui.js'; import { RetroEffect } from './retro.js'; import { Sound } from './sound.js'; const GW = 320; const GH = 200; class Game { constructor() { this.state = 'LOADING'; this.clock = new THREE.Clock(); this.cockpitView = false; this.fixedDelta = 1 / 60; this.accumulator = 0; this.initRenderer(); this.initScene(); this.initCamera(); this.initInput(); this.initComponents(); this.initUI(); // Start loading sequence this.startLoading(); // Begin render loop this.animate(); } initRenderer() { this.renderer = new THREE.WebGLRenderer({ canvas: document.getElementById('game-canvas'), antialias: false, powerPreference: 'low-power', }); this.renderer.setSize(GW, GH, false); this.renderer.setPixelRatio(1); } initScene() { this.scene = new THREE.Scene(); this.scene.background = new THREE.Color(0x4466AA); this.scene.fog = new THREE.Fog(0x4466AA, 600, 2000); // Hemisphere light (sky/ground) const hemi = new THREE.HemisphereLight(0x8888CC, 0x444422, 0.6); this.scene.add(hemi); // Directional light (sun) const sun = new THREE.DirectionalLight(0xFFFFDD, 1.2); sun.position.set(200, 120, 100); this.scene.add(sun); // Ambient this.scene.add(new THREE.AmbientLight(0x404050, 0.3)); // Low-poly clouds this.buildClouds(); } buildClouds() { const cloudMat = new THREE.MeshLambertMaterial({ color: 0xDDDDDD, flatShading: true, transparent: true, opacity: 0.85, }); const cloudPositions = [ { x: 300, z: 200, y: 180 }, { x: -400, z: -100, y: 220 }, { x: 500, z: -300, y: 160 }, { x: -200, z: 400, y: 200 }, { x: 100, z: -500, y: 190 }, { x: -600, z: 300, y: 170 }, { x: 400, z: 500, y: 210 }, { x: -300, z: -400, y: 185 }, { x: 700, z: 100, y: 195 }, { x: -500, z: -200, y: 175 }, ]; cloudPositions.forEach(({ x, y, z }) => { const cloud = new THREE.Group(); const numPuffs = 3 + Math.floor(Math.random() * 4); for (let i = 0; i < numPuffs; i++) { const size = 10 + Math.random() * 20; const puff = new THREE.Mesh( new THREE.SphereGeometry(size, 5, 4), cloudMat ); puff.position.set( (Math.random() - 0.5) * 30, (Math.random() - 0.5) * 5, (Math.random() - 0.5) * 20 ); puff.scale.y = 0.4; cloud.add(puff); } cloud.position.set(x, y, z); this.scene.add(cloud); }); } initCamera() { this.camera = new THREE.PerspectiveCamera(75, GW / GH, 0.5, 3000); this.camera.position.set(0, 15, 20); } initInput() { this.keys = {}; window.addEventListener('keydown', (e) => { this.keys[e.code] = true; this.handleKeydown(e.code); }); window.addEventListener('keyup', (e) => { this.keys[e.code] = false; }); } initComponents() { this.terrain = new Terrain(2000, 100); this.airport = new Airport(); this.aircraft = new Aircraft(); this.flightModel = new FlightModel(); this.hud = new HUD(); this.retro = new RetroEffect(this.renderer, GW, GH); this.sound = new Sound(); } initUI() { this.ui = new UI(); this.ui.onNewFlight = () => this.startFlight(); this.ui.onResume = () => this.resumeFlight(); this.ui.onMainMenu = () => this.returnToMenu(); this.ui.onQuit = () => this.quitGame(); } handleKeydown(code) { // Init audio on first keypress this.sound.init(); if (this.ui.menuActive) { if (this.ui.handleMenuInput(code)) return; } if (this.ui.controlsActive) { if (this.ui.handleControlsInput()) return; } if (this.ui.pauseMenu.style.display !== 'none') { if (this.ui.handlePauseInput(code)) return; } if (this.state === 'FLYING' && code === 'Escape') { this.pauseFlight(); this.sound.beep(400, 60); } if (this.state === 'FLYING' && code === 'KeyC') { this.cockpitView = !this.cockpitView; } } getFlightInput() { return { throttleUp: !!this.keys['KeyW'], throttleDown: !!this.keys['KeyS'], pitch: this.keys['ArrowUp'] ? 1 : this.keys['ArrowDown'] ? -1 : 0, roll: this.keys['ArrowLeft'] ? 1 : this.keys['ArrowRight'] ? -1 : 0, yaw: this.keys['KeyQ'] ? -1 : this.keys['KeyE'] ? 1 : 0, }; } async startLoading() { await this.ui.showLoading(async (step) => { if (step === 0) { // Generate terrain const terrainMesh = this.terrain.createMesh(); this.scene.add(terrainMesh); const water = this.terrain.createWater(); this.scene.add(water); } else if (step === 1) { // Build airport this.airport.build(); this.scene.add(this.airport.getGroup()); } else if (step === 2) { // Place aircraft const aircraftGroup = this.aircraft.getGroup(); // Position on runway const terrainH = this.terrain.getHeightAt(0, 50); this.flightModel.position.set(0, terrainH + 2, 50); this.flightModel.orientation.y = Math.PI; // Face down runway (south) aircraftGroup.position.copy(this.flightModel.position); aircraftGroup.rotation.copy(this.flightModel.orientation); this.scene.add(aircraftGroup); } }); this.state = 'MENU'; } startFlight() { this.ui.hideMainMenu(); this.state = 'FLYING'; this.hud.show(); this.sound.takeoffSound(); } pauseFlight() { this.state = 'PAUSED'; this.ui.showPauseMenu(); } resumeFlight() { this.state = 'FLYING'; this.ui.hidePauseMenu(); } returnToMenu() { this.state = 'MENU'; this.ui.hidePauseMenu(); this.ui.showMainMenu(); this.hud.hide(); } quitGame() { this.state = 'QUIT'; this.ui.showQuitScreen(); this.hud.hide(); } updateCamera() { const acPos = this.flightModel.position; const acRot = this.flightModel.orientation; if (this.cockpitView) { const forward = new THREE.Vector3(0, 0, -1).applyEuler(acRot); const up = new THREE.Vector3(0, 1, 0).applyEuler(acRot); this.camera.position.copy(acPos) .add(forward.clone().multiplyScalar(3)) .add(up.clone().multiplyScalar(1.5)); this.camera.lookAt(acPos.clone().add(forward.clone().multiplyScalar(20))); } else { // Chase camera const forward = new THREE.Vector3(0, 0, -1).applyEuler(acRot); const up = new THREE.Vector3(0, 1, 0).applyEuler(acRot); const desiredPos = acPos.clone() .add(forward.multiplyScalar(-20)) .add(up.multiplyScalar(10)); this.camera.position.lerp(desiredPos, 0.06); const lookTarget = acPos.clone().add(up.multiplyScalar(2)); this.camera.lookAt(lookTarget); } } animate() { requestAnimationFrame(() => this.animate()); const deltaRaw = this.clock.getDelta(); const delta = Math.min(deltaRaw, 0.1); const elapsed = this.clock.getElapsedTime(); if (this.state === 'FLYING') { this.accumulator += delta; while (this.accumulator >= this.fixedDelta) { const input = this.getFlightInput(); this.flightModel.update( this.fixedDelta, input, (x, z) => this.terrain.getHeightAt(x, z) ); // Sync aircraft mesh const acGroup = this.aircraft.getGroup(); acGroup.position.copy(this.flightModel.position); acGroup.rotation.copy(this.flightModel.orientation); // Propeller this.aircraft.updatePropeller(this.flightModel.thrust, this.fixedDelta); // Landing gear this.aircraft.retractGear(this.flightModel.onGround); this.accumulator -= this.fixedDelta; } this.updateCamera(); this.hud.draw(this.flightModel); this.sound.update(this.flightModel.thrust); } // Update retro effect this.retro.update(elapsed); // Render with retro post-processing this.retro.render(this.scene, this.camera); } } new Game();