← Flight Simulator results

Flight Simulator

Qwen 3.6 27b MTP · composer

Initial view of Flight Simulator

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 103,261 bytes · SHA-256 6aea09bc4e88
<!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; }
body { overflow: hidden; background: #000; font-family: 'Segoe UI', Arial, sans-serif; color: #fff; user-select: none; }
#app { width: 100vw; height: 100vh; position: relative; }
#canvas-container { width: 100%; height: 100%; position: absolute; top: 0; left: 0; }

/* Menu Screen */
#menu-screen {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  background: linear-gradient(135deg, #0a0e27 0%, #1a1a3e 40%, #0d1b2a 100%);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  z-index: 100; overflow-y: auto; padding: 20px;
}
.title { font-size: 4em; font-weight: 900; letter-spacing: 8px; background: linear-gradient(90deg, #ff6b35, #f7c948, #ff6b35); -webkit-background-clip: text; -webkit-text-fill-color: transparent; text-shadow: none; margin-bottom: 5px; }
.subtitle { font-size: 1.2em; color: #8899aa; letter-spacing: 4px; margin-bottom: 40px; }

h2 { color: #f7c948; font-size: 1.3em; margin-bottom: 15px; text-transform: uppercase; letter-spacing: 2px; }
h3 { color: #aabbcc; font-size: 1em; margin-bottom: 10px; }

/* Aircraft Cards */
.aircraft-cards, .scenario-cards { display: flex; gap: 15px; flex-wrap: wrap; justify-content: center; margin-bottom: 25px; }
.ac-card, .sc-card {
  width: 200px; padding: 15px; border-radius: 10px; cursor: pointer;
  background: rgba(255,255,255,0.05); border: 2px solid rgba(255,255,255,0.1);
  transition: all 0.3s; text-align: center;
}
.ac-card:hover, .sc-card:hover { background: rgba(255,255,255,0.1); border-color: #f7c948; transform: translateY(-3px); }
.ac-card.selected, .sc-card.selected { border-color: #ff6b35; background: rgba(255,107,53,0.15); box-shadow: 0 0 20px rgba(255,107,53,0.3); }
.ac-icon { font-size: 3em; margin-bottom: 8px; }
.ac-name { font-weight: bold; font-size: 1.1em; color: #fff; margin-bottom: 5px; }
.ac-stats { font-size: 0.75em; color: #8899aa; line-height: 1.6; text-align: left; }
.sc-name { font-weight: bold; font-size: 1em; color: #fff; margin-bottom: 5px; }
.sc-desc { font-size: 0.75em; color: #8899aa; line-height: 1.4; }

/* Start Button */
#start-btn {
  padding: 15px 50px; font-size: 1.2em; font-weight: bold; letter-spacing: 3px;
  background: linear-gradient(90deg, #ff6b35, #e85d26); color: #fff; border: none;
  border-radius: 8px; cursor: pointer; transition: all 0.3s; margin-top: 15px;
}
#start-btn:hover:not(:disabled) { transform: scale(1.05); box-shadow: 0 0 30px rgba(255,107,53,0.5); }
#start-btn:disabled { opacity: 0.4; cursor: not-allowed; }

/* Controls Info */
#controls-info { margin-top: 25px; padding: 15px 25px; background: rgba(255,255,255,0.03); border-radius: 8px; max-width: 400px; }
.control-row { display: flex; justify-content: space-between; margin: 4px 0; font-size: 0.8em; color: #99aabb; }
kbd { background: rgba(255,255,255,0.1); padding: 2px 8px; border-radius: 3px; font-family: monospace; color: #f7c948; }

/* HUD */
#hud { position: absolute; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 50; }
.hud-box { background: rgba(0,20,40,0.6); border: 1px solid rgba(0,255,200,0.3); padding: 5px 10px; border-radius: 4px; display: flex; align-items: center; gap: 5px; }
.hud-label { font-size: 0.6em; color: #00ffc8; text-transform: uppercase; letter-spacing: 1px; }
.hud-unit { font-size: 0.6em; color: #00ffc8; }

#hud-top { position: absolute; top: 15px; left: 50%; transform: translateX(-50%); display: flex; gap: 20px; }
.hud-altitude span:nth-child(2), .hud-heading span:nth-child(2) { font-size: 1.4em; font-weight: bold; color: #fff; min-width: 60px; text-align: center; }

/* Speed & Alt Tapes */
.hud-tape { position: absolute; top: 50%; transform: translateY(-50%); width: 80px; height: 200px; overflow: hidden; background: rgba(0,20,40,0.3); border: 1px solid rgba(0,255,200,0.2); }
.hud-left { left: 15px; }
.hud-right { right: 15px; }
.tape-values { position: absolute; width: 100%; transition: transform 0.1s linear; }
.tape-val { height: 30px; display: flex; align-items: center; justify-content: center; font-size: 0.8em; color: #66aacc; border-bottom: 1px solid rgba(0,255,200,0.1); }
.tape-current { position: absolute; top: 50%; transform: translateY(-50%); width: 100%; text-align: center; font-size: 1.3em; font-weight: bold; color: #fff; z-index: 2; background: rgba(0,20,40,0.8); padding: 3px 0; }
.tape-label { position: absolute; bottom: 5px; width: 100%; text-align: center; font-size: 0.7em; color: #00ffc8; }

/* Center Horizon */
#hud-center { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 400px; height: 300px; }
#horizon-canvas { width: 100%; height: 100%; border-radius: 8px; opacity: 0.7; }

/* Bottom HUD */
#hud-bottom { position: absolute; bottom: 15px; left: 50%; transform: translateX(-50%); display: flex; gap: 15px; align-items: center; }
.hud-throttle span:nth-child(2) { font-size: 1.2em; font-weight: bold; color: #fff; min-width: 40px; text-align: center; }
.throttle-bar { width: 80px; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; margin-left: 5px; }
#throttle-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #00ff88, #f7c948, #ff6b35); transition: width 0.1s; border-radius: 3px; }
.hud-gforce span:nth-child(2) { font-size: 1.2em; font-weight: bold; color: #fff; min-width: 40px; text-align: center; }
#hud-warnings { min-width: 150px; text-align: center; }
#warning-text { font-size: 0.8em; color: #ff4444; font-weight: bold; animation: blink 0.5s infinite alternate; }
@keyframes blink { from { opacity: 1; } to { opacity: 0.3; } }

/* Crosshair */
#crosshair { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 40px; height: 40px; border: 2px solid rgba(255,0,0,0.7); border-radius: 50%; }
#crosshair::before { content: '+'; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); color: rgba(255,0,0,0.8); font-size: 1.5em; }

/* Overlays */
#pause-overlay, #game-over, #crash-overlay {
  position: absolute; top: 0; left: 0; width: 100%; height: 100%;
  background: rgba(0,0,0,0.85); display: flex; flex-direction: column; align-items: center; justify-content: center; z-index: 200;
}
#pause-overlay h2 { font-size: 3em; color: #f7c948; margin-bottom: 10px; }
#game-over h2, #crash-overlay h2 { font-size: 2.5em; margin-bottom: 15px; }
#go-title { color: #00ff88; }
#crash-overlay h2 { color: #ff4444; }
#game-over p, #crash-overlay p { color: #aabbcc; font-size: 1.1em; margin-bottom: 20px; max-width: 500px; text-align: center; }

button { padding: 12px 30px; font-size: 1em; font-weight: bold; border: none; border-radius: 6px; cursor: pointer; transition: all 0.2s; margin: 5px; }
#restart-btn, #restart-crash-btn { background: linear-gradient(90deg, #ff6b35, #e85d26); color: #fff; }
#menu-btn { background: rgba(255,255,255,0.1); color: #fff; border: 1px solid rgba(255,255,255,0.3) !important; }
button:hover { transform: scale(1.05); }

/* Dogfight enemy indicators */
.enemy-indicator { position: absolute; width: 8px; height: 8px; background: #ff4444; border-radius: 50%; pointer-events: none; z-index: 60; box-shadow: 0 0 10px #ff4444; }
/* ── css: hud-extra ── */
.hud-health { display: flex; align-items: center; gap: 5px; }
.health-bar { width: 80px; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; }
#health-fill { height: 100%; width: 100%; background: linear-gradient(90deg, #ff4444, #ffaa00, #00ff88); transition: width 0.2s; border-radius: 3px; }
</style>
</head>
<body>
<div id="app">
  <!-- Main canvas container -->
  <div id="canvas-container"></div>

  <!-- Loading / Menu Screen -->
  <div id="menu-screen">
    <h1 class="title">APEX AERO</h1>
    <p class="subtitle">Flight Simulator</p>
    
    <div id="aircraft-select">
      <h2>Select Aircraft</h2>
      <div class="aircraft-cards" id="aircraft-cards"></div>
    </div>

    <div id="scenario-select">
      <h2>Select Scenario</h2>
      <div class="scenario-cards" id="scenario-cards"></div>
    </div>

    <button id="start-btn" disabled="">LAUNCH SIMULATION</button>
    
    <div id="controls-info">
      <h3>Controls</h3>
      <div class="control-row"><span>Pitch Down/Up:</span><kbd>W / S</kbd></div>
      <div class="control-row"><span>Roll Left/Right:</span><kbd>A / D</kbd></div>
      <div class="control-row"><span>Yaw Left/Right:</span><kbd>Q / E</kbd></div>
      <div class="control-row"><span>Throttle Up/Down:</span><kbd>Shift / Ctrl</kbd></div>
      <div class="control-row"><span>Brakes:</span><kbd>B</kbd></div>
      <div class="control-row"><span>Gear Toggle:</span><kbd>G</kbd></div>
      <div class="control-row"><span>Flaps:</span><kbd>F</kbd></div>
      <div class="control-row"><span>Fire (Dogfight):</span><kbd>Space / Left Click</kbd></div>
      <div class="control-row"><span>Camera Toggle:</span><kbd>C</kbd></div>
    </div>
  </div>

  <!-- HUD Overlay -->
  <div id="hud" style="display:none;">
    <!-- Top bar: altitude and heading -->
    <div id="hud-top">
      <div class="hud-box hud-altitude">
        <span class="hud-label">ALT</span>
        <span id="hud-alt-value">0</span><span class="hud-unit">FT</span>
      </div>
      <div class="hud-box hud-heading">
        <span class="hud-label">HDG</span>
        <span id="hud-hdg-value">360</span><span class="hud-unit">°</span>
      </div>
    </div>

    <!-- Left side: airspeed tape -->
    <div id="hud-speed-tape" class="hud-tape hud-left">
      <div class="tape-values" id="speed-tape-values"></div>
      <div class="tape-current" id="speed-current">0</div>
      <span class="hud-unit tape-label">KTS</span>
    </div>

    <!-- Right side: altitude tape -->
    <div id="hud-alt-tape" class="hud-tape hud-right">
      <div class="tape-values" id="alt-tape-values"></div>
      <div class="tape-current" id="alt-current">0</div>
      <span class="hud-unit tape-label">FT</span>
    </div>

    <!-- Center: pitch ladder and horizon -->
    <div id="hud-center">
      <canvas id="horizon-canvas" width="400" height="300"></canvas>
      <div id="hud-pitch-ladder"></div>
      <div id="hud-aircraft-symbol"></div>
    </div>

    <!-- Bottom: throttle, G-force, warnings -->
    <div id="hud-bottom">
      <div class="hud-box hud-throttle">
        <span class="hud-label">THR</span>
        <span id="hud-thr-value">0</span><span class="hud-unit">%</span>
        <div class="throttle-bar"><div id="throttle-fill"></div></div>
      </div>
      <div class="hud-box hud-gforce" id="hud-gforce">
        <span class="hud-label">G</span>
        <span id="hud-g-value">1.0</span>
      </div>
      <div class="hud-box hud-warnings" id="hud-warnings"></div>
      <div class="hud-box hud-score" id="hud-score" style="display:none;">
        <span class="hud-label">SCORE</span>
        <span id="hud-score-value">0</span>
      </div>
    <div class="hud-box hud-health" id="hud-health" style="display:none;">
    <span class="hud-label">HP</span>
    <div class="health-bar"><div id="health-fill"></div></div>
    <span id="hud-hp-value" style="font-size:0.9em;color:#fff;min-width:30px;text-align:center;">100</span>
</div></div>

    <!-- Crosshair for dogfight -->
    <div id="crosshair" style="display:none;"></div>
  </div>

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

  <!-- Game Over / Mission Complete -->
  <div id="game-over" style="display:none;">
    <h2 id="go-title">MISSION COMPLETE</h2>
    <p id="go-message"></p>
    <button id="restart-btn">PLAY AGAIN</button>
    <button id="menu-btn">MAIN MENU</button>
  </div>

  <!-- Crash overlay -->
  <div id="crash-overlay" style="display:none;">
    <h2>CRASHED!</h2>
    <p id="crash-reason"></p>
    <button id="restart-crash-btn">RETRY</button>
  </div>
</div>
<script>
'use strict';

// ── const: DEG2RAD ──
const DEG2RAD = Math.PI / 180;

// ── const: RAD2DEG ──
const RAD2DEG = 180 / Math.PI;

// ── const: GRAVITY ──
const GRAVITY = 9.81;

// ── const: SEA_LEVEL_PRESSURE ──
// m/s²
const SEA_LEVEL_PRESSURE = 101325;

// ── const: SEA_LEVEL_TEMP ──
// Pa
const SEA_LEVEL_TEMP = 288.15;

// ── const: TEMP_LAPSE_RATE ──
// K
const TEMP_LAPSE_RATE = 0.0065;

// ── function: getAirDensity ──
// K/m

// Sea level air density (kg/m³)
function getAirDensity(altitudeMeters) {
    const h = Math.max(0, altitudeMeters);
    const temp = SEA_LEVEL_TEMP - TEMP_LAPSE_RATE * h;
    const pressure = SEA_LEVEL_PRESSURE * Math.pow(temp / SEA_LEVEL_TEMP, 5.25588);
    return (pressure / (287.05 * temp)); // ideal gas law
}

// ── const: AIRCRAFT_PROFILES ──
// Aircraft profiles with distinct physics characteristics
const AIRCRAFT_PROFILES = {
    falcon: {
        name: "Falcon X-1",
        icon: "🛩️",
        description: "Light fighter - agile and fast",
        mass: 4500, // kg
        wingArea: 28, // m²
        maxThrust: 65000, // N (per engine, single)
        thrustResponseTime: 1.5, // seconds to full throttle
        clMax: 1.8, // max lift coefficient
        cd0: 0.02, // zero-lift drag coefficient
        kInduced: 0.04, // induced drag factor (1/(π*e*AR))
        rollRate: 2.5, // rad/s max
    pitchRate: 1.8, // rad/s max
    yawRate: 0.6, // rad/s max
    momentOfInertiaX: 8000, // kg·m² (roll)
    momentOfInertiaY: 25000, // kg·m² (pitch)
    momentOfInertiaZ: 30000, // kg·m² (yaw)
    maxSpeed: 340, // m/s (~660 kts)
    stallSpeed: 35, // m/s at sea level, 1G
    color: 0x2244aa,
    accentColor: 0xff6b35,
    scale: 1.0
    },
    titan: {
        name: "Titan B-2",
        icon: "✈️",
        description: "Heavy bomber - powerful but slow",
        mass: 18000, // kg
        wingArea: 75, // m²
        maxThrust: 140000, // N (total)
        thrustResponseTime: 3.0, // seconds to full throttle
        clMax: 2.2, // higher for slower flight
        cd0: 0.035, // more drag due to size
        kInduced: 0.03,
    rollRate: 1.2, // rad/s max - sluggish
    pitchRate: 0.8, // rad/s max
    yawRate: 0.3, // rad/s max
    momentOfInertiaX: 65000,
    momentOfInertiaY: 180000,
    momentOfInertiaZ: 220000,
    maxSpeed: 250, // m/s (~485 kts)
    stallSpeed: 45, // m/s - heavier needs more speed
    color: 0x3a5a3a,
    accentColor: 0x66cc66,
    scale: 1.5
    },
    viper: {
        name: "Viper S-7",
        icon: "🚀",
        description: "Interceptor - extreme performance",
        mass: 8200, // kg
        wingArea: 36, // m²
        maxThrust: 120000, // N (total)
        thrustResponseTime: 1.0, // very responsive engine
        clMax: 2.5, // delta wing high CL
        cd0: 0.018, // clean aerodynamics
        kInduced: 0.035,
    rollRate: 3.5, // rad/s max - extremely agile
    pitchRate: 2.2, // rad/s max
    yawRate: 0.8, // rad/s max
    momentOfInertiaX: 15000,
    momentOfInertiaY: 45000,
    momentOfInertiaZ: 55000,
    maxSpeed: 380, // m/s (~740 kts) - fastest
    stallSpeed: 40, // m/s
    color: 0x662222,
    accentColor: 0xffcc00,
    scale: 1.15
    }
};

// ── const: SCENARIOS ──
// Scenario definitions
const SCENARIOS = {
    takeoffLanding: {
        name: "Takeoff & Landing",
        icon: "🛫",
        description: "Master precision flight - take off and land safely on the runway"
    },
    dogfight: {
        name: "Dogfight Arena",
        icon: "⚔️",
        description: "High-speed aerial combat against enemy aircraft"
    }
};

// ── const: INPUT ──
// Input state
const INPUT = {
    pitchDown: false, pitchUp: false,
    rollLeft: false, rollRight: false,
    yawLeft: false, yawRight: false,
    throttleUp: false, throttleDown: false,
    brakes: false, gearToggle: false, flaps: false, fire: false, cameraToggle: false,
    mouseX: 0, mouseY: 0, mouseDown: false
};

// ── let: gameState ──
// Game state
let gameState = 'menu';

// ── let: selectedAircraft ──
// menu, playing, paused, gameover, crashed
let selectedAircraft = null;

// ── let: selectedScenario ──
let selectedScenario = null;

// ── class: FlightPhysics ──
class FlightPhysics {
    constructor(profile) {
        this.profile = profile;
        // Position (meters) - world space
        this.position = new THREE.Vector3(0, 5, 0);
        // Velocity (m/s) - body frame initially aligned with world
        this.velocity = new THREE.Vector3(0, 0, 0);
        // Orientation as quaternion
        this.quaternion = new THREE.Euler(0, 0, 0);
        this.orientationQuat = new THREE.Quaternion();
        // Angular velocity (rad/s) - body frame
        this.angularVelocity = new THREE.Vector3(0, 0, 0);
        
        // Control surfaces deflection (-1 to 1)
        this.elevatorDeflection = 0;
        this.aileronDeflection = 0;
        this.rudderDeflection = 0;
        
        // Engine state
        this.throttleTarget = 0; // 0-1 target
        this.throttleCurrent = 0; // 0-1 actual (with response lag)
        this.currentThrust = 0; // N
        
        // Gear and flaps
        this.gearDown = true;
        this.flapAngle = 0; // radians, 0 to ~0.35
        this.brakeForce = 0; // N per wheel
        
        // Derived state
        this.airSpeed = 0; // m/s (true airspeed)
        this.angleOfAttack = 0; // radians
        this.sideslipAngle = 0; // radians
        this.gLoad = 1.0;
        this.liftForce = new THREE.Vector3();
        this.dragForce = new THREE.Vector3();
        this.totalForce = new THREE.Vector3();
        
        // Warnings
        this.stallWarning = false;
        this.overGWarning = false;
        this.speedWarning = false;
    }
    
    reset(x, y, z, heading) {
        this.position.set(x, y, z);
        this.velocity.set(0, 0, 0);
        this.quaternion.set(0, -heading * DEG2RAD, 0); // pitch, yaw, roll
        this.orientationQuat.setFromEuler(this.quaternion);
        this.angularVelocity.set(0, 0, 0);
        this.elevatorDeflection = 0;
        this.aileronDeflection = 0;
        this.rudderDeflection = 0;
        this.throttleTarget = 0;
        this.throttleCurrent = 0;
        this.currentThrust = 0;
        this.gearDown = true;
        this.flapAngle = 0;
        this.brakeForce = 0;
    }
    
    update(dt) {
        const p = this.profile;
        
        // Clamp dt to prevent physics explosion
        dt = Math.min(dt, 0.05);
        
        // Update throttle response (engine lag)
        const thrustRate = 1.0 / p.thrustResponseTime;
        if (this.throttleCurrent < this.throttleTarget) {
            this.throttleCurrent = Math.min(this.throttleTarget, this.throttleCurrent + thrustRate * dt);
        } else {
            this.throttleCurrent = Math.max(this.throttleTarget, this.throttleCurrent - thrustRate * dt);
        }
        
        // Current thrust force (N)
        this.currentThrust = p.maxThrust * this.throttleCurrent;
        
        // Get world-space velocity magnitude
        const velWorld = this.velocity.length();
        this.airSpeed = Math.max(0, -this.velocity.dot(this.getForward()));
        
        // Air density at current altitude
        const rho = getAirDensity(Math.max(0, this.position.y));
        const dynamicPressure = 0.5 * rho * this.airSpeed * this.airSpeed;
        
        // Calculate angle of attack (simplified)
        const forwardBody = new THREE.Vector3(1, 0, 0);
        const velBody = this.velocity.clone().applyQuaternion(this.orientationQuat.clone().invert());
        if (this.airSpeed > 1.0) {
            this.angleOfAttack = Math.atan2(-velBody.y, Math.max(1, -velBody.x));
            this.sideslipAngle = Math.asin(Math.max(-1, Math.min(1, velBody.z / Math.max(1, this.airSpeed))));
        } else {
            this.angleOfAttack = 0;
            this.sideslipAngle = 0;
        }
        
        // Clamp angle of attack for realistic behavior
        const aoaMax = 0.45; // ~26 degrees before stall
        const aoaEffective = Math.max(-aoaMax, Math.min(aoaMax, this.angleOfAttack));
        
        // Lift coefficient: CL = CL_max * (1 - |alpha|/alpha_stall) for linear region
        // With flap bonus
        const flapBonus = this.flapAngle / 0.35; // 0 to 1
        let clAlpha = 6.28; // lift curve slope per radian (thin airfoil theory ~2π)
        let clBase = clAlpha * aoaEffective;
        
        // Add flap contribution
        const clFlap = flapBonus * 1.5;
        let clTotal = Math.min(p.clMax + clFlap, clBase + clFlap);
        
        // Stall: if AoA exceeds critical, lift drops dramatically
        if (Math.abs(this.angleOfAttack) > aoaMax * 0.8) {
            const stallFactor = 1.0 - Math.pow((Math.abs(this.angleOfAttack) - aoaMax * 0.8) / (aoaMax * 0.2), 2);
            clTotal *= Math.max(0, stallFactor);
        }
        
        this.stallWarning = (this.airSpeed > 5 && Math.abs(this.angleOfAttack) > aoaMax * 0.7);
        
        // Lift force magnitude: L = CL * q * S
        const liftMagnitude = clTotal * dynamicPressure * p.wingArea;
        
        // Drag calculation
        // Profile drag (parasite): CD0 * q * S
        const profileDrag = p.cd0 * dynamicPressure * p.wingArea;
        
        // Induced drag: K * CL² * q * S
        const inducedDrag = p.kInduced * clTotal * clTotal * dynamicPressure * p.wingArea;
        
        // Gear and flap drag penalty
        const gearDrag = this.gearDown ? 0.015 * dynamicPressure * p.wingArea : 0;
        const flapDrag = flapBonus * 0.02 * dynamicPressure * p.wingArea;
        
        const totalDragMagnitude = profileDrag + inducedDrag + gearDrag + flapDrag;
        
        // Speed warning (approaching max)
        this.speedWarning = this.airSpeed > p.maxSpeed * 0.9;
        
        // Build force vectors in body frame, then transform to world
        
        // Thrust: along body X axis (forward)
        const thrustBody = new THREE.Vector3(this.currentThrust, 0, 0);
        const thrustWorld = thrustBody.applyQuaternion(this.orientationQuat);
        
        // Drag: opposite to velocity direction
        let dragWorld = new THREE.Vector3(0, 0, 0);
        if (this.airSpeed > 0.1) {
            const velDir = this.velocity.clone().normalize();
            dragWorld.copy(velDir).multiplyScalar(totalDragMagnitude);
        }
        
        // Lift: perpendicular to velocity in the plane of symmetry
        let liftWorld = new THREE.Vector3(0, 0, 0);
        if (this.airSpeed > 0.1) {
            const velDir = this.velocity.clone().normalize();
            // Lift direction is perpendicular to velocity and right wing vector
            const rightBody = new THREE.Vector3(0, 0, 1).applyQuaternion(this.orientationQuat);
            const liftDir = new THREE.Vector3().crossVectors(rightBody, velDir).normalize();
            liftWorld.copy(liftDir).multiplyScalar(-liftMagnitude); // negative because our convention
        }
        
        this.liftForce.copy(liftWorld);
        this.dragForce.copy(dragWorld);
        
        // Gravity: straight down in world space
        const gravityForce = new THREE.Vector3(0, -p.mass * GRAVITY, 0);
        
        // Brake force (only effective on ground)
        let brakeWorld = new THREE.Vector3(0, 0, 0);
        if (this.position.y < 2 && this.brakeForce > 0) {
            const frictionCoeff = 0.8;
            const normalForce = p.mass * GRAVITY - liftMagnitude; // reduced by lift
            brakeWorld.copy(this.velocity).normalize().multiplyScalar(-Math.min(this.brakeForce, frictionCoeff * Math.max(0, normalForce)));
        }
        
        // Total force
        this.totalForce.copy(thrustWorld)
            .add(dragWorld)
            .add(liftWorld)
            .add(gravityForce)
            .add(brakeWorld);
        
        // Ground collision: prevent going below terrain
        if (this.position.y < 0.5 && liftMagnitude < p.mass * GRAVITY) {
            this.totalForce.y = Math.max(0, this.totalForce.y + p.mass * GRAVITY);
            this.velocity.y = Math.max(0, this.velocity.y);
            // Ground friction on lateral movement
            if (this.position.y <= 0.5) {
                const groundFriction = 0.9;
                this.velocity.x *= (1 - groundFriction * dt);
                this.velocity.z *= (1 - groundFriction * dt);
            }
        }
        
        // Newton's second law: F = ma => a = F/m
        const acceleration = this.totalForce.clone().divideScalar(p.mass);
        
        // Integrate velocity and position
        this.velocity.add(acceleration.multiplyScalar(dt));
        this.position.add(this.velocity.clone().multiplyScalar(dt));
        
        // Calculate G-load (normal force / weight)
        const normalAccel = liftMagnitude / p.mass;
        this.gLoad = 1.0 + normalAccel / GRAVITY;
        if (this.position.y < 2) {
            this.gLoad = Math.max(1, this.gLoad); // ground supports weight
        }
        this.overGWarning = Math.abs(this.gLoad) > 7;
        
        // === ROTATIONAL DYNAMICS ===
        // Calculate control surface moments
        
        // Elevator moment (pitch): proportional to deflection and dynamic pressure
        const elevatorMoment = -this.elevatorDeflection * dynamicPressure * p.wingArea * 3.0 / p.mass;
        
        // Aileron moment (roll)
        const aileronMoment = this.aileronDeflection * dynamicPressure * p.wingArea * 2.5 / p.mass;
        
        // Rudder moment (yaw) + adverse yaw from ailerons
        const rudderMoment = -this.rudderDeflection * dynamicPressure * p.wingArea * 1.5 / p.mass;
        const adverseYaw = -this.aileronDeflection * dynamicPressure * p.wingArea * 0.3 / p.mass;
        
        // Damping moments (proportional to angular velocity)
        const rollDamping = -this.angularVelocity.x * dynamicPressure * p.wingArea * 0.5 / p.mass;
        const pitchDamping = -this.angularVelocity.y * dynamicPressure * p.wingArea * 1.0 / p.mass;
        const yawDamping = -this.angularVelocity.z * dynamicPressure * p.wingArea * 0.8 / p.mass;
        
        // Total angular acceleration (with moment of inertia)
        const rollAccel = (aileronMoment + rollDamping) / Math.max(1, p.momentOfInertiaX / 1000);
        const pitchAccel = (elevatorMoment + pitchDamping) / Math.max(1, p.momentOfInertiaY / 1000);
        const yawAccel = (rudderMoment + adverseYaw + yawDamping) / Math.max(1, p.momentOfInertiaZ / 1000);
        
        // Integrate angular velocity with rate limits
        this.angularVelocity.x += rollAccel * dt;
        this.angularVelocity.y += pitchAccel * dt;
        this.angularVelocity.z += yawAccel * dt;
        
        // Clamp angular rates to aircraft limits
        const speedFactor = Math.min(1, this.airSpeed / 30); // reduced control at low speed
        this.angularVelocity.x = THREE.MathUtils.clamp(this.angularVelocity.x, -p.rollRate * speedFactor, p.rollRate * speedFactor);
        this.angularVelocity.y = THREE.MathUtils.clamp(this.angularVelocity.y, -p.pitchRate * speedFactor, p.pitchRate * speedFactor);
        this.angularVelocity.z = THREE.MathUtils.clamp(this.angularVelocity.z, -p.yawRate * speedFactor, p.yawRate * speedFactor);
        
        // Integrate orientation using angular velocity
        const angVelQuat = new THREE.Quaternion().setFromEuler(
            new THREE.Euler(this.angularVelocity.x * dt, this.angularVelocity.y * dt, this.angularVelocity.z * dt)
        );
        this.orientationQuat.multiply(angVelQuat).normalize();
        
        // Update Euler angles for reference
        this.quaternion.setFromQuaternion(this.orientationQuat);
    }
    
    getForward() {
        return new THREE.Vector3(1, 0, 0).applyQuaternion(this.orientationQuat);
    }
    
    getUp() {
        return new THREE.Vector3(0, 1, 0).applyQuaternion(this.orientationQuat);
    }
    
    getRight() {
        return new THREE.Vector3(0, 0, 1).applyQuaternion(this.orientationQuat);
    }
}

// ── function: buildEnemyModel ──
function buildEnemyModel() {
    const group = new THREE.Group();
    const s = 0.9;
    
    const bodyMat = new THREE.MeshPhongMaterial({ color: 0x882222, shininess: 60 });
    const darkMat = new THREE.MeshPhongMaterial({ color: 0x1a1a1a, shininess: 40 });
    
    // Simple enemy fighter shape
    const fuselageGeo = new THREE.CylinderGeometry(0.25 * s, 0.2 * s, 5 * s, 6);
    const fuselage = new THREE.Mesh(fuselageGeo, bodyMat);
    fuselage.rotation.z = Math.PI / 2;
    group.add(fuselage);
    
    // Nose
    const noseGeo = new THREE.ConeGeometry(0.25 * s, 1.2 * s, 6);
    const nose = new THREE.Mesh(noseGeo, bodyMat);
    nose.rotation.z = -Math.PI / 2;
    nose.position.x = 3.1 * s;
    group.add(nose);
    
    // Wings
    const wingShape = new THREE.Shape();
    wingShape.moveTo(0, 0);
    wingShape.lineTo(2 * s, -1 * s);
    wingShape.lineTo(2 * s, -2.2 * s);
    wingShape.lineTo(-0.5 * s, -2 * s);
    wingShape.lineTo(-0.5 * s, 0);
    
    const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.05 * s });
    const rWing = new THREE.Mesh(wingGeo, bodyMat);
    rWing.rotation.x = -Math.PI / 2;
    rWing.position.set(0.3 * s, -0.05 * s, 0.03 * s);
    group.add(rWing);
    
    const lWing = new THREE.Mesh(wingGeo, bodyMat);
    lWing.rotation.x = Math.PI / 2;
    lWing.position.set(0.3 * s, -0.05 * s, -0.03 * s);
    group.add(lWing);
    
    // Tail fin
    const tailFinShape = new THREE.Shape();
    tailFinShape.moveTo(0, 0);
    tailFinShape.lineTo(-1 * s, -0.2 * s);
    tailFinShape.lineTo(-2 * s, 0);
    const tailGeo = new THREE.ExtrudeGeometry(tailFinShape, { depth: 0.03 * s });
    const tailFin = new THREE.Mesh(tailGeo, bodyMat);
    tailFin.position.set(-1.5 * s, 0.2 * s, -0.015 * s);
    group.add(tailFin);
    
    // Red accent on wings
    [-2, 2].forEach(z => {
        const tipGeo = new THREE.BoxGeometry(0.3 * s, 0.06 * s, 0.15 * s);
        const tipMat = new THREE.MeshPhongMaterial({ color: 0xff0000 });
        const tip = new THREE.Mesh(tipGeo, tipMat);
        tip.position.set(2 * s, -0.05 * s, z * s);
        group.add(tip);
    });
    
    // Exhaust glow
    const exhaustGeo = new THREE.CylinderGeometry(0.12 * s, 0.18 * s, 0.3 * s, 6);
    const exhaustMat = new THREE.MeshPhongMaterial({ color: 0xff4400, emissive: 0xff2200 });
    const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);
    exhaust.rotation.z = Math.PI / 2;
    exhaust.position.x = -2.65 * s;
    group.add(exhaust);
    
    return group;
}

// ── class: NoiseGenerator ──
// Simplex-like noise implementation for terrain generation
class NoiseGenerator {
    constructor(seed) {
        this.seed = seed || Math.random() * 65536;
        this.perm = new Uint8Array(512);
        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]
        ];
        this.init();
    }
    
    init() {
        const p = new Uint8Array(256);
        for (let i = 0; i < 256; i++) p[i] = i;
        
        // Shuffle with seed
        let s = this.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];
        }
    }
    
    dot3(g, x, y) {
        return g[0] * x + g[1] * y;
    }
    
    noise2D(x, y) {
        const F2 = 0.5 * (Math.sqrt(3) - 1);
        const G2 = (3 - Math.sqrt(3)) / 6;
        
        const s = (x + y) * F2;
        const i = Math.floor(x + s);
        const j = Math.floor(y + s);
        
        const t = (i + j) * G2;
        const X0 = i - t;
        const Y0 = j - t;
        
        const x0 = x - X0;
        const y0 = y - Y0;
        
        let i1, j1;
        if (x0 > y0) { i1 = 1; j1 = 0; }
        else { i1 = 0; j1 = 1; }
        
        const x1 = x0 - i1 + G2;
        const y1 = y0 - j1 + G2;
        const x2 = x0 - 1 + 2 * G2;
        const y2 = y0 - 1 + 2 * G2;
        
        const ii = i & 255;
        const 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;
        if (t0 < 0) n0 = 0;
        else { t0 *= t0; n0 = t0 * t0 * this.dot3(this.grad3[gi0], x0, y0); }
        
        let t1 = 0.5 - x1 * x1 - y1 * y1;
        if (t1 < 0) n1 = 0;
        else { t1 *= t1; n1 = t1 * t1 * this.dot3(this.grad3[gi1], x1, y1); }
        
        let t2 = 0.5 - x2 * x2 - y2 * y2;
        if (t2 < 0) n2 = 0;
        else { t2 *= t2; n2 = t2 * t2 * this.dot3(this.grad3[gi2], x2, y2); }
        
        return 70 * (n0 + n1 + n2);
    }
    
    // Fractal Brownian Motion for realistic terrain
    fbm(x, y, octaves) {
        let value = 0;
        let amplitude = 1;
        let frequency = 1;
        let maxValue = 0;
        
        for (let i = 0; i < octaves; i++) {
            value += amplitude * this.noise2D(x * frequency, y * frequency);
            maxValue += amplitude;
            amplitude *= 0.5;
            frequency *= 2;
        }
        
        return value / maxValue;
    }
    
    // Get terrain height at world position (x, z)
    getHeight(wx, wz) {
        const scale = 0.003;
        let h = this.fbm(wx * scale, wz * scale, 6);
        
        // Normalize to 0-1 range then map to height
        h = (h + 1) / 2;
        
        // Apply power for mountain peaks
        h = Math.pow(h, 1.5);
        
        // Base terrain height
        let height = h * 800; // max ~800m
        
        // Add some detail noise
        const detail = this.fbm(wx * scale * 4, wz * scale * 4, 3) * 50;
        height += detail;
        
        return Math.max(0, height);
    }
    
    // Get terrain color based on height and slope
    getTerrainColor(height, slope) {
        if (height < 10) return new THREE.Color(0x2a6e3f); // grass lowland
        if (height < 50) return new THREE.Color(0x3d8b4f); // forest green
        if (height < 150) return new THREE.Color(0x5a7a4a); // dark green hills
        if (height < 300) return new THREE.Color(0x6b6b5e); // rocky brown
        if (height < 500) return new THREE.Color(0x8a8a7a); // gray rock
        if (height < 650) return new THREE.Color(0xb0b0a8); // light rock
        return new THREE.Color(0xe8e8f0); // snow peaks
    }
}

// ── function: generateTerrain ──
// Terrain mesh generator with LOD-like approach
function generateTerrain(noise, size, resolution) {
    const halfSize = size / 2;
    const segments = resolution;
    
    const geometry = new THREE.PlaneGeometry(size, size, segments, segments);
    geometry.rotateX(-Math.PI / 2); // Lay flat
    
    const positions = geometry.attributes.position.array;
    const colors = new Float32Array(positions.length);
    
    for (let i = 0; i < positions.length; i += 3) {
        const x = positions[i];
        const z = positions[i + 2];
        
        // Get height from noise
        const h = noise.getHeight(x, z);
        positions[i + 1] = h;
        
        // Calculate approximate slope for coloring
        const dx = size / segments;
        const dz = size / segments;
        const hL = noise.getHeight(x - dx, z);
        const hR = noise.getHeight(x + dx, z);
        const hU = noise.getHeight(x, z - dz);
        const hD = noise.getHeight(x, z + dz);
        const slope = Math.sqrt(Math.pow((hR - hL) / (2 * dx), 2) + Math.pow((hD - hU) / (2 * dz), 2));
        
        // Color based on height and slope
        let color;
        if (slope > 0.5) {
            color = new THREE.Color(0x6b6b5e); // steep = rocky
        } else {
            color = noise.getTerrainColor(h, slope);
        }
        
        colors[i] = color.r;
        colors[i + 1] = color.g;
        colors[i + 2] = color.b;
    }
    
    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    geometry.computeVertexNormals();
    
    const material = new THREE.MeshPhongMaterial({
        vertexColors: true,
        flatShading: false,
        shininess: 10,
        side: THREE.DoubleSide
    });
    
    return new THREE.Mesh(geometry, material);
}

// ── function: generateRunway ──
// Generate a runway for takeoff/landing scenario
function generateRunway(length) {
    const group = new THREE.Group();
    
    // Runway surface (dark asphalt)
    const runwayGeo = new THREE.PlaneGeometry(30, length || 2000);
    const runwayMat = new THREE.MeshPhongMaterial({ 
        color: 0x333333, shininess: 5 
    });
    const runway = new THREE.Mesh(runwayGeo, runwayMat);
    runway.rotation.x = -Math.PI / 2;
    group.add(runway);
    
    // Center line markings
    for (let z = -(length || 2000) / 2 + 50; z < (length || 2000) / 2 - 50; z += 40) {
        const markGeo = new THREE.PlaneGeometry(1, 20);
        const markMat = new THREE.MeshPhongMaterial({ color: 0xffffff });
        const mark = new THREE.Mesh(markGeo, markMat);
        mark.rotation.x = -Math.PI / 2;
        mark.position.set(0, 0.05, z);
        group.add(mark);
    }
    
    // Edge lights (yellow)
    [-15, 15].forEach(x => {
        for (let z = -(length || 2000) / 2; z < (length || 2000) / 2; z += 30) {
            const lightGeo = new THREE.SphereGeometry(0.3, 6, 4);
            const lightMat = new THREE.MeshPhongMaterial({ 
                color: 0xffcc00, emissive: 0xffaa00, emissiveIntensity: 0.5 
            });
            const light = new THREE.Mesh(lightGeo, lightMat);
            light.position.set(x, 0.3, z);
            group.add(light);
        }
    });
    
    // Threshold markings (white stripes at start)
    for (let i = 0; i < 6; i++) {
        const stripeGeo = new THREE.PlaneGeometry(4, 2);
        const stripeMat = new THREE.MeshPhongMaterial({ color: 0xffffff });
        
        [-8 + i * 3.2, 8 - i * 3.2].forEach(x => {
            const stripe = new THREE.Mesh(stripeGeo, stripeMat);
            stripe.rotation.x = -Math.PI / 2;
            stripe.position.set(x, 0.06, -(length || 2000) / 2 + 100);
            group.add(stripe);
        });
    }
    
    // Runway numbers (simplified as white rectangles)
    const numGeo = new THREE.PlaneGeometry(8, 3);
    const numMat = new THREE.MeshPhongMaterial({ color: 0xffffff });
    [-150, 150].forEach(z => {
        const num = new THREE.Mesh(numGeo, numMat);
        num.rotation.x = -Math.PI / 2;
        num.position.set(0, 0.06, z);
        group.add(num);
    });
    
    return group;
}

// ── function: generateWater ──
// Generate water plane for ocean/coastline effect
function generateWater(size) {
    const geo = new THREE.PlaneGeometry(size * 2, size * 2, 50, 50);
    geo.rotateX(-Math.PI / 2);
    
    const mat = new THREE.MeshPhongMaterial({ 
        color: 0x1a4a7a, 
        transparent: true, 
        opacity: 0.7, 
        shininess: 100, 
        specular: 0x88bbff 
    });
    
    const water = new THREE.Mesh(geo, mat);
    water.position.y = -5; // Slightly below terrain base
    
    return { mesh: water, material: mat };
}

// ── class: TakeoffLandingMission ──
// Takeoff & Landing Mission Manager
class TakeoffLandingMission {
    constructor(scene, physics) {
        this.scene = scene;
        this.physics = physics;
        this.phase = 'taxi'; // taxi, takeoff, climb, cruise, descent, approach, landing, complete
        this.score = 0;
        this.runwayLength = 2500;
        this.targetAltitude = 300; // meters (~1000ft) for cruise
        this.landingZoneStart = -this.runwayLength / 2 + 200;
        
        // Landing metrics
        this.touchdownSpeed = 0;
        this.touchdownFlareRate = 0;
        this.touchdownDistanceFromThreshold = 0;
        
        // Runway object reference
        this.runway = null;
    }
    
    init() {
        // Place runway at origin, flat area
        const noise = new NoiseGenerator(42);
        this.noise = noise;
        
        // Create runway
        this.runway = generateRunway(this.runwayLength);
        this.scene.add(this.runway);
        
        // Position aircraft on runway for takeoff
        this.physics.reset(0, 1.5, -this.runwayLength / 2 + 100, Math.PI); // facing down runway
        
        // Set initial throttle to idle
        this.physics.throttleTarget = 0.1;
        
        // Add approach lights and markers
        this.addApproachMarkers();
    }
    
    addApproachMarkers() {
        // PAPI-like lights on the side of runway
        const papiGroup = new THREE.Group();
        for (let i = 0; i < 4; i++) {
            const lightGeo = new THREE.SphereGeometry(0.5, 6, 4);
            const color = i < 2 ? 0xff0000 : 0xffffff; // red and white PAPI
            const lightMat = new THREE.MeshPhongMaterial({ 
                color: color, emissive: color, emissiveIntensity: 0.8 
            });
            const light = new THREE.Mesh(lightGeo, lightMat);
            light.position.set(18, 2, -this.runwayLength / 2 + 300 + i * 5);
            papiGroup.add(light);
        }
        this.scene.add(papiGroup);
    }
    
    update(dt) {
        const p = this.physics;
        const alt = Math.max(0, p.position.y);
        const speedKts = p.airSpeed * 1.94384; // m/s to knots
        
        // Phase detection
        if (this.phase === 'taxi') {
            if (speedKts > 20) this.phase = 'takeoff';
        } else if (this.phase === 'takeoff') {
            if (alt > 10) this.phase = 'climb';
        } else if (this.phase === 'climb') {
            if (alt >= this.targetAltitude * 0.9) {
                this.phase = 'cruise';
                this.score += 25; // Reached altitude
            }
        } else if (this.phase === 'cruise') {
            // Check if player is descending toward landing zone
            const zPos = p.position.z;
            if (zPos > -this.runwayLength / 2 + 100 && alt < this.targetAltitude * 0.5) {
                this.phase = 'descent';
            }
        } else if (this.phase === 'descent') {
            if (alt < 30) this.phase = 'approach';
        } else if (this.phase === 'approach') {
            // Check for landing
            if (alt <= 2 && speedKts > 5) {
                this.touchdownSpeed = speedKts;
                this.touchdownFlareRate = p.velocity.y;
                this.touchdownDistanceFromThreshold = Math.abs(p.position.z - (-this.runwayLength / 2 + 300));
                
                // Score landing
                let landingScore = 100;
                const stallSpd = p.profile.stallSpeed * 1.94384;
                const idealTouchdown = stallSpd * 1.15;
                
                // Speed score (ideal: ~15% above stall)
                landingScore -= Math.abs(speedKts - idealTouchdown) * 2;
                
                // Flare rate score (should be near zero or slightly negative)
                landingScore -= Math.abs(p.velocity.y) * 10;
                
                // Distance from threshold score
                landingScore -= this.touchdownDistanceFromThreshold / 50;
                
                this.score += Math.max(0, Math.round(landingScore));
                
                if (speedKts < stallSpd * 2 && Math.abs(p.velocity.y) < 3) {
                    this.phase = 'complete';
                    return true; // Mission complete
                } else {
                    this.phase = 'taxi'; // Try again
                }
            }
        }
        
        return false;
    }
    
    getInstructions() {
        const instructions = [
            `Phase: ${this.phase.toUpperCase()}`,
        ];
        
        switch (this.phase) {
            case 'taxi':
                instructions.push("Increase throttle and accelerate down the runway");
                break;
            case 'takeoff':
                instructions.push("Pull up gently with W key to rotate");
                break;
            case 'climb':
                instructions.push(`Climb to ${Math.round(this.targetAltitude * 3.28)} ft`);
                break;
            case 'cruise':
                instructions.push("Fly down the runway, then descend for landing");
                break;
            case 'descent':
                instructions.push("Reduce throttle and begin descent");
                break;
            case 'approach':
                instructions.push("Flare gently before touchdown - aim for ~15% above stall speed");
                break;
            case 'complete':
                instructions.push(`Excellent landing! Score: ${this.score}`);
                break;
        }
        
        return instructions.join('\n');
    }
    
    cleanup() {
        if (this.runway) this.scene.remove(this.runway);
    }
}

// ── class: DogfightMission ──
// Dogfight Mission Manager with enemy AI
class DogfightMission {
    constructor(scene, physics) {
        this.scene = scene;
        this.physics = physics;
        this.enemies = [];
        this.bullets = [];
        this.enemyBullets = [];
        this.score = 0;
        this.playerHealth = 100;
        this.maxEnemies = 4;
        this.fireCooldown = 0;
        this.explosions = [];
        
        // Arena bounds
        this.arenaRadius = 5000;
        this.minAltitude = 200;
        this.maxAltitude = 3000;
    }
    
    init() {
        const noise = new NoiseGenerator(123);
        this.noise = noise;
        
        // Position player in the air for dogfight
        this.physics.reset(0, 500, 0, Math.PI / 4);
        this.physics.velocity.set(80, 0, 80); // Start with some speed
        this.physics.throttleTarget = 0.6;
        this.physics.gearDown = false;
        
        // Spawn initial enemies
        for (let i = 0; i < this.maxEnemies; i++) {
            this.spawnEnemy();
        }
    }
    
    spawnEnemy() {
        const angle = Math.random() * Math.PI * 2;
        const dist = 1500 + Math.random() * 2000;
        const alt = 300 + Math.random() * 1500;
        
        const enemyModel = buildEnemyModel();
        this.scene.add(enemyModel);
        
        // Enemy physics (simplified)
        const enemyProfile = { ...AIRCRAFT_PROFILES.falcon };
        const enemyPhysics = new FlightPhysics(enemyProfile);
        enemyPhysics.reset(
            Math.cos(angle) * dist,
            alt,
            Math.sin(angle) * dist,
            angle + Math.PI // Face toward center
        );
        enemyPhysics.velocity.set(
            -Math.cos(angle) * 60,
            0,
            -Math.sin(angle) * 60
        );
        enemyPhysics.throttleTarget = 0.5;
        
        this.enemies.push({
            model: enemyModel,
            physics: enemyPhysics,
            health: 100,
            state: 'patrol', // patrol, engage, evade, retreat
            targetAngle: Math.random() * Math.PI * 2,
            fireCooldown: 0,
            alive: true
        });
    }
    
    update(dt) {
        const p = this.physics;
        
        // Update player bullets
        this.updateBullets(this.bullets, dt);
        this.updateBullets(this.enemyBullets, dt);
        
        // Update explosions
        this.explosions = this.explosions.filter(exp => {
            exp.life -= dt;
            if (exp.mesh) {
                const s = Math.max(0.1, exp.life / exp.maxLife);
                exp.mesh.scale.setScalar(s * 5);
                exp.mesh.material.opacity = s;
            }
            return exp.life > 0;
        });
        
        // Fire cooldown
        if (this.fireCooldown > 0) this.fireCooldown -= dt;
        
        // Player fire
        if (INPUT.fire && this.fireCooldown <= 0) {
            this.fireBullet(p);
            this.fireCooldown = 0.15; // Rate of fire
        }
        
        // Update enemies
        for (const enemy of this.enemies) {
            if (!enemy.alive) continue;
            
            this.updateEnemyAI(enemy, dt);
            enemy.physics.update(dt);
            
            // Sync model position/rotation
            enemy.model.position.copy(enemy.physics.position);
            enemy.model.quaternion.copy(enemy.physics.orientationQuat);
            
            // Enemy fire at player
            if (enemy.fireCooldown <= 0 && enemy.state === 'engage') {
                const dist = p.position.distanceTo(enemy.physics.position);
                if (dist < 1500) {
                    this.enemyFireBullet(enemy);
                    enemy.fireCooldown = 0.3 + Math.random() * 0.2;
                }
            }
            enemy.fireCooldown -= dt;
            
            // Keep enemies in bounds
            const distFromCenter = Math.sqrt(
                enemy.physics.position.x ** 2 + 
                enemy.physics.position.z ** 2
            );
            if (distFromCenter > this.arenaRadius) {
                // Turn back toward center
                const angleToCenter = Math.atan2(-enemy.physics.position.x, -enemy.physics.position.z);
                enemy.targetAngle = angleToCenter;
            }
            
            // Keep altitude in bounds
            if (enemy.physics.position.y < this.minAltitude) {
                enemy.physics.elevatorDeflection = 0.5;
            } else if (enemy.physics.position.y > this.maxAltitude) {
                enemy.physics.elevatorDeflection = -0.3;
            }
        }
        
        // Check bullet collisions with enemies
        for (const bullet of this.bullets) {
            for (const enemy of this.enemies) {
                if (!enemy.alive) continue;
                const dist = bullet.position.distanceTo(enemy.physics.position);
                if (dist < 15) {
                    enemy.health -= 25;
                    bullet.hit = true;
                    
                    // Create hit effect
                    this.createExplosion(bullet.position.clone(), 0.3, 0xff8844);
                    
                    if (enemy.health <= 0) {
                        this.destroyEnemy(enemy);
                    }
                }
            }
        }
        
        // Check enemy bullet collisions with player
        for (const bullet of this.enemyBullets) {
            const dist = bullet.position.distanceTo(p.position);
            if (dist < 10) {
                this.playerHealth -= 15;
                bullet.hit = true;
                this.createExplosion(bullet.position.clone(), 0.2, 0xff4444);
                
                if (this.playerHealth <= 0) {
                    return 'crashed'; // Player destroyed
                }
            }
        }
        
        // Clean up hit bullets
        this.bullets = this.bullets.filter(b => !b.hit && b.alive);
        this.enemyBullets = this.enemyBullets.filter(b => !b.hit && b.alive);
        
        // Respawn enemies if all destroyed
        const aliveEnemies = this.enemies.filter(e => e.alive).length;
        if (aliveEnemies === 0) {
            for (let i = 0; i < this.maxEnemies; i++) {
                setTimeout(() => this.spawnEnemy(), i * 1000);
            }
        }
        
        // Check win condition (score based)
        if (this.score >= 500) {
            return 'complete';
        }
        
        return null;
    }
    
    updateEnemyAI(enemy, dt) {
        const ep = enemy.physics;
        const pp = this.physics.position;
        
        // Calculate distance and direction to player
        const dx = pp.x - ep.position.x;
        const dz = pp.z - ep.position.z;
        const dy = pp.y - ep.position.y;
        const distToPlayer = Math.sqrt(dx * dx + dz * dz);
        
        // State machine
        if (distToPlayer < 800) {
            enemy.state = 'engage';
        } else if (distToPlayer > 2500) {
            enemy.state = 'patrol';
        } else {
            enemy.state = Math.random() < 0.7 ? 'engage' : 'patrol';
        }
        
        // Calculate desired heading toward player
        const angleToPlayer = Math.atan2(dx, dz);
        
        if (enemy.state === 'engage') {
            // Try to get behind the player
            enemy.targetAngle = angleToPlayer + 0.5; // Offset for tail chase
            
            // Adjust altitude to match player
            if (dy > 100) ep.elevatorDeflection = -0.3;
            else if (dy < -100) ep.elevatorDeflection = 0.3;
            else ep.elevatorDeflection *= 0.9; // Dampen
            
            // Bank toward player
            const bankAngle = Math.atan2(dx, dz) - angleToPlayer;
            ep.aileronDeflection = THREE.MathUtils.clamp(bankAngle * 2, -1, 1);
            
        } else {
            // Patrol: fly in circles or random patterns
            enemy.targetAngle += dt * 0.3;
            ep.elevatorDeflection = Math.sin(Date.now() * 0.001) * 0.2;
            ep.aileronDeflection = Math.cos(Date.now() * 0.0015) * 0.3;
        }
        
        // Smoothly turn toward target angle
        const headingDiff = enemy.targetAngle - (Math.atan2(
            new THREE.Vector3(1, 0, 0).applyQuaternion(ep.orientationQuat).x,
            new THREE.Vector3(1, 0, 0).applyQuaternion(ep.orientationQuat).z
        ));
        
        // Normalize angle difference
        let normalizedDiff = headingDiff;
        while (normalizedDiff > Math.PI) normalizedDiff -= Math.PI * 2;
        while (normalizedDiff < -Math.PI) normalizedDiff += Math.PI * 2;
        
        ep.rudderDeflection = THREE.MathUtils.clamp(normalizedDiff, -1, 1);
        
        // Maintain throttle
        if (enemy.state === 'engage') {
            ep.throttleTarget = 0.7 + Math.random() * 0.3;
        } else {
            ep.throttleTarget = 0.4;
        }
    }
    
    fireBullet(physics) {
        const forward = physics.getForward();
        const bulletGeo = new THREE.SphereGeometry(0.5, 4, 4);
        const bulletMat = new THREE.MeshPhongMaterial({ 
            color: 0xffff00, emissive: 0xffaa00, emissiveIntensity: 1 
        });
        const bulletMesh = new THREE.Mesh(bulletGeo, bulletMat);
        
        const startPos = physics.position.clone().add(forward.multiplyScalar(5));
        bulletMesh.position.copy(startPos);
        this.scene.add(bulletMesh);
        
        this.bullets.push({
            mesh: bulletMesh,
            position: startPos.clone(),
            velocity: forward.clone().multiplyScalar(400), // Bullet speed m/s
            alive: true,
            hit: false,
            life: 3.0
        });
    }
    
    enemyFireBullet(enemy) {
        const ep = enemy.physics;
        const forward = ep.getForward();
        
        const bulletGeo = new THREE.SphereGeometry(0.4, 4, 4);
        const bulletMat = new THREE.MeshPhongMaterial({ 
            color: 0xff4444, emissive: 0xff2222, emissiveIntensity: 1 
        });
        const bulletMesh = new THREE.Mesh(bulletGeo, bulletMat);
        
        const startPos = ep.position.clone().add(forward.multiplyScalar(5));
        bulletMesh.position.copy(startPos);
        this.scene.add(bulletMesh);
        
        // Add some inaccuracy
        const dir = forward.clone();
        dir.x += (Math.random() - 0.5) * 0.05;
        dir.y += (Math.random() - 0.5) * 0.03;
        dir.z += (Math.random() - 0.5) * 0.05;
        dir.normalize();
        
        this.enemyBullets.push({
            mesh: bulletMesh,
            position: startPos.clone(),
            velocity: dir.multiplyScalar(350),
            alive: true,
            hit: false,
            life: 2.5
        });
    }
    
    updateBullets(bulletArray, dt) {
        for (const bullet of bulletArray) {
            if (!bullet.alive || bullet.hit) continue;
            
            bullet.life -= dt;
            if (bullet.life <= 0) {
                bullet.alive = false;
                this.scene.remove(bullet.mesh);
                continue;
            }
            
            // Gravity effect on bullets (slight drop)
            bullet.velocity.y -= GRAVITY * 0.1 * dt;
            
            const displacement = bullet.velocity.clone().multiplyScalar(dt);
            bullet.position.add(displacement);
            bullet.mesh.position.copy(bullet.position);
            
            // Remove if too far or below ground
            if (bullet.position.y < -10 || 
                Math.abs(bullet.position.x) > 10000 || 
                Math.abs(bullet.position.z) > 10000) {
                bullet.alive = false;
                this.scene.remove(bullet.mesh);
            }
        }
    }
    
    createExplosion(position, maxLife, color) {
        const geo = new THREE.SphereGeometry(1, 8, 6);
        const mat = new THREE.MeshPhongMaterial({ 
            color: color, emissive: color, emissiveIntensity: 2,
            transparent: true, opacity: 1 
        });
        const mesh = new THREE.Mesh(geo, mat);
        mesh.position.copy(position);
        this.scene.add(mesh);
        
        this.explosions.push({ mesh, life: maxLife, maxLife });
    }
    
    destroyEnemy(enemy) {
        enemy.alive = false;
        this.createExplosion(enemy.physics.position.clone(), 1.5, 0xff6622);
        this.scene.remove(enemy.model);
        
        // Remove associated bullets
        this.enemyBullets = this.enemyBullets.filter(b => b.mesh !== null);
        
        this.score += 100;
    }
    
    cleanup() {
        for (const enemy of this.enemies) {
            if (enemy.model.parent) this.scene.remove(enemy.model);
        }
        for (const bullet of [...this.bullets, ...this.enemyBullets]) {
            if (bullet.mesh && bullet.mesh.parent) this.scene.remove(bullet.mesh);
        }
        for (const exp of this.explosions) {
            if (exp.mesh && exp.mesh.parent) this.scene.remove(exp.mesh);
        }
    }
}

// ── let: horizonCtx ──
// HUD Canvas renderer for artificial horizon
let horizonCtx = null;

// ── function: initHUD ──
function initHUD() {
    const canvas = document.getElementById('horizon-canvas');
    if (canvas) {
        horizonCtx = canvas.getContext('2d');
    }
}

// ── function: drawArtificialHorizon ──
function drawArtificialHorizon(physics) {
    if (!horizonCtx) return;
    
    const ctx = horizonCtx;
    const w = 400, h = 300;
    ctx.clearRect(0, 0, w, h);
    
    // Get pitch and roll from orientation
    const euler = new THREE.Euler().setFromQuaternion(physics.orientationQuat);
    const pitchDeg = euler.x * RAD2DEG;
    const rollDeg = euler.z * RAD2DEG;
    
    ctx.save();
    ctx.translate(w / 2, h / 2);
    ctx.rotate(-rollDeg * DEG2RAD); // Roll rotation
    
    // Sky (top half) - blue gradient
    const skyGrad = ctx.createLinearGradient(0, -h, 0, 0);
    skyGrad.addColorStop(0, '#1a3a6e');
    skyGrad.addColorStop(1, '#4a8abf');
    ctx.fillStyle = skyGrad;
    ctx.fillRect(-w, -h, w * 2, h);
    
    // Ground (bottom half) - brown gradient
    const groundGrad = ctx.createLinearGradient(0, 0, 0, h);
    groundGrad.addColorStop(0, '#5a7a4a');
    groundGrad.addColorStop(1, '#3a5a2a');
    ctx.fillStyle = groundGrad;
    ctx.fillRect(-w, 0, w * 2, h);
    
    // Horizon line
    const horizonOffset = pitchDeg * 5; // Scale: 5 pixels per degree
    
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(-w, -horizonOffset);
    ctx.lineTo(w, -horizonOffset);
    ctx.stroke();
    
    // Pitch ladder lines
    ctx.font = '10px monospace';
    ctx.textAlign = 'center';
    for (let deg = -30; deg <= 30; deg += 5) {
        if (deg === 0) continue;
        
        const y = -horizonOffset - deg * 5;
        if (y < -h || y > h) continue;
        
        const lineWidth = Math.abs(deg) % 10 === 0 ? 60 : 30;
        ctx.strokeStyle = '#ffffff';
        ctx.lineWidth = Math.abs(deg) % 10 === 0 ? 2 : 1;
        
        // Dashed line for above horizon, solid for below
        if (deg > 0) {
            ctx.setLineDash([4, 4]);
        } else {
            ctx.setLineDash([]);
        }
        
        ctx.beginPath();
        ctx.moveTo(-lineWidth, y);
        ctx.lineTo(lineWidth, y);
        ctx.stroke();
        
        // Wing marks at ±5 degrees
        if (Math.abs(deg) === 5) {
            ctx.fillStyle = '#ffffff';
            ctx.fillRect(-lineWidth - 10, y - 2, 10, 4);
            ctx.fillRect(lineWidth, y - 2, 10, 4);
        }
        
        // Degree labels
        if (Math.abs(deg) % 10 === 0 && Math.abs(deg) > 0) {
            ctx.fillStyle = '#ffffff';
            ctx.fillText(Math.abs(deg).toString(), -lineWidth - 15, y + 4);
            ctx.fillText(Math.abs(deg).toString(), lineWidth + 15, y + 4);
        }
    }
    ctx.setLineDash([]);
    
    ctx.restore();
    
    // Aircraft symbol (fixed in center)
    ctx.save();
    ctx.translate(w / 2, h / 2);
    
    // Left wing reference
    ctx.strokeStyle = '#ffcc00';
    ctx.lineWidth = 3;
    ctx.beginPath();
    ctx.moveTo(-50, 10);
    ctx.lineTo(-20, 10);
    ctx.lineTo(-15, 18);
    ctx.stroke();
    
    // Right wing reference
    ctx.beginPath();
    ctx.moveTo(50, 10);
    ctx.lineTo(20, 10);
    ctx.lineTo(15, 18);
    ctx.stroke();
    
    // Center dot
    ctx.fillStyle = '#ffcc00';
    ctx.beginPath();
    ctx.arc(0, 10, 4, 0, Math.PI * 2);
    ctx.fill();
    
    // Roll indicator arc at top
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.arc(0, -h / 2 + 30, 40, Math.PI * 0.15, Math.PI * 0.85);
    ctx.stroke();
    
    // Roll pointer
    const rollRad = -rollDeg * DEG2RAD;
    const ptrX = Math.sin(rollRad) * 40;
    const ptrY = -h / 2 + 30 + (1 - Math.cos(rollRad)) * 40;
    ctx.fillStyle = '#ffcc00';
    ctx.beginPath();
    ctx.moveTo(ptrX, ptrY);
    ctx.lineTo(ptrX - 5, ptrY - 8);
    ctx.lineTo(ptrX + 5, ptrY - 8);
    ctx.closePath();
    ctx.fill();
    
    ctx.restore();
}

// ── function: updateTapes ──
// Update speed tape values dynamically
function updateTapes(physics) {
    const speedKts = Math.round(physics.airSpeed * 1.94384);
    const altFt = Math.max(0, Math.round(physics.position.y * 3.28084));
    
    // Speed tape
    const speedTape = document.getElementById('speed-tape-values');
    let speedHTML = '';
    for (let i = -5; i <= 5; i++) {
        const val = Math.max(0, speedKts + i * 20);
        const isCurrent = i === 0;
        speedHTML += `<div class="tape-val" style="${isCurrent ? 'color:#fff;font-weight:bold;' : ''}">${val}</div>`;
    }
    speedTape.innerHTML = speedHTML;
    
    // Altitude tape
    const altTape = document.getElementById('alt-tape-values');
    let altHTML = '';
    for (let i = -5; i <= 5; i++) {
        const val = Math.max(0, altFt + i * 200);
        const isCurrent = i === 0;
        altHTML += `<div class="tape-val" style="${isCurrent ? 'color:#fff;font-weight:bold;' : ''}">${val}</div>`;
    }
    altTape.innerHTML = altHTML;
}

// ── class: CameraManager ──
// Camera manager with multiple modes
class CameraManager {
    constructor(camera) {
        this.camera = camera;
        this.mode = 0; // 0=chase, 1=cockpit, 2=top-down
        this.smoothPos = new THREE.Vector3();
        this.smoothTarget = new THREE.Vector3();
        this.initialized = false;
    }
    
    update(physics) {
        if (!this.initialized) {
            this.smoothPos.copy(this.camera.position);
            this.smoothTarget.copy(physics.position);
            this.initialized = true;
        }
        
        switch (this.mode) {
            case 0: this.updateChaseCamera(physics); break;
            case 1: this.updateCockpitCamera(physics); break;
            case 2: this.updateTopDownCamera(physics); break;
        }
    }
    
    updateChaseCamera(physics) {
        const forward = physics.getForward();
        const up = physics.getUp();
        
        // Desired camera position: behind and above aircraft
        const desiredPos = physics.position.clone()
            .add(forward.multiplyScalar(-25))
            .add(up.multiplyScalar(8));
        
        // Smooth interpolation
        this.smoothPos.lerp(desiredPos, 0.08);
        
        // Look slightly ahead of aircraft
        const lookTarget = physics.position.clone()
            .add(forward.multiplyScalar(30));
        this.smoothTarget.lerp(lookTarget, 0.1);
        
        this.camera.position.copy(this.smoothPos);
        this.camera.lookAt(this.smoothTarget);
    }
    
    updateCockpitCamera(physics) {
        const forward = physics.getForward();
        const up = physics.getUp();
        
        // Camera at cockpit position, looking forward
        const camPos = physics.position.clone()
            .add(forward.multiplyScalar(2))
            .add(up.multiplyScalar(0.5));
        
        this.smoothPos.lerp(camPos, 0.15);
        
        const lookTarget = physics.position.clone()
            .add(forward.multiplyScalar(100));
        this.smoothTarget.lerp(lookTarget, 0.15);
        
        this.camera.position.copy(this.smoothPos);
        this.camera.lookAt(this.smoothTarget);
    }
    
    updateTopDownCamera(physics) {
        const desiredPos = new THREE.Vector3(
            physics.position.x,
            physics.position.y + 200,
            physics.position.z + 50
        );
        
        this.smoothPos.lerp(desiredPos, 0.05);
        this.smoothTarget.lerp(physics.position, 0.1);
        
        this.camera.position.copy(this.smoothPos);
        this.camera.lookAt(this.smoothTarget);
    }
    
    toggleMode() {
        this.mode = (this.mode + 1) % 3;
    }
}

// ── class: ParticleSystem ──
// Particle system for engine exhaust and effects
class ParticleSystem {
    constructor(scene, maxParticles) {
        this.scene = scene;
        this.maxParticles = maxParticles || 200;
        
        const geometry = new THREE.BufferGeometry();
        const positions = new Float32Array(maxParticles * 3);
        const colors = new Float32Array(maxParticles * 4); // RGBA
        
        geometry.setAttribute('position', new THREE.BufferAttribute(positions, 3));
        geometry.setAttribute('color', new THREE.BufferAttribute(colors, 4));
        
        this.material = new THREE.PointsMaterial({
            size: 2,
            vertexColors: true,
            transparent: true,
            opacity: 0.6,
            blending: THREE.AdditiveBlending,
            depthWrite: false
        });
        
        this.points = new THREE.Points(geometry, this.material);
        scene.add(this.points);
        
        this.particles = [];
    }
    
    emit(position, velocity, color, life) {
        if (this.particles.length >= this.maxParticles) return;
        
        this.particles.push({
            position: position.clone(),
            velocity: velocity.clone(),
            color: new THREE.Color(color),
            life: life || 1.0,
            maxLife: life || 1.0
        });
    }
    
    update(dt) {
        const positions = this.points.geometry.attributes.position.array;
        const colors = this.points.geometry.attributes.color.array;
        
        for (let i = this.particles.length - 1; i >= 0; i--) {
            const p = this.particles[i];
            p.life -= dt;
            
            if (p.life <= 0) {
                this.particles.splice(i, 1);
                continue;
            }
        }
        
        // Update buffer attributes
        for (let i = 0; i < this.maxParticles; i++) {
            const idx3 = i * 3;
            const idx4 = i * 4;
            
            if (i < this.particles.length) {
                const p = this.particles[i];
                
                // Update position
                p.position.add(p.velocity.clone().multiplyScalar(dt));
                p.velocity.multiplyScalar(0.98); // Drag
                
                positions[idx3] = p.position.x;
                positions[idx3 + 1] = p.position.y;
                positions[idx3 + 2] = p.position.z;
                
                const alpha = p.life / p.maxLife;
                colors[idx4] = p.color.r;
                colors[idx4 + 1] = p.color.g;
                colors[idx4 + 2] = p.color.b;
                colors[idx4 + 3] = alpha * 0.6;
            } else {
                positions[idx3] = 0;
                positions[idx3 + 1] = -9999; // Hide below terrain
                positions[idx3 + 2] = 0;
                colors[idx4 + 3] = 0;
            }
        }
        
        this.points.geometry.attributes.position.needsUpdate = true;
        this.points.geometry.attributes.color.needsUpdate = true;
    }
}

// ── function: createClouds ──
// Cloud system for atmosphere
function createClouds(scene, count) {
    const clouds = [];
    
    for (let i = 0; i < count; i++) {
        const group = new THREE.Group();
        
        // Each cloud is made of several overlapping spheres
        const numPuffs = 3 + Math.floor(Math.random() * 4);
        for (let j = 0; j < numPuffs; j++) {
            const size = 20 + Math.random() * 40;
            const geo = new THREE.SphereGeometry(size, 8, 6);
            const mat = new THREE.MeshPhongMaterial({
                color: 0xffffff,
                transparent: true,
                opacity: 0.7 + Math.random() * 0.2,
                flatShading: true
            });
            const puff = new THREE.Mesh(geo, mat);
            puff.position.set(
                (Math.random() - 0.5) * size * 2,
                (Math.random() - 0.5) * size * 0.3,
                (Math.random() - 0.5) * size * 1.5
            );
            group.add(puff);
        }
        
        // Position cloud in the sky
        group.position.set(
            (Math.random() - 0.5) * 20000,
            800 + Math.random() * 2000,
            (Math.random() - 0.5) * 20000
        );
        
        scene.add(group);
        clouds.push({ group, speed: 5 + Math.random() * 15 });
    }
    
    return clouds;
}

// ── function: updateClouds ──
function updateClouds(clouds, dt) {
    for (const cloud of clouds) {
        cloud.group.position.x += cloud.speed * dt;
        
        // Wrap around
        if (cloud.group.position.x > 10000) {
            cloud.group.position.x = -10000;
        }
    }
}

// ── let: renderer ──
let renderer = null;

// ── let: scene ──
let scene = null;

// ── let: camera ──
let camera = null;

// ── let: clock ──
let clock = null;

// ── let: aircraftModel ──
let aircraftModel = null;

// ── let: physics ──
let physics = null;

// ── let: mission ──
let mission = null;

// ── let: cameraManager ──
let cameraManager = null;

// ── let: particles ──
let particles = null;

// ── let: clouds ──
let clouds = [];

// ── let: terrainChunks ──
let terrainChunks = [];

// ── let: waterMesh ──
let waterMesh = null;

// ── let: waterMaterial ──
let waterMaterial = null;

// ── let: keysDown ──
let keysDown = {};

// ── let: lastTime ──
let lastTime = 0;

// ── function: initScene ──
function initScene() {
    renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
    renderer.setSize(window.innerWidth, window.innerHeight);
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.shadowMap.enabled = true;
    renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    renderer.toneMapping = THREE.ACESFilmicToneMapping;
    renderer.toneMappingExposure = 1.0;
    document.getElementById('canvas-container').appendChild(renderer.domElement);

    scene = new THREE.Scene();
    scene.background = new THREE.Color(0x87CEEB);
    scene.fog = new THREE.FogExp2(0x87CEEB, 0.00015);

    camera = new THREE.PerspectiveCamera(60, window.innerWidth / window.innerHeight, 1, 50000);
    camera.position.set(0, 100, -200);

    clock = new THREE.Clock();
    setupLighting();
    createSkyDome();
}

// ── function: setupLighting ──
function setupLighting() {
    const ambient = new THREE.AmbientLight(0x405070, 0.6);
    scene.add(ambient);

    const hemi = new THREE.HemisphereLight(0x87CEEB, 0x3a5a2a, 0.4);
    scene.add(hemi);

    const sun = new THREE.DirectionalLight(0xfff5e6, 1.2);
    sun.position.set(5000, 8000, 3000);
    sun.castShadow = true;
    sun.shadow.mapSize.width = 2048;
    sun.shadow.mapSize.height = 2048;
    sun.shadow.camera.near = 100;
    sun.shadow.camera.far = 20000;
    sun.shadow.camera.left = -5000;
    sun.shadow.camera.right = 5000;
    sun.shadow.camera.top = 5000;
    sun.shadow.camera.bottom = -5000;
    scene.add(sun);

    const fill = new THREE.DirectionalLight(0x8899bb, 0.3);
    fill.position.set(-3000, 2000, -2000);
    scene.add(fill);
}

// ── function: createSkyDome ──
function createSkyDome() {
    const skyGeo = new THREE.SphereGeometry(25000, 16, 12);
    const skyMat = new THREE.ShaderMaterial({
        uniforms: {
            topColor: { value: new THREE.Color(0x0044aa) },
            bottomColor: { value: new THREE.Color(0xaaddff) },
            offset: { value: 20 },
            exponent: { value: 0.6 }
        },
        vertexShader: `
            varying vec3 vWorldPosition;
            void main() {
                vec4 worldPosition = modelMatrix * vec4(position, 1.0);
                vWorldPosition = worldPosition.xyz;
                gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
            }
        `,
        fragmentShader: `
            uniform vec3 topColor;
            uniform vec3 bottomColor;
            uniform float offset;
            uniform float exponent;
            varying vec3 vWorldPosition;
            void main() {
                float h = normalize(vWorldPosition + offset).y;
                gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
            }
        `,
        side: THREE.BackSide
    });

    const sky = new THREE.Mesh(skyGeo, skyMat);
    scene.add(sky);
}

// ── function: buildTerrainChunks ──
function buildTerrainChunks(centerX, centerZ) {
    for (let i = terrainChunks.length - 1; i >= 0; i--) {
        const chunk = terrainChunks[i];
        const dx = chunk.cx - centerX;
        const dz = chunk.cz - centerZ;
        if (Math.sqrt(dx * dx + dz * dz) > 8000) {
            scene.remove(chunk.mesh);
            terrainChunks.splice(i, 1);
        }
    }

    const chunkSize = 4000;
    const resolution = 64;
    const noise = new NoiseGenerator(42);

    for (let x = -1; x <= 1; x++) {
        for (let z = -1; z <= 1; z++) {
            const cx = centerX + x * chunkSize;
            const cz = centerZ + z * chunkSize;

            let found = false;
            for (const chunk of terrainChunks) {
                if (Math.abs(chunk.cx - cx) < 1 && Math.abs(chunk.cz - cz) < 1) {
                    found = true;
                    break;
                }
            }

            if (!found) {
                const terrain = generateTerrain(noise, chunkSize, resolution);
                terrain.position.set(cx, 0, cz);
                scene.add(terrain);
                terrainChunks.push({ mesh: terrain, cx, cz });
            }
        }
    }
}

// ── function: initMenu ──
function initMenu() {
    const aircraftCards = document.getElementById('aircraft-cards');
    aircraftCards.innerHTML = '';

    for (const [key, profile] of Object.entries(AIRCRAFT_PROFILES)) {
        const card = document.createElement('div');
        card.className = 'ac-card';
        card.dataset.aircraft = key;
        card.innerHTML = `
            <div class="ac-icon">${profile.icon}</div>
            <div class="ac-name">${profile.name}</div>
            <div class="ac-stats">
                Speed: ${Math.round(profile.maxSpeed * 1.94384)} kts<br>
                Thrust: ${(profile.maxThrust / 1000).toFixed(0)} kN<br>
                Weight: ${(profile.mass / 1000).toFixed(1)} t<br>
                Roll Rate: ${Math.round(profile.rollRate * RAD2DEG)}°/s<br>
                ${profile.description}
            </div>
        `;

        card.addEventListener('click', () => {
            document.querySelectorAll('.ac-card').forEach(c => c.classList.remove('selected'));
            card.classList.add('selected');
            selectedAircraft = key;
            checkStartReady();
        });

        aircraftCards.appendChild(card);
    }

    const scenarioCards = document.getElementById('scenario-cards');
    scenarioCards.innerHTML = '';

    for (const [key, sc] of Object.entries(SCENARIOS)) {
        const card = document.createElement('div');
        card.className = 'sc-card';
        card.dataset.scenario = key;
        card.innerHTML = `
            <div class="ac-icon">${sc.icon}</div>
            <div class="sc-name">${sc.name}</div>
            <div class="sc-desc">${sc.description}</div>
        `;

        card.addEventListener('click', () => {
            document.querySelectorAll('.sc-card').forEach(c => c.classList.remove('selected'));
            card.classList.add('selected');
            selectedScenario = key;
            checkStartReady();
        });

        scenarioCards.appendChild(card);
    }

    document.getElementById('start-btn').addEventListener('click', startGame);
    document.getElementById('restart-btn').addEventListener('click', () => {
        document.getElementById('game-over').style.display = 'none';
        startGame();
    });
    document.getElementById('menu-btn').addEventListener('click', returnToMenu);
    document.getElementById('restart-crash-btn').addEventListener('click', () => {
        document.getElementById('crash-overlay').style.display = 'none';
        startGame();
    });
}

// ── function: checkStartReady ──
function checkStartReady() {
    const btn = document.getElementById('start-btn');
    btn.disabled = !(selectedAircraft && selectedScenario);
}

// ── function: startGame ──
function startGame() {
    if (!selectedAircraft || !selectedScenario) return;

    document.getElementById('menu-screen').style.display = 'none';
    document.getElementById('hud').style.display = 'block';
    document.getElementById('game-over').style.display = 'none';
    document.getElementById('crash-overlay').style.display = 'none';

    cleanupGameObjects();

    const profile = AIRCRAFT_PROFILES[selectedAircraft];
    physics = new FlightPhysics(profile);
    aircraftModel = buildAircraftModel(profile);
    scene.add(aircraftModel);

    cameraManager = new CameraManager(camera);
    particles = new ParticleSystem(scene, 300);
    clouds = createClouds(scene, 25);

    if (waterMesh) scene.remove(waterMesh);
    const water = generateWater(10000);
    waterMesh = water.mesh;
    waterMaterial = water.material;
    scene.add(waterMesh);

    if (selectedScenario === 'takeoffLanding') {
        mission = new TakeoffLandingMission(scene, physics);
        mission.init();
        document.getElementById('crosshair').style.display = 'none';
        document.getElementById('hud-score').style.display = 'flex';
    } else if (selectedScenario === 'dogfight') {
        mission = new DogfightMission(scene, physics);
        mission.init();
        document.getElementById('crosshair').style.display = 'block';
        document.getElementById('hud-score').style.display = 'flex';
    }

    buildTerrainChunks(0, 0);
    gameState = 'playing';
    lastTime = performance.now();
}

// ── function: cleanupGameObjects ──
function cleanupGameObjects() {
    if (aircraftModel && aircraftModel.parent) scene.remove(aircraftModel);
    if (mission) mission.cleanup();
    if (particles && particles.points.parent) scene.remove(particles.points);

    for (const cloud of (clouds || [])) {
        if (cloud.group.parent) scene.remove(cloud.group);
    }

    for (const chunk of terrainChunks) {
        if (chunk.mesh.parent) scene.remove(chunk.mesh);
    }
    terrainChunks = [];
}

// ── function: returnToMenu ──
function returnToMenu() {
    gameState = 'menu';
    cleanupGameObjects();

    document.getElementById('menu-screen').style.display = 'flex';
    document.getElementById('hud').style.display = 'none';
    document.getElementById('game-over').style.display = 'none';
    document.getElementById('crash-overlay').style.display = 'none';
}

// ── function: handleInput ──
function handleInput(dt) {
    const p = physics;
    if (!p || gameState !== 'playing') return;

    const pitchInput = (INPUT.pitchDown ? 1 : 0) - (INPUT.pitchUp ? 1 : 0);
    p.elevatorDeflection = THREE.MathUtils.lerp(p.elevatorDeflection, pitchInput * 0.8, dt * 5);

    const rollInput = (INPUT.rollLeft ? 1 : 0) - (INPUT.rollRight ? 1 : 0);
    p.aileronDeflection = THREE.MathUtils.lerp(p.aileronDeflection, rollInput * 0.8, dt * 5);

    const yawInput = (INPUT.yawLeft ? 1 : 0) - (INPUT.yawRight ? 1 : 0);
    p.rudderDeflection = THREE.MathUtils.lerp(p.rudderDeflection, yawInput * 0.8, dt * 5);

    if (INPUT.throttleUp) {
        p.throttleTarget = Math.min(1, p.throttleTarget + dt * 0.8);
    }
    if (INPUT.throttleDown) {
        p.throttleTarget = Math.max(0, p.throttleTarget - dt * 0.8);
    }

    p.brakeForce = INPUT.brakes ? 50000 : 0;

    if (INPUT.gearToggle) {
        p.gearDown = !p.gearDown;
        INPUT.gearToggle = false;
    }

    if (INPUT.flaps) {
        p.flapAngle = p.flapAngle > 0.1 ? 0 : 0.35;
        INPUT.flaps = false;
    }
}

// ── function: emitExhaust ──
function emitExhaust() {
    if (!physics || !particles) return;

    const thrust = physics.currentThrust;
    if (thrust > 1000) {
        const backward = physics.getForward().multiplyScalar(-1);
        const pos = physics.position.clone().add(backward.multiplyScalar(4));

        const spread = thrust / physics.profile.maxThrust;
        const vel = new THREE.Vector3(
            -50 * spread + (Math.random() - 0.5) * 10,
            (Math.random() - 0.5) * 5,
            (Math.random() - 0.5) * 10
        );

        const r = Math.min(1, spread);
        const g = Math.max(0.3, 1 - spread * 0.7);
        const b = Math.max(0.2, 1 - spread);

        particles.emit(pos, vel, new THREE.Color(r, g, b), 0.5 + Math.random() * 0.5);
    }
}

// ── function: checkCrashConditions ──
function checkCrashConditions() {
    if (!physics) return null;
    const p = physics;

    if (p.position.y < 1 && p.airSpeed > 50) {
        const euler = new THREE.Euler().setFromQuaternion(p.orientationQuat);
        if (Math.abs(euler.x) > 0.4 || Math.abs(euler.z) > 0.3) {
            return 'Crashed! Too steep angle on touchdown';
        }
    }

    if (p.position.y < 1 && p.velocity.y < -15) {
        return 'Crashed! Impact too hard';
    }

    if (Math.abs(p.gLoad) > 9) {
        return 'Structural failure! Exceeded G limits';
    }

    return null;
}

// ── function: showGameOver ──
function showGameOver(completed) {
    gameState = 'gameover';
    const overlay = document.getElementById('game-over');
    overlay.style.display = 'flex';

    document.getElementById('go-title').textContent = completed ? 'MISSION COMPLETE!' : 'MISSION FAILED';
    document.getElementById('go-title').style.color = completed ? '#00ff88' : '#ff4444';

    let message = '';
    if (selectedScenario === 'takeoffLanding') {
        message = `Score: ${mission.score} points\n`;
        message += `Touchdown Speed: ${Math.round(mission.touchdownSpeed)} kts\n`;
        message += `Vertical Speed: ${mission.touchdownFlareRate.toFixed(1)} m/s`;
    } else if (selectedScenario === 'dogfight') {
        message = `Score: ${mission.score} points\n`;
        message += `Enemies destroyed! Great flying, pilot.`;
    }

    document.getElementById('go-message').textContent = message;
}

// ── function: showCrash ──
function showCrash(reason) {
    gameState = 'crashed';
    const overlay = document.getElementById('crash-overlay');
    overlay.style.display = 'flex';
    document.getElementById('crash-reason').textContent = reason;
}

// ── function: onWindowResize ──
function onWindowResize() {
    camera.aspect = window.innerWidth / window.innerHeight;
    camera.updateProjectionMatrix();
    renderer.setSize(window.innerWidth, window.innerHeight);
}

// ── function: gameLoop ──
function gameLoop() {
    requestAnimationFrame(gameLoop);

    const now = performance.now();
    let dt = (now - lastTime) / 1000;
    lastTime = now;
    dt = Math.min(dt, 0.05);

    if (gameState === 'playing') {
        handleInput(dt);
        physics.update(dt);

        aircraftModel.position.copy(physics.position);
        aircraftModel.quaternion.copy(physics.orientationQuat);

        cameraManager.update(physics);
        emitExhaust();
        particles.update(dt);
        updateClouds(clouds, dt);

        if (waterMesh) {
            const positions = waterMesh.geometry.attributes.position.array;
            for (let i = 1; i < positions.length - 2; i += 3) {
                positions[i] = Math.sin(positions[i-1] * 0.01 + now * 0.001) * 2 +
                              Math.cos(positions[i+1] * 0.015 + now * 0.0015) * 1.5 - 5;
            }
            waterMesh.geometry.attributes.position.needsUpdate = true;
        }

        if (mission) {
            const result = mission.update(dt);
            if (result === 'complete') showGameOver(true);
            else if (result === 'crashed') showCrash('Destroyed in combat!');
        }

        const crashReason = checkCrashConditions();
        if (crashReason) showCrash(crashReason);

        buildTerrainChunks(physics.position.x, physics.position.z);
        updateHUD(physics, mission);
        updateTapes(physics);
    }

    renderer.render(scene, camera);
}

// ── function: updateHUD ──
function updateHUD(physics, mission) {
    // Speed tape current value
    const speedKts = Math.round(physics.airSpeed * 1.94384);
    const el = document.getElementById('speed-current');
    if (el) el.textContent = speedKts;

    // Altitude in feet
    const altFt = Math.max(0, Math.round(physics.position.y * 3.28084));
    const altEl = document.getElementById('alt-current');
    if (altEl) altEl.textContent = altFt;
    const hudAltEl = document.getElementById('hud-alt-value');
    if (hudAltEl) hudAltEl.textContent = altFt;

    // Heading
    const forward = physics.getForward();
    let heading = Math.atan2(forward.x, forward.z) * RAD2DEG;
    if (heading < 0) heading += 360;
    const hdgEl = document.getElementById('hud-hdg-value');
    if (hdgEl) hdgEl.textContent = Math.round(heading);

    // Throttle
    const thrPct = Math.round(physics.throttleCurrent * 100);
    const thrEl = document.getElementById('hud-thr-value');
    if (thrEl) thrEl.textContent = thrPct;
    const thrFill = document.getElementById('throttle-fill');
    if (thrFill) thrFill.style.width = thrPct + '%';

    // G-force
    const gValEl = document.getElementById('hud-g-value');
    if (gValEl) gValEl.textContent = physics.gLoad.toFixed(1);

    // Warnings
    const warningsEl = document.getElementById('hud-warnings');
    let warningText = '';
    if (physics.stallWarning) warningText += '⚠ STALL ';
    if (physics.overGWarning) warningText += '⚠ HIGH G ';
    if (physics.speedWarning) warningText += '⚠ OVERSPEED ';
    if (physics.position.y < 5 && physics.airSpeed > 10) warningText += '🛬 TERRAIN ';

    if (warningText) {
        warningsEl.innerHTML = '<span id="warning-text">' + warningText + '</span>';
    } else {
        warningsEl.innerHTML = '';
    }

    // Artificial horizon on canvas
    drawArtificialHorizon(physics);

    // Mission-specific HUD elements
    if (mission) {
        const scoreEl = document.getElementById('hud-score');
        const scoreVal = document.getElementById('hud-score-value');

        if (selectedScenario === 'dogfight') {
            scoreEl.style.display = 'flex';
            if (scoreVal) scoreVal.textContent = mission.score;

            // Health bar for dogfight
            const healthBar = document.getElementById('hud-health');
            const hpFill = document.getElementById('health-fill');
            const hpValue = document.getElementById('hud-hp-value');
            if (healthBar) healthBar.style.display = 'flex';
            
            const hpPct = Math.max(0, mission.playerHealth);
            if (hpFill) {
                hpFill.style.width = hpPct + '%';
                hpFill.style.background = hpPct > 50 ? '#00ff88' : hpPct > 25 ? '#ffaa00' : '#ff4444';
            }
            if (hpValue) {
                hpValue.textContent = Math.round(hpPct);
                hpValue.style.color = hpPct > 50 ? '#00ff88' : hpPct > 25 ? '#ffaa00' : '#ff4444';
            }

            // Hide G-force display in dogfight mode to save space
            const gforceEl = document.getElementById('hud-gforce');
            if (gforceEl) gforceEl.style.display = 'none';
        } else {
            scoreEl.style.display = 'flex';
            if (scoreVal) scoreVal.textContent = mission.score;

            // Show G-force for takeoff/landing
            const healthBar = document.getElementById('hud-health');
            if (healthBar) healthBar.style.display = 'none';
            const gforceEl = document.getElementById('hud-gforce');
            if (gforceEl) gforceEl.style.display = 'flex';
        }
    }
}

// ── function: buildAircraftModel ──
function buildAircraftModel(profile) {
    const group = new THREE.Group();
    const s = profile.scale;

    const bodyMat = new THREE.MeshPhongMaterial({ color: profile.color, shininess: 80, specular: 0x444444 });
    const accentMat = new THREE.MeshPhongMaterial({ color: profile.accentColor, shininess: 60 });
    const darkMat = new THREE.MeshPhongMaterial({ color: 0x222222, shininess: 40 });
    const glassMat = new THREE.MeshPhongMaterial({ color: 0x88ccff, transparent: true, opacity: 0.5, shininess: 100 });

    if (profile.name === "Falcon X-1") {
        // Fuselage
        const fuselageGeo = new THREE.CylinderGeometry(0.3 * s, 0.25 * s, 6 * s, 8);
        const fuselage = new THREE.Mesh(fuselageGeo, bodyMat);
        fuselage.rotation.z = Math.PI / 2;
        group.add(fuselage);

        // Nose cone
        const noseGeo = new THREE.ConeGeometry(0.3 * s, 1.5 * s, 8);
        const nose = new THREE.Mesh(noseGeo, bodyMat);
        nose.rotation.z = -Math.PI / 2;
        nose.position.x = 3.75 * s;
        group.add(nose);

        // Cockpit canopy (half sphere)
        const cockpitGeo = new THREE.SphereGeometry(0.35 * s, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
        const cockpit = new THREE.Mesh(cockpitGeo, glassMat);
        cockpit.position.set(1.0 * s, 0.25 * s, 0);
        group.add(cockpit);

        // Wings - swept back using extruded shapes
        const wingShape = new THREE.Shape();
        wingShape.moveTo(0, 0);
        wingShape.lineTo(3 * s, -1.5 * s);
        wingShape.lineTo(3 * s, -2.8 * s);
        wingShape.lineTo(-1 * s, -2.5 * s);
        wingShape.lineTo(-1 * s, 0);

        const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.06 * s });
        const rightWing = new THREE.Mesh(wingGeo, bodyMat);
        rightWing.rotation.x = -Math.PI / 2;
        rightWing.position.set(0.5 * s, -0.06 * s, 0.1 * s);
        group.add(rightWing);

        const leftWing = new THREE.Mesh(wingGeo, bodyMat);
        leftWing.rotation.x = Math.PI / 2;
        leftWing.position.set(0.5 * s, -0.06 * s, -0.1 * s);
        group.add(leftWing);

        // Wing tip accents
        const tipGeo = new THREE.BoxGeometry(0.3 * s, 0.08 * s, 0.2 * s);
        [-2.75, 2.75].forEach(z => {
            const tip = new THREE.Mesh(tipGeo, accentMat);
            tip.position.set(3 * s, -0.06 * s, z * s);
            group.add(tip);
        });

        // Vertical stabilizer (tail fin)
        const tailFinShape = new THREE.Shape();
        tailFinShape.moveTo(0, 0);
        tailFinShape.lineTo(-1.5 * s, -0.3 * s);
        tailFinShape.lineTo(-2.5 * s, 0);
        const tailFinGeo = new THREE.ExtrudeGeometry(tailFinShape, { depth: 0.04 * s });
        const tailFin = new THREE.Mesh(tailFinGeo, bodyMat);
        tailFin.position.set(-1.5 * s, 0.25 * s, -0.02 * s);
        group.add(tailFin);

        // Horizontal stabilizers
        const hStabShape = new THREE.Shape();
        hStabShape.moveTo(0, 0);
        hStabShape.lineTo(-1.5 * s, -0.8 * s);
        hStabShape.lineTo(-1.5 * s, -1.4 * s);
        hStabShape.lineTo(-2.5 * s, -1.2 * s);
        hStabShape.lineTo(-2.5 * s, 0);
        const hStabGeo = new THREE.ExtrudeGeometry(hStabShape, { depth: 0.04 * s });

        [-1, 1].forEach(side => {
            const stab = new THREE.Mesh(hStabGeo, bodyMat);
            stab.rotation.x = side * -Math.PI / 2;
            stab.position.set(-1.5 * s, -0.04 * s, side * 0.1 * s);
            group.add(stab);
        });

        // Engine exhaust
        const exhaustGeo = new THREE.CylinderGeometry(0.15 * s, 0.2 * s, 0.3 * s, 8);
        const exhaust = new THREE.Mesh(exhaustGeo, darkMat);
        exhaust.rotation.z = Math.PI / 2;
        exhaust.position.x = -3.15 * s;
        group.add(exhaust);

    } else if (profile.name === "Titan B-2") {
        // Main fuselage
        const bodyGeo = new THREE.BoxGeometry(8 * s, 0.9 * s, 1.5 * s);
        const body = new THREE.Mesh(bodyGeo, bodyMat);
        group.add(body);

        // Nose cone (4-sided)
        const noseGeo2 = new THREE.ConeGeometry(0.6 * s, 2 * s, 4);
        const nose2 = new THREE.Mesh(noseGeo2, bodyMat);
        nose2.rotation.z = -Math.PI / 2;
        nose2.position.x = 5 * s;
        group.add(nose2);

        // Cockpit bubble
        const cabGeo = new THREE.SphereGeometry(0.4 * s, 8, 6);
        const cabin = new THREE.Mesh(cabGeo, glassMat);
        cabin.position.set(3.5 * s, 0.5 * s, 0);
        group.add(cabin);

        // Large straight wings
        const wingGeo2 = new THREE.BoxGeometry(4 * s, 0.1 * s, 6 * s);
        const wings = new THREE.Mesh(wingGeo2, bodyMat);
        wings.position.set(-0.5 * s, -0.1 * s, 0);
        group.add(wings);

        // Wing tip accents
        const wTipGeo = new THREE.BoxGeometry(0.3 * s, 0.15 * s, 0.4 * s);
        [-3, 3].forEach(z => {
            const wt = new THREE.Mesh(wTipGeo, accentMat);
            wt.position.set(-0.5 * s, -0.1 * s, z * s);
            group.add(wt);
        });

        // Twin tail fins
        [-0.6, 0.6].forEach(z => {
            const fin = new THREE.Mesh(new THREE.BoxGeometry(2 * s, 1.5 * s, 0.08 * s), bodyMat);
            fin.position.set(-3.5 * s, 0.75 * s, z * s);
            group.add(fin);
        });

        // Horizontal stabilizer (FIXED: wrapped in Mesh)
        const hStab2 = new THREE.Mesh(new THREE.BoxGeometry(1.5 * s, 0.08 * s, 4 * s), bodyMat);
        hStab2.position.set(-3.5 * s, -0.1 * s, 0);
        group.add(hStab2);

        // Engine nacelles (under wings)
        [-2, 2].forEach(z => {
            const nacelle = new THREE.Mesh(new THREE.CylinderGeometry(0.3 * s, 0.35 * s, 1.5 * s, 8), darkMat);
            nacelle.rotation.z = Math.PI / 2;
            nacelle.position.set(-0.5 * s, -0.6 * s, z * s);
            group.add(nacelle);
        });

    } else if (profile.name === "Viper S-7") {
        // Pointed nose
        const noseGeo3 = new THREE.ConeGeometry(0.25 * s, 4 * s, 6);
        const nose3 = new THREE.Mesh(noseGeo3, bodyMat);
        nose3.rotation.z = -Math.PI / 2;
        nose3.position.x = 3 * s;
        group.add(nose3);

        // Main fuselage (tapered cylinder)
        const bodyGeo3 = new THREE.CylinderGeometry(0.15 * s, 0.35 * s, 4 * s, 6);
        const body3 = new THREE.Mesh(bodyGeo3, bodyMat);
        body3.rotation.z = Math.PI / 2;
        body3.position.x = -0.5 * s;
        group.add(body3);

        // Cockpit canopy (FIXED: using cylinder + spheres instead of CapsuleGeometry)
        const cabBody = new THREE.Mesh(new THREE.CylinderGeometry(0.18 * s, 0.18 * s, 0.8 * s, 6), glassMat);
        cabBody.rotation.z = Math.PI / 2;
        cabBody.position.set(1.5 * s, 0.3 * s, 0);
        group.add(cabBody);

        const cabFront = new THREE.Mesh(new THREE.SphereGeometry(0.18 * s, 6, 4), glassMat);
        cabFront.position.set(1.9 * s, 0.3 * s, 0);
        group.add(cabFront);

        // Delta wings (large triangular)
        const deltaShape = new THREE.Shape();
        deltaShape.moveTo(0, 0);
        deltaShape.lineTo(2 * s, -1.5 * s);
        deltaShape.lineTo(-1 * s, -3.5 * s);
        deltaShape.lineTo(-2 * s, -3.5 * s);
        deltaShape.lineTo(-2 * s, 0);

        const deltaGeo = new THREE.ExtrudeGeometry(deltaShape, { depth: 0.05 * s });
        [-1, 1].forEach(side => {
            const wing = new THREE.Mesh(deltaGeo, bodyMat);
            wing.rotation.x = side * -Math.PI / 2;
            wing.position.set(1 * s, -0.05 * s, side * 0.03 * s);
            group.add(wing);
        });

        // Wing leading edge accents
        const leGeo = new THREE.BoxGeometry(3 * s, 0.06 * s, 0.1 * s);
        [-2.8, 2.8].forEach(z => {
            const le = new THREE.Mesh(leGeo, accentMat);
            le.position.set(0 * s, -0.05 * s, z * s);
            group.add(le);
        });

        // Small vertical stabilizer at rear
        const vStabShape = new THREE.Shape();
        vStabShape.moveTo(0, 0);
        vStabShape.lineTo(-1 * s, -0.2 * s);
        vStabShape.lineTo(-1.8 * s, 0);
        const vStabGeo = new THREE.ExtrudeGeometry(vStabShape, { depth: 0.03 * s });
        const vStab = new THREE.Mesh(vStabGeo, bodyMat);
        vStab.position.set(-1.5 * s, 0.2 * s, -0.015 * s);
        group.add(vStab);

        // Twin engine exhausts
        [-0.15, 0.15].forEach(z => {
            const ex = new THREE.Mesh(new THREE.CylinderGeometry(0.1 * s, 0.15 * s, 0.4 * s, 6), darkMat);
            ex.rotation.z = Math.PI / 2;
            ex.position.set(-2.7 * s, -0.1 * s, z * s);
            group.add(ex);
        });
    }

    // Landing gear (common to all)
    const gearMat = new THREE.MeshPhongMaterial({ color: 0x444444 });
    [-0.5, 0.5].forEach(z => {
        const strut = new THREE.Mesh(new THREE.CylinderGeometry(0.03 * s, 0.03 * s, 0.6 * s, 4), gearMat);
        strut.position.set(1 * s, -0.5 * s, z * s);
        group.add(strut);

        const wheel = new THREE.Mesh(new THREE.CylinderGeometry(0.12 * s, 0.12 * s, 0.08 * s, 8), darkMat);
        wheel.rotation.x = Math.PI / 2;
        wheel.position.set(1 * s, -0.8 * s, z * s);
        group.add(wheel);
    });

    // Nose gear
    const noseStrut = new THREE.Mesh(new THREE.CylinderGeometry(0.025 * s, 0.025 * s, 0.4 * s, 4), gearMat);
    noseStrut.position.set(3.5 * s, -0.4 * s, 0);
    group.add(noseStrut);

    // Center the model
    const box = new THREE.Box3().setFromObject(group);
    const center = box.getCenter(new THREE.Vector3());
    group.position.sub(center);

    return group;
}

// ── main ──
(function() {
    // Initialize Three.js scene
    initScene();

    // Initialize HUD canvas
    initHUD();

    // Build menu UI
    initMenu();

    // Keyboard input handling
    document.addEventListener('keydown', function(e) {
        keysDown[e.code] = true;

        if (gameState === 'playing') {
            switch (e.code) {
                case 'KeyW': INPUT.pitchUp = true; break;
                case 'KeyS': INPUT.pitchDown = true; break;
                case 'KeyA': INPUT.rollLeft = true; break;
                case 'KeyD': INPUT.rollRight = true; break;
                case 'KeyQ': INPUT.yawLeft = true; break;
                case 'KeyE': INPUT.yawRight = true; break;
                case 'ShiftLeft': case 'ShiftRight': INPUT.throttleUp = true; e.preventDefault(); break;
                case 'ControlLeft': case 'ControlRight': INPUT.throttleDown = true; e.preventDefault(); break;
                case 'KeyB': INPUT.brakes = true; break;
                case 'KeyG': INPUT.gearToggle = true; break;
                case 'KeyF': INPUT.flaps = true; break;
                case 'Space': INPUT.fire = true; e.preventDefault(); break;
                case 'KeyC': if (cameraManager) cameraManager.toggleMode(); break;
                case 'Escape':
                    gameState = 'paused';
                    document.getElementById('pause-overlay').style.display = 'flex';
                    break;
            }
        } else if (gameState === 'paused') {
            if (e.code === 'Escape') {
                gameState = 'playing';
                document.getElementById('pause-overlay').style.display = 'none';
                lastTime = performance.now();
            }
        }
    });

    document.addEventListener('keyup', function(e) {
        keysDown[e.code] = false;

        switch (e.code) {
            case 'KeyW': INPUT.pitchUp = false; break;
            case 'KeyS': INPUT.pitchDown = false; break;
            case 'KeyA': INPUT.rollLeft = false; break;
            case 'KeyD': INPUT.rollRight = false; break;
            case 'KeyQ': INPUT.yawLeft = false; break;
            case 'KeyE': INPUT.yawRight = false; break;
            case 'ShiftLeft': case 'ShiftRight': INPUT.throttleUp = false; break;
            case 'ControlLeft': case 'ControlRight': INPUT.throttleDown = false; break;
            case 'KeyB': INPUT.brakes = false; break;
            case 'Space': INPUT.fire = false; break;
        }
    });

    // Mouse input for firing in dogfight mode
    document.addEventListener('mousedown', function(e) {
        if (gameState === 'playing' && e.button === 0) {
            INPUT.mouseDown = true;
            INPUT.fire = true;
        }
    });

    document.addEventListener('mouseup', function(e) {
        if (e.button === 0) {
            INPUT.mouseDown = false;
            // Only clear fire if not holding spacebar
            if (!keysDown['Space']) INPUT.fire = false;
        }
    });

    // Window resize handling
    window.addEventListener('resize', onWindowResize);

    // Start the game loop
    lastTime = performance.now();
    requestAnimationFrame(gameLoop);
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"composer","sessionId":"2c837dde-2d50-424a-b2c1-729bebf061a2","tokensIn":2083481,"tokensOut":47477,"tokensTotal":2130958,"turns":49,"toolCalls":48,"failedToolCalls":1,"timestamp":"2026-07-17T17:04:24.160Z"} -->