← Flight Simulator results

Flight Simulator

thinkingcap qwen3.6 27b · middle

Initial view of Flight Simulator

Browser check: External resource unavailable · Artifact runtime error. This is a technical check, not a quality review.

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 56,832 bytes · SHA-256 1e455305e476
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apex Aero — Flight Simulator</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; font-family: 'Segoe UI', Arial, sans-serif; cursor: none; }
canvas { display: block; }

/* ── Menu Screen ── */
#menu-screen {
  position: fixed; inset: 0; z-index: 1000;
  background: linear-gradient(135deg, #0a0e27 0%, #1a1a3e 40%, #0d1b2a 100%);
  display: flex; flex-direction: column; align-items: center; justify-content: center;
  transition: opacity 0.6s;
}
#menu-screen.hidden { opacity: 0; pointer-events: none; }
#menu-screen h1 {
  font-size: 4rem; color: #fff; letter-spacing: 0.3em; text-transform: uppercase;
  text-shadow: 0 0 40px rgba(0,200,255,0.6), 0 0 80px rgba(0,100,255,0.3);
  margin-bottom: 0.2em;
}
#menu-screen .subtitle { color: #7ec8e3; font-size: 1rem; letter-spacing: 0.5em; margin-bottom: 3rem; }

.aircraft-select { display: flex; gap: 2rem; margin-bottom: 3rem; }
.ac-card {
  width: 220px; padding: 1.5rem; border-radius: 12px; 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; color: #ccc;
}
.ac-card:hover { border-color: rgba(0,200,255,0.5); background: rgba(0,200,255,0.1); transform: translateY(-4px); }
.ac-card.selected { border-color: #00c8ff; background: rgba(0,200,255,0.15); box-shadow: 0 0 30px rgba(0,200,255,0.3); }
.ac-card .icon { font-size: 3rem; margin-bottom: 0.5rem; }
.ac-card h3 { color: #fff; margin-bottom: 0.5rem; font-size: 1.1rem; }
.ac-card p { font-size: 0.75rem; line-height: 1.4; color: #999; }

.scenario-select { display: flex; gap: 2rem; margin-bottom: 3rem; }
.sc-card {
  width: 260px; padding: 1.5rem; border-radius: 12px; 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; color: #ccc;
}
.sc-card:hover { border-color: rgba(255,165,0,0.5); background: rgba(255,165,0,0.1); transform: translateY(-4px); }
.sc-card.selected { border-color: #ffa500; background: rgba(255,165,0,0.15); box-shadow: 0 0 30px rgba(255,165,0,0.3); }
.sc-card .icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
.sc-card h3 { color: #fff; margin-bottom: 0.5rem; }
.sc-card p { font-size: 0.75rem; line-height: 1.4; color: #999; }

#start-btn {
  padding: 1rem 4rem; font-size: 1.2rem; letter-spacing: 0.3em; text-transform: uppercase;
  background: linear-gradient(135deg, #00c8ff, #0066ff); color: #fff; border: none;
  border-radius: 8px; cursor: pointer; transition: all 0.3s; font-weight: bold;
}
#start-btn:hover { transform: scale(1.05); box-shadow: 0 0 40px rgba(0,200,255,0.5); }

/* ── HUD Overlay ── */
#hud {
  position: fixed; inset: 0; z-index: 100; pointer-events: none; display: none;
}
#hud.active { display: block; }

.hud-top-left {
  position: absolute; top: 20px; left: 20px; color: #0f0; font-family: 'Courier New', monospace;
  font-size: 13px; line-height: 1.6; text-shadow: 0 0 8px rgba(0,255,0,0.5);
}
.hud-top-right {
  position: absolute; top: 20px; right: 20px; color: #0f0; font-family: 'Courier New', monospace;
  font-size: 13px; line-height: 1.6; text-align: right; text-shadow: 0 0 8px rgba(0,255,0,0.5);
}
.hud-bottom {
  position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
  color: #ff0; font-family: 'Courier New', monospace; font-size: 14px; text-align: center;
  text-shadow: 0 0 8px rgba(255,255,0,0.5);
}
.hud-center {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 300px; height: 300px; pointer-events: none;
}

/* Crosshair */
.crosshair-h, .crosshair-v {
  position: absolute; background: rgba(0,255,0,0.7);
}
.crosshair-h { width: 40px; height: 1px; top: 50%; left: 50%; transform: translate(-50%, -50%); }
.crosshair-v { width: 1px; height: 40px; top: 50%; left: 50%; transform: translate(-50%, -50%); }

/* Pitch ladder */
.pitch-ladder { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 200px; height: 200px; }
.pitch-line {
  position: absolute; left: 50%; transform: translateX(-50%); height: 1px;
  background: rgba(0,255,0,0.4); width: 60px;
}

/* Horizon line */
.horizon-line {
  position: absolute; top: 50%; left: 0; right: 0; height: 1px;
  background: rgba(0,255,0,0.3); transform-origin: center;
}

/* Stall warning */
.stall-warning {
  position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%);
  color: #f00; font-size: 2rem; font-weight: bold; letter-spacing: 0.3em;
  text-shadow: 0 0 20px rgba(255,0,0,0.8); display: none; animation: blink 0.3s infinite;
}
@keyframes blink { 0%,100%{opacity:1;} 50%{opacity:0.3;} }

/* G-meter */
.g-meter {
  position: absolute; right: 20px; top: 50%; transform: translateY(-50%);
  width: 30px; height: 200px; border: 1px solid rgba(0,255,0,0.4); border-radius: 4px;
}
.g-meter-fill {
  position: absolute; bottom: 50%; left: 2px; right: 2px; background: linear-gradient(to top, #0f0, #ff0, #f00);
  transition: height 0.1s; border-radius: 2px;
}

/* Minimap */
.minimap {
  position: absolute; bottom: 20px; right: 20px; width: 150px; height: 150px;
  border: 1px solid rgba(0,255,0,0.4); border-radius: 50%; overflow: hidden;
  background: rgba(0,20,0,0.7);
}

/* Damage overlay */
.damage-overlay {
  position: fixed; inset: 0; z-index: 99; pointer-events: none;
  border: 8px solid transparent; transition: border-color 0.3s;
}
.damage-overlay.hit { border-color: rgba(255,0,0,0.6); }

/* Pause overlay */
#pause-overlay {
  position: fixed; inset: 0; z-index: 500; background: rgba(0,0,0,0.7);
  display: none; align-items: center; justify-content: center; flex-direction: column; color: #fff;
}
#pause-overlay.active { display: flex; }
#pause-overlay h2 { font-size: 3rem; margin-bottom: 1rem; letter-spacing: 0.2em; }
#pause-overlay p { font-size: 1rem; color: #aaa; margin-bottom: 0.5rem; }

/* Controls help */
.controls-help {
  position: absolute; bottom: 20px; left: 20px; color: rgba(255,255,255,0.4);
  font-size: 11px; line-height: 1.6; font-family: 'Courier New', monospace;
}

/* Target reticle for dogfight */
.target-reticle {
  position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
  width: 80px; height: 80px; border: 2px solid rgba(255,0,0,0.6); border-radius: 50%;
  display: none;
}
.target-reticle.locked { border-color: #f00; animation: pulse 0.5s infinite; }
@keyframes pulse { 0%,100%{transform:translate(-50%,-50%) scale(1);} 50%{transform:translate(-50%,-50%) scale(1.1);} }

/* Score display */
.score-display {
  position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
  color: #ffa500; font-size: 1.5rem; font-weight: bold; text-shadow: 0 0 10px rgba(255,165,0,0.5);
}

/* Landing score */
.landing-score {
  position: fixed; inset: 0; z-index: 600; display: none; align-items: center; justify-content: center;
  background: rgba(0,0,0,0.8); color: #fff; flex-direction: column;
}
.landing-score.active { display: flex; }
.landing-score h2 { font-size: 3rem; margin-bottom: 1rem; }
.landing-score .grade { font-size: 6rem; color: #0f0; text-shadow: 0 0 40px rgba(0,255,0,0.8); }

/* Loading */
#loading-screen {
  position: fixed; inset: 0; z-index: 2000; background: #0a0e27;
  display: flex; align-items: center; justify-content: center; color: #fff; font-size: 1.5rem;
}
</style>
</head>
<body>

<!-- Loading -->
<div id="loading-screen">Initializing Apex Aero...</div>

<!-- Menu Screen -->
<div id="menu-screen" class="hidden">
  <h1>Apex Aero</h1>
  <div class="subtitle">Flight Simulator</div>

  <div style="color:#aaa;margin-bottom:1rem;font-size:0.9rem;">SELECT AIRCRAFT</div>
  <div class="aircraft-select" id="aircraft-select"></div>

  <div style="color:#aaa;margin-bottom:1rem;font-size:0.9rem;">SELECT SCENARIO</div>
  <div class="scenario-select" id="scenario-select"></div>

  <button id="start-btn">LAUNCH</button>
</div>

<!-- HUD -->
<div id="hud">
  <div class="hud-top-left" id="hud-left"></div>
  <div class="hud-top-right" id="hud-right"></div>
  <div class="hud-bottom" id="hud-bottom"></div>
  <div class="hud-center">
    <div class="crosshair-h"></div>
    <div class="crosshair-v"></div>
    <div class="horizon-line" id="horizon-line"></div>
    <div class="pitch-ladder" id="pitch-ladder"></div>
  </div>
  <div class="stall-warning" id="stall-warning">⚠ STALL ⚠</div>
  <div class="g-meter"><div class="g-meter-fill" id="g-meter-fill"></div></div>
  <div class="minimap" id="minimap"><canvas id="minimap-canvas" width="150" height="150"></canvas></div>
  <div class="target-reticle" id="target-reticle"></div>
  <div class="score-display" id="score-display"></div>
  <div class="controls-help">W/S: Pitch | A/D: Roll | Q/E: Yaw | SHIFT/CTRL: Throttle ↑↓ | SPACE: Brake | P: Pause</div>
</div>

<div class="damage-overlay" id="damage-overlay"></div>

<!-- Pause -->
<div id="pause-overlay">
  <h2>PAUSED</h2>
  <p>Press P to resume</p>
  <p style="margin-top:1rem;color:#666;">ESC for main menu</p>
</div>

<!-- Landing Score -->
<div class="landing-score" id="landing-score">
  <h2>Landing Complete</h2>
  <div class="grade" id="landing-grade"></div>
  <p style="margin-top:1rem;color:#aaa;" id="landing-details"></p>
  <button onclick="location.reload()" style="margin-top:2rem;padding:0.8rem 2rem;background:#0f0;border:none;border-radius:6px;cursor:pointer;font-size:1rem;">Play Again</button>
</div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r152/three.min.js"></script>
<script>
// ═══════════════════════════════════════════════════════
//  APEX AERO — FLIGHT SIMULATOR
// ═══════════════════════════════════════════════════════

// ── Aircraft Definitions ──
const AIRCRAFT = {
  falcon: {
    name: "Falcon X-1", icon: "🦅", desc: "High-performance fighter. Fast, agile, excellent for dogfighting.",
    color: 0x4488ff, accentColor: 0x2266cc,
    mass: 8500, wingArea: 35, maxThrust: 180000, thrustIdle: 30000,
    clMax: 2.2, cd0: 0.02, kInduced: 0.04,
    rollRate: 2.5, pitchRate: 2.0, yawRate: 1.2,
    maxSpeed: 350, stallSpeed: 65, engineResponseTime: 0.8,
    turnRadius: 300, gLimit: 9,
    length: 16, wingspan: 10, height: 4.5
  },
  hawk: {
    name: "Hawk R-7", icon: "🦾", desc: "Balanced multi-role aircraft. Good all-around performance.",
    color: 0x88cc44, accentColor: 0x669933,
    mass: 12000, wingArea: 50, maxThrust: 140000, thrustIdle: 25000,
    clMax: 1.8, cd0: 0.025, kInduced: 0.035,
    rollRate: 1.8, pitchRate: 1.6, yawRate: 1.0,
    maxSpeed: 280, stallSpeed: 70, engineResponseTime: 1.0,
    turnRadius: 400, gLimit: 7.5,
    length: 18, wingspan: 13, height: 5
  },
  titan: {
    name: "Titan B-9", icon: "🛩️", desc: "Heavy bomber. Slow but stable. Requires skill for precision landings.",
    color: 0xcc6644, accentColor: 0x994422,
    mass: 35000, wingArea: 180, maxThrust: 400000, thrustIdle: 60000,
    clMax: 2.5, cd0: 0.035, kInduced: 0.025,
    rollRate: 0.8, pitchRate: 0.7, yawRate: 0.5,
    maxSpeed: 220, stallSpeed: 80, engineResponseTime: 1.5,
    turnRadius: 600, gLimit: 4,
    length: 30, wingspan: 35, height: 9
  }
};

// ── Scenario Definitions ──
const SCENARIOS = {
  landing: { name: "Takeoff & Landing", icon: "🛬", desc: "Master precision flight. Take off and land safely on the runway." },
  dogfight: { name: "Dogfight Arena", icon: "⚔️", desc: "Engage enemy aircraft in high-speed aerial combat." }
};

// ── Global State ──
let selectedAircraft = 'falcon';
let selectedScenario = 'landing';
let gameRunning = false;
let paused = false;
let score = 0;

// ── Three.js Setup ──
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x87CEEB, 0.00015);

const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 1, 50000);
const 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.body.appendChild(renderer.domElement);

// ── Lighting ──
const ambientLight = new THREE.AmbientLight(0x6688aa, 0.5);
scene.add(ambientLight);

const sunLight = new THREE.DirectionalLight(0xffeedd, 2.0);
sunLight.position.set(5000, 3000, 2000);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 4096;
sunLight.shadow.mapSize.height = 4096;
sunLight.shadow.camera.near = 100;
sunLight.shadow.camera.far = 20000;
sunLight.shadow.camera.left = -5000;
sunLight.shadow.camera.right = 5000;
sunLight.shadow.camera.top = 5000;
sunLight.shadow.camera.bottom = -5000;
scene.add(sunLight);

const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x445522, 0.6);
scene.add(hemiLight);

// ── Sky Dome ──
function createSky() {
  const skyGeo = new THREE.SphereGeometry(30000, 32, 32);
  const skyMat = new THREE.ShaderMaterial({
    uniforms: {
      topColor: { value: new THREE.Color(0x0044aa) },
      bottomColor: { value: new THREE.Color(0xaaddff) },
      offset: { value: 20 },
      exponent: { value: 0.5 }
    },
    vertexShader: `
      varying vec3 vWorldPosition;
      void main() {
        vec4 worldPos = modelMatrix * vec4(position, 1.0);
        vWorldPosition = worldPos.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
  });
  scene.add(new THREE.Mesh(skyGeo, skyMat));
}
createSky();

// ── Clouds ──
function createClouds() {
  const cloudGroup = new THREE.Group();
  for (let i = 0; i < 200; i++) {
    const size = 100 + Math.random() * 300;
    const geo = new THREE.SphereGeometry(size, 8, 6);
    const mat = new THREE.MeshStandardMaterial({
      color: 0xffffff, transparent: true, opacity: 0.4 + Math.random() * 0.3,
      roughness: 1, metalness: 0
    });
    const cloud = new THREE.Mesh(geo, mat);
    cloud.position.set(
      (Math.random() - 0.5) * 20000,
      800 + Math.random() * 3000,
      (Math.random() - 0.5) * 20000
    );
    cloud.scale.y = 0.3;
    cloudGroup.add(cloud);
  }
  scene.add(cloudGroup);
}
createClouds();

// ── Procedural Terrain ──
const TERRAIN_SIZE = 16000;
const TERRAIN_SEGMENTS = 512;

function noise2D(x, z) {
  const n = Math.sin(x * 12.9898 + z * 78.233) * 43758.5453;
  return n - Math.floor(n);
}

function fbmNoise(x, z, octaves) {
  let val = 0, amp = 1, freq = 1, maxAmp = 0;
  for (let i = 0; i < octaves; i++) {
    val += (noise2D(x * freq, z * freq) - 0.5) * amp;
    maxAmp += amp;
    amp *= 0.5;
    freq *= 2;
  }
  return val / maxAmp;
}

function getTerrainHeight(x, z) {
  const scale = 0.0003;
  let h = fbmNoise(x * scale, z * scale, 6);
  h = Math.abs(h) * 2;
  // Mountains in certain regions
  const distFromCenter = Math.sqrt(x*x + z*z);
  const mountainFactor = Math.max(0, 1 - distFromCenter / 8000);
  h *= (1 + mountainFactor * 3);

  // Flat areas for runways near center
  if (distFromCenter < 500) {
    h *= distFromCenter / 500;
  }

  return Math.max(0, h * 2000);
}

function createTerrain() {
  const geo = new THREE.PlaneGeometry(TERRAIN_SIZE, TERRAIN_SIZE, TERRAIN_SEGMENTS, TERRAIN_SEGMENTS);
  geo.rotateX(-Math.PI / 2);

  const positions = geo.attributes.position;
  const colors = new Float32Array(positions.count * 3);

  for (let i = 0; i < positions.count; i++) {
    const x = positions.getX(i);
    const z = positions.getZ(i);
    const h = getTerrainHeight(x, z);
    positions.setY(i, h);

    // Vertex colors based on height
    let r, g, b;
    if (h < 20) {
      // Water/shore
      r = 0.15; g = 0.35; b = 0.45;
    } else if (h < 100) {
      // Grass
      const t = (h - 20) / 80;
      r = 0.2 + t * 0.1; g = 0.5 + t * 0.1; b = 0.15;
    } else if (h < 400) {
      // Forest/rock
      const t = (h - 100) / 300;
      r = 0.25 + t * 0.2; g = 0.35 + t * 0.1; b = 0.15 + t * 0.1;
    } else if (h < 700) {
      // Rock
      const t = (h - 400) / 300;
      r = 0.4 + t * 0.2; g = 0.35 + t * 0.15; b = 0.3 + t * 0.1;
    } else {
      // Snow
      const t = Math.min(1, (h - 700) / 400);
      r = 0.6 + t * 0.35; g = 0.6 + t * 0.35; b = 0.65 + t * 0.3;
    }

    colors[i * 3] = r;
    colors[i * 3 + 1] = g;
    colors[i * 3 + 2] = b;
  }

  geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  geo.computeVertexNormals();

  const mat = new THREE.MeshStandardMaterial({
    vertexColors: true, roughness: 0.9, metalness: 0.1, flatShading: false
  });

  const terrain = new THREE.Mesh(geo, mat);
  terrain.receiveShadow = true;
  scene.add(terrain);
  return terrain;
}

const terrain = createTerrain();

// ── Runway ──
function createRunway() {
  const runwayGroup = new THREE.Group();

  // Main runway surface
  const rwGeo = new THREE.PlaneGeometry(60, 2500);
  rwGeo.rotateX(-Math.PI / 2);
  const rwMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 });
  const runway = new THREE.Mesh(rwGeo, rwMat);
  runway.position.y = 1;
  runway.receiveShadow = true;
  runwayGroup.add(runway);

  // Center line markings
  for (let i = -1200; i < 1250; i += 40) {
    const markGeo = new THREE.PlaneGeometry(2, 20);
    markGeo.rotateX(-Math.PI / 2);
    const markMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
    const mark = new THREE.Mesh(markGeo, markMat);
    mark.position.set(0, 1.5, i);
    runwayGroup.add(mark);
  }

  // Threshold markings
  for (let side of [-1, 1]) {
    for (let j = 0; j < 4; j++) {
      const tGeo = new THREE.PlaneGeometry(8, 30);
      tGeo.rotateX(-Math.PI / 2);
      const tMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
      const tMark = new THREE.Mesh(tGeo, tMat);
      tMark.position.set(side * (15 + j * 8), 1.5, -1200);
      runwayGroup.add(tMark);

      const tMark2 = tMark.clone();
      tMark2.position.z = 1200;
      runwayGroup.add(tMark2);
    }
  }

  // Edge lights
  for (let side of [-1, 1]) {
    for (let i = -1250; i <= 1250; i += 30) {
      const lightGeo = new THREE.SphereGeometry(0.5);
      const lightMat = new THREE.MeshStandardMaterial({ color: 0x44aaff, emissive: 0x44aaff, emissiveIntensity: 2 });
      const light = new THREE.Mesh(lightGeo, lightMat);
      light.position.set(side * 31, 1.5, i);
      runwayGroup.add(light);
    }
  }

  // PAPI lights (approach slope indicators)
  for (let i = 0; i < 4; i++) {
    const pGeo = new THREE.BoxGeometry(2, 2, 1);
    const pMat = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 1 });
    const pLight = new THREE.Mesh(pGeo, pMat);
    pLight.position.set(35, 2, -1250 + i * 4);
    runwayGroup.add(pLight);
  }

  scene.add(runwayGroup);
}
createRunway();

// ── Aircraft Model Builder ──
function buildAircraftModel(type) {
  const ac = AIRCRAFT[type];
  const group = new THREE.Group();

  // Fuselage
  const fuselageGeo = new THREE.CylinderGeometry(1.2, 0.8, ac.length * 0.7, 12);
  fuselageGeo.rotateZ(Math.PI / 2);
  const bodyMat = new THREE.MeshStandardMaterial({ color: ac.color, roughness: 0.3, metalness: 0.6 });
  const fuselage = new THREE.Mesh(fuselageGeo, bodyMat);
  group.add(fuselage);

  // Nose cone
  const noseGeo = new THREE.ConeGeometry(1.2, ac.length * 0.35, 12);
  noseGeo.rotateZ(-Math.PI / 2);
  const nose = new THREE.Mesh(noseGeo, bodyMat);
  nose.position.x = ac.length * 0.4;
  group.add(nose);

  // Cockpit canopy
  const cockpitGeo = new THREE.SphereGeometry(1.0, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
  const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x88ccff, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.7 });
  const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
  cockpit.position.set(ac.length * 0.15, 1.2, 0);
  group.add(cockpit);

  // Wings
  const wingHalfSpan = ac.wingspan / 2;
  const wingGeo = new THREE.BoxGeometry(ac.length * 0.3, 0.25, ac.wingspan);
  const wings = new THREE.Mesh(wingGeo, bodyMat);
  wings.position.set(-ac.length * 0.1, -0.2, 0);
  group.add(wings);

  // Wing tips (ailerons)
  for (let side of [-1, 1]) {
    const tipGeo = new THREE.BoxGeometry(1.5, 0.3, 2);
    const tipMat = new THREE.MeshStandardMaterial({ color: ac.accentColor });
    const tip = new THREE.Mesh(tipGeo, tipMat);
    tip.position.set(-ac.length * 0.1, -0.2, side * wingHalfSpan);
    group.add(tip);

    // Navigation lights
    const navLightGeo = new THREE.SphereGeometry(0.3);
    const navColor = side === -1 ? 0xff0000 : 0x00ff00;
    const navMat = new THREE.MeshStandardMaterial({ color: navColor, emissive: navColor, emissiveIntensity: 3 });
    const navLight = new THREE.Mesh(navLightGeo, navMat);
    navLight.position.set(-ac.length * 0.1, -0.2, side * (wingHalfSpan + 0.5));
    group.add(navLight);
  }

  // Tail section
  const tailFuselageGeo = new THREE.CylinderGeometry(0.6, 0.4, ac.length * 0.3, 8);
  tailFuselageGeo.rotateZ(Math.PI / 2);
  const tailFuselage = new THREE.Mesh(tailFuselageGeo, bodyMat);
  tailFuselage.position.x = -ac.length * 0.45;
  group.add(tailFuselage);

  // Vertical stabilizer
  const vStabGeo = new THREE.BoxGeometry(2, ac.height * 0.6, 0.15);
  const vStab = new THREE.Mesh(vStabGeo, bodyMat);
  vStab.position.set(-ac.length * 0.55, ac.height * 0.3, 0);
  group.add(vStab);

  // Rudder accent
  const rudderGeo = new THREE.BoxGeometry(1, ac.height * 0.4, 0.2);
  const rudderMat = new THREE.MeshStandardMaterial({ color: ac.accentColor });
  const rudder = new THREE.Mesh(rudderGeo, rudderMat);
  rudder.position.set(-ac.length * 0.65, ac.height * 0.25, 0);
  group.add(rudder);

  // Horizontal stabilizers
  for (let side of [-1, 1]) {
    const hStabGeo = new THREE.BoxGeometry(3, 0.15, ac.wingspan * 0.4);
    const hStab = new THREE.Mesh(hStabGeo, bodyMat);
    hStab.position.set(-ac.length * 0.5, side * ac.height * 0.25, 0);
    group.add(hStab);
  }

  // Engine exhausts
  for (let side of [-1, 1]) {
    const engineGeo = new THREE.CylinderGeometry(0.6, 0.8, 3, 8);
    engineGeo.rotateZ(Math.PI / 2);
    const engineMat = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.7, metalness: 0.8 });
    const engine = new THREE.Mesh(engineGeo, engineMat);
    engine.position.set(-ac.length * 0.35, -0.5, side * 2);
    group.add(engine);

    // Exhaust glow
    const exhaustGeo = new THREE.SphereGeometry(0.7);
    const exhaustMat = new THREE.MeshStandardMaterial({ color: 0xff4400, emissive: 0xff4400, emissiveIntensity: 0 });
    const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);
    exhaust.position.set(-ac.length * 0.5, -0.5, side * 2);
    exhaust.name = 'exhaust';
    group.add(exhaust);
  }

  // Landing gear (simplified)
  for (let x of [ac.length * 0.3, -ac.length * 0.3]) {
    const gearGeo = new THREE.CylinderGeometry(0.15, 0.15, 2, 6);
    const gearMat = new THREE.MeshStandardMaterial({ color: 0x444444 });
    for (let side of [-1, 1]) {
      const gear = new THREE.Mesh(gearGeo, gearMat);
      gear.position.set(x, -1.5, side * 2);
      group.add(gear);

      // Wheel
      const wheelGeo = new THREE.CylinderGeometry(0.4, 0.4, 0.3, 8);
      wheelGeo.rotateZ(Math.PI / 2);
      const wheelMat = new THREE.MeshStandardMaterial({ color: 0x222222 });
      const wheel = new THREE.Mesh(wheelGeo, wheelMat);
      wheel.position.set(x, -2.5, side * 2);
      group.add(wheel);
    }
  }

  // Nose gear
  const noseGearGeo = new THREE.CylinderGeometry(0.12, 0.12, 1.5, 6);
  const noseGearMat = new THREE.MeshStandardMaterial({ color: 0x444444 });
  const noseGear = new THREE.Mesh(noseGearGeo, noseGearMat);
  noseGear.position.set(ac.length * 0.35, -1.2, 0);
  group.add(noseGear);

  // Scale the model appropriately
  const scaleFactor = type === 'titan' ? 1.5 : (type === 'hawk' ? 1.2 : 1.0);
  group.scale.setScalar(scaleFactor);

  return group;
}

// ── Enemy Aircraft for Dogfight ──
function buildEnemyModel() {
  const group = new THREE.Group();
  const bodyMat = new THREE.MeshStandardMaterial({ color: 0xcc2222, roughness: 0.3, metalness: 0.6 });

  const fuselageGeo = new THREE.CylinderGeometry(1, 0.7, 14, 10);
  fuselageGeo.rotateZ(Math.PI / 2);
  group.add(new THREE.Mesh(fuselageGeo, bodyMat));

  const noseGeo = new THREE.ConeGeometry(1, 5, 10);
  noseGeo.rotateZ(-Math.PI / 2);
  const nose = new THREE.Mesh(noseGeo, bodyMat);
  nose.position.x = 9;
  group.add(nose);

  const wingGeo = new THREE.BoxGeometry(4, 0.2, 10);
  group.add(new THREE.Mesh(wingGeo, bodyMat));

  for (let side of [-1, 1]) {
    const tipGeo = new THREE.SphereGeometry(0.3);
    const tipMat = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 2 });
    const tip = new THREE.Mesh(tipGeo, tipMat);
    tip.position.set(0, 0, side * 5.3);
    group.add(tip);
  }

  return group;
}

// ── Physics State ──
const physics = {
  position: new THREE.Vector3(0, 10, -1200),
  velocity: new THREE.Vector3(0, 0, 0),
  quaternion: new THREE.Quaternion(),
  angularVelocity: new THREE.Vector3(0, 0, 0),

  // Engine state
  throttleTarget: 0.0,
  throttleCurrent: 0.0,
  rpm: 0,

  // Flight data
  airspeed: 0,
  altitude: 10,
  verticalSpeed: 0,
  gForce: 1,
  angleOfAttack: 0,
  isStalled: false,
  onGround: true,

  // Inertia tensor (simplified)
  inertiaX: 1.0,
  inertiaY: 2.0,
  inertiaZ: 2.5,

  // Damage
  health: 100,
  damaged: false
};

// ── Input State ──
const input = {
  pitch: 0, roll: 0, yaw: 0, throttleUp: false, throttleDown: false, brake: false
};

document.addEventListener('keydown', (e) => {
  switch(e.code) {
    case 'KeyW': input.pitch = -1; break;
    case 'KeyS': input.pitch = 1; break;
    case 'KeyA': input.roll = -1; break;
    case 'KeyD': input.roll = 1; break;
    case 'KeyQ': input.yaw = -1; break;
    case 'KeyE': input.yaw = 1; break;
    case 'ShiftLeft': case 'ShiftRight': input.throttleUp = true; break;
    case 'ControlLeft': case 'ControlRight': input.throttleDown = true; break;
    case 'Space': input.brake = true; e.preventDefault(); break;
    case 'KeyP': togglePause(); break;
    case 'Escape': if(paused) { /* handled in pause */ } break;
  }
});

document.addEventListener('keyup', (e) => {
  switch(e.code) {
    case 'KeyW': case 'KeyS': input.pitch = 0; break;
    case 'KeyA': case 'KeyD': input.roll = 0; break;
    case 'KeyQ': case 'KeyE': input.yaw = 0; break;
    case 'ShiftLeft': case 'ShiftRight': input.throttleUp = false; break;
    case 'ControlLeft': case 'ControlRight': input.throttleDown = false; break;
    case 'Space': input.brake = false; break;
  }
});

// ── Audio System ──
let audioCtx, engineGain, windGain, masterGain;
let engineOsc1, engineOsc2, engineNoise;

function initAudio() {
  try {
    audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    masterGain = audioCtx.createGain();
    masterGain.gain.value = 0.3;
    masterGain.connect(audioCtx.destination);

    // Engine sound - layered oscillators + noise
    engineGain = audioCtx.createGain();
    engineGain.gain.value = 0;
    engineGain.connect(masterGain);

    engineOsc1 = audioCtx.createOscillator();
    engineOsc1.type = 'sawtooth';
    engineOsc1.frequency.value = 80;
    const osc1Gain = audioCtx.createGain();
    osc1Gain.gain.value = 0.3;
    engineOsc1.connect(osc1Gain);
    osc1Gain.connect(engineGain);
    engineOsc1.start();

    engineOsc2 = audioCtx.createOscillator();
    engineOsc2.type = 'square';
    engineOsc2.frequency.value = 40;
    const osc2Gain = audioCtx.createGain();
    osc2Gain.gain.value = 0.15;
    engineOsc2.connect(osc2Gain);
    osc2Gain.connect(engineGain);
    engineOsc2.start();

    // Engine noise (filtered white noise)
    const bufferSize = audioCtx.sampleRate * 2;
    const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
    const data = noiseBuffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;

    engineNoise = audioCtx.createBufferSource();
    engineNoise.buffer = noiseBuffer;
    engineNoise.loop = true;
    const noiseFilter = audioCtx.createBiquadFilter();
    noiseFilter.type = 'lowpass';
    noiseFilter.frequency.value = 200;
    const noiseGain = audioCtx.createGain();
    noiseGain.gain.value = 0.4;
    engineNoise.connect(noiseFilter);
    noiseFilter.connect(noiseGain);
    noiseGain.connect(engineGain);
    engineNoise.start();

    // Wind sound
    windGain = audioCtx.createGain();
    windGain.gain.value = 0;
    const windBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
    const windData = windBuffer.getChannelData(0);
    for (let i = 0; i < bufferSize; i++) windData[i] = Math.random() * 2 - 1;

    const windSource = audioCtx.createBufferSource();
    windSource.buffer = windBuffer;
    windSource.loop = true;
    const windFilter = audioCtx.createBiquadFilter();
    windFilter.type = 'bandpass';
    windFilter.frequency.value = 500;
    windFilter.Q.value = 0.5;
    windSource.connect(windFilter);
    windFilter.connect(windGain);
    windSource.start();

    // Wind connects to master directly (not engine gain)
    windGain.connect(masterGain);

  } catch(e) { console.warn('Audio init failed:', e); }
}

function updateAudio() {
  if (!audioCtx || audioCtx.state === 'suspended') return;

  const ac = AIRCRAFT[selectedAircraft];
  const throttleRatio = physics.throttleCurrent / (ac.maxThrust - ac.thrustIdle);
  const speedFactor = Math.min(physics.airspeed / ac.maxSpeed, 1);

  // Engine pitch and volume based on throttle + RPM
  const baseFreq = 40 + throttleRatio * 120;
  engineOsc1.frequency.setTargetAtTime(baseFreq, audioCtx.currentTime, 0.1);
  engineOsc2.frequency.setTargetAtTime(baseFreq * 0.5, audioCtx.currentTime, 0.1);

  const vol = Math.max(0.05, throttleRatio * 0.8 + speedFactor * 0.2);
  engineGain.gain.setTargetAtTime(vol, audioCtx.currentTime, 0.1);

  // Wind noise based on airspeed
  const windVol = Math.min(speedFactor * 0.6, 0.5);
  windGain.gain.setTargetAtTime(windVol, audioCtx.currentTime, 0.2);
}

// ── Dogfight Enemies ──
let enemies = [];
const ENEMY_COUNT = 4;

function spawnEnemies() {
  for (let i = 0; i < ENEMY_COUNT; i++) {
    const model = buildEnemyModel();
    const angle = (i / ENEMY_COUNT) * Math.PI * 2;
    const dist = 1500 + Math.random() * 1000;
    model.position.set(
      physics.position.x + Math.cos(angle) * dist,
      300 + Math.random() * 800,
      physics.position.z + Math.sin(angle) * dist
    );
    scene.add(model);

    enemies.push({
      mesh: model,
      velocity: new THREE.Vector3(
        (Math.random() - 0.5) * 100,
        (Math.random() - 0.5) * 20,
        (Math.random() - 0.5) * 100
      ),
      health: 100,
      alive: true,
      targetAngle: Math.random() * Math.PI * 2,
      turnTimer: 0,
      altitude: model.position.y
    });
  }
}

function updateEnemies(dt) {
  const playerPos = physics.position;

  enemies.forEach(enemy => {
    if (!enemy.alive) return;

    enemy.turnTimer -= dt;
    if (enemy.turnTimer <= 0) {
      // Pick new target direction - sometimes chase player, sometimes patrol
      if (Math.random() < 0.4) {
        const toPlayer = playerPos.clone().sub(enemy.mesh.position).normalize();
        enemy.targetAngle = Math.atan2(toPlayer.x, toPlayer.z);
      } else {
        enemy.targetAngle += (Math.random() - 0.5) * 1.5;
      }
      enemy.turnTimer = 2 + Math.random() * 4;

      // Vary altitude
      if (Math.random() < 0.3) {
        enemy.altitude = 200 + Math.random() * 1000;
      }
    }

    const speed = 80 + Math.random() * 40;
    const dirX = Math.sin(enemy.targetAngle);
    const dirZ = Math.cos(enemy.targetAngle);

    // Smooth velocity change
    enemy.velocity.x += (dirX * speed - enemy.velocity.x) * dt * 2;
    enemy.velocity.z += (dirZ * speed - enemy.velocity.z) * dt * 2;
    enemy.velocity.y += ((enemy.altitude - enemy.mesh.position.y) * 0.5 - enemy.velocity.y) * dt;

    enemy.mesh.position.add(enemy.velocity.clone().multiplyScalar(dt));

    // Orient mesh to velocity direction
    const velDir = enemy.velocity.clone().normalize();
    if (velDir.length() > 0.1) {
      const targetQuat = new THREE.Quaternion();
      const lookMatrix = new THREE.Matrix4();
      lookMatrix.lookAt(enemy.mesh.position, enemy.mesh.position.clone().add(velDir), new THREE.Vector3(0, 1, 0));
      targetQuat.setFromRotationMatrix(lookMatrix);
      enemy.mesh.quaternion.slerp(targetQuat, dt * 3);
    }

    // Keep above terrain
    const th = getTerrainHeight(enemy.mesh.position.x, enemy.mesh.position.z);
    if (enemy.mesh.position.y < th + 50) {
      enemy.velocity.y += 100;
      enemy.altitude = Math.max(enemy.altitude, th + 200);
    }

    // Collision with player
    const distToPlayer = enemy.mesh.position.distanceTo(playerPos);
    if (distToPlayer < 30) {
      physics.health -= 5 * dt;
      document.getElementById('damage-overlay').classList.add('hit');
      setTimeout(() => document.getElementById('damage-overlay').classList.remove('hit'), 200);

      // Push player away
      const pushDir = playerPos.clone().sub(enemy.mesh.position).normalize();
      physics.velocity.add(pushDir.multiplyScalar(50));
    }

    if (physics.health <= 0) {
      enemy.alive = false;
      scene.remove(enemy.mesh);
    }
  });
}

// ── Weapon System ──
let missiles = [];
let canFire = true;
let fireCooldown = 0;

function fireWeapon() {
  if (!canFire || fireCooldown > 0) return;
  fireCooldown = 1.5;
  canFire = false;

  // Create missile trail
  const missileGeo = new THREE.SphereGeometry(2);
  const missileMat = new THREE.MeshStandardMaterial({ color: 0xffaa00, emissive: 0xff6600, emissiveIntensity: 3 });
  const missile = new THREE.Mesh(missileGeo, missileMat);

  // Start from aircraft position + forward offset
  const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.quaternion);
  missile.position.copy(physics.position).add(forward.multiplyScalar(20));

  scene.add(missile);

  missiles.push({
    mesh: missile,
    velocity: forward.clone().multiplyScalar(400),
    life: 5.0
  });
}

document.addEventListener('mousedown', () => {
  if (selectedScenario === 'dogfight' && gameRunning && !paused) fireWeapon();
});

function updateMissiles(dt) {
  for (let i = missiles.length - 1; i >= 0; i--) {
    const m = missiles[i];
    m.mesh.position.add(m.velocity.clone().multiplyScalar(dt));
    m.life -= dt;

    // Check hit enemies
    let hit = false;
    enemies.forEach(enemy => {
      if (!enemy.alive) return;
      if (m.mesh.position.distanceTo(enemy.mesh.position) < 40) {
        enemy.health -= 50;
        if (enemy.health <= 0) {
          enemy.alive = false;
          scene.remove(enemy.mesh);
          score += 100;

          // Explosion effect
          createExplosion(enemy.mesh.position.clone());
        } else {
          createExplosion(m.mesh.position.clone(), true);
        }
        hit = true;
      }
    });

    if (hit || m.life <= 0) {
      scene.remove(m.mesh);
      missiles.splice(i, 1);
    }
  }
}

function createExplosion(pos, small) {
  const count = small ? 8 : 20;
  for (let i = 0; i < count; i++) {
    const geo = new THREE.SphereGeometry(small ? 1 : 3);
    const mat = new THREE.MeshStandardMaterial({
      color: Math.random() > 0.5 ? 0xff4400 : 0xffaa00,
      emissive: 0xff6600, emissiveIntensity: 2
    });
    const particle = new THREE.Mesh(geo, mat);
    particle.position.copy(pos);
    scene.add(particle);

    const vel = new THREE.Vector3(
      (Math.random() - 0.5) * 100,
      Math.random() * 80,
      (Math.random() - 0.5) * 100
    );

    setTimeout(() => scene.remove(particle), small ? 300 : 800);

    // Animate particle
    let t = 0;
    const anim = () => {
      t += 0.016;
      particle.position.add(vel.clone().multiplyScalar(0.016));
      vel.y -= 20 * 0.016;
      particle.scale.multiplyScalar(0.97);
      if (t < (small ? 0.3 : 0.8)) requestAnimationFrame(anim);
    };
    anim();
  }
}

// ── Landing Score System ──
let landingScore = null;
let hasLanded = false;

function evaluateLanding() {
  if (hasLanded) return;
  const ac = AIRCRAFT[selectedAircraft];

  // Check if on runway and slow enough
  const distFromCenter = Math.sqrt(physics.position.x * physics.position.x + physics.position.z * physics.position.z);
  const isOnRunway = Math.abs(physics.position.x) < 30 && Math.abs(physics.position.z) < 1250;

  if (isOnRunway && physics.onGround && physics.airspeed < ac.stallSpeed + 10) {
    hasLanded = true;

    let grade, points;
    const speedFactor = Math.max(0, 1 - physics.airspeed / (ac.stallSpeed * 2));
    const vertSpeedFactor = Math.max(0, 1 - Math.abs(physics.verticalSpeed) / 5);
    const alignmentFactor = Math.max(0, 1 - Math.abs(physics.position.x) / 30);

    points = (speedFactor * 40 + vertSpeedFactor * 30 + alignmentFactor * 30).toFixed(0);

    if (points >= 90) grade = 'A+';
    else if (points >= 80) grade = 'A';
    else if (points >= 70) grade = 'B';
    else if (points >= 60) grade = 'C';
    else if (points >= 40) grade = 'D';
    else grade = 'F';

    landingScore = { grade, points };

    setTimeout(() => {
      document.getElementById('landing-grade').textContent = grade;
      document.getElementById('landing-details').textContent = `Score: ${points}/100 | Speed: ${physics.airspeed.toFixed(1)} m/s | Vertical: ${Math.abs(physics.verticalSpeed).toFixed(1)} m/s`;
      document.getElementById('landing-score').classList.add('active');
    }, 2000);
  }

  // Crash detection
  if (physics.onGround && physics.airspeed > ac.stallSpeed * 3) {
    hasLanded = true;
    createExplosion(physics.position.clone());
    setTimeout(() => {
      document.getElementById('landing-grade').textContent = 'CRASH';
      document.getElementById('landing-grade').style.color = '#f00';
      document.getElementById('landing-details').textContent = `Impact speed: ${physics.airspeed.toFixed(1)} m/s`;
      document.getElementById('landing-score').classList.add('active');
    }, 1500);
  }
}

// ── Physics Engine ──
function updatePhysics(dt) {
  const ac = AIRCRAFT[selectedAircraft];
  dt = Math.min(dt, 0.033); // Cap delta time

  // Air density based on altitude (simplified barometric formula)
  const seaLevelDensity = 1.225;
  const airDensity = seaLevelDensity * Math.exp(-physics.altitude / 8500);

  // ── Throttle Response ──
  if (input.throttleUp && physics.throttleTarget < 1) {
    physics.throttleTarget += dt / ac.engineResponseTime;
  }
  if (input.throttleDown && physics.throttleTarget > 0) {
    physics.throttleTarget -= dt / ac.engineResponseTime;
  }
  physics.throttleTarget = Math.max(0, Math.min(1, physics.throttleTarget));

  // Smooth throttle transition
  const thrustDiff = physics.throttleTarget - physics.throttleCurrent;
  physics.throttleCurrent += thrustDiff * dt * (1 / ac.engineResponseTime);

  // Current thrust force
  const currentThrust = ac.thrustIdle + (ac.maxThrust - ac.thrustIdle) * physics.throttleCurrent;

  // ── Transform basis vectors ──
  const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.quaternion);
  const up = new THREE.Vector3(0, 1, 0).applyQuaternion(physics.quaternion);
  const right = new THREE.Vector3(0, 0, 1).applyQuaternion(physics.quaternion);

  // ── Airspeed ──
  physics.airspeed = physics.velocity.length();

  // ── Angle of Attack ──
  if (physics.airspeed > 1) {
    const velDir = physics.velocity.clone().normalize();
    physics.angleOfAttack = Math.asin(Math.max(-1, Math.min(1, -forward.dot(velDir))));
  }

  // ── Lift Calculation ──
  let cl = 0;
  if (physics.airspeed > 5) {
    const aoaRad = physics.angleOfAttack;
    // Linear lift region
    if (Math.abs(aoaRad) < 0.3) {
      cl = 2 * Math.PI * aoaRad + 0.2; // Cl_alpha ~ 2π for thin airfoil
    } else if (Math.abs(aoaRad) < 0.5) {
      // Approaching stall - lift peaks then drops
      const stallFactor = 1 - ((Math.abs(aoaRad) - 0.3) / 0.2);
      cl = ac.clMax * stallFactor;
    } else {
      // Post-stall
      cl = ac.clMax * 0.3;
    }

    physics.isStalled = Math.abs(aoaRad) > 0.4 && !physics.onGround;
  }

  const liftMagnitude = 0.5 * airDensity * physics.airspeed * physics.airspeed * ac.wingArea * cl;
  const liftVector = up.clone().multiplyScalar(liftMagnitude);

  // ── Drag Calculation ──
  const cdProfile = ac.cd0;
  const cdInduced = ac.kInduced * cl * cl;
  const totalCd = cdProfile + cdInduced;
  const dragMagnitude = 0.5 * airDensity * physics.airspeed * physics.airspeed * ac.wingArea * totalCd;

  let dragVector = new THREE.Vector3();
  if (physics.airspeed > 1) {
    dragVector = physics.velocity.clone().normalize().multiplyScalar(-dragMagnitude);
  }

  // ── Weight ──
  const weightVector = new THREE.Vector3(0, -ac.mass * 9.81, 0);

  // ── Thrust Vector ──
  const thrustVector = forward.clone().multiplyScalar(currentThrust);

  // ── Ground friction (when on ground) ──
  let groundFriction = new THREE.Vector3();
  if (physics.onGround) {
    const horizontalVel = new THREE.Vector3(physics.velocity.x, 0, physics.velocity.z);
    if (horizontalVel.length() > 1) {
      groundFriction = horizontalVel.clone().normalize().multiplyScalar(-ac.mass * 9.81 * 0.4);

      // Braking
      if (input.brake) {
        groundFriction.multiplyScalar(2.5);
      }
    }
  }

  // ── Net Force ──
  const netForce = new THREE.Vector3();
  netForce.add(liftVector);
  netForce.add(dragVector);
  netForce.add(weightVector);
  netForce.add(thrustVector);
  netForce.add(groundFriction);

  // ── Acceleration & Velocity Update ──
  const acceleration = netForce.clone().divideScalar(ac.mass);
  physics.velocity.add(acceleration.multiplyScalar(dt));

  // ── Position Update ──
  physics.position.add(physics.velocity.clone().multiplyScalar(dt));

  // ── Terrain Collision ──
  const terrainHeight = getTerrainHeight(physics.position.x, physics.position.z);
  if (physics.position.y < terrainHeight + 3) {
    physics.position.y = terrainHeight + 3;
    physics.velocity.y = Math.max(0, physics.velocity.y);

    if (!physics.onGround && physics.airspeed > ac.stallSpeed * 2) {
      // Hard crash
      physics.health -= (physics.airspeed - ac.stallSpeed * 2) * 5;
    }

    physics.onGround = true;
    physics.verticalSpeed = 0;
  } else {
    if (physics.onGround && currentThrust > ac.mass * 9.81 * 0.7 && physics.airspeed > ac.stallSpeed * 0.5) {
      physics.onGround = false;
    }
    // Also take off with enough lift
    if (physics.onGround && liftMagnitude > ac.mass * 9.81 * 0.9) {
      physics.onGround = false;
    }
  }

  physics.altitude = physics.position.y - terrainHeight;
  physics.verticalSpeed = physics.velocity.dot(new THREE.Vector3(0, 1, 0));

  // ── Rotational Dynamics (Inertia-based) ──
  const controlAuthority = Math.min(physics.airspeed / 50, 1); // Controls less effective at low speed

  // Target angular rates from input
  const targetRollRate = -input.roll * ac.rollRate * controlAuthority;
  const targetPitchRate = -input.pitch * ac.pitchRate * controlAuthority;
  const targetYawRate = input.yaw * ac.yawRate * controlAuthority;

  // Smooth angular velocity changes (inertia)
  physics.angularVelocity.x += (targetRollRate - physics.angularVelocity.x) * dt * 3;
  physics.angularVelocity.y += (targetYawRate - physics.angularVelocity.y) * dt * 2;
  physics.angularVelocity.z += (targetPitchRate - physics.angularVelocity.z) * dt * 3;

  // Apply angular damping
  physics.angularVelocity.multiplyScalar(0.95);

  // Convert angular velocity to quaternion rotation
  const rotQuat = new THREE.Quaternion();
  const angVelScaled = physics.angularVelocity.clone().multiplyScalar(dt);
  rotQuat.setFromAxisAngle(
    physics.angularVelocity.clone().normalize(),
    physics.angularVelocity.length() * dt
  );

  // Apply rotation: roll around forward, pitch around right, yaw around up
  const rollQuat = new THREE.Quaternion().setFromAxisAngle(forward, -input.roll * ac.rollRate * controlAuthority * dt);
  const pitchQuat = new THREE.Quaternion().setFromAxisAngle(right, -input.pitch * ac.pitchRate * controlAuthority * dt);
  const yawQuat = new THREE.Quaternion().setFromAxisAngle(up, input.yaw * ac.yawRate * controlAuthority * dt);

  physics.quaternion.premultiply(rollQuat);
  physics.quaternion.premultiply(pitchQuat);
  physics.quaternion.premultiply(yawQuat);
  physics.quaternion.normalize();

  // ── G-Force Calculation ──
  const totalAccel = acceleration.length();
  physics.gForce = (totalAccel + 9.81) / 9.81;

  // ── Auto-level when no input and not stalled ──
  if (!input.pitch && !input.roll && !physics.isStalled && !physics.onGround) {
    const currentUp = new THREE.Vector3(0, 1, 0).applyQuaternion(physics.quaternion);
    const worldUp = new THREE.Vector3(0, 1, 0);
    const levelQuat = new THREE.Quaternion().setFromUnitVectors(currentUp, worldUp);
    physics.quaternion.slerp(levelQuat, dt * 0.5);
  }

  // ── Landing evaluation ──
  if (selectedScenario === 'landing') {
    evaluateLanding();
  }
}

// ── Camera System ──
const cameraOffset = new THREE.Vector3(-40, 12, 0);
let cameraSmoothPos = new THREE.Vector3();
let cameraSmoothTarget = new THREE.Vector3();

function updateCamera() {
  // Calculate desired camera position relative to aircraft
  const camLocalOffset = cameraOffset.clone().applyQuaternion(physics.quaternion);
  const desiredCamPos = physics.position.clone().add(camLocalOffset);

  // Smooth follow
  cameraSmoothPos.lerp(desiredCamPos, 0.08);
  camera.position.copy(cameraSmoothPos);

  // Look slightly ahead of aircraft
  const lookAhead = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.quaternion).multiplyScalar(50);
  cameraSmoothTarget.lerp(physics.position.clone().add(lookAhead), 0.1);
  camera.lookAt(cameraSmoothTarget);

  // FOV based on speed (warp effect at high speed)
  const ac = AIRCRAFT[selectedAircraft];
  const speedRatio = physics.airspeed / ac.maxSpeed;
  camera.fov = 65 + speedRatio * 15;
  camera.updateProjectionMatrix();
}

// ── HUD Update ──
function updateHUD() {
  const ac = AIRCRAFT[selectedAircraft];
  const kts = (physics.airspeed * 1.943).toFixed(0);
  const altFt = (physics.altitude * 3.281).toFixed(0);
  const vsFpm = (physics.verticalSpeed * 196.85).toFixed(0);
  const heading = ((Math.atan2(physics.velocity.x, physics.velocity.z) * 180 / Math.PI + 360) % 360).toFixed(0);

  document.getElementById('hud-left').innerHTML = `
    <div style="font-size:1.4em;font-weight:bold;">${kts} KTS</div>
    <div>AIRSPEED</div>
    <br>
    <div style="font-size:1.3em;">${altFt} FT</div>
    <div>ALTITUDE</div>
    <br>
    <div>${vsFpm > 0 ? '+' : ''}${vsFpm} FPM</div>
    <div>V/SPEED</div>
  `;

  document.getElementById('hud-right').innerHTML = `
    <div style="font-size:1.3em;">HDG ${heading}°</div>
    <br>
    <div>G: ${physics.gForce.toFixed(1)}</div>
    <br>
    <div>THR: ${(physics.throttleCurrent * 100).toFixed(0)}%</div>
    <br>
    <div>${ac.name}</div>
    <div style="color:${physics.health > 50 ? '#0f0' : '#f00'};">HP: ${Math.max(0, physics.health).toFixed(0)}%</div>
  `;

  // Bottom status bar
  let bottomText = '';
  if (physics.isStalled) {
    document.getElementById('stall-warning').style.display = 'block';
  } else {
    document.getElementById('stall-warning').style.display = 'none';
  }

  if (selectedScenario === 'dogfight') {
    const aliveCount = enemies.filter(e => e.alive).length;
    bottomText = `ENEMIES: ${aliveCount}/${ENEMY_COUNT} | SCORE: ${score}`;
  } else {
    bottomText = physics.onGround ? 'ON GROUND' : 'AIRBORNE';
  }

  document.getElementById('hud-bottom').textContent = bottomText;

  // G-meter
  const gPercent = Math.min(100, ((physics.gForce - 1) / (ac.gLimit - 1)) * 100);
  document.getElementById('g-meter-fill').style.height = `${Math.max(0, gPercent)}%`;

  // Horizon line rotation
  const horizonLine = document.getElementById('horizon-line');
  const pitchDeg = physics.angleOfAttack * 180 / Math.PI;
  horizonLine.style.transform = `rotate(${-pitchDeg}deg)`;

  // Score display for dogfight
  if (selectedScenario === 'dogfight') {
    document.getElementById('score-display').textContent = `SCORE: ${score}`;
  } else {
    document.getElementById('score-display').textContent = '';
  }

  // Target reticle for dogfight
  const targetReticle = document.getElementById('target-reticle');
  if (selectedScenario === 'dogfight') {
    let closestDist = Infinity;
    enemies.forEach(e => {
      if (!e.alive) return;
      const d = e.mesh.position.distanceTo(physics.position);
      if (d < closestDist) closestDist = d;
    });

    if (closestDist < 800) {
      targetReticle.style.display = 'block';
      targetReticle.classList.toggle('locked', closestDist < 300);
    } else {
      targetReticle.style.display = 'none';
    }
  } else {
    targetReticle.style.display = 'none';
  }

  // Minimap
  updateMinimap();
}

function updateMinimap() {
  const canvas = document.getElementById('minimap-canvas');
  const ctx = canvas.getContext('2d');
  ctx.clearRect(0, 0, 150, 150);

  // Background
  ctx.fillStyle = 'rgba(0,30,0,0.8)';
  ctx.fillRect(0, 0, 150, 150);

  const scale = 0.02;
  const cx = 75, cy = 75;

  // Draw terrain contour (simplified)
  for (let x = -60; x < 60; x += 8) {
    for (let z = -60; z < 60; z += 8) {
      const worldX = physics.position.x + x / scale;
      const worldZ = physics.position.z + z / scale;
      const h = getTerrainHeight(worldX, worldZ);
      const brightness = Math.min(255, (h / 1000) * 255);
      ctx.fillStyle = `rgb(${brightness*0.3},${brightness*0.6},${brightness*0.2})`;
      ctx.fillRect(cx + x, cy + z, 7, 7);
    }
  }

  // Draw runway
  ctx.strokeStyle = '#fff';
  ctx.lineWidth = 1;
  const rwDistZ = (-1250 - physics.position.z) * scale;
  if (Math.abs(rwDistZ) < 60) {
    ctx.beginPath();
    ctx.moveTo(cx, cy + Math.max(-60, rwDistZ));
    ctx.lineTo(cx, cy + Math.min(60, rwDistZ + 2500 * scale));
    ctx.stroke();
  }

  // Draw enemies (dogfight)
  if (selectedScenario === 'dogfight') {
    enemies.forEach(e => {
      if (!e.alive) return;
      const dx = (e.mesh.position.x - physics.position.x) * scale;
      const dz = (e.mesh.position.z - physics.position.z) * scale;
      if (Math.abs(dx) < 60 && Math.abs(dz) < 60) {
        ctx.fillStyle = '#f00';
        ctx.beginPath();
        ctx.arc(cx + dx, cy + dz, 3, 0, Math.PI * 2);
        ctx.fill();
      }
    });
  }

  // Player position (center)
  ctx.fillStyle = '#0f0';
  ctx.beginPath();
  ctx.arc(cx, cy, 4, 0, Math.PI * 2);
  ctx.fill();

  // Direction indicator
  const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.quaternion);
  ctx.strokeStyle = '#0f0';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(cx, cy);
  ctx.lineTo(cx + forward.x * 15, cy + forward.z * 15);
  ctx.stroke();

  // Border circle
  ctx.strokeStyle = 'rgba(0,255,0,0.4)';
  ctx.lineWidth = 1;
  ctx.beginPath();
  ctx.arc(cx, cy, 73, 0, Math.PI * 2);
  ctx.stroke();
}

// ── Aircraft Mesh Update ──
let aircraftMesh;

function updateAircraftVisuals() {
  if (!aircraftMesh) return;

  // Position and rotation
  aircraftMesh.position.copy(physics.position);
  aircraftMesh.quaternion.copy(physics.quaternion);

  // Exhaust glow based on throttle
  const ac = AIRCRAFT[selectedAircraft];
  aircraftMesh.traverse(child => {
    if (child.name === 'exhaust') {
      child.material.emissiveIntensity = physics.throttleCurrent * 3;
    }
  });
}

// ── Pause Toggle ──
function togglePause() {
  paused = !paused;
  document.getElementById('pause-overlay').classList.toggle('active', paused);
}

// ── Menu Setup ──
function setupMenu() {
  const acSelect = document.getElementById('aircraft-select');
  Object.entries(AIRCRAFT).forEach(([key, ac]) => {
    const card = document.createElement('div');
    card.className = 'ac-card' + (key === selectedAircraft ? ' selected' : '');
    card.innerHTML = `
      <div class="icon">${ac.icon}</div>
      <h3>${ac.name}</h3>
      <p>${ac.desc}</p>
      <p style="margin-top:0.5rem;color:#666;">Max Speed: ${ac.maxSpeed} m/s | Weight: ${(ac.mass/1000).toFixed(0)}t</p>
    `;
    card.addEventListener('click', () => {
      document.querySelectorAll('.ac-card').forEach(c => c.classList.remove('selected'));
      card.classList.add('selected');
      selectedAircraft = key;
    });
    acSelect.appendChild(card);
  });

  const scSelect = document.getElementById('scenario-select');
  Object.entries(SCENARIOS).forEach(([key, sc]) => {
    const card = document.createElement('div');
    card.className = 'sc-card' + (key === selectedScenario ? ' selected' : '');
    card.innerHTML = `
      <div class="icon">${sc.icon}</div>
      <h3>${sc.name}</h3>
      <p>${sc.desc}</p>
    `;
    card.addEventListener('click', () => {
      document.querySelectorAll('.sc-card').forEach(c => c.classList.remove('selected'));
      card.classList.add('selected');
      selectedScenario = key;
    });
    scSelect.appendChild(card);
  });

  document.getElementById('start-btn').addEventListener('click', startGame);
}

// ── Start Game ──
function startGame() {
  document.getElementById('menu-screen').classList.add('hidden');
  document.getElementById('hud').classList.add('active');

  // Reset physics state
  physics.position.set(0, 15, -1200);
  physics.velocity.set(0, 0, 0);
  physics.quaternion.identity();
  physics.angularVelocity.set(0, 0, 0);
  physics.throttleTarget = 0;
  physics.throttleCurrent = 0;
  physics.health = 100;
  physics.onGround = true;
  hasLanded = false;
  score = 0;

  // Build aircraft model
  if (aircraftMesh) scene.remove(aircraftMesh);
  aircraftMesh = buildAircraftModel(selectedAircraft);
  scene.add(aircraftMesh);

  // Spawn enemies for dogfight
  enemies.forEach(e => { if (e.mesh && e.mesh.parent) scene.remove(e.mesh); });
  enemies = [];
  missiles.forEach(m => scene.remove(m.mesh));
  missiles = [];

  if (selectedScenario === 'dogfight') {
    spawnEnemies();
  }

  // Init audio
  initAudio();
  if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();

  gameRunning = true;
}

// ── Main Game Loop ──
let lastTime = performance.now();

function gameLoop(time) {
  requestAnimationFrame(gameLoop);

  const dt = Math.min((time - lastTime) / 1000, 0.05);
  lastTime = time;

  if (!gameRunning || paused) {
    renderer.render(scene, camera);
    return;
  }

  // Update physics
  updatePhysics(dt);

  // Update visuals
  updateAircraftVisuals();
  updateCamera();

  // Dogfight updates
  if (selectedScenario === 'dogfight') {
    fireCooldown -= dt;
    if (fireCooldown <= 0) canFire = true;
    updateEnemies(dt);
    updateMissiles(dt);

    // Respawn enemies if all dead
    const aliveCount = enemies.filter(e => e.alive).length;
    if (aliveCount === 0 && score > 0) {
      setTimeout(() => spawnEnemies(), 3000);
    }
  }

  // Update HUD
  updateHUD();

  // Update audio
  updateAudio();

  // Render
  renderer.render(scene, camera);
}

// ── Window Resize ──
window.addEventListener('resize', () => {
  camera.aspect = window.innerWidth / window.innerHeight;
  camera.updateProjectionMatrix();
  renderer.setSize(window.innerWidth, window.innerHeight);
});

// ── Initialize ──
function init() {
  setupMenu();
  document.getElementById('menu-screen').classList.remove('hidden');
  document.getElementById('loading-screen').style.display = 'none';
  requestAnimationFrame(gameLoop);
}

init();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"middle","sessionId":"e5527513-7e77-44c1-a691-4e379174b60d","tokensIn":23853,"tokensOut":19974,"tokensTotal":43827,"turns":2,"toolCalls":1,"failedToolCalls":0,"timestamp":"2026-07-18T22:48:06.980Z"} -->