← Flight Sim results

Flight Sim

Grok 4.5 · legacy

◈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 49,646 bytes · SHA-256 5986008fa313
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Horizon Flight Simulator</title>
<style>
  @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;600;700&family=Share+Tech+Mono&display=swap');

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

  html, body {
    width: 100%;
    height: 100%;
    overflow: hidden;
    background: #000;
    font-family: 'Share Tech Mono', monospace;
    color: #e8f4ff;
    user-select: none;
    -webkit-user-select: none;
  }

  #canvas-container {
    position: fixed;
    inset: 0;
    z-index: 0;
  }

  canvas { display: block; width: 100%; height: 100%; }

  /* ── Title / Start screen ── */
  #overlay {
    position: fixed;
    inset: 0;
    z-index: 20;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background:
      radial-gradient(ellipse at 50% 30%, rgba(20,60,120,0.55) 0%, transparent 55%),
      linear-gradient(180deg, #020810 0%, #0a1a30 50%, #041018 100%);
    transition: opacity 0.8s ease, visibility 0.8s;
  }
  #overlay.hidden {
    opacity: 0;
    visibility: hidden;
    pointer-events: none;
  }

  .logo {
    font-family: 'Orbitron', sans-serif;
    font-weight: 700;
    font-size: clamp(2rem, 6vw, 3.6rem);
    letter-spacing: 0.18em;
    text-transform: uppercase;
    background: linear-gradient(180deg, #ffffff 0%, #7ec8ff 45%, #3a8fd4 100%);
    -webkit-background-clip: text;
    -webkit-text-fill-color: transparent;
    background-clip: text;
    text-shadow: none;
    filter: drop-shadow(0 0 28px rgba(100,180,255,0.45));
    margin-bottom: 0.4rem;
  }

  .tagline {
    font-size: 0.95rem;
    letter-spacing: 0.35em;
    color: #6a9skonstante;
    text-transform: uppercase;
    margin-bottom: 2.5rem;
    opacity: 0.85;
  }

  .panel {
    background: rgba(8, 20, 40, 0.72);
    border: 1px solid rgba(100, 170, 255, 0.25);
    border-radius: 12px;
    padding: 1.6rem 2rem;
    max-width: 420px;
    width: 90%;
    backdrop-filter: blur(12px);
    box-shadow:
      0 0 0 1px rgba(255,255,255,0.04) inset,
      0 20px 50px rgba(0,0,0,0.5),
      0 0 40px rgba(50,120,220,0.12);
  }

  .panel h2 {
    font-family: 'Orbitron', sans-serif;
    font-size: 0.85rem;
    letter-spacing: 0.2em;
    color: #8ec8ff;
    margin-bottom: 1rem;
    text-align: center;
  }

  .controls-grid {
    display: grid;
    grid-template-columns: 110px 1fr;
    gap: 0.55rem 1rem;
    font-size: 0.82rem;
    line-height: 1.4;
  }
  .key {
    color: #fff;
    background: rgba(60,120,200,0.25);
    border: 1px solid rgba(120,180,255,0.35);
    border-radius: 4px;
    padding: 0.15rem 0.45rem;
    font-size: 0.75rem;
    white-space: nowrap;
    text-align: center;
  }
  .desc { color: #9bb8d4; }

  #start-btn {
    margin-top: 1.8rem;
    font-family: 'Orbitron', sans-serif;
    font-size: 0.95rem;
    font-weight: 600;
    letter-spacing: 0.25em;
    text-transform: uppercase;
    color: #041018;
    background: linear-gradient(180deg, #a8d8ff 0%, #4a9fe0 50%, #2a78c0 100%);
    border: none;
    border-radius: 6px;
    padding: 0.9rem 2.8rem;
    cursor: pointer;
    box-shadow:
      0 0 0 1px rgba(255,255,255,0.3) inset,
      0 8px 24px rgba(60,140,255,0.4),
      0 0 40px rgba(80,160,255,0.25);
    transition: transform 0.15s, box-shadow 0.15s;
  }
  #start-btn:hover {
    transform: translateY(-2px);
    box-shadow:
      0 0 0 1px rgba(255,255,255,0.4) inset,
      0 12px 32px rgba(60,140,255,0.55),
      0 0 50px rgba(80,160,255,0.4);
  }
  #start-btn:active { transform: translateY(0); }

  .footer-note {
    margin-top: 1.5rem;
    font-size: 0.72rem;
    color: #4a6a88;
    letter-spacing: 0.08em;
  }

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

  .hud-corner {
    position: absolute;
    width: 48px;
    height: 48px;
    border-color: rgba(120,200,255,0.45);
    border-style: solid;
    border-width: 0;
  }
  .hud-corner.tl { top: 18px; left: 18px; border-top-width: 2px; border-left-width: 2px; }
  .hud-corner.tr { top: 18px; right: 18px; border-top-width: 2px; border-right-width: 2px; }
  .hud-corner.bl { bottom: 18px; left: 18px; border-bottom-width: 2px; border-left-width: 2px; }
  .hud-corner.br { bottom: 18px; right: 18px; border-bottom-width: 2px; border-right-width: 2px; }

  #speed-tape, #alt-tape {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 72px;
    height: 220px;
    background: linear-gradient(90deg, rgba(0,15,30,0.55), rgba(0,20,40,0.35));
    border: 1px solid rgba(100,180,255,0.28);
    border-radius: 4px;
    overflow: hidden;
    backdrop-filter: blur(4px);
  }
  #speed-tape { left: 36px; }
  #alt-tape { right: 36px; }

  .tape-label {
    position: absolute;
    top: 6px;
    left: 0;
    right: 0;
    text-align: center;
    font-family: 'Orbitron', sans-serif;
    font-size: 0.58rem;
    letter-spacing: 0.15em;
    color: rgba(140,200,255,0.7);
  }

  .tape-value {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    font-family: 'Orbitron', sans-serif;
    font-size: 1.15rem;
    font-weight: 600;
    color: #fff;
    text-shadow: 0 0 12px rgba(100,180,255,0.8);
    background: rgba(0,30,60,0.7);
    border: 1px solid rgba(120,200,255,0.5);
    border-radius: 3px;
    padding: 0.2rem 0.45rem;
    min-width: 58px;
    text-align: center;
    z-index: 2;
  }

  .tape-unit {
    display: block;
    font-size: 0.5rem;
    letter-spacing: 0.1em;
    color: rgba(140,200,255,0.65);
    font-weight: 400;
    margin-top: 1px;
  }

  /* Artificial horizon indicators */
  #crosshair {
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    width: 120px;
    height: 120px;
  }
  #crosshair svg { width: 100%; height: 100%; overflow: visible; }

  #pitch-ladder {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 180px;
    height: 280px;
    transform: translate(-50%, -50%);
    overflow: hidden;
    opacity: 0.7;
  }

  /* Top bar */
  #top-bar {
    position: absolute;
    top: 22px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    gap: 2rem;
    align-items: center;
    background: rgba(0,15,35,0.55);
    border: 1px solid rgba(100,180,255,0.25);
    border-radius: 6px;
    padding: 0.45rem 1.4rem;
    backdrop-filter: blur(6px);
  }
  .instr {
    text-align: center;
    min-width: 70px;
  }
  .instr .lab {
    font-family: 'Orbitron', sans-serif;
    font-size: 0.52rem;
    letter-spacing: 0.18em;
    color: rgba(140,200,255,0.65);
  }
  .instr .val {
    font-family: 'Orbitron', sans-serif;
    font-size: 1.05rem;
    font-weight: 600;
    color: #e8f4ff;
    text-shadow: 0 0 8px rgba(100,180,255,0.5);
  }
  .instr .unit {
    font-size: 0.55rem;
    color: rgba(140,200,255,0.5);
    margin-left: 2px;
  }

  /* Bottom status */
  #status-bar {
    position: absolute;
    bottom: 28px;
    left: 50%;
    transform: translateX(-50%);
    display: flex;
    gap: 1.5rem;
    align-items: center;
    background: rgba(0,15,35,0.55);
    border: 1px solid rgba(100,180,255,0.22);
    border-radius: 6px;
    padding: 0.4rem 1.2rem;
    backdrop-filter: blur(6px);
    font-size: 0.75rem;
  }
  .status-item { color: #8ab4d4; }
  .status-item span { color: #c8e4ff; }
  .throttle-bar {
    width: 90px;
    height: 8px;
    background: rgba(0,40,80,0.6);
    border-radius: 3px;
    overflow: hidden;
    border: 1px solid rgba(100,180,255,0.25);
  }
  .throttle-fill {
    height: 100%;
    width: 40%;
    background: linear-gradient(90deg, #2a8fd4, #7ec8ff);
    border-radius: 2px;
    transition: width 0.08s linear;
    box-shadow: 0 0 8px rgba(100,180,255,0.5);
  }

  /* Compass */
  #compass {
    position: absolute;
    bottom: 78px;
    left: 50%;
    transform: translateX(-50%);
    width: 200px;
    height: 28px;
    background: rgba(0,15,35,0.55);
    border: 1px solid rgba(100,180,255,0.25);
    border-radius: 4px;
    overflow: hidden;
    backdrop-filter: blur(4px);
  }
  #compass-strip {
    position: absolute;
    top: 0;
    left: 50%;
    height: 100%;
    display: flex;
    align-items: center;
    will-change: transform;
  }
  .comp-mark {
    flex-shrink: 0;
    width: 40px;
    text-align: center;
    font-size: 0.65rem;
    color: rgba(160,210,255,0.7);
  }
  .comp-mark.cardinal {
    color: #fff;
    font-family: 'Orbitron', sans-serif;
    font-weight: 600;
    font-size: 0.7rem;
  }
  .comp-needle {
    position: absolute;
    top: 0;
    left: 50%;
    width: 2px;
    height: 100%;
    background: #ff6b4a;
    transform: translateX(-50%);
    box-shadow: 0 0 6px #ff6b4a;
    z-index: 2;
  }

  /* Stall / warning */
  #warn {
    position: absolute;
    top: 30%;
    left: 50%;
    transform: translateX(-50%);
    font-family: 'Orbitron', sans-serif;
    font-size: 1.1rem;
    font-weight: 700;
    letter-spacing: 0.3em;
    color: #ff4455;
    text-shadow: 0 0 16px rgba(255,40,60,0.8);
    opacity: 0;
    transition: opacity 0.15s;
    pointer-events: none;
  }
  #warn.show { opacity: 1; animation: pulse 0.5s ease infinite alternate; }
  @keyframes pulse { from { opacity: 0.6; } to { opacity: 1; } }

  /* Mini map */
  #minimap {
    position: absolute;
    bottom: 28px;
    right: 28px;
    width: 120px;
    height: 120px;
    border-radius: 50%;
    border: 2px solid rgba(100,180,255,0.4);
    background: rgba(5,25,50,0.65);
    overflow: hidden;
    backdrop-filter: blur(4px);
    box-shadow: 0 0 20px rgba(0,0,0,0.4), inset 0 0 30px rgba(20,60,120,0.3);
  }
  #minimap canvas {
    width: 100%;
    height: 100%;
  }
  .mm-label {
    position: absolute;
    top: 6px;
    left: 0; right: 0;
    text-align: center;
    font-family: 'Orbitron', sans-serif;
    font-size: 0.48rem;
    letter-spacing: 0.15em;
    color: rgba(140,200,255,0.55);
  }

  /* Pause */
  #pause-menu {
    position: fixed;
    inset: 0;
    z-index: 30;
    display: none;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    background: rgba(2,8,18,0.72);
    backdrop-filter: blur(8px);
  }
  #pause-menu.show { display: flex; }
  #pause-menu h1 {
    font-family: 'Orbitron', sans-serif;
    font-size: 2rem;
    letter-spacing: 0.3em;
    color: #a0d0ff;
    margin-bottom: 1.5rem;
  }
  #pause-menu p {
    color: #6a90b0;
    font-size: 0.85rem;
    letter-spacing: 0.1em;
  }

  /* FPS counter */
  #fps {
    position: absolute;
    top: 22px;
    left: 80px;
    font-size: 0.65rem;
    color: rgba(140,200,255,0.4);
    font-family: 'Orbitron', sans-serif;
  }

  /* Help toast */
  #help {
    position: absolute;
    bottom: 110px;
    left: 28px;
    font-size: 0.7rem;
    color: rgba(140,180,220,0.5);
    line-height: 1.6;
    max-width: 200px;
  }
