← Flight Simulator results

Flight Simulator

Grok 4.5 · full

◈Preview unavailable — try playing the result

Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 82,053 bytes · SHA-256 bfdc114b8d0e
<!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>
  @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700&family=Rajdhani:wght@400;500;600;700&display=swap');

  * { margin: 0; padding: 0; box-sizing: border-box; }

  html, body {
    width: 100%; height: 100%;
    overflow: hidden;
    background: #02060c;
    font-family: 'Rajdhani', system-ui, sans-serif;
    color: #e8f4ff;
    user-select: none;
  }

  #canvas-wrap {
    position: fixed; inset: 0;
    z-index: 0;
  }
  canvas { display: block; width: 100%; height: 100%; }

  /* ── Menus ── */
  .screen {
    position: fixed; inset: 0;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    z-index: 20;
    background:
      radial-gradient(ellipse at 50% 0%, rgba(30,80,160,0.35), transparent 55%),
      radial-gradient(ellipse at 80% 100%, rgba(180,60,20,0.2), transparent 50%),
      linear-gradient(180deg, #040a14 0%, #0a1628 50%, #06101c 100%);
    transition: opacity 0.5s, visibility 0.5s;
  }
  .screen.hidden { opacity: 0; visibility: hidden; pointer-events: none; }

  .logo {
    font-family: 'Orbitron', sans-serif;
    font-size: clamp(2.4rem, 6vw, 4.2rem);
    font-weight: 700;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    background: linear-gradient(180deg, #ffffff 0%, #7ec8ff 45%, #2a7fd4 100%);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
    text-shadow: 0 0 40px rgba(80,160,255,0.35);
    margin-bottom: 0.2rem;
  }
  .tagline {
    font-size: 1rem;
    letter-spacing: 0.45em;
    text-transform: uppercase;
    color: #6a9fc4;
    margin-bottom: 2.5rem;
  }

  .panel {
    background: rgba(8, 18, 36, 0.82);
    border: 1px solid rgba(100, 170, 255, 0.22);
    border-radius: 12px;
    padding: 1.6rem 1.8rem;
    backdrop-filter: blur(14px);
    box-shadow: 0 20px 60px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.05);
    max-width: 920px;
    width: min(94vw, 920px);
  }
  .panel h2 {
    font-family: 'Orbitron', sans-serif;
    font-size: 0.85rem;
    letter-spacing: 0.25em;
    color: #7ec8ff;
    margin-bottom: 1rem;
    text-transform: uppercase;
  }

  .fleet {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: 0.9rem;
    margin-bottom: 1.4rem;
  }
  @media (max-width: 720px) {
    .fleet { grid-template-columns: 1fr; }
  }

  .aircraft-card {
    position: relative;
    padding: 1.1rem 1rem 1rem;
    border-radius: 10px;
    border: 1px solid rgba(100,160,255,0.18);
    background: linear-gradient(160deg, rgba(20,40,70,0.7), rgba(10,20,40,0.85));
    cursor: pointer;
    transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
  }
  .aircraft-card:hover {
    border-color: rgba(120,190,255,0.55);
    transform: translateY(-2px);
  }
  .aircraft-card.selected {
    border-color: #4db8ff;
    box-shadow: 0 0 0 1px rgba(77,184,255,0.4), 0 10px 30px rgba(40,120,220,0.25);
    background: linear-gradient(160deg, rgba(25,60,110,0.85), rgba(12,28,55,0.9));
  }
  .aircraft-card .name {
    font-family: 'Orbitron', sans-serif;
    font-size: 0.95rem;
    letter-spacing: 0.08em;
    margin-bottom: 0.2rem;
  }
  .aircraft-card .role {
    font-size: 0.78rem;
    color: #7aa0c0;
    margin-bottom: 0.75rem;
    text-transform: uppercase;
    letter-spacing: 0.12em;
  }
  .stat-row {
    display: flex; justify-content: space-between; align-items: center;
    font-size: 0.78rem; margin: 0.28rem 0; color: #a8c8e0;
  }
  .bar {
    width: 88px; height: 5px; border-radius: 3px;
    background: rgba(255,255,255,0.08); overflow: hidden;
  }
  .bar > i {
    display: block; height: 100%; border-radius: 3px;
    background: linear-gradient(90deg, #2a8fd4, #6ed0ff);
  }

  .modes {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: 0.9rem;
    margin-bottom: 1.4rem;
  }
  @media (max-width: 560px) { .modes { grid-template-columns: 1fr; } }

  .mode-card {
    padding: 1.15rem 1.1rem;
    border-radius: 10px;
    border: 1px solid rgba(100,160,255,0.18);
    background: linear-gradient(160deg, rgba(18,36,62,0.75), rgba(8,16,30,0.85));
    cursor: pointer;
    transition: border-color 0.2s, transform 0.2s, box-shadow 0.2s;
  }
  .mode-card:hover { border-color: rgba(120,190,255,0.5); transform: translateY(-2px); }
  .mode-card.selected {
    border-color: #ff9a4d;
    box-shadow: 0 0 0 1px rgba(255,154,77,0.35), 0 10px 28px rgba(180,80,20,0.2);
  }
  .mode-card .m-title {
    font-family: 'Orbitron', sans-serif;
    font-size: 0.92rem;
    letter-spacing: 0.1em;
    margin-bottom: 0.35rem;
  }
  .mode-card .m-desc {
    font-size: 0.82rem;
    color: #8aacc8;
    line-height: 1.35;
  }

  .btn-row {
    display: flex; gap: 0.8rem; justify-content: center; flex-wrap: wrap;
  }
  .btn {
    font-family: 'Orbitron', sans-serif;
    font-size: 0.82rem;
    letter-spacing: 0.18em;
    text-transform: uppercase;
    padding: 0.85rem 1.8rem;
    border-radius: 8px;
    border: 1px solid rgba(100,180,255,0.45);
    background: linear-gradient(180deg, #1a5a9a, #0d3a6a);
    color: #eaf6ff;
    cursor: pointer;
    transition: transform 0.15s, box-shadow 0.15s, filter 0.15s;
  }
  .btn:hover {
    transform: translateY(-1px);
    filter: brightness(1.12);
    box-shadow: 0 8px 24px rgba(40,120,220,0.35);
  }
  .btn.primary {
    background: linear-gradient(180deg, #2b8fd8, #1560a8);
    border-color: rgba(140,210,255,0.65);
  }
  .btn.ghost {
    background: transparent;
    border-color: rgba(120,160,200,0.35);
  }

  .controls-hint {
    margin-top: 1.2rem;
    font-size: 0.78rem;
    color: #5a809a;
    text-align: center;
    line-height: 1.55;
  }
  .controls-hint kbd {
    display: inline-block;
    padding: 0.1rem 0.4rem;
    margin: 0 0.1rem;
    border-radius: 3px;
    border: 1px solid rgba(100,140,180,0.35);
    background: rgba(20,40,70,0.6);
    font-family: 'Rajdhani', sans-serif;
    font-size: 0.75rem;
    color: #9ec4e0;
  }

  /* ── HUD ── */
  #hud {
    position: fixed; inset: 0;
    z-index: 10;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.4s;
  }
  #hud.active { opacity: 1; }

  .hud-corner {
    position: absolute;
    font-family: 'Orbitron', sans-serif;
    font-size: 0.68rem;
    letter-spacing: 0.12em;
    color: rgba(150, 210, 255, 0.75);
    text-transform: uppercase;
  }
  .hud-corner.tl { top: 16px; left: 18px; }
  .hud-corner.tr { top: 16px; right: 18px; text-align: right; }
  .hud-corner.bl { bottom: 16px; left: 18px; }
  .hud-corner.br { bottom: 16px; right: 18px; text-align: right; }

  .hud-value {
    font-size: 1.55rem;
    font-weight: 600;
    color: #dff2ff;
    letter-spacing: 0.04em;
    line-height: 1.15;
    text-shadow: 0 0 12px rgba(80,160,255,0.45);
  }
  .hud-unit {
    font-size: 0.65rem;
    color: #6a9ab8;
    letter-spacing: 0.15em;
  }
  .hud-label {
    font-size: 0.62rem;
    color: #5a88a8;
    margin-bottom: 0.15rem;
  }

  #horizon-wrap {
    position: absolute;
    left: 50%; top: 50%;
    width: 220px; height: 220px;
    transform: translate(-50%, -50%);
    pointer-events: none;
  }
  #horizon {
    width: 100%; height: 100%;
  }

  #speed-tape, #alt-tape {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 64px;
    height: 220px;
    border: 1px solid rgba(100,180,255,0.2);
    background: rgba(4, 12, 24, 0.45);
    backdrop-filter: blur(4px);
    border-radius: 4px;
    overflow: hidden;
  }
  #speed-tape { left: calc(50% - 200px); }
  #alt-tape { right: calc(50% - 200px); }

  .tape-center {
    position: absolute;
    left: 0; right: 0; top: 50%;
    height: 28px;
    margin-top: -14px;
    border-top: 1px solid rgba(120,200,255,0.5);
    border-bottom: 1px solid rgba(120,200,255,0.5);
    background: rgba(20,80,140,0.35);
    display: flex; align-items: center; justify-content: center;
    font-family: 'Orbitron', sans-serif;
    font-size: 0.95rem;
    color: #fff;
    z-index: 2;
  }
  .tape-marks {
    position: absolute; left: 0; right: 0; top: 0;
    will-change: transform;
  }
  .tape-mark {
    height: 28px;
    display: flex; align-items: center; justify-content: flex-end;
    padding-right: 8px;
    font-size: 0.72rem;
    color: rgba(160,210,255,0.7);
    font-family: 'Orbitron', sans-serif;
    border-bottom: 1px solid rgba(80,140,200,0.12);
  }
  #alt-tape .tape-mark { justify-content: flex-start; padding-left: 8px; }

  #throttle-bar {
    position: absolute;
    left: 24px;
    top: 50%;
    transform: translateY(-50%);
    width: 14px;
    height: 180px;
    border: 1px solid rgba(100,180,255,0.25);
    border-radius: 3px;
    background: rgba(4,12,24,0.5);
    overflow: hidden;
  }
  #throttle-fill {
    position: absolute;
    left: 0; right: 0; bottom: 0;
    background: linear-gradient(0deg, #1a6ab0, #4ec8ff);
    transition: height 0.08s linear;
  }
  #throttle-label {
    position: absolute;
    left: 24px;
    top: calc(50% + 100px);
    font-family: 'Orbitron', sans-serif;
    font-size: 0.6rem;
    color: #6a9ab8;
    letter-spacing: 0.1em;
  }

  #aoa-bar {
    position: absolute;
    right: 24px;
    top: 50%;
    transform: translateY(-50%);
    width: 14px;
    height: 180px;
    border: 1px solid rgba(100,180,255,0.25);
    border-radius: 3px;
    background: rgba(4,12,24,0.5);
    overflow: hidden;
  }
  #aoa-fill {
    position: absolute;
    left: 0; right: 0;
    height: 4px;
    background: #7ee0ff;
    box-shadow: 0 0 8px rgba(100,220,255,0.6);
  }
  #aoa-stall {
    position: absolute;
    left: 0; right: 0;
    height: 2px;
    background: #ff4466;
    opacity: 0.7;
  }
  #aoa-label {
    position: absolute;
    right: 24px;
    top: calc(50% + 100px);
    font-family: 'Orbitron', sans-serif;
    font-size: 0.6rem;
    color: #6a9ab8;
    letter-spacing: 0.1em;
    text-align: right;
  }

  #warn {
    position: absolute;
    top: 22%;
    left: 50%;
    transform: translateX(-50%);
    font-family: 'Orbitron', sans-serif;
    font-size: 0.95rem;
    letter-spacing: 0.28em;
    color: #ff4060;
    text-shadow: 0 0 16px rgba(255,40,60,0.6);
    opacity: 0;
    transition: opacity 0.15s;
    text-align: center;
  }
  #warn.show { opacity: 1; animation: pulse 0.6s ease-in-out infinite alternate; }
  @keyframes pulse { from { opacity: 0.55; } to { opacity: 1; } }

  #mission-banner {
    position: absolute;
    top: 12%;
    left: 50%;
    transform: translateX(-50%);
    text-align: center;
    pointer-events: none;
  }
  #mission-banner .mb-title {
    font-family: 'Orbitron', sans-serif;
    font-size: 0.78rem;
    letter-spacing: 0.3em;
    color: #7ec8ff;
    margin-bottom: 0.3rem;
  }
  #mission-banner .mb-obj {
    font-size: 0.95rem;
    color: #c8e4ff;
    letter-spacing: 0.04em;
  }

  #g-meter {
    position: absolute;
    bottom: 90px;
    left: 50%;
    transform: translateX(-50%);
    font-family: 'Orbitron', sans-serif;
    font-size: 0.75rem;
    letter-spacing: 0.15em;
    color: #8ab8d8;
  }
  #g-meter span { color: #dff2ff; font-size: 1.05rem; }

  #crosshair {
    position: absolute;
    left: 50%; top: 50%;
    width: 28px; height: 28px;
    margin: -14px 0 0 -14px;
    border: 1px solid rgba(150,210,255,0.35);
    border-radius: 50%;
  }
  #crosshair::before, #crosshair::after {
    content: '';
    position: absolute;
    background: rgba(150,210,255,0.45);
  }
  #crosshair::before {
    left: 50%; top: 3px; bottom: 3px; width: 1px; margin-left: -0.5px;
  }
  #crosshair::after {
    top: 50%; left: 3px; right: 3px; height: 1px; margin-top: -0.5px;
  }

  #minimap {
    position: absolute;
    bottom: 18px;
    right: 18px;
    width: 140px;
    height: 140px;
    border-radius: 8px;
    border: 1px solid rgba(100,180,255,0.3);
    background: rgba(4,14,28,0.65);
    overflow: hidden;
  }
  #minimap canvas { width: 100%; height: 100%; display: block; }

  #pause-overlay {
    position: fixed; inset: 0;
    z-index: 30;
    display: none;
    align-items: center;
    justify-content: center;
    background: rgba(2,8,16,0.72);
    backdrop-filter: blur(6px);
  }
  #pause-overlay.show { display: flex; }
  #pause-overlay .panel { text-align: center; min-width: 320px; }
  #pause-overlay h2 { margin-bottom: 1.2rem; }

  #flash {
    position: fixed; inset: 0;
    z-index: 25;
    pointer-events: none;
    background: #fff;
    opacity: 0;
    transition: opacity 0.08s;
  }

  #loading {
    position: fixed; inset: 0; z-index: 50;
    display: flex; flex-direction: column;
    align-items: center; justify-content: center;
    background: #030810;
    transition: opacity 0.6s;
  }
  #loading.hide { opacity: 0; pointer-events: none; }
  .load-bar {
    width: 220px; height: 3px;
    background: rgba(255,255,255,0.08);
    border-radius: 2px;
    overflow: hidden;
    margin-top: 1.2rem;
  }
  .load-bar > i {
    display: block; height: 100%; width: 0%;
    background: linear-gradient(90deg, #2a8fd4, #7ed0ff);
    transition: width 0.3s;
  }
</style>
</head>
<body>
<div id="loading">
  <div class="logo" style="font-size:2rem">APEX AERO</div>
  <div class="tagline" style="margin-bottom:0">INITIALIZING SYSTEMS</div>
  <div class="load-bar"><i id="load-fill"></i></div>
</div>

<div id="canvas-wrap"></div>
<div id="flash"></div>

<!-- Main Menu -->
<div id="menu" class="screen">
  <div class="logo">Apex Aero</div>
  <div class="tagline">Precision Flight Dynamics</div>
  <div class="panel">
    <h2>Select Aircraft</h2>
    <div class="fleet" id="fleet"></div>
    <h2>Mission Profile</h2>
    <div class="modes">
      <div class="mode-card selected" data-mode="takeoff" id="mode-takeoff">
        <div class="m-title">✦ Takeoff & Landing</div>
        <div class="m-desc">Precision runway ops. Manage lift, airspeed, and stall margins. Land within the zone.</div>
      </div>
      <div class="mode-card" data-mode="dogfight" id="mode-dogfight">
        <div class="m-title">⚡ Dogfight Arena</div>
        <div class="m-desc">High-G combat. Track and destroy the unmanned target drone before it escapes.</div>
      </div>
    </div>
    <div class="btn-row">
      <button class="btn primary" id="btn-start">Engage</button>
    </div>
    <div class="controls-hint">
      <kbd>W</kbd><kbd>S</kbd> pitch · <kbd>A</kbd><kbd>D</kbd> roll · <kbd>Q</kbd><kbd>E</kbd> yaw<br>
      <kbd>Shift</kbd>/<kbd>Ctrl</kbd> or <kbd>↑</kbd>/<kbd>↓</kbd> throttle · <kbd>Space</kbd> fire · <kbd>C</kbd> camera · <kbd>P</kbd>/<kbd>Esc</kbd> pause · pointer lock mouse look
    </div>
  </div>
</div>

<!-- Pause -->
<div id="pause-overlay">
  <div class="panel">
    <h2>Systems Hold</h2>
    <div class="btn-row" style="flex-direction:column;align-items:stretch">
      <button class="btn primary" id="btn-resume">Resume</button>
      <button class="btn ghost" id="btn-restart">Restart Mission</button>
      <button class="btn ghost" id="btn-menu">Main Menu</button>
    </div>
  </div>
</div>

<!-- HUD -->
<div id="hud">
  <div class="hud-corner tl">
    <div class="hud-label">Aircraft</div>
    <div class="hud-value" id="h-name" style="font-size:1rem">—</div>
    <div class="hud-unit" id="h-mode">—</div>
  </div>
  <div class="hud-corner tr">
    <div class="hud-label">Heading</div>
    <div class="hud-value" id="h-hdg">000</div>
    <div class="hud-unit">DEG</div>
  </div>
  <div class="hud-corner bl">
    <div class="hud-label">Vertical Speed</div>
    <div class="hud-value" id="h-vs" style="font-size:1.15rem">0</div>
    <div class="hud-unit">FT/MIN</div>
  </div>
  <div class="hud-corner br">
    <div class="hud-label">Throttle · RPM</div>
    <div class="hud-value" id="h-thr" style="font-size:1.15rem">0%</div>
    <div class="hud-unit" id="h-rpm">0 RPM</div>
  </div>

  <div id="speed-tape">
    <div class="tape-marks" id="spd-marks"></div>
    <div class="tape-center" id="spd-readout">0</div>
  </div>
  <div id="alt-tape">
    <div class="tape-marks" id="alt-marks"></div>
    <div class="tape-center" id="alt-readout">0</div>
  </div>

  <div id="horizon-wrap">
    <svg id="horizon" viewBox="0 0 220 220">
      <defs>
        <clipPath id="hclip"><circle cx="110" cy="110" r="78"/></clipPath>
      </defs>
      <g id="h-ball" clip-path="url(#hclip)">
        <rect id="h-sky" x="-50" y="-100" width="320" height="210" fill="rgba(40,100,180,0.35)"/>
        <rect id="h-gnd" x="-50" y="110" width="320" height="210" fill="rgba(80,60,30,0.4)"/>
        <line x1="-50" y1="110" x2="270" y2="110" stroke="rgba(220,200,140,0.7)" stroke-width="2"/>
        <g id="h-pitch"></g>
      </g>
      <circle cx="110" cy="110" r="78" fill="none" stroke="rgba(120,190,255,0.35)" stroke-width="1.5"/>
      <polygon points="110,108 105,118 115,118" fill="#7ee0ff"/>
      <line x1="70" y1="110" x2="95" y2="110" stroke="#7ee0ff" stroke-width="2"/>
      <line x1="125" y1="110" x2="150" y2="110" stroke="#7ee0ff" stroke-width="2"/>
    </svg>
  </div>

  <div id="throttle-bar"><div id="throttle-fill" style="height:0%"></div></div>
  <div id="throttle-label">THR</div>
  <div id="aoa-bar">
    <div id="aoa-stall" style="bottom:72%"></div>
    <div id="aoa-fill" style="bottom:40%"></div>
  </div>
  <div id="aoa-label">AoA</div>

  <div id="crosshair"></div>
  <div id="warn"></div>
  <div id="mission-banner">
    <div class="mb-title" id="mb-title">MISSION</div>
    <div class="mb-obj" id="mb-obj">—</div>
  </div>
  <div id="g-meter">LOAD <span id="h-g">1.0</span> G</div>
  <div id="minimap"><canvas id="mm-canvas" width="280" height="280"></canvas></div>
</div>

<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
<script>
// ═══════════════════════════════════════════════════════════
// Constants & Aircraft Catalog
// ═══════════════════════════════════════════════════════════
const FT = 3.28084;
const M_PER_FT = 1 / FT;
const SEA_LEVEL_DENSITY = 1.225;
const GRAVITY = 9.81;
const WORLD_SIZE = 12000;
const TERRAIN_RES = 192;

const FLEET = [
  {
    id: 'falcon',
    name: 'XF-14 FALCON',
    role: 'Air Superiority',
    mass: 9200,
    wingArea: 52.5,
    wingspan: 14.2,
    cl0: 0.18,
    clAlpha: 5.2,
    clMax: 1.45,
    cd0: 0.022,
    kInduced: 0.045,
    maxThrust: 145000,
    throttleLag: 0.55,
    pitchAuth: 1.35,
    rollAuth: 2.1,
    yawAuth: 0.85,
    pitchDamp: 2.8,
    rollDamp: 3.2,
    yawDamp: 2.4,
    maxSpeed: 720,
    stallAoA: 0.28,
    color: 0x3a7ab8,
    accent: 0xc0d8f0,
    style: 'fighter'
  },
  {
    id: 'condor',
    name: 'C-80 CONDOR',
    role: 'Heavy Transport',
    mass: 42000,
    wingArea: 168,
    wingspan: 42,
    cl0: 0.32,
    clAlpha: 4.6,
    clMax: 1.65,
    cd0: 0.038,
    kInduced: 0.052,
    maxThrust: 280000,
    throttleLag: 1.4,
    pitchAuth: 0.55,
    rollAuth: 0.7,
    yawAuth: 0.45,
    pitchDamp: 3.6,
    rollDamp: 4.0,
    yawDamp: 3.2,
    maxSpeed: 280,
    stallAoA: 0.32,
    color: 0x6a7a8a,
    accent: 0xd4c48a,
    style: 'transport'
  },
  {
    id: 'viper',
    name: 'AV-7 VIPER',
    role: 'Agility Interceptor',
    mass: 6100,
    wingArea: 38,
    wingspan: 9.8,
    cl0: 0.12,
    clAlpha: 5.8,
    clMax: 1.55,
    cd0: 0.019,
    kInduced: 0.048,
    maxThrust: 118000,
    throttleLag: 0.32,
    pitchAuth: 1.85,
    rollAuth: 2.8,
    yawAuth: 1.15,
    pitchDamp: 2.2,
    rollDamp: 2.6,
    yawDamp: 2.0,
    maxSpeed: 780,
    stallAoA: 0.30,
    color: 0xb83828,
    accent: 0x202028,
    style: 'interceptor'
  }
];

// ═══════════════════════════════════════════════════════════
// State
// ═══════════════════════════════════════════════════════════
const state = {
  phase: 'menu', // menu | flying | paused | ended
  mode: 'takeoff',
  aircraftId: 'falcon',
  pointerLocked: false,
  keys: Object.create(null),
  mouseDX: 0,
  mouseDY: 0,
  camMode: 0, // 0 chase, 1 cockpit, 2 free
  mission: {
    status: 'idle',
    message: '',
    landed: false,
    airborne: false,
    takeoffDist: 0,
    shots: 0,
    hits: 0,
    time: 0
  }
};

// ═══════════════════════════════════════════════════════════
// Audio Engine (Web Audio API)
// ═══════════════════════════════════════════════════════════
class AudioFX {
  constructor() {
    this.ctx = null;
    this.master = null;
    this.engine = null;
    this.wind = null;
    this.started = false;
  }

  async init() {
    if (this.started) return;
    const AC = window.AudioContext || window.webkitAudioContext;
    this.ctx = new AC();
    this.master = this.ctx.createGain();
    this.master.gain.value = 0.35;
    this.master.connect(this.ctx.destination);

    // Engine: layered oscillators through filter
    this.engineGain = this.ctx.createGain();
    this.engineGain.gain.value = 0;
    this.engineFilter = this.ctx.createBiquadFilter();
    this.engineFilter.type = 'lowpass';
    this.engineFilter.frequency.value = 400;
    this.engineFilter.Q.value = 0.7;
    this.engineGain.connect(this.engineFilter);
    this.engineFilter.connect(this.master);

    this.osc1 = this.ctx.createOscillator();
    this.osc1.type = 'sawtooth';
    this.osc1.frequency.value = 55;
    this.osc1G = this.ctx.createGain();
    this.osc1G.gain.value = 0.35;
    this.osc1.connect(this.osc1G);
    this.osc1G.connect(this.engineGain);

    this.osc2 = this.ctx.createOscillator();
    this.osc2.type = 'square';
    this.osc2.frequency.value = 110;
    this.osc2G = this.ctx.createGain();
    this.osc2G.gain.value = 0.12;
    this.osc2.connect(this.osc2G);
    this.osc2G.connect(this.engineGain);

    // Noise for turbofan hiss / wind
    const bufLen = this.ctx.sampleRate * 2;
    const noiseBuf = this.ctx.createBuffer(1, bufLen, this.ctx.sampleRate);
    const data = noiseBuf.getChannelData(0);
    for (let i = 0; i < bufLen; i++) data[i] = Math.random() * 2 - 1;

    this.noiseSrc = this.ctx.createBufferSource();
    this.noiseSrc.buffer = noiseBuf;
    this.noiseSrc.loop = true;
    this.noiseFilter = this.ctx.createBiquadFilter();
    this.noiseFilter.type = 'bandpass';
    this.noiseFilter.frequency.value = 800;
    this.noiseFilter.Q.value = 0.5;
    this.noiseGain = this.ctx.createGain();
    this.noiseGain.gain.value = 0;
    this.noiseSrc.connect(this.noiseFilter);
    this.noiseFilter.connect(this.noiseGain);
    this.noiseGain.connect(this.master);

    // Wind ambience
    this.windSrc = this.ctx.createBufferSource();
    this.windSrc.buffer = noiseBuf;
    this.windSrc.loop = true;
    this.windFilter = this.ctx.createBiquadFilter();
    this.windFilter.type = 'highpass';
    this.windFilter.frequency.value = 400;
    this.windGain = this.ctx.createGain();
    this.windGain.gain.value = 0.02;
    this.windSrc.connect(this.windFilter);
    this.windFilter.connect(this.windGain);
    this.windGain.connect(this.master);

    this.osc1.start();
    this.osc2.start();
    this.noiseSrc.start();
    this.windSrc.start();
    this.started = true;
  }

  resume() {
    if (this.ctx && this.ctx.state === 'suspended') this.ctx.resume();
  }

  update(rpmNorm, thr, speed, aoa, altitude) {
    if (!this.started) return;
    const t = this.ctx.currentTime;
    const engineVol = 0.08 + thr * 0.55 + rpmNorm * 0.2;
    const densScale = Math.exp(-altitude / 10000);
    this.engineGain.gain.setTargetAtTime(engineVol * densScale, t, 0.08);
    const baseF = 40 + rpmNorm * 90 + thr * 40;
    this.osc1.frequency.setTargetAtTime(baseF, t, 0.05);
    this.osc2.frequency.setTargetAtTime(baseF * 2.02, t, 0.05);
    this.engineFilter.frequency.setTargetAtTime(280 + thr * 1400 + rpmNorm * 600, t, 0.08);

    const rush = Math.min(1, speed / 180);
    const aoaBoost = Math.min(1, Math.abs(aoa) * 3);
    this.noiseGain.gain.setTargetAtTime((0.02 + rush * 0.22 + aoaBoost * 0.08) * densScale, t, 0.1);
    this.noiseFilter.frequency.setTargetAtTime(500 + rush * 2500 + aoaBoost * 800, t, 0.1);
    this.windGain.gain.setTargetAtTime(0.015 + rush * 0.08 + altitude / 40000, t, 0.15);
  }

  boom() {
    if (!this.started) return;
    const t = this.ctx.currentTime;
    const o = this.ctx.createOscillator();
    const g = this.ctx.createGain();
    o.type = 'sine';
    o.frequency.setValueAtTime(180, t);
    o.frequency.exponentialRampToValueAtTime(40, t + 0.35);
    g.gain.setValueAtTime(0.5, t);
    g.gain.exponentialRampToValueAtTime(0.001, t + 0.4);
    o.connect(g); g.connect(this.master);
    o.start(t); o.stop(t + 0.45);
  }

  gun() {
    if (!this.started) return;
    const t = this.ctx.currentTime;
    const bufLen = Math.floor(this.ctx.sampleRate * 0.06);
    const buf = this.ctx.createBuffer(1, bufLen, this.ctx.sampleRate);
    const d = buf.getChannelData(0);
    for (let i = 0; i < bufLen; i++) d[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufLen * 0.25));
    const src = this.ctx.createBufferSource();
    src.buffer = buf;
    const f = this.ctx.createBiquadFilter();
    f.type = 'highpass'; f.frequency.value = 900;
    const g = this.ctx.createGain();
    g.gain.value = 0.35;
    src.connect(f); f.connect(g); g.connect(this.master);
    src.start();
  }

  stallWarn(on) {
    if (!this.started) return;
    if (on && !this._stallOsc) {
      const t = this.ctx.currentTime;
      this._stallOsc = this.ctx.createOscillator();
      this._stallG = this.ctx.createGain();
      this._stallOsc.type = 'square';
      this._stallOsc.frequency.value = 880;
      this._stallG.gain.value = 0.04;
      this._stallOsc.connect(this._stallG);
      this._stallG.connect(this.master);
      this._stallOsc.start();
    } else if (!on && this._stallOsc) {
      try { this._stallOsc.stop(); } catch (_) {}
      this._stallOsc = null;
    }
  }
}

