← Flight Simulator results

Flight Simulator

Qwen 3.6 27b MTP · 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 108,646 bytes · SHA-256 8196a2de5587
<!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://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Segoe UI', system-ui, sans-serif; }

#app { position: relative; width: 100%; height: 100%; }
#game-container { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
#three-canvas { display: block; width: 100%; height: 100%; }

/* HUD Canvas */
#hud-canvas { position: absolute !important; top: 0 !important; left: 0 !important; width: 100% !important; height: 100% !important; pointer-events: none !important; z-index: 5; }

/* Menu Overlay */
#menu-overlay {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  background: linear-gradient(135deg, #0a0e27 0%, #1a1a3e 40%, #0d1b2a 100%);
  display: flex; align-items: center; justify-content: center;
  z-index: 100; overflow-y: auto; padding: 20px;
}

#menu-content {
  max-width: 900px; width: 100%; text-align: center; color: #fff;
  animation: fadeInUp 0.8s ease-out;
}

@keyframes fadeInUp {
  from { opacity: 0; transform: translateY(30px); }
  to { opacity: 1; transform: translateY(0); }
}

#title {
  font-size: 64px; font-weight: 900; letter-spacing: 8px;
  background: linear-gradient(90deg, #ff6b35, #f7c948, #4ecdc4);
  -webkit-background-clip: text; -webkit-text-fill-color: transparent;
  background-clip: text; margin-bottom: 5px;
}

#subtitle { font-size: 18px; color: #8899aa; letter-spacing: 4px; margin-bottom: 40px; }