</style>
</head>
<body>

<div id="canvas-container"></div>

<!-- Start overlay -->
<div id="overlay">
  <div class="logo">HORIZON</div>
  <div class="tagline">Flight Simulator</div>
  <div class="panel">
    <h2>Flight Controls</h2>
    <div class="controls-grid">
      <span class="key">W / S</span><span class="desc">Pitch down / up</span>
      <span class="key">A / D</span><span class="desc">Roll left / right</span>
      <span class="key">Q / E</span><span class="desc">Yaw left / right</span>
      <span class="key">↑ / ↓</span><span class="desc">Throttle up / down</span>
      <span class="key">Shift</span><span class="desc">Boost thrust</span>
      <span class="key">Space</span><span class="desc">Airbrake</span>
      <span class="key">C</span><span class="desc">Cycle camera</span>
      <span class="key">R</span><span class="desc">Reset aircraft</span>
      <span class="key">Esc</span><span class="desc">Pause</span>
      <span class="key">Mouse</span><span class="desc">Look (cockpit cam)</span>
    </div>
  </div>
  <button id="start-btn">Take Flight</button>
  <div class="footer-note">CLICK TO ENABLE POINTER LOCK · BEST IN FULLSCREEN</div>
</div>

<!-- HUD -->
<div id="hud">
  <div class="hud-corner tl"></div>
  <div class="hud-corner tr"></div>
  <div class="hud-corner bl"></div>
  <div class="hud-corner br"></div>

  <div id="speed-tape">
    <div class="tape-label">SPD</div>
    <div class="tape-value" id="spd-val">0<span class="tape-unit">KTS</span></div>
  </div>
  <div id="alt-tape">
    <div class="tape-label">ALT</div>
    <div class="tape-value" id="alt-val">0<span class="tape-unit">FT</span></div>
  </div>

  <div id="crosshair">
    <svg viewBox="0 0 120 120" fill="none" xmlns="http://www.w3.org/2000/svg">
      <line x1="20" y1="60" x2="48" y2="60" stroke="rgba(160,220,255,0.75)" stroke-width="1.5"/>
      <line x1="72" y1="60" x2="100" y2="60" stroke="rgba(160,220,255,0.75)" stroke-width="1.5"/>
      <line x1="60" y1="20" x2="60" y2="42" stroke="rgba(160,220,255,0.55)" stroke-width="1.2"/>
      <circle cx="60" cy="60" r="5" stroke="rgba(160,220,255,0.8)" stroke-width="1.5" fill="none"/>
      <line x1="60" y1="60" x2="60" y2="60" stroke="rgba(255,100,80,0.9)" stroke-width="2"/>
      <!-- wing bars -->
      <path d="M28 68 L48 60 L28 52" stroke="rgba(160,220,255,0.6)" stroke-width="1.2" fill="none"/>
      <path d="M92 68 L72 60 L92 52" stroke="rgba(160,220,255,0.6)" stroke-width="1.2" fill="none"/>
    </svg>
  </div>

  <div id="top-bar">
    <div class="instr">
      <div class="lab">HEADING</div>
      <div class="val"><span id="hdg-val">360</span><span class="unit">°</span></div>
    </div>
    <div class="instr">
      <div class="lab">VS</div>
      <div class="val"><span id="vs-val">0</span><span class="unit">FPM</span></div>
    </div>
    <div class="instr">
      <div class="lab">G</div>
      <div class="val"><span id="g-val">1.0</span></div>
    </div>
    <div class="instr">
      <div class="lab">PITCH</div>
      <div class="val"><span id="pitch-val">0</span><span class="unit">°</span></div>
    </div>
    <div class="instr">
      <div class="lab">ROLL</div>
      <div class="val"><span id="roll-val">0</span><span class="unit">°</span></div>
    </div>
  </div>

  <div id="compass">
    <div class="comp-needle"></div>
    <div id="compass-strip"></div>
  </div>

  <div id="status-bar">
    <div class="status-item">THR <span id="thr-pct">40</span>%</div>
    <div class="throttle-bar"><div class="throttle-fill" id="thr-fill"></div></div>
    <div class="status-item">CAM <span id="cam-name">CHASE</span></div>
    <div class="status-item">GEAR <span id="gear-ind" style="color:#5f5">UP</span></div>
  </div>

  <div id="minimap">
    <div class="mm-label">MAP</div>
    <canvas id="mm-canvas" width="120" height="120"></canvas>
  </div>

  <div id="warn">STALL</div>
  <div id="fps">-- FPS</div>
  <div id="help">
    W/S pitch · A/D roll<br>
    Q/E yaw · ↑↓ throttle<br>
    C camera · R reset · Esc pause
  </div>