const audio = new AudioFX();

// ═══════════════════════════════════════════════════════════
// Terrain
// ═══════════════════════════════════════════════════════════
function hash2(x, z) {
  let n = Math.sin(x * 127.1 + z * 311.7) * 43758.5453;
  return n - Math.floor(n);
}
function smoothNoise(x, z) {
  const ix = Math.floor(x), iz = Math.floor(z);
  const fx = x - ix, fz = z - iz;
  const ux = fx * fx * (3 - 2 * fx);
  const uz = fz * fz * (3 - 2 * fz);
  const a = hash2(ix, iz);
  const b = hash2(ix + 1, iz);
  const c = hash2(ix, iz + 1);
  const d = hash2(ix + 1, iz + 1);
  return a + (b - a) * ux + (c - a) * uz + (a - b - c + d) * ux * uz;
}
function fbm(x, z) {
  let v = 0, a = 1, f = 1, s = 0;
  for (let i = 0; i < 6; i++) {
    v += a * smoothNoise(x * f, z * f);
    s += a;
    a *= 0.5;
    f *= 2.05;
  }
  return v / s;
}

function terrainHeight(x, z) {
  // Strong flat corridor for the active runway (x≈0, z from -500..2200)
  const rx = Math.abs(x) / 90;
  const rz = (z < -500) ? (-500 - z) / 400
           : (z > 2200) ? (z - 2200) / 400
           : 0;
  const runwayInflu = Math.exp(-(rx * rx * 2.2 + rz * rz * 2.2));
  const continent = fbm(x * 0.00018, z * 0.00018);
  const ridges = Math.abs(fbm(x * 0.00055 + 40, z * 0.00055 - 12) - 0.5) * 2;
  const hills = fbm(x * 0.0012, z * 0.0012);
  let h = continent * 420 + ridges * 380 + hills * 90 - 80;
  // Soft coast drop-off
  const edge = Math.max(Math.abs(x), Math.abs(z)) / (WORLD_SIZE * 0.48);
  if (edge > 0.75) h -= (edge - 0.75) * 900;
  // Flatten runway to a constant pad for reliable takeoff / landing
  h = h * (1 - runwayInflu) + 2 * runwayInflu;
  return Math.max(h, -40);
}