h2 { font-size: 22px; color: #aabbcc; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 3px; }

/* Aircraft Cards */
#aircraft-select { display: flex; gap: 15px; justify-content: center; flex-wrap: wrap; margin-bottom: 30px; }

.aircraft-card {
  background: rgba(255,255,255,0.05); border: 2px solid rgba(255,255,255,0.1);
  border-radius: 12px; padding: 20px; width: 270px; cursor: pointer;
  transition: all 0.3s ease; text-align: left;
}

.aircraft-card:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3); transform: translateY(-3px); }
.aircraft-card.selected { border-color: #4ecdc4; background: rgba(78, 205, 196, 0.15); box-shadow: 0 0 20px rgba(78, 205, 196, 0.3); }

.plane-icon { font-size: 40px; margin-bottom: 10px; }
.aircraft-card h3 { font-size: 16px; color: #fff; margin-bottom: 8px; }
.aircraft-card p { font-size: 12px; color: #99aabb; line-height: 1.4; margin-bottom: 10px; }

.stats { display: flex; gap: 8px; flex-wrap: wrap; }
.stats span { font-size: 11px; color: #7788aa; background: rgba(255,255,255,0.05); padding: 3px 8px; border-radius: 4px; }

/* Scenario Buttons */
#scenario-select { margin-bottom: 30px; }

.scenario-btn {
  display: block; width: 100%; max-width: 600px; margin: 10px auto; padding: 20px;
  background: rgba(255,255,255,0.05); border: 2px solid rgba(255,255,255,0.1);
  border-radius: 12px; color: #fff; cursor: pointer; text-align: left;
  transition: all 0.3s ease; font-family: inherit;
}

.scenario-btn:hover { background: rgba(255,255,255,0.1); border-color: rgba(255,255,255,0.3); }
.scenario-btn.selected { border-color: #f7c948; background: rgba(247, 201, 72, 0.15); box-shadow: 0 0 20px rgba(247, 201, 72, 0.3); }

.scenario-btn .icon { font-size: 28px; margin-right: 15px; vertical-align: middle; }
.scenario-btn strong { font-size: 18px; display: inline-block; vertical-align: middle; }
.scenario-btn p { font-size: 13px; color: #99aabb; margin-top: 8px; margin-left: 45px; }

/* Start Button */
#start-btn {
  padding: 16px 60px; font-size: 20px; font-weight: bold; letter-spacing: 3px;
  background: linear-gradient(90deg, #ff6b35, #f7c948); border: none;
  border-radius: 8px; color: #fff; cursor: pointer; transition: all 0.3s ease;
  font-family: inherit; text-transform: uppercase;
}

#start-btn:hover:not(:disabled) { transform: scale(1.05); box-shadow: 0 0 30px rgba(247, 201, 72, 0.5); }
#start-btn:disabled { opacity: 0.3; cursor: not-allowed; }

/* Controls Help */
#controls-help { margin-top: 30px; padding: 20px; background: rgba(255,255,255,0.03); border-radius: 8px; text-align: left; max-width: 600px; margin-left: auto; margin-right: auto; }
#controls-help h3 { font-size: 14px; color: #aabbcc; margin-bottom: 10px; text-align: center; letter-spacing: 2px; }

.control-row { font-size: 13px; color: #8899aa; padding: 4px 0; }
kbd { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.2); border-radius: 4px; padding: 2px 8px; font-family: monospace; color: #fff; }

/* Pause Overlay */
#pause-overlay {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.7); display: flex; align-items: center; justify-content: center;
  z-index: 90; color: #fff; text-align: center;
}

#pause-overlay h2 { font-size: 48px; letter-spacing: 10px; margin-bottom: 20px; }
#pause-overlay p { font-size: 18px; color: #aabbcc; }

/* Event Messages */
#event-container { position: absolute; top: 100px; left: 50%; transform: translateX(-50%); z-index: 10; pointer-events: none; text-align: center; width: 80%; max-width: 600px; }

.event-msg {
  padding: 12px 24px; margin-bottom: 8px; border-radius: 8px; font-size: 16px;
  animation: eventSlideIn 0.3s ease-out, eventFadeOut 0.5s ease-in forwards;
  text-shadow: 1px 1px 2px rgba(0,0,0,0.8);
}

.event-msg.message { background: rgba(0,0,0,0.6); color: #ffffff; }
.event-msg.objective { background: rgba(78, 205, 196, 0.3); color: #4ecdc4; border: 1px solid #4ecdc4; }
.event-msg.warning { background: rgba(255, 100, 0, 0.3); color: #ff8c00; border: 1px solid #ff6b35; }
.event-msg.success { background: rgba(0, 200, 100, 0.3); color: #4ecdc4; border: 1px solid #4ecdc4; }
.event-msg.failure { background: rgba(200, 0, 0, 0.3); color: #ff4444; border: 1px solid #ff0000; }

@keyframes eventSlideIn { from { opacity: 0; transform: translateY(-20px); } to { opacity: 1; transform: translateY(0); } }
@keyframes eventFadeOut { from { opacity: 1; } to { opacity: 0; } }
</style>
</head>
<body>
<div id="game-container">
    <canvas id="three-canvas"></canvas>
  </div>
  <!-- Menu Overlay -->
  <div id="menu-overlay">
    <div id="menu-content">
      <h1 id="title">APEX AERO</h1>
      <p id="subtitle">Flight Simulator</p>
      
      <div id="aircraft-select">
        <h2>Select Aircraft</h2>
        <div class="aircraft-card" data-plane="falcon">
          <div class="plane-icon">✈️</div>
          <h3>F-16 Fighting Falcon</h3>
          <p>Lightweight fighter. High thrust-to-weight, exceptional maneuverability.</p>
          <div class="stats">
            <span>Speed: ★★★★☆</span>
            <span>Agility: ★★★★★</span>
            <span>Stability: ★★★☆☆</span>
          </div>
        </div>
        <div class="aircraft-card" data-plane="thunderbolt">
          <div class="plane-icon">🛩️</div>
          <h3>A-10 Thunderbolt II</h3>
          <p>Close air support. Heavy armor, slow but stable, devastating firepower.</p>
          <div class="stats">
            <span>Speed: ★★☆☆☆</span>
            <span>Agility: ★★★☆☆</span>
            <span>Stability: ★★★★★</span>
          </div>
        </div>
        <div class="aircraft-card" data-plane="fulcrum">
          <div class="plane-icon">🛫</div>
          <h3>MiG-29 Fulcrum</h3>
          <p>Twin-engine interceptor. Excellent high-altitude performance and turn rate.</p>
          <div class="stats">
            <span>Speed: ★★★★☆</span>
            <span>Agility: ★★★★☆</span>
            <span>Stability: ★★★☆☆</span>
          </div>
        </div>
      </div>

      <div id="scenario-select">
        <h2>Select Mission</h2>
        <button class="scenario-btn" data-scenario="takeoff">
          <span class="icon">🛫</span>
          <strong>Takeoff &amp; Landing</strong>
          <p>Precision flight — manage speed, altitude, and approach for a safe landing.</p>
        </button>
        <button class="scenario-btn" data-scenario="dogfight">
          <span class="icon">⚔️</span>
          <strong>Dogfight</strong>
          <p>Air combat — engage enemy drones with high-G maneuvers and missiles.</p>
        </button>
      </div>

      <button id="start-btn" disabled="">START MISSION</button>
      
      <div id="controls-help">
        <h3>Controls</h3>
        <div class="control-row"><kbd>W/S</kbd> Pitch Up/Down &nbsp;|&nbsp; <kbd>A/D</kbd> Roll Left/Right</div>
        <div class="control-row"><kbd>Q/E</kbd> Yaw (Rudder) &nbsp;|&nbsp; <kbd>Shift/=</kbd> Throttle Up</div>
        <div class="control-row"><kbd>Ctrl/-</kbd> Throttle Down &nbsp;|&nbsp; <kbd>C</kbd> Toggle Camera</div>
        <div class="control-row"><kbd>Space</kbd> Fire (Dogfight) &nbsp;|&nbsp; <kbd>Click</kbd> Lock Mouse</div>
      </div>
    </div>
  </div>

  <!-- Pause Overlay -->
  <div id="pause-overlay" style="display:none">
    <h2>PAUSED</h2>
    <p>Press ESC to resume</p>
  </div>

  <!-- Event Messages -->
  <div id="event-container"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./aircraft":"src/aircraft.ts","./physics":"src/physics.ts","./audio":"src/audio.ts","./input":"src/input.ts","./scenarios":"src/scenarios.ts","./hud":"src/hud.ts","./renderer":"src/renderer.ts"},"src/physics.ts":{"./types":"src/types.ts"},"src/aircraft.ts":{"./types":"src/types.ts"},"src/input.ts":{"./types":"src/types.ts"},"src/scenarios.ts":{"./types":"src/types.ts","./terrain":"src/terrain.ts"},"src/hud.ts":{"./types":"src/types.ts"},"src/renderer.ts":{"./types":"src/types.ts","./aircraft":"src/aircraft.ts","./terrain":"src/terrain.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 physics_1 = require("./physics");
const audio_1 = require("./audio");
const input_1 = require("./input");
const scenarios_1 = require("./scenarios");
const hud_1 = require("./hud");
const renderer_1 = require("./renderer");
// ===== Game State =====
let gameState = 'menu';
let selectedAircraft = '';
let selectedScenario = '';
let paused = false;
// Systems
let inputManager;
let audioSystem;
let hud;
let renderer;
let takeoffScenario = null;
let dogfightScenario = null;
// Aircraft state
let aircraftState;
let currentProfile = aircraft_1.AIRCRAFT_PROFILES.falcon;
// Timing
let lastTime = 0;
let eventQueue = [];
function init() {
    // Initialize systems
    inputManager = new input_1.InputManager();
    audioSystem = new audio_1.FlightAudio();
    hud = new hud_1.HUD();
    // Setup throttle wheel
    (0, input_1.setupThrottleWheel)((delta) => {
        if (gameState === 'playing') {
            aircraftState.throttle = Math.max(0, Math.min(1, aircraftState.throttle + delta * 0.05));
        }
    });
    // Initialize renderer
    const container = document.getElementById('game-container');
    renderer = new renderer_1.SceneRenderer(container);
    // Add HUD canvas to game container
    container.appendChild(hud.getCanvas());
    // Setup menu interactions
    setupMenu();
    // Start render loop (runs even in menu for background)
    requestAnimationFrame(gameLoop);
}
function setupMenu() {
    const aircraftCards = document.querySelectorAll('.aircraft-card');
    const scenarioBtns = document.querySelectorAll('.scenario-btn');
    const startBtn = document.getElementById('start-btn');
    // Aircraft selection
    aircraftCards.forEach(card => {
        card.addEventListener('click', () => {
            aircraftCards.forEach(c => c.classList.remove('selected'));
            card.classList.add('selected');
            selectedAircraft = card.dataset.plane || '';
            checkStartReady();
        });
    });
    // Scenario selection
    scenarioBtns.forEach(btn => {
        btn.addEventListener('click', () => {
            scenarioBtns.forEach(b => b.classList.remove('selected'));
            btn.classList.add('selected');
            selectedScenario = btn.dataset.scenario || '';
            checkStartReady();
        });
    });
    // Start button
    startBtn.addEventListener('click', () => {
        if (!startBtn.disabled) {
            startGame();
        }
    });
    // Keyboard shortcuts for menu
    window.addEventListener('keydown', (e) => {
        if (gameState === 'menu') {
            if (e.key === '1' && aircraftCards[0])
                aircraftCards[0].click();
            if (e.key === '2' && aircraftCards[1])
                aircraftCards[1].click();
            if (e.key === '3' && aircraftCards[2])
                aircraftCards[2].click();
        }
        // In-game controls
        if (gameState === 'playing') {
            if (e.code === 'Escape') {
                paused = !paused;
                document.getElementById('pause-overlay').style.display = paused ? 'flex' : 'none';
                if (!paused)
                    inputManager.lockPointer();
            }
            if (e.code === 'KeyC' && !paused)
                renderer.toggleCameraMode();
            if (e.code === 'Space' && !paused && selectedScenario === 'dogfight') {
                dogfightScenario?.fireMissile(aircraftState.position);
                audioSystem.playExplosion();
            }
        }
    });
    // Click to lock pointer during gameplay
    document.getElementById('game-container').addEventListener('click', () => {
        if (gameState === 'playing' && !paused) {
            inputManager.lockPointer();
        }
    });
}
function checkStartReady() {
    const startBtn = document.getElementById('start-btn');
    startBtn.disabled = !(selectedAircraft && selectedScenario);
}
function startGame() {
    // Hide menu
    document.getElementById('menu-overlay').style.display = 'none';
    // Set aircraft profile
    currentProfile = aircraft_1.AIRCRAFT_PROFILES[selectedAircraft];
    // Initialize aircraft state
    if (selectedScenario === 'takeoff') {
        // Start on runway for takeoff scenario
        aircraftState = createInitialState(0, 2, -500);
        takeoffScenario = new scenarios_1.TakeoffScenario();
        dogfightScenario = null;
    }
    else {
        // Start in the air for dogfight scenario
        aircraftState = createInitialState(300, 500, 0);
        aircraftState.velocity.x = 120; // Already flying at speed
        takeoffScenario = null;
        dogfightScenario = new scenarios_1.DogfightScenario();
        // Spawn drone meshes
        const drones = dogfightScenario.drones || [];
        setTimeout(() => {
            if (dogfightScenario) {
                const d = dogfightScenario.drones;
                for (let i = 0; i < d.length; i++) {
                    renderer.addDrone(d[i], i);
                }
            }
        }, 100);
    }
    // Create aircraft mesh
    const mesh = (0, aircraft_1.createAircraftMesh)(currentProfile);
    renderer.setAircraftMesh(mesh);
    // Start audio
    audioSystem.init();
    audioSystem.resume();
    audioSystem.startEngine(currentProfile.engineCount);
    gameState = 'playing';
    paused = false;
    lastTime = performance.now();
    // Lock pointer for controls
    setTimeout(() => inputManager.lockPointer(), 500);
}
function createInitialState(x, y, z) {
    return {
        position: { x, y, z },
        velocity: { x: 0, y: 0, z: 0 },
        orientation: { pitch: 0, roll: 0, yaw: 0 },
        angularVel: { pitch: 0, roll: 0, yaw: 0 },
        throttle: selectedScenario === 'takeoff' ? 0.1 : 0.6,
        airspeed: 0,
        aoa: 0,
        altitude: y,
        gForce: 1,
        stallWarning: false,
        damage: 0,
    };
}
function gameLoop(timestamp) {
    const dt = Math.min((timestamp - lastTime) / 1000, 0.1); // Cap delta time
    lastTime = timestamp;
    if (gameState === 'playing' && !paused) {
        update(dt);
    }
    // Always render
    renderer.render();
    requestAnimationFrame(gameLoop);
}
function update(dt) {
    // Get input
    const controls = inputManager.update();
    // Update throttle from input
    aircraftState.throttle += controls.throttle * dt * 0.8;
    aircraftState.throttle = Math.max(0, Math.min(1, aircraftState.throttle));
    // Run physics step
    (0, physics_1.physicsStep)(aircraftState, currentProfile, controls, dt);
    // Update renderer with new state
    renderer.updateAircraft(aircraftState, dt);
    // Update scenario
    let scenarioInfo = '';
    let instructions = '';
    const newEvents = [];
    if (takeoffScenario) {
        scenarioInfo = takeoffScenario.getPhase();
        instructions = takeoffScenario.getInstructions();
        const events = takeoffScenario.update(dt, aircraftState);
        for (const e of events)
            newEvents.push(e);
    }
    else if (dogfightScenario) {
        scenarioInfo = dogfightScenario.getPhase() + ` | Targets: ${dogfightScenario.getRemainingTargets()} | Score: ${dogfightScenario.getScore()}`;
        instructions = dogfightScenario.getInstructions();
        const events = dogfightScenario.update(dt, aircraftState);
        for (const e of events)
            newEvents.push(e);
        // Update drone positions in renderer
        const drones = dogfightScenario.drones || [];
        renderer.updateDrones(drones);
    }
    // Queue events for display
    for (const e of newEvents) {
        eventQueue.push({ text: e.text, type: e.type, createdAt: Date.now() });
    }
    // Update audio
    audioSystem.update(aircraftState.throttle, aircraftState.airspeed, aircraftState.altitude, aircraftState.stallWarning);
    // Render HUD
    hud.render(aircraftState, currentProfile, scenarioInfo, instructions);
    // Process event display
    updateEventDisplay();
}
function updateEventDisplay() {
    const container = document.getElementById('event-container');
    // Remove expired events (older than 4 seconds)
    const now = Date.now();
    eventQueue = eventQueue.filter(e => (now - e.createdAt) < 4000);
    // Show only the last few events
    const recentEvents = eventQueue.slice(-3);
    // Build HTML for events
    let html = '';
    for (const e of recentEvents) {
        html += `<div class="event-msg ${e.type}">${e.text}</div>`;
    }
    container.innerHTML = html;
}
// ===== Start the game when DOM is ready =====
init();
};

// ── 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.SEA_LEVEL_DENSITY = exports.GRAVITY = exports.SEA_LEVEL_TEMP = exports.SEA_LEVEL_PRESSURE = void 0;
// Atmospheric constants
exports.SEA_LEVEL_PRESSURE = 101325; // Pa
exports.SEA_LEVEL_TEMP = 288.15; // K
exports.GRAVITY = 9.81; // m/s²
exports.SEA_LEVEL_DENSITY = 1.225; // kg/m³
};

// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};

// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAirDensity = getAirDensity;
exports.physicsStep = physicsStep;
exports.getStallSpeed = getStallSpeed;
exports.getTurnRadius = getTurnRadius;
exports.getLoadFactor = getLoadFactor;
const types_1 = require("./types");
const DEG = Math.PI / 180;
// Atmospheric model (simplified ISA)
function getAirDensity(altitude) {
    // Exponential atmosphere model
    const scaleHeight = 8500; // meters
    return types_1.SEA_LEVEL_DENSITY * Math.exp(-Math.max(0, altitude) / scaleHeight);
}
// ===== Core Physics Step =====
function physicsStep(state, profile, input, dt) {
    // Clamp dt to prevent spiral of death
    dt = Math.min(dt, 0.05);
    const rho = getAirDensity(state.altitude);
    // --- Compute body-frame velocity ---
    const bodyVel = worldToBody(state.velocity, state.orientation);
    const u = bodyVel.x; // forward (x+)
    const v = bodyVel.y; // right (y+)
    const w = bodyVel.z; // down (z+)
    // Airspeed magnitude
    const V = Math.sqrt(u * u + v * v + w * w);
    state.airspeed = V;
    // Angle of attack (radians) - angle between velocity vector and wing plane
    let aoa;
    if (V > 0.1) {
        aoa = Math.atan2(w, u);
    }
    else {
        aoa = 0;
    }
    state.aoa = aoa;
    // --- Lift Calculation ---
    // Cl varies with AoA: linear region then stall
    const clMax = profile.clMax;
    const maxAoA = profile.maxAoA;
    let cl;
    if (Math.abs(aoa) < maxAoA) {
        // Linear lift curve: Cl = a * alpha + Cl0
        const a = clMax / maxAoA; // lift curve slope
        cl = a * aoa;
    }
    else {
        // Post-stall: lift drops dramatically
        const stallFactor = Math.max(0.1, 1 - (Math.abs(aoa) - maxAoA) * 5);
        cl = clMax * Math.sign(aoa) * stallFactor;
    }
    const dynamicPressure = 0.5 * rho * V * V;
    const liftForce = cl * dynamicPressure * profile.wingArea;
    // --- Drag Calculation ---
    // Total drag = parasitic + induced
    const cdParasite = profile.cd0;
    const cdInduced = profile.k * cl * cl;
    const cdTotal = cdParasite + cdInduced;
    const dragForce = cdTotal * dynamicPressure * profile.wingArea;
    // --- Thrust ---
    const thrust = state.throttle * profile.maxThrust;
    // --- Forces in body frame ---
    // Lift acts perpendicular to velocity (upward in body z)
    // Drag acts opposite to velocity
    // Thrust acts along body x axis
    let fxBody = thrust - dragForce * Math.cos(aoa);
    let fyBody = 0;
    let fzBody = liftForce - dragForce * Math.sin(aoa);
    // Gravity in body frame (projected)
    const pitch = state.orientation.pitch;
    const roll = state.orientation.roll;
    const gravityX = types_1.GRAVITY * Math.sin(pitch);
    const gravityZ = types_1.GRAVITY * Math.cos(pitch) * Math.cos(roll);
    const gravityY = types_1.GRAVITY * Math.cos(pitch) * Math.sin(roll);
    fxBody -= profile.mass * gravityX;
    fyBody -= profile.mass * gravityY;
    fzBody -= profile.mass * gravityZ;
    // --- Acceleration in body frame ---
    const ax = fxBody / profile.mass;
    const ay = fyBody / profile.mass;
    const az = fzBody / profile.mass;
    // Transform acceleration to world frame
    const worldAccel = bodyToWorld({ x: ax, y: ay, z: az }, state.orientation);
    // --- Update velocity ---
    state.velocity.x += worldAccel.x * dt;
    state.velocity.y += worldAccel.y * dt;
    state.velocity.z += worldAccel.z * dt;
    // --- Angular dynamics (control surface response) ---
    const lag = profile.controlLag;
    const responseFactor = Math.min(1, dt / lag);
    // Target angular rates from input
    const targetPitchRate = -input.pitch * profile.pitchRate; // negative: nose up = negative pitch rate in our convention
    const targetRollRate = input.roll * profile.rollRate;
    const targetYawRate = input.yaw * profile.yawRate;
    // Smooth angular velocity toward targets (simulates control surface lag)
    const responseSpeed = 1.0 / Math.max(lag, 0.02);
    const blend = 1 - Math.exp(-responseSpeed * dt);
    state.angularVel.pitch += (targetPitchRate - state.angularVel.pitch) * blend;
    state.angularVel.roll += (targetRollRate - state.angularVel.roll) * blend;
    state.angularVel.yaw += (targetYawRate - state.angularVel.yaw) * blend;
    // Add aerodynamic damping to angular rates (speed-dependent)
    const speedFactor = Math.min(1, V / 200);
    const dampFactor = Math.exp(-dt * (1.5 + speedFactor * 3));
    state.angularVel.pitch *= dampFactor;
    state.angularVel.roll *= dampFactor;
    state.angularVel.yaw *= dampFactor;
    // Control authority decreases at low speeds and high AoA
    const controlAuthority = Math.min(1, V / 50) * (state.stallWarning ? 0.3 : 1);
    state.angularVel.pitch *= controlAuthority;
    state.angularVel.roll *= controlAuthority;
    // --- Update orientation ---
    state.orientation.pitch += state.angularVel.pitch * dt;
    state.orientation.roll += state.angularVel.roll * dt;
    state.orientation.yaw += state.angularVel.yaw * dt;
    // Clamp pitch to prevent gimbal lock issues
    state.orientation.pitch = Math.max(-Math.PI / 2 + 0.01, Math.min(Math.PI / 2 - 0.01, state.orientation.pitch));
    // --- Update position ---
    state.position.x += state.velocity.x * dt;
    state.position.y += state.velocity.y * dt;
    state.position.z += state.velocity.z * dt;
    // --- Altitude (y is up in our world) ---
    state.altitude = -state.position.z; // z+ is down, so altitude = -z
    // --- G-Force calculation ---
    const totalAccelMag = Math.sqrt(worldAccel.x * worldAccel.x + worldAccel.y * worldAccel.y + (worldAccel.z + types_1.GRAVITY) * (worldAccel.z + types_1.GRAVITY));
    state.gForce = totalAccelMag / types_1.GRAVITY;
    // --- Stall detection ---
    const stallSpeed = Math.sqrt((2 * profile.mass * types_1.GRAVITY) / (rho * clMax * profile.wingArea));
    state.stallWarning = V < stallSpeed * 1.2 && Math.abs(aoa) > maxAoA * 0.7;
    // --- Ground collision ---
    if (state.altitude < 0) {
        const impactSpeed = Math.abs(state.velocity.z);
        if (impactSpeed > 5) {
            state.damage += impactSpeed * dt * 0.1;
            // Bounce with energy loss
            state.velocity.z *= -0.3;
            state.position.z = 0;
            state.altitude = 0;
        }
        else {
            state.position.z = 0;
            state.altitude = 0;
            // Ground friction
            state.velocity.x *= (1 - dt * 2);
            state.velocity.y *= (1 - dt * 2);
        }
    }
    // Clamp damage
    state.damage = Math.min(1, Math.max(0, state.damage));
}
// ===== Coordinate Transformations =====
function worldToBody(v, rot) {
    const cp = Math.cos(rot.pitch), sp = Math.sin(rot.pitch);
    const cr = Math.cos(rot.roll), sr = Math.sin(rot.roll);
    const cy = Math.cos(rot.yaw), sy = Math.sin(rot.yaw);
    // YXZ rotation order (yaw, then pitch, then roll)
    const x1 = v.x * cy + v.y * sy;
    const y1 = -v.x * sp * sy + v.y * sp * cy + v.z * cp;
    const z1 = v.x * cp * sy - v.y * cp * cy + v.z * sp;
    return {
        x: x1,
        y: -y1 * sr + z1 * cr,
        z: y1 * cr + z1 * sr
    };
}
function bodyToWorld(v, rot) {
    const cp = Math.cos(rot.pitch), sp = Math.sin(rot.pitch);
    const cr = Math.cos(rot.roll), sr = Math.sin(rot.roll);
    const cy = Math.cos(rot.yaw), sy = Math.sin(rot.yaw);
    // Inverse of YXZ rotation
    const y1 = -v.y * sr + v.z * cr;
    const z1 = v.y * cr + v.z * sr;
    return {
        x: v.x * cy - y1 * sp * sy + z1 * cp * sy,
        y: v.x * sy + y1 * sp * cy + z1 * cp * cy,
        z: -y1 * cp + z1 * sp
    };
}
// ===== Utility Functions =====
function getStallSpeed(profile, altitude) {
    const rho = getAirDensity(altitude);
    return Math.sqrt((2 * profile.mass * types_1.GRAVITY) / (rho * profile.clMax * profile.wingArea));
}
function getTurnRadius(speed, bankAngle) {
    if (Math.abs(bankAngle) < 0.01)
        return Infinity;
    return (speed * speed) / (types_1.GRAVITY * Math.tan(Math.abs(bankAngle)));
}
function getLoadFactor(bankAngle) {
    return 1 / Math.cos(bankAngle);
}
};

// ── module: src/aircraft.ts ──
__mods["src/aircraft.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.MODEL_SCALE = exports.AIRCRAFT_PROFILES = void 0;
exports.createAircraftMesh = createAircraftMesh;
exports.AIRCRAFT_PROFILES = {
    falcon: {
        name: 'F-16 Fighting Falcon',
        description: 'Lightweight multirole fighter. High thrust-to-weight ratio, exceptional maneuverability.',
        mass: 9200,
        wingArea: 27.87,
        wingspan: 9.45,
        length: 15.03,
        maxThrust: 128000, // F110-GE-129 with afterburner
        engineCount: 1,
        clMax: 1.6,
        cd0: 0.015,
        k: 0.04,
        maxAoA: 30 * Math.PI / 180,
        pitchRate: 2.5,
        rollRate: 4.0,
        yawRate: 1.2,
        controlLag: 0.08,
        color: 0x7a8b6f,
        accentColor: 0xcccccc,
    },
    thunderbolt: {
        name: 'A-10 Thunderbolt II',
        description: 'Close air support aircraft. Heavy armor, slow but stable, devastating firepower.',
        mass: 13500,
        wingArea: 47.5,
        wingspan: 17.53,
        length: 16.26,
        maxThrust: 105800, // Twin GE F404
        engineCount: 2,
        clMax: 1.8,
        cd0: 0.025,
        k: 0.035,
        maxAoA: 25 * Math.PI / 180,
        pitchRate: 1.5,
        rollRate: 2.5,
        yawRate: 0.9,
        controlLag: 0.15,
        color: 0x4a5d3e,
        accentColor: 0x8b7355,
    },
    fulcrum: {
        name: 'MiG-29 Fulcrum',
        description: 'Twin-engine interceptor. Excellent high-altitude performance and turn rate.',
        mass: 11000,
        wingArea: 38.05,
        wingspan: 11.36,
        length: 17.32,
        maxThrust: 164000, // Twin RD-33 with afterburner
        engineCount: 2,
        clMax: 1.5,
        cd0: 0.018,
        k: 0.038,
        maxAoA: 28 * Math.PI / 180,
        pitchRate: 2.2,
        rollRate: 3.5,
        yawRate: 1.0,
        controlLag: 0.1,
        color: 0x6b7d8e,
        accentColor: 0xff4444,
    },
};
// ===== 3D Model Generation using Three.js primitives =====
function createAircraftMesh(profile) {
    const group = new THREE.Group();
    const mainMat = new THREE.MeshPhongMaterial({ color: profile.color, shininess: 80 });
    const accentMat = new THREE.MeshPhongMaterial({ color: profile.accentColor, shininess: 120 });
    const darkMat = new THREE.MeshPhongMaterial({ color: 0x333333, shininess: 60 });
    if (profile.name.includes('Falcon')) {
        buildFalcon(group, mainMat, accentMat, darkMat);
    }
    else if (profile.name.includes('Thunderbolt')) {
        buildA10(group, mainMat, accentMat, darkMat);
    }
    else {
        buildMiG29(group, mainMat, accentMat, darkMat);
    }
    return group;
}
function buildFalcon(group, bodyMat, accentMat, darkMat) {
    // Fuselage - pointed nose to tail
    const fuselageGeo = new THREE.CylinderGeometry(0.35, 0.25, 8, 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.35, 2, 8);
    const nose = new THREE.Mesh(noseGeo, bodyMat);
    nose.rotation.x = -Math.PI / 2;
    nose.position.z = -5;
    group.add(nose);
    // Cockpit canopy (glass)
    const cockpitGeo = new THREE.SphereGeometry(0.4, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
    const cockpitMat = new THREE.MeshPhongMaterial({ color: 0x88ccff, transparent: true, opacity: 0.6, shininess: 200 });
    const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
    cockpit.position.set(0, 0.35, -1.5);
    group.add(cockpit);
    // Main wings (swept)
    const wingShape = new THREE.Shape();
    wingShape.moveTo(0, 0);
    wingShape.lineTo(4, -2);
    wingShape.lineTo(4.5, -3.5);
    wingShape.lineTo(0.5, -3.5);
    wingShape.closePath();
    const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.12, bevelEnabled: false });
    const leftWing = new THREE.Mesh(wingGeo, bodyMat);
    leftWing.rotation.x = -Math.PI / 2;
    leftWing.position.set(0.35, -0.1, -1);
    group.add(leftWing);
    const rightWing = new THREE.Mesh(wingGeo, bodyMat);
    rightWing.rotation.x = Math.PI / 2;
    rightWing.rotation.z = Math.PI;
    rightWing.position.set(-0.35, -0.1, -1);
    group.add(rightWing);
    // Vertical stabilizer (tail fin)
    const tailFinGeo = new THREE.BoxGeometry(0.08, 2, 2.5);
    const tailFin = new THREE.Mesh(tailFinGeo, bodyMat);
    tailFin.position.set(0, 1, 3);
    group.add(tailFin);
    // Horizontal stabilizers (swept back)
    const hStabGeo = new THREE.BoxGeometry(2.5, 0.08, 1.5);
    const leftHStab = new THREE.Mesh(hStabGeo, bodyMat);
    leftHStab.position.set(0.8, -0.1, 3.2);
    group.add(leftHStab);
    const rightHStab = new THREE.Mesh(hStabGeo, bodyMat);
    rightHStab.position.set(-0.8, -0.1, 3.2);
    group.add(rightHStab);
    // Engine intake (side-mounted)
    const intakeGeo = new THREE.BoxGeometry(0.6, 0.5, 1.5);
    const leftIntake = new THREE.Mesh(intakeGeo, darkMat);
    leftIntake.position.set(0.7, -0.2, -0.5);
    group.add(leftIntake);
    const rightIntake = new THREE.Mesh(intakeGeo, darkMat);
    rightIntake.position.set(-0.7, -0.2, -0.5);
    group.add(rightIntake);
    // Engine exhaust
    const exhaustGeo = new THREE.CylinderGeometry(0.3, 0.4, 1, 8);
    const exhaust = new THREE.Mesh(exhaustGeo, darkMat);
    exhaust.rotation.x = Math.PI / 2;
    exhaust.position.set(0, 0, 4.5);
    group.add(exhaust);
    // Afterburner glow (emissive)
    const flameMat = new THREE.MeshPhongMaterial({ color: 0xff6600, emissive: 0xff3300, emissiveIntensity: 0.5 });
    const flameGeo = new THREE.ConeGeometry(0.25, 1.5, 8);
    const flame = new THREE.Mesh(flameGeo, flameMat);
    flame.rotation.x = -Math.PI / 2;
    flame.position.set(0, 0, 5.5);
    flame.name = 'flame';
    group.add(flame);
    // Wingtip missiles (accent)
    const missileGeo = new THREE.CylinderGeometry(0.06, 0.06, 1.2, 6);
    const leftMissile = new THREE.Mesh(missileGeo, accentMat);
    leftMissile.rotation.x = Math.PI / 2;
    leftMissile.position.set(4.5, -0.1, -2.5);
    group.add(leftMissile);
    const rightMissile = new THREE.Mesh(missileGeo, accentMat);
    rightMissile.rotation.x = Math.PI / 2;
    rightMissile.position.set(-4.5, -0.1, -2.5);
    group.add(rightMissile);
}
function buildA10(group, bodyMat, accentMat, darkMat) {
    // Fuselage - long and thick
    const fuselageGeo = new THREE.CylinderGeometry(0.5, 0.4, 10, 8);
    const fuselage = new THREE.Mesh(fuselageGeo, bodyMat);
    fuselage.rotation.x = Math.PI / 2;
    group.add(fuselage);
    // Nose
    const noseGeo = new THREE.ConeGeometry(0.5, 1.5, 8);
    const nose = new THREE.Mesh(noseGeo, bodyMat);
    nose.rotation.x = -Math.PI / 2;
    nose.position.z = -5.75;
    group.add(nose);
    // Cockpit (tandem seating)
    const cockpitGeo = new THREE.BoxGeometry(0.8, 0.6, 2);
    const cockpitMat = new THREE.MeshPhongMaterial({ color: 0x88ccff, transparent: true, opacity: 0.5 });
    const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
    cockpit.position.set(0, 0.45, -2);
    group.add(cockpit);
    // Wings (straight, wide)
    const wingGeo = new THREE.BoxGeometry(8, 0.15, 3);
    const leftWing = new THREE.Mesh(wingGeo, bodyMat);
    leftWing.position.set(4, -0.1, -1);
    group.add(leftWing);
    const rightWing = new THREE.Mesh(wingGeo, bodyMat);
    rightWing.position.set(-4, -0.1, -1);
    group.add(rightWing);
    // Wing hardpoints (weapons)
    const rocketGeo = new THREE.CylinderGeometry(0.08, 0.08, 2, 6);
    for (let i = 0; i < 3; i++) {
        const r1 = new THREE.Mesh(rocketGeo, accentMat);
        r1.rotation.x = Math.PI / 2;
        r1.position.set(5 + i * 1.2, -0.4, -1);
        group.add(r1);
        const r2 = new THREE.Mesh(rocketGeo, accentMat);
        r2.rotation.x = Math.PI / 2;
        r2.position.set(-5 - i * 1.2, -0.4, -1);
        group.add(r2);
    }
    // Twin vertical stabilizers (angled outward)
    const tailFinGeo = new THREE.BoxGeometry(0.1, 2.5, 2);
    const leftTail = new THREE.Mesh(tailFinGeo, bodyMat);
    leftTail.position.set(1.2, 1.2, 4);
    leftTail.rotation.z = -0.3;
    group.add(leftTail);
    const rightTail = new THREE.Mesh(tailFinGeo, bodyMat);
    rightTail.position.set(-1.2, 1.2, 4);
    rightTail.rotation.z = 0.3;
    group.add(rightTail);
    // Engines (mounted high on fuselage)
    const engineGeo = new THREE.CylinderGeometry(0.35, 0.4, 2, 8);
    const leftEngine = new THREE.Mesh(engineGeo, darkMat);
    leftEngine.rotation.x = Math.PI / 2;
    leftEngine.position.set(1, 0.6, 3);
    group.add(leftEngine);
    const rightEngine = new THREE.Mesh(engineGeo, darkMat);
    rightEngine.rotation.x = Math.PI / 2;
    rightEngine.position.set(-1, 0.6, 3);
    group.add(rightEngine);
    // Engine exhausts
    const flameMat = new THREE.MeshPhongMaterial({ color: 0xff4400, emissive: 0xff2200, emissiveIntensity: 0.5 });
    for (let i = 0; i < 2; i++) {
        const flameGeo = new THREE.ConeGeometry(0.3, 1.2, 8);
        const flame = new THREE.Mesh(flameGeo, flameMat);
        flame.rotation.x = -Math.PI / 2;
        flame.position.set(i === 0 ? 1 : -1, 0.6, 5);
        flame.name = 'flame';
        group.add(flame);
    }
    // GAU-8 cannon (visible under nose)
    const cannonGeo = new THREE.CylinderGeometry(0.12, 0.12, 3, 8);
    const cannon = new THREE.Mesh(cannonGeo, darkMat);
    cannon.rotation.x = Math.PI / 2;
    cannon.position.set(0, -0.5, -4);
    group.add(cannon);
}
function buildMiG29(group, bodyMat, accentMat, darkMat) {
    // Fuselage
    const fuselageGeo = new THREE.CylinderGeometry(0.4, 0.3, 10, 8);
    const fuselage = new THREE.Mesh(fuselageGeo, bodyMat);
    fuselage.rotation.x = Math.PI / 2;
    group.add(fuselage);
    // Nose
    const noseGeo = new THREE.ConeGeometry(0.4, 2, 8);
    const nose = new THREE.Mesh(noseGeo, bodyMat);
    nose.rotation.x = -Math.PI / 2;
    nose.position.z = -6;
    group.add(nose);
    // Cockpit
    const cockpitGeo = new THREE.SphereGeometry(0.45, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
    const cockpitMat = new THREE.MeshPhongMaterial({ color: 0x88ccff, transparent: true, opacity: 0.5 });
    const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
    cockpit.position.set(0, 0.4, -2);
    group.add(cockpit);
    // Wings (swept with leading edge extensions)
    const wingShape = new THREE.Shape();
    wingShape.moveTo(0, 0);
    wingShape.lineTo(3.5, -1.5);
    wingShape.lineTo(4, -3);
    wingShape.lineTo(0.8, -3);
    wingShape.closePath();
    const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.12, bevelEnabled: false });
    const leftWing = new THREE.Mesh(wingGeo, bodyMat);
    leftWing.rotation.x = -Math.PI / 2;
    leftWing.position.set(0.45, -0.1, -1.5);
    group.add(leftWing);
    const rightWing = new THREE.Mesh(wingGeo, bodyMat);
    rightWing.rotation.x = Math.PI / 2;
    rightWing.rotation.z = Math.PI;
    rightWing.position.set(-0.45, -0.1, -1.5);
    group.add(rightWing);
    // Leading edge extensions (LERX)
    const lerxGeo = new THREE.BoxGeometry(1.2, 0.08, 3);
    const leftLerx = new THREE.Mesh(lerxGeo, bodyMat);
    leftLerx.position.set(0.6, 0.15, -2.5);
    group.add(leftLerx);
    const rightLerx = new THREE.Mesh(lerxGeo, bodyMat);
    rightLerx.position.set(-0.6, 0.15, -2.5);
    group.add(rightLerx);
    // Twin vertical stabilizers
    const tailFinGeo = new THREE.BoxGeometry(0.08, 2.2, 2.5);
    const leftTail = new THREE.Mesh(tailFinGeo, bodyMat);
    leftTail.position.set(1, 1.1, 3.5);
    group.add(leftTail);
    const rightTail = new THREE.Mesh(tailFinGeo, bodyMat);
    rightTail.position.set(-1, 1.1, 3.5);
    group.add(rightTail);
    // Red star accent on tail fins
    const starGeo = new THREE.CircleGeometry(0.4, 6);
    const starMat = new THREE.MeshPhongMaterial({ color: 0xff2222 });
    for (let i = 0; i < 2; i++) {
        const star = new THREE.Mesh(starGeo, starMat);
        star.position.set(i === 0 ? 1.05 : -1.05, 1.1, 3.5);
        star.rotation.y = i === 0 ? Math.PI / 2 : -Math.PI / 2;
        group.add(star);
    }
    // Twin engines (centerline intakes)
    const intakeGeo = new THREE.BoxGeometry(0.8, 0.4, 1.5);
    const leftIntake = new THREE.Mesh(intakeGeo, darkMat);
    leftIntake.position.set(0.6, -0.2, -1);
    group.add(leftIntake);
    const rightIntake = new THREE.Mesh(intakeGeo, darkMat);
    rightIntake.position.set(-0.6, -0.2, -1);
    group.add(rightIntake);
    // Twin exhausts
    const flameMat = new THREE.MeshPhongMaterial({ color: 0xff5500, emissive: 0xff3300, emissiveIntensity: 0.5 });
    for (let i = 0; i < 2; i++) {
        const exhaustGeo = new THREE.CylinderGeometry(0.25, 0.35, 1, 8);
        const exhaust = new THREE.Mesh(exhaustGeo, darkMat);
        exhaust.rotation.x = Math.PI / 2;
        exhaust.position.set(i === 0 ? 0.6 : -0.6, 0, 5.5);
        group.add(exhaust);
        const flameGeo = new THREE.ConeGeometry(0.2, 1.5, 8);
        const flame = new THREE.Mesh(flameGeo, flameMat);
        flame.rotation.x = -Math.PI / 2;
        flame.position.set(i === 0 ? 0.6 : -0.6, 0, 6.5);
        flame.name = 'flame';
        group.add(flame);
    }
    // Wingtip missiles
    const missileGeo = new THREE.CylinderGeometry(0.07, 0.07, 1.5, 6);
    const leftMissile = new THREE.Mesh(missileGeo, accentMat);
    leftMissile.rotation.x = Math.PI / 2;
    leftMissile.position.set(4, -0.1, -2.5);
    group.add(leftMissile);
    const rightMissile = new THREE.Mesh(missileGeo, accentMat);
    rightMissile.rotation.x = Math.PI / 2;
    rightMissile.position.set(-4, -0.1, -2.5);
    group.add(rightMissile);
}
// Scale factor for visual representation (models are scaled down from real size)
exports.MODEL_SCALE = 0.3;
};

// ── module: src/terrain.ts ──
__mods["src/terrain.ts"] = function (exports, require, module) {
"use strict";
// ===== Procedural Terrain Generation for Apex Aero =====
Object.defineProperty(exports, "__esModule", { value: true });
exports.terrainNoise = void 0;
exports.getTerrainHeight = getTerrainHeight;
exports.createTerrainChunk = createTerrainChunk;
exports.createRunway = createRunway;
exports.createWaterPlane = createWaterPlane;
exports.createSkyDome = createSkyDome;
exports.createClouds = createClouds;
// Simplex-like noise implementation (compact 2D)
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, z) {
        return g[0] * x + g[1] * y + g[2] * z;
    }
    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, 0));
        let t1 = 0.5 - x1 * x1 - y1 * y1;
        n1 = t1 < 0 ? 0 : (t1 *= t1, t1 * t1 * this.dot3(this.grad3[gi1], x1, y1, 0));
        let t2 = 0.5 - x2 * x2 - y2 * y2;
        n2 = t2 < 0 ? 0 : (t2 *= t2, t2 * t2 * this.dot3(this.grad3[gi2], x2, y2, 0));
        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;
    }
}
exports.terrainNoise = new Noise(137);
// ===== Heightmap sampling =====
function getTerrainHeight(x, z) {
    // Multi-octave noise for varied terrain
    let h = 0;
    // Large-scale features (mountains, valleys)
    h += exports.terrainNoise.fbm(x * 0.001, z * 0.001, 4) * 800;
    // Medium detail
    h += exports.terrainNoise.fbm(x * 0.005 + 100, z * 0.005 + 100, 3) * 200;
    // Small detail
    h += exports.terrainNoise.fbm(x * 0.02 + 200, z * 0.02 + 200, 2) * 50;
    // Flatten near origin for runway area (within 500m radius)
    const distFromOrigin = Math.sqrt(x * x + z * z);
    if (distFromOrigin < 600) {
        const flattenFactor = Math.max(0, 1 - (distFromOrigin / 600));
        h *= (1 - flattenFactor * 0.95); // Flatten to near sea level
    }
    return Math.max(-20, h); // Minimum height slightly below sea level for water effect
}
// ===== Terrain Mesh Generation =====
function createTerrainChunk(centerX, centerZ, size, segments = 128) {
    const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
    geometry.rotateX(-Math.PI / 2); // Lay flat on XZ plane
    const positions = geometry.attributes.position.array;
    const colors = new Float32Array(positions.length);
    for (let i = 0; i < positions.length; i += 3) {
        const worldX = centerX + positions[i];
        const worldZ = centerZ + positions[i + 2];
        const height = getTerrainHeight(worldX, worldZ);
        positions[i + 1] = height;
        // Vertex coloring based on height and slope
        const normalizedH = Math.max(0, Math.min(1, (height + 50) / 900));
        if (normalizedH < 0.08) {
            // Water/shore - sandy color
            colors[i] = 0.76;
            colors[i + 1] = 0.7;
            colors[i + 2] = 0.5;
        }
        else if (normalizedH < 0.3) {
            // Lowland grass
            const t = (normalizedH - 0.08) / 0.22;
            colors[i] = 0.15 + t * 0.1;
            colors[i + 1] = 0.45 - t * 0.1;
            colors[i + 2] = 0.1 + t * 0.05;
        }
        else if (normalizedH < 0.6) {
            // Highland/forest
            const t = (normalizedH - 0.3) / 0.3;
            colors[i] = 0.25 - t * 0.1;
            colors[i + 1] = 0.35 - t * 0.1;
            colors[i + 2] = 0.15 - t * 0.05;
        }
        else if (normalizedH < 0.8) {
            // Rocky terrain
            const t = (normalizedH - 0.6) / 0.2;
            colors[i] = 0.4 + t * 0.1;
            colors[i + 1] = 0.35 + t * 0.1;
            colors[i + 2] = 0.3 + t * 0.1;
        }
        else {
            // Snow caps
            const t = (normalizedH - 0.8) / 0.2;
            colors[i] = 0.5 + t * 0.45;
            colors[i + 1] = 0.45 + t * 0.45;
            colors[i + 2] = 0.4 + t * 0.5;
        }
    }
    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    geometry.computeVertexNormals();
    const material = new THREE.MeshPhongMaterial({
        vertexColors: true,
        flatShading: false,
        shininess: 10,
    });
    return new THREE.Mesh(geometry, material);
}
// ===== Runway Generation =====
function createRunway() {
    const group = new THREE.Group();
    // Main runway surface
    const runwayGeo = new THREE.PlaneGeometry(60, 2000);
    runwayGeo.rotateX(-Math.PI / 2);
    const runwayMat = new THREE.MeshPhongMaterial({ color: 0x333333 });
    const runway = new THREE.Mesh(runwayGeo, runwayMat);
    runway.position.set(0, 1.5, -500); // Centered at origin area
    group.add(runway);
    // Runway markings (center line)
    for (let i = 0; i < 40; i++) {
        const markGeo = new THREE.PlaneGeometry(2, 15);
        markGeo.rotateX(-Math.PI / 2);
        const markMat = new THREE.MeshPhongMaterial({ color: 0xffffff });
        const mark = new THREE.Mesh(markGeo, markMat);
        mark.position.set(0, 1.6, -500 + i * 25 - 487);
        group.add(mark);
    }
    // Threshold markings (large white rectangles at each end)
    for (let end = 0; end < 2; end++) {
        const zPos = end === 0 ? 510 : -1510;
        for (let i = 0; i < 4; i++) {
            const barGeo = new THREE.PlaneGeometry(8, 3);
            barGeo.rotateX(-Math.PI / 2);
            const barMat = new THREE.MeshPhongMaterial({ color: 0xffffff });
            const bar = new THREE.Mesh(barGeo, barMat);
            bar.position.set((i - 1.5) * 10, 1.6, zPos);
            group.add(bar);
        }
    }
    // Runway edge lights (small emissive dots)
    const lightMat = new THREE.MeshPhongMaterial({ color: 0x44ff44, emissive: 0x22aa22 });
    for (let i = 0; i < 50; i++) {
        const zPos = -1500 + i * 60;
        for (let side = -1; side <= 1; side += 2) {
            const lightGeo = new THREE.SphereGeometry(0.3, 4, 4);
            const light = new THREE.Mesh(lightGeo, lightMat);
            light.position.set(side * 32, 1.7, zPos);
            group.add(light);
        }
    }
    // Taxiway (perpendicular to runway)
    const taxiGeo = new THREE.PlaneGeometry(20, 400);
    taxiGeo.rotateX(-Math.PI / 2);
    const taxiMat = new THREE.MeshPhongMaterial({ color: 0x3a3a3a });
    const taxiway = new THREE.Mesh(taxiGeo, taxiMat);
    taxiway.position.set(50, 1.4, -500);
    group.add(taxiway);
    // Control tower (simple structure)
    const towerBaseGeo = new THREE.CylinderGeometry(8, 10, 30, 8);
    const towerMat = new THREE.MeshPhongMaterial({ color: 0x667788 });
    const tower = new THREE.Mesh(towerBaseGeo, towerMat);
    tower.position.set(-50, 15, -200);
    group.add(tower);
    // Tower cab (glass)
    const cabGeo = new THREE.BoxGeometry(14, 6, 14);
    const cabMat = new THREE.MeshPhongMaterial({ color: 0x88bbdd, transparent: true, opacity: 0.5 });
    const cab = new THREE.Mesh(cabGeo, cabMat);
    cab.position.set(-50, 33, -200);
    group.add(cab);
    return group;
}
// ===== Water Plane (for low areas) =====
function createWaterPlane() {
    const geo = new THREE.PlaneGeometry(20000, 20000);
    geo.rotateX(-Math.PI / 2);
    const mat = new THREE.MeshPhongMaterial({
        color: 0x1a5276,
        transparent: true,
        opacity: 0.7,
        shininess: 100,
        specular: new THREE.Color(0x88ccff),
    });
    const water = new THREE.Mesh(geo, mat);
    water.position.y = -5; // Slightly below sea level
    return water;
}
// ===== Sky Dome =====
function createSkyDome() {
    const geo = new THREE.SphereGeometry(15000, 32, 16);
    const mat = new THREE.MeshPhongMaterial({
        color: 0x87ceeb,
        side: THREE.BackSide,
        fog: false,
    });
    return new THREE.Mesh(geo, mat);
}
// ===== Cloud Generation =====
function createClouds() {
    const group = new THREE.Group();
    const cloudMat = new THREE.MeshPhongMaterial({
        color: 0xffffff,
        transparent: true,
        opacity: 0.8,
        fog: false,
    });
    for (let i = 0; i < 60; i++) {
        const cloudGroup = new THREE.Group();
        const numPuffs = 3 + Math.floor(Math.random() * 5);
        for (let j = 0; j < numPuffs; j++) {
            const puffGeo = new THREE.SphereGeometry(20 + Math.random() * 40, 8, 6);
            const puff = new THREE.Mesh(puffGeo, cloudMat);
            puff.position.set((Math.random() - 0.5) * 80, (Math.random() - 0.5) * 15, (Math.random() - 0.5) * 40);
            puff.scale.y = 0.4; // Flatten clouds
            cloudGroup.add(puff);
        }
        cloudGroup.position.set((Math.random() - 0.5) * 12000, 800 + Math.random() * 600, (Math.random() - 0.5) * 12000);
        group.add(cloudGroup);
    }
    return group;
}
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
// ===== Dynamic Audio System for Apex Aero =====
// Uses Web Audio API to generate procedural engine sounds, wind noise, and alerts
Object.defineProperty(exports, "__esModule", { value: true });
exports.FlightAudio = void 0;
class FlightAudio {
    constructor() {
        this.ctx = null;
        this.masterGain = null;
        // Engine sound nodes
        this.engineOsc1 = null;
        this.engineOsc2 = null;
        this.engineGain = null;
        this.engineFilter = null;
        // Wind noise nodes
        this.windNoise = null;
        this.windGain = null;
        this.windFilter = null;
        // Stall warning oscillator
        this.stallOsc = null;
        this.stallGain = null;
        // Alert sound
        this.alertOsc = null;
        this.alertGain = null;
        this.isPlaying = false;
        this.lastThrottle = 0;
        this.lastAirspeed = 0;
    }
    init() {
        if (this.ctx)
            return;
        this.ctx = new AudioContext();
        this.masterGain = this.ctx.createGain();
        this.masterGain.gain.value = 0.5;
        this.masterGain.connect(this.ctx.destination);
    }
    resume() {
        if (this.ctx && this.ctx.state === 'suspended') {
            this.ctx.resume();
        }
    }
    startEngine(engineCount) {
        if (!this.ctx || !this.masterGain)
            return;
        if (this.isPlaying)
            return;
        this.isPlaying = true;
        // Engine sound - layered oscillators for realistic turbine noise
        this.engineFilter = this.ctx.createBiquadFilter();
        this.engineFilter.type = 'lowpass';
        this.engineFilter.frequency.value = 400;
        this.engineFilter.Q.value = 2;
        this.engineGain = this.ctx.createGain();
        this.engineGain.gain.value = 0;
        // Low rumble (fundamental)
        this.engineOsc1 = this.ctx.createOscillator();
        this.engineOsc1.type = 'sawtooth';
        this.engineOsc1.frequency.value = 60 * engineCount;
        // Higher harmonic for turbine whine
        this.engineOsc2 = this.ctx.createOscillator();
        this.engineOsc2.type = 'square';
        this.engineOsc2.frequency.value = 180 * engineCount;
        this.engineOsc1.connect(this.engineFilter);
        this.engineOsc2.connect(this.engineFilter);
        this.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.windNoise = this.ctx.createBufferSource();
        this.windNoise.buffer = buffer;
        this.windNoise.loop = true;
        this.windFilter = this.ctx.createBiquadFilter();
        this.windFilter.type = 'bandpass';
        this.windFilter.frequency.value = 500;
        this.windFilter.Q.value = 0.5;
        this.windGain = this.ctx.createGain();
        this.windGain.gain.value = 0;
        this.windNoise.connect(this.windFilter);
        this.windFilter.connect(this.windGain);
        this.windGain.connect(this.masterGain);
        this.windNoise.start();
        // Stall warning oscillator (silent until needed)
        this.stallOsc = this.ctx.createOscillator();
        this.stallOsc.type = 'square';
        this.stallOsc.frequency.value = 800;
        this.stallGain = this.ctx.createGain();
        this.stallGain.gain.value = 0;
        this.stallOsc.connect(this.stallGain);
        this.stallGain.connect(this.masterGain);
        this.stallOsc.start();
        // Alert oscillator (silent until needed)
        this.alertOsc = this.ctx.createOscillator();
        this.alertOsc.type = 'sine';
        this.alertOsc.frequency.value = 1200;
        this.alertGain = this.ctx.createGain();
        this.alertGain.gain.value = 0;
        this.alertOsc.connect(this.alertGain);
        this.alertGain.connect(this.masterGain);
        this.alertOsc.start();
    }
    update(throttle, airspeed, altitude, stallWarning) {
        if (!this.ctx || !this.engineOsc1 || !this.engineOsc2 || !this.engineFilter)
            return;
        // Smooth throttle transitions to avoid audio clicks
        const targetThrottle = Math.max(0, Math.min(1, throttle));
        this.lastThrottle += (targetThrottle - this.lastThrottle) * 0.1;
        this.lastAirspeed += (airspeed - this.lastAirspeed) * 0.05;
        // Engine frequency scales with throttle
        const baseFreq = 40 + this.lastThrottle * 200;
        this.engineOsc1.frequency.value = baseFreq;
        this.engineOsc2.frequency.value = baseFreq * 3;
        // Filter opens up at higher RPM (more high-frequency content)
        this.engineFilter.frequency.value = 200 + this.lastThrottle * 2000;
        // Engine volume scales with throttle, reduced at altitude
        const altFactor = Math.max(0.3, 1 - altitude / 15000);
        const engineVol = (0.05 + this.lastThrottle * 0.4) * altFactor;
        if (this.engineGain) {
            this.engineGain.gain.value += (engineVol - this.engineGain.gain.value) * 0.1;
        }
        // Wind noise scales with airspeed
        const windVol = Math.min(0.3, this.lastAirspeed / 400);
        if (this.windGain && this.windFilter) {
            this.windGain.gain.value += (windVol - this.windGain.gain.value) * 0.05;
            // Wind filter frequency shifts with speed
            this.windFilter.frequency.value = 300 + this.lastAirspeed * 5;
        }
        // Stall warning sound
        if (this.stallOsc && this.stallGain) {
            const stallVol = stallWarning ? 0.15 : 0;
            this.stallGain.gain.value += (stallVol - this.stallGain.gain.value) * 0.2;
            // Pulsing effect
            if (stallWarning) {
                this.stallOsc.frequency.value = 600 + Math.sin(Date.now() / 150) * 400;
            }
        }
    }
    playAlert() {
        if (!this.alertGain || !this.alertOsc)
            return;
        this.alertGain.gain.value = 0.2;
        this.alertOsc.frequency.value = 1500;
        setTimeout(() => {
            if (this.alertGain)
                this.alertGain.gain.value = 0;
        }, 300);
    }
    playExplosion() {
        if (!this.ctx || !this.masterGain)
            return;
        // Create noise burst for explosion
        const bufferSize = this.ctx.sampleRate * 0.5;
        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 / bufferSize;
            data[i] = (Math.random() * 2 - 1) * Math.exp(-t * 8);
        }
        const source = this.ctx.createBufferSource();
        source.buffer = buffer;
        const filter = this.ctx.createBiquadFilter();
        filter.type = 'lowpass';
        filter.frequency.value = 800;
        const gain = this.ctx.createGain();
        gain.gain.value = 0.5;
        source.connect(filter);
        filter.connect(gain);
        gain.connect(this.masterGain);
        source.start();
    }
    stop() {
        if (this.engineOsc1) {
            try {
                this.engineOsc1.stop();
            }
            catch { }
        }
        if (this.engineOsc2) {
            try {
                this.engineOsc2.stop();
            }
            catch { }
        }
        if (this.windNoise) {
            try {
                this.windNoise.stop();
            }
            catch { }
        }
        if (this.stallOsc) {
            try {
                this.stallOsc.stop();
            }
            catch { }
        }
        if (this.alertOsc) {
            try {
                this.alertOsc.stop();
            }
            catch { }
        }
        this.isPlaying = false;
    }
    setVolume(vol) {
        if (this.masterGain) {
            this.masterGain.gain.value = vol;
        }
    }
}
exports.FlightAudio = FlightAudio;
};

// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputManager = void 0;
exports.setupThrottleWheel = setupThrottleWheel;
class InputManager {
    constructor() {
        this.keys = new Set();
        this.mouse = { dx: 0, dy: 0, locked: false };
        this.controlSurface = { pitch: 0, roll: 0, yaw: 0, throttle: 0 };
        window.addEventListener('keydown', (e) => this.onKeyDown(e));
        window.addEventListener('keyup', (e) => this.onKeyUp(e));
        document.addEventListener('mousemove', (e) => this.onMouseMove(e));
        document.addEventListener('pointerlockchange', () => {
            this.mouse.locked = document.pointerLockElement !== null;
        });
    }
    onKeyDown(e) {
        this.keys.add(e.code);
        e.preventDefault();
    }
    onKeyUp(e) {
        this.keys.delete(e.code);
        e.preventDefault();
    }
    onMouseMove(e) {
        if (this.mouse.locked) {
            this.mouse.dx += e.movementX;
            this.mouse.dy += e.movementY;
        }
    }
    lockPointer() {
        const canvas = document.querySelector('canvas');
        if (canvas) {
            canvas.requestPointerLock();
        }
    }
    update() {
        // Reset control surface inputs
        this.controlSurface.pitch = 0;
        this.controlSurface.roll = 0;
        this.controlSurface.yaw = 0;
        this.controlSurface.throttle = 0;
        // Keyboard controls (primary)
        if (this.keys.has('KeyW') || this.keys.has('ArrowUp')) {
            this.controlSurface.pitch = -1; // Nose up
        }
        if (this.keys.has('KeyS') || this.keys.has('ArrowDown')) {
            this.controlSurface.pitch = 1; // Nose down
        }
        if (this.keys.has('KeyA') || this.keys.has('ArrowLeft')) {
            this.controlSurface.roll = -1; // Left bank
        }
        if (this.keys.has('KeyD') || this.keys.has('ArrowRight')) {
            this.controlSurface.roll = 1; // Right bank
        }
        if (this.keys.has('KeyQ')) {
            this.controlSurface.yaw = -1; // Rudder left
        }
        if (this.keys.has('KeyE')) {
            this.controlSurface.yaw = 1; // Rudder right
        }
        // Throttle controls
        if (this.keys.has('ShiftLeft') || this.keys.has('Equal')) {
            this.controlSurface.throttle = 1; // Increase throttle
        }
        if (this.keys.has('ControlLeft') || this.keys.has('Minus')) {
            this.controlSurface.throttle = -1; // Decrease throttle
        }
        // Mouse controls (when pointer locked) - add to keyboard inputs
        if (this.mouse.locked) {
            const sensitivity = 0.003;
            this.controlSurface.pitch += Math.max(-1, Math.min(1, this.mouse.dy * sensitivity));
            this.controlSurface.roll += Math.max(-1, Math.min(1, -this.mouse.dx * sensitivity));
            // Mouse wheel for throttle (handled separately)
        }
        // Clear mouse delta after reading
        this.mouse.dx = 0;
        this.mouse.dy = 0;
        return { ...this.controlSurface };
    }
    isPressed(code) {
        return this.keys.has(code);
    }
}
exports.InputManager = InputManager;
// Mouse wheel handler for throttle (needs to be set up once)
function setupThrottleWheel(onThrottleChange) {
    document.addEventListener('wheel', (e) => {
        onThrottleChange(-Math.sign(e.deltaY));
        e.preventDefault();
    }, { passive: false });
}
};