</div>

<!-- Pause -->
<div id="pause-menu">
  <h1>PAUSED</h1>
  <p>PRESS ESC TO RESUME</p>
</div>

<script type="importmap">
{
  "imports": {
    "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js",
    "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.160.0/examples/jsm/"
  }
}
</script>

<script type="module">
import * as THREE from 'three';
import { Sky } from 'three/addons/objects/Sky.js';

// ═══════════════════════════════════════════════════════════
//  HORIZON FLIGHT SIMULATOR
// ═══════════════════════════════════════════════════════════

const GRAVITY = 9.81;
const FT_PER_M = 3.28084;
const KTS_PER_MS = 1.94384;
const WORLD_SIZE = 16000;
const SEA_LEVEL = 0;

// ── State ────────────────────────────────────────────────
const keys = {};
let running = false;
let paused = false;
let pointerLocked = false;

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

// ── Scene ────────────────────────────────────────────────
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x87b5d4, 0.00012);

// ── Cameras ──────────────────────────────────────────────
const chaseCam = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.5, 50000);
const cockpitCam = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 50000);
const freeCam = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.5, 50000);
const cams = [
  { cam: chaseCam, name: 'CHASE' },
  { cam: cockpitCam, name: 'COCKPIT' },
  { cam: freeCam, name: 'ORBIT' }
];
let camIdx = 0;
let activeCam = chaseCam;

// Orbit mouse look
let orbitYaw = 0;
let orbitPitch = 0.25;
let mouseDX = 0;
let mouseDY = 0;

// ── Sky ──────────────────────────────────────────────────
const sky = new Sky();
sky.scale.setScalar(45000);
scene.add(sky);
const sun = new THREE.Vector3();
const skyUniforms = sky.material.uniforms;
skyUniforms['turbidity'].value = 4;
skyUniforms['rayleigh'].value = 2.2;
skyUniforms['mieCoefficient'].value = 0.004;
skyUniforms['mieDirectionalG'].value = 0.72;
const sunPhi = THREE.MathUtils.degToRad(88.5);
const sunTheta = THREE.MathUtils.degToRad(160);
sun.setFromSphericalCoords(1, sunPhi, sunTheta);
skyUniforms['sunPosition'].value.copy(sun);

// ── Lights ───────────────────────────────────────────────
const hemi = new THREE.HemisphereLight(0xb1d0ff, 0x3a5040, 0.55);
scene.add(hemi);

const sunLight = new THREE.DirectionalLight(0xfff4e0, 1.8);
sunLight.position.copy(sun).multiplyScalar(5000);
sunLight.castShadow = true;
sunLight.shadow.mapSize.set(2048, 2048);
sunLight.shadow.camera.near = 100;
sunLight.shadow.camera.far = 12000;
sunLight.shadow.camera.left = -3000;
sunLight.shadow.camera.right = 3000;
sunLight.shadow.camera.top = 3000;
sunLight.shadow.camera.bottom = -3000;
sunLight.shadow.bias = -0.0002;
scene.add(sunLight);

const amb = new THREE.AmbientLight(0x406080, 0.25);
scene.add(amb);

