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 <noreply@anthropic.com>
This commit is contained in:
2026-05-05 10:43:22 +00:00
commit 76bad46d09
13 changed files with 2965 additions and 0 deletions
+186
View File
@@ -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;
}
+87
View File
@@ -0,0 +1,87 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Wings of the 90s - Flight Simulator</title>
<link rel="stylesheet" href="css/style.css">
<link href="https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap" rel="stylesheet">
</head>
<body>
<canvas id="game-canvas" width="320" height="200"></canvas>
<div id="crt-overlay"></div>
<div id="loading-screen">
<div class="loading-box">
<h1>WINGS OF THE 90S</h1>
<h2>Flight Simulator v1.0</h2>
<div class="progress-container">
<div class="progress-bar" id="progress-bar"></div>
</div>
<p id="loading-message">INITIALIZING...</p>
<div class="loading-plane">
<pre>
__
/ \
| . . |
\___/
/ \
/ \
~~~~~
</pre>
</div>
</div>
</div>
<div id="main-menu" style="display:none;">
<div class="menu-box">
<h1>WINGS OF THE 90S</h1>
<div id="menu-items">
<div class="menu-item selected" data-action="new-flight">NEW FLIGHT</div>
<div class="menu-item" data-action="controls">CONTROLS</div>
<div class="menu-item" data-action="quit">QUIT</div>
</div>
<p class="copyright">(C) 1993 PIXEL WINGS SOFTWARE</p>
</div>
</div>
<div id="controls-screen" style="display:none;">
<div class="menu-box">
<h2>CONTROLS</h2>
<div class="controls-list">
<p>W / S &nbsp;... Throttle Up / Down</p>
<p>ARROW UP / DOWN &nbsp;... Pitch</p>
<p>ARROW LEFT / RIGHT ... Roll</p>
<p>Q / E &nbsp;... Yaw (Rudder)</p>
<p>C &nbsp;... Toggle Camera View</p>
<p>ESC &nbsp;... Pause / Menu</p>
</div>
<p class="prompt">PRESS ANY KEY TO CONTINUE</p>
</div>
</div>
<div id="pause-menu" style="display:none;">
<div class="menu-box">
<h2>PAUSED</h2>
<div id="pause-items">
<div class="menu-item selected" data-action="resume">RESUME</div>
<div class="menu-item" data-action="main-menu">MAIN MENU</div>
</div>
</div>
</div>
<div id="hud-overlay" style="display:none;">
<canvas id="hud-canvas" width="320" height="200"></canvas>
</div>
<div id="quit-screen" style="display:none;">
<div class="menu-box">
<h1>GOODBYE!</h1>
<p>Thanks for flying Wings of the 90s.</p>
<p class="prompt">CLOSE THIS TAB TO EXIT</p>
</div>
</div>
<script type="module" src="js/main.js"></script>
</body>
</html>
+219
View File
@@ -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;
}
}
+223
View File
@@ -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;
}
}
+163
View File
@@ -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;
}
}
+274
View File
@@ -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];
}
}
+306
View File
@@ -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();
+108
View File
@@ -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);
}
}
+77
View File
@@ -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
}
}
}
+168
View File
@@ -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;
}
}
+142
View File
@@ -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';
}
}
+996
View File
@@ -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
}
}
}
}
}
+16
View File
@@ -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"
}
}