Flight Simulator
thinkingcap qwen3.6 27b · typescript

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 65,174 bytes · SHA-256 161386b10820
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Apex Aero - Flight Simulator</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; font-family: 'Courier New', monospace; color: #fff; }
#canvas-container { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
/* HUD */
#hud { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
.hud-item { display: flex; align-items: center; gap: 6px; margin-bottom: 4px; font-size: 13px; text-shadow: 0 0 8px rgba(0,255,0,.5); }
.hud-label { color: #0f0; font-weight: bold; min-width: 70px; }
.hud-unit { color: #0a0; font-size: 11px; }
#hud-top-left { position: absolute; top: 20px; left: 20px; }
#hud-top-right { position: absolute; top: 20px; right: 20px; text-align: right; }
#hud-bottom-left { position: absolute; bottom: 60px; left: 20px; }
#hud-bottom-right { position: absolute; bottom: 60px; right: 20px; text-align: right; }
#hud-center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 40px; height: 40px; border: 1px solid rgba(0,255,0,.3); border-radius: 50%; }
#hud-bottom-center { position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%); text-align: center; }
/* Menu Overlay */
#menu-overlay { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 100; background: linear-gradient(135deg, #0a0a2e 0%, #1a1a4e 50%, #0d0d3d 100%); display: flex; align-items: center; justify-content: center; }
#menu-content { text-align: center; max-width: 800px; padding: 40px; }
#menu-content h1 { font-size: 64px; letter-spacing: 20px; color: #fff; text-shadow: 0 0 30px rgba(0,150,255,.8), 0 0 60px rgba(0,100,255,.4); margin-bottom: 5px; }
.subtitle { font-size: 18px; color: #6af; letter-spacing: 8px; margin-bottom: 30px; }
#menu-content h2 { font-size: 20px; color: #6cf; margin: 20px 0 15px; letter-spacing: 4px; }
/* Aircraft Cards */
.aircraft-cards, .scenario-cards { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; margin-bottom: 20px; }
.ac-card, .sc-card { background: rgba(0,30,60,.7); border: 2px solid #2a4a8a; border-radius: 10px; padding: 15px; width: 220px; cursor: pointer; transition: all .3s; }
.ac-card:hover, .sc-card:hover { border-color: #6cf; background: rgba(0,50,100,.7); transform: translateY(-3px); }
.ac-card.selected, .sc-card.selected { border-color: #0f0; box-shadow: 0 0 20px rgba(0,255,0,.4); }
.ac-card h3, .sc-card h3 { color: #6cf; margin-bottom: 8px; font-size: 16px; }
.ac-card p, .sc-card p { font-size: 11px; color: #aac; line-height: 1.5; text-align: left; }
.stat-bar { height: 4px; background: #234; border-radius: 2px; margin-top: 3px; }
.stat-fill { height: 100%; background: linear-gradient(90deg, #0af, #0f6); border-radius: 2px; }
/* Start Button */
#start-btn { background: linear-gradient(180deg, #0a6, #084); color: #fff; border: none; padding: 15px 50px; font-size: 20px; letter-spacing: 6px; cursor: pointer; border-radius: 5px; margin-top: 20px; transition: all .3s; font-family: 'Courier New', monospace; }
#start-btn:hover { background: linear-gradient(180deg, #0c8, #0a6); box-shadow: 0 0 30px rgba(0,255,100,.5); transform: scale(1.05); }
/* Controls Info */
#controls-info { margin-top: 25px; padding: 15px; background: rgba(0,20,40,.6); border-radius: 8px; font-size: 12px; color: #8ac; line-height: 1.8; }
/* Message Overlay */
#message-overlay { position: fixed; top: 30%; left: 50%; transform: translate(-50%, -50%); z-index: 50; font-size: 24px; color: #f44; text-shadow: 0 0 20px rgba(255,0,0,.8); letter-spacing: 3px; }
</style>
</head>
<body>
<div id="canvas-container"></div><div id="hud" style="display:none"><div id="hud-top-left"><div class="hud-item"><span class="hud-label">AIRSPEED</span><span id="hud-airspeed">0</span> <span class="hud-unit">KTS</span></div><div class="hud-item"><span class="hud-label">ALTITUDE</span><span id="hud-altitude">0</span> <span class="hud-unit">FT</span></div></div><div id="hud-top-right"><div class="hud-item"><span class="hud-label">HEADING</span><span id="hud-heading">000</span> <span class="hud-unit">°</span></div><div class="hud-item"><span class="hud-label">VERT SPEED</span><span id="hud-vspeed">0</span> <span class="hud-unit">FT/M</span></div></div><div id="hud-bottom-left"><div class="hud-item"><span class="hud-label">THROTTLE</span><span id="hud-throttle">0</span><span class="hud-unit">%</span></div><div class="hud-item"><span class="hud-label">G-FORCE</span><span id="hud-gforce">1.0</span> <span class="hud-unit">G</span></div></div><div id="hud-bottom-right"><div class="hud-item"><span class="hud-label">PITCH</span><span id="hud-pitch">0.0</span> <span class="hud-unit">°</span></div><div class="hud-item"><span class="hud-label">ROLL</span><span id="hud-roll">0.0</span> <span class="hud-unit">°</span></div></div><div id="hud-center"></div><div id="hud-bottom-center"><div class="hud-item"><span class="hud-label">AOA</span><span id="hud-aoa">0.0</span> <span class="hud-unit">°</span></div><div class="hud-item"><span class="hud-label">DRAG</span><span id="hud-drag">0</span> <span class="hud-unit">N</span></div></div></div><div id="menu-overlay"><div id="menu-content"><h1>APEX AERO</h1><p class="subtitle">Flight Simulator</p><div id="aircraft-select"><h2>Select Aircraft</h2><div class="aircraft-cards" id="aircraft-cards"></div></div><div id="scenario-select"><h2>Select Scenario</h2><div class="scenario-cards" id="scenario-cards"></div></div><button id="start-btn">LAUNCH SIMULATION</button><div id="controls-info"><h3>Controls</h3><p><b>W/S</b> Pitch Down/Up <b>A/D</b> Roll Left/Right <b>Q/E</b> Yaw Left/Right</p><p><b>Shift/Ctrl</b> Throttle Up/Down <b>T</b> Toggle Trim <b>R</b> Reset Position</p></div></div></div><div id="message-overlay" style="display:none"><span id="message-text"></span></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./aircraft":"src/aircraft.ts","./game":"src/game.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./audio":"src/audio.ts"},"src/aircraft.ts":{"./types":"src/types.ts"},"src/physics.ts":{"./types":"src/types.ts"},"src/input.ts":{"./types":"src/types.ts"},"src/models.ts":{"./types":"src/types.ts"},"src/renderer.ts":{"./types":"src/types.ts","./terrain":"src/terrain.ts","./models":"src/models.ts"},"src/game.ts":{"./types":"src/types.ts","./physics":"src/physics.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./audio":"src/audio.ts","./models":"src/models.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const aircraft_1 = require("./aircraft");
const game_1 = require("./game");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const audio_1 = require("./audio");
let renderer;
let input;
let audio;
let gameManager;
let animFrameId = 0;
let selectedAircraftIndex = 0;
let selectedScenarioIndex = 0;
function init() {
// Build UI
buildMenu();
// Initialize systems
renderer = new renderer_1.Renderer();
input = new input_1.InputManager();
audio = new audio_1.AudioSystem();
gameManager = new game_1.GameManager(renderer, input, audio);
// Setup terrain (always)
renderer.setupTerrain();
// Handle window resize
window.addEventListener('resize', () => {
renderer.resize();
});
// Start button handler
document.getElementById('start-btn').addEventListener('click', startGame);
// Reset key handler
window.addEventListener('keydown', (e) => {
if (e.key.toLowerCase() === 'r' && gameManager.running && !gameManager.gameOver) {
resetPosition();
}
});
}
function buildMenu() {
const aircraftCards = document.getElementById('aircraft-cards');
for (let i = 0; i < aircraft_1.AIRCRAFT_LIST.length; i++) {
const ac = aircraft_1.AIRCRAFT_LIST[i];
const card = document.createElement('div');
card.className = 'ac-card' + (i === selectedAircraftIndex ? ' selected' : '');
card.innerHTML = `
<h3>${ac.name}</h3>
<p>${ac.description}</p>
<p style="margin-top:8px">
Speed: ${bar(ac.maxThrust / ac.mass, 15)}<br/>
Agility: ${bar(ac.rollAuthority * 20, 100)}<br/>
Stability: ${bar((1 - ac.cd0) * 100, 100)}
</p>
`;
card.addEventListener('click', () => {
selectedAircraftIndex = i;
updateSelection();
});
aircraftCards.appendChild(card);
}
const scenarioCards = document.getElementById('scenario-cards');
for (let i = 0; i < game_1.SCENARIOS.length; i++) {
const sc = game_1.SCENARIOS[i];
const card = document.createElement('div');
card.className = 'sc-card' + (i === selectedScenarioIndex ? ' selected' : '');
card.innerHTML = `
<h3>${sc.name}</h3>
<p>${sc.description}</p>
`;
card.addEventListener('click', () => {
selectedScenarioIndex = i;
updateSelection();
});
scenarioCards.appendChild(card);
}
}
function bar(value, max) {
const pct = Math.min(100, (value / max) * 100);
return `<div class="stat-bar"><div class="stat-fill" style="width:${pct}%"></div></div>`;
}
function updateSelection() {
const acCards = document.querySelectorAll('.ac-card');
acCards.forEach((c, i) => c.classList.toggle('selected', i === selectedAircraftIndex));
const scCards = document.querySelectorAll('.sc-card');
scCards.forEach((c, i) => c.classList.toggle('selected', i === selectedScenarioIndex));
}
function startGame() {
// Hide menu
document.getElementById('menu-overlay').style.display = 'none';
// Show HUD
document.getElementById('hud').style.display = 'block';
// Init audio on user gesture
audio.init();
audio.resume();
// Start game
const profile = aircraft_1.AIRCRAFT_LIST[selectedAircraftIndex];
const scenario = game_1.SCENARIOS[selectedScenarioIndex];
gameManager.start(profile, scenario);
if (scenario.type === 'takeoff') {
renderer.setupRunway();
}
// Start render loop
lastTime = performance.now();
gameLoop(performance.now());
}
let lastTime;
function gameLoop(timestamp) {
const dt = (timestamp - lastTime) / 1000;
lastTime = timestamp;
if (gameManager.running && !gameManager.gameOver) {
gameManager.update();
gameManager.updateHUD();
}
else if (gameManager.gameOver) {
// Show end screen after a delay
setTimeout(() => {
showEndScreen();
}, 3000);
gameManager.running = false;
}
animFrameId = requestAnimationFrame(gameLoop);
}
function resetPosition() {
if (gameManager.scenario.type === 'takeoff') {
gameManager.state.position.set(0, 12, -800);
gameManager.state.velocity.set(0, 0, 0);
gameManager.state.orientation.setFromAxisAngle(new THREE.Vector3(1, 0, 0), Math.PI);
gameManager.state.angularVelocity.set(0, 0, 0);
gameManager.state.throttle = 0.25;
}
else {
gameManager.state.position.set(0, gameManager.scenario.initialAltitude, 0);
gameManager.state.velocity.set(80, 5, 0);
gameManager.state.orientation.identity();
gameManager.state.angularVelocity.set(0, 0, 0);
gameManager.state.throttle = 0.6;
}
}
function showEndScreen() {
const overlay = document.getElementById('menu-overlay');
if (overlay) {
overlay.style.display = 'flex';
const content = document.getElementById('menu-content');
content.innerHTML = `
<h1>${gameManager.message}</h1>
<p class="subtitle" style="margin-top:20px">Flight Time: ${Math.round((performance.now() - lastTime) / 1000)}s</p>
<button id="restart-btn" style="background:linear-gradient(180deg,#0a6,#084);color:#fff;border:none;padding:15px 50px;font-size:20px;letter-spacing:6px;cursor:pointer;border-radius:5px;margin-top:30px;font-family:'Courier New',monospace">FLY AGAIN</button>
`;
document.getElementById('restart-btn').addEventListener('click', () => {
location.reload();
});
}
// Hide HUD
document.getElementById('hud').style.display = 'none';
}
// Start initialization
init();
};
// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/aircraft.ts ──
__mods["src/aircraft.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AIRCRAFT_LIST = exports.mig21 = exports.a10 = exports.f16 = void 0;
exports.getAircraftById = getAircraftById;
// F-16 Fighting Falcon - High performance fighter
exports.f16 = {
name: 'F-16 Fighting Falcon',
description: 'High-thrust multirole fighter. Excellent maneuverability and acceleration.',
color: 0x5a7a4a,
accentColor: 0xffaa00,
mass: 9200,
wingArea: 27.87,
referenceLength: 15.0,
cl0: 0.0,
cla: 6.5,
cd0: 0.015,
k: 0.04,
alphaStall: Math.PI / 9, // ~20 degrees
clMax: 2.0,
maxThrust: 128000,
minThrottle: 0.15,
engineResponseRate: 3.0,
inertiaX: 4500,
inertiaY: 65000,
inertiaZ: 68000,
pitchAuthority: 1.2,
rollAuthority: 1.8,
yawAuthority: 0.6,
modelScale: 1.0,
};
// A-10 Thunderbolt II - Close air support gunship
exports.a10 = {
name: 'A-10 Thunderbolt II',
description: 'Heavy gunship. Slow but extremely stable and forgiving at low speeds.',
color: 0x4a5a3a,
accentColor: 0xff6600,
mass: 10300,
wingArea: 47.5,
referenceLength: 16.2,
cl0: 0.0,
cla: 5.8,
cd0: 0.025,
k: 0.035,
alphaStall: Math.PI / 7, // ~25 degrees
clMax: 2.4,
maxThrust: 101000,
minThrottle: 0.2,
engineResponseRate: 2.0,
inertiaX: 6000,
inertiaY: 85000,
inertiaZ: 90000,
pitchAuthority: 0.8,
rollAuthority: 1.0,
yawAuthority: 0.4,
modelScale: 1.2,
};
// MiG-21 Fishbed - Lightweight interceptor
exports.mig21 = {
name: 'MiG-21 Fishbed',
description: 'Lightweight interceptor. Fast and agile but unforgiving at low speeds.',
color: 0x6a7a8a,
accentColor: 0xff3333,
mass: 5900,
wingArea: 23.1,
referenceLength: 14.5,
cl0: 0.0,
cla: 7.0,
cd0: 0.018,
k: 0.05,
alphaStall: Math.PI / 12, // ~15 degrees
clMax: 1.6,
maxThrust: 69700,
minThrottle: 0.25,
engineResponseRate: 4.0,
inertiaX: 2800,
inertiaY: 35000,
inertiaZ: 37000,
pitchAuthority: 1.5,
rollAuthority: 2.2,
yawAuthority: 0.8,
modelScale: 0.9,
};
exports.AIRCRAFT_LIST = [exports.f16, exports.a10, exports.mig21];
function getAircraftById(id) {
return exports.AIRCRAFT_LIST[id] || exports.f16;
}
};
// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAtmosphere = getAtmosphere;
exports.updatePhysics = updatePhysics;
exports.updateThrottle = updateThrottle;
exports.createInitialState = createInitialState;
const GRAVITY = 9.81; // m/s²
const SEA_LEVEL_DENSITY = 1.225; // kg/m³
const SCALE_HEIGHT = 8500; // meters (atmospheric scale height)
// Compute atmospheric properties at given altitude
function getAtmosphere(altitude) {
const h = Math.max(0, altitude);
const density = SEA_LEVEL_DENSITY * Math.exp(-h / SCALE_HEIGHT);
const temperature = 288.15 - 6.5e-3 * h; // ISA model (troposphere)
const pressure = 101325 * Math.exp(-h / SCALE_HEIGHT);
return { density, temperature: Math.max(temperature, 216.65), pressure };
}
// Compute lift force in body frame (positive Z is up in aircraft frame)
function computeLift(profile, state, atm) {
const vSq = state.velocity.lengthSq();
if (vSq < 0.01)
return 0;
let alpha = state.angleOfAttack;
// Clamp angle of attack for lift calculation
let cl = profile.cl0 + profile.cla * alpha;
if (Math.abs(alpha) > profile.alphaStall) {
// Post-stall: lift drops off
const stallFactor = Math.cos((alpha - Math.sign(alpha) * profile.alphaStall) * 2);
cl *= Math.max(0.3, stallFactor);
}
cl = Math.max(-profile.clMax * 0.5, Math.min(profile.clMax, cl));
return 0.5 * atm.density * vSq * profile.wingArea * cl;
}
// Compute drag force (opposite to velocity direction)
function computeDrag(profile, state, atm) {
const vSq = state.velocity.lengthSq();
if (vSq < 0.01)
return 0;
const v = Math.sqrt(vSq);
// Profile drag (parasite drag)
const cdProfile = profile.cd0 * (1 + 20 * Math.pow(state.angleOfAttack, 2));
// Induced drag (proportional to lift squared)
let alpha = state.angleOfAttack;
let cl = profile.cl0 + profile.cla * alpha;
if (Math.abs(alpha) > profile.alphaStall) {
const stallFactor = Math.cos((alpha - Math.sign(alpha) * profile.alphaStall) * 2);
cl *= Math.max(0.3, stallFactor);
}
cl = Math.max(-profile.clMax * 0.5, Math.min(profile.clMax, cl));
const cdInduced = profile.k * cl * cl;
const totalCd = cdProfile + cdInduced;
return 0.5 * atm.density * vSq * profile.wingArea * totalCd;
}
// Compute thrust force along aircraft forward axis
function computeThrust(profile, state) {
// Thrust decreases slightly with altitude (less air for combustion)
const altFactor = Math.max(0.5, Math.exp(-state.altitude / (SCALE_HEIGHT * 2)));
return profile.maxThrust * state.throttle * altFactor;
}
// Update aircraft physics for one timestep
function updatePhysics(profile, state, controls, dt, terrainAltitude) {
// Clamp dt to prevent instability
dt = Math.min(dt, 0.05);
const atm = getAtmosphere(state.altitude);
// --- Thrust ---
const thrustMag = computeThrust(profile, state);
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(state.orientation);
const thrustForce = forward.multiplyScalar(thrustMag);
// --- Gravity ---
const gravityForce = new THREE.Vector3(0, -GRAVITY * profile.mass, 0);
// --- Lift (perpendicular to velocity in the plane of symmetry) ---
const liftMag = computeLift(profile, state, atm);
let liftDir;
if (state.velocity.lengthSq() < 0.1) {
liftDir = new THREE.Vector3(0, 1, 0); // straight up when stationary
}
else {
const velNorm = state.velocity.clone().normalize();
const bodyUp = new THREE.Vector3(0, 1, 0).applyQuaternion(state.orientation);
// Lift is perpendicular to velocity, in the plane of symmetry
liftDir = bodyUp.clone().sub(velNorm.clone().multiplyScalar(bodyUp.dot(velNorm))).normalize();
if (liftDir.lengthSq() < 0.001)
liftDir.set(0, 1, 0);
}
const liftForce = liftDir.multiplyScalar(liftMag);
// --- Drag (opposite to velocity) ---
const dragMag = computeDrag(profile, state, atm);
let dragForce;
if (state.velocity.lengthSq() < 0.1) {
dragForce = new THREE.Vector3(0, 0, 0);
}
else {
dragForce = state.velocity.clone().normalize().multiplyScalar(-dragMag);
}
// --- Total force ---
const totalForce = thrustForce.add(liftForce).add(dragForce).add(gravityForce);
// --- Update velocity (F = ma) ---
const acceleration = totalForce.multiplyScalar(1 / profile.mass);
state.velocity.add(acceleration.multiplyScalar(dt));
// --- Ground collision ---
if (state.position.y <= terrainAltitude + 2) {
state.position.y = terrainAltitude + 2;
if (state.velocity.y < 0) {
const impactSpeed = Math.abs(state.velocity.y);
if (impactSpeed > 15) {
// Hard crash
state.velocity.set(0, 0, 0);
return { dragForce: dragMag };
}
// Bounce/dampen
state.velocity.y *= -0.3;
}
state.onGround = true;
// Ground friction
const groundFriction = new THREE.Vector3(-state.velocity.x * 2, 0, -state.velocity.z * 2);
state.velocity.add(groundFriction.multiplyScalar(dt));
}
else {
state.onGround = false;
}
// --- Update position ---
state.position.add(state.velocity.clone().multiplyScalar(dt));
// --- Angular dynamics (control surface response) ---
const airspeedFactor = Math.min(1, state.airspeed / 50);
const speedDamp = Math.max(0.2, airspeedFactor);
// Target angular rates from controls
const targetRollRate = controls.roll * profile.rollAuthority * speedDamp;
const targetPitchRate = -controls.pitch * profile.pitchAuthority * speedDamp;
const targetYawRate = controls.yaw * profile.yawAuthority * speedDamp;
// Smooth angular acceleration (inertia)
const rollAccel = (targetRollRate - state.angularVelocity.x) / Math.max(0.1, profile.inertiaX / 5000);
const pitchAccel = (targetPitchRate - state.angularVelocity.y) / Math.max(0.1, profile.inertiaY / 5000);
const yawAccel = (targetYawRate - state.angularVelocity.z) / Math.max(0.1, profile.inertiaZ / 5000);
// Damping on angular velocity
const damping = 0.92;
state.angularVelocity.x = state.angularVelocity.x * damping + rollAccel * dt;
state.angularVelocity.y = state.angularVelocity.y * damping + pitchAccel * dt;
state.angularVelocity.z = state.angularVelocity.z * damping + yawAccel * dt;
// --- Update orientation from angular velocity ---
const angVelMag = state.angularVelocity.length();
if (angVelMag > 0.001) {
const axis = state.angularVelocity.clone().normalize();
const angle = angVelMag * dt;
const deltaQuat = new THREE.Quaternion().setFromAxisAngle(axis, angle);
state.orientation.multiply(deltaQuat);
state.orientation.normalize();
}
// --- Compute derived quantities ---
const velNorm = state.velocity.clone().normalize();
const bodyForward = new THREE.Vector3(0, 0, -1).applyQuaternion(state.orientation);
// Angle of attack: angle between velocity vector and aircraft plane of symmetry
const cosAoA = Math.max(-1, Math.min(1, velNorm.dot(bodyForward)));
state.angleOfAttack = Math.acos(cosAoA) * (velNorm.y > bodyForward.y ? -1 : 1);
// Sideslip angle
const bodyRight = new THREE.Vector3(1, 0, 0).applyQuaternion(state.orientation);
state.sideslipAngle = Math.asin(Math.max(-1, Math.min(1, velNorm.dot(bodyRight))));
// Airspeed
state.airspeed = state.velocity.length();
// G-force (total acceleration / gravity)
const totalAccelMag = acceleration.length();
state.gForce = 1 + totalAccelMag / GRAVITY;
// Stall detection
state.isStalled = Math.abs(state.angleOfAttack) > profile.alphaStall && state.airspeed < 60;
return { dragForce: dragMag };
}
// Update throttle based on controls
function updateThrottle(state, controls, dt, maxRate) {
if (controls.throttleUp) {
state.throttle = Math.min(1.0, state.throttle + maxRate * dt);
}
else if (controls.throttleDown) {
state.throttle = Math.max(0.05, state.throttle - maxRate * dt);
}
}
// Initialize aircraft state
function createInitialState(position) {
return {
position: position.clone(),
velocity: new THREE.Vector3(0, 0, 0),
orientation: new THREE.Quaternion(),
angularVelocity: new THREE.Vector3(0, 0, 0),
throttle: 0.25,
angleOfAttack: 0,
sideslipAngle: 0,
airspeed: 0,
altitude: position.y,
gForce: 1,
isStalled: false,
onGround: true,
};
}
};
// ── module: src/terrain.ts ──
__mods["src/terrain.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TerrainGenerator = void 0;
exports.createTerrainMesh = createTerrainMesh;
exports.createWater = createWater;
exports.createRunway = createRunway;
// Simplex-like noise for terrain generation
class Noise {
constructor(seed = 42) {
this.perm = new Array(512);
const p = new Array(256);
for (let i = 0; i < 256; i++)
p[i] = i;
// Shuffle with seed
let s = seed;
for (let i = 255; i > 0; i--) {
s = (s * 16807 + 0) % 2147483647;
const j = s % (i + 1);
[p[i], p[j]] = [p[j], p[i]];
}
for (let i = 0; i < 512; i++)
this.perm[i] = p[i & 255];
this.grad3 = [
[1, 1, 0], [-1, 1, 0], [1, -1, 0], [-1, -1, 0],
[1, 0, 1], [-1, 0, 1], [1, 0, -1], [-1, 0, -1],
[0, 1, 1], [0, -1, 1], [0, 1, -1], [0, -1, -1]
];
}
dot3(g, x, y) {
return g[0] * x + g[1] * y;
}
noise2D(x, y) {
const F2 = 0.5 * (Math.sqrt(3) - 1);
const G2 = (3 - Math.sqrt(3)) / 6;
const s = (x + y) * F2;
const i = Math.floor(x + s);
const j = Math.floor(y + s);
const t = (i + j) * G2;
const X0 = i - t, Y0 = j - t;
const x0 = x - X0, y0 = y - Y0;
let i1, j1;
if (x0 > y0) {
i1 = 1;
j1 = 0;
}
else {
i1 = 0;
j1 = 1;
}
const x1 = x0 - i1 + G2, y1 = y0 - j1 + G2;
const x2 = x0 - 1 + 2 * G2, y2 = y0 - 1 + 2 * G2;
const ii = i & 255, jj = j & 255;
const gi0 = this.perm[ii + this.perm[jj]] % 12;
const gi1 = this.perm[ii + i1 + this.perm[jj + j1]] % 12;
const gi2 = this.perm[ii + 1 + this.perm[jj + 1]] % 12;
let n0, n1, n2;
let t0 = 0.5 - x0 * x0 - y0 * y0;
n0 = t0 < 0 ? 0 : (t0 *= t0, t0 * t0 * this.dot3(this.grad3[gi0], x0, y0));
let t1 = 0.5 - x1 * x1 - y1 * y1;
n1 = t1 < 0 ? 0 : (t1 *= t1, t1 * t1 * this.dot3(this.grad3[gi1], x1, y1));
let t2 = 0.5 - x2 * x2 - y2 * y2;
n2 = t2 < 0 ? 0 : (t2 *= t2, t2 * t2 * this.dot3(this.grad3[gi2], x2, y2));
return 70 * (n0 + n1 + n2);
}
fbm(x, y, octaves = 6) {
let val = 0, amp = 1, freq = 1, max = 0;
for (let i = 0; i < octaves; i++) {
val += this.noise2D(x * freq, y * freq) * amp;
max += amp;
amp *= 0.5;
freq *= 2;
}
return val / max;
}
}
class TerrainGenerator {
constructor(seed = 42) {
this.waterLevel = 10;
this.noise = new Noise(seed);
}
getHeight(x, z) {
const scale = 0.003;
let h = this.noise.fbm(x * scale, z * scale, 6);
// Normalize to 0-1 range roughly
h = (h + 1) / 2;
// Add ridges
let ridge = Math.abs(this.noise.fbm(x * scale * 2 + 5.3, z * scale * 2 + 3.7, 4));
ridge = (1 - ridge) ** 2;
h = h * 0.6 + ridge * 0.4;
// Scale to world units
const baseHeight = h * 500;
// Flatten near origin for runway area
const distFromOrigin = Math.sqrt(x * x + z * z);
if (distFromOrigin < 300) {
const flatFactor = distFromOrigin / 300;
return baseHeight * flatFactor + this.waterLevel;
}
// Add water level offset
return Math.max(this.waterLevel, baseHeight);
}
isWater(x, z) {
return this.getHeight(x, z) <= this.waterLevel + 1;
}
getTerrainColor(h, x, z) {
if (h < this.waterLevel + 2)
return [0.15, 0.3, 0.5]; // water
if (h < this.waterLevel + 5)
return [0.76, 0.7, 0.5]; // sand
const slope = Math.abs(this.noise.fbm(x * 0.01, z * 0.01, 3));
if (h > 350) {
// Snow caps
return [0.9, 0.92, 0.95];
}
if (h > 250) {
// Rocky
const r = 0.4 + slope * 0.1;
return [r, r - 0.05, r - 0.1];
}
if (slope > 0.3) {
// Rocky slopes
return [0.35, 0.32, 0.28];
}
// Grass with variation
const green = 0.3 + h / 1000;
return [0.2 + slope * 0.1, Math.min(0.6, green), 0.15];
}
}
exports.TerrainGenerator = TerrainGenerator;
// Build terrain mesh using a grid of quads with vertex colors
function createTerrainMesh(generator, size = 8000, segments = 256) {
const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
geometry.rotateX(-Math.PI / 2);
const positions = geometry.attributes.position;
if (!positions)
return null;
const colors = new Float32Array(positions.count * 3);
const vertexCount = positions.count;
for (let i = 0; i < vertexCount; i++) {
const x = positions.getX(i);
const z = positions.getZ(i);
const h = generator.getHeight(x, z);
positions.setY(i, h);
const [r, g, b] = generator.getTerrainColor(h, x, z);
colors[i * 3] = r;
colors[i * 3 + 1] = g;
colors[i * 3 + 2] = b;
}
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geometry.computeVertexNormals();
const material = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.85,
metalness: 0.1,
flatShading: false,
});
return new THREE.Mesh(geometry, material);
}
// Create water plane
function createWater(size = 8000) {
const geometry = new THREE.PlaneGeometry(size, size);
geometry.rotateX(-Math.PI / 2);
const material = new THREE.MeshStandardMaterial({
color: 0x1a5276,
transparent: true,
opacity: 0.7,
roughness: 0.1,
metalness: 0.3,
});
const water = new THREE.Mesh(geometry, material);
water.position.y = 10;
return water;
}
// Create runway for takeoff/landing scenario
function createRunway() {
const group = new THREE.Group();
// Runway surface
const rwGeo = new THREE.PlaneGeometry(60, 2000);
rwGeo.rotateX(-Math.PI / 2);
const rwMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.9 });
const runway = new THREE.Mesh(rwGeo, rwMat);
runway.position.y = 11;
group.add(runway);
// Runway markings (center line)
for (let i = -950; i < 950; i += 40) {
const markGeo = new THREE.PlaneGeometry(2, 20);
markGeo.rotateX(-Math.PI / 2);
const markMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
const mark = new THREE.Mesh(markGeo, markMat);
mark.position.set(0, 11.5, i);
group.add(mark);
}
// Edge lights
for (let z = -980; z <= 980; z += 20) {
for (const x of [-30, 30]) {
const lightGeo = new THREE.SphereGeometry(0.5, 6, 6);
const lightMat = new THREE.MeshStandardMaterial({
color: 0x00ff88,
emissive: 0x00ff88,
emissiveIntensity: 2,
});
const light = new THREE.Mesh(lightGeo, lightMat);
light.position.set(x, 11.5, z);
group.add(light);
}
}
return group;
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputManager = void 0;
class InputManager {
constructor() {
this.keys = new Set();
this.controls = { pitch: 0, roll: 0, yaw: 0, throttleUp: false, throttleDown: false };
window.addEventListener('keydown', (e) => this.onKeyDown(e));
window.addEventListener('keyup', (e) => this.onKeyUp(e));
}
onKeyDown(e) {
const key = e.key.toLowerCase();
this.keys.add(key);
if (['w', 's', 'a', 'd', 'q', 'e', 'shift', 'control'].includes(key)) {
e.preventDefault();
}
}
onKeyUp(e) {
this.keys.delete(e.key.toLowerCase());
}
update() {
const sensitivity = 1.0;
// Pitch (W/S)
let pitch = 0;
if (this.keys.has('w'))
pitch += sensitivity;
if (this.keys.has('s'))
pitch -= sensitivity;
this.controls.pitch = Math.max(-1, Math.min(1, pitch));
// Roll (A/D)
let roll = 0;
if (this.keys.has('a'))
roll += sensitivity;
if (this.keys.has('d'))
roll -= sensitivity;
this.controls.roll = Math.max(-1, Math.min(1, roll));
// Yaw (Q/E)
let yaw = 0;
if (this.keys.has('q'))
yaw += sensitivity;
if (this.keys.has('e'))
yaw -= sensitivity;
this.controls.yaw = Math.max(-1, Math.min(1, yaw));
// Throttle (Shift/Ctrl)
this.controls.throttleUp = this.keys.has('shift');
this.controls.throttleDown = this.keys.has('control') || this.keys.has('ctrl');
}
}
exports.InputManager = InputManager;
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioSystem = void 0;
class AudioSystem {
constructor() {
this.ctx = null;
this.masterGain = null;
this.engineOsc1 = null;
this.engineOsc2 = null;
this.engineGain = null;
this.windNoiseSource = null;
this.windGain = null;
this.windFilter = null;
this.initialized = false;
}
init() {
if (this.initialized)
return;
try {
this.ctx = new AudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.4;
this.masterGain.connect(this.ctx.destination);
// Engine sound: two oscillators for richness
this.engineOsc1 = this.ctx.createOscillator();
this.engineOsc1.type = 'sawtooth';
this.engineOsc1.frequency.value = 60;
this.engineOsc2 = this.ctx.createOscillator();
this.engineOsc2.type = 'square';
this.engineOsc2.frequency.value = 30;
this.engineGain = this.ctx.createGain();
this.engineGain.gain.value = 0.15;
const engineFilter = this.ctx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 400;
engineFilter.Q.value = 2;
this.engineOsc1.connect(engineFilter);
this.engineOsc2.connect(engineFilter);
engineFilter.connect(this.engineGain);
this.engineGain.connect(this.masterGain);
this.engineOsc1.start();
this.engineOsc2.start();
// Wind noise (filtered white noise)
const bufferSize = 2 * this.ctx.sampleRate;
const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
this.windNoiseSource = this.ctx.createBufferSource();
this.windNoiseSource.buffer = buffer;
this.windNoiseSource.loop = true;
this.windFilter = this.ctx.createBiquadFilter();
this.windFilter.type = 'bandpass';
this.windFilter.frequency.value = 800;
this.windFilter.Q.value = 0.5;
this.windGain = this.ctx.createGain();
this.windGain.gain.value = 0;
this.windNoiseSource.connect(this.windFilter);
this.windFilter.connect(this.windGain);
this.windGain.connect(this.masterGain);
this.windNoiseSource.start();
this.initialized = true;
}
catch (e) {
console.warn('Audio init failed:', e);
}
}
update(throttle, airspeed, altitude, isStalled) {
if (!this.initialized || !this.ctx)
return;
// Engine pitch and volume based on throttle
const baseFreq = 40 + throttle * 120;
const altFactor = Math.max(0.7, Math.exp(-altitude / 8500));
if (this.engineOsc1) {
this.engineOsc1.frequency.linearRampToValueAtTime(baseFreq * altFactor, this.ctx.currentTime + 0.05);
}
if (this.engineOsc2) {
this.engineOsc2.frequency.linearRampToValueAtTime(baseFreq * 0.5 * altFactor, this.ctx.currentTime + 0.05);
}
if (this.engineGain) {
const vol = 0.05 + throttle * 0.3;
this.engineGain.gain.linearRampToValueAtTime(vol, this.ctx.currentTime + 0.05);
}
// Wind noise based on airspeed and altitude
if (this.windGain) {
const windVol = Math.min(0.4, (airspeed / 200) * (1 + altitude / 5000));
this.windGain.gain.linearRampToValueAtTime(windVol, this.ctx.currentTime + 0.05);
}
if (this.windFilter) {
const windFreq = 400 + airspeed * 8;
this.windFilter.frequency.linearRampToValueAtTime(Math.min(3000, windFreq), this.ctx.currentTime + 0.05);
}
// Stall warning: add a pulsing tone
if (isStalled && this.engineOsc1) {
const t = this.ctx.currentTime;
this.engineOsc1.frequency.setValueAtTime(800, t);
this.engineOsc1.frequency.linearRampToValueAtTime(baseFreq * altFactor, t + 0.3);
}
}
resume() {
if (this.ctx && this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
}
exports.AudioSystem = AudioSystem;
};
// ── module: src/models.ts ──
__mods["src/models.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAircraftModel = buildAircraftModel;
exports.buildEnemyModel = buildEnemyModel;
exports.buildWaypointMarker = buildWaypointMarker;
// Build a 3D model of an aircraft from primitives
function buildAircraftModel(profile) {
const group = new THREE.Group();
const s = profile.modelScale;
const bodyMat = new THREE.MeshStandardMaterial({
color: profile.color,
roughness: 0.4,
metalness: 0.6,
});
const accentMat = new THREE.MeshStandardMaterial({
color: profile.accentColor,
emissive: profile.accentColor,
emissiveIntensity: 0.3,
roughness: 0.3,
metalness: 0.5,
});
const darkMat = new THREE.MeshStandardMaterial({
color: 0x222222,
roughness: 0.7,
metalness: 0.8,
});
// Fuselage (main body) - elongated cylinder
const fuselageGeo = new THREE.CylinderGeometry(0.3 * s, 0.25 * s, 4 * s, 8);
const fuselage = new THREE.Mesh(fuselageGeo, bodyMat);
fuselage.rotation.x = Math.PI / 2;
group.add(fuselage);
// Nose cone
const noseGeo = new THREE.ConeGeometry(0.3 * s, 1.5 * s, 8);
const nose = new THREE.Mesh(noseGeo, bodyMat);
nose.rotation.x = -Math.PI / 2;
nose.position.z = -2.75 * s;
group.add(nose);
// Cockpit canopy (glass)
const cockpitGeo = new THREE.SphereGeometry(0.35 * s, 8, 6);
const cockpitMat = new THREE.MeshStandardMaterial({
color: 0x44aaff,
roughness: 0.1,
metalness: 0.9,
transparent: true,
opacity: 0.6,
});
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(0, 0.25 * s, -0.8 * s);
group.add(cockpit);
// Wings (flat boxes)
const wingSpan = profile.wingArea > 30 ? 10 : 7;
const wingGeo = new THREE.BoxGeometry(wingSpan * s, 0.08 * s, 1.5 * s);
const wings = new THREE.Mesh(wingGeo, bodyMat);
wings.position.set(0, -0.05 * s, 0.2 * s);
group.add(wings);
// Wing tips (accent)
for (const side of [-1, 1]) {
const tipGeo = new THREE.BoxGeometry(0.3 * s, 0.1 * s, 0.5 * s);
const tip = new THREE.Mesh(tipGeo, accentMat);
tip.position.set(side * wingSpan * s / 2, -0.05 * s, 0.2 * s);
group.add(tip);
}
// Horizontal stabilizer (tail wings)
const hStabGeo = new THREE.BoxGeometry(3 * s, 0.06 * s, 1 * s);
const hStab = new THREE.Mesh(hStabGeo, bodyMat);
hStab.position.set(0, 0, 2 * s);
group.add(hStab);
// Vertical stabilizer (tail fin)
const vStabGeo = new THREE.BoxGeometry(0.06 * s, 1.5 * s, 1.2 * s);
const vStab = new THREE.Mesh(vStabGeo, bodyMat);
vStab.position.set(0, 0.7 * s, 1.8 * s);
group.add(vStab);
// Tail fin accent stripe
const finStripeGeo = new THREE.BoxGeometry(0.08 * s, 0.3 * s, 0.8 * s);
const finStripe = new THREE.Mesh(finStripeGeo, accentMat);
finStripe.position.set(0, 1 * s, 1.7 * s);
group.add(finStripe);
// Engine exhaust(s)
for (const xOff of profile.wingArea > 35 ? [-0.8 * s, 0.8 * s] : [0]) {
const exhaustGeo = new THREE.CylinderGeometry(0.12 * s, 0.15 * s, 0.6 * s, 8);
const exhaust = new THREE.Mesh(exhaustGeo, darkMat);
exhaust.rotation.x = Math.PI / 2;
exhaust.position.set(xOff, -0.1 * s, 2.3 * s);
group.add(exhaust);
// Exhaust glow
const glowGeo = new THREE.CylinderGeometry(0.08 * s, 0.05 * s, 0.3 * s, 6);
const glowMat = new THREE.MeshStandardMaterial({
color: 0xff4400,
emissive: 0xff2200,
emissiveIntensity: 2,
});
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.rotation.x = Math.PI / 2;
glow.position.set(xOff, -0.1 * s, 2.65 * s);
glow.name = 'exhaustGlow';
group.add(glow);
}
// Landing gear (simple)
for (const xOff of [-0.5 * s, 0.5 * s]) {
const gearGeo = new THREE.CylinderGeometry(0.03 * s, 0.03 * s, 0.4 * s, 4);
const gear = new THREE.Mesh(gearGeo, darkMat);
gear.position.set(xOff, -0.45 * s, -0.5 * s);
group.add(gear);
const wheelGeo = new THREE.CylinderGeometry(0.08 * s, 0.08 * s, 0.06 * s, 8);
const wheel = new THREE.Mesh(wheelGeo, darkMat);
wheel.rotation.z = Math.PI / 2;
wheel.position.set(xOff, -0.65 * s, -0.5 * s);
group.add(wheel);
}
// Nose gear
const noseGearGeo = new THREE.CylinderGeometry(0.02 * s, 0.02 * s, 0.3 * s, 4);
const noseGear = new THREE.Mesh(noseGearGeo, darkMat);
noseGear.position.set(0, -0.35 * s, -1.8 * s);
group.add(noseGear);
return group;
}
// Build enemy aircraft model (simpler)
function buildEnemyModel(color) {
const group = new THREE.Group();
const mat = new THREE.MeshStandardMaterial({ color, roughness: 0.5, metalness: 0.4 });
const darkMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.7, metalness: 0.6 });
// Body
const bodyGeo = new THREE.CylinderGeometry(0.25, 0.2, 3.5, 8);
const body = new THREE.Mesh(bodyGeo, mat);
body.rotation.x = Math.PI / 2;
group.add(body);
// Nose
const noseGeo = new THREE.ConeGeometry(0.25, 1.2, 8);
const nose = new THREE.Mesh(noseGeo, mat);
nose.rotation.x = -Math.PI / 2;
nose.position.z = -2.35;
group.add(nose);
// Wings
const wingGeo = new THREE.BoxGeometry(7, 0.06, 1.2);
const wings = new THREE.Mesh(wingGeo, mat);
wings.position.set(0, -0.04, 0.15);
group.add(wings);
// Tail
const tailGeo = new THREE.BoxGeometry(2.5, 0.05, 0.8);
const tail = new THREE.Mesh(tailGeo, mat);
tail.position.set(0, 0, 1.7);
group.add(tail);
// Vertical stabilizer
const vStabGeo = new THREE.BoxGeometry(0.05, 1.2, 1);
const vStab = new THREE.Mesh(vStabGeo, mat);
vStab.position.set(0, 0.6, 1.5);
group.add(vStab);
// Exhaust glow
const glowGeo = new THREE.CylinderGeometry(0.07, 0.04, 0.25, 6);
const glowMat = new THREE.MeshStandardMaterial({ color: 0xff3300, emissive: 0xff1100, emissiveIntensity: 2 });
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.rotation.x = Math.PI / 2;
glow.position.set(0, -0.08, 2.35);
group.add(glow);
return group;
}
// Build target waypoint marker
function buildWaypointMarker() {
const group = new THREE.Group();
// Ring
const ringGeo = new THREE.TorusGeometry(3, 0.2, 8, 16);
const ringMat = new THREE.MeshStandardMaterial({ color: 0x00ff88, emissive: 0x00ff44, emissiveIntensity: 1 });
const ring = new THREE.Mesh(ringGeo, ringMat);
group.add(ring);
// Inner glow sphere
const innerGeo = new THREE.SphereGeometry(1.5, 8, 8);
const innerMat = new THREE.MeshStandardMaterial({ color: 0x00ff88, emissive: 0x00ff44, emissiveIntensity: 0.5, transparent: true, opacity: 0.3 });
const inner = new THREE.Mesh(innerGeo, innerMat);
group.add(inner);
return group;
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const terrain_1 = require("./terrain");
const models_1 = require("./models");
class Renderer {
constructor() {
this.exhaustGlow = null;
// Scene with fog for depth
this.scene = new THREE.Scene();
const skyColor = 0x6db3f2;
this.scene.background = new THREE.Color(skyColor);
this.scene.fog = new THREE.FogExp2(skyColor, 0.00012);
// Camera
this.camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 1, 20000);
// Renderer
this.renderer = new THREE.WebGLRenderer({ antialias: true });
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.setClearColor(skyColor);
document.getElementById('canvas-container').appendChild(this.renderer.domElement);
// Lighting
const ambientLight = new THREE.AmbientLight(0x6688aa, 0.5);
this.scene.add(ambientLight);
const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x445522, 0.6);
this.scene.add(hemiLight);
// Sun (directional light)
this.sunLight = new THREE.DirectionalLight(0xffffee, 1.2);
this.sunLight.position.set(500, 800, 300);
this.scene.add(this.sunLight);
// Clouds
this.createClouds();
// Terrain generator
this.terrainGenerator = new terrain_1.TerrainGenerator(Math.floor(Math.random() * 10000));
// Aircraft mesh (placeholder)
this.aircraftMesh = new THREE.Group();
}
setupAircraft(profile) {
const model = (0, models_1.buildAircraftModel)(profile);
while (this.aircraftMesh.children.length > 0) {
this.aircraftMesh.remove(this.aircraftMesh.children[0]);
}
// Copy children from model to aircraft mesh
for (const child of model.children) {
this.aircraftMesh.add(child);
}
// Find exhaust glow for animation
for (const child of this.aircraftMesh.children) {
if (child.name === 'exhaustGlow') {
this.exhaustGlow = child;
break;
}
}
this.scene.add(this.aircraftMesh);
}
setupTerrain() {
const terrain = (0, terrain_1.createTerrainMesh)(this.terrainGenerator, 12000, 300);
if (terrain)
this.scene.add(terrain);
const water = (0, terrain_1.createWater)(12000);
this.scene.add(water);
}
setupRunway() {
const runway = (0, terrain_1.createRunway)();
this.scene.add(runway);
}
setupEnemies(count) {
// Enemies are set up by the game manager
}
getTerrainHeight(x, z) {
return this.terrainGenerator.getHeight(x, z);
}
updateCamera(state, dt) {
// Chase camera with smooth follow
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(state.orientation);
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(state.orientation);
// Camera offset behind and above aircraft
const camDist = 25;
const camHeight = 8;
const targetPos = state.position.clone()
.sub(forward.multiplyScalar(camDist))
.add(up.multiplyScalar(camHeight));
// Smooth camera movement
this.camera.position.lerp(targetPos, Math.min(1, dt * 4));
// Look slightly ahead of aircraft
const lookTarget = state.position.clone().add(forward.multiplyScalar(20));
this.camera.lookAt(lookTarget);
}
updateAircraftVisual(state) {
this.aircraftMesh.position.copy(state.position);
this.aircraftMesh.quaternion.copy(state.orientation);
// Animate exhaust glow based on throttle
if (this.exhaustGlow) {
const intensity = state.throttle * 3;
this.exhaustGlow.material.emissiveIntensity = Math.max(0.5, intensity);
const scale = 0.8 + state.throttle * 0.6;
this.exhaustGlow.scale.set(scale, scale, scale);
}
}
updateSun(state) {
// Move sun relative to aircraft for consistent lighting
this.sunLight.position.copy(state.position).add(new THREE.Vector3(500, 800, 300));
}
render() {
this.renderer.render(this.scene, this.camera);
}
createClouds() {
const cloudMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.6,
roughness: 1,
metalness: 0,
});
for (let i = 0; i < 80; i++) {
const cloudGroup = new THREE.Group();
const numPuffs = 3 + Math.floor(Math.random() * 4);
for (let j = 0; j < numPuffs; j++) {
const puffGeo = new THREE.SphereGeometry(20 + Math.random() * 40, 7, 5);
const puff = new THREE.Mesh(puffGeo, cloudMat);
puff.position.set((Math.random() - 0.5) * 60, (Math.random() - 0.5) * 10, (Math.random() - 0.5) * 40);
puff.scale.y = 0.4;
cloudGroup.add(puff);
}
cloudGroup.position.set((Math.random() - 0.5) * 10000, 300 + Math.random() * 600, (Math.random() - 0.5) * 10000);
this.scene.add(cloudGroup);
}
}
resize() {
this.camera.aspect = window.innerWidth / window.innerHeight;
this.renderer.setSize(window.innerWidth, window.innerHeight);
}
}
exports.Renderer = Renderer;
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameManager = exports.SCENARIOS = void 0;
const Physics = __importStar(require("./physics"));
const models_1 = require("./models");
exports.SCENARIOS = [
{
type: 'takeoff',
name: 'Takeoff & Landing',
description: 'Master the art of precision flight. Take off from the runway and land safely.',
initialAltitude: 12,
terrainScale: 1.0,
},
{
type: 'dogfight',
name: 'Dogfight',
description: 'Engage enemy aircraft in high-speed aerial combat. Destroy all hostiles!',
initialAltitude: 500,
terrainScale: 1.2,
},
];
class GameManager {
constructor(renderer, input, audio) {
this.enemies = [];
this.waypoints = [];
this.running = false;
this.gameOver = false;
this.message = '';
this.lastTime = 0;
this.renderer = renderer;
this.input = input;
this.audio = audio;
this.state = Physics.createInitialState(new THREE.Vector3(0, 12, 0));
this.profile = null; // Set later
this.scenario = exports.SCENARIOS[0];
}
start(profile, scenario) {
this.profile = profile;
this.scenario = scenario;
this.renderer.setupAircraft(profile);
if (scenario.type === 'takeoff') {
// Get terrain height at runway position and place aircraft above it
const rwAlt = this.renderer.getTerrainHeight(0, -900);
this.state = Physics.createInitialState(new THREE.Vector3(0, rwAlt + 20, -900));
// Orient aircraft pointing toward +Z (forward along runway)
// In Three.js, -Z is forward by default; we want the nose to point +Z
const euler = new THREE.Euler(0, Math.PI, 0);
this.state.orientation.setFromEuler(euler);
this.state.onGround = true;
}
else {
// Dogfight: start in the air with some speed
this.state = Physics.createInitialState(new THREE.Vector3(0, scenario.initialAltitude, 0));
this.state.velocity.set(80, 5, 0); // Initial forward speed
this.state.throttle = 0.6;
}
this.enemies = [];
if (scenario.type === 'dogfight') {
this.spawnEnemies(4);
}
this.running = true;
this.gameOver = false;
this.message = '';
this.lastTime = performance.now();
}
spawnEnemies(count) {
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const dist = 500 + Math.random() * 300;
const pos = new THREE.Vector3(this.state.position.x + Math.cos(angle) * dist, 200 + Math.random() * 400, this.state.position.z + Math.sin(angle) * dist);
const model = (0, models_1.buildEnemyModel)(0xcc3333);
model.position.copy(pos);
this.renderer.scene.add(model);
this.enemies.push({
position: pos.clone(),
velocity: new THREE.Vector3((Math.random() - 0.5) * 60, 0, (Math.random() - 0.5) * 60),
orientation: new THREE.Quaternion(),
health: 100,
alive: true,
mesh: model,
targetPosition: pos.clone(),
});
}
// Add waypoints for dogfight objectives
for (let i = 0; i < 3; i++) {
const angle = (i / 3) * Math.PI * 2 + Math.PI / 6;
const dist = 800;
const wpPos = new THREE.Vector3(this.state.position.x + Math.cos(angle) * dist, 300 + Math.random() * 200, this.state.position.z + Math.sin(angle) * dist);
const marker = (0, models_1.buildWaypointMarker)();
marker.position.copy(wpPos);
this.renderer.scene.add(marker);
this.waypoints.push(marker);
}
}
update() {
if (!this.running || this.gameOver)
return;
const now = performance.now();
let dt = (now - this.lastTime) / 1000;
this.lastTime = now;
dt = Math.min(dt, 0.05); // Cap delta time
// Update input
this.input.update();
// Update throttle
Physics.updateThrottle(this.state, this.input.controls, dt, this.profile.engineResponseRate);
// Get terrain altitude at current position
const terrainAlt = this.renderer.getTerrainHeight(this.state.position.x, this.state.position.z);
this.state.altitude = Math.max(0, this.state.position.y - terrainAlt);
// Update physics
const { dragForce } = Physics.updatePhysics(this.profile, this.state, this.input.controls, dt, terrainAlt);
// Check crash conditions (only after aircraft has been airborne for a bit)
if (!this.state.onGround && this.state.altitude < 2 && this.state.airspeed > 10) {
// Crashed into ground at speed
this.gameOver = true;
this.message = 'CRASHED!';
return;
}
// Update aircraft visual
this.renderer.updateAircraftVisual(this.state);
// Update camera
this.renderer.updateCamera(this.state, dt);
// Update sun position
this.renderer.updateSun(this.state);
// Update enemies
this.updateEnemies(dt);
// Check dogfight objectives
if (this.scenario.type === 'dogfight') {
this.checkDogfightObjectives();
}
// Check takeoff/landing objectives
if (this.scenario.type === 'takeoff') {
this.checkTakeoffObjectives();
}
// Update audio
this.audio.update(this.state.throttle, this.state.airspeed, this.state.altitude, this.state.isStalled);
// Render
this.renderer.render();
}
updateEnemies(dt) {
for (const enemy of this.enemies) {
if (!enemy.alive)
continue;
// Simple AI: fly toward player with some randomness
const toPlayer = new THREE.Vector3().subVectors(this.state.position, enemy.position);
const distToPlayer = toPlayer.length();
// Change target periodically
if (Math.random() < 0.01) {
enemy.targetPosition.set(this.state.position.x + (Math.random() - 0.5) * 600, Math.max(100, this.state.position.y + (Math.random() - 0.5) * 200), this.state.position.z + (Math.random() - 0.5) * 600);
}
// Move toward target
const toTarget = new THREE.Vector3().subVectors(enemy.targetPosition, enemy.position);
if (toTarget.lengthSq() < 100) {
enemy.targetPosition.set(this.state.position.x + (Math.random() - 0.5) * 800, Math.max(100, 200 + Math.random() * 400), this.state.position.z + (Math.random() - 0.5) * 800);
}
const dir = toTarget.normalize();
enemy.velocity.lerp(dir.multiplyScalar(70), dt * 2);
// Keep above terrain
const terrAlt = this.renderer.getTerrainHeight(enemy.position.x, enemy.position.z);
if (enemy.position.y < terrAlt + 50) {
enemy.velocity.y += 10;
}
enemy.position.add(enemy.velocity.clone().multiplyScalar(dt));
// Orient toward velocity direction
if (enemy.velocity.lengthSq() > 1) {
const velDir = enemy.velocity.clone().normalize();
const forward = new THREE.Vector3(0, 0, -1);
const quat = new THREE.Quaternion().setFromUnitVectors(forward, velDir);
enemy.orientation.copy(quat);
}
// Update mesh position
if (enemy.mesh) {
enemy.mesh.position.copy(enemy.position);
enemy.mesh.quaternion.copy(enemy.orientation);
}
// Collision with player
if (distToPlayer < 15) {
enemy.health -= dt * 30;
if (enemy.health <= 0) {
enemy.alive = false;
if (enemy.mesh)
this.renderer.scene.remove(enemy.mesh);
}
}
// Enemy shoots at player (proximity damage)
if (distToPlayer < 50 && Math.random() < dt * 0.3) {
this.message = 'UNDER FIRE!';
setTimeout(() => { this.message = ''; }, 1000);
}
}
}
checkDogfightObjectives() {
const aliveEnemies = this.enemies.filter(e => e.alive).length;
if (aliveEnemies === 0) {
this.gameOver = true;
this.message = 'VICTORY! All enemies destroyed!';
}
// Check waypoint proximity for bonus
for (const wp of this.waypoints) {
const dist = this.state.position.distanceTo(wp.position);
if (dist < 20) {
this.renderer.scene.remove(wp);
this.message = 'WAYPOINT CLEARED!';
setTimeout(() => { this.message = ''; }, 1500);
}
}
}
checkTakeoffObjectives() {
// Success: reach altitude of 200m+ and maintain for a bit
if (this.state.altitude > 200 && this.state.airspeed > 40) {
this.message = 'SUCCESSFULLY AIRBORNE!';
setTimeout(() => { this.message = ''; }, 3000);
}
// Landing: touch down gently on runway area
if (this.state.onGround && Math.abs(this.state.position.x) < 40 && Math.abs(this.state.position.z) < 1000) {
const verticalSpeed = Math.abs(this.state.velocity.y);
if (verticalSpeed < 5) {
this.gameOver = true;
this.message = 'PERFECT LANDING!';
}
else {
this.gameOver = true;
this.message = 'HARDED LANDING - SIM ENDED';
}
}
}
updateHUD() {
const kts = Math.round(this.state.airspeed * 1.94384); // m/s to knots
const altFt = Math.round(this.state.altitude * 3.28084); // meters to feet
const vspeedFpm = Math.round(this.state.velocity.y * 196.85); // m/s to ft/min
// Get heading from orientation
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(this.state.orientation);
let heading = Math.atan2(forward.x, forward.z) * 180 / Math.PI;
if (heading < 0)
heading += 360;
// Get pitch and roll from orientation
const euler = new THREE.Euler().setFromQuaternion(this.state.orientation);
const pitchDeg = -euler.x * 180 / Math.PI;
const rollDeg = euler.z * 180 / Math.PI;
document.getElementById('hud-airspeed').textContent = kts.toString();
document.getElementById('hud-altitude').textContent = altFt.toString();
document.getElementById('hud-heading').textContent = heading.toFixed(0).padStart(3, '0');
document.getElementById('hud-vspeed').textContent = vspeedFpm.toString();
document.getElementById('hud-throttle').textContent = Math.round(this.state.throttle * 100).toString();
document.getElementById('hud-gforce').textContent = this.state.gForce.toFixed(1);
document.getElementById('hud-pitch').textContent = pitchDeg.toFixed(1);
document.getElementById('hud-roll').textContent = rollDeg.toFixed(1);
document.getElementById('hud-aoa').textContent = (this.state.angleOfAttack * 180 / Math.PI).toFixed(1);
document.getElementById('hud-drag').textContent = Math.round(this.state.airspeed > 0 ?
0.5 * 1.225 * this.state.airspeed ** 2 * this.profile.wingArea * this.profile.cd0 : 0).toString();
// Stall warning
if (this.state.isStalled) {
document.getElementById('hud-center').style.borderColor = 'rgba(255,0,0,0.8)';
document.getElementById('hud-center').innerHTML = '<span style="color:#f00;font-size:14px">STALL</span>';
}
else {
document.getElementById('hud-center').style.borderColor = 'rgba(0,255,0,0.3)';
document.getElementById('hud-center').innerHTML = '';
}
// Message display
const msgEl = document.getElementById('message-overlay');
if (msgEl) {
if (this.message) {
msgEl.style.display = 'block';
document.getElementById('message-text').textContent = this.message;
}
else {
msgEl.style.display = 'none';
}
}
}
stop() {
this.running = false;
}
}
exports.GameManager = GameManager;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>