// ── Terrain ──────────────────────────────────────────────
function createTerrain() {
  const segs = 256;
  const size = WORLD_SIZE;
  const geo = new THREE.PlaneGeometry(size, size, segs, segs);
  geo.rotateX(-Math.PI / 2);

  const pos = geo.attributes.position;
  const colors = [];
  const color = new THREE.Color();

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

    // Multi-octave noise
    let h = 0;
    h += Math.sin(x * 0.0008) * Math.cos(z * 0.0007) * 180;
    h += Math.sin(x * 0.0021 + 1.3) * Math.cos(z * 0.0019) * 90;
    h += Math.sin(x * 0.005 + 2.1) * Math.cos(z * 0.0045 + 0.7) * 35;
    h += Math.sin(x * 0.012) * Math.sin(z * 0.011) * 12;

    // Ridges / mountains off to the sides
    const dist = Math.sqrt(x * x + z * z);
    const ridge = Math.max(0, dist - 2500) * 0.04;
    h += ridge * (0.6 + 0.4 * Math.sin(x * 0.001) * Math.cos(z * 0.0012));

    // Central valley / runway area flatter
    const flat = Math.exp(-dist * dist / (1800 * 1800));
    h *= (1 - flat * 0.92);
    h = Math.max(h, -2);

    pos.setY(i, h);

    // Vertex colors by height
    if (h < 5) {
      color.setHSL(0.28, 0.35, 0.28 + Math.random() * 0.04); // grass
    } else if (h < 80) {
      color.setHSL(0.25, 0.4, 0.22 + h / 400);
    } else if (h < 200) {
      color.setHSL(0.1, 0.25, 0.3 + h / 800); // rocky
    } else {
      color.setHSL(0.08, 0.1, 0.45 + h / 1200); // snow peaks
    }
    // Water tint for near-zero
    if (h < 1) color.setHSL(0.55, 0.35, 0.32);
    colors.push(color.r, color.g, color.b);
  }

  geo.setAttribute('color', new THREE.Float32BufferAttribute(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 = true;
  return mesh;
}

const terrain = createTerrain();
scene.add(terrain);

// Terrain height sampling (approximate via noisy function matching generator)
function terrainHeight(x, z) {
  let h = 0;
  h += Math.sin(x * 0.0008) * Math.cos(z * 0.0007) * 180;
  h += Math.sin(x * 0.0021 + 1.3) * Math.cos(z * 0.0019) * 90;
  h += Math.sin(x * 0.005 + 2.1) * Math.cos(z * 0.0045 + 0.7) * 35;
  h += Math.sin(x * 0.012) * Math.sin(z * 0.011) * 12;
  const dist = Math.sqrt(x * x + z * z);
  const ridge = Math.max(0, dist - 2500) * 0.04;
  h += ridge * (0.6 + 0.4 * Math.sin(x * 0.001) * Math.cos(z * 0.0012));
  const flat = Math.exp(-dist * dist / (1800 * 1800));
  h *= (1 - flat * 0.92);
  return Math.max(h, -2);
}

// ── Water ────────────────────────────────────────────────
const waterGeo = new THREE.PlaneGeometry(WORLD_SIZE * 1.2, WORLD_SIZE * 1.2, 1, 1);
waterGeo.rotateX(-Math.PI / 2);
const waterMat = new THREE.MeshStandardMaterial({
  color: 0x1a4a6e,
  roughness: 0.15,
  metalness: 0.6,
  transparent: true,
  opacity: 0.75
});
const water = new THREE.Mesh(waterGeo, waterMat);
water.position.y = -1.5;
water.receiveShadow = true;
scene.add(water);

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

  // Main strip
  const strip = new THREE.Mesh(
    new THREE.BoxGeometry(40, 0.4, 1400),
    new THREE.MeshStandardMaterial({ color: 0x2a2a2e, roughness: 0.85 })
  );
  strip.position.y = 0.5;
  strip.receiveShadow = true;
  g.add(strip);

  // Centerline dashes
  for (let i = -650; i <= 650; i += 40) {
    const dash = new THREE.Mesh(
      new THREE.BoxGeometry(1.2, 0.42, 18),
      new THREE.MeshStandardMaterial({ color: 0xf0f0e0, roughness: 0.9 })
    );
    dash.position.set(0, 0.55, i);
    g.add(dash);
  }

  // Edge lights
  const lightMat = new THREE.MeshStandardMaterial({
    color: 0xffffff, emissive: 0xffaa40, emissiveIntensity: 1.2, roughness: 0.4
  });
  for (let i = -680; i <= 680; i += 30) {
    for (const side of [-18, 18]) {
      const lg = new THREE.Mesh(new THREE.SphereGeometry(0.6, 6, 6), lightMat);
      lg.position.set(side, 0.9, i);
      g.add(lg);
    }
  }

  // Threshold marks
  for (const z of [-680, 680]) {
    for (let x = -14; x <= 14; x += 4) {
      const m = new THREE.Mesh(
        new THREE.BoxGeometry(1.5, 0.42, 30),
        new THREE.MeshStandardMaterial({ color: 0xf0f0e0 })
      );
      m.position.set(x, 0.55, z);
      g.add(m);
    }
  }

  // Apron
  const apron = new THREE.Mesh(
    new THREE.BoxGeometry(120, 0.3, 100),
    new THREE.MeshStandardMaterial({ color: 0x333338, roughness: 0.9 })
  );
  apron.position.set(80, 0.4, 0);
  g.add(apron);

  // Tower
  const tower = new THREE.Mesh(
    new THREE.BoxGeometry(10, 40, 10),
    new THREE.MeshStandardMaterial({ color: 0xd0d0c8, roughness: 0.7 })
  );
  tower.position.set(100, 20, 40);
  tower.castShadow = true;
  g.add(tower);
  const towerTop = new THREE.Mesh(
    new THREE.CylinderGeometry(8, 7, 8, 8),
    new THREE.MeshStandardMaterial({ color: 0x2266aa, roughness: 0.4, metalness: 0.3, transparent: true, opacity: 0.85 })
  );
  towerTop.position.set(100, 44, 40);
  g.add(towerTop);

  // Hangar
  const hangar = new THREE.Mesh(
    new THREE.BoxGeometry(50, 16, 40),
    new THREE.MeshStandardMaterial({ color: 0x6a7080, roughness: 0.75 })
  );
  hangar.position.set(90, 8, -60);
  hangar.castShadow = true;
  g.add(hangar);

  return g;
}
scene.add(createRunway());

// ── Clouds ───────────────────────────────────────────────
function createClouds() {
  const group = new THREE.Group();
  const mat = new THREE.MeshStandardMaterial({
    color: 0xffffff,
    roughness: 1,
    transparent: true,
    opacity: 0.78,
    depthWrite: false
  });

  for (let i = 0; i < 60; i++) {
    const cg = new THREE.Group();
    const n = 3 + Math.floor(Math.random() * 5);
    for (let j = 0; j < n; j++) {
      const s = 40 + Math.random() * 90;
      const puff = new THREE.Mesh(new THREE.SphereGeometry(s, 7, 7), mat);
      puff.position.set(
        (Math.random() - 0.5) * 120,
        (Math.random() - 0.5) * 30,
        (Math.random() - 0.5) * 80
      );
      puff.scale.y = 0.45 + Math.random() * 0.3;
      cg.add(puff);
    }
    cg.position.set(
      (Math.random() - 0.5) * WORLD_SIZE * 0.85,
      350 + Math.random() * 600,
      (Math.random() - 0.5) * WORLD_SIZE * 0.85
    );
    group.add(cg);
  }
  return group;
}
const clouds = createClouds();
scene.add(clouds);

// ── Distant mountains (impostors) ────────────────────────
function createDistantPeaks() {
  const g = new THREE.Group();
  const mat = new THREE.MeshStandardMaterial({
    color: 0x4a5a6a,
    roughness: 1,
    flatShading: true
  });
  for (let i = 0; i < 24; i++) {
    const ang = (i / 24) * Math.PI * 2;
    const dist = 7000 + Math.random() * 2000;
    const h = 400 + Math.random() * 800;
    const r = 300 + Math.random() * 500;
    const peak = new THREE.Mesh(new THREE.ConeGeometry(r, h, 5), mat);
    peak.position.set(Math.cos(ang) * dist, h * 0.3, Math.sin(ang) * dist);
    g.add(peak);
  }
  return g;
}
scene.add(createDistantPeaks());

