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:
+186
@@ -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
@@ -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 ... Throttle Up / Down</p>
|
||||
<p>ARROW UP / DOWN ... Pitch</p>
|
||||
<p>ARROW LEFT / RIGHT ... Roll</p>
|
||||
<p>Q / E ... Yaw (Rudder)</p>
|
||||
<p>C ... Toggle Camera View</p>
|
||||
<p>ESC ... 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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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';
|
||||
}
|
||||
}
|
||||
Generated
+996
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user