// ── module: src/scenarios.ts ──
__mods["src/scenarios.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DogfightScenario = exports.TakeoffScenario = void 0;
const terrain_1 = require("./terrain");
class TakeoffScenario {
    constructor() {
        this.phase = 'taxi';
        this.events = [];
        this.startTime = 0;
        this.runwayEndZ = -1500;
        this.runwayStartZ = 500;
        this.landed = false;
        this.startTime = performance.now();
    }
    update(dt, state) {
        const newEvents = [];
        const elapsed = (performance.now() - this.startTime) / 1000;
        // Phase transitions based on aircraft state
        switch (this.phase) {
            case 'taxi':
                if (!this.landed && state.altitude < 5 && state.airspeed > 20) {
                    newEvents.push({ type: 'objective', text: '🛫 Accelerate to takeoff speed! Full throttle!' });
                }
                if (state.altitude >= 5 && state.airspeed > 40) {
                    this.phase = 'takeoff';
                    newEvents.push({ type: 'success', text: '✈️ LIFTOFF! You are airborne!', duration: 3000 });
                }
                break;
            case 'takeoff':
                if (state.altitude > 100 && state.airspeed > 60) {
                    this.phase = 'climb';
                    newEvents.push({ type: 'objective', text: '📈 Climb to 500m altitude and cruise at 150+ m/s' });
                }
                break;
            case 'climb':
                if (state.altitude > 500 && state.airspeed > 150) {
                    this.phase = 'cruise';
                    newEvents.push({ type: 'success', text: '✅ Cruise altitude reached! Now descend for landing.', duration: 4000 });
                }
                else if (state.altitude < 20 && state.airspeed > 30) {
                    // Too low during climb phase - warning
                    newEvents.push({ type: 'warning', text: '⚠️ Pull up! Maintain altitude!' });
                }
                break;
            case 'cruise':
                if (state.altitude < 200 && state.airspeed > 50) {
                    this.phase = 'descent';
                    newEvents.push({ type: 'objective', text: '📉 Descending... Align with runway and prepare for landing' });
                }
                break;
            case 'descent':
                if (state.altitude < 50 && state.airspeed > 40 && state.airspeed < 120) {
                    this.phase = 'landing';
                    newEvents.push({ type: 'objective', text: '🛬 Final approach! Reduce throttle, aim for runway' });
                }
                break;
            case 'landing':
                if (state.altitude <= 2 && state.airspeed < 60) {
                    this.landed = true;
                    newEvents.push({ type: 'success', text: '🎉 TOUCHDOWN! Successful landing!', duration: 5000 });
                    // Evaluate landing quality
                    if (state.airspeed < 45 && Math.abs(state.orientation.pitch) < 0.1) {
                        setTimeout(() => {
                            newEvents.push({ type: 'success', text: '⭐ Perfect Landing! Smooth and controlled.', duration: 4000 });
                        }, 2000);
                    }
                    else if (state.airspeed > 55) {
                        setTimeout(() => {
                            newEvents.push({ type: 'warning', text: '⚠️ Hard landing - too much speed on touchdown!', duration: 3000 });
                        }, 2000);
                    }
                    this.phase = 'taxi'; // Reset for another attempt
                }
                else if (state.altitude > 100) {
                    // Missed approach
                    newEvents.push({ type: 'warning', text: '🔄 Missed approach! Climb and try again' });
                    this.phase = 'climb';
                }
                break;
        }
        // Stall warning during critical phases
        if (state.stallWarning && (this.phase === 'takeoff' || this.phase === 'landing')) {
            newEvents.push({ type: 'warning', text: '🚨 STALL WARNING! Increase throttle!' });
        }
        // Ground proximity warning
        const terrainH = (0, terrain_1.getTerrainHeight)(state.position.x, state.position.z);
        if (state.altitude < 30 && state.altitude > 2 && this.phase !== 'landing' && this.phase !== 'taxi') {
            newEvents.push({ type: 'warning', text: '⚠️ TERRAIN! PULL UP!' });
        }
        return newEvents;
    }
    getPhase() {
        const labels = {
            taxi: '🛞 Taxiing', takeoff: '🛫 Takeoff', climb: '📈 Climbing',
            cruise: '✈️ Cruising', descent: '📉 Descending', landing: '🛬 Landing'
        };
        return labels[this.phase];
    }
    getInstructions() {
        switch (this.phase) {
            case 'taxi': return 'W/S: Pitch | A/D: Roll | Q/E: Yaw | Shift/= : Throttle Up | Ctrl/- : Throttle Down';
            case 'takeoff': return 'Full throttle! Pull up gently when speed builds. Watch your airspeed.';
            case 'climb': return 'Maintain climb rate. Keep nose slightly up, full throttle.';
            case 'cruise': return 'Cruising altitude reached. Reduce throttle to 60%. Begin descent.';
            case 'descent': return 'Reduce throttle gradually. Align with runway heading (south).';
            case 'landing': return 'Flare gently before touchdown. Aim for ~45 m/s on contact.';
        }
    }
}
exports.TakeoffScenario = TakeoffScenario;
class DogfightScenario {
    constructor() {
        this.drones = [];
        this.events = [];
        this.startTime = 0;
        this.score = 0;
        this.missilesFired = false;
        this.phase = 'briefing';
        this.startTime = performance.now();
        this.spawnDrones(5);
    }
    spawnDrones(count) {
        for (let i = 0; i < count; i++) {
            const angle = (i / count) * Math.PI * 2;
            const radius = 3000 + Math.random() * 2000;
            this.drones.push({
                position: {
                    x: Math.cos(angle) * radius,
                    y: 0,
                    z: -500 + Math.sin(angle) * radius,
                },
                velocity: {
                    x: (Math.random() - 0.5) * 100,
                    y: 0,
                    z: (Math.random() - 0.5) * 100,
                },
                alive: true,
                health: 100,
                mesh: null,
            });
        }
    }
    update(dt, state) {
        const newEvents = [];
        const elapsed = (performance.now() - this.startTime) / 1000;
        // Briefing phase
        if (this.phase === 'briefing') {
            if (elapsed > 5) {
                this.phase = 'combat';
                newEvents.push({ type: 'objective', text: '⚔️ COMBAT START! Engage enemy drones!', duration: 3000 });
            }
            else {
                newEvents.push({ type: 'message', text: `📋 Mission: Destroy ${this.drones.length} enemy drones. Use mouse to aim, Space to fire.` });
            }
        }
        // Combat phase - update drone AI with patrol + engagement behavior
        if (this.phase === 'combat') {
            const aliveDrones = this.drones.filter(d => d.alive);
            for (let idx = 0; idx < this.drones.length; idx++) {
                const drone = this.drones[idx];
                if (!drone.alive)
                    continue;
                // AI: patrol in circles while tracking player
                const dx = state.position.x - drone.position.x;
                const dz = state.position.z - drone.position.z;
                const dist = Math.sqrt(dx * dx + dz * dz);
                // Each drone has a patrol center and radius
                const patrolAngle = elapsed * 0.3 + idx * (Math.PI * 2 / this.drones.length);
                const patrolRadius = 1500;
                const targetX = Math.cos(patrolAngle) * patrolRadius;
                const targetZ = -500 + Math.sin(patrolAngle) * patrolRadius;
                // Blend between patrolling and engaging player
                let engageFactor = 0;
                if (dist < 800) {
                    engageFactor = Math.max(0, 1 - dist / 800);
                }
                // Move toward blended target
                const blendX = targetX * (1 - engageFactor) + state.position.x * engageFactor;
                const blendZ = targetZ * (1 - engageFactor) + state.position.z * engageFactor;
                const toTargetX = blendX - drone.position.x;
                const toTargetZ = blendZ - drone.position.z;
                const toTargetDist = Math.sqrt(toTargetX * toTargetX + toTargetZ * toTargetZ);
                if (toTargetDist > 10) {
                    const accel = engageFactor > 0.5 ? 30 : 15;
                    drone.velocity.x += (toTargetX / toTargetDist) * dt * accel;
                    drone.velocity.z += (toTargetZ / toTargetDist) * dt * accel;
                }
                // Clamp speed
                const speed = Math.sqrt(drone.velocity.x ** 2 + drone.velocity.z ** 2);
                const maxSpeed = engageFactor > 0.5 ? 180 : 120;
                if (speed > maxSpeed) {
                    drone.velocity.x *= maxSpeed / speed;
                    drone.velocity.z *= maxSpeed / speed;
                }
                // Update position
                drone.position.x += drone.velocity.x * dt;
                drone.position.z += drone.velocity.z * dt;
                // Maintain altitude with variation
                const targetAlt = 400 + Math.sin(elapsed * 0.5 + idx) * 200;
                drone.position.y += (targetAlt - drone.position.y) * dt * 0.3;
                // Keep above terrain
                const terrainH = (0, terrain_1.getTerrainHeight)(drone.position.x, drone.position.z);
                if (drone.position.y < terrainH + 50) {
                    drone.position.y = terrainH + 50;
                }
                // Check if player is in range to fire at drone
                if (dist < 250 && this.missilesFired) {
                    drone.health -= dt * 40;
                    if (drone.health <= 0) {
                        drone.alive = false;
                        this.score += 100;
                        newEvents.push({ type: 'success', text: `💥 Target destroyed! Score: ${this.score}`, duration: 2000 });
                        // Check victory condition
                        if (aliveDrones.length <= 1) {
                            this.phase = 'victory';
                            newEvents.push({ type: 'success', text: '🏆 VICTORY! All targets eliminated!', duration: 5000 });
                        }
                    }
                }
            }
            // Proximity warnings (throttled)
            const closestDrone = aliveDrones.reduce((closest, d) => {
                const dx2 = state.position.x - d.position.x;
                const dz2 = state.position.z - d.position.z;
                const dist2 = Math.sqrt(dx2 * dx2 + dz2 * dz2);
                return dist2 < closest.dist ? { drone: d, dist: dist2 } : closest;
            }, { drone: null, dist: Infinity });
            if (closestDrone.drone && closestDrone.dist < 200) {
                newEvents.push({ type: 'warning', text: `⚠️ Enemy at ${Math.round(closestDrone.dist)}m! Press Space to fire!` });
            }
            // G-Force warnings during aggressive maneuvers
            if (state.gForce > 6) {
                newEvents.push({ type: 'warning', text: '🔴 HIGH G! Risk of blackout!' });
            }
            else if (state.gForce > 4 && Math.random() < 0.1) {
                newEvents.push({ type: 'message', text: `G-Load: ${state.gForce.toFixed(1)}G` });
            }
            // Stall warning in combat
            if (state.stallWarning) {
                newEvents.push({ type: 'warning', text: '🚨 STALL! You are vulnerable!' });
            }
        }
        this.missilesFired = false;
        return newEvents;
    }
    fireMissile(playerPos) {
        this.missilesFired = true;
        // Check if any drone is in range of player
        const aliveDrones = this.drones.filter(d => d.alive);
        for (const drone of aliveDrones) {
            const dx = drone.position.x - playerPos.x;
            const dz = drone.position.z - playerPos.z;
            if (Math.sqrt(dx * dx + dz * dz) < 300) {
                return true;
            }
        }
        return false;
    }
    getScore() {
        return this.score;
    }
    getRemainingTargets() {
        return this.drones.filter(d => d.alive).length;
    }
    getPhase() {
        const labels = { briefing: '📋 Briefing', combat: '⚔️ Combat', victory: '🏆 Victory!', defeat: '💀 Defeated' };
        return labels[this.phase];
    }
    getInstructions() {
        if (this.phase === 'briefing')
            return 'Waiting for mission start...';
        return 'Mouse: Aim | Space: Fire missiles | W/S/A/D: Maneuver | Shift/= : Throttle Up';
    }
}
exports.DogfightScenario = DogfightScenario;
};

// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HUD = void 0;
class HUD {
    constructor() {
        this.width = 1920;
        this.height = 1080;
        this.canvas = document.createElement('canvas');
        this.canvas.id = 'hud-canvas';
        this.canvas.width = this.width;
        this.canvas.height = this.height;
        this.ctx = this.canvas.getContext('2d');
        this.ctx.imageSmoothingEnabled = false;
        // Position over the 3D canvas
        this.canvas.style.position = 'absolute';
        this.canvas.style.top = '0';
        this.canvas.style.left = '0';
        this.canvas.style.width = '100%';
        this.canvas.style.height = '100%';
        this.canvas.style.pointerEvents = 'none';
    }
    getCanvas() {
        return this.canvas;
    }
    render(state, profile, scenarioInfo, instructions) {
        const ctx = this.ctx;
        const w = this.width;
        const h = this.height;
        // Clear with transparency
        ctx.clearRect(0, 0, w, h);
        // --- Flight Data Tape (Left side - Altitude) ---
        this.drawAltitudeTape(ctx, state.altitude, 150, h / 2);
        // --- Speed Tape (Right side - Airspeed) ---
        this.drawSpeedTape(ctx, state.airspeed, w - 150, h / 2);
        // --- Pitch Ladder & Horizon (Center) ---
        this.drawPitchLadder(ctx, state.orientation.pitch, state.orientation.roll, w / 2, h / 2);
        // --- G-Meter (Bottom center) ---
        this.drawGMeter(ctx, state.gForce, w / 2, h - 100);
        // --- Throttle Indicator (Bottom left) ---
        this.drawThrottleBar(ctx, state.throttle, 80, h - 60);
        // --- AoA Indicator (Near center) ---
        this.drawAoAGauge(ctx, state.aoa, profile.maxAoA, w / 2 + 150, h / 2 + 100);
        // --- Stall Warning ---
        if (state.stallWarning) {
            ctx.save();
            const flash = Math.sin(Date.now() / 100) > 0;
            if (flash) {
                ctx.fillStyle = '#ff0000';
                ctx.font = 'bold 48px monospace';
                ctx.textAlign = 'center';
                ctx.fillText('STALL', w / 2, h / 2 - 150);
            }
            ctx.restore();
        }
        // --- Scenario Info (Top center) ---
        ctx.save();
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 24px monospace';
        ctx.textAlign = 'center';
        ctx.fillText(scenarioInfo, w / 2, 40);
        ctx.font = '16px monospace';
        ctx.fillStyle = '#aaddff';
        ctx.fillText(instructions, w / 2, 70);
        ctx.restore();
        // --- Crosshair (Center) ---
        this.drawCrosshair(ctx, w / 2, h / 2);
        // --- Damage indicator ---
        if (state.damage > 0) {
            ctx.save();
            ctx.fillStyle = `rgba(255, 0, 0, ${Math.min(0.3, state.damage * 0.5)})`;
            ctx.fillRect(0, 0, w, h);
            ctx.restore();
        }
        // --- Compass strip (top) ---
        this.drawCompass(ctx, state.orientation.yaw, w / 2, 90);
        // --- Radar display (bottom right corner for dogfight mode) ---
        if (scenarioInfo.includes('Combat') || scenarioInfo.includes('Targets')) {
            this.drawRadar(ctx, w - 130, h - 150, 80);
        }
    }
    drawRadar(ctx, cx, cy, radius) {
        ctx.save();
        // Background circle
        ctx.fillStyle = 'rgba(0, 30, 0, 0.6)';
        ctx.beginPath();
        ctx.arc(cx, cy, radius, 0, Math.PI * 2);
        ctx.fill();
        // Border
        ctx.strokeStyle = '#00ff44';
        ctx.lineWidth = 1;
        ctx.stroke();
        // Range rings
        ctx.strokeStyle = 'rgba(0, 255, 68, 0.3)';
        for (let r = radius / 3; r < radius; r += radius / 3) {
            ctx.beginPath();
            ctx.arc(cx, cy, r, 0, Math.PI * 2);
            ctx.stroke();
        }
        // Cross lines
        ctx.strokeStyle = 'rgba(0, 255, 68, 0.2)';
        ctx.beginPath();
        ctx.moveTo(cx - radius, cy);
        ctx.lineTo(cx + radius, cy);
        ctx.moveTo(cx, cy - radius);
        ctx.lineTo(cx, cy + radius);
        ctx.stroke();
        // Player blip (center)
        ctx.fillStyle = '#00ff44';
        ctx.beginPath();
        ctx.arc(cx, cy, 3, 0, Math.PI * 2);
        ctx.fill();
        // Sweep line
        const sweepAngle = (Date.now() / 1000) % (Math.PI * 2);
        ctx.strokeStyle = 'rgba(0, 255, 68, 0.5)';
        ctx.beginPath();
        ctx.moveTo(cx, cy);
        ctx.lineTo(cx + Math.cos(sweepAngle) * radius, cy + Math.sin(sweepAngle) * radius);
        ctx.stroke();
        // Label
        ctx.fillStyle = '#00ff44';
        ctx.font = '10px monospace';
        ctx.textAlign = 'center';
        ctx.fillText('RADAR', cx, cy - radius - 5);
        ctx.restore();
    }
    drawAltitudeTape(ctx, altitude, x, cy) {
        ctx.save();
        // Background panel
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        ctx.fillRect(x - 60, cy - 150, 120, 300);
        ctx.strokeStyle = '#00ff88';
        ctx.lineWidth = 1;
        ctx.strokeRect(x - 60, cy - 150, 120, 300);
        // Current altitude display
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 28px monospace';
        ctx.textAlign = 'center';
        ctx.fillText(Math.round(altitude).toString(), x, cy + 8);
        // Tape markings
        ctx.font = '14px monospace';
        ctx.fillStyle = '#00ff88';
        const altRange = 500;
        for (let a = Math.floor((altitude - altRange) / 100) * 100; a <= altitude + altRange; a += 50) {
            if (a < 0)
                continue;
            const y = cy - ((a - altitude) / altRange) * 150;
            if (y < cy - 140 || y > cy + 140)
                continue;
            const isMajor = a % 100 === 0;
            ctx.beginPath();
            ctx.moveTo(x - 60, y);
            ctx.lineTo(x - 60 + (isMajor ? 20 : 10), y);
            ctx.strokeStyle = '#00ff88';
            ctx.lineWidth = isMajor ? 2 : 1;
            ctx.stroke();
            if (isMajor) {
                ctx.fillText(Math.round(a).toString(), x - 35, y + 4);
            }
        }
        // Bracket indicator
        ctx.strokeStyle = '#00ff88';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(x - 60, cy - 15);
        ctx.lineTo(x - 75, cy);
        ctx.lineTo(x - 60, cy + 15);
        ctx.stroke();
        // Unit label
        ctx.fillStyle = '#88ffaa';
        ctx.font = '12px monospace';
        ctx.fillText('ALT (m)', x, cy + 30);
        ctx.restore();
    }
    drawSpeedTape(ctx, speed, x, cy) {
        ctx.save();
        // Background panel
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        ctx.fillRect(x - 60, cy - 150, 120, 300);
        ctx.strokeStyle = '#00aaff';
        ctx.lineWidth = 1;
        ctx.strokeRect(x - 60, cy - 150, 120, 300);
        // Current speed display
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 28px monospace';
        ctx.textAlign = 'center';
        ctx.fillText(Math.round(speed).toString(), x, cy + 8);
        // Tape markings
        ctx.font = '14px monospace';
        ctx.fillStyle = '#00aaff';
        const speedRange = 200;
        for (let s = Math.floor((speed - speedRange) / 50) * 50; s <= speed + speedRange; s += 25) {
            if (s < 0)
                continue;
            const y = cy - ((s - speed) / speedRange) * 150;
            if (y < cy - 140 || y > cy + 140)
                continue;
            const isMajor = s % 50 === 0;
            ctx.beginPath();
            ctx.moveTo(x + 60, y);
            ctx.lineTo(x + 60 - (isMajor ? 20 : 10), y);
            ctx.strokeStyle = '#00aaff';
            ctx.lineWidth = isMajor ? 2 : 1;
            ctx.stroke();
            if (isMajor) {
                ctx.fillText(Math.round(s).toString(), x + 35, y + 4);
            }
        }
        // Bracket indicator
        ctx.strokeStyle = '#00aaff';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(x + 60, cy - 15);
        ctx.lineTo(x + 75, cy);
        ctx.lineTo(x + 60, cy + 15);
        ctx.stroke();
        // Unit label
        ctx.fillStyle = '#88ccff';
        ctx.font = '12px monospace';
        ctx.fillText('SPD (m/s)', x, cy + 30);
        ctx.restore();
    }
    drawPitchLadder(ctx, pitch, roll, cx, cy) {
        ctx.save();
        ctx.translate(cx, cy);
        ctx.rotate(-roll); // Roll the ladder with aircraft bank
        const pitchDeg = pitch * 180 / Math.PI;
        const ladderWidth = 400;
        const lineSpacing = 35;
        // Sky/ground fill (simple gradient)
        const skyGrad = ctx.createLinearGradient(0, -200, 0, 0);
        skyGrad.addColorStop(0, 'rgba(100, 150, 200, 0.3)');
        skyGrad.addColorStop(1, 'rgba(100, 150, 200, 0.1)');
        ctx.fillStyle = skyGrad;
        ctx.fillRect(-ladderWidth / 2 - 50, -200, ladderWidth + 100, 200);
        const groundGrad = ctx.createLinearGradient(0, 0, 0, 200);
        groundGrad.addColorStop(0, 'rgba(80, 60, 30, 0.1)');
        groundGrad.addColorStop(1, 'rgba(80, 60, 30, 0.3)');
        ctx.fillStyle = groundGrad;
        ctx.fillRect(-ladderWidth / 2 - 50, 0, ladderWidth + 100, 200);
        // Horizon line
        const horizonY = -pitchDeg * (lineSpacing / 5);
        ctx.strokeStyle = '#ffffff';
        ctx.lineWidth = 2;
        ctx.beginPath();
        ctx.moveTo(-ladderWidth / 2, horizonY);
        ctx.lineTo(ladderWidth / 2, horizonY);
        ctx.stroke();
        // Pitch ladder lines (every 5 degrees)
        ctx.font = '14px monospace';
        ctx.textAlign = 'center';
        for (let deg = -30; deg <= 30; deg += 5) {
            if (deg === 0)
                continue;
            const y = horizonY - deg * (lineSpacing / 5);
            if (Math.abs(y) > 180)
                continue;
            const isMajor = deg % 10 === 0;
            ctx.strokeStyle = '#ffffff';
            ctx.lineWidth = isMajor ? 2 : 1;
            ctx.beginPath();
            ctx.moveTo(-ladderWidth / 4, y);
            ctx.lineTo(ladderWidth / 4, y);
            ctx.stroke();
            if (isMajor) {
                ctx.fillStyle = '#ffffff';
                ctx.fillText(deg.toString(), -ladderWidth / 4 - 20, y + 5);
                ctx.fillText(deg.toString(), ladderWidth / 4 + 20, y + 5);
            }
        }
        // Pitch dots at horizon
        ctx.fillStyle = '#ffffff';
        ctx.beginPath();
        ctx.arc(-15, horizonY - 3, 3, 0, Math.PI * 2);
        ctx.fill();
        ctx.beginPath();
        ctx.arc(15, horizonY - 3, 3, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
    }
    drawCrosshair(ctx, cx, cy) {
        ctx.save();
        ctx.strokeStyle = '#00ff88';
        ctx.lineWidth = 1.5;
        // Simple crosshair
        const size = 30;
        ctx.beginPath();
        ctx.moveTo(cx - size, cy);
        ctx.lineTo(cx - 10, cy);
        ctx.moveTo(cx + 10, cy);
        ctx.lineTo(cx + size, cy);
        ctx.moveTo(cx, cy - size);
        ctx.lineTo(cx, cy - 10);
        ctx.moveTo(cx, cy + 10);
        ctx.lineTo(cx, cy + size);
        ctx.stroke();
        // Center dot
        ctx.fillStyle = '#00ff88';
        ctx.beginPath();
        ctx.arc(cx, cy, 2, 0, Math.PI * 2);
        ctx.fill();
        ctx.restore();
    }
    drawGMeter(ctx, gForce, cx, cy) {
        ctx.save();
        // Background
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        ctx.fillRect(cx - 80, cy - 15, 160, 30);
        // G-bar (green to red gradient)
        const gClamped = Math.max(0, Math.min(9, gForce));
        const barWidth = (gClamped / 9) * 140;
        let color;
        if (gClamped < 3)
            color = '#00ff88';
        else if (gClamped < 5)
            color = '#ffff00';
        else if (gClamped < 7)
            color = '#ff8800';
        else
            color = '#ff0000';
        ctx.fillStyle = color;
        ctx.fillRect(cx - 70, cy - 12, barWidth, 24);
        // G value text
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 16px monospace';
        ctx.textAlign = 'center';
        ctx.fillText(`${gForce.toFixed(1)}G`, cx, cy + 5);
        // Label
        ctx.fillStyle = '#88ffaa';
        ctx.font = '10px monospace';
        ctx.fillText('G-FORCE', cx, cy - 20);
        ctx.restore();
    }
    drawThrottleBar(ctx, throttle, x, y) {
        ctx.save();
        // Background
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        ctx.fillRect(x - 10, y - 60, 20, 120);
        // Throttle fill (bottom to top)
        const fillHeight = throttle * 100;
        let color;
        if (throttle < 0.3)
            color = '#4488ff';
        else if (throttle < 0.7)
            color = '#44ff44';
        else
            color = '#ffaa00';
        ctx.fillStyle = color;
        ctx.fillRect(x - 8, y + 50 - fillHeight, 16, fillHeight);
        // Label
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 14px monospace';
        ctx.textAlign = 'center';
        ctx.fillText(`${Math.round(throttle * 100)}%`, x, y - 70);
        ctx.fillStyle = '#88ffaa';
        ctx.font = '10px monospace';
        ctx.fillText('THR', x, y + 65);
        ctx.restore();
    }
    drawAoAGauge(ctx, aoa, maxAoA, cx, cy) {
        ctx.save();
        const aoaDeg = aoa * 180 / Math.PI;
        const maxDeg = maxAoA * 180 / Math.PI;
        // Background arc
        ctx.strokeStyle = 'rgba(255, 255, 255, 0.3)';
        ctx.lineWidth = 6;
        ctx.beginPath();
        ctx.arc(cx, cy, 40, Math.PI * 0.75, Math.PI * 0.25, true);
        ctx.stroke();
        // AoA fill arc
        const aoaRatio = Math.max(-1, Math.min(1, aoaDeg / maxDeg));
        let color;
        if (Math.abs(aoaDeg) < maxDeg * 0.6)
            color = '#00ff88';
        else if (Math.abs(aoaDeg) < maxDeg * 0.9)
            color = '#ffff00';
        else
            color = '#ff4444';
        ctx.strokeStyle = color;
        ctx.lineWidth = 6;
        ctx.beginPath();
        const startAngle = Math.PI * 0.75 + aoaRatio * Math.PI * 0.25;
        ctx.arc(cx, cy, 40, Math.PI * 0.75, startAngle, aoaDeg < 0);
        ctx.stroke();
        // Text
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 16px monospace';
        ctx.textAlign = 'center';
        ctx.fillText(`${aoaDeg.toFixed(1)}°`, cx, cy + 5);
        ctx.fillStyle = '#88ffaa';
        ctx.font = '10px monospace';
        ctx.fillText('AoA', cx, cy - 25);
        ctx.restore();
    }
    drawCompass(ctx, yaw, cx, cy) {
        ctx.save();
        // Background strip
        ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
        ctx.fillRect(cx - 150, cy - 18, 300, 36);
        // Heading value
        const headingDeg = ((yaw * 180 / Math.PI) % 360 + 360) % 360;
        ctx.fillStyle = '#ffffff';
        ctx.font = 'bold 20px monospace';
        ctx.textAlign = 'center';
        ctx.fillText(`${Math.round(headingDeg)}°`, cx, cy + 7);
        // Cardinal directions
        ctx.font = '12px monospace';
        ctx.fillStyle = '#aaddff';
        const dirs = [
            { label: 'N', deg: 0 }, { label: 'E', deg: 90 },
            { label: 'S', deg: 180 }, { label: 'W', deg: 270 }
        ];
        for (const dir of dirs) {
            let relDeg = dir.deg - headingDeg;
            if (relDeg < -180)
                relDeg += 360;
            if (relDeg > 180)
                relDeg -= 360;
            const xOff = (relDeg / 180) * 140;
            if (Math.abs(xOff) < 140) {
                ctx.globalAlpha = Math.max(0, 1 - Math.abs(xOff) / 140);
                ctx.fillText(dir.label, cx + xOff, cy - 5);
            }
        }
        // Heading indicator triangle
        ctx.fillStyle = '#ffaa00';
        ctx.beginPath();
        ctx.moveTo(cx, cy - 20);
        ctx.lineTo(cx - 6, cy - 14);
        ctx.lineTo(cx + 6, cy - 14);
        ctx.closePath();
        ctx.fill();
        ctx.restore();
    }
}
exports.HUD = HUD;
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SceneRenderer = void 0;
const aircraft_1 = require("./aircraft");
const terrain_1 = require("./terrain");
class SceneRenderer {
    constructor(canvasContainer) {
        this.aircraftMesh = null;
        this.terrainChunk = null;
        this.runwayGroup = null;
        this.waterPlane = null;
        this.skyDome = null;
        this.cloudsGroup = null;
        this.droneMeshes = new Map();
        this.contrailPoints = [[], []]; // Left and right wing contrails
        this.contrailLines = [];
        this.shadowMesh = null;
        // Camera settings
        this.cameraMode = 'chase';
        this.chaseDistance = 30;
        this.chaseHeight = 10;
        this.cameraSmoothness = 0.03;
        this.freeCamAngle = 0;
        this.freeCamPitch = 0.3;
        // Camera target position (smoothed)
        this.camTargetPos = new THREE.Vector3();
        this.camLookAtPos = new THREE.Vector3();
        // Scene
        this.scene = new THREE.Scene();
        this.scene.fog = new THREE.FogExp2(0x87ceeb, 0.00015);
        // Camera
        const aspect = window.innerWidth / window.innerHeight;
        this.camera = new THREE.PerspectiveCamera(65, aspect, 1, 30000);
        // Renderer
        this.renderer = new THREE.WebGLRenderer({ antialias: true });
        this.renderer.setSize(window.innerWidth, window.innerHeight);
        this.renderer.setClearColor(0x87ceeb);
        this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
        canvasContainer.appendChild(this.renderer.domElement);
        // Lighting
        this.setupLighting();
        // Environment
        this.buildEnvironment();
        // Handle resize
        window.addEventListener('resize', () => this.onResize());
    }
    setupLighting() {
        // Ambient light (soft fill)
        const ambient = new THREE.AmbientLight(0x404060, 0.5);
        this.scene.add(ambient);
        // Hemisphere light (sky/ground gradient)
        const hemi = new THREE.HemisphereLight(0x87ceeb, 0x3a5f0b, 0.6);
        this.scene.add(hemi);
        // Directional light (sun)
        const sun = new THREE.DirectionalLight(0xfff4e0, 1.2);
        sun.position.set(5000, 8000, -3000);
        this.scene.add(sun);
        // Secondary fill light
        const fill = new THREE.DirectionalLight(0x8899bb, 0.3);
        fill.position.set(-3000, 2000, 5000);
        this.scene.add(fill);
    }
    buildEnvironment() {
        // Sky dome
        this.skyDome = (0, terrain_1.createSkyDome)();
        this.scene.add(this.skyDome);
        // Clouds
        this.cloudsGroup = (0, terrain_1.createClouds)();
        this.scene.add(this.cloudsGroup);
        // Water plane (below terrain)
        this.waterPlane = (0, terrain_1.createWaterPlane)();
        this.scene.add(this.waterPlane);
        // Terrain chunk (large area around origin)
        const terrainSize = 16000;
        this.terrainChunk = (0, terrain_1.createTerrainChunk)(0, 0, terrainSize, 256);
        this.scene.add(this.terrainChunk);
        // Runway
        this.runwayGroup = (0, terrain_1.createRunway)();
        this.scene.add(this.runwayGroup);
        // Contrail lines (two, one for each wingtip)
        const contrailMat = new THREE.MeshPhongMaterial({
            color: 0xffffff, transparent: true, opacity: 0.4, fog: false
        });
        for (let i = 0; i < 2; i++) {
            const geo = new THREE.BufferGeometry();
            const positions = new Float32Array(500 * 3); // Max 500 points per contrail
            geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
            const line = new THREE.Line(geo, contrailMat);
            this.scene.add(line);
            this.contrailLines.push(line);
        }
        // Ground shadow for aircraft (dark circle projected on ground)
        const shadowGeo = new THREE.CircleGeometry(15, 16);
        shadowGeo.rotateX(-Math.PI / 2);
        const shadowMat = new THREE.MeshPhongMaterial({
            color: 0x000000, transparent: true, opacity: 0.3, fog: false
        });
        this.shadowMesh = new THREE.Mesh(shadowGeo, shadowMat);
        this.scene.add(this.shadowMesh);
    }
    setAircraftMesh(mesh) {
        if (this.aircraftMesh) {
            this.scene.remove(this.aircraftMesh);
        }
        this.aircraftMesh = mesh;
        // Scale down for visual representation
        const s = aircraft_1.MODEL_SCALE;
        mesh.scale.set(s, s, s);
        this.scene.add(mesh);
    }
    addDrone(drone, index) {
        // Create a simple enemy drone model
        const group = new THREE.Group();
        // Body
        const bodyGeo = new THREE.BoxGeometry(3, 1.5, 6);
        const bodyMat = new THREE.MeshPhongMaterial({ color: 0x8b0000 });
        const body = new THREE.Mesh(bodyGeo, bodyMat);
        group.add(body);
        // Wings
        const wingGeo = new THREE.BoxGeometry(10, 0.3, 2);
        const wingMat = new THREE.MeshPhongMaterial({ color: 0x660000 });
        const wings = new THREE.Mesh(wingGeo, wingMat);
        group.add(wings);
        // Red beacon
        const beaconGeo = new THREE.SphereGeometry(0.5, 8, 8);
        const beaconMat = new THREE.MeshPhongMaterial({ color: 0xff0000, emissive: 0xff0000 });
        const beacon = new THREE.Mesh(beaconGeo, beaconMat);
        beacon.position.y = 1.5;
        group.add(beacon);
        // Scale and position
        const s = aircraft_1.MODEL_SCALE * 2;
        group.scale.set(s, s, s);
        this.scene.add(group);
        drone.mesh = group;
        this.droneMeshes.set(index, group);
        return group;
    }
    removeDrone(index) {
        const mesh = this.droneMeshes.get(index);
        if (mesh) {
            this.scene.remove(mesh);
            this.droneMeshes.delete(index);
        }
    }
    updateAircraft(state, dt) {
        if (!this.aircraftMesh)
            return;
        // Update position (world coordinates: x=right, y=up, z=back in Three.js)
        // Our physics uses: x=forward, y=right, z=down
        // Map to Three.js: x→-z, y→x, z→y (negated for up)
        this.aircraftMesh.position.set(state.position.y, // right → X
        -state.altitude, // altitude → Y (inverted since physics z+ is down)
        -state.position.x // forward → -Z
        );
        // Update rotation
        this.aircraftMesh.rotation.set(state.orientation.pitch, // pitch around X
        state.orientation.yaw, // yaw around Y
        -state.orientation.roll // roll around Z (negated for correct visual)
        );
        // Animate engine flames based on throttle
        const flameObj = this.aircraftMesh.getObjectByName('flame');
        if (flameObj) {
            const flame = flameObj;
            const mat = flame.material;
            if (mat) {
                mat.emissiveIntensity = state.throttle * 1.5 + Math.random() * 0.3;
            }
            // Scale flame with throttle
            flame.scale.set(0.5 + state.throttle, 0.5 + state.throttle, 0.3 + state.throttle * 2);
        }
        // Update ground shadow
        if (this.shadowMesh) {
            const terrainH = (0, terrain_1.getTerrainHeight)(state.position.y, -state.position.x);
            this.shadowMesh.position.set(state.position.y, Math.max(-10, terrainH), -state.position.x);
            // Shadow scales with altitude (larger and more transparent when higher)
            const altAboveGround = Math.max(0, state.altitude - terrainH);
            const shadowScale = 1 + altAboveGround / 200;
            this.shadowMesh.scale.set(shadowScale, shadowScale, 1);
            this.shadowMesh.material.opacity = Math.max(0.05, 0.4 - altAboveGround / 2000);
        }
        // Update contrails at high altitude with throttle
        this.updateContrails(state, dt);
        // Update camera
        this.updateCamera(state, dt);
    }
    updateContrails(state, dt) {
        // Only generate contrails above ~5000m with significant throttle
        if (state.altitude < 5000 || state.throttle < 0.3)
            return;
        const aircraftPos = new THREE.Vector3(state.position.y, -state.altitude, -state.position.x);
        // Calculate wingtip positions based on orientation
        const rightDir = new THREE.Vector3(1, 0, 0);
        rightDir.applyEuler(new THREE.Euler(state.orientation.pitch, state.orientation.yaw, state.orientation.roll));
        const halfSpan = 4.75 * aircraft_1.MODEL_SCALE; // Half wingspan in visual scale
        const leftTip = aircraftPos.clone().add(rightDir.clone().multiplyScalar(halfSpan));
        const rightTip = aircraftPos.clone().add(rightDir.clone().multiplyScalar(-halfSpan));
        // Add new points to contrail arrays
        this.contrailPoints[0].push(leftTip.x, leftTip.y, leftTip.z);
        this.contrailPoints[1].push(rightTip.x, rightTip.y, rightTip.z);
        // Limit trail length (keep last 200 points)
        const maxPoints = 200;
        if (this.contrailPoints[0].length > maxPoints * 3) {
            this.contrailPoints[0] = this.contrailPoints[0].slice(-maxPoints * 3);
        }
        if (this.contrailPoints[1].length > maxPoints * 3) {
            this.contrailPoints[1] = this.contrailPoints[1].slice(-maxPoints * 3);
        }
        // Update line geometries
        for (let i = 0; i < 2; i++) {
            if (this.contrailLines[i]) {
                const geo = this.contrailLines[i].geometry;
                const attr = geo.attributes.position;
                const arr = attr.array;
                // Copy points to buffer
                for (let j = 0; j < this.contrailPoints[i].length && j < arr.length; j++) {
                    arr[j] = this.contrailPoints[i][j];
                }
                // Fade out old points by reducing opacity based on age
                const mat = this.contrailLines[i].material;
                if (mat) {
                    mat.opacity = Math.min(0.5, state.throttle * 0.4) * Math.max(0, 1 - (state.altitude - 5000) / 15000);
                }
                attr.needsUpdate = true;
                geo.setDrawRange(0, this.contrailPoints[i].length / 3);
            }
        }
    }
    updateCamera(state, dt) {
        const aircraftPos = new THREE.Vector3(state.position.y, -state.altitude, -state.position.x);
        if (this.cameraMode === 'chase') {
            // Chase camera behind and above the aircraft
            const forward = new THREE.Vector3(0, 0, -1);
            forward.applyEuler(new THREE.Euler(state.orientation.pitch, state.orientation.yaw, 0));
            const targetPos = aircraftPos.clone();
            targetPos.add(forward.multiplyScalar(-this.chaseDistance));
            targetPos.y += this.chaseHeight;
            // Smooth camera movement with speed-dependent smoothing
            const smoothFactor = Math.min(1, (this.cameraSmoothness + dt * 3) * (1 + state.airspeed / 200));
            this.camTargetPos.lerp(targetPos, smoothFactor);
            this.camera.position.copy(this.camTargetPos);
            // Look slightly ahead of aircraft
            const lookAhead = forward.clone().multiplyScalar(50);
            this.camLookAtPos.lerp(aircraftPos.clone().add(lookAhead), smoothFactor * 1.5);
            this.camera.lookAt(this.camLookAtPos);
        }
        else if (this.cameraMode === 'cockpit') {
            // Cockpit view - inside the aircraft
            const forward = new THREE.Vector3(0, 0, -1);
            forward.applyEuler(new THREE.Euler(state.orientation.pitch, state.orientation.yaw, 0));
            this.camera.position.copy(aircraftPos).add(forward.multiplyScalar(2));
            this.camera.position.y += 1;
            const lookTarget = aircraftPos.clone().add(forward.multiplyScalar(100));
            this.camera.lookAt(lookTarget);
        }
        else if (this.cameraMode === 'free') {
            // Free orbit camera around aircraft
            this.freeCamAngle += dt * 0.3;
            const radius = 60;
            const freePos = new THREE.Vector3(aircraftPos.x + Math.cos(this.freeCamAngle) * radius, aircraftPos.y + this.chaseHeight + Math.sin(this.freeCamPitch) * 20, aircraftPos.z + Math.sin(this.freeCamAngle) * radius);
            this.camera.position.lerp(freePos, 0.05);
            this.camera.lookAt(aircraftPos);
        }
        // Update fog density based on altitude (thinner air = less fog at high altitude)
        if (this.scene.fog) {
            const altFactor = Math.max(0.3, 1 - state.altitude / 10000);
            this.scene.fog.density = 0.00015 * altFactor;
        }
        // Update sky color based on altitude
        if (this.skyDome && this.skyDome.material) {
            const mat = this.skyDome.material;
            const altFade = Math.max(0, 1 - state.altitude / 20000);
            // Sky gets darker at higher altitude
            const r = Math.floor(135 * altFade + 10 * (1 - altFade));
            const g = Math.floor(206 * altFade + 10 * (1 - altFade));
            const b = Math.floor(235 * altFade + 20 * (1 - altFade));
            mat.color.setHex((r << 16) | (g << 8) | b);
        }
    }
    updateDrones(drones) {
        for (let i = 0; i < drones.length; i++) {
            const drone = drones[i];
            if (!drone.alive)
                continue;
            let mesh = this.droneMeshes.get(i);
            if (!mesh && drone.mesh) {
                mesh = drone.mesh;
                this.droneMeshes.set(i, mesh);
            }
            if (mesh) {
                // Map physics coordinates to Three.js
                mesh.position.set(drone.position.y || 0, -drone.position.z + (0, terrain_1.getTerrainHeight)(drone.position.x, drone.position.z), -drone.position.x);
                // Face direction of travel
                if (Math.abs(drone.velocity.x) > 1 || Math.abs(drone.velocity.z) > 1) {
                    const angle = Math.atan2(drone.velocity.x, drone.velocity.z);
                    mesh.rotation.y = -angle;
                }
            }
        }
    }
    toggleCameraMode() {
        if (this.cameraMode === 'chase')
            this.cameraMode = 'cockpit';
        else if (this.cameraMode === 'cockpit')
            this.cameraMode = 'free';
        else
            this.cameraMode = 'chase';
    }
    render() {
        // Update sky dome to follow camera
        if (this.skyDome) {
            this.skyDome.position.copy(this.camera.position);
        }
        this.renderer.render(this.scene, this.camera);
    }
    onResize() {
        const w = window.innerWidth;
        const h = window.innerHeight;
        this.camera.aspect = w / h;
        this.camera.updateProjectionMatrix();
        this.renderer.setSize(w, h);
    }
    getRenderer() {
        return this.renderer;
    }
    getScene() {
        return this.scene;
    }
}
exports.SceneRenderer = SceneRenderer;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"typescript","sessionId":"53ca77f9-7724-4c1a-8720-702d11643579","tokensIn":4361472,"tokensOut":60639,"tokensTotal":4422111,"turns":81,"toolCalls":80,"failedToolCalls":3,"timestamp":"2026-07-30T20:53:27.197Z"} -->