// ── Trees (instanced) ────────────────────────────────────
function createTrees() {
  const trunkGeo = new THREE.CylinderGeometry(0.4, 0.7, 6, 5);
  const leafGeo = new THREE.ConeGeometry(3.5, 10, 6);
  const trunkMat = new THREE.MeshStandardMaterial({ color: 0x4a3020, roughness: 1 });
  const leafMat = new THREE.MeshStandardMaterial({ color: 0x2a6030, roughness: 0.9 });

  const count = 400;
  const trunks = new THREE.InstancedMesh(trunkGeo, trunkMat, count);
  const leaves = new THREE.InstancedMesh(leafGeo, leafMat, count);
  const dummy = new THREE.Object3D();

  let placed = 0;
  let attempts = 0;
  while (placed < count && attempts < count * 20) {
    attempts++;
    const x = (Math.random() - 0.5) * 5000;
    const z = (Math.random() - 0.5) * 5000;
    // Keep clear of runway
    if (Math.abs(x) < 80 && Math.abs(z) < 800) continue;
    const y = terrainHeight(x, z);
    if (y < 2 || y > 120) continue;
    const scale = 0.7 + Math.random() * 1.2;

    dummy.position.set(x, y + 3 * scale, z);
    dummy.scale.setScalar(scale);
    dummy.rotation.y = Math.random() * Math.PI;
    dummy.updateMatrix();
    trunks.setMatrixAt(placed, dummy.matrix);

    dummy.position.set(x, y + 9 * scale, z);
    dummy.updateMatrix();
    leaves.setMatrixAt(placed, dummy.matrix);
    placed++;
  }
  trunks.castShadow = true;
  leaves.castShadow = true;
  scene.add(trunks);
  scene.add(leaves);
}
createTrees();

// ── Aircraft ─────────────────────────────────────────────
function createAircraft() {
  const ac = new THREE.Group();

  const bodyMat = new THREE.MeshStandardMaterial({
    color: 0xe8ecf0, roughness: 0.35, metalness: 0.55
  });
  const accentMat = new THREE.MeshStandardMaterial({
    color: 0x1a6aaf, roughness: 0.4, metalness: 0.5
  });
  const darkMat = new THREE.MeshStandardMaterial({
    color: 0x1a1a22, roughness: 0.6, metalness: 0.4
  });
  const glassMat = new THREE.MeshStandardMaterial({
    color: 0x88ccee, roughness: 0.05, metalness: 0.9, transparent: true, opacity: 0.55
  });
  const glowMat = new THREE.MeshStandardMaterial({
    color: 0xff4400, emissive: 0xff2200, emissiveIntensity: 1.5, roughness: 0.3
  });

  // Fuselage
  const fuse = new THREE.Mesh(new THREE.CapsuleGeometry(1.1, 7.5, 6, 12), bodyMat);
  fuse.rotation.z = Math.PI / 2;
  fuse.castShadow = true;
  ac.add(fuse);

  // Nose cone
  const nose = new THREE.Mesh(new THREE.ConeGeometry(1.05, 2.8, 10), bodyMat);
  nose.rotation.z = -Math.PI / 2;
  nose.position.z = -5.5;
  nose.castShadow = true;
  ac.add(nose);

  // Cockpit canopy
  const canopy = new THREE.Mesh(new THREE.SphereGeometry(1.15, 10, 8, 0, Math.PI * 2, 0, Math.PI * 0.5), glassMat);
  canopy.position.set(0, 0.7, -1.5);
  canopy.scale.set(0.9, 0.85, 1.6);
  ac.add(canopy);

  // Main wings
  const wingShape = new THREE.Shape();
  wingShape.moveTo(0, 0);
  wingShape.lineTo(7, -0.8);
  wingShape.lineTo(7.2, -1.5);
  wingShape.lineTo(0, -1.8);
  wingShape.lineTo(0, 0);
  const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.18, bevelEnabled: false });
  const wingL = new THREE.Mesh(wingGeo, accentMat);
  wingL.rotation.x = Math.PI / 2;
  wingL.rotation.z = Math.PI / 2;
  wingL.position.set(0, -0.15, 0.3);
  wingL.castShadow = true;
  ac.add(wingL);
  const wingR = wingL.clone();
  wingR.scale.z = -1;
  ac.add(wingR);

  // Wingtips
  for (const s of [-1, 1]) {
    const tip = new THREE.Mesh(
      new THREE.BoxGeometry(0.12, 1.2, 0.6),
      accentMat
    );
    tip.position.set(s * 7.1, 0.4, 1.0);
    ac.add(tip);
  }

  // Horizontal stabilizer
  const hstab = new THREE.Mesh(new THREE.BoxGeometry(4.5, 0.12, 1.3), bodyMat);
  hstab.position.set(0, 0.3, 4.2);
  hstab.castShadow = true;
  ac.add(hstab);

  // Vertical stabilizer
  const vstab = new THREE.Mesh(new THREE.BoxGeometry(0.12, 2.4, 1.6), accentMat);
  vstab.position.set(0, 1.4, 4.0);
  vstab.castShadow = true;
  ac.add(vstab);

  // Engine / intake
  const intake = new THREE.Mesh(new THREE.CylinderGeometry(0.7, 0.85, 2.5, 10), darkMat);
  intake.rotation.x = Math.PI / 2;
  intake.position.set(0, -0.5, 2.5);
  ac.add(intake);

  // Exhaust glow
  const exhaust = new THREE.Mesh(new THREE.SphereGeometry(0.55, 8, 8), glowMat);
  exhaust.position.set(0, -0.5, 4.0);
  exhaust.scale.z = 1.6;
  ac.add(exhaust);
  ac.userData.exhaust = exhaust;

  // Afterburner light
  const abLight = new THREE.PointLight(0xff5500, 0, 25);
  abLight.position.set(0, -0.5, 5);
  ac.add(abLight);
  ac.userData.abLight = abLight;

  // Landing gear (visual, retracted look as small stubs)
  for (const [x, z] of [[0, -2.5], [-1.5, 1], [1.5, 1]]) {
    const gear = new THREE.Mesh(
      new THREE.CylinderGeometry(0.12, 0.12, 0.8, 6),
      darkMat
    );
    gear.position.set(x, -1.3, z);
    ac.add(gear);
    const wheel = new THREE.Mesh(
      new THREE.TorusGeometry(0.35, 0.12, 6, 10),
      darkMat
    );
    wheel.rotation.y = Math.PI / 2;
    wheel.position.set(x, -1.75, z);
    ac.add(wheel);
  }

  // Stripes
  const stripe = new THREE.Mesh(
    new THREE.BoxGeometry(0.05, 0.08, 6),
    new THREE.MeshStandardMaterial({ color: 0xffcc00, roughness: 0.5, metalness: 0.3 })
  );
  stripe.position.set(1.05, 0.2, -0.5);
  ac.add(stripe);
  const stripe2 = stripe.clone();
  stripe2.position.x = -1.05;
  ac.add(stripe2);

  // Scale up for visibility
  ac.scale.setScalar(1.0);
  ac.rotation.order = 'YXZ';

  return ac;
}

