Flight Simulator
Qwen 3.6 27b · typescript
◈Preview unavailable — try playing the result
Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.
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 91,131 bytes · SHA-256 309ca5686baf
<!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.160.0/build/three.min.js"></script>
<style>
</style>
</head>
<body>
<div id="app" style="position:relative;width:100vw;height:100vh;overflow:hidden;">
<canvas id="gameCanvas" style="display:block;width:100%;height:100%;"></canvas>
<canvas id="hudCanvas" style="position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;"></canvas>
<div id="loadingOverlay" style="position:absolute;top:0;left:0;width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:#0a0a2e;color:#88aacc;font-family:Arial;font-size:24px;z-index:100;">Loading Apex Aero...</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./aircraft":"src/aircraft.ts","./terrain":"src/terrain.ts","./audio":"src/audio.ts","./input":"src/input.ts","./hud":"src/hud.ts","./game":"src/game.ts","./noise":"src/noise.ts","./atmosphere":"src/atmosphere.ts"},"src/noise.ts":{"./types":"src/types.ts"},"src/physics.ts":{"./types":"src/types.ts"},"src/aircraft.ts":{"./types":"src/types.ts","./noise":"src/noise.ts"},"src/terrain.ts":{"./noise":"src/noise.ts","./types":"src/types.ts"},"src/hud.ts":{"./types":"src/types.ts"},"src/game.ts":{"./types":"src/types.ts","./noise":"src/noise.ts","./physics":"src/physics.ts","./input":"src/input.ts","./audio":"src/audio.ts"},"src/atmosphere.ts":{"./noise":"src/noise.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 });
// Apex Aero - Flight Simulator Entry Point
const types_1 = require("./types");
const aircraft_1 = require("./aircraft");
const terrain_1 = require("./terrain");
const audio_1 = require("./audio");
const input_1 = require("./input");
const hud_1 = require("./hud");
const game_1 = require("./game");
const atmosphere_1 = require("./atmosphere");
let renderer;
let scene;
let camera;
let clock;
let hud;
let hudCanvas;
let terrainManager;
let audioSystem;
let gameManager;
let input;
let aircraftGroup;
let aircraftModel;
let playerState;
let selectedAircraft = 'fighter';
let cameraMode = 'chase';
let targetMeshes = [];
let particleMeshes = [];
let ambientLight;
let dirLight;
let skyDome;
let clouds;
function init() {
// Get existing DOM elements
const canvas = document.getElementById('gameCanvas');
hudCanvas = document.getElementById('hudCanvas');
const loadingOverlay = document.getElementById('loadingOverlay');
if (!canvas || !hudCanvas)
return;
// Three.js setup
renderer = new THREE.WebGLRenderer({
canvas,
antialias: true,
powerPreference: 'high-performance',
});
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.FogExp2(0x87CEEB, 0.00015);
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 50000);
camera.position.set(0, 50, 50);
clock = new THREE.Clock();
// Lighting
ambientLight = new THREE.AmbientLight(0x404060, 0.6);
scene.add(ambientLight);
dirLight = new THREE.DirectionalLight(0xfff5e0, 1.5);
dirLight.position.set(500, 1000, 500);
dirLight.castShadow = true;
dirLight.shadow.mapSize.width = 2048;
dirLight.shadow.mapSize.height = 2048;
dirLight.shadow.camera.near = 10;
dirLight.shadow.camera.far = 5000;
dirLight.shadow.camera.left = -1000;
dirLight.shadow.camera.right = 1000;
dirLight.shadow.camera.top = 1000;
dirLight.shadow.camera.bottom = -1000;
scene.add(dirLight);
const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x4a7c3f, 0.4);
scene.add(hemiLight);
// Terrain
terrainManager = new terrain_1.TerrainManager(scene);
// Water
(0, terrain_1.createWater)(scene);
// Atmosphere
skyDome = (0, atmosphere_1.createSkyDome)(scene);
clouds = (0, atmosphere_1.createClouds)(scene);
(0, atmosphere_1.createSun)(scene);
// Audio
audioSystem = new audio_1.AudioSystem();
// HUD
hud = new hud_1.HUD(hudCanvas);
hud.resize(window.innerWidth, window.innerHeight);
// Input
input = (0, input_1.createInputState)();
(0, input_1.setupInput)(input, canvas);
// Game manager
gameManager = new game_1.GameManager(scene, audioSystem);
// Resize handler
window.addEventListener('resize', onResize);
// Hide loading overlay
if (loadingOverlay)
loadingOverlay.style.display = 'none';
// Start render loop
animate();
}
function onResize() {
const w = window.innerWidth;
const h = window.innerHeight;
camera.aspect = w / h;
camera.updateProjectionMatrix();
renderer.setSize(w, h);
hud.resize(w, h);
}
function loadGameMode() {
// Cleanup previous mode
if (aircraftGroup) {
scene.remove(aircraftGroup);
}
targetMeshes.forEach(m => scene.remove(m));
targetMeshes = [];
particleMeshes.forEach(m => scene.remove(m));
particleMeshes = [];
selectedAircraft = gameManager.selectedAircraftType;
const profile = aircraft_1.AIRCRAFT_PROFILES[selectedAircraft];
// Create aircraft
aircraftModel = (0, aircraft_1.buildAircraftModel)(selectedAircraft);
aircraftGroup = new THREE.Group();
aircraftGroup.add(aircraftModel);
// Starting position
const startPos = new THREE.Vector3(0, 15, 0);
playerState = (0, aircraft_1.createAircraftState)(startPos);
aircraftGroup.position.copy(startPos);
scene.add(aircraftGroup);
// Mode-specific setup
if (gameManager.state === types_1.GameState.TakeoffLanding) {
(0, terrain_1.createRunway)(scene, new THREE.Vector3(-200, 0, 0), new THREE.Vector3(800, 0, 0), 40, 1000);
}
if (gameManager.state === types_1.GameState.Dogfight) {
const ds = gameManager.dogfight;
if (ds) {
ds.targets.forEach((target, i) => {
const mesh = (0, terrain_1.createTargetDrone)(scene, target.position);
targetMeshes.push(mesh);
target.mesh = mesh;
});
}
}
// Init audio on first interaction
audioSystem.init();
}
function updateCamera(dt) {
if (!playerState || !aircraftGroup)
return;
const pos = aircraftGroup.position;
if (cameraMode === 'chase') {
// Chase camera
const camDist = 30;
const camHeight = 10;
// Get aircraft forward direction
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(playerState.orientation);
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(playerState.orientation);
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(playerState.orientation);
// Camera offset behind and above aircraft
const camOffset = new THREE.Vector3()
.addVectors(forward.clone().multiplyScalar(-camDist), up.clone().multiplyScalar(camHeight));
// Apply mouse camera rotation
const euler = new THREE.Euler(input.cameraPitch, input.cameraYaw, 0, 'YXZ');
const q = new THREE.Quaternion().setFromEuler(euler);
camOffset.applyQuaternion(q);
const camPos = new THREE.Vector3().addVectors(pos, camOffset);
camera.position.lerp(camPos, Math.min(1, dt * 5));
// Look at aircraft with slight offset
const lookTarget = new THREE.Vector3()
.addVectors(pos, forward.clone().multiplyScalar(20));
camera.lookAt(lookTarget);
}
else {
// Cockpit camera
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(playerState.orientation);
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(playerState.orientation);
const camPos = new THREE.Vector3().copy(pos);
camPos.add(forward.clone().multiplyScalar(1));
camPos.add(up.clone().multiplyScalar(0.5));
camera.position.copy(camPos);
const lookTarget = new THREE.Vector3().addVectors(pos, forward.clone().multiplyScalar(100));
camera.lookAt(lookTarget);
}
}
function updateParticles() {
// Remove old particle meshes
particleMeshes.forEach(m => scene.remove(m));
particleMeshes = [];
const particles = gameManager.getParticles();
// Create particle system
if (particles.length === 0)
return;
// Use individual meshes for simplicity
const maxParticles = Math.min(particles.length, 200);
for (let i = 0; i < maxParticles; i++) {
const p = particles[i];
if (p.life > p.maxLife)
continue;
const geo = new THREE.SphereGeometry(p.size * 0.3, 4, 4);
const lifeRatio = 1 - p.life / p.maxLife;
const mat = new THREE.MeshBasicMaterial({
color: p.color,
transparent: true,
opacity: lifeRatio * 0.6,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(p.position);
scene.add(mesh);
particleMeshes.push(mesh);
}
}
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05);
const time = clock.getElapsedTime();
// Process input
const controls = (0, input_1.processInput)(input, dt);
// Handle mode transitions
const prevState = gameManager.state;
if (gameManager.state !== prevState) {
loadGameMode();
}
if (gameManager.state === types_1.GameState.Menu) {
// Slow camera orbit in menu
const orbitSpeed = 0.1;
camera.position.x = Math.sin(time * orbitSpeed) * 200;
camera.position.z = Math.cos(time * orbitSpeed) * 200;
camera.position.y = 100 + Math.sin(time * 0.15) * 20;
camera.lookAt(0, 50, 0);
// Update terrain
terrainManager.update(camera.position);
// Update sky dome
if (skyDome)
(0, atmosphere_1.updateSkyDome)(skyDome, camera.position);
// Render HUD
const profile = aircraft_1.AIRCRAFT_PROFILES[selectedAircraft];
hud.render(playerState || (0, aircraft_1.createAircraftState)(new THREE.Vector3(0, 15, 0)), profile, gameManager.state, gameManager.dogfight, time, selectedAircraft);
renderer.render(scene, camera);
return;
}
if (!playerState || !aircraftGroup) {
renderer.render(scene, camera);
return;
}
// Update game
const profile = aircraft_1.AIRCRAFT_PROFILES[selectedAircraft];
gameManager.update(playerState, profile, input, dt, time);
// Apply controls to aircraft state
playerState.pitchInput = controls.pitch;
playerState.rollInput = controls.roll;
playerState.yawInput = controls.yaw;
playerState.throttle = Math.max(0, Math.min(1, playerState.throttle + controls.throttleDelta));
// Camera mode toggle
if (input.keyT) {
cameraMode = cameraMode === 'chase' ? 'cockpit' : 'chase';
input.keyT = false; // Debounce
}
// Update aircraft visual position
aircraftGroup.position.copy(playerState.position);
aircraftGroup.quaternion.copy(playerState.orientation);
// Update aircraft visuals (engine glow, propeller)
(0, aircraft_1.updateAircraftVisuals)(aircraftModel, playerState.throttle, playerState.airspeed, selectedAircraft, time);
// Update camera
updateCamera(dt);
// Update terrain chunks
terrainManager.update(playerState.position);
// Update target positions (dogfight)
if (gameManager.state === types_1.GameState.Dogfight && gameManager.dogfight) {
gameManager.dogfight.targets.forEach((target, i) => {
if (targetMeshes[i]) {
targetMeshes[i].position.copy(target.position);
targetMeshes[i].quaternion.copy(target.orientation);
}
});
}
// Update particles
updateParticles();
// Update shadow light to follow aircraft
dirLight.position.set(playerState.position.x + 500, playerState.position.y + 1000, playerState.position.z + 500);
// Render HUD
hud.render(playerState, profile, gameManager.state, gameManager.dogfight, time, selectedAircraft);
// Update sky dome position
if (skyDome)
(0, atmosphere_1.updateSkyDome)(skyDome, camera.position);
// Render scene
renderer.render(scene, camera);
}
// Start the game
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";
// Core type definitions for Apex Aero Flight Simulator
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameState = exports.BiomeType = void 0;
var BiomeType;
(function (BiomeType) {
BiomeType[BiomeType["Water"] = 0] = "Water";
BiomeType[BiomeType["Beach"] = 1] = "Beach";
BiomeType[BiomeType["Grassland"] = 2] = "Grassland";
BiomeType[BiomeType["Forest"] = 3] = "Forest";
BiomeType[BiomeType["Mountain"] = 4] = "Mountain";
BiomeType[BiomeType["Snow"] = 5] = "Snow";
})(BiomeType || (exports.BiomeType = BiomeType = {}));
var GameState;
(function (GameState) {
GameState[GameState["Menu"] = 0] = "Menu";
GameState[GameState["TakeoffLanding"] = 1] = "TakeoffLanding";
GameState[GameState["Dogfight"] = 2] = "Dogfight";
GameState[GameState["Crashed"] = 3] = "Crashed";
})(GameState || (exports.GameState = GameState = {}));
};
// ── module: src/noise.ts ──
__mods["src/noise.ts"] = function (exports, require, module) {
"use strict";
// Simplex-like noise implementation for terrain generation
Object.defineProperty(exports, "__esModule", { value: true });
exports.noise3D = noise3D;
exports.fbm = fbm;
exports.ridgedNoise = ridgedNoise;
exports.terrainHeight = terrainHeight;
exports.getBiome = getBiome;
const F2 = 0.5 * (Math.sqrt(3) - 1);
const G2 = (3 - Math.sqrt(3)) / 6;
const F3 = 1 / 3;
const G3 = 1 / 6;
const 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]
];
// Permutation table
const perm = new Uint8Array(512);
(function initPerm(seed) {
const p = new Uint8Array(256);
for (let i = 0; i < 256; i++)
p[i] = i;
let s = seed || 42;
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++)
perm[i] = p[i & 255];
})(12345);
function dot3(g, x, y, z) {
return g[0] * x + g[1] * y + g[2] * z;
}
function noise3D(x, y, z) {
const s = (x + y + z) * F3;
const i = Math.floor(x + s);
const j = Math.floor(y + s);
const k = Math.floor(z + s);
const t = (i + j + k) * G3;
const X0 = i - t, Y0 = j - t, Z0 = k - t;
const x0 = x - X0, y0 = y - Y0, z0 = z - Z0;
let i1, j1, k1, i2, j2, k2;
if (x0 >= y0) {
if (y0 >= z0) {
i1 = 1;
j1 = 0;
k1 = 0;
i2 = 1;
j2 = 1;
k2 = 0;
}
else if (x0 >= z0) {
i1 = 1;
j1 = 0;
k1 = 0;
i2 = 1;
j2 = 0;
k2 = 1;
}
else {
i1 = 0;
j1 = 0;
k1 = 1;
i2 = 1;
j2 = 0;
k2 = 1;
}
}
else {
if (y0 < z0) {
i1 = 0;
j1 = 0;
k1 = 1;
i2 = 0;
j2 = 1;
k2 = 1;
}
else if (x0 < z0) {
i1 = 0;
j1 = 1;
k1 = 0;
i2 = 0;
j2 = 1;
k2 = 1;
}
else {
i1 = 0;
j1 = 1;
k1 = 0;
i2 = 1;
j2 = 1;
k2 = 0;
}
}
const x1 = x0 - i1 + G3, y1 = y0 - j1 + G3, z1 = z0 - k1 + G3;
const x2 = x0 - i2 + 2 * G3, y2 = y0 - j2 + 2 * G3, z2 = z0 - k2 + 2 * G3;
const x3 = x0 - 1 + 3 * G3, y3 = y0 - 1 + 3 * G3, z3 = z0 - 1 + 3 * G3;
const ii = i & 255, jj = j & 255, kk = k & 255;
const g0 = grad3[perm[ii + perm[jj + perm[kk]]] % 12];
const g1 = grad3[perm[ii + i1 + perm[jj + j1 + perm[kk + k1]]] % 12];
const g2 = grad3[perm[ii + i2 + perm[jj + j2 + perm[kk + k2]]] % 12];
const g3 = grad3[perm[ii + 1 + perm[jj + 1 + perm[kk + 1]]] % 12];
let n0, n1, n2, n3;
let t0 = 0.6 - x0 * x0 - y0 * y0 - z0 * z0;
n0 = t0 < 0 ? 0 : (t0 *= t0, t0 * t0 * dot3(g0, x0, y0, z0));
let t1 = 0.6 - x1 * x1 - y1 * y1 - z1 * z1;
n1 = t1 < 0 ? 0 : (t1 *= t1, t1 * t1 * dot3(g1, x1, y1, z1));
let t2 = 0.6 - x2 * x2 - y2 * y2 - z2 * z2;
n2 = t2 < 0 ? 0 : (t2 *= t2, t2 * t2 * dot3(g2, x2, y2, z2));
let t3 = 0.6 - x3 * x3 - y3 * y3 - z3 * z3;
n3 = t3 < 0 ? 0 : (t3 *= t3, t3 * t3 * dot3(g3, x3, y3, z3));
return 32 * (n0 + n1 + n2 + n3);
}
// Fractal Brownian Motion for more natural terrain
function fbm(x, y, octaves = 6, persistence = 0.5, lacunarity = 2.0) {
let value = 0;
let amplitude = 1;
let frequency = 1;
let maxVal = 0;
for (let i = 0; i < octaves; i++) {
value += amplitude * noise3D(x * frequency, y * frequency, 0);
maxVal += amplitude;
amplitude *= persistence;
frequency *= lacunarity;
}
return value / maxVal;
}
// Ridged noise for mountain ridges
function ridgedNoise(x, y, octaves = 4) {
let value = 0;
let amplitude = 1;
let frequency = 1;
let previous = 1;
let total = 0;
let maxTotal = 0;
for (let i = 0; i < octaves; i++) {
let n = Math.abs(noise3D(x * frequency, y * frequency, 0));
n = 1 - n;
n = n * n;
value += (n * amplitude * previous);
previous = n;
total += amplitude;
maxTotal += amplitude;
amplitude *= 0.5;
frequency *= 2;
}
return value / maxTotal;
}
// Combined terrain height function
function terrainHeight(x, z) {
const scale = 0.002;
const baseHeight = fbm(x * scale, z * scale, 6, 0.5, 2.0);
const ridgeHeight = ridgedNoise(x * scale * 1.5, z * scale * 1.5, 4);
const detail = noise3D(x * scale * 4, z * scale * 4, 0) * 0.1;
let height = baseHeight * 800 + ridgeHeight * 400 + detail * 50;
// Flatten near origin for runway area
const distFromOrigin = Math.sqrt(x * x + z * z);
const runwayRadius = 500;
if (distFromOrigin < runwayRadius) {
const t = distFromOrigin / runwayRadius;
const flatten = Math.pow(t, 2);
height = height * flatten + 10 * (1 - flatten);
}
return Math.max(height, -50); // Sea floor
}
// Get biome at location
const types_1 = require("./types");
function getBiome(x, z) {
const h = terrainHeight(x, z);
if (h < 0)
return types_1.BiomeType.Water;
if (h < 5)
return types_1.BiomeType.Beach;
if (h < 200)
return noise3D(x * 0.01, z * 0.01, 1) > 0 ? types_1.BiomeType.Forest : types_1.BiomeType.Grassland;
if (h < 500)
return types_1.BiomeType.Mountain;
return types_1.BiomeType.Snow;
}
};
// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
// Realistic aerodynamic physics model for flight simulation
// Implements Newtonian mechanics with lift, drag, thrust, weight, and control surface effects
Object.defineProperty(exports, "__esModule", { value: true });
exports.airDensity = airDensity;
exports.speedOfSound = speedOfSound;
exports.machNumber = machNumber;
exports.liftCoefficient = liftCoefficient;
exports.dragCoefficient = dragCoefficient;
exports.calculateForces = calculateForces;
exports.updatePhysics = updatePhysics;
exports.updateThrust = updateThrust;
exports.checkTerrainCollision = checkTerrainCollision;
exports.createCrashParticles = createCrashParticles;
exports.updateParticles = updateParticles;
exports.createContrail = createContrail;
// Physical constants
const G = 9.80665; // m/s²
const SEA_LEVEL_DENSITY = 1.225; // kg/m³
const SEA_LEVEL_TEMP = 288.15; // K
const TEMP_LAPSE = 0.0065; // K/m
const R = 287.05; // J/(kg·K)
// Air density at altitude (ISA model)
function airDensity(altitude) {
if (altitude < 0)
return SEA_LEVEL_DENSITY;
const temp = Math.max(SEA_LEVEL_TEMP - TEMP_LAPSE * altitude, 200);
const ratio = temp / SEA_LEVEL_TEMP;
return SEA_LEVEL_DENSITY * Math.pow(ratio, R / (G * TEMP_LAPSE) - 1);
}
// Speed of sound at altitude
function speedOfSound(altitude) {
const temp = Math.max(SEA_LEVEL_TEMP - TEMP_LAPSE * altitude, 200);
return Math.sqrt(1.4 * R * temp);
}
// Mach number
function machNumber(airspeed, altitude) {
return airspeed / speedOfSound(altitude);
}
// Lift coefficient from angle of attack (linear + stall model)
function liftCoefficient(alpha, profile) {
// Linear region
let cl = profile.cl0 + profile.clAlpha * alpha;
// Stall model: gradual loss of lift beyond stall angle
if (Math.abs(alpha) > profile.stallAngle) {
const excess = Math.abs(alpha) - profile.stallAngle;
const stallFactor = Math.exp(-excess * 5);
cl *= stallFactor;
// Add some uncommanded lift at extreme angles (deep stall)
if (excess > 1.0) {
cl += Math.sign(alpha) * 0.3 * (excess - 1.0);
}
}
return Math.max(-profile.clMax * 0.5, Math.min(profile.clMax, cl));
}
// Drag coefficient (profile drag + induced drag + compressibility drag)
function dragCoefficient(alpha, cl, mach, profile) {
// Profile drag (parasitic)
const cdProfile = profile.cd0;
// Induced drag (from lift)
const cdInduced = profile.k * cl * cl;
// Compressibility drag (increases near Mach 1)
let cdCompress = 0;
if (mach > 0.7) {
cdCompress = 0.001 * Math.pow((mach - 0.7) * 10, 3);
}
// Wave drag at supersonic speeds
if (mach > 1.0) {
cdCompress += 0.05 * Math.pow(mach - 1.0, 2);
}
return cdProfile + cdInduced + cdCompress;
}
// Calculate forces in body frame
function calculateForces(state, profile, altitude) {
const rho = airDensity(altitude);
const v = state.airspeed;
const vSq = v * v;
const alpha = state.angleOfAttack;
const beta = state.sideslipAngle;
const mach = machNumber(v, altitude);
// Weight
const weight = profile.mass * G;
// Dynamic pressure
const q = 0.5 * rho * vSq;
// Lift
const cl = liftCoefficient(alpha, profile);
const lift = q * profile.wingArea * cl;
// Drag
const cd = dragCoefficient(alpha, cl, mach, profile);
const drag = q * profile.referenceArea * cd;
// Thrust (altitude-corrected)
const altFactor = Math.max(0, 1 - altitude * profile.thrustRollOff / 1000);
const availableThrust = profile.maxThrust * altFactor;
const thrust = state.currentThrust * altFactor;
// Side force from sideslip
const sideForce = q * profile.wingArea * 0.3 * beta;
// Transform forces to body frame
// Body frame: X forward, Y right, Z down (aerospace convention)
const forceBody = new THREE.Vector3(thrust - drag, // X: thrust minus drag
-sideForce, // Y: side force (negative for stability)
lift - weight // Z: lift minus weight
);
// Torque from control surfaces
const controlAuthority = Math.min(1, q / 5000); // Controls less effective at low speed
const pitchTorque = -state.pitchInput * controlAuthority * 5000;
const rollTorque = -state.rollInput * controlAuthority * 3000;
const yawTorque = -state.yawInput * controlAuthority * 1500;
// Damping torques (proportional to angular velocity)
const pitchDamping = -state.angularVelocity.y * 800;
const rollDamping = -state.angularVelocity.x * 600;
const yawDamping = -state.angularVelocity.z * 400;
// Adverse yaw from roll
const adverseYaw = -state.rollInput * controlAuthority * 500;
const torqueBody = new THREE.Vector3(rollTorque + rollDamping, pitchTorque + pitchDamping, yawTorque + yawDamping + adverseYaw);
return { lift, drag, thrust, weight, forceBody, torqueBody };
}
// Update aircraft state using physics (semi-implicit Euler integration)
function updatePhysics(state, profile, dt) {
if (dt > 0.05)
dt = 0.05; // Cap timestep for stability
const altitude = state.position.y;
const { forceBody, torqueBody } = calculateForces(state, profile, altitude);
// Transform body-frame forces to world frame
const forceWorld = forceBody.clone().applyQuaternion(state.orientation);
const torqueWorld = torqueBody.clone().applyQuaternion(state.orientation);
// --- Translational dynamics ---
// F = ma => a = F/m
const acceleration = forceWorld.clone().divideScalar(profile.mass);
// Update velocity
state.velocity.x += acceleration.x * dt;
state.velocity.y += acceleration.y * dt;
state.velocity.z += acceleration.z * dt;
// Clamp velocity to prevent numerical instability
const maxSpeed = 700; // ~Mach 2
if (state.velocity.lengthSq() > maxSpeed * maxSpeed) {
state.velocity.normalize().multiplyScalar(maxSpeed);
}
// Update position
state.position.x += state.velocity.x * dt;
state.position.y += state.velocity.y * dt;
state.position.z += state.velocity.z * dt;
// --- Rotational dynamics ---
// τ = Iα => α = τ/I
const angAccelX = torqueWorld.x / profile.ix;
const angAccelY = torqueWorld.y / profile.iy;
const angAccelZ = torqueWorld.z / profile.iz;
// Update angular velocity
state.angularVelocity.x += angAccelX * dt;
state.angularVelocity.y += angAccelY * dt;
state.angularVelocity.z += angAccelZ * dt;
// Angular velocity damping (gyroscopic stability)
const angDamping = 0.97;
state.angularVelocity.x *= angDamping;
state.angularVelocity.y *= angDamping;
state.angularVelocity.z *= angDamping;
// Clamp angular velocities
const maxAngVel = 2.5;
if (state.angularVelocity.length() > maxAngVel) {
state.angularVelocity.normalize().multiplyScalar(maxAngVel);
}
// Update orientation using angular velocity
const angVelMag = state.angularVelocity.length();
if (angVelMag > 0.001) {
const axis = new THREE.Vector3().copy(state.angularVelocity).normalize();
const angle = angVelMag * dt;
const deltaQuat = new THREE.Quaternion().setFromAxisAngle(axis, angle);
state.orientation.multiply(deltaQuat);
state.orientation.normalize();
}
// --- Update aerodynamic state ---
// Forward direction in world space
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(state.orientation);
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(state.orientation);
const right = new THREE.Vector3(1, 0, 0).applyQuaternion(state.orientation);
// Velocity direction
const velDir = state.velocity.clone().normalize();
state.airspeed = state.velocity.length();
// Angle of attack: angle between velocity and aircraft plane
const velProj = velDir.clone().sub(forward.clone().multiplyScalar(velDir.dot(forward)));
const velInPlane = velProj.clone().normalize();
if (velInPlane.lengthSq() > 0.001) {
state.angleOfAttack = Math.asin(Math.max(-1, Math.min(1, velDir.dot(up))));
}
// Sideslip angle
state.sideslipAngle = Math.asin(Math.max(-1, Math.min(1, velDir.dot(right))));
// Dynamic pressure
const rho = airDensity(state.position.y);
state.dynamicPressure = 0.5 * rho * state.airspeed * state.airspeed;
// Stall detection
state.isStalled = Math.abs(state.angleOfAttack) > profile.stallAngle && state.airspeed > 10;
// G-force calculation
const totalAccel = acceleration.length();
state.gForce = totalAccel / G;
// --- Engine dynamics ---
// Smooth throttle response
const thrustDelta = (state.throttle * profile.maxThrust - state.currentThrust) * profile.engineResponseRate * dt;
state.currentThrust = Math.max(0, Math.min(profile.maxThrust, state.currentThrust + thrustDelta));
// RPM tracking
state.rpm = state.throttle * 100;
}
// Engine thrust update
function updateThrust(state, targetThrottle, dt) {
state.throttle = Math.max(0, Math.min(1, targetThrottle));
}
// Check terrain collision
function checkTerrainCollision(position, terrainHeight, aircraftSize = 5) {
return position.y < terrainHeight + aircraftSize;
}
// Create crash particles
function createCrashParticles(position, count = 50) {
const particles = [];
for (let i = 0; i < count; i++) {
particles.push({
position: position.clone(),
velocity: new THREE.Vector3((Math.random() - 0.5) * 100, Math.random() * 80 + 20, (Math.random() - 0.5) * 100),
life: 0,
maxLife: Math.random() * 3 + 2,
size: Math.random() * 3 + 1,
color: Math.random() > 0.5 ? 0xff6600 : 0xff3300,
});
}
return particles;
}
// Update particles
function updateParticles(particles, dt) {
return particles
.map(p => {
p.life += dt;
p.velocity.y -= G * dt;
p.position.x += p.velocity.x * dt;
p.position.y += p.velocity.y * dt;
p.position.z += p.velocity.z * dt;
return p;
})
.filter(p => p.life < p.maxLife);
}
// Create contrail particles
function createContrail(position, velocity, altitude) {
// Contrails form at high altitude with high speed
if (altitude < 3000)
return null;
if (Math.random() > 0.3)
return null;
return {
position: position.clone(),
velocity: velocity.clone().multiplyScalar(-0.1),
life: 0,
maxLife: 8 + Math.random() * 4,
size: 2 + Math.random() * 2,
color: 0xffffff,
};
}
};
// ── module: src/aircraft.ts ──
__mods["src/aircraft.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AIRCRAFT_PROFILES = void 0;
exports.createAircraftState = createAircraftState;
exports.buildAircraftModel = buildAircraftModel;
exports.updateAircraftVisuals = updateAircraftVisuals;
// Three distinct aircraft with very different flight characteristics
exports.AIRCRAFT_PROFILES = {
// F-16 style fighter - high performance, agile
fighter: {
name: 'Viper F-16',
mass: 9200,
wingArea: 27.87,
wingSpan: 13.0,
referenceArea: 27.87,
clMax: 1.8,
clAlpha: 6.5,
cl0: 0.0,
cd0: 0.015,
k: 0.04,
maxThrust: 128000,
thrustRollOff: 0.0065,
engineResponseRate: 8000,
pitchRate: 1.5,
rollRate: 2.0,
yawRate: 0.8,
controlResponse: 4.0,
ix: 8000,
iy: 12000,
iz: 18000,
stallAngle: 0.44,
color: 0x708090,
accentColor: 0xff4444,
},
// A-10 style attack aircraft - heavy, slow, stable
attack: {
name: 'Thunderbolt A-10',
mass: 13000,
wingArea: 47.5,
wingSpan: 17.5,
referenceArea: 47.5,
clMax: 2.2,
clAlpha: 5.5,
cl0: 0.1,
cd0: 0.025,
k: 0.05,
maxThrust: 80000,
thrustRollOff: 0.006,
engineResponseRate: 4000,
pitchRate: 0.8,
rollRate: 1.2,
yawRate: 0.5,
controlResponse: 2.5,
ix: 15000,
iy: 22000,
iz: 32000,
stallAngle: 0.35,
color: 0x556B2F,
accentColor: 0xffaa00,
},
// Light sport aircraft - slow, easy to fly
sport: {
name: 'Skyhawk Cessna',
mass: 1150,
wingArea: 16.2,
wingSpan: 11.0,
referenceArea: 16.2,
clMax: 1.5,
clAlpha: 5.8,
cl0: 0.2,
cd0: 0.03,
k: 0.06,
maxThrust: 8000,
thrustRollOff: 0.007,
engineResponseRate: 2000,
pitchRate: 0.6,
rollRate: 0.8,
yawRate: 0.3,
controlResponse: 1.8,
ix: 2000,
iy: 3500,
iz: 4500,
stallAngle: 0.30,
color: 0xffffff,
accentColor: 0x0066cc,
},
};
function createAircraftState(startPos) {
return {
position: startPos.clone(),
orientation: new THREE.Quaternion(),
velocity: new THREE.Vector3(),
angularVelocity: new THREE.Vector3(),
throttle: 0,
currentThrust: 0,
rpm: 0,
angleOfAttack: 0,
sideslipAngle: 0,
airspeed: 0,
dynamicPressure: 0,
isStalled: false,
gForce: 1,
pitchInput: 0,
rollInput: 0,
yawInput: 0,
};
}
// Build 3D model for an aircraft type
function buildAircraftModel(type) {
const group = new THREE.Group();
const profile = exports.AIRCRAFT_PROFILES[type];
if (!profile)
return group;
const mainColor = profile.color;
const accentColor = profile.accentColor;
const matMain = new THREE.MeshStandardMaterial({
color: mainColor,
metalness: 0.6,
roughness: 0.3,
});
const matAccent = new THREE.MeshStandardMaterial({
color: accentColor,
metalness: 0.5,
roughness: 0.4,
});
const matGlass = new THREE.MeshStandardMaterial({
color: 0x88ccff,
metalness: 0.1,
roughness: 0.1,
transparent: true,
});
const matDark = new THREE.MeshStandardMaterial({
color: 0x222222,
metalness: 0.8,
roughness: 0.2,
});
if (type === 'fighter') {
buildFighter(group, matMain, matAccent, matGlass, matDark);
}
else if (type === 'attack') {
buildAttack(group, matMain, matAccent, matGlass, matDark);
}
else {
buildSport(group, matMain, matAccent, matGlass, matDark);
}
group.scale.set(1.5, 1.5, 1.5);
return group;
}
function buildFighter(group, matMain, matAccent, matGlass, matDark) {
// Fuselage - sleek delta wing fighter
const fuselageGeo = new THREE.ConeGeometry(0.3, 4, 8);
const fuselage = new THREE.Mesh(fuselageGeo, matMain);
fuselage.rotation.x = Math.PI / 2;
fuselage.position.z = -0.5;
group.add(fuselage);
// Nose cone
const noseGeo = new THREE.ConeGeometry(0.15, 1.5, 8);
const nose = new THREE.Mesh(noseGeo, matDark);
nose.rotation.x = Math.PI / 2;
nose.position.z = -2.5;
group.add(nose);
// Cockpit
const cockpitGeo = new THREE.SphereGeometry(0.25, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
const cockpit = new THREE.Mesh(cockpitGeo, matGlass);
cockpit.position.set(0, 0.2, -0.5);
group.add(cockpit);
// Main wings (delta)
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.lineTo(3, 1.5);
wingShape.lineTo(2.5, 2.5);
wingShape.lineTo(0, 2);
wingShape.lineTo(0, 0);
const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.05, bevelEnabled: false });
const wingLeft = new THREE.Mesh(wingGeo, matMain);
wingLeft.rotation.x = -Math.PI / 2;
wingLeft.position.set(0, -0.1, -0.5);
group.add(wingLeft);
const wingRight = new THREE.Mesh(wingGeo, matMain);
wingRight.rotation.x = -Math.PI / 2;
wingRight.rotation.z = Math.PI;
wingRight.position.set(0, -0.1, -0.5);
group.add(wingRight);
// Vertical stabilizer
const vstabGeo = new THREE.BoxGeometry(0.05, 1.2, 1.0);
const vstab = new THREE.Mesh(vstabGeo, matMain);
vstab.position.set(0, 0.5, 1.0);
group.add(vstab);
// Engine nozzle
const nozzleGeo = new THREE.CylinderGeometry(0.2, 0.25, 0.5, 8);
const nozzle = new THREE.Mesh(nozzleGeo, matDark);
nozzle.rotation.x = Math.PI / 2;
nozzle.position.z = 1.5;
group.add(nozzle);
// Engine glow
const glowGeo = new THREE.CircleGeometry(0.22, 8);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.8 });
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.position.z = 1.76;
glow.name = 'engineGlow';
group.add(glow);
// Landing gear
const gearGeo = new THREE.CylinderGeometry(0.03, 0.03, 0.5, 4);
const gearMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
const gear1 = new THREE.Mesh(gearGeo, gearMat);
gear1.position.set(-0.3, -0.4, -0.5);
group.add(gear1);
const gear2 = new THREE.Mesh(gearGeo, gearMat);
gear2.position.set(0.3, -0.4, -0.5);
group.add(gear2);
const gear3 = new THREE.Mesh(gearGeo, gearMat);
gear3.position.set(0, -0.3, 1.0);
group.add(gear3);
}
function buildAttack(group, matMain, matAccent, matGlass, matDark) {
// Fuselage - thick, straight wing
const fuselageGeo = new THREE.BoxGeometry(0.8, 0.6, 5);
const fuselage = new THREE.Mesh(fuselageGeo, matMain);
fuselage.position.z = -0.5;
group.add(fuselage);
// Nose
const noseGeo = new THREE.ConeGeometry(0.4, 1.5, 6);
const nose = new THREE.Mesh(noseGeo, matMain);
nose.rotation.x = Math.PI / 2;
nose.position.z = -3;
group.add(nose);
// Cockpit (tandem, two seats)
const cockpitGeo = new THREE.BoxGeometry(0.5, 0.4, 1.2);
const cockpit = new THREE.Mesh(cockpitGeo, matGlass);
cockpit.position.set(0, 0.4, -0.5);
group.add(cockpit);
// Straight wings (wide span)
const wingGeo = new THREE.BoxGeometry(7, 0.1, 1.5);
const wing = new THREE.Mesh(wingGeo, matMain);
wing.position.set(0, -0.1, -0.5);
group.add(wing);
// Wingtip tanks
const tankGeo = new THREE.CylinderGeometry(0.15, 0.15, 1.5, 6);
const tankL = new THREE.Mesh(tankGeo, matAccent);
tankL.rotation.x = Math.PI / 2;
tankL.position.set(-3.5, -0.1, -0.5);
group.add(tankL);
const tankR = new THREE.Mesh(tankGeo, matAccent);
tankR.rotation.x = Math.PI / 2;
tankR.position.set(3.5, -0.1, -0.5);
group.add(tankR);
// Twin vertical stabilizers
const vstabGeo = new THREE.BoxGeometry(0.08, 1.0, 1.2);
const vs1 = new THREE.Mesh(vstabGeo, matMain);
vs1.position.set(-0.5, 0.5, 1.5);
vs1.rotation.z = 0.2;
group.add(vs1);
const vs2 = new THREE.Mesh(vstabGeo, matMain);
vs2.position.set(0.5, 0.5, 1.5);
vs2.rotation.z = -0.2;
group.add(vs2);
// Twin engines
const engGeo = new THREE.CylinderGeometry(0.25, 0.3, 1.0, 8);
const engL = new THREE.Mesh(engGeo, matDark);
engL.rotation.x = Math.PI / 2;
engL.position.set(-0.35, -0.1, 2);
group.add(engL);
const engR = new THREE.Mesh(engGeo, matDark);
engR.rotation.x = Math.PI / 2;
engR.position.set(0.35, -0.1, 2);
group.add(engR);
// Engine glows
const glowGeo = new THREE.CircleGeometry(0.28, 8);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xff8800, transparent: true, opacity: 0.7 });
const glowL = new THREE.Mesh(glowGeo, glowMat);
glowL.position.set(-0.35, -0.1, 2.51);
glowL.name = 'engineGlow';
group.add(glowL);
const glowR = new THREE.Mesh(glowGeo, glowMat);
glowR.position.set(0.35, -0.1, 2.51);
group.add(glowR);
// Gun pod under fuselage
const gunGeo = new THREE.CylinderGeometry(0.08, 0.08, 1.0, 6);
const gun = new THREE.Mesh(gunGeo, matDark);
gun.rotation.x = Math.PI / 2;
gun.position.set(0, -0.4, -2.5);
group.add(gun);
// Landing gear
const gearGeo = new THREE.CylinderGeometry(0.04, 0.04, 0.6, 4);
const gearMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
const gear1 = new THREE.Mesh(gearGeo, gearMat);
gear1.position.set(-1, -0.5, -1);
group.add(gear1);
const gear2 = new THREE.Mesh(gearGeo, gearMat);
gear2.position.set(1, -0.5, -1);
group.add(gear2);
const gear3 = new THREE.Mesh(gearGeo, gearMat);
gear3.position.set(0, -0.4, 1.5);
group.add(gear3);
}
function buildSport(group, matMain, matAccent, matGlass, matDark) {
// Fuselage - rounded, small
const fuselageGeo = new THREE.CapsuleGeometry(0.3, 2.5, 8, 12);
const fuselage = new THREE.Mesh(fuselageGeo, matMain);
fuselage.rotation.x = Math.PI / 2;
fuselage.position.z = -0.5;
group.add(fuselage);
// Nose cone
const noseGeo = new THREE.SphereGeometry(0.25, 8, 6);
const nose = new THREE.Mesh(noseGeo, matMain);
nose.position.z = -1.5;
group.add(nose);
// Cockpit (large windows)
const cockpitGeo = new THREE.SphereGeometry(0.35, 8, 6, 0, Math.PI * 2, 0, Math.PI * 0.6);
const cockpit = new THREE.Mesh(cockpitGeo, matGlass);
cockpit.position.set(0, 0.2, -0.8);
group.add(cockpit);
// Low-mounted wings
const wingGeo = new THREE.BoxGeometry(4, 0.08, 0.8);
const wing = new THREE.Mesh(wingGeo, matMain);
wing.position.set(0, -0.15, -0.3);
group.add(wing);
// Wing struts
const strutGeo = new THREE.CylinderGeometry(0.02, 0.02, 0.5, 4);
const strutL = new THREE.Mesh(strutGeo, matDark);
strutL.position.set(-1, -0.3, -0.3);
group.add(strutL);
const strutR = new THREE.Mesh(strutGeo, matDark);
strutR.position.set(1, -0.3, -0.3);
group.add(strutR);
// Tail
const hstabGeo = new THREE.BoxGeometry(1.5, 0.06, 0.5);
const hstab = new THREE.Mesh(hstabGeo, matMain);
hstab.position.set(0, 0.1, 1.5);
group.add(hstab);
const vstabGeo = new THREE.BoxGeometry(0.05, 0.8, 0.6);
const vstab = new THREE.Mesh(vstabGeo, matMain);
vstab.position.set(0, 0.4, 1.5);
group.add(vstab);
// Propeller
const propGeo = new THREE.BoxGeometry(2, 0.05, 0.08);
const propMat = new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.8 });
const prop = new THREE.Mesh(propGeo, propMat);
prop.position.z = -1.8;
prop.name = 'propeller';
group.add(prop);
// Prop hub
const hubGeo = new THREE.CylinderGeometry(0.08, 0.08, 0.15, 6);
const hub = new THREE.Mesh(hubGeo, matDark);
hub.rotation.x = Math.PI / 2;
hub.position.z = -1.8;
group.add(hub);
// Landing gear (tail dragger)
const gearGeo = new THREE.CylinderGeometry(0.03, 0.03, 0.4, 4);
const gearMat = new THREE.MeshStandardMaterial({ color: 0x333333 });
const gear1 = new THREE.Mesh(gearGeo, gearMat);
gear1.position.set(-0.4, -0.4, -0.3);
group.add(gear1);
const gear2 = new THREE.Mesh(gearGeo, gearMat);
gear2.position.set(0.4, -0.4, -0.3);
group.add(gear2);
const gear3 = new THREE.Mesh(gearGeo, gearMat);
gear3.position.set(0, -0.2, 1.7);
group.add(gear3);
}
// Update visual elements (engine glow, propeller)
function updateAircraftVisuals(group, throttle, airspeed, type, time) {
// Engine glow intensity
const glows = group.children.filter(c => c.name === 'engineGlow');
glows.forEach(glow => {
const mesh = glow;
const mat = mesh.material;
if (mat) {
const intensity = 0.2 + throttle * 0.8;
mat.opacity = intensity;
if (throttle > 0.7) {
mat.color.set(0xff4400);
}
else if (throttle > 0.4) {
mat.color.set(0xff8800);
}
else {
mat.color.set(0xffaa44);
}
}
});
// Propeller rotation
if (type === 'sport') {
const prop = group.children.find(c => c.name === 'propeller');
if (prop) {
prop.rotation.z = time * airspeed * 0.5;
}
}
}
};
// ── module: src/terrain.ts ──
__mods["src/terrain.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.TerrainManager = void 0;
exports.createWater = createWater;
exports.createRunway = createRunway;
exports.createTargetDrone = createTargetDrone;
// Procedural terrain generation with heightmap and biome coloring
const noise_1 = require("./noise");
const types_1 = require("./types");
const TERRAIN_CHUNK_SIZE = 2000;
const TERRAIN_RESOLUTION = 200;
const CHUNKS_AROUND = 3;
// Biome colors
const BIOME_COLORS = {
[types_1.BiomeType.Water]: { top: 0x1a5276, bottom: 0x0e3652 },
[types_1.BiomeType.Beach]: { top: 0xd4b483, bottom: 0xc4a473 },
[types_1.BiomeType.Grassland]: { top: 0x4a7c3f, bottom: 0x3a6c2f },
[types_1.BiomeType.Forest]: { top: 0x2d5a1e, bottom: 0x1d4a0e },
[types_1.BiomeType.Mountain]: { top: 0x8b7355, bottom: 0x6b5335 },
[types_1.BiomeType.Snow]: { top: 0xf0f0f0, bottom: 0xd0d0d0 },
};
class TerrainManager {
constructor(scene) {
this.chunks = new Map();
this.scene = scene;
this.playerPosition = new THREE.Vector3();
}
update(playerPos) {
this.playerPosition.copy(playerPos);
const playerChunkX = Math.floor(playerPos.x / TERRAIN_CHUNK_SIZE);
const playerChunkZ = Math.floor(playerPos.z / TERRAIN_CHUNK_SIZE);
// Determine which chunks should exist
const neededKeys = new Set();
for (let dx = -CHUNKS_AROUND; dx <= CHUNKS_AROUND; dx++) {
for (let dz = -CHUNKS_AROUND; dz <= CHUNKS_AROUND; dz++) {
const cx = playerChunkX + dx;
const cz = playerChunkZ + dz;
neededKeys.add(`${cx},${cz}`);
}
}
// Remove far chunks
for (const [key, mesh] of this.chunks) {
if (!neededKeys.has(key)) {
this.scene.remove(mesh);
mesh.geometry.dispose();
this.chunks.delete(key);
}
}
// Add new chunks
for (const key of neededKeys) {
if (!this.chunks.has(key)) {
const [cx, cz] = key.split(',').map(Number);
const mesh = this.createChunk(cx, cz);
this.scene.add(mesh);
this.chunks.set(key, mesh);
}
}
}
createChunk(cx, cz) {
const size = TERRAIN_CHUNK_SIZE;
const res = TERRAIN_RESOLUTION;
const segments = res;
const baseX = cx * size;
const baseZ = cz * size;
const vertices = [];
const colors = [];
const indices = [];
for (let iz = 0; iz <= segments; iz++) {
for (let ix = 0; ix <= segments; ix++) {
const wx = baseX + (ix / segments) * size;
const wz = baseZ + (iz / segments) * size;
const h = (0, noise_1.terrainHeight)(wx, wz);
vertices.push(wx, h, wz);
const biome = (0, noise_1.getBiome)(wx, wz);
const colorData = BIOME_COLORS[biome] || BIOME_COLORS[types_1.BiomeType.Grassland];
// Add some variation based on noise
const variation = ((0, noise_1.terrainHeight)(wx + 10, wz + 10) - h) * 0.01;
const c = new THREE.Color(colorData.top);
c.r = Math.max(0, Math.min(1, c.r + variation));
c.g = Math.max(0, Math.min(1, c.g + variation * 0.5));
c.b = Math.max(0, Math.min(1, c.b + variation * 0.3));
colors.push(c.r, c.g, c.b);
}
}
for (let iz = 0; iz < segments; iz++) {
for (let ix = 0; ix < segments; ix++) {
const a = iz * (segments + 1) + ix;
const b = a + 1;
const c = a + segments + 1;
const d = c + 1;
indices.push(a, c, b);
indices.push(b, c, d);
}
}
const geometry = new THREE.BufferGeometry();
geometry.setAttribute('position', new THREE.BufferAttribute(new Float32Array(vertices), 3));
geometry.setAttribute('color', new THREE.BufferAttribute(new Float32Array(colors), 3));
geometry.setIndex(indices);
geometry.computeVertexNormals();
const material = new THREE.MeshStandardMaterial({
vertexColors: true,
metalness: 0.1,
roughness: 0.9,
flatShading: true,
});
const mesh = new THREE.Mesh(geometry, material);
mesh.receiveShadow = true;
return mesh;
}
getHeightAt(x, z) {
return (0, noise_1.terrainHeight)(x, z);
}
cleanup() {
for (const [, mesh] of this.chunks) {
this.scene.remove(mesh);
mesh.geometry.dispose();
}
this.chunks.clear();
}
}
exports.TerrainManager = TerrainManager;
// Water plane
function createWater(scene) {
const geo = new THREE.PlaneGeometry(20000, 20000);
const mat = new THREE.MeshStandardMaterial({
color: 0x1a6baa,
metalness: 0.8,
roughness: 0.2,
transparent: true,
opacity: 0.7,
});
const water = new THREE.Mesh(geo, mat);
water.rotation.x = -Math.PI / 2;
water.position.y = -5;
water.receiveShadow = true;
scene.add(water);
return water;
}
// Runway for takeoff/landing
function createRunway(scene, start, end, width = 40, length = 1000) {
const geo = new THREE.PlaneGeometry(width, length);
const mat = new THREE.MeshStandardMaterial({
color: 0x333333,
metalness: 0.1,
roughness: 0.8,
});
const runway = new THREE.Mesh(geo, mat);
const dir = new THREE.Vector3().subVectors(end, start).normalize();
const mid = new THREE.Vector3().addVectors(start, end).multiplyScalar(0.5);
runway.position.copy(mid);
runway.position.y = (0, noise_1.terrainHeight)(mid.x, mid.z) + 0.5;
// Rotate to align with runway direction
const up = new THREE.Vector3(0, 1, 0);
const right = new THREE.Vector3().crossVectors(dir, up).normalize();
const newUp = new THREE.Vector3().crossVectors(right, dir).normalize();
const matrix = new THREE.Matrix4();
const basis = new THREE.Matrix4();
basis.set(right.x, newUp.x, dir.x, 0, right.y, newUp.y, dir.y, 0, right.z, newUp.z, dir.z, 0, 0, 0, 0, 1);
runway.rotation.setFromRotationMatrix(basis);
runway.receiveShadow = true;
scene.add(runway);
// Add runway lights
const lightGeo = new THREE.SphereGeometry(0.5, 4, 4);
const lightMat = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
for (let i = 0; i < 10; i++) {
const t = i / 9;
const pos = new THREE.Vector3().lerpVectors(start, end, t);
pos.y = (0, noise_1.terrainHeight)(pos.x, pos.z) + 1;
const lightL = new THREE.Mesh(lightGeo, lightMat);
lightL.position.copy(pos);
lightL.position.x += right.x * (width / 2 + 2);
lightL.position.z += right.z * (width / 2 + 2);
scene.add(lightL);
const lightR = new THREE.Mesh(lightGeo, lightMat.clone());
lightR.position.copy(pos);
lightR.position.x -= right.x * (width / 2 + 2);
lightR.position.z -= right.z * (width / 2 + 2);
scene.add(lightR);
}
// Threshold markings
const markGeo = new THREE.PlaneGeometry(5, 30);
const markMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const markStart = new THREE.Mesh(markGeo, markMat);
markStart.position.copy(start);
markStart.position.y = (0, noise_1.terrainHeight)(start.x, start.z) + 0.6;
markStart.rotation.x = -Math.PI / 2;
scene.add(markStart);
return runway;
}
// Target drones for dogfight
function createTargetDrone(scene, position) {
const group = new THREE.Group();
// Body
const bodyGeo = new THREE.CapsuleGeometry(0.5, 2, 6, 8);
const bodyMat = new THREE.MeshStandardMaterial({
color: 0xcc2222,
metalness: 0.5,
roughness: 0.4,
});
const body = new THREE.Mesh(bodyGeo, bodyMat);
body.rotation.x = Math.PI / 2;
group.add(body);
// Wings
const wingGeo = new THREE.BoxGeometry(3, 0.1, 0.8);
const wingMat = new THREE.MeshStandardMaterial({
color: 0xaa1111,
metalness: 0.5,
roughness: 0.4,
});
const wing = new THREE.Mesh(wingGeo, wingMat);
wing.position.z = -0.3;
group.add(wing);
// Engine glow
const glowGeo = new THREE.CircleGeometry(0.4, 8);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.6 });
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.position.z = 1.1;
group.add(glow);
group.position.copy(position);
scene.add(group);
return group.children[0];
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInputState = createInputState;
exports.setupInput = setupInput;
exports.processInput = processInput;
function createInputState() {
return {
pitchUp: false, pitchDown: false,
rollLeft: false, rollRight: false,
yawLeft: false, yawRight: false,
throttleUp: false, throttleDown: false,
fire: false, fireHeld: false,
keyW: false, keyS: false, keyA: false, keyD: false,
keyQ: false, keyE: false, keySpace: false,
keyR: false, keyT: false, keyEnter: false, keyEscape: false,
mouseX: 0, mouseY: 0,
mouseLeft: false, mouseRight: false,
cameraPitch: 0, cameraYaw: 0,
};
}
function setupInput(input, canvas) {
const keyMap = {
'w': 'keyW', 's': 'keyS', 'a': 'keyA', 'd': 'keyD',
'q': 'keyQ', 'e': 'keyE', ' ': 'keySpace',
'r': 'keyR', 't': 'keyT', 'Enter': 'keyEnter',
'Escape': 'keyEscape',
};
window.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if (key in keyMap) {
input[keyMap[key]] = true;
}
// Flight controls
if (key === 'arrowup' || key === 'w')
input.pitchUp = true;
if (key === 'arrowdown' || key === 's')
input.pitchDown = true;
if (key === 'arrowleft' || key === 'a')
input.rollLeft = true;
if (key === 'arrowright' || key === 'd')
input.rollRight = true;
if (key === 'q')
input.yawLeft = true;
if (key === 'e')
input.yawRight = true;
if (key === 'shift')
input.throttleUp = true;
if (key === 'control')
input.throttleDown = true;
if (key === ' ')
input.fire = true;
e.preventDefault();
});
window.addEventListener('keyup', (e) => {
const key = e.key.toLowerCase();
if (key in keyMap) {
input[keyMap[key]] = false;
}
if (key === 'arrowup' || key === 'w')
input.pitchUp = false;
if (key === 'arrowdown' || key === 's')
input.pitchDown = false;
if (key === 'arrowleft' || key === 'a')
input.rollLeft = false;
if (key === 'arrowright' || key === 'd')
input.rollRight = false;
if (key === 'q')
input.yawLeft = false;
if (key === 'e')
input.yawRight = false;
if (key === 'shift')
input.throttleUp = false;
if (key === 'control')
input.throttleDown = false;
if (key === ' ')
input.fire = false;
e.preventDefault();
});
canvas.addEventListener('mousedown', (e) => {
if (e.button === 0)
input.mouseLeft = true;
if (e.button === 2)
input.mouseRight = true;
});
canvas.addEventListener('mouseup', (e) => {
if (e.button === 0)
input.mouseLeft = false;
if (e.button === 2)
input.mouseRight = false;
});
canvas.addEventListener('mousemove', (e) => {
input.mouseX = e.movementX || 0;
input.mouseY = e.movementY || 0;
});
canvas.addEventListener('contextmenu', (e) => e.preventDefault());
// Pointer lock for camera control
canvas.addEventListener('click', () => {
canvas.requestPointerLock();
});
document.addEventListener('pointerlockchange', () => {
if (!document.pointerLockElement) {
// Show pause overlay
}
});
}
function processInput(input, dt) {
const sensitivity = 1.0;
let pitch = 0;
let roll = 0;
let yaw = 0;
let throttleDelta = 0;
// Keyboard controls
if (input.pitchUp)
pitch -= sensitivity;
if (input.pitchDown)
pitch += sensitivity;
if (input.rollLeft)
roll -= sensitivity;
if (input.rollRight)
roll += sensitivity;
if (input.yawLeft)
yaw -= sensitivity;
if (input.yawRight)
yaw += sensitivity;
// Throttle
const throttleRate = 0.5 * dt;
if (input.throttleUp)
throttleDelta = throttleRate;
if (input.throttleDown)
throttleDelta = -throttleRate;
// Mouse camera control (when pointer locked)
if (document.pointerLockElement) {
input.cameraYaw -= input.mouseX * 0.002;
input.cameraPitch -= input.mouseY * 0.002;
input.cameraPitch = Math.max(-Math.PI / 2.5, Math.min(Math.PI / 2.5, input.cameraPitch));
input.mouseX = 0;
input.mouseY = 0;
}
return { pitch, roll, yaw, throttleDelta };
}
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
// Audio system using Web Audio API
// Dynamic engine sound, wind noise, and environmental audio
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioSystem = void 0;
class AudioSystem {
constructor() {
this.ctx = null;
this.engineGain = null;
this.engineOsc = null;
this.engineOsc2 = null;
this.windGain = null;
this.windNoise = null;
this.masterGain = null;
this.initialized = false;
this.currentThrottle = 0;
this.currentAirspeed = 0;
this.currentAltitude = 0;
}
init() {
if (this.initialized)
return;
try {
this.ctx = new AudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.3;
this.masterGain.connect(this.ctx.destination);
// Engine sound - two oscillators for richness
this.engineGain = this.ctx.createGain();
this.engineGain.gain.value = 0;
this.engineGain.connect(this.masterGain);
this.engineOsc = this.ctx.createOscillator();
this.engineOsc.type = 'sawtooth';
this.engineOsc.frequency.value = 80;
this.engineOsc.connect(this.engineGain);
this.engineOsc.start();
this.engineOsc2 = this.ctx.createOscillator();
this.engineOsc2.type = 'square';
this.engineOsc2.frequency.value = 40;
const osc2Gain = this.ctx.createGain();
osc2Gain.gain.value = 0.3;
this.engineOsc2.connect(osc2Gain);
osc2Gain.connect(this.engineGain);
this.engineOsc2.start();
// Wind noise
this.windGain = this.ctx.createGain();
this.windGain.gain.value = 0;
this.windGain.connect(this.masterGain);
const windFilter = this.ctx.createBiquadFilter();
windFilter.type = 'lowpass';
windFilter.frequency.value = 500;
windFilter.connect(this.windGain);
// Create noise buffer
const bufferSize = this.ctx.sampleRate * 2;
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.windNoise = this.ctx.createBufferSource();
this.windNoise.buffer = buffer;
this.windNoise.loop = true;
this.windNoise.connect(windFilter);
this.windNoise.start();
this.initialized = true;
}
catch (e) {
console.warn('Audio init failed:', e);
}
}
update(throttle, airspeed, altitude, isStalled) {
if (!this.initialized || !this.ctx)
return;
// Smooth transitions
this.currentThrottle += (throttle - this.currentThrottle) * 0.1;
this.currentAirspeed += (airspeed - this.currentAirspeed) * 0.05;
this.currentAltitude += (altitude - this.currentAltitude) * 0.05;
// Engine sound
if (this.engineOsc && this.engineOsc2 && this.engineGain) {
// Base frequency from throttle
const baseFreq = 60 + this.currentThrottle * 200;
// Add variation from airspeed
const speedFreq = this.currentAirspeed * 0.5;
this.engineOsc.frequency.value = Math.max(30, baseFreq + speedFreq);
this.engineOsc2.frequency.value = Math.max(20, baseFreq * 0.5 + speedFreq * 0.3);
// Volume from throttle and altitude
const altFactor = Math.max(0.3, 1 - this.currentAltitude / 15000);
this.engineGain.gain.value = this.currentThrottle * 0.6 * altFactor + 0.02;
}
// Wind noise
if (this.windGain) {
const windVolume = Math.min(0.4, this.currentAirspeed / 300);
// Stall warning - increase wind noise
const stallBoost = isStalled ? 0.3 : 0;
this.windGain.gain.value = windVolume + stallBoost;
}
// Resume context if suspended (browser autoplay policy)
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
}
playExplosion() {
if (!this.initialized || !this.ctx || !this.masterGain)
return;
const bufferSize = 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++) {
const t = i / this.ctx.sampleRate;
data[i] = (Math.random() * 2 - 1) * Math.exp(-t * 3) * 2;
}
const source = this.ctx.createBufferSource();
source.buffer = buffer;
const filter = this.ctx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 200;
const gain = this.ctx.createGain();
gain.gain.value = 0.8;
source.connect(filter);
filter.connect(gain);
gain.connect(this.masterGain);
source.start();
}
playGunshot() {
if (!this.initialized || !this.ctx || !this.masterGain)
return;
const osc = this.ctx.createOscillator();
osc.type = 'sawtooth';
osc.frequency.value = 200;
osc.frequency.exponentialRampToValueAtTime(50, this.ctx.currentTime + 0.1);
const gain = this.ctx.createGain();
gain.gain.value = 0.3;
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.15);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.15);
}
playStallWarning() {
if (!this.initialized || !this.ctx || !this.masterGain)
return;
const osc = this.ctx.createOscillator();
osc.type = 'sine';
osc.frequency.value = 800;
const gain = this.ctx.createGain();
gain.gain.value = 0.15;
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + 0.3);
osc.connect(gain);
gain.connect(this.masterGain);
osc.start();
osc.stop(this.ctx.currentTime + 0.3);
}
setVolume(v) {
if (this.masterGain) {
this.masterGain.gain.value = v;
}
}
dispose() {
if (this.engineOsc)
this.engineOsc.stop();
if (this.engineOsc2)
this.engineOsc2.stop();
if (this.windNoise)
this.windNoise.stop();
if (this.ctx)
this.ctx.close();
this.initialized = false;
}
}
exports.AudioSystem = AudioSystem;
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HUD = void 0;
// HUD overlay rendering on a 2D canvas
const types_1 = require("./types");
class HUD {
constructor(canvas) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.width = canvas.width;
this.height = canvas.height;
}
resize(w, h) {
this.width = w;
this.height = h;
this.canvas.width = w;
this.canvas.height = h;
}
render(state, profile, gameState, dogfightState, time, selectedAircraft) {
const ctx = this.ctx;
const w = this.width;
const h = this.height;
ctx.clearRect(0, 0, w, h);
if (gameState === types_1.GameState.Menu) {
this.renderMenu(ctx, w, h, time);
return;
}
if (gameState === types_1.GameState.Crashed) {
this.renderCrashScreen(ctx, w, h, time);
return;
}
this.renderFlightHUD(ctx, w, h, state, profile, time);
if (gameState === types_1.GameState.TakeoffLanding) {
this.renderTakeoffHUD(ctx, w, h, state, time);
}
if (gameState === types_1.GameState.Dogfight && dogfightState) {
this.renderDogfightHUD(ctx, w, h, dogfightState, state, time);
}
}
renderMenu(ctx, w, h, time) {
// Background gradient
const grad = ctx.createLinearGradient(0, 0, 0, h);
grad.addColorStop(0, '#0a0a2e');
grad.addColorStop(1, '#1a1a4e');
ctx.fillStyle = grad;
ctx.fillRect(0, 0, w, h);
// Stars
ctx.fillStyle = 'rgba(255,255,255,0.5)';
for (let i = 0; i < 100; i++) {
const x = (Math.sin(i * 127.1 + time * 0.01) * 0.5 + 0.5) * w;
const y = (Math.cos(i * 311.7 + time * 0.005) * 0.5 + 0.5) * h;
const size = Math.sin(time * 2 + i) * 0.5 + 1;
ctx.fillRect(x, y, size, size);
}
// Title
ctx.textAlign = 'center';
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 64px Arial';
ctx.fillText('APEX AERO', w / 2, h * 0.25);
ctx.font = '24px Arial';
ctx.fillStyle = '#88aacc';
ctx.fillText('Flight Simulator', w / 2, h * 0.32);
// Aircraft selection
const aircraft = ['fighter', 'attack', 'sport'];
const names = ['Viper F-16', 'Thunderbolt A-10', 'Skyhawk Cessna'];
const desc = ['Agile fighter jet', 'Heavy attack aircraft', 'Light sport plane'];
for (let i = 0; i < 3; i++) {
const y = h * 0.45 + i * 80;
ctx.fillStyle = 'rgba(255,255,255,0.1)';
ctx.fillRect(w * 0.2, y, w * 0.6, 60);
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 20px Arial';
ctx.textAlign = 'left';
ctx.fillText(`[${i + 1}] ${names[i]}`, w * 0.25, y + 25);
ctx.fillStyle = '#88aacc';
ctx.font = '14px Arial';
ctx.fillText(desc[i], w * 0.25, y + 45);
}
// Mode selection
ctx.textAlign = 'center';
ctx.fillStyle = '#aaddff';
ctx.font = '18px Arial';
ctx.fillText('[T] Takeoff/Landing [D] Dogfight', w / 2, h * 0.8);
ctx.fillText('Press corresponding key to select', w / 2, h * 0.85);
// Controls help
ctx.fillStyle = '#668899';
ctx.font = '12px Arial';
ctx.fillText('W/S: Pitch | A/D: Roll | Q/E: Yaw | Shift/Ctrl: Throttle | Space: Fire', w / 2, h * 0.95);
}
renderFlightHUD(ctx, w, h, state, profile, time) {
ctx.textAlign = 'center';
ctx.font = '14px monospace';
// Crosshair
const cx = w / 2;
const cy = h / 2;
ctx.strokeStyle = 'rgba(0, 255, 0, 0.7)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx - 20, cy);
ctx.lineTo(cx - 8, cy);
ctx.moveTo(cx + 8, cy);
ctx.lineTo(cx + 20, cy);
ctx.moveTo(cx, cy - 20);
ctx.lineTo(cx, cy - 8);
ctx.moveTo(cx, cy + 8);
ctx.lineTo(cx, cy + 20);
ctx.stroke();
// Pitch ladder
ctx.strokeStyle = 'rgba(0, 255, 0, 0.4)';
ctx.fillStyle = 'rgba(0, 255, 0, 0.6)';
ctx.font = '10px monospace';
for (let deg = -30; deg <= 30; deg += 10) {
if (deg === 0)
continue;
const yOff = deg * 3;
const y = cy + yOff;
const lineW = Math.abs(deg) === 10 ? 40 : 20;
ctx.beginPath();
ctx.moveTo(cx - lineW, y);
ctx.lineTo(cx + lineW, y);
ctx.stroke();
if (Math.abs(deg) === 10) {
ctx.fillText(`${Math.abs(deg)}°`, cx + 50, y + 3);
}
}
// Airspeed (left side)
const speedKnots = Math.round(state.airspeed * 1.94384);
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(20, h / 2 - 100, 80, 200);
ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)';
ctx.strokeRect(20, h / 2 - 100, 80, 200);
ctx.fillStyle = '#00ff00';
ctx.font = 'bold 24px monospace';
ctx.fillText(`${speedKnots}`, 60, h / 2 - 20);
ctx.font = '10px monospace';
ctx.fillText('KTS', 60, h / 2);
// Speed tape
for (let i = -5; i <= 5; i++) {
const spd = speedKnots + i * 20;
if (spd < 0)
continue;
const y = h / 2 - 40 - i * 16;
ctx.font = '9px monospace';
ctx.fillText(`${spd}`, 60, y);
}
// Altitude (right side)
const alt = Math.round(state.position.y);
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(w - 100, h / 2 - 100, 80, 200);
ctx.strokeStyle = 'rgba(0, 255, 0, 0.5)';
ctx.strokeRect(w - 100, h / 2 - 100, 80, 200);
ctx.fillStyle = '#00ff00';
ctx.font = 'bold 24px monospace';
ctx.fillText(`${alt}`, w - 60, h / 2 - 20);
ctx.font = '10px monospace';
ctx.fillText('FT', w - 60, h / 2);
// Altitude tape
for (let i = -5; i <= 5; i++) {
const a = alt + i * 100;
const y = h / 2 - 40 - i * 16;
ctx.font = '9px monospace';
ctx.fillText(`${a}`, w - 60, y);
}
// Heading
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(state.orientation);
const heading = Math.atan2(forward.x, forward.z) * 180 / Math.PI;
const headingNorm = ((heading % 360) + 360) % 360;
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(w / 2 - 60, 10, 120, 30);
ctx.fillStyle = '#00ff00';
ctx.font = 'bold 16px monospace';
ctx.fillText(`${headingNorm.toFixed(0)}°`, w / 2, 30);
// G-Force indicator (bottom center)
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(w / 2 - 50, h - 60, 100, 40);
ctx.fillStyle = state.gForce > 3 ? '#ff4444' : state.gForce > 2 ? '#ffaa00' : '#00ff00';
ctx.font = 'bold 14px monospace';
ctx.fillText(`G: ${state.gForce.toFixed(1)}`, w / 2, h - 35);
// Throttle bar (bottom right)
const throttleW = 120;
const throttleH = 15;
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(w - throttleW - 20, h - 60, throttleW, throttleH);
const throttleColor = state.throttle > 0.8 ? '#ff4444' : state.throttle > 0.5 ? '#ffaa00' : '#44ff44';
ctx.fillStyle = throttleColor;
ctx.fillRect(w - throttleW - 20, h - 60, throttleW * state.throttle, throttleH);
ctx.fillStyle = '#00ff00';
ctx.font = '10px monospace';
ctx.fillText(`THR: ${Math.round(state.throttle * 100)}%`, w - throttleW / 2 - 20, h - 40);
// Stall warning
if (state.isStalled) {
const flash = Math.sin(time * 10) > 0;
if (flash) {
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = '#ff0000';
ctx.font = 'bold 36px Arial';
ctx.textAlign = 'center';
ctx.fillText('⚠ STALL ⚠', w / 2, h / 2 + 60);
}
}
// Aircraft name (top left)
ctx.textAlign = 'left';
ctx.fillStyle = '#88aacc';
ctx.font = '12px Arial';
ctx.fillText(profile.name, 20, 30);
// Mach number
const mach = state.airspeed / 340;
ctx.fillText(`M: ${mach.toFixed(2)}`, 20, 48);
// Angle of attack
ctx.fillText(`AoA: ${(state.angleOfAttack * 180 / Math.PI).toFixed(1)}°`, 20, 66);
}
renderTakeoffHUD(ctx, w, h, state, time) {
ctx.textAlign = 'center';
ctx.fillStyle = '#ffdd44';
ctx.font = '16px Arial';
// Phase indicator
let phase = 'FLIGHT';
if (state.position.y < 20 && state.airspeed < 30)
phase = 'TAXI';
else if (state.position.y < 50 && state.airspeed > 30)
phase = 'TAKEOFF';
else if (state.position.y > 200)
phase = 'CRUISE';
else if (state.position.y < 100 && state.velocity.y > 0)
phase = 'APPROACH';
ctx.fillText(`Phase: ${phase}`, w / 2, h - 80);
// Vertical speed
const vsi = state.velocity.y * 196.85; // ft/min
ctx.fillText(`VSI: ${vsi.toFixed(0)} ft/min`, w / 2, h - 95);
}
renderDogfightHUD(ctx, w, h, ds, state, time) {
ctx.textAlign = 'center';
// Score
ctx.fillStyle = '#ffdd44';
ctx.font = 'bold 18px Arial';
ctx.fillText(`Score: ${ds.score}`, w / 2, 60);
// Time remaining
ctx.fillStyle = ds.timeRemaining < 30 ? '#ff4444' : '#00ff00';
ctx.fillText(`Time: ${ds.timeRemaining.toFixed(0)}s`, w / 2, 80);
// Health bar
const healthW = 200;
const healthH = 15;
const healthX = w / 2 - healthW / 2;
const healthY = h - 90;
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(healthX, healthY, healthW, healthH);
const healthPct = ds.playerHealth / 100;
ctx.fillStyle = healthPct > 0.5 ? '#44ff44' : healthPct > 0.25 ? '#ffaa00' : '#ff4444';
ctx.fillRect(healthX, healthY, healthW * healthPct, healthH);
ctx.strokeStyle = '#ffffff';
ctx.strokeRect(healthX, healthY, healthW, healthH);
ctx.fillStyle = '#ffffff';
ctx.font = '11px monospace';
ctx.fillText(`HP: ${ds.playerHealth}`, w / 2, healthY + 12);
// Ammo
ctx.fillText(`Ammo: ${ds.ammo}/${ds.maxAmmo}`, w / 2, h - 110);
// Target count
const alive = ds.targets.filter(t => t.isAlive).length;
ctx.fillStyle = '#ff6666';
ctx.fillText(`Targets: ${alive}`, w / 2, h - 130);
// Target reticle when aiming at enemy
if (alive > 0) {
ctx.strokeStyle = 'rgba(255, 0, 0, 0.5)';
ctx.lineWidth = 2;
const cx = w / 2, cy = h / 2;
ctx.beginPath();
ctx.arc(cx, cy, 35, 0, Math.PI * 2);
ctx.stroke();
// Corner brackets
const s = 15;
ctx.beginPath();
ctx.moveTo(cx - 35, cy - s);
ctx.lineTo(cx - 35, cy + s);
ctx.moveTo(cx + 35, cy - s);
ctx.lineTo(cx + 35, cy + s);
ctx.moveTo(cx - s, cy - 35);
ctx.lineTo(cx + s, cy - 35);
ctx.moveTo(cx - s, cy + 35);
ctx.lineTo(cx + s, cy + 35);
ctx.stroke();
}
}
renderCrashScreen(ctx, w, h, time) {
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
ctx.fillRect(0, 0, w, h);
ctx.textAlign = 'center';
ctx.fillStyle = '#ff4444';
ctx.font = 'bold 48px Arial';
ctx.fillText('CRASHED', w / 2, h / 2 - 30);
ctx.fillStyle = '#ffffff';
ctx.font = '20px Arial';
ctx.fillText('Press R to restart', w / 2, h / 2 + 20);
ctx.fillText('Press ESC for menu', w / 2, h / 2 + 50);
// Flicker effect
if (Math.sin(time * 5) > 0) {
ctx.fillStyle = 'rgba(255, 100, 0, 0.2)';
ctx.fillRect(0, 0, w, h);
}
}
}
exports.HUD = HUD;
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameManager = void 0;
// Game mode management
const types_1 = require("./types");
const noise_1 = require("./noise");
const physics_1 = require("./physics");
class GameManager {
constructor(scene, audio) {
this.gameState = types_1.GameState.Menu;
this.selectedAircraft = 'fighter';
this.takeoffState = null;
this.dogfightState = null;
this.particles = [];
this.scene = scene;
this.audio = audio;
}
get state() { return this.gameState; }
get selectedAircraftType() { return this.selectedAircraft; }
get dogfight() { return this.dogfightState; }
selectAircraft(index) {
const types = ['fighter', 'attack', 'sport'];
if (index >= 0 && index < types.length) {
this.selectedAircraft = types[index];
}
}
startTakeoffLanding() {
this.gameState = types_1.GameState.TakeoffLanding;
this.takeoffState = {
runwayStart: new THREE.Vector3(-200, 0, 0),
runwayEnd: new THREE.Vector3(800, 0, 0),
runwayWidth: 40,
runwayLength: 1000,
phase: 'taxi',
score: 0,
};
this.particles = [];
}
startDogfight() {
this.gameState = types_1.GameState.Dogfight;
this.dogfightState = {
targets: [],
playerHealth: 100,
score: 0,
ammo: 30,
maxAmmo: 30,
lastShot: 0,
timeLimit: 180,
timeRemaining: 180,
};
this.particles = [];
// Spawn targets
for (let i = 0; i < 5; i++) {
const angle = (i / 5) * Math.PI * 2;
const dist = 500 + Math.random() * 500;
const pos = new THREE.Vector3(Math.cos(angle) * dist, 200 + Math.random() * 400, Math.sin(angle) * dist);
this.dogfightState.targets.push({
position: pos,
velocity: new THREE.Vector3((Math.random() - 0.5) * 50, 0, (Math.random() - 0.5) * 50),
health: 100,
maxHealth: 100,
isAlive: true,
orientation: new THREE.Quaternion(),
mesh: null,
lastShot: 0,
});
}
}
update(aircraftState, profile, input, dt, time) {
if (this.gameState === types_1.GameState.Menu) {
this.handleMenuInput(input);
return;
}
if (this.gameState === types_1.GameState.Crashed) {
this.handleCrashInput(input);
return;
}
// Update physics
(0, physics_1.updatePhysics)(aircraftState, profile, dt);
// Update particles
this.particles = (0, physics_1.updateParticles)(this.particles, dt);
// Contrails
const contrail = (0, physics_1.createContrail)(aircraftState.position, aircraftState.velocity, aircraftState.position.y);
if (contrail)
this.particles.push(contrail);
// Terrain collision
const groundHeight = (0, noise_1.terrainHeight)(aircraftState.position.x, aircraftState.position.z);
if ((0, physics_1.checkTerrainCollision)(aircraftState.position, groundHeight, 5)) {
this.crash();
return;
}
// Mode-specific updates
if (this.gameState === types_1.GameState.TakeoffLanding) {
this.updateTakeoffLanding(aircraftState, dt, time);
}
if (this.gameState === types_1.GameState.Dogfight && this.dogfightState) {
this.updateDogfight(aircraftState, input, dt, time);
}
// Update audio
this.audio.update(aircraftState.throttle, aircraftState.airspeed, aircraftState.position.y, aircraftState.isStalled);
}
handleMenuInput(input) {
// Aircraft selection: 1, 2, 3 keys or a/s/d
if (input.keyA || input.keyW)
this.selectAircraft(0); // fighter
if (input.keyS)
this.selectAircraft(1); // attack
if (input.keyD || input.keyE)
this.selectAircraft(2); // sport
// Start game: T for takeoff, Enter for dogfight, Space for quick start
if (input.keyT) {
this.startTakeoffLanding();
}
if (input.keyEnter || input.keySpace) {
this.startDogfight();
}
}
handleCrashInput(input) {
if (input.keyR) {
this.gameState = types_1.GameState.Menu;
}
if (input.keyEscape) {
this.gameState = types_1.GameState.Menu;
}
}
updateTakeoffLanding(state, dt, time) {
if (!this.takeoffState)
return;
const ts = this.takeoffState;
const alt = state.position.y;
const speed = state.airspeed;
// Phase detection
if (alt < 20 && speed < 30) {
ts.phase = 'taxi';
}
else if (alt < 50 && speed > 30) {
ts.phase = 'takeoff';
}
else if (alt > 200) {
ts.phase = 'flight';
}
else if (alt < 100 && state.velocity.y > 0) {
ts.phase = 'approach';
}
// Score based on smooth landing
if (ts.phase === 'taxi' && alt < 5 && speed < 5) {
ts.score += dt * 10;
}
}
updateDogfight(state, input, dt, time) {
if (!this.dogfightState)
return;
const ds = this.dogfightState;
// Timer
ds.timeRemaining -= dt;
if (ds.timeRemaining <= 0) {
ds.timeRemaining = 0;
// Time's up - check if all targets destroyed
const alive = ds.targets.filter(t => t.isAlive).length;
if (alive === 0) {
ds.score += 1000;
}
}
// Shooting
if (input.fire && time - ds.lastShot > 0.15 && ds.ammo > 0) {
ds.lastShot = time;
ds.ammo--;
this.audio.playGunshot();
this.shootAtTargets(state, ds);
}
// Ammo reload
if (ds.ammo <= 0 && time - ds.lastShot > 3) {
ds.ammo = ds.maxAmmo;
}
// Update target AI
ds.targets.forEach(target => {
if (!target.isAlive)
return;
this.updateTargetAI(target, state, dt, time);
});
// Target shooting at player
ds.targets.forEach(target => {
if (!target.isAlive)
return;
const dist = target.position.distanceTo(state.position);
if (dist < 500 && time - target.lastShot > 2) {
target.lastShot = time;
// Check line of sight
const dir = new THREE.Vector3().subVectors(state.position, target.position).normalize();
// Simple hit check
if (Math.random() < 0.15) {
ds.playerHealth -= 10;
if (ds.playerHealth <= 0) {
ds.playerHealth = 0;
this.crash();
}
}
}
});
}
shootAtTargets(state, ds) {
const forward = new THREE.Vector3(0, 0, -1).applyQuaternion(state.orientation);
ds.targets.forEach(target => {
if (!target.isAlive)
return;
const dist = target.position.distanceTo(state.position);
if (dist > 800)
return;
// Check if target is in front
const toTarget = new THREE.Vector3().subVectors(target.position, state.position).normalize();
const dot = forward.dot(toTarget);
if (dot > 0.9) { // Within narrow cone
target.health -= 34;
if (target.health <= 0) {
target.isAlive = false;
target.health = 0;
ds.score += 200;
this.audio.playExplosion();
// Create explosion particles
this.particles.push(...(0, physics_1.createCrashParticles)(target.position, 30));
}
}
});
}
updateTargetAI(target, playerState, dt, time) {
// Simple AI: fly in circles and occasionally chase player
const distToPlayer = target.position.distanceTo(playerState.position);
if (distToPlayer < 1000) {
// Chase player
const toPlayer = new THREE.Vector3().subVectors(playerState.position, target.position).normalize();
target.velocity.x += toPlayer.x * 10 * dt;
target.velocity.z += toPlayer.z * 10 * dt;
}
else {
// Patrol pattern
const angle = time * 0.3;
const patrolRadius = 300;
const patrolX = Math.cos(angle) * patrolRadius;
const patrolZ = Math.sin(angle) * patrolRadius;
target.velocity.x += (patrolX - target.velocity.x) * 0.01;
target.velocity.z += (patrolZ - target.velocity.z) * 0.01;
}
// Keep altitude
if (target.position.y < 200) {
target.velocity.y += 5 * dt;
}
else if (target.position.y > 600) {
target.velocity.y -= 5 * dt;
}
// Clamp velocity
const maxSpeed = 100;
if (target.velocity.length() > maxSpeed) {
target.velocity.normalize().multiplyScalar(maxSpeed);
}
// Update position
target.position.x += target.velocity.x * dt;
target.position.y += target.velocity.y * dt;
target.position.z += target.velocity.z * dt;
// Ground collision
const groundH = (0, noise_1.terrainHeight)(target.position.x, target.position.z);
if (target.position.y < groundH + 20) {
target.position.y = groundH + 20;
target.velocity.y = Math.abs(target.velocity.y);
}
// Update orientation to face velocity direction
if (target.velocity.length() > 1) {
const velDir = target.velocity.clone().normalize();
const forward = new THREE.Vector3(0, 0, -1);
const quat = new THREE.Quaternion().setFromUnitVectors(forward, velDir);
target.orientation.copy(quat);
}
}
crash() {
this.gameState = types_1.GameState.Crashed;
this.audio.playExplosion();
}
getParticles() {
return this.particles;
}
resetToMenu() {
this.gameState = types_1.GameState.Menu;
this.takeoffState = null;
this.dogfightState = null;
this.particles = [];
}
}
exports.GameManager = GameManager;
};
// ── module: src/atmosphere.ts ──
__mods["src/atmosphere.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createClouds = createClouds;
exports.createSun = createSun;
exports.createSkyDome = createSkyDome;
exports.updateSkyDome = updateSkyDome;
function createClouds(scene) {
const cloudGroup = new THREE.Group();
const cloudMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.7,
metalness: 0,
roughness: 1,
});
// Create cloud clusters
for (let i = 0; i < 80; i++) {
const cloud = createCloudCluster(cloudMat);
const x = (Math.random() - 0.5) * 8000;
const z = (Math.random() - 0.5) * 8000;
const y = 500 + Math.random() * 1500;
cloud.position.set(x, y, z);
cloudGroup.add(cloud);
}
scene.add(cloudGroup);
return cloudGroup;
}
function createCloudCluster(mat) {
const group = new THREE.Group();
const numPuffs = 3 + Math.floor(Math.random() * 5);
for (let i = 0; i < numPuffs; i++) {
const size = 30 + Math.random() * 60;
const geo = new THREE.SphereGeometry(size, 7, 5);
const puff = new THREE.Mesh(geo, mat);
puff.position.set((Math.random() - 0.5) * 80, (Math.random() - 0.5) * 20, (Math.random() - 0.5) * 80);
group.add(puff);
}
return group;
}
function createSun(scene) {
// Sun disc
const sunGeo = new THREE.SphereGeometry(100, 16, 16);
const sunMat = new THREE.MeshBasicMaterial({
color: 0xffffcc,
transparent: true,
opacity: 0.9,
});
const sun = new THREE.Mesh(sunGeo, sunMat);
sun.position.set(2000, 3000, 3000);
scene.add(sun);
// Sun glow
const glowGeo = new THREE.SphereGeometry(200, 16, 16);
const glowMat = new THREE.MeshBasicMaterial({
color: 0xffffaa,
transparent: true,
opacity: 0.15,
});
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.position.copy(sun.position);
scene.add(glow);
return sun;
}
// Create a sky dome with gradient
function createSkyDome(scene) {
const skyGeo = new THREE.SphereGeometry(20000, 32, 15);
const skyMat = new THREE.ShaderMaterial({
side: THREE.BackSide,
uniforms: {
topColor: { value: new THREE.Color(0x0077ff) },
bottomColor: { value: new THREE.Color(0xaaddff) },
offset: { value: 4 },
exponent: { value: 0.6 },
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + offset).y;
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
}
`,
});
const sky = new THREE.Mesh(skyGeo, skyMat);
scene.add(sky);
return sky;
}
// Update sky dome to follow camera
function updateSkyDome(sky, cameraPos) {
sky.position.copy(cameraPos);
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>