function buildTerrain(scene) {
  const geo = new THREE.PlaneGeometry(WORLD_SIZE, WORLD_SIZE, TERRAIN_RES, TERRAIN_RES);
  geo.rotateX(-Math.PI / 2);
  const pos = geo.attributes.position;
  const colors = new Float32Array(pos.count * 3);
  const c = new THREE.Color();

  for (let i = 0; i < pos.count; i++) {
    const x = pos.getX(i);
    const z = pos.getZ(i);
    const y = terrainHeight(x, z);
    pos.setY(i, y);

    if (y < 2) {
      c.setHSL(0.55, 0.35, 0.18 + Math.max(0, y + 40) / 200);
    } else if (y < 40) {
      // grass / runway green-brown
      const t = (y - 2) / 38;
      c.setRGB(0.18 + t * 0.08, 0.28 + t * 0.12, 0.12 + t * 0.04);
      // runway strip tint
      if (Math.abs(x) < 45 && z > -400 && z < 2200) {
        c.setRGB(0.22, 0.22, 0.24);
      }
    } else if (y < 220) {
      const t = (y - 40) / 180;
      c.setRGB(0.25 + t * 0.15, 0.32 - t * 0.1, 0.14);
    } else if (y < 420) {
      const t = (y - 220) / 200;
      c.setRGB(0.38 + t * 0.2, 0.32 + t * 0.15, 0.26 + t * 0.15);
    } else {
      c.setRGB(0.82, 0.85, 0.9);
    }
    colors[i * 3] = c.r;
    colors[i * 3 + 1] = c.g;
    colors[i * 3 + 2] = c.b;
  }
  geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
  geo.computeVertexNormals();

  const mat = new THREE.MeshStandardMaterial({
    vertexColors: true,
    roughness: 0.92,
    metalness: 0.05,
    flatShading: false
  });
  const mesh = new THREE.Mesh(geo, mat);
  mesh.receiveShadow = true;
  mesh.castShadow = false;
  scene.add(mesh);

  // Water plane
  const waterGeo = new THREE.PlaneGeometry(WORLD_SIZE * 1.2, WORLD_SIZE * 1.2);
  waterGeo.rotateX(-Math.PI / 2);
  const waterMat = new THREE.MeshStandardMaterial({
    color: 0x0a2a48,
    roughness: 0.15,
    metalness: 0.65,
    transparent: true,
    opacity: 0.88
  });
  const water = new THREE.Mesh(waterGeo, waterMat);
  water.position.y = 0.5;
  scene.add(water);

  // Runway markings
  const rMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1e, roughness: 0.85 });
  const runway = new THREE.Mesh(new THREE.BoxGeometry(70, 0.4, 2400), rMat);
  runway.position.set(0, 2.3, 800);
  runway.receiveShadow = true;
  scene.add(runway);

  const lineMat = new THREE.MeshBasicMaterial({ color: 0xf0e8c0 });
  for (let i = 0; i < 28; i++) {
    const line = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.45, 28), lineMat);
    line.position.set(0, 2.55, -200 + i * 80);
    scene.add(line);
  }
  // Threshold chevrons
  const thrMat = new THREE.MeshBasicMaterial({ color: 0xe8e0a0 });
  for (let s = -1; s <= 1; s += 2) {
    for (let k = 0; k < 6; k++) {
      const ch = new THREE.Mesh(new THREE.BoxGeometry(3, 0.42, 14), thrMat);
      ch.position.set(s * (8 + k * 4), 2.55, -280);
      scene.add(ch);
      const ch2 = ch.clone();
      ch2.position.z = 1900;
      scene.add(ch2);
    }
  }

  // Landing zone marker rings (mission)
  state._runwayStart = new THREE.Vector3(0, 3, -300);
  state._runwayEnd = new THREE.Vector3(0, 3, 1900);

  return mesh;
}