const aircraft = createAircraft();
scene.add(aircraft);

// ── Flight model ─────────────────────────────────────────
const plane = {
  // Position / velocity in world space
  pos: new THREE.Vector3(0, 80, 200),
  vel: new THREE.Vector3(0, 0, -60), // start with forward speed
  // Orientation: heading (yaw), pitch, roll in radians
  yaw: Math.PI, // facing -Z initially? wait: vel is -Z so heading = PI (facing -Z is south if +Z is north)
  pitch: 0,
  roll: 0,
  // Control state
  throttle: 0.55,
  // Derived
  airspeed: 60,
  aoa: 0,
  gForce: 1,
  onGround: false,
  stalling: false,
  crashed: false
};

// Forward, up, right axes
const forward = new THREE.Vector3();
const up = new THREE.Vector3();
const right = new THREE.Vector3();
const _q = new THREE.Quaternion();
const _e = new THREE.Euler();
const _v = new THREE.Vector3();

function updateAxes() {
  _e.set(plane.pitch, plane.yaw, plane.roll, 'YXZ');
  _q.setFromEuler(_e);
  forward.set(0, 0, -1).applyQuaternion(_q);
  up.set(0, 1, 0).applyQuaternion(_q);
  right.set(1, 0, 0).applyQuaternion(_q);
}

function resetAircraft() {
  plane.pos.set(0, 80, 200);
  plane.vel.set(0, 0, -70);
  plane.yaw = 0;
  plane.pitch = 0.05;
  plane.roll = 0;
  plane.throttle = 0.55;
  plane.airspeed = 70;
  plane.aoa = 0;
  plane.gForce = 1;
  plane.onGround = false;
  plane.stalling = false;
  plane.crashed = false;
  updateAxes();
  aircraft.position.copy(plane.pos);
  aircraft.quaternion.copy(_q);
}

resetAircraft();

// ── Physics step ─────────────────────────────────────────
function physics(dt) {
  if (plane.crashed) return;

  // ── Controls ──
  const pitchRate = 1.1;
  const rollRate = 1.8;
  const yawRate = 0.7;

  let pitchIn = 0, rollIn = 0, yawIn = 0;

  if (keys['w'] || keys['ArrowUp'] && keys['ShiftLeft']) { /* handled below */ }
  // Pitch: W = nose down (negative pitch rate for flight-sim convention in our euler)
  // In aviation: pull back (S) = pitch up
  if (keys['w']) pitchIn -= 1;
  if (keys['s']) pitchIn += 1;
  if (keys['a']) rollIn -= 1;
  if (keys['d']) rollIn += 1;
  if (keys['q']) yawIn += 1;
  if (keys['e']) yawIn -= 1;

  // Throttle
  if (keys['ArrowUp']) plane.throttle = Math.min(1, plane.throttle + 0.4 * dt);
  if (keys['ArrowDown']) plane.throttle = Math.max(0, plane.throttle - 0.4 * dt);
  // Alternate throttle keys
  if (keys['='] || keys['+']) plane.throttle = Math.min(1, plane.throttle + 0.4 * dt);
  if (keys['-'] || keys['_']) plane.throttle = Math.max(0, plane.throttle - 0.4 * dt);

  // Mouse pitch/roll assist when locked (cockpit feel)
  if (pointerLocked && camIdx === 1) {
    pitchIn += mouseDY * 0.4;
    rollIn += mouseDX * 0.5;
  }
  mouseDX *= 0.85;
  mouseDY *= 0.85;

  // Recenter tendency for roll when no input (stability)
  if (Math.abs(rollIn) < 0.01) {
    plane.roll *= (1 - 0.4 * dt);
  }

  plane.pitch += pitchIn * pitchRate * dt;
  plane.roll += rollIn * rollRate * dt;
  plane.yaw += yawIn * yawRate * dt;

  // Clamp pitch
  plane.pitch = THREE.MathUtils.clamp(plane.pitch, -1.3, 1.3);
  // Wrap roll
  if (plane.roll > Math.PI) plane.roll -= Math.PI * 2;
  if (plane.roll < -Math.PI) plane.roll += Math.PI * 2;

  updateAxes();

  // ── Forces ──
  plane.airspeed = plane.vel.length();
  const speed = Math.max(plane.airspeed, 0.1);

  // Angle of attack: angle between velocity and aircraft nose
  const velDir = _v.copy(plane.vel).normalize();
  const aoa = Math.asin(THREE.MathUtils.clamp(velDir.dot(up), -1, 1));
  plane.aoa = aoa;

  // Thrust
  const maxThrust = 55;
  let thrustMag = plane.throttle * maxThrust;
  if (keys['ShiftLeft'] || keys['ShiftRight']) thrustMag *= 1.55;
  // Airbrake
  if (keys[' ']) thrustMag *= 0.2;

  const thrust = forward.clone().multiplyScalar(thrustMag);

  // Lift — peaks around ~10° AoA, drops after stall ~18°
  const aoaDeg = THREE.MathUtils.radToDeg(aoa);
  let liftCoeff = 0;
  const stallAngle = 16;
  if (Math.abs(aoaDeg) < stallAngle) {
    liftCoeff = 0.12 * aoaDeg + 0.25; // base + linear lift
  } else {
    // Post-stall: drop
    liftCoeff = (0.12 * stallAngle + 0.25) * Math.max(0, 1 - (Math.abs(aoaDeg) - stallAngle) / 20);
    liftCoeff *= Math.sign(aoaDeg || 1);
  }
  plane.stalling = Math.abs(aoaDeg) > stallAngle && speed > 15;

  const rho = Math.exp(-plane.pos.y / 9000) * 1.2; // air density falloff
  const wingArea = 18;
  const liftMag = 0.5 * rho * speed * speed * wingArea * Math.abs(liftCoeff) * 0.018;
  const liftDir = up.clone();
  if (liftCoeff < 0) liftDir.negate();
  // Also reduce lift efficiency when inverted / high bank
  const lift = liftDir.multiplyScalar(liftMag);

  // Drag
  const dragCoeff = 0.02 + Math.abs(liftCoeff) * 0.015 + (keys[' '] ? 0.08 : 0);
  const dragMag = 0.5 * rho * speed * speed * dragCoeff * 2.5;
  const drag = velDir.clone().multiplyScalar(-dragMag);

  // Gravity
  const grav = new THREE.Vector3(0, -GRAVITY, 0);

  // Net acceleration
  const mass = 12; // simplified mass units
  const accel = new THREE.Vector3()
    .add(thrust)
    .add(lift)
    .add(drag)
    .divideScalar(mass)
    .add(grav);

  // G-force estimate (along body up)
  const bodyAccel = accel.clone().sub(grav);
  plane.gForce = 1 + bodyAccel.dot(up) / GRAVITY;
  plane.gForce = THREE.MathUtils.clamp(plane.gForce, -3, 9);

  // Integrate velocity & position
  plane.vel.addScaledVector(accel, dt);

  // Ground interaction
  const groundY = terrainHeight(plane.pos.x, plane.pos.z) + 2.2;
  const nextY = plane.pos.y + plane.vel.y * dt;

  if (nextY < groundY) {
    plane.pos.y = groundY;
    // Hard impact?
    if (plane.vel.y < -18 || Math.abs(plane.roll) > 0.9 || Math.abs(plane.pitch) > 0.7) {
      // Crash
      plane.crashed = true;
      plane.vel.set(0, 0, 0);
    } else {
      // Soft landing / taxi
      plane.vel.y = Math.max(0, plane.vel.y);
      // Friction on ground
      plane.vel.x *= (1 - 1.5 * dt);
      plane.vel.z *= (1 - 1.5 * dt);
      plane.onGround = true;
      // Level gently
      plane.pitch *= (1 - 3 * dt);
      plane.roll *= (1 - 3 * dt);
      // If throttle high enough, takeoff
      if (plane.airspeed > 45 && plane.throttle > 0.6) {
        plane.vel.y += 5 * dt;
      }
    }
  } else {
    plane.pos.y = nextY;
    plane.onGround = false;
  }

  plane.pos.x += plane.vel.x * dt;
  plane.pos.z += plane.vel.z * dt;

  // World bounds soft clamp
  const bound = WORLD_SIZE * 0.48;
  plane.pos.x = THREE.MathUtils.clamp(plane.pos.x, -bound, bound);
  plane.pos.z = THREE.MathUtils.clamp(plane.pos.z, -bound, bound);

  // Ceiling
  if (plane.pos.y > 3500) {
    plane.pos.y = 3500;
    plane.vel.y = Math.min(0, plane.vel.y);
  }

  // Apply to mesh
  aircraft.position.copy(plane.pos);
  _e.set(plane.pitch, plane.yaw, plane.roll, 'YXZ');
  aircraft.quaternion.setFromEuler(_e);

  // Exhaust visual
  const thrFlash = plane.throttle * (keys['ShiftLeft'] || keys['ShiftRight'] ? 2.2 : 1);
  aircraft.userData.exhaust.material.emissiveIntensity = 0.6 + thrFlash * 1.8;
  aircraft.userData.exhaust.scale.set(0.7 + thrFlash * 0.3, 0.7 + thrFlash * 0.3, 1 + thrFlash * 1.5);
  aircraft.userData.abLight.intensity = thrFlash * 4;
}

