From 76bad46d09a954e0433014aa73d9592ead777cd4 Mon Sep 17 00:00:00 2001 From: mac-container-dev Date: Tue, 5 May 2026 10:43:22 +0000 Subject: [PATCH] Initial commit: Retro 90s flight simulator MVP Loading screen, main menu, 3D flight sim with CRT post-processing, procedural terrain, airport with buildings, low-poly aircraft, flight physics, HUD instruments, and sound. Co-Authored-By: Claude Opus 4.7 --- css/style.css | 186 +++++++++ index.html | 87 ++++ js/aircraft.js | 219 ++++++++++ js/airport.js | 223 ++++++++++ js/flight-model.js | 163 ++++++++ js/hud.js | 274 +++++++++++++ js/main.js | 306 ++++++++++++++ js/retro.js | 108 +++++ js/sound.js | 77 ++++ js/terrain.js | 168 ++++++++ js/ui.js | 142 +++++++ package-lock.json | 996 +++++++++++++++++++++++++++++++++++++++++++++ package.json | 16 + 13 files changed, 2965 insertions(+) create mode 100644 css/style.css create mode 100644 index.html create mode 100644 js/aircraft.js create mode 100644 js/airport.js create mode 100644 js/flight-model.js create mode 100644 js/hud.js create mode 100644 js/main.js create mode 100644 js/retro.js create mode 100644 js/sound.js create mode 100644 js/terrain.js create mode 100644 js/ui.js create mode 100644 package-lock.json create mode 100644 package.json diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..20ad475 --- /dev/null +++ b/css/style.css @@ -0,0 +1,186 @@ +* { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +body { + background: #000; + overflow: hidden; + font-family: 'Press Start 2P', monospace; + color: #fff; + width: 100vw; + height: 100vh; +} + +#game-canvas { + width: 100vw; + height: 100vh; + image-rendering: pixelated; + image-rendering: crisp-edges; + display: block; +} + +#crt-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + pointer-events: none; + z-index: 100; + background: repeating-linear-gradient( + 0deg, + rgba(0, 0, 0, 0.12) 0px, + rgba(0, 0, 0, 0.12) 1px, + transparent 1px, + transparent 2px + ); +} + +#loading-screen { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: #0000AA; + display: flex; + align-items: center; + justify-content: center; + z-index: 50; +} + +.loading-box { + text-align: center; +} + +.loading-box h1 { + font-size: 24px; + color: #FFFF55; + margin-bottom: 8px; + text-shadow: 2px 2px #AA0000; +} + +.loading-box h2 { + font-size: 10px; + color: #AAAAAA; + margin-bottom: 30px; +} + +.progress-container { + width: 240px; + height: 16px; + border: 2px solid #FFFFFF; + margin: 0 auto 20px; + background: #000055; +} + +.progress-bar { + height: 100%; + width: 0%; + background: #00AAAA; + transition: width 0.3s; +} + +#loading-message { + font-size: 10px; + color: #55FF55; + margin-bottom: 20px; +} + +.loading-plane pre { + font-size: 8px; + color: #FFFFFF; + line-height: 1.2; +} + +.menu-box { + text-align: center; + padding: 30px 40px; + background: rgba(0, 0, 0, 0.85); + border: 2px solid #AAAAAA; +} + +.menu-box h1 { + font-size: 20px; + color: #FFFF55; + margin-bottom: 20px; + text-shadow: 2px 2px #AA0000; +} + +.menu-box h2 { + font-size: 14px; + color: #FFFF55; + margin-bottom: 20px; +} + +.menu-item { + font-size: 12px; + padding: 10px 20px; + margin: 6px 0; + cursor: pointer; + color: #AAAAAA; +} + +.menu-item.selected { + color: #FFFFFF; + background: #0000AA; +} + +.copyright { + font-size: 8px; + color: #555555; + margin-top: 30px; +} + +.prompt { + font-size: 8px; + color: #55FF55; + margin-top: 20px; + animation: blink 1s infinite; +} + +@keyframes blink { + 0%, 49% { opacity: 1; } + 50%, 100% { opacity: 0; } +} + +#controls-screen, +#pause-menu, +#main-menu, +#quit-screen { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 40; +} + +.controls-list p { + font-size: 9px; + color: #AAAAAA; + margin: 8px 0; + text-align: left; +} + +#hud-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 30; + pointer-events: none; +} + +#hud-canvas { + width: 100vw; + height: 100vh; + image-rendering: pixelated; + image-rendering: crisp-edges; +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..273f7d2 --- /dev/null +++ b/index.html @@ -0,0 +1,87 @@ + + + + + + Wings of the 90s - Flight Simulator + + + + + +
+ +
+
+

WINGS OF THE 90S

+

Flight Simulator v1.0

+
+
+
+

INITIALIZING...

+
+
+    __
+  /   \
+ | . . |
+  \___/
+        / \
+       /   \
+      ~~~~~
+        
+
+
+
+ + + + + + + + + + + + + + diff --git a/js/aircraft.js b/js/aircraft.js new file mode 100644 index 0000000..bd5e7ef --- /dev/null +++ b/js/aircraft.js @@ -0,0 +1,219 @@ +import * as THREE from 'three'; + +function flatMat(color, opts = {}) { + return new THREE.MeshLambertMaterial({ color, flatShading: true, ...opts }); +} + +export class Aircraft { + constructor() { + this.group = new THREE.Group(); + this.propeller = null; + this.landingGear = []; + this.gearExtended = true; + this.build(); + } + + build() { + const bodyMat = flatMat(0x6688AA); + const whiteMat = flatMat(0xFFFFFF); + const redMat = flatMat(0xCC0000); + const cockpitMat = flatMat(0x333333, { transparent: true, opacity: 0.6 }); + const darkMat = flatMat(0x333333); + const metalMat = flatMat(0x888888); + + // Fuselage + const fuselage = new THREE.Mesh( + new THREE.CylinderGeometry(0.6, 0.45, 9, 6), + bodyMat + ); + fuselage.rotation.x = Math.PI / 2; + this.group.add(fuselage); + + // Nose cone + const nose = new THREE.Mesh( + new THREE.SphereGeometry(0.4, 6, 4, 0, Math.PI * 2, 0, Math.PI / 2), + bodyMat + ); + nose.rotation.x = -Math.PI / 2; + nose.position.z = -4.5; + this.group.add(nose); + + // Tail cone + const tailCone = new THREE.Mesh( + new THREE.CylinderGeometry(0.4, 0.15, 2.5, 6), + bodyMat + ); + tailCone.rotation.x = Math.PI / 2; + tailCone.position.z = 5.5; + this.group.add(tailCone); + + // Cockpit canopy + const canopy = new THREE.Mesh( + new THREE.SphereGeometry(0.55, 6, 4, 0, Math.PI * 2, 0, Math.PI / 2), + cockpitMat + ); + canopy.position.set(0, 0.4, -1); + this.group.add(canopy); + + // Wings + const wings = new THREE.Mesh( + new THREE.BoxGeometry(14, 0.2, 2), + bodyMat + ); + wings.position.z = -0.5; + this.group.add(wings); + + // Wingtips (red) + const leftTip = new THREE.Mesh(new THREE.BoxGeometry(0.8, 0.25, 1.6), redMat); + leftTip.position.set(-6.9, 0, -0.5); + this.group.add(leftTip); + + const rightTip = leftTip.clone(); + rightTip.position.x = 6.9; + this.group.add(rightTip); + + // Ailerons (white strips on wing trailing edge) + const leftAileron = new THREE.Mesh(new THREE.BoxGeometry(2, 0.15, 0.4), whiteMat); + leftAileron.position.set(-5, -0.15, 0.3); + this.group.add(leftAileron); + + const rightAileron = leftAileron.clone(); + rightAileron.position.x = 5; + this.group.add(rightAileron); + + // Vertical stabilizer + const vStab = new THREE.Mesh( + new THREE.BoxGeometry(0.15, 2.5, 1.5), + bodyMat + ); + vStab.position.set(0, 1.2, 5.5); + this.group.add(vStab); + + // Rudder (white) + const rudder = new THREE.Mesh( + new THREE.BoxGeometry(0.12, 1.8, 0.5), + whiteMat + ); + rudder.position.set(0, 1.2, 6.3); + this.group.add(rudder); + + // Red tail band + const tailBand = new THREE.Mesh( + new THREE.BoxGeometry(0.12, 0.6, 1.2), + redMat + ); + tailBand.position.set(0, 1.8, 5.5); + this.group.add(tailBand); + + // Horizontal stabilizer + const hStab = new THREE.Mesh( + new THREE.BoxGeometry(5, 0.15, 1.2), + bodyMat + ); + hStab.position.set(0, 0.1, 5.5); + this.group.add(hStab); + + // Elevators (white) + const leftElev = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.12, 0.4), whiteMat); + leftElev.position.set(-2.5, -0.05, 6); + this.group.add(leftElev); + + const rightElev = leftElev.clone(); + rightElev.position.x = 2.5; + this.group.add(rightElev); + + // Engine cowling + const cowling = new THREE.Mesh( + new THREE.CylinderGeometry(0.45, 0.55, 1.5, 6), + darkMat + ); + cowling.rotation.x = Math.PI / 2; + cowling.position.z = -5; + this.group.add(cowling); + + // Propeller + this.propeller = new THREE.Group(); + const blade1 = new THREE.Mesh( + new THREE.BoxGeometry(0.12, 3, 0.08), + metalMat + ); + this.propeller.add(blade1); + const blade2 = new THREE.Mesh( + new THREE.BoxGeometry(3, 0.12, 0.08), + metalMat + ); + this.propeller.add(blade2); + // Hub + const hub = new THREE.Mesh( + new THREE.SphereGeometry(0.15, 4, 4), + metalMat + ); + this.propeller.add(hub); + this.propeller.position.z = -5.8; + this.group.add(this.propeller); + + // Landing gear + const gearMat = flatMat(0x222222); + const wheelMat = flatMat(0x111111); + + // Nose gear + const noseGearStrut = new THREE.Mesh( + new THREE.CylinderGeometry(0.06, 0.06, 2, 4), + gearMat + ); + noseGearStrut.position.set(0, -1.5, -3.5); + this.group.add(noseGearStrut); + const noseWheel = new THREE.Mesh( + new THREE.SphereGeometry(0.2, 4, 4), + wheelMat + ); + noseWheel.position.set(0, -2.5, -3.5); + this.group.add(noseWheel); + this.landingGear.push(noseGearStrut, noseWheel); + + // Left main gear + const leftGearStrut = new THREE.Mesh( + new THREE.CylinderGeometry(0.06, 0.06, 2, 4), + gearMat + ); + leftGearStrut.position.set(-2, -1.5, -1); + this.group.add(leftGearStrut); + const leftWheel = new THREE.Mesh( + new THREE.SphereGeometry(0.25, 4, 4), + wheelMat + ); + leftWheel.position.set(-2, -2.5, -1); + this.group.add(leftWheel); + this.landingGear.push(leftGearStrut, leftWheel); + + // Right main gear + const rightGearStrut = leftGearStrut.clone(); + rightGearStrut.position.x = 2; + this.group.add(rightGearStrut); + const rightWheel = leftWheel.clone(); + rightWheel.position.x = 2; + this.group.add(rightWheel); + this.landingGear.push(rightGearStrut, rightWheel); + + this.group.rotation.order = 'YXZ'; + } + + updatePropeller(thrust, delta) { + if (this.propeller) { + this.propeller.rotation.z += thrust * delta * 60; + } + } + + retractGear(onGround) { + if (onGround && !this.gearExtended) { + this.landingGear.forEach(part => { part.visible = true; }); + } else if (!onGround && this.gearExtended) { + this.landingGear.forEach(part => { part.visible = false; }); + } + this.gearExtended = onGround; + } + + getGroup() { + return this.group; + } +} diff --git a/js/airport.js b/js/airport.js new file mode 100644 index 0000000..ae7460d --- /dev/null +++ b/js/airport.js @@ -0,0 +1,223 @@ +import * as THREE from 'three'; + +const EGA_COLORS = { + BLACK: 0x000000, + BLUE: 0x0000AA, + GREEN: 0x00AA00, + CYAN: 0x00AAAA, + RED: 0xAA0000, + MAGENTA: 0xAA00AA, + BROWN: 0xAA5500, + LIGHT_GRAY: 0xAAAAAA, + DARK_GRAY: 0x555555, + LIGHT_BLUE: 0x5555FF, + LIGHT_GREEN: 0x55FF55, + LIGHT_CYAN: 0x55FFFF, + LIGHT_RED: 0xFF5555, + YELLOW: 0xFFFF55, + WHITE: 0xFFFFFF, +}; + +function flatMat(color) { + return new THREE.MeshLambertMaterial({ color, flatShading: true }); +} + +export class Airport { + constructor() { + this.group = new THREE.Group(); + } + + createRunwayTexture() { + const canvas = document.createElement('canvas'); + canvas.width = 512; + canvas.height = 512; + const ctx = canvas.getContext('2d'); + + ctx.fillStyle = '#444444'; + ctx.fillRect(0, 0, 512, 512); + + // Edge lines + ctx.fillStyle = '#FFFFFF'; + ctx.fillRect(18, 0, 4, 512); + ctx.fillRect(490, 0, 4, 512); + + // Center dashed line + for (let y = 0; y < 512; y += 40) { + ctx.fillRect(254, y, 4, 20); + } + + // Threshold markings + for (let x = 30; x < 482; x += 28) { + ctx.fillRect(x, 0, 14, 35); + ctx.fillRect(x, 477, 14, 35); + } + + // Touchdown zone markers + for (let x = 60; x < 160; x += 40) { + ctx.fillRect(x, 80, 10, 60); + ctx.fillRect(x, 372, 10, 60); + } + + const texture = new THREE.CanvasTexture(canvas); + texture.magFilter = THREE.NearestFilter; + texture.minFilter = THREE.NearestFilter; + return texture; + } + + buildRunway() { + const runwayLength = 180; + const runwayWidth = 30; + + const geometry = new THREE.PlaneGeometry(runwayWidth, runwayLength); + geometry.rotateX(-Math.PI / 2); + + const texture = this.createRunwayTexture(); + + const material = new THREE.MeshLambertMaterial({ + map: texture, + flatShading: true, + }); + + const runway = new THREE.Mesh(geometry, material); + runway.position.y = 0.1; + this.group.add(runway); + + // Taxiway + const taxiGeo = new THREE.PlaneGeometry(8, 120); + taxiGeo.rotateX(-Math.PI / 2); + const taxiMat = flatMat(0x555555); + const taxi = new THREE.Mesh(taxiGeo, taxiMat); + taxi.position.set(0, 0.15, -70); + this.group.add(taxi); + + // Apron + const apronGeo = new THREE.PlaneGeometry(60, 40); + apronGeo.rotateX(-Math.PI / 2); + const apron = new THREE.Mesh(apronGeo, taxiMat); + apron.position.set(0, 0.15, -110); + this.group.add(apron); + } + + buildControlTower() { + const tower = new THREE.Group(); + + // Base cylinder (octagonal for low-poly look) + const baseGeo = new THREE.CylinderGeometry(5, 6, 20, 8); + const baseMat = flatMat(0xAAAAAA); + const base = new THREE.Mesh(baseGeo, baseMat); + base.position.y = 10; + tower.add(base); + + // Cab (wider top) + const cabGeo = new THREE.CylinderGeometry(7, 5, 6, 8); + const cab = new THREE.Mesh(cabGeo, baseMat); + cab.position.y = 23; + tower.add(cab); + + // Windows (dark band around cab) + const winGeo = new THREE.CylinderGeometry(6.5, 5.5, 3, 8); + const winMat = flatMat(0x0000AA); + const windows = new THREE.Mesh(winGeo, winMat); + windows.position.y = 23; + tower.add(windows); + + // Roof + const roofGeo = new THREE.ConeGeometry(7.5, 3, 8); + const roofMat = flatMat(0x555555); + const roof = new THREE.Mesh(roofGeo, roofMat); + roof.position.y = 27.5; + tower.add(roof); + + // Antenna + const antGeo = new THREE.CylinderGeometry(0.1, 0.1, 5, 4); + const antenna = new THREE.Mesh(antGeo, flatMat(0xAA0000)); + antenna.position.y = 31; + tower.add(antenna); + + tower.position.set(0, 0, -130); + this.group.add(tower); + } + + buildHangar(index) { + const hangar = new THREE.Group(); + + // Body + const bodyGeo = new THREE.BoxGeometry(25, 10, 18); + const bodyMat = flatMat(0x888888); + const body = new THREE.Mesh(bodyGeo, bodyMat); + body.position.y = 5; + hangar.add(body); + + // Door + const doorGeo = new THREE.BoxGeometry(14, 8, 0.5); + const doorMat = flatMat(0x555555); + const door = new THREE.Mesh(doorGeo, doorMat); + door.position.set(0, 4, 9); + hangar.add(door); + + // Pitched roof + const roofShape = new THREE.Shape(); + roofShape.moveTo(-13, 0); + roofShape.lineTo(0, 6); + roofShape.lineTo(13, 0); + roofShape.lineTo(-13, 0); + + const extrudeSettings = { depth: 20, bevelEnabled: false }; + const roofGeo = new THREE.ExtrudeGeometry(roofShape, extrudeSettings); + const roofMat = flatMat(0xAA4444); + const roof = new THREE.Mesh(roofGeo, roofMat); + roof.position.set(0, 10, -10); + hangar.add(roof); + + const xOffset = (index - 1) * 40; + hangar.position.set(xOffset, 0, -160); + this.group.add(hangar); + } + + buildFuelTruck() { + const truck = new THREE.Group(); + + // Body + const bodyGeo = new THREE.BoxGeometry(2, 2, 4); + const bodyMat = flatMat(0xAA5500); + const body = new THREE.Mesh(bodyGeo, bodyMat); + body.position.y = 1; + truck.add(body); + + // Cabin + const cabGeo = new THREE.BoxGeometry(2, 1.5, 1.5); + const cabMat = flatMat(0xAAAAAA); + const cab = new THREE.Mesh(cabGeo, cabMat); + cab.position.set(0, 2.25, -1.5); + truck.add(cab); + + truck.position.set(20, 0, -90); + this.group.add(truck); + } + + build() { + this.buildRunway(); + this.buildControlTower(); + for (let i = 0; i < 3; i++) { + this.buildHangar(i); + } + this.buildFuelTruck(); + + // Small windsock near runway + const poleGeo = new THREE.CylinderGeometry(0.1, 0.1, 5, 4); + const pole = new THREE.Mesh(poleGeo, flatMat(0xAAAAAA)); + pole.position.set(-20, 2.5, 10); + this.group.add(pole); + + const sockGeo = new THREE.ConeGeometry(0.8, 3, 6); + const sockMat = flatMat(0xFF5555); + const sock = new THREE.Mesh(sockGeo, sockMat); + sock.rotation.z = Math.PI / 2; + sock.position.set(-21.5, 4.5, 10); + this.group.add(sock); + } + + getGroup() { + return this.group; + } +} diff --git a/js/flight-model.js b/js/flight-model.js new file mode 100644 index 0000000..f59564e --- /dev/null +++ b/js/flight-model.js @@ -0,0 +1,163 @@ +import * as THREE from 'three'; + +const PITCH_LIMIT = 85 * Math.PI / 180; +const STALL_SPEED = 25; +const GROUND_LIFT_REDUCTION = 0.15; + +export class FlightModel { + constructor() { + this.position = new THREE.Vector3(0, 8, 0); + this.velocity = new THREE.Vector3(0, 0, 0); + this.orientation = new THREE.Euler(0, 0, 0, 'YXZ'); + this.angularVelocity = new THREE.Vector3(0, 0, 0); + this.thrust = 0; + this.mass = 1200; + this.maxThrust = 50000; + this.dragCoefficient = 0.02; + this.liftCoefficient = 0.15; + this.gravity = 9.81; + this.onGround = true; + this.groundHeight = 2; + } + + getForwardVector() { + const dir = new THREE.Vector3(0, 0, -1); + dir.applyEuler(this.orientation); + return dir; + } + + getUpVector() { + const up = new THREE.Vector3(0, 1, 0); + up.applyEuler(this.orientation); + return up; + } + + update(delta, input, terrainHeightAtPos) { + this.groundHeight = terrainHeightAtPos(this.position.x, this.position.z) + 2; + this.onGround = this.position.y <= this.groundHeight + 0.5; + + // Throttle control + if (input.throttleUp) { + this.thrust = Math.min(1, this.thrust + delta * 0.3); + } else if (input.throttleDown) { + this.thrust = Math.max(0, this.thrust - delta * 0.3); + } + + const speed = this.velocity.length(); + + // Thrust force + const forward = this.getForwardVector(); + const thrustForce = forward.clone().multiplyScalar(this.thrust * this.maxThrust); + + // Drag + const dragMagnitude = this.dragCoefficient * speed * speed; + const dragForce = speed > 0 + ? this.velocity.clone().normalize().multiplyScalar(-dragMagnitude) + : new THREE.Vector3(); + + // Lift + let liftFactor = this.liftCoefficient; + if (this.onGround && speed < STALL_SPEED) { + liftFactor *= GROUND_LIFT_REDUCTION * (speed / STALL_SPEED); + } else if (speed < STALL_SPEED) { + liftFactor *= 0.5 * (speed / STALL_SPEED); + } + const up = this.getUpVector(); + const liftForce = up.clone().multiplyScalar(liftFactor * speed * speed); + + // Gravity + const gravityForce = new THREE.Vector3(0, -this.gravity * this.mass, 0); + + // Ground friction + let frictionForce = new THREE.Vector3(); + if (this.onGround) { + const horizontalVel = new THREE.Vector3(this.velocity.x, 0, this.velocity.z); + const hSpeed = horizontalVel.length(); + if (hSpeed > 0.1) { + frictionForce = horizontalVel.normalize().multiplyScalar(-hSpeed * 20); + } + } + + // Net force and acceleration + const netForce = new THREE.Vector3() + .add(thrustForce) + .add(dragForce) + .add(liftForce) + .add(gravityForce) + .add(frictionForce); + const acceleration = netForce.divideScalar(this.mass); + + // Integrate velocity and position + this.velocity.add(acceleration.multiplyScalar(delta)); + this.position.add(this.velocity.clone().multiplyScalar(delta)); + + // Ground collision + if (this.position.y < this.groundHeight) { + this.position.y = this.groundHeight; + if (this.velocity.y < 0) { + this.velocity.y = 0; + } + this.onGround = true; + } + + // Rotation + this.updateRotation(delta, input); + } + + updateRotation(delta, input) { + const pitchInput = input.pitch || 0; + const rollInput = input.roll || 0; + const yawInput = input.yaw || 0; + + // Dampen angular velocity + this.angularVelocity.multiplyScalar(0.92); + + // Apply control inputs (per-second rates) + this.angularVelocity.x += pitchInput * 1.5; + this.angularVelocity.y += yawInput * 0.8; + this.angularVelocity.z += rollInput * 1.2; + + // Clamp angular rates + this.angularVelocity.x = Math.max(-2, Math.min(2, this.angularVelocity.x)); + this.angularVelocity.y = Math.max(-1, Math.min(1, this.angularVelocity.y)); + this.angularVelocity.z = Math.max(-2, Math.min(2, this.angularVelocity.z)); + + // Integrate + this.orientation.x += this.angularVelocity.x * delta; + this.orientation.y += this.angularVelocity.y * delta; + this.orientation.z += this.angularVelocity.z * delta; + + // Clamp pitch + this.orientation.x = Math.max(-PITCH_LIMIT, Math.min(PITCH_LIMIT, this.orientation.x)); + + // Auto-leveling + this.orientation.x *= 0.995; + this.orientation.z *= 0.985; + } + + getAirspeedKnots() { + return this.velocity.length() * 1.94384; + } + + getAltitudeFeet() { + return Math.max(0, (this.position.y - this.groundHeight) * 3.28084); + } + + getHeadingDeg() { + const forward = this.getForwardVector(); + let heading = Math.atan2(-forward.x, -forward.z) * 180 / Math.PI; + return ((heading % 360) + 360) % 360; + } + + getVerticalSpeedFtMin() { + return -this.velocity.y * 196.85; + } + + getPitchDeg() { + return this.orientation.x * 180 / Math.PI; + } + + getRollDeg() { + return this.orientation.z * 180 / Math.PI; + } +} diff --git a/js/hud.js b/js/hud.js new file mode 100644 index 0000000..2585c72 --- /dev/null +++ b/js/hud.js @@ -0,0 +1,274 @@ +export class HUD { + constructor() { + this.canvas = document.getElementById('hud-canvas'); + this.ctx = this.canvas.getContext('2d'); + this.visible = false; + } + + show() { + this.visible = true; + document.getElementById('hud-overlay').style.display = 'block'; + } + + hide() { + this.visible = false; + document.getElementById('hud-overlay').style.display = 'none'; + } + + clear() { + this.ctx.clearRect(0, 0, 320, 200); + } + + draw(flightModel) { + if (!this.visible) return; + this.clear(); + const ctx = this.ctx; + + const speed = flightModel.getAirspeedKnots(); + const altitude = flightModel.getAltitudeFeet(); + const heading = flightModel.getHeadingDeg(); + const vsi = flightModel.getVerticalSpeedFtMin(); + const pitch = flightModel.getPitchDeg(); + const roll = flightModel.getRollDeg(); + const throttle = flightModel.thrust; + + // Artificial horizon (center-bottom) + this.drawArtificialHorizon(ctx, pitch, roll); + + // Heading tape (top center) + this.drawHeadingTape(ctx, heading); + + // Airspeed indicator (left side) + this.drawAirspeed(ctx, speed); + + // Altimeter (right side) + this.drawAltimeter(ctx, altitude); + + // Vertical speed indicator (right side, below altimeter) + this.drawVSI(ctx, vsi); + + // Throttle indicator (bottom left) + this.drawThrottle(ctx, throttle); + + // Heading number + const cardinal = this.headingToCardinal(heading); + ctx.fillStyle = '#00FF00'; + ctx.font = '8px "Press Start 2P", monospace'; + ctx.textAlign = 'center'; + ctx.fillText(`${Math.round(heading)}° ${cardinal}`, 160, 12); + } + + drawArtificialHorizon(ctx, pitch, roll) { + const w = 100; + const h = 60; + const x = 110; + const y = 140; + + ctx.save(); + ctx.beginPath(); + ctx.rect(x, y, w, h); + ctx.clip(); + + const cx = x + w / 2; + const cy = y + h / 2; + + ctx.translate(cx, cy); + ctx.rotate(-roll * Math.PI / 180); + + const pitchOffset = pitch * 1.5; + + // Sky + ctx.fillStyle = '#0000AA'; + ctx.fillRect(-60, -40 + pitchOffset, 120, 40); + + // Ground + ctx.fillStyle = '#8B4513'; + ctx.fillRect(-60, pitchOffset, 120, 40); + + // Horizon line + ctx.strokeStyle = '#FFFFFF'; + ctx.lineWidth = 1; + ctx.beginPath(); + ctx.moveTo(-60, pitchOffset); + ctx.lineTo(60, pitchOffset); + ctx.stroke(); + + // Pitch lines + ctx.strokeStyle = '#FFFFFF88'; + ctx.lineWidth = 1; + for (let p = -20; p <= 20; p += 10) { + if (p === 0) continue; + const py = p * 1.5; + ctx.beginPath(); + ctx.moveTo(-10, py); + ctx.lineTo(10, py); + ctx.stroke(); + } + + ctx.restore(); + + // Aircraft reference symbol + ctx.strokeStyle = '#FF8800'; + ctx.lineWidth = 2; + ctx.beginPath(); + ctx.moveTo(cx - 18, cy); + ctx.lineTo(cx - 6, cy); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(cx + 6, cy); + ctx.lineTo(cx + 18, cy); + ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(cx, cy); + ctx.lineTo(cx, cy + 3); + ctx.stroke(); + + // Border + ctx.strokeStyle = '#00FF00'; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, w, h); + } + + drawHeadingTape(ctx, heading) { + const y = 22; + const ctx2 = ctx; + + ctx2.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx2.fillRect(80, 14, 160, 18); + ctx2.strokeStyle = '#00FF00'; + ctx2.lineWidth = 1; + ctx2.strokeRect(80, 14, 160, 18); + + ctx2.font = '6px "Press Start 2P", monospace'; + ctx2.textAlign = 'center'; + ctx2.fillStyle = '#00FF00'; + + const cards = [ + { deg: 0, label: 'N' }, + { deg: 90, label: 'E' }, + { deg: 180, label: 'S' }, + { deg: 270, label: 'W' }, + ]; + + cards.forEach(({ deg, label }) => { + const diff = heading - deg; + const px = (diff / 30) * 8; + const lx = 160 + px; + if (lx > 85 && lx < 235) { + ctx2.fillText(label, lx, y); + } + }); + } + + drawAirspeed(ctx, speed) { + const x = 8; + const y = 40; + const w = 30; + const h = 100; + + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(x, y, w, h); + ctx.strokeStyle = '#00FF00'; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, w, h); + + // Speed bar + const fill = Math.min(1, speed / 200); + const barH = fill * (h - 10); + ctx.fillStyle = fill > 0.85 ? '#FF5555' : fill > 0.6 ? '#FFFF55' : '#00FF00'; + ctx.fillRect(x + 2, y + h - 4 - barH, w - 4, barH); + + // Speed ticks + ctx.fillStyle = '#00FF00'; + ctx.font = '6px "Press Start 2P", monospace'; + ctx.textAlign = 'left'; + for (let s = 0; s <= 200; s += 50) { + const tickY = y + h - 4 - (s / 200) * (h - 10); + ctx.fillRect(x, tickY - 0.5, 4, 1); + } + + // Current speed + ctx.textAlign = 'center'; + ctx.font = '8px "Press Start 2P", monospace'; + ctx.fillText(Math.round(speed), x + w / 2, y - 4); + ctx.font = '5px "Press Start 2P", monospace'; + ctx.fillText('KT', x + w / 2, y + h + 10); + } + + drawAltimeter(ctx, altitude) { + const x = 282; + const y = 40; + const w = 30; + const h = 100; + + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(x, y, w, h); + ctx.strokeStyle = '#00FF00'; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, w, h); + + // Altitude bar + const fill = Math.min(1, altitude / 15000); + const barH = fill * (h - 10); + ctx.fillStyle = '#00FF00'; + ctx.fillRect(x + 2, y + h - 4 - barH, w - 4, barH); + + // Current altitude + ctx.fillStyle = '#00FF00'; + ctx.font = '8px "Press Start 2P", monospace'; + ctx.textAlign = 'center'; + ctx.fillText(Math.round(altitude), x + w / 2, y - 4); + ctx.font = '5px "Press Start 2P", monospace'; + ctx.fillText('FT', x + w / 2, y + h + 10); + } + + drawVSI(ctx, vsi) { + const x = 282; + const y = 155; + const w = 30; + const h = 35; + + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(x, y, w, h); + ctx.strokeStyle = '#00FF00'; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, w, h); + + ctx.fillStyle = '#00FF00'; + ctx.font = '6px "Press Start 2P", monospace'; + ctx.textAlign = 'center'; + const vsiText = vsi >= 0 ? `+${Math.round(vsi)}` : Math.round(vsi); + ctx.fillText(vsiText, x + w / 2, y + 14); + ctx.font = '5px "Press Start 2P", monospace'; + ctx.fillText('FT/M', x + w / 2, y + 26); + } + + drawThrottle(ctx, throttle) { + const x = 8; + const y = 160; + const w = 40; + const h = 8; + + ctx.fillStyle = 'rgba(0, 0, 0, 0.5)'; + ctx.fillRect(x, y, w, h); + ctx.strokeStyle = '#00FF00'; + ctx.lineWidth = 1; + ctx.strokeRect(x, y, w, h); + + const fill = throttle * (w - 2); + ctx.fillStyle = throttle > 0.8 ? '#FF5555' : '#00AAAA'; + ctx.fillRect(x + 1, y + 1, fill, h - 2); + + ctx.fillStyle = '#00FF00'; + ctx.font = '5px "Press Start 2P", monospace'; + ctx.textAlign = 'left'; + ctx.fillText('THR', x, y - 3); + } + + headingToCardinal(deg) { + const dirs = ['N', 'NNE', 'NE', 'ENE', 'E', 'ESE', 'SE', 'SSE', + 'S', 'SSW', 'SW', 'WSW', 'W', 'WNW', 'NW', 'NNW']; + const idx = Math.round(deg / 22.5) % 16; + return dirs[idx]; + } +} diff --git a/js/main.js b/js/main.js new file mode 100644 index 0000000..05f91d1 --- /dev/null +++ b/js/main.js @@ -0,0 +1,306 @@ +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(); diff --git a/js/retro.js b/js/retro.js new file mode 100644 index 0000000..3d0872b --- /dev/null +++ b/js/retro.js @@ -0,0 +1,108 @@ +import * as THREE from 'three'; + +export class RetroEffect { + constructor(renderer, width, height) { + this.renderer = renderer; + this.width = width; + this.height = height; + + this.renderTarget = new THREE.WebGLRenderTarget(width, height, { + minFilter: THREE.NearestFilter, + magFilter: THREE.NearestFilter, + }); + + this.scene = new THREE.Scene(); + this.camera = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + + this.quad = new THREE.Mesh( + new THREE.PlaneGeometry(2, 2), + new THREE.ShaderMaterial({ + uniforms: { + tDiffuse: { value: null }, + resolution: { value: new THREE.Vector2(width, height) }, + time: { value: 0 }, + }, + vertexShader: ` + varying vec2 vUv; + void main() { + vUv = uv; + gl_Position = vec4(position, 0.0, 1.0); + } + `, + fragmentShader: ` + uniform sampler2D tDiffuse; + uniform vec2 resolution; + uniform float time; + varying vec2 vUv; + + vec2 barrelDistortion(vec2 uv, float amount) { + vec2 center = uv - 0.5; + float dist = dot(center, center); + return uv + center * dist * amount; + } + + vec3 quantize(vec3 color, float levels) { + return floor(color * levels + 0.5) / levels; + } + + void main() { + vec2 distortedUv = barrelDistortion(vUv, 0.25); + + // Clamp distorted UV + distortedUv = clamp(distortedUv, 0.0, 1.0); + + // Chromatic aberration + float aberration = 0.0015; + vec2 dir = distortedUv - 0.5; + float dist = length(dir); + + float r = texture2D(tDiffuse, distortedUv + dir * aberration).r; + vec2 gbSample = distortedUv - dir * aberration * 0.5; + float g = texture2D(tDiffuse, gbSample).g; + float b = texture2D(tDiffuse, distortedUv - dir * aberration).b; + vec3 color = vec3(r, g, b); + + // Scanline + float scanline = sin(vUv.y * resolution.y * 3.14159) * 0.08; + color -= scanline; + + // Quantize to EGA-like palette + color = quantize(color, 16.0); + + // Vignette + float vignette = 1.0 - dist * 0.6; + vignette = smoothstep(0.0, 1.0, vignette); + color *= vignette; + + // Subtle phosphor warmth + color.r += 0.01; + color.g += 0.005; + + // CRT curvature darkening at edges + float edgeDarken = smoothstep(0.3, 1.2, dist); + color *= 1.0 - edgeDarken * 0.3; + + gl_FragColor = vec4(color, 1.0); + } + `, + }) + ); + + this.scene.add(this.quad); + } + + update(time) { + this.quad.material.uniforms.time.value = time; + } + + render(scene, camera) { + // Render scene to offscreen target + this.renderer.setRenderTarget(this.renderTarget); + this.renderer.render(scene, camera); + + // Render post-processed quad to screen + this.renderer.setRenderTarget(null); + this.quad.material.uniforms.tDiffuse.value = this.renderTarget.texture; + this.renderer.render(this.scene, this.camera); + } +} diff --git a/js/sound.js b/js/sound.js new file mode 100644 index 0000000..37f455e --- /dev/null +++ b/js/sound.js @@ -0,0 +1,77 @@ +export class Sound { + constructor() { + this.ctx = null; + this.engineOsc = null; + this.engineGain = null; + this.initialized = false; + } + + init() { + if (this.initialized) return; + try { + this.ctx = new (window.AudioContext || window.webkitAudioContext)(); + this.engineOsc = this.ctx.createOscillator(); + this.engineOsc.type = 'sawtooth'; + this.engineGain = this.ctx.createGain(); + this.engineGain.gain.value = 0; + + // Low-pass filter for muffled engine sound + this.engineFilter = this.ctx.createBiquadFilter(); + this.engineFilter.type = 'lowpass'; + this.engineFilter.frequency.value = 300; + + this.engineOsc.connect(this.engineFilter); + this.engineFilter.connect(this.engineGain); + this.engineGain.connect(this.ctx.destination); + this.engineOsc.start(); + this.initialized = true; + } catch { + // Audio not available, silently fail + } + } + + update(throttle) { + if (!this.initialized || !this.engineOsc) return; + const baseFreq = 60; + const maxFreq = 180; + this.engineOsc.frequency.value = baseFreq + throttle * (maxFreq - baseFreq); + this.engineGain.gain.value = Math.min(throttle * 0.08, 0.08); + this.engineFilter.frequency.value = 200 + throttle * 200; + } + + beep(frequency = 800, duration = 80) { + if (!this.initialized) return; + try { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.type = 'square'; + osc.frequency.value = frequency; + gain.gain.value = 0.05; + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.start(); + osc.stop(this.ctx.currentTime + duration / 1000); + } catch { + // Ignore audio errors + } + } + + takeoffSound() { + if (!this.initialized) return; + try { + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + osc.type = 'triangle'; + osc.frequency.value = 400; + osc.frequency.linearRampToValueAtTime(800, this.ctx.currentTime + 0.3); + gain.gain.value = 0.08; + gain.gain.linearRampToValueAtTime(0, this.ctx.currentTime + 0.5); + osc.connect(gain); + gain.connect(this.ctx.destination); + osc.start(); + osc.stop(this.ctx.currentTime + 0.5); + } catch { + // Ignore + } + } +} diff --git a/js/terrain.js b/js/terrain.js new file mode 100644 index 0000000..ef23020 --- /dev/null +++ b/js/terrain.js @@ -0,0 +1,168 @@ +import * as THREE from 'three'; + +const EGA = { + GREEN: new THREE.Color(0.20, 0.67, 0.13), + DARK_GREEN: new THREE.Color(0.15, 0.40, 0.10), + BROWN: new THREE.Color(0.45, 0.30, 0.15), + TAN: new THREE.Color(0.55, 0.45, 0.30), + WHITE: new THREE.Color(1, 1, 1), + BLUE: new THREE.Color(0, 0, 0.67), +}; + +export class Terrain { + constructor(size = 2000, segments = 100) { + this.size = size; + this.segments = segments; + this.heightData = null; + this.cellSize = size / segments; + } + + hash(x, z) { + const n = Math.sin(x * 127.1 + z * 311.7) * 43758.5453; + return n - Math.floor(n); + } + + smoothNoise(x, z) { + const ix = Math.floor(x); + const iz = Math.floor(z); + const fx = x - ix; + const fz = z - iz; + const sfx = fx * fx * (3 - 2 * fx); + const sfz = fz * fz * (3 - 2 * fz); + + const n00 = this.hash(ix, iz); + const n10 = this.hash(ix + 1, iz); + const n01 = this.hash(ix, iz + 1); + const n11 = this.hash(ix + 1, iz + 1); + + const nx0 = n00 + (n10 - n00) * sfx; + const nx1 = n01 + (n11 - n01) * sfx; + return nx0 + (nx1 - nx0) * sfz; + } + + getHeightRaw(x, z) { + let h = 0; + h += this.smoothNoise(x * 0.002, z * 0.002) * 50; + h += this.smoothNoise(x * 0.006, z * 0.006) * 20; + h += this.smoothNoise(x * 0.015, z * 0.015) * 8; + return Math.max(0, h - 15); + } + + buildHeightData() { + const { segments: seg, size } = this; + this.heightData = new Float32Array((seg + 1) * (seg + 1)); + const half = size / 2; + for (let iz = 0; iz <= seg; iz++) { + for (let ix = 0; ix <= seg; ix++) { + const x = -half + ix * this.cellSize; + const z = -half + iz * this.cellSize; + this.heightData[iz * (seg + 1) + ix] = this.getHeightRaw(x, z); + } + } + } + + getHeightAt(x, z) { + if (!this.heightData) return this.getHeightRaw(x, z); + const { size, segments: seg } = this; + const half = size / 2; + const gx = ((x + half) / size) * seg; + const gz = ((z + half) / size) * seg; + const ix = Math.floor(gx); + const iz = Math.floor(gz); + if (ix < 0 || ix >= seg || iz < 0 || iz >= seg) return 0; + const fx = gx - ix; + const fz = gz - iz; + const row = seg + 1; + const h00 = this.heightData[iz * row + Math.min(ix, seg)]; + const h10 = this.heightData[iz * row + Math.min(ix + 1, seg)]; + const h01 = this.heightData[(iz + 1) * row + Math.min(ix, seg)]; + const h11 = this.heightData[(iz + 1) * row + Math.min(ix + 1, seg)]; + const nx0 = h00 + (h10 - h00) * fx; + const nx1 = h01 + (h11 - h01) * fx; + return nx0 + (nx1 - nx0) * fz; + } + + flattenAirport(radius) { + if (!this.heightData) return; + const { segments: seg } = this; + const row = seg + 1; + const center = seg / 2; + for (let iz = 0; iz <= seg; iz++) { + for (let ix = 0; ix <= seg; ix++) { + const dx = ix - center; + const dz = iz - center; + const dist = Math.sqrt(dx * dx + dz * dz) * this.cellSize; + if (dist < radius) { + const fade = 1 - Math.min(1, dist / radius); + this.heightData[iz * row + ix] *= (1 - fade * 0.95); + } + } + } + } + + createMesh() { + this.buildHeightData(); + this.flattenAirport(200); + + const { segments: seg, size } = this; + const geometry = new THREE.PlaneGeometry(size, size, seg, seg); + geometry.rotateX(-Math.PI / 2); + + const positions = geometry.attributes.position.array; + const colors = new Float32Array(positions.length); + + for (let i = 0; i < positions.length; i += 3) { + const x = positions[i]; + const z = positions[i + 2]; + const h = this.getHeightAt(x, z); + positions[i + 1] = h; + + if (h < 3) { + colors[i] = EGA.GREEN.r; + colors[i + 1] = EGA.GREEN.g; + colors[i + 2] = EGA.GREEN.b; + } else if (h < 12) { + colors[i] = EGA.DARK_GREEN.r; + colors[i + 1] = EGA.DARK_GREEN.g; + colors[i + 2] = EGA.DARK_GREEN.b; + } else if (h < 30) { + colors[i] = EGA.BROWN.r; + colors[i + 1] = EGA.BROWN.g; + colors[i + 2] = EGA.BROWN.b; + } else if (h < 45) { + colors[i] = EGA.TAN.r; + colors[i + 1] = EGA.TAN.g; + colors[i + 2] = EGA.TAN.b; + } else { + const mix = Math.min(1, (h - 45) / 20); + colors[i] = EGA.TAN.r + (EGA.WHITE.r - EGA.TAN.r) * mix; + colors[i + 1] = EGA.TAN.g + (EGA.WHITE.g - EGA.TAN.g) * mix; + colors[i + 2] = EGA.TAN.b + (EGA.WHITE.b - EGA.TAN.b) * mix; + } + } + + geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3)); + geometry.computeVertexNormals(); + + const material = new THREE.MeshLambertMaterial({ + vertexColors: true, + flatShading: true, + }); + + return new THREE.Mesh(geometry, material); + } + + createWater() { + const geometry = new THREE.PlaneGeometry(this.size * 2, this.size * 2); + geometry.rotateX(-Math.PI / 2); + const material = new THREE.MeshLambertMaterial({ + color: EGA.BLUE, + transparent: true, + opacity: 0.7, + flatShading: true, + }); + const water = new THREE.Mesh(geometry, material); + water.position.y = 0.5; + return water; + } +} diff --git a/js/ui.js b/js/ui.js new file mode 100644 index 0000000..6880c92 --- /dev/null +++ b/js/ui.js @@ -0,0 +1,142 @@ +export class UI { + constructor() { + this.loadingScreen = document.getElementById('loading-screen'); + this.mainMenu = document.getElementById('main-menu'); + this.pauseMenu = document.getElementById('pause-menu'); + this.controlsScreen = document.getElementById('controls-screen'); + this.quitScreen = document.getElementById('quit-screen'); + this.progressBar = document.getElementById('progress-bar'); + this.loadingMessage = document.getElementById('loading-message'); + this.menuItems = document.querySelectorAll('#menu-items .menu-item'); + this.pauseItems = document.querySelectorAll('#pause-items .menu-item'); + this.currentMenuItem = 0; + this.currentPauseItem = 0; + this.onNewFlight = null; + this.onResume = null; + this.onMainMenu = null; + this.onQuit = null; + this.menuActive = false; + this.controlsActive = false; + } + + delay(ms) { + return new Promise(r => setTimeout(r, ms)); + } + + async showLoading(onStep) { + const steps = [ + { label: 'GENERATING TERRAIN...', duration: 600 }, + { label: 'BUILDING AIRPORT...', duration: 500 }, + { label: 'LOADING AIRCRAFT...', duration: 400 }, + { label: 'CALIBRATING INSTRUMENTS...', duration: 400 }, + { label: 'READY', duration: 300 }, + ]; + + for (let i = 0; i < steps.length; i++) { + const step = steps[i]; + this.loadingMessage.textContent = step.label; + this.progressBar.style.width = `${((i + 1) / steps.length) * 100}%`; + if (onStep) onStep(i); + await this.delay(step.duration); + } + + this.loadingScreen.style.display = 'none'; + this.showMainMenu(); + } + + showMainMenu() { + this.mainMenu.style.display = 'flex'; + this.currentMenuItem = 0; + this.updateMenuHighlight(); + this.menuActive = true; + } + + hideMainMenu() { + this.mainMenu.style.display = 'none'; + this.menuActive = false; + } + + handleMenuInput(key) { + if (!this.menuActive) return false; + if (key === 'ArrowUp' || key === 'ArrowDown') { + const items = this.menuItems; + if (key === 'ArrowUp') { + this.currentMenuItem = (this.currentMenuItem - 1 + items.length) % items.length; + } else { + this.currentMenuItem = (this.currentMenuItem + 1) % items.length; + } + this.updateMenuHighlight(); + return true; + } else if (key === 'Enter' || key === ' ') { + const action = this.menuItems[this.currentMenuItem].dataset.action; + if (action === 'new-flight' && this.onNewFlight) this.onNewFlight(); + else if (action === 'controls') this.showControls(); + else if (action === 'quit' && this.onQuit) this.onQuit(); + return true; + } + return false; + } + + updateMenuHighlight() { + this.menuItems.forEach((item, i) => { + item.classList.toggle('selected', i === this.currentMenuItem); + }); + } + + showControls() { + this.controlsScreen.style.display = 'flex'; + this.controlsActive = true; + } + + hideControls() { + this.controlsScreen.style.display = 'none'; + this.controlsActive = false; + } + + handleControlsInput() { + if (!this.controlsActive) return false; + this.hideControls(); + return true; + } + + showPauseMenu() { + this.pauseMenu.style.display = 'flex'; + this.currentPauseItem = 0; + this.updatePauseHighlight(); + } + + hidePauseMenu() { + this.pauseMenu.style.display = 'none'; + } + + handlePauseInput(key) { + if (this.pauseMenu.style.display === 'none') return false; + if (key === 'ArrowUp' || key === 'ArrowDown') { + const items = this.pauseItems; + if (key === 'ArrowUp') { + this.currentPauseItem = (this.currentPauseItem - 1 + items.length) % items.length; + } else { + this.currentPauseItem = (this.currentPauseItem + 1) % items.length; + } + this.updatePauseHighlight(); + return true; + } else if (key === 'Enter' || key === ' ') { + const action = this.pauseItems[this.currentPauseItem].dataset.action; + if (action === 'resume' && this.onResume) this.onResume(); + else if (action === 'main-menu' && this.onMainMenu) this.onMainMenu(); + return true; + } + return false; + } + + updatePauseHighlight() { + this.pauseItems.forEach((item, i) => { + item.classList.toggle('selected', i === this.currentPauseItem); + }); + } + + showQuitScreen() { + this.quitScreen.style.display = 'flex'; + this.mainMenu.style.display = 'none'; + } +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..8995775 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,996 @@ +{ + "name": "retro-flight-sim", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "retro-flight-sim", + "version": "0.1.0", + "dependencies": { + "three": "^0.160.0" + }, + "devDependencies": { + "vite": "^5.0.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/three": { + "version": "0.160.1", + "resolved": "https://registry.npmjs.org/three/-/three-0.160.1.tgz", + "integrity": "sha512-Bgl2wPJypDOZ1stAxwfWAcJ0WQf7QzlptsxkjYiURPz+n5k4RBDLsq+6f9Y75TYxn6aHLcWz+JNmwTOXWrQTBQ==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..e2b9d43 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "retro-flight-sim", + "version": "0.1.0", + "description": "Early 90s Amiga/DOS-style 3D flight simulator", + "type": "module", + "dependencies": { + "three": "^0.160.0" + }, + "devDependencies": { + "vite": "^5.0.0" + }, + "scripts": { + "dev": "vite", + "build": "vite build" + } +}