// Sparse trees / rocks as instanced meshes
function decorateTerrain(scene) {
  const treeGeo = new THREE.ConeGeometry(4, 16, 5);
  const treeMat = new THREE.MeshStandardMaterial({ color: 0x1a4a28, roughness: 0.9 });
  const trees = new THREE.InstancedMesh(treeGeo, treeMat, 400);
  const dummy = new THREE.Object3D();
  let n = 0;
  for (let i = 0; i < 1200 && n < 400; i++) {
    const x = (Math.random() - 0.5) * WORLD_SIZE * 0.85;
    const z = (Math.random() - 0.5) * WORLD_SIZE * 0.85;
    if (Math.abs(x) < 120 && z > -500 && z < 2300) continue;
    const y = terrainHeight(x, z);
    if (y < 8 || y > 280) continue;
    dummy.position.set(x, y + 6, z);
    const s = 0.6 + Math.random() * 1.4;
    dummy.scale.set(s, s * (0.8 + Math.random() * 0.6), s);
    dummy.rotation.y = Math.random() * Math.PI;
    dummy.updateMatrix();
    trees.setMatrixAt(n++, dummy.matrix);
  }
  trees.instanceMatrix.needsUpdate = true;
  trees.castShadow = true;
  scene.add(trees);
}

// ═══════════════════════════════════════════════════════════
// Procedural Aircraft Models
// ═══════════════════════════════════════════════════════════
function makeAircraftMesh(spec) {
  const root = new THREE.Group();
  const bodyMat = new THREE.MeshStandardMaterial({
    color: spec.color, metalness: 0.55, roughness: 0.35
  });
  const accentMat = new THREE.MeshStandardMaterial({
    color: spec.accent, metalness: 0.4, roughness: 0.45
  });
  const canopyMat = new THREE.MeshStandardMaterial({
    color: 0x88ccee, metalness: 0.9, roughness: 0.1, transparent: true, opacity: 0.55
  });
  const darkMat = new THREE.MeshStandardMaterial({ color: 0x111118, metalness: 0.6, roughness: 0.4 });

  if (spec.style === 'fighter') {
    // Fuselage
    const fuse = new THREE.Mesh(new THREE.CapsuleGeometry(0.7, 7.5, 4, 12), bodyMat);
    fuse.rotation.z = Math.PI / 2;
    fuse.scale.set(1, 1, 0.85);
    fuse.position.set(0, 0, 0);
    root.add(fuse);
    // Nose
    const nose = new THREE.Mesh(new THREE.ConeGeometry(0.55, 2.2, 10), bodyMat);
    nose.rotation.x = -Math.PI / 2;
    nose.position.set(0, 0, 4.6);
    root.add(nose);
    // Canopy
    const canopy = new THREE.Mesh(new THREE.SphereGeometry(0.7, 10, 8, 0, Math.PI * 2, 0, Math.PI / 2), canopyMat);
    canopy.scale.set(0.9, 0.7, 1.4);
    canopy.position.set(0, 0.55, 1.5);
    root.add(canopy);
    // Wings
    const wingShape = new THREE.Shape();
    wingShape.moveTo(0, 0);
    wingShape.lineTo(5.5, -1.2);
    wingShape.lineTo(5.5, -1.8);
    wingShape.lineTo(0, -2.6);
    wingShape.lineTo(0, 0);
    const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.12, bevelEnabled: false });
    const wingL = new THREE.Mesh(wingGeo, bodyMat);
    wingL.rotation.set(-0.05, 0, 0.08);
    wingL.position.set(0.4, -0.1, 0.5);
    root.add(wingL);
    const wingR = wingL.clone();
    wingR.scale.x = -1;
    wingR.position.x = -0.4;
    root.add(wingR);
    // Twin vertical tails
    for (const sx of [-1, 1]) {
      const vt = new THREE.Mesh(new THREE.BoxGeometry(0.1, 1.6, 1.4), accentMat);
      vt.position.set(sx * 1.1, 0.9, -3.2);
      vt.rotation.z = sx * 0.25;
      root.add(vt);
    }
    // Engines
    for (const sx of [-1, 1]) {
      const eng = new THREE.Mesh(new THREE.CylinderGeometry(0.45, 0.5, 2.5, 10), darkMat);
      eng.rotation.x = Math.PI / 2;
      eng.position.set(sx * 0.7, -0.15, -3.5);
      root.add(eng);
      const glow = new THREE.Mesh(
        new THREE.CircleGeometry(0.4, 12),
        new THREE.MeshBasicMaterial({ color: 0x44aaff, transparent: true, opacity: 0.7 })
      );
      glow.position.set(sx * 0.7, -0.15, -4.8);
      root.add(glow);
    }
    // Stabilizers
    const hstab = new THREE.Mesh(new THREE.BoxGeometry(4.2, 0.08, 1.1), bodyMat);
    hstab.position.set(0, 0.1, -3.6);
    root.add(hstab);
  } else if (spec.style === 'transport') {
    const fuse = new THREE.Mesh(new THREE.CapsuleGeometry(1.5, 14, 4, 14), bodyMat);
    fuse.rotation.z = Math.PI / 2;
    root.add(fuse);
    const nose = new THREE.Mesh(new THREE.SphereGeometry(1.4, 12, 10), bodyMat);
    nose.scale.set(1, 1, 1.4);
    nose.position.set(0, 0, 7.5);
    root.add(nose);
    const cockpit = new THREE.Mesh(new THREE.BoxGeometry(2.2, 1.2, 2.5), canopyMat);
    cockpit.position.set(0, 1.0, 6.2);
    root.add(cockpit);
    // High wings
    const wing = new THREE.Mesh(new THREE.BoxGeometry(28, 0.22, 4.2), bodyMat);
    wing.position.set(0, 1.4, 0.5);
    wing.rotation.z = 0.03;
    root.add(wing);
    // Engines under wing
    for (const sx of [-8, -3.5, 3.5, 8]) {
      const eng = new THREE.Mesh(new THREE.CylinderGeometry(0.55, 0.6, 2.8, 10), darkMat);
      eng.rotation.x = Math.PI / 2;
      eng.position.set(sx, 0.5, 1.2);
      root.add(eng);
      const glow = new THREE.Mesh(
        new THREE.CircleGeometry(0.48, 12),
        new THREE.MeshBasicMaterial({ color: 0x66ccff, transparent: true, opacity: 0.55 })
      );
      glow.position.set(sx, 0.5, -0.2);
      root.add(glow);
    }
    // Tail
    const hstab = new THREE.Mesh(new THREE.BoxGeometry(10, 0.15, 2.5), bodyMat);
    hstab.position.set(0, 0.5, -7.5);
    root.add(hstab);
    const vstab = new THREE.Mesh(new THREE.BoxGeometry(0.2, 4.5, 3), accentMat);
    vstab.position.set(0, 2.6, -7.2);
    root.add(vstab);
  } else {
    // Interceptor — delta / canard
    const fuse = new THREE.Mesh(new THREE.CapsuleGeometry(0.55, 6.5, 4, 12), bodyMat);
    fuse.rotation.z = Math.PI / 2;
    root.add(fuse);
    const nose = new THREE.Mesh(new THREE.ConeGeometry(0.45, 2.5, 8), bodyMat);
    nose.rotation.x = -Math.PI / 2;
    nose.position.set(0, 0, 4.2);
    root.add(nose);
    const canopy = new THREE.Mesh(new THREE.SphereGeometry(0.55, 10, 8, 0, Math.PI * 2, 0, Math.PI / 2), canopyMat);
    canopy.scale.set(0.8, 0.55, 1.3);
    canopy.position.set(0, 0.42, 1.2);
    root.add(canopy);
    // Delta wing
    const dShape = new THREE.Shape();
    dShape.moveTo(0, 1.5);
    dShape.lineTo(5.2, -3.8);
    dShape.lineTo(0.3, -3.8);
    dShape.lineTo(0, 1.5);
    const dGeo = new THREE.ExtrudeGeometry(dShape, { depth: 0.1, bevelEnabled: false });
    const dL = new THREE.Mesh(dGeo, bodyMat);
    dL.position.set(0.2, -0.05, 0.8);
    root.add(dL);
    const dR = dL.clone();
    dR.scale.x = -1;
    dR.position.x = -0.2;
    root.add(dR);
    // Canards
    const canard = new THREE.Mesh(new THREE.BoxGeometry(2.4, 0.06, 0.7), accentMat);
    canard.position.set(0, 0.1, 2.8);
    root.add(canard);
    // Single vertical tail
    const vt = new THREE.Mesh(new THREE.BoxGeometry(0.12, 1.9, 1.6), accentMat);
    vt.position.set(0, 1.0, -2.8);
    root.add(vt);
    // Engine
    const eng = new THREE.Mesh(new THREE.CylinderGeometry(0.5, 0.55, 2.2, 10), darkMat);
    eng.rotation.x = Math.PI / 2;
    eng.position.set(0, -0.1, -3.2);
    root.add(eng);
    const glow = new THREE.Mesh(
      new THREE.CircleGeometry(0.45, 12),
      new THREE.MeshBasicMaterial({ color: 0xff6633, transparent: true, opacity: 0.75 })
    );
    glow.position.set(0, -0.1, -4.35);
    root.add(glow);
  }

  root.traverse(o => {
    if (o.isMesh) {
      o.castShadow = true;
      o.receiveShadow = true;
    }
  });

  // Scale to feel right in world units
  const scale = spec.style === 'transport' ? 1.1 : 1.0;
  root.scale.setScalar(scale);
  return root;
}

// ═══════════════════════════════════════════════════════════
// Flight Physics (Newtonian aerodynamics)
// ═══════════════════════════════════════════════════════════
class Aircraft {
  constructor(spec, mesh) {
    this.spec = spec;
    this.mesh = mesh;
    this.position = new THREE.Vector3();
    this.velocity = new THREE.Vector3();
    this.orientation = new THREE.Quaternion();
    this.angularVel = new THREE.Vector3(); // local body rates (pitch, yaw, roll) rad/s loosely

    this.throttle = 0;
    this.throttleActual = 0;
    this.rpm = 0;

    // Control inputs [-1,1]
    this.input = { pitch: 0, roll: 0, yaw: 0 };

    this.onGround = true;
    this.gearDown = true;
    this.aoa = 0;
    this.sideslip = 0;
    this.gLoad = 1;
    this.airspeed = 0;
    this.altitude = 0;
    this.heading = 0;
    this.pitchAngle = 0;
    this.bankAngle = 0;
    this.stalling = false;
    this.crashed = false;
    this.alive = true;

    this._fwd = new THREE.Vector3();
    this._up = new THREE.Vector3();
    this._right = new THREE.Vector3();
    this._liftDir = new THREE.Vector3();
    this._force = new THREE.Vector3();
    this._tmp = new THREE.Vector3();
    this._q = new THREE.Quaternion();
    this._euler = new THREE.Euler();
  }

  reset(pos, heading = 0, grounded = true) {
    this.position.copy(pos);
    this.velocity.set(0, 0, 0);
    this.angularVel.set(0, 0, 0);
    this.orientation.setFromEuler(new THREE.Euler(0, heading, 0, 'YXZ'));
    this.throttle = grounded ? 0 : 0.7;
    this.throttleActual = this.throttle;
    this.rpm = this.throttle;
    this.onGround = grounded;
    this.crashed = false;
    this.alive = true;
    this.gLoad = 1;
    this.aoa = 0;
    this.sideslip = 0;
    this.pitchAngle = 0;
    this.bankAngle = 0;
    this.heading = heading;
    this.airspeed = 0;
    this.altitude = pos.y;
    this.stalling = false;
    this.syncMesh();
  }

  syncMesh() {
    this.mesh.position.copy(this.position);
    this.mesh.quaternion.copy(this.orientation);
  }

  setControls(pitch, roll, yaw, thrTarget) {
    this.input.pitch = THREE.MathUtils.clamp(pitch, -1, 1);
    this.input.roll = THREE.MathUtils.clamp(roll, -1, 1);
    this.input.yaw = THREE.MathUtils.clamp(yaw, -1, 1);
    this.throttle = THREE.MathUtils.clamp(thrTarget, 0, 1);
  }

  densityAt(alt) {
    // ISA approx
    return SEA_LEVEL_DENSITY * Math.exp(-alt / 8500);
  }

  update(dt) {
    if (!this.alive) return;
    const s = this.spec;
    dt = Math.min(dt, 0.05);

    // Engine response lag
    const lag = s.throttleLag;
    const thrRate = (this.throttle - this.throttleActual) / Math.max(lag, 0.05);
    this.throttleActual += thrRate * dt;
    this.throttleActual = THREE.MathUtils.clamp(this.throttleActual, 0, 1);
    this.rpm += (this.throttleActual - this.rpm) * Math.min(1, dt * 2.5);

    // Body axes
    this._fwd.set(0, 0, 1).applyQuaternion(this.orientation);
    this._up.set(0, 1, 0).applyQuaternion(this.orientation);
    this._right.set(1, 0, 0).applyQuaternion(this.orientation);

    const speed = this.velocity.length();
    this.airspeed = speed;
    this.altitude = this.position.y;

    // Angle of attack & sideslip relative to airflow
    if (speed > 0.5) {
      const vNorm = this._tmp.copy(this.velocity).normalize();
      // aoa: angle between velocity and its projection on body XZ (pitch plane)
      const vFwd = vNorm.dot(this._fwd);
      const vUp = vNorm.dot(this._up);
      const vRight = vNorm.dot(this._right);
      this.aoa = Math.atan2(-vUp, Math.max(0.001, vFwd));
      this.sideslip = Math.atan2(vRight, Math.max(0.001, vFwd));
    } else {
      this.aoa = 0;
      this.sideslip = 0;
    }

    const rho = this.densityAt(Math.max(0, this.altitude));
    const qDyn = 0.5 * rho * speed * speed;

    // Lift coefficient with stall model
    let cl = s.cl0 + s.clAlpha * this.aoa;
    const stall = s.stallAoA;
    this.stalling = Math.abs(this.aoa) > stall * 0.85;
    if (Math.abs(this.aoa) > stall) {
      // Post-stall drop
      const excess = (Math.abs(this.aoa) - stall) / stall;
      cl = Math.sign(cl || 1) * s.clMax * Math.max(0.15, 1 - excess * 1.4);
      // Add buffeting torque
      this.angularVel.x += (Math.random() - 0.5) * excess * 0.8 * dt;
      this.angularVel.z += (Math.random() - 0.5) * excess * 1.2 * dt;
    } else {
      cl = THREE.MathUtils.clamp(cl, -s.clMax, s.clMax);
    }

    // Drag: parasitic + induced
    const cd = s.cd0 + s.kInduced * cl * cl + Math.abs(this.sideslip) * 0.15;

    // Force accumulation in world frame
    this._force.set(0, 0, 0);

    // Thrust along body forward
    const thrust = s.maxThrust * this.throttleActual * (rho / SEA_LEVEL_DENSITY);
    this._force.addScaledVector(this._fwd, thrust);

    // Lift perpendicular to velocity, in the plane of body up/fwd (approx)
    if (speed > 1) {
      // Lift direction: perpendicular to velocity toward body up
      this._liftDir.copy(this._up).addScaledVector(this._fwd, 0); // base
      // More correct: lift = normalize(vel × right) direction with sign of aoa
      this._liftDir.crossVectors(this.velocity, this._right).normalize();
      // Ensure lift points mostly "up" relative to body
      if (this._liftDir.dot(this._up) < 0) this._liftDir.negate();

      const liftMag = qDyn * s.wingArea * cl;
      this._force.addScaledVector(this._liftDir, liftMag);

      // Drag opposite velocity
      const dragMag = qDyn * s.wingArea * cd;
      this._force.addScaledVector(this.velocity, -dragMag / speed);

      // Side force from sideslip
      const cy = -1.8 * this.sideslip;
      this._force.addScaledVector(this._right, qDyn * s.wingArea * cy * 0.35);
    }

    // Gravity
    this._force.y -= s.mass * GRAVITY;

    // Ground interaction
    const groundY = terrainHeight(this.position.x, this.position.z) + 2.2;
    if (this.position.y <= groundY + 0.05 && this.velocity.y <= 0.5) {
      this.onGround = true;
      this.position.y = groundY;

      // Impact check — only on hard landings / cartwheels, not continuous taxi
      const impact = -this.velocity.y;
      const bank = Math.abs(this.bankAngle);
      const wasAirborne = !this.onGround || impact > 2;
      if (wasAirborne && (impact > 14 || (bank > 0.75 && speed > 20) || (Math.abs(this.pitchAngle) > 0.7 && impact > 4))) {
        this.crash();
        return;
      }

      // Kill vertical / settle
      if (this.velocity.y < 0) this.velocity.y = 0;
      this.velocity.y = 0;

      // Body forward projected on ground plane
      const forwardGround = this._tmp.set(this._fwd.x, 0, this._fwd.z);
      if (forwardGround.lengthSq() > 0.001) forwardGround.normalize();
      else forwardGround.set(0, 0, 1);

      // Thrust along ground track
      this.velocity.addScaledVector(forwardGround, (thrust / s.mass) * dt);

      // Lateral tire scrub — keep mostly along heading
      let speedH = Math.hypot(this.velocity.x, this.velocity.z);
      if (speedH > 0.15) {
        const along = this.velocity.x * forwardGround.x + this.velocity.z * forwardGround.z;
        const latX = this.velocity.x - forwardGround.x * along;
        const latZ = this.velocity.z - forwardGround.z * along;
        // Kill sideslip strongly, mild rolling resistance along track
        const rollMu = 0.02 + (1 - this.throttleActual) * 0.12; // brake when throttling down
        const latDamp = Math.exp(-8 * dt);
        this.velocity.x = forwardGround.x * along * Math.exp(-rollMu * dt) + latX * latDamp;
        this.velocity.z = forwardGround.z * along * Math.exp(-rollMu * dt) + latZ * latDamp;

        // Steer with yaw input
        const steer = this.input.yaw * 0.85 * Math.min(1, speedH / 25);
        this._q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), -steer * dt);
        this.orientation.premultiply(this._q);
        // Redirect velocity with heading slightly
        const f2 = new THREE.Vector3(0, 0, 1).applyQuaternion(this.orientation);
        f2.y = 0;
        if (f2.lengthSq() > 0.001) {
          f2.normalize();
          const spd = Math.hypot(this.velocity.x, this.velocity.z);
          const blend = 1 - Math.exp(-2.5 * dt);
          const tx = f2.x * spd;
          const tz = f2.z * spd;
          this.velocity.x += (tx - this.velocity.x) * blend;
          this.velocity.z += (tz - this.velocity.z) * blend;
        }
      }

      // Integrate ground roll
      this.position.x += this.velocity.x * dt;
      this.position.z += this.velocity.z * dt;
      this.position.y = terrainHeight(this.position.x, this.position.z) + 2.2;
      speedH = Math.hypot(this.velocity.x, this.velocity.z);
      this.airspeed = speedH;

      // Attitude: allow limited nose-up with elevator for rotation
      this._euler.setFromQuaternion(this.orientation, 'YXZ');
      this._euler.z *= Math.exp(-5 * dt);
      // euler.x > 0 = nose down; nose-up target is negative
      const targetPitch = THREE.MathUtils.clamp(-this.input.pitch * 0.18, -0.22, 0.06);
      this._euler.x += (targetPitch - this._euler.x) * Math.min(1, 3 * dt);
      this.orientation.setFromEuler(this._euler);

      // Liftoff when lift exceeds weight (rotation helps)
      const rotateAoA = Math.max(0.04, -this._euler.x + 0.02);
      const liftOffCl = s.cl0 + s.clAlpha * rotateAoA;
      const liftoffLift = 0.5 * rho * speedH * speedH * s.wingArea * liftOffCl;
      if (liftoffLift > s.mass * GRAVITY * 0.98 && speedH > 28) {
        this.onGround = false;
        this.velocity.y = Math.max(2.0, (liftoffLift / s.mass - GRAVITY) * 0.3);
      }
    } else {
      this.onGround = false;
      // Integrate forces
      this.velocity.addScaledVector(this._force, dt / s.mass);
      this.position.addScaledVector(this.velocity, dt);

      // Ground collision from below mid-air
      if (this.position.y < groundY) {
        this.position.y = groundY;
        if (-this.velocity.y > 10 || Math.abs(this.bankAngle) > 0.7) {
          this.crash();
          return;
        }
        this.velocity.y = Math.max(0, this.velocity.y);
        this.onGround = true;
      }
    }

    // ── Rotational dynamics (simplified moments) ──
    if (!this.onGround || this.airspeed > 40) {
      const dyn = Math.min(1, (0.5 * rho * Math.max(speed, 5) ** 2) / 8000);
      const qCtrl = Math.max(0.15, dyn);

      // Body axes: +X right, +Y up, +Z forward → +rx pitches nose DOWN (right-hand).
      // Invert pitch so +input (W) is nose-up.
      this.angularVel.x += (-this.input.pitch * s.pitchAuth * qCtrl - this.angularVel.x * s.pitchDamp * 0.35) * dt;
      this.angularVel.z += (-this.input.roll * s.rollAuth * qCtrl - this.angularVel.z * s.rollDamp * 0.35) * dt;
      this.angularVel.y += (-this.input.yaw * s.yawAuth * qCtrl - this.angularVel.y * s.yawDamp * 0.4) * dt;

      // Stability: weathercock / pitch stability lean toward zero aoa/sideslip
      this.angularVel.x += (-this.aoa * 1.4) * dt * qCtrl;
      this.angularVel.y += (-this.sideslip * 1.1) * dt * qCtrl;

      // Couple: roll from yaw, etc. mild
      this.angularVel.z += this.angularVel.y * 0.15 * dt;

      // Limit rates
      this.angularVel.x = THREE.MathUtils.clamp(this.angularVel.x, -2.2, 2.2);
      this.angularVel.y = THREE.MathUtils.clamp(this.angularVel.y, -1.4, 1.4);
      this.angularVel.z = THREE.MathUtils.clamp(this.angularVel.z, -3.5, 3.5);

      // Apply angular velocity in body frame
      const wx = this.angularVel.x * dt;
      const wy = this.angularVel.y * dt;
      const wz = this.angularVel.z * dt;
      const dq = new THREE.Quaternion(
        wx * 0.5, wy * 0.5, wz * 0.5, 1
      ).normalize();
      this.orientation.multiply(dq).normalize();
    } else if (this.onGround) {
      // Nose wheel / elevators slight on ground for attitude
      this._euler.setFromQuaternion(this.orientation, 'YXZ');
      this._euler.x += this.input.pitch * 0.25 * dt;
      this._euler.x = THREE.MathUtils.clamp(this._euler.x, -0.2, 0.18);
      this.orientation.setFromEuler(this._euler);
    }

    // Derived instruments
    this._euler.setFromQuaternion(this.orientation, 'YXZ');
    this.pitchAngle = this._euler.x;
    this.heading = this._euler.y;
    this.bankAngle = this._euler.z;

    // G-load estimate: acceleration along body up / g
    if (speed > 5 && !this.onGround) {
      const liftApprox = (qDyn * s.wingArea * cl) / (s.mass * GRAVITY);
      this.gLoad = THREE.MathUtils.clamp(liftApprox * Math.cos(this.bankAngle * 0.2) + 0.15, -2, 12);
    } else {
      this.gLoad = 1;
    }

    this.syncMesh();
  }

  crash() {
    this.crashed = true;
    this.alive = false;
    this.velocity.set(0, 0, 0);
    audio.boom();
    document.getElementById('flash').style.opacity = '0.7';
    setTimeout(() => { document.getElementById('flash').style.opacity = '0'; }, 120);
  }
}

// ═══════════════════════════════════════════════════════════
// Target drone for dogfight
// ═══════════════════════════════════════════════════════════
class Drone {
  constructor(scene) {
    this.mesh = makeAircraftMesh({
      style: 'interceptor',
      color: 0x445566,
      accent: 0xffaa22
    });
    this.mesh.scale.setScalar(0.85);
    scene.add(this.mesh);
    this.position = new THREE.Vector3();
    this.velocity = new THREE.Vector3();
    this.orientation = new THREE.Quaternion();
    this.hp = 100;
    this.alive = true;
    this.t = 0;
    this._f = new THREE.Vector3();
    this._u = new THREE.Vector3();
  }

  spawn(near) {
    this.position.set(
      near.x + 400 + Math.random() * 200,
      Math.max(near.y + 80, 180),
      near.z + 600
    );
    this.velocity.set(0, 0, 90);
    this.orientation.identity();
    this.hp = 100;
    this.alive = true;
    this.t = 0;
    this.mesh.visible = true;
    this.sync();
  }

  sync() {
    this.mesh.position.copy(this.position);
    this.mesh.quaternion.copy(this.orientation);
  }

  update(dt, playerPos) {
    if (!this.alive) return;
    this.t += dt;
    // Orbit / weave AI
    const toPlayer = this._f.copy(playerPos).sub(this.position);
    const dist = toPlayer.length();
    toPlayer.normalize();

    // Desired direction: bank around player
    const side = this._u.set(-toPlayer.z, 0, toPlayer.x);
    const desired = toPlayer.multiplyScalar(0.35).add(side.multiplyScalar(0.75));
    desired.y += Math.sin(this.t * 0.7) * 0.25 + (220 - this.position.y) * 0.002;
    desired.normalize();

    // Steer velocity
    this.velocity.lerp(desired.multiplyScalar(95 + Math.sin(this.t * 0.3) * 20), 1 - Math.exp(-1.2 * dt));
    this.position.addScaledVector(this.velocity, dt);

    // Face velocity
    if (this.velocity.lengthSq() > 1) {
      const look = new THREE.Matrix4().lookAt(
        this.position,
        this._f.copy(this.position).add(this.velocity),
        new THREE.Vector3(0, 1, 0)
      );
      this.orientation.setFromRotationMatrix(look);
    }
    this.sync();

    // Terrain avoid
    const gh = terrainHeight(this.position.x, this.position.z) + 40;
    if (this.position.y < gh) {
      this.position.y = gh;
      this.velocity.y = Math.abs(this.velocity.y) * 0.5 + 10;
    }
  }

  hit(dmg = 34) {
    this.hp -= dmg;
    if (this.hp <= 0) {
      this.alive = false;
      this.mesh.visible = false;
      audio.boom();
      return true;
    }
    return false;
  }
}

// ═══════════════════════════════════════════════════════════
// Projectiles
// ═══════════════════════════════════════════════════════════
class Guns {
  constructor(scene) {
    this.scene = scene;
    this.bolts = [];
    this._geo = new THREE.SphereGeometry(0.45, 6, 6);
    this._mat = new THREE.MeshBasicMaterial({ color: 0xffee88 });
    this.cooldown = 0;
  }

  fire(origin, dir, speed = 420) {
    if (this.cooldown > 0) return false;
    const mesh = new THREE.Mesh(this._geo, this._mat);
    mesh.position.copy(origin);
    this.scene.add(mesh);
    this.bolts.push({
      mesh,
      vel: dir.clone().normalize().multiplyScalar(speed),
      life: 2.2
    });
    this.cooldown = 0.1;
    audio.gun();
    return true;
  }

  update(dt, drone) {
    this.cooldown = Math.max(0, this.cooldown - dt);
    for (let i = this.bolts.length - 1; i >= 0; i--) {
      const b = this.bolts[i];
      b.life -= dt;
      b.mesh.position.addScaledVector(b.vel, dt);
      if (drone && drone.alive && b.mesh.position.distanceTo(drone.position) < 12) {
        const killed = drone.hit(28);
        state.mission.hits++;
        this.scene.remove(b.mesh);
        this.bolts.splice(i, 1);
        if (killed) {
          state.mission.status = 'success';
          state.mission.message = 'TARGET DESTROYED';
          showWarn('TARGET DESTROYED', 3);
        }
        continue;
      }
      if (b.life <= 0) {
        this.scene.remove(b.mesh);
        this.bolts.splice(i, 1);
      }
    }
  }

  clear() {
    for (const b of this.bolts) this.scene.remove(b.mesh);
    this.bolts.length = 0;
  }
}

// ═══════════════════════════════════════════════════════════
// Scene bootstrap
// ═══════════════════════════════════════════════════════════
if (typeof THREE === 'undefined') {
  document.getElementById('loading').innerHTML =
    '<div class="logo" style="font-size:1.6rem">APEX AERO</div>' +
    '<div class="tagline">Unable to load Three.js from CDN. Serve this file over HTTP and ensure network access.</div>';
  throw new Error('THREE.js not available');
}

const wrap = document.getElementById('canvas-wrap');
const renderer = new THREE.WebGLRenderer({ antialias: true, powerPreference: 'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
wrap.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x87b8e0);
scene.fog = new THREE.FogExp2(0x87b8e0, 0.000085);

const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.5, 25000);

// Sky dome
{
  const skyGeo = new THREE.SphereGeometry(14000, 32, 16);
  const skyMat = new THREE.ShaderMaterial({
    side: THREE.BackSide,
    depthWrite: false,
    uniforms: {
      topColor: { value: new THREE.Color(0x1a4a8a) },
      midColor: { value: new THREE.Color(0x6aacdc) },
      botColor: { value: new THREE.Color(0xc8dce8) },
      offset: { value: 0 },
      exponent: { value: 0.55 }
    },
    vertexShader: `
      varying vec3 vW;
      void main() {
        vec4 w = modelMatrix * vec4(position, 1.0);
        vW = w.xyz;
        gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
      }
    `,
    fragmentShader: `
      uniform vec3 topColor;
      uniform vec3 midColor;
      uniform vec3 botColor;
      uniform float offset;
      uniform float exponent;
      varying vec3 vW;
      void main() {
        float h = normalize(vW + offset).y;
        vec3 col = mix(botColor, midColor, max(pow(max(h, 0.0), exponent), 0.0));
        col = mix(col, topColor, max(pow(max(h, 0.0), exponent * 2.2), 0.0));
        gl_FragColor = vec4(col, 1.0);
      }
    `
  });
  // Fix typo in uniform
  skyMat.uniforms.midColor.value = new THREE.Color(0x6aacdc);
  const sky = new THREE.Mesh(skyGeo, skyMat);
  scene.add(sky);
  state._sky = sky;
}

// Lights
const hemi = new THREE.HemisphereLight(0xb8d8ff, 0x3a2a18, 0.55);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff2d8, 1.35);
sun.position.set(400, 800, 200);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.near = 10;
sun.shadow.camera.far = 3000;
sun.shadow.camera.left = -600;
sun.shadow.camera.right = 600;
sun.shadow.camera.top = 600;
sun.shadow.camera.bottom = -600;
scene.add(sun);
scene.add(new THREE.AmbientLight(0x406080, 0.25));

// Direct renderer (no external postprocessing modules)

// Build world
document.getElementById('load-fill').style.width = '20%';
buildTerrain(scene);
document.getElementById('load-fill').style.width = '55%';
decorateTerrain(scene);
document.getElementById('load-fill').style.width = '70%';

// Mission ring for landing
const landingRing = new THREE.Mesh(
  new THREE.RingGeometry(28, 36, 48),
  new THREE.MeshBasicMaterial({ color: 0x44ffaa, side: THREE.DoubleSide, transparent: true, opacity: 0.55 })
);
landingRing.rotation.x = -Math.PI / 2;
landingRing.position.set(0, 2.8, 1600);
landingRing.visible = false;
scene.add(landingRing);