// ── Camera ───────────────────────────────────────────────
const chaseOffset = new THREE.Vector3(0, 4, 14);
const _camPos = new THREE.Vector3();
const _lookAt = new THREE.Vector3();

function updateCamera(dt) {
  updateAxes();

  if (camIdx === 0) {
    // Chase — smooth follow behind aircraft
    const desired = chaseOffset.clone().applyQuaternion(aircraft.quaternion).add(plane.pos);
    // Lift chase cam roughly with pitch so we stay above
    desired.y = Math.max(desired.y, plane.pos.y + 2);
    chaseCam.position.lerp(desired, 1 - Math.pow(0.001, dt));
    _lookAt.copy(plane.pos).addScaledVector(forward, 20).addScaledVector(up, 1);
    chaseCam.lookAt(_lookAt);
    // Keep upright-ish
    chaseCam.up.lerp(new THREE.Vector3(0, 1, 0), 0.05);
  } else if (camIdx === 1) {
    // Cockpit
    const cp = new THREE.Vector3(0, 0.9, -1.2).applyQuaternion(aircraft.quaternion).add(plane.pos);
    cockpitCam.position.copy(cp);
    // Free look offset
    const lookQ = aircraft.quaternion.clone();
    const lookE = new THREE.Euler().setFromQuaternion(lookQ, 'YXZ');
    lookE.y -= orbitYaw * 0.5;
    lookE.x -= orbitPitch * 0.4;
    lookQ.setFromEuler(lookE);
    const lookDir = new THREE.Vector3(0, 0, -1).applyQuaternion(lookQ);
    cockpitCam.lookAt(cp.clone().add(lookDir));
    cockpitCam.up.copy(up);
  } else {
    // Orbit
    const dist = 22;
    const ox = Math.sin(orbitYaw) * Math.cos(orbitPitch) * dist;
    const oy = Math.sin(orbitPitch) * dist + 3;
    const oz = Math.cos(orbitYaw) * Math.cos(orbitPitch) * dist;
    freeCam.position.copy(plane.pos).add(new THREE.Vector3(ox, oy, oz));
    freeCam.lookAt(plane.pos);
    freeCam.up.set(0, 1, 0);
  }

  activeCam = cams[camIdx].cam;
}

// ── HUD ──────────────────────────────────────────────────
const spdEl = document.getElementById('spd-val');
const altEl = document.getElementById('alt-val');
const hdgEl = document.getElementById('hdg-val');
const vsEl = document.getElementById('vs-val');
const gEl = document.getElementById('g-val');
const pitchEl = document.getElementById('pitch-val');
const rollEl = document.getElementById('roll-val');
const thrPct = document.getElementById('thr-pct');
const thrFill = document.getElementById('thr-fill');
const camName = document.getElementById('cam-name');
const warnEl = document.getElementById('warn');
const fpsEl = document.getElementById('fps');
const compassStrip = document.getElementById('compass-strip');
const mmCanvas = document.getElementById('mm-canvas');
const mmCtx = mmCanvas.getContext('2d');

// Build compass strip (repeat headings so we can scroll)
(function buildCompass() {
  const dirs = ['N', '3', '6', 'E', '12', '15', 'S', '21', '24', 'W', '30', '33'];
  // 3 full revolutions for seamless scroll
  let html = '';
  for (let r = 0; r < 5; r++) {
    for (let i = 0; i < 36; i++) {
      const deg = i * 10;
      const label = deg % 90 === 0
        ? ['N', 'E', 'S', 'W'][deg / 90]
        : String(deg / 10);
      const cls = deg % 90 === 0 ? 'comp-mark cardinal' : 'comp-mark';
      html += `<div class="${cls}">${label}</div>`;
    }
  }
  compassStrip.innerHTML = html;
})();