// Cloud puffs (simple billboards)
{
  const cloudGeo = new THREE.SphereGeometry(1, 8, 6);
  const cloudMat = new THREE.MeshStandardMaterial({
    color: 0xffffff, transparent: true, opacity: 0.45, roughness: 1, metalness: 0
  });
  for (let i = 0; i < 60; i++) {
    const c = new THREE.Mesh(cloudGeo, cloudMat);
    const x = (Math.random() - 0.5) * WORLD_SIZE * 0.9;
    const z = (Math.random() - 0.5) * WORLD_SIZE * 0.9;
    const y = 280 + Math.random() * 420;
    c.position.set(x, y, z);
    c.scale.set(40 + Math.random() * 80, 18 + Math.random() * 30, 40 + Math.random() * 80);
    scene.add(c);
  }
}

// Aircraft instances (only one active at a time)
const craftMeshes = {};
const craftPhysics = {};
for (const spec of FLEET) {
  const mesh = makeAircraftMesh(spec);
  mesh.visible = false;
  scene.add(mesh);
  craftMeshes[spec.id] = mesh;
  craftPhysics[spec.id] = new Aircraft(spec, mesh);
}
let plane = craftPhysics.falcon;
let drone = new Drone(scene);
drone.mesh.visible = false;
const guns = new Guns(scene);

document.getElementById('load-fill').style.width = '100%';

// ═══════════════════════════════════════════════════════════
// UI setup
// ═══════════════════════════════════════════════════════════
function buildFleetUI() {
  const el = document.getElementById('fleet');
  el.innerHTML = '';
  for (const a of FLEET) {
    const card = document.createElement('div');
    card.className = 'aircraft-card' + (a.id === state.aircraftId ? ' selected' : '');
    card.dataset.id = a.id;
    const thrN = a.maxThrust / a.mass;
    const bars = (v) => `<i style="width:${v}%"></i>`;
    card.innerHTML = `
      <div class="name">${a.name}</div>
      <div class="role">${a.role}</div>
      <div class="stat-row"><span>Thrust/Wt</span><div class="bar">${bars(THREE.MathUtils.clamp(thrN / 25 * 100, 10, 100))}</div></div>
      <div class="stat-row"><span>Agility</span><div class="bar">${bars(THREE.MathUtils.clamp(a.rollAuth / 2.8 * 100, 10, 100))}</div></div>
      <div class="stat-row"><span>Top Speed</span><div class="bar">${bars(THREE.MathUtils.clamp(a.maxSpeed / 780 * 100, 10, 100))}</div></div>
      <div class="stat-row"><span>Mass</span><div class="bar">${bars(THREE.MathUtils.clamp(a.mass / 42000 * 100, 10, 100))}</div></div>
    `;
    card.addEventListener('click', () => {
      state.aircraftId = a.id;
      document.querySelectorAll('.aircraft-card').forEach(c => c.classList.toggle('selected', c.dataset.id === a.id));
    });
    el.appendChild(card);
  }
}
buildFleetUI();

document.querySelectorAll('.mode-card').forEach(card => {
  card.addEventListener('click', () => {
    state.mode = card.dataset.mode;
    document.querySelectorAll('.mode-card').forEach(c => c.classList.toggle('selected', c === card));
  });
});

// Pitch ladder ticks
{
  const g = document.getElementById('h-pitch');
  for (let p = -30; p <= 30; p += 10) {
    if (p === 0) continue;
    const y = 110 - p * 1.6;
    const len = Math.abs(p) % 20 === 0 ? 28 : 18;
    const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
    line.setAttribute('x1', 110 - len);
    line.setAttribute('x2', 110 + len);
    line.setAttribute('y1', y);
    line.setAttribute('y2', y);
    line.setAttribute('stroke', 'rgba(180,220,255,0.45)');
    line.setAttribute('stroke-width', '1');
    g.appendChild(line);
    const txt = document.createElementNS('http://www.w3.org/2000/svg', 'text');
    txt.setAttribute('x', 110 - len - 4);
    txt.setAttribute('y', y + 3);
    txt.setAttribute('fill', 'rgba(180,220,255,0.5)');
    txt.setAttribute('font-size', '8');
    txt.setAttribute('text-anchor', 'end');
    txt.setAttribute('font-family', 'Orbitron, sans-serif');
    txt.textContent = String(p);
    g.appendChild(txt);
  }
}

// Tape marks
function buildTape(el, step, count, unit = 1) {
  el.innerHTML = '';
  for (let i = 0; i < count; i++) {
    const d = document.createElement('div');
    d.className = 'tape-mark';
    d.dataset.v = String(i * step * unit);
    d.textContent = String(i * step);
    el.appendChild(d);
  }
}
buildTape(document.getElementById('spd-marks'), 20, 40);
buildTape(document.getElementById('alt-marks'), 100, 80);

// ═══════════════════════════════════════════════════════════
// Input
// ═══════════════════════════════════════════════════════════
window.addEventListener('keydown', e => {
  state.keys[e.code] = true;
  if (['Space', 'ArrowUp', 'ArrowDown'].includes(e.code)) e.preventDefault();
  if (e.code === 'KeyP' || e.code === 'Escape') {
    if (state.phase === 'flying') pauseGame();
    else if (state.phase === 'paused') resumeGame();
  }
  if (e.code === 'KeyC' && state.phase === 'flying') {
    state.camMode = (state.camMode + 1) % 3;
  }
});
window.addEventListener('keyup', e => { state.keys[e.code] = false; });

renderer.domElement.addEventListener('click', () => {
  if (state.phase === 'flying') renderer.domElement.requestPointerLock();
});
document.addEventListener('pointerlockchange', () => {
  state.pointerLocked = document.pointerLockElement === renderer.domElement;
});
document.addEventListener('mousemove', e => {
  if (!state.pointerLocked) return;
  state.mouseDX += e.movementX;
  state.mouseDY += e.movementY;
});

// ═══════════════════════════════════════════════════════════
// Game flow
// ═══════════════════════════════════════════════════════════
function showWarn(msg, secs = 1.5) {
  const w = document.getElementById('warn');
  w.textContent = msg;
  w.classList.add('show');
  clearTimeout(showWarn._t);
  showWarn._t = setTimeout(() => w.classList.remove('show'), secs * 1000);
}

function selectCraft(id) {
  for (const k of Object.keys(craftMeshes)) craftMeshes[k].visible = false;
  plane = craftPhysics[id];
  craftMeshes[id].visible = true;
  document.getElementById('h-name').textContent = plane.spec.name;
}

function startMission() {
  audio.init().then(() => audio.resume());
  state.phase = 'flying';
  state.camMode = 0;
  selectCraft(state.aircraftId);
  guns.clear();
  drone.mesh.visible = false;
  drone.alive = false;

  state.mission = {
    status: 'active',
    message: '',
    landed: false,
    airborne: false,
    takeoffDist: 0,
    shots: 0,
    hits: 0,
    time: 0
  };

  landingRing.visible = state.mode === 'takeoff';

  if (state.mode === 'takeoff') {
    const spawnZ = -350;
    const spawnY = terrainHeight(0, spawnZ) + 2.2;
    plane.reset(new THREE.Vector3(0, spawnY, spawnZ), 0, true);
    document.getElementById('h-mode').textContent = 'TAKEOFF / LANDING';
    document.getElementById('mb-title').textContent = 'PRECISION OPS';
    document.getElementById('mb-obj').textContent = 'Throttle up · Rotate · Climb out · Return & land in the zone';
    showWarn('CLEARED FOR TAKEOFF', 2.5);
  } else {
    plane.reset(new THREE.Vector3(0, 320, 0), 0, false);
    plane.velocity.set(0, 0, 120);
    plane.airspeed = 120;
    plane.throttle = 0.75;
    plane.throttleActual = 0.75;
    thrTarget = 0.75;
    drone.spawn(plane.position);
    document.getElementById('h-mode').textContent = 'DOGFIGHT';
    document.getElementById('mb-title').textContent = 'INTERCEPT';
    document.getElementById('mb-obj').textContent = 'Engage target drone — SPACE to fire';
    showWarn('WEAPONS FREE', 2);
  }

  thrTarget = plane.throttle;
  document.getElementById('menu').classList.add('hidden');
  document.getElementById('hud').classList.add('active');
  document.getElementById('pause-overlay').classList.remove('show');
  renderer.domElement.requestPointerLock();
}

function pauseGame() {
  state.phase = 'paused';
  document.getElementById('pause-overlay').classList.add('show');
  document.exitPointerLock?.();
}
function resumeGame() {
  state.phase = 'flying';
  document.getElementById('pause-overlay').classList.remove('show');
  renderer.domElement.requestPointerLock();
  audio.resume();
}
function toMenu() {
  state.phase = 'menu';
  document.getElementById('pause-overlay').classList.remove('show');
  document.getElementById('hud').classList.remove('active');
  document.getElementById('menu').classList.remove('hidden');
  document.exitPointerLock?.();
  for (const k of Object.keys(craftMeshes)) craftMeshes[k].visible = false;
  drone.mesh.visible = false;
  guns.clear();
}

document.getElementById('btn-start').addEventListener('click', startMission);
document.getElementById('btn-resume').addEventListener('click', resumeGame);
document.getElementById('btn-restart').addEventListener('click', startMission);
document.getElementById('btn-menu').addEventListener('click', toMenu);

// ═══════════════════════════════════════════════════════════
// Camera
// ═══════════════════════════════════════════════════════════
const camOffset = new THREE.Vector3();
const camLook = new THREE.Vector3();
const camSmoothPos = new THREE.Vector3(0, 30, -40);
const camSmoothLook = new THREE.Vector3();
let freeYaw = 0, freePitch = 0.2;

function updateCamera(dt) {
  const p = plane.position;
  const fwd = new THREE.Vector3(0, 0, 1).applyQuaternion(plane.orientation);
  const up = new THREE.Vector3(0, 1, 0).applyQuaternion(plane.orientation);

  if (state.camMode === 0) {
    // Chase
    camOffset.copy(fwd).multiplyScalar(-22 - plane.airspeed * 0.04).addScaledVector(up, 6);
    camSmoothPos.lerp(p.clone().add(camOffset), 1 - Math.exp(-3.5 * dt));
    camLook.copy(p).addScaledVector(fwd, 25).addScaledVector(up, 1);
    camSmoothLook.lerp(camLook, 1 - Math.exp(-5 * dt));
    camera.position.copy(camSmoothPos);
    camera.up.lerp(up, 0.15).normalize();
    camera.lookAt(camSmoothLook);
  } else if (state.camMode === 1) {
    // Cockpit
    camera.position.copy(p).addScaledVector(up, 1.2).addScaledVector(fwd, 1.5);
    camera.up.copy(up);
    camera.lookAt(p.clone().addScaledVector(fwd, 50).addScaledVector(up, 1.0));
    plane.mesh.visible = false;
  } else {
    // Orbit free via mouse residual
    freeYaw -= state.mouseDX * 0.004;
    freePitch = THREE.MathUtils.clamp(freePitch + state.mouseDY * 0.003, -1.2, 1.2);
    const r = 35;
    camera.position.set(
      p.x + Math.sin(freeYaw) * Math.cos(freePitch) * r,
      p.y + Math.sin(freePitch) * r + 8,
      p.z + Math.cos(freeYaw) * Math.cos(freePitch) * r
    );
    camera.up.set(0, 1, 0);
    camera.lookAt(p);
  }
  if (state.camMode !== 1) plane.mesh.visible = true;

  // Keep sky with cam
  if (state._sky) state._sky.position.copy(camera.position);
  sun.position.set(camera.position.x + 400, 800, camera.position.z + 200);
  sun.target.position.copy(camera.position);
  sun.target.updateMatrixWorld();

  // Fog / sky tint by altitude
  const alt = plane.altitude;
  const fogCol = new THREE.Color().setHSL(0.58, 0.35, THREE.MathUtils.clamp(0.55 + alt / 5000, 0.55, 0.85));
  scene.fog.color.lerp(fogCol, 0.05);
  scene.background.copy(scene.fog.color);
}

// ═══════════════════════════════════════════════════════════
// HUD update
// ═══════════════════════════════════════════════════════════
function pad3(n) {
  n = ((Math.round(n) % 360) + 360) % 360;
  return String(n).padStart(3, '0');
}

function updateHUD() {
  const kts = plane.airspeed * 1.94384;
  const altFt = plane.altitude * FT;
  const vs = plane.velocity.y * FT * 60;

  document.getElementById('spd-readout').textContent = String(Math.max(0, Math.round(kts)));
  document.getElementById('alt-readout').textContent = String(Math.max(0, Math.round(altFt)));
  document.getElementById('h-hdg').textContent = pad3(THREE.MathUtils.radToDeg(plane.heading));
  document.getElementById('h-vs').textContent = (vs >= 0 ? '+' : '') + Math.round(vs);
  document.getElementById('h-thr').textContent = Math.round(plane.throttleActual * 100) + '%';
  document.getElementById('h-rpm').textContent = Math.round(plane.rpm * 100) + '% N1';
  document.getElementById('h-g').textContent = plane.gLoad.toFixed(1);
  document.getElementById('throttle-fill').style.height = (plane.throttleActual * 100) + '%';

  // AoA bar: map -0.2..0.4 rad to 0..100%
  const aoaPct = THREE.MathUtils.clamp((plane.aoa + 0.15) / 0.5, 0, 1) * 100;
  document.getElementById('aoa-fill').style.bottom = aoaPct + '%';
  document.getElementById('aoa-stall').style.bottom = THREE.MathUtils.clamp((plane.spec.stallAoA + 0.15) / 0.5, 0, 1) * 100 + '%';

  // Tapes
  const spdOff = -(kts / 20) * 28 + 110 - 14;
  document.getElementById('spd-marks').style.transform = `translateY(${spdOff}px)`;
  const altOff = -(altFt / 100) * 28 + 110 - 14;
  document.getElementById('alt-marks').style.transform = `translateY(${altOff}px)`;

  // Artificial horizon
  const bankDeg = THREE.MathUtils.radToDeg(plane.bankAngle);
  // pitchAngle < 0 ⇒ nose up (Three body axes); move horizon down
  const pitchPx = -THREE.MathUtils.radToDeg(plane.pitchAngle) * 1.6;
  const ball = document.getElementById('h-ball');
  ball.setAttribute('transform', `rotate(${-bankDeg} 110 110) translate(0 ${pitchPx})`);

  // Warnings
  if (!plane.alive) {
    document.getElementById('warn').textContent = 'AIRCRAFT LOST';
    document.getElementById('warn').classList.add('show');
    audio.stallWarn(false);
  } else if (plane.stalling && !plane.onGround && plane.airspeed > 20) {
    document.getElementById('warn').textContent = 'STALL  STALL';
    document.getElementById('warn').classList.add('show');
    audio.stallWarn(true);
  } else if (plane.gLoad > 7) {
    document.getElementById('warn').textContent = 'OVER-G';
    document.getElementById('warn').classList.add('show');
    audio.stallWarn(false);
  } else if (state.mission.status !== 'success' && state.mission.status !== 'fail') {
    document.getElementById('warn').classList.remove('show');
    audio.stallWarn(false);
  }

  // Minimap
  const mm = document.getElementById('mm-canvas');
  const ctx = mm.getContext('2d');
  const W = mm.width, H = mm.height;
  ctx.fillStyle = 'rgba(8,20,40,0.9)';
  ctx.fillRect(0, 0, W, H);
  const scale = 0.04;
  ctx.save();
  ctx.translate(W / 2, H / 2);
  // runway
  ctx.fillStyle = 'rgba(200,200,220,0.35)';
  ctx.fillRect(-70 * scale * 5, -(-300) * scale - 2400 * scale * 0.5, 70 * scale * 10, 2400 * scale);
  // player
  ctx.rotate(-plane.heading);
  ctx.fillStyle = '#6ed0ff';
  ctx.beginPath();
  ctx.moveTo(0, -8);
  ctx.lineTo(6, 8);
  ctx.lineTo(-6, 8);
  ctx.closePath();
  ctx.fill();
  // drone
  if (drone.alive) {
    const dx = (drone.position.x - plane.position.x) * scale;
    const dz = (drone.position.z - plane.position.z) * scale;
    // unrotate for world relative… we rotated by heading so:
    ctx.fillStyle = '#ff6644';
    ctx.beginPath();
    ctx.arc(dx, -dz, 5, 0, Math.PI * 2);
    ctx.fill();
  }
  // landing ring
  if (state.mode === 'takeoff') {
    const lx = (0 - plane.position.x) * scale;
    const lz = (1600 - plane.position.z) * scale;
    ctx.strokeStyle = 'rgba(80,255,180,0.7)';
    ctx.beginPath();
    ctx.arc(lx, -lz, 10, 0, Math.PI * 2);
    ctx.stroke();
  }
  ctx.restore();
  ctx.strokeStyle = 'rgba(100,180,255,0.4)';
  ctx.strokeRect(1, 1, W - 2, H - 2);

  // Mission objectives progress
  if (state.mode === 'takeoff' && state.mission.status === 'active') {
    if (!plane.onGround && plane.altitude > 30) state.mission.airborne = true;
    if (state.mission.airborne && plane.onGround && plane.airspeed < 8) {
      const dz = Math.hypot(plane.position.x - 0, plane.position.z - 1600);
      if (dz < 50) {
        state.mission.status = 'success';
        state.mission.message = 'LANDING COMPLETE';
        showWarn('LANDING NAILED', 4);
        document.getElementById('mb-obj').textContent = 'Mission complete — precision landing successful';
      } else if (dz < 200) {
        document.getElementById('mb-obj').textContent = `On ground — taxi into the zone (${Math.round(dz)} m)`;
      } else {
        document.getElementById('mb-obj').textContent = 'Landed off-zone — taxi or reposition for another approach';
      }
    } else if (state.mission.airborne) {
      document.getElementById('mb-obj').textContent = `Airborne · Fly pattern · Land in glowing zone  · Alt ${Math.round(altFt)} ft`;
    } else {
      document.getElementById('mb-obj').textContent = `On runway · Throttle: ${Math.round(plane.throttle * 100)}% · IAS ${Math.round(kts)} kts · Rotate ~120 kts`;
    }
  } else if (state.mode === 'dogfight' && state.mission.status === 'active') {
    const d = plane.position.distanceTo(drone.position);
    document.getElementById('mb-obj').textContent =
      `Target ${Math.round(d)} m · HP ${Math.max(0, Math.round(drone.hp))} · Hits ${state.mission.hits}`;
  }
}

// ═══════════════════════════════════════════════════════════
// Controls reading
// ═══════════════════════════════════════════════════════════
let thrTarget = 0;

function readControls(dt) {
  const k = state.keys;
  let pitch = 0, roll = 0, yaw = 0;

  if (k['KeyW'] || k['KeyS']) {
    pitch = (k['KeyW'] ? 1 : 0) + (k['KeyS'] ? -1 : 0);
  }
  if (k['KeyA'] || k['KeyD']) {
    roll = (k['KeyA'] ? -1 : 0) + (k['KeyD'] ? 1 : 0);
  }
  if (k['KeyQ'] || k['KeyE']) {
    yaw = (k['KeyQ'] ? -1 : 0) + (k['KeyE'] ? 1 : 0);
  }

  // Mouse as alternate stick when pointer locked (subtle)
  if (state.pointerLocked && state.camMode !== 2) {
    pitch += THREE.MathUtils.clamp(-state.mouseDY * 0.012, -1, 1);
    roll += THREE.MathUtils.clamp(state.mouseDX * 0.012, -1, 1);
  }
  state.mouseDX = 0;
  state.mouseDY = 0;

  // Throttle
  if (k['ShiftLeft'] || k['ShiftRight'] || k['ArrowUp']) thrTarget += 0.55 * dt;
  if (k['ControlLeft'] || k['ControlRight'] || k['ArrowDown']) thrTarget -= 0.55 * dt;
  // number keys 1-0 rough set
  for (let i = 1; i <= 9; i++) {
    if (k['Digit' + i]) thrTarget = i / 10;
  }
  if (k['Digit0']) thrTarget = 1;
  thrTarget = THREE.MathUtils.clamp(thrTarget, 0, 1);

  plane.setControls(pitch, roll, yaw, thrTarget);

  // Fire
  if (k['Space'] && state.mode === 'dogfight' && plane.alive) {
    const fwd = new THREE.Vector3(0, 0, 1).applyQuaternion(plane.orientation);
    const origin = plane.position.clone().addScaledVector(fwd, 8);
    origin.y += 0.5;
    if (guns.fire(origin, fwd, 500 + plane.airspeed)) {
      state.mission.shots++;
    }
  }
}

// ═══════════════════════════════════════════════════════════
// Main loop
// ═══════════════════════════════════════════════════════════
const clock = new THREE.Clock();

function tick() {
  requestAnimationFrame(tick);
  const dt = Math.min(clock.getDelta(), 0.05);

  if (state.phase === 'flying') {
    readControls(dt);
    plane.update(dt);

    if (state.mode === 'dogfight') {
      drone.update(dt, plane.position);
      guns.update(dt, drone);
    } else {
      guns.update(dt, null);
    }

    state.mission.time += dt;

    if (!plane.alive && state.mission.status === 'active') {
      state.mission.status = 'fail';
      document.getElementById('mb-obj').textContent = 'Mission failed — press P for options';
    }

    updateCamera(dt);
    updateHUD();

    audio.update(
      plane.rpm,
      plane.throttleActual,
      plane.airspeed,
      plane.aoa,
      plane.altitude
    );

    // Ambient light shift with altitude
    const dens = plane.densityAt(plane.altitude);
    sun.intensity = 0.7 + dens * 0.7;
  } else if (state.phase === 'menu') {
    // Orbit preview camera over runway
    const t = performance.now() * 0.00015;
    camera.position.set(Math.sin(t) * 180, 60 + Math.sin(t * 0.7) * 15, Math.cos(t) * 180 + 600);
    camera.up.set(0, 1, 0);
    camera.lookAt(0, 5, 800);
    if (state._sky) state._sky.position.copy(camera.position);
  }

  renderer.render(scene, camera);
}

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

// Boot
setTimeout(() => {
  document.getElementById('loading').classList.add('hide');
  tick();
}, 400);
</script>
</body>
</html>
<!-- agent-meta {"model":"x-ai/grok-4.5","provider":"openrouter","persona":"full","sessionId":"00745108-e2e3-4da2-815b-bc1d3ac6ae4f","tokensIn":2583583,"tokensOut":40485,"tokensTotal":2624068,"turns":37,"toolCalls":37,"failedToolCalls":0,"timestamp":"2026-07-17T17:59:05.416Z"} -->