function updateHUD() {
  const kts = Math.round(plane.airspeed * KTS_PER_MS);
  const altFt = Math.round(Math.max(0, plane.pos.y) * FT_PER_M);
  let hdg = Math.round(((THREE.MathUtils.radToDeg(-plane.yaw) % 360) + 360) % 360);
  if (hdg === 0) hdg = 360;
  const vs = Math.round(plane.vel.y * FT_PER_M * 60);
  const pitchDeg = Math.round(THREE.MathUtils.radToDeg(plane.pitch));
  const rollDeg = Math.round(THREE.MathUtils.radToDeg(plane.roll));

  spdEl.innerHTML = `${kts}<span class="tape-unit">KTS</span>`;
  altEl.innerHTML = `${altFt}<span class="tape-unit">FT</span>`;
  hdgEl.textContent = String(hdg).padStart(3, '0');
  vsEl.textContent = (vs >= 0 ? '+' : '') + vs;
  gEl.textContent = plane.gForce.toFixed(1);
  pitchEl.textContent = pitchDeg;
  rollEl.textContent = rollDeg;
  thrPct.textContent = Math.round(plane.throttle * 100);
  thrFill.style.width = (plane.throttle * 100) + '%';
  camName.textContent = cams[camIdx].name;

  // Compass scroll: each mark is 40px, 10° per mark
  // Center on current heading
  const offset = hdg / 10 * 40;
  // Strip starts with multiple revolutions; center of rev 2
  const base = 36 * 40 * 2;
  compassStrip.style.transform = `translate(calc(-50% + ${base - offset}px), 0)`;

  // Warnings
  if (plane.crashed) {
    warnEl.textContent = 'CRASHED — PRESS R';
    warnEl.classList.add('show');
  } else if (plane.stalling) {
    warnEl.textContent = 'STALL';
    warnEl.classList.add('show');
  } else if (plane.airspeed < 30 && !plane.onGround) {
    warnEl.textContent = 'LOW SPEED';
    warnEl.classList.add('show');
  } else if (plane.pos.y - terrainHeight(plane.pos.x, plane.pos.z) < 25 && plane.vel.y < -8) {
    warnEl.textContent = 'TERRAIN';
    warnEl.classList.add('show');
  } else {
    warnEl.classList.remove('show');
  }

  // Minimap
  drawMinimap(hdg);
}

function drawMinimap(hdg) {
  const ctx = mmCtx;
  const W = 120, H = 120, cx = 60, cy = 60;
  ctx.clearRect(0, 0, W, H);

  // Background
  ctx.fillStyle = 'rgba(10,40,70,0.9)';
  ctx.beginPath();
  ctx.arc(cx, cy, 58, 0, Math.PI * 2);
  ctx.fill();

  // Scale: minimap shows ~4000m radius
  const scale = 58 / 4000;

  ctx.save();
  ctx.translate(cx, cy);
  ctx.rotate(-THREE.MathUtils.degToRad(hdg) + Math.PI); // north up relative to aircraft heading

  // Runway
  ctx.save();
  ctx.translate(-plane.pos.x * scale, -plane.pos.z * scale);
  ctx.fillStyle = 'rgba(180,180,190,0.7)';
  ctx.fillRect(-2, -35, 4, 70);
  // Tower marker
  ctx.fillStyle = 'rgba(100,180,255,0.8)';
  ctx.beginPath();
  ctx.arc(5, 2, 2.5, 0, Math.PI * 2);
  ctx.fill();
  ctx.restore();

  // Grid rings
  ctx.strokeStyle = 'rgba(100,170,255,0.15)';
  ctx.lineWidth = 1;
  for (const r of [20, 40, 55]) {
    ctx.beginPath();
    ctx.arc(0, 0, r, 0, Math.PI * 2);
    ctx.stroke();
  }

  ctx.restore();

  // Aircraft chevron (always center pointing up)
  ctx.fillStyle = '#ff6644';
  ctx.beginPath();
  ctx.moveTo(cx, cy - 7);
  ctx.lineTo(cx - 5, cy + 6);
  ctx.lineTo(cx, cy + 2);
  ctx.lineTo(cx + 5, cy + 6);
  ctx.closePath();
  ctx.fill();

  // Border
  ctx.strokeStyle = 'rgba(120,190,255,0.5)';
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.arc(cx, cy, 58, 0, Math.PI * 2);
  ctx.stroke();
}

// ── Input ────────────────────────────────────────────────
window.addEventListener('keydown', (e) => {
  keys[e.key] = true;
  keys[e.code] = true;

  if (e.key === 'Escape' && running) {
    paused = !paused;
    document.getElementById('pause-menu').classList.toggle('show', paused);
    if (paused && document.pointerLockElement) document.exitPointerLock();
  }
  if ((e.key === 'c' || e.key === 'C') && running && !paused) {
    camIdx = (camIdx + 1) % cams.length;
    orbitYaw = 0;
    orbitPitch = camIdx === 2 ? 0.3 : 0;
  }
  if ((e.key === 'r' || e.key === 'R') && running) {
    resetAircraft();
  }
  // Prevent arrow keys scrolling
  if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key)) {
    e.preventDefault();
  }
});

window.addEventListener('keyup', (e) => {
  keys[e.key] = false;
  keys[e.code] = false;
});

document.addEventListener('mousemove', (e) => {
  if (!pointerLocked) return;
  mouseDX += e.movementX * 0.002;
  mouseDY += e.movementY * 0.002;
  orbitYaw += e.movementX * 0.005;
  orbitPitch = THREE.MathUtils.clamp(orbitPitch + e.movementY * 0.004, -1.2, 1.2);
});

renderer.domElement.addEventListener('click', () => {
  if (running && !paused) renderer.domElement.requestPointerLock();
});

document.addEventListener('pointerlockchange', () => {
  pointerLocked = document.pointerLockElement === renderer.domElement;
});

window.addEventListener('resize', () => {
  const w = window.innerWidth;
  const h = window.innerHeight;
  for (const c of cams) {
    c.cam.aspect = w / h;
    c.cam.updateProjectionMatrix();
  }
  renderer.setSize(w, h);
});

// ── Start ────────────────────────────────────────────────
document.getElementById('start-btn').addEventListener('click', () => {
  document.getElementById('overlay').classList.add('hidden');
  document.getElementById('hud').classList.add('active');
  running = true;
  paused = false;
  resetAircraft();
  renderer.domElement.requestPointerLock();
});

// ── Main loop ────────────────────────────────────────────
let last = performance.now();
let frames = 0;
let fpsT = 0;

function frame(now) {
  requestAnimationFrame(frame);
  const dt = Math.min((now - last) / 1000, 0.05);
  last = now;

  frames++;
  fpsT += dt;
  if (fpsT >= 0.5) {
    fpsEl.textContent = Math.round(frames / fpsT) + ' FPS';
    frames = 0;
    fpsT = 0;
  }

  // Gentle cloud drift
  clouds.rotation.y += dt * 0.001;

  if (running && !paused) {
    physics(dt);
    updateCamera(dt);
    updateHUD();
  } else if (!running) {
    // Idle showcase orbit on title screen
    const t = now * 0.00015;
    chaseCam.position.set(Math.sin(t) * 18, 6 + Math.sin(t * 0.7) * 2, Math.cos(t) * 18 + 200);
    chaseCam.position.y += 70;
    chaseCam.lookAt(plane.pos);
    aircraft.position.copy(plane.pos);
    aircraft.rotation.y = t * 0.5;
  }

  // Shadow camera follows plane
  sunLight.position.copy(plane.pos).add(sun.clone().multiplyScalar(2000));
  sunLight.target.position.copy(plane.pos);
  sunLight.target.updateMatrixWorld();

  renderer.render(scene, running ? activeCam : chaseCam);
}

requestAnimationFrame(frame);
</script>
</body>
</html>