← Flight Simulator results

Flight Simulator

Ornith 1.5 35B A3B · full

Initial view of Flight Simulator

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 51,425 bytes · SHA-256 fd03f081baa7
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apex Aero — Flight Simulator</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { width:100%; height:100%; overflow:hidden; background:#05070d; font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif; color:#e8f0ff; }
  #app { position:fixed; inset:0; }
  canvas { display:block; }

  /* ---------- MENUS ---------- */
  .screen { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
            background:radial-gradient(ellipse at 50% 30%, #1a2a4a 0%, #0a0f1c 60%, #04060d 100%); z-index:50; }
  .hidden { display:none !important; }
  .title { font-size:64px; font-weight:800; letter-spacing:6px; background:linear-gradient(90deg,#4fc3ff,#8affc0,#4fc3ff);
           -webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:0 0 40px rgba(79,195,255,.25); }
  .subtitle { font-size:15px; letter-spacing:8px; color:#7fa8d4; margin-top:6px; text-transform:uppercase; }
  .panel { margin-top:34px; width:min(920px,92vw); }
  .section-label { font-size:12px; letter-spacing:4px; color:#6f8fb8; text-transform:uppercase; margin:18px 0 10px; }

  .card-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:14px; }
  .card { background:linear-gradient(160deg,#141d31,#0c1220); border:1px solid #24354f; border-radius:12px;
          padding:16px; cursor:pointer; transition:.18s; user-select:none; }
  .card:hover { border-color:#4fc3ff; transform:translateY(-3px); box-shadow:0 10px 30px rgba(79,195,255,.18); }
  .card.selected { border-color:#8affc0; box-shadow:0 0 0 2px #8affc0, 0 10px 30px rgba(138,255,192,.25); }
  .card h3 { font-size:18px; margin-bottom:2px; }
  .card .role { font-size:11px; color:#7fa8d4; text-transform:uppercase; letter-spacing:2px; margin-bottom:12px; }
  .stat { display:flex; justify-content:space-between; font-size:12px; margin:5px 0; color:#b9cbe4; }
  .stat .bar { flex:1; margin:0 8px; height:6px; background:#1c2840; border-radius:3px; overflow:hidden; }
  .stat .bar i { display:block; height:100%; background:linear-gradient(90deg,#4fc3ff,#8affc0); }
  .stat .v { width:34px; text-align:right; color:#8fd0ff; font-variant-numeric:tabular-nums; }

  .mode-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:14px; }
  .mode { background:linear-gradient(160deg,#141d31,#0c1220); border:1px solid #24354f; border-radius:12px;
          padding:20px; cursor:pointer; transition:.18s; }
  .mode:hover { border-color:#4fc3ff; box-shadow:0 10px 30px rgba(79,195,255,.18); }
  .mode.selected { border-color:#8affc0; box-shadow:0 0 0 2px #8affc0; }
  .mode h3 { font-size:18px; margin-bottom:6px; }
  .mode p { font-size:13px; color:#93aecf; line-height:1.5; }

  button.play { margin-top:30px; padding:16px 52px; font-size:18px; font-weight:700; letter-spacing:3px; color:#04121e;
                background:linear-gradient(90deg,#4fc3ff,#8affc0); border:none; border-radius:10px; cursor:pointer;
                transition:.18s; text-transform:uppercase; }
  button.play:hover { transform:scale(1.04); box-shadow:0 0 40px rgba(138,255,192,.4); }
  .controls-hint { margin-top:26px; font-size:12px; color:#6f8fb8; text-align:center; line-height:1.9; }
  .controls-hint kbd { background:#141d31; border:1px solid #2a3d5c; border-bottom-width:2px; border-radius:4px;
                       padding:1px 7px; font-family:inherit; color:#cfe3ff; }

  /* ---------- HUD ---------- */
  #hud { position:absolute; inset:0; pointer-events:none; z-index:30; font-variant-numeric:tabular-nums; }
  .hud-el { position:absolute; font-size:13px; }
  .hud-box { background:rgba(6,14,24,.55); border:1px solid rgba(79,195,255,.35); border-radius:8px; padding:10px 14px;
             backdrop-filter:blur(3px); }
  .hud-box .lbl { font-size:10px; letter-spacing:2px; color:#6f8fb8; text-transform:uppercase; }
  .hud-box .val { font-size:22px; font-weight:700; color:#cfe9ff; }
  .hud-box .val small { font-size:12px; font-weight:400; color:#7fa8d4; }

  #hud-alt { top:20px; left:20px; }
  #hud-speed { top:20px; right:20px; text-align:right; }
  #hud-engine { bottom:20px; left:20px; min-width:210px; }
  #hud-engine .lbl { margin-top:8px; }
  #hud-engine .bar { width:180px; height:8px; background:#1c2840; border-radius:4px; overflow:hidden; margin-top:3px; }
  #hud-engine .bar i { display:block; height:100%; width:0%; background:linear-gradient(90deg,#40e070,#f7ff50,#ff5050); transition:.1s; }
  #hud-throttle { bottom:20px; right:20px; text-align:right; min-width:190px; }
  #hud-throttle .bar { width:180px; height:8px; background:#1c2840; border-radius:4px; overflow:hidden; margin-left:auto; margin-top:3px; }
  #hud-throttle .bar i { display:block; height:100%; width:0%; background:linear-gradient(90deg,#4fc3ff,#8affc0); transition:.1s; }

  #attitude { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:220px; height:220px; border-radius:50%;
              border:2px solid rgba(138,255,192,.5); overflow:hidden; opacity:.85; }
  #att-canvas { width:100%; height:100%; }
  #hud-pitch { position:absolute; top:24px; left:50%; transform:translateX(-50%); text-align:center; }
  #hud-hdg  { position:absolute; bottom:20px; left:50%; transform:translateX(-50%); text-align:center; }

  #stall-warn { position:absolute; top:120px; left:50%; transform:translateX(-50%); font-size:26px; font-weight:800;
                letter-spacing:4px; color:#ff5050; text-shadow:0 0 20px rgba(255,80,80,.7); opacity:0; transition:.15s; }
  #stall-warn.show { opacity:1; animation:blink .5s infinite; }
  @keyframes blink { 50% { opacity:.3; } }

  #crosshair { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:30px; height:30px; opacity:0; }
  #crosshair::before,#crosshair::after { content:''; position:absolute; background:rgba(138,255,192,.7); }
  #crosshair::before { left:50%; top:0; width:1px; height:100%; transform:translateX(-50%); }
  #crosshair::after { top:50%; left:0; height:1px; width:100%; transform:translateY(-50%); }

  #dogfight-ui { position:absolute; inset:0; }
  #hud-timer { top:20px; left:50%; transform:translateX(-50%); text-align:center; }
  #hud-timer .val { font-size:30px; }
  #hud-kills { top:70px; left:50%; transform:translateX(-50%); text-align:center; }
  #hud-locked { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); margin-top:40px; font-size:16px;
                color:#ff5050; letter-spacing:2px; opacity:0; transition:.15s; }
  #hud-locked.show { opacity:1; }

  #message { position:absolute; top:180px; left:50%; transform:translateX(-50%); font-size:20px; font-weight:600;
             color:#eaf4ff; text-shadow:0 2px 10px rgba(0,0,0,.6); opacity:0; transition:.4s; text-align:center; max-width:80vw; }
  #message.show { opacity:1; }

  #banner { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); text-align:center; z-index:60;
            background:rgba(4,8,16,.9); border:1px solid #2a3d5c; border-radius:16px; padding:40px 60px; pointer-events:auto; }
  #banner h2 { font-size:34px; margin-bottom:8px; }
  #banner p { color:#93aecf; margin-bottom:24px; font-size:15px; }
  #banner button { padding:14px 40px; font-size:16px; font-weight:700; letter-spacing:2px; background:linear-gradient(90deg,#4fc3ff,#8affc0);
                   border:none; border-radius:10px; cursor:pointer; text-transform:uppercase; }

  #pause-hint { position:absolute; bottom:8px; left:50%; transform:translateX(-50%); font-size:11px; color:#56708f; letter-spacing:1px; }
  #load-msg { position:absolute; bottom:40px; left:50%; transform:translateX(-50%); color:#8fd0ff; letter-spacing:3px; font-size:14px; }
</style>
</head>
<body>
<div id="app"></div>

<!-- ================= START / MENU SCREEN ================= -->
<div id="menu" class="screen">
  <div class="title">APEX AERO</div>
  <div class="subtitle">High-Fidelity Flight Simulator</div>

  <div class="panel">
    <div class="section-label">Select Aircraft</div>
    <div class="card-grid" id="aircraft-grid"></div>
  </div>

  <div class="panel">
    <div class="section-label">Mission</div>
    <div class="mode-grid" id="mode-grid"></div>
  </div>

  <button class="play" id="fly-btn">Deploy</button>

  <div class="controls-hint">
    <kbd>Mouse</kbd> Pitch &amp; Roll &nbsp;·&nbsp; <kbd>A</kbd>/<kbd>D</kbd> Yaw &nbsp;·&nbsp; <kbd>W</kbd>/<kbd>S</kbd> Throttle &nbsp;·&nbsp; <kbd>Shift</kbd>/<kbd>Ctrl</kbd> Boost/Turbulence &nbsp;·&nbsp; <kbd>Space</kbd> Brake &nbsp;·&nbsp; <kbd>F</kbd> Cannons &nbsp;·&nbsp; <kbd>C</kbd> Camera &nbsp;·&nbsp; <kbd>P</kbd> Pause
  </div>
</div>

<!-- ================= HUD ================= -->
<div id="hud" class="hidden">
  <div id="hud-alt" class="hud-el hud-box">
    <div class="lbl">Altitude</div>
    <div class="val"><span id="v-alt">0</span> <small>m</small></div>
  </div>
  <div id="hud-speed" class="hud-el hud-box">
    <div class="lbl">Airspeed</div>
    <div class="val"><span id="v-spd">0</span> <small>km/h</small></div>
  </div>
  <div id="hud-pitch" class="hud-el hud-box">
    <div class="lbl">Pitch</div>
    <div class="val"><span id="v-pitch">0</span><small>°</small></div>
  </div>
  <div id="hud-hdg" class="hud-el hud-box">
    <div class="lbl">Heading</div>
    <div class="val"><span id="v-hdg">036</span><small>°</small></div>
  </div>

  <div id="attitude"><canvas id="att-canvas" width="220" height="220"></canvas></div>

  <div id="hud-engine" class="hud-el hud-box">
    <div class="lbl">Engine</div>
    <div class="val"><span id="v-rpm">0</span> <small>%</small></div>
    <div class="bar"><i id="rpm-bar"></i></div>
    <div class="lbl">Temp</div>
    <div class="bar"><i id="heat-bar" style="background:linear-gradient(90deg,#4fc3ff,#f7ff50,#ff5050)"></i></div>
    <div class="lbl">Fuel</div>
    <div class="bar"><i id="fuel-bar" style="background:linear-gradient(90deg,#8affc0,#4fc3ff)"></i></div>
  </div>

  <div id="hud-throttle" class="hud-el hud-box">
    <div class="lbl">Throttle</div>
    <div class="val"><span id="v-thr">0</span> <small>%</small></div>
    <div class="bar"><i id="thr-bar"></i></div>
  </div>

  <div id="stall-warn">⚠ STALL ⚠</div>

  <div id="hud-timer" class="hud-el hud-box hidden"><div class="lbl">Time</div><div class="val" id="v-timer">90</div></div>
  <div id="hud-kills" class="hud-el hud-box hidden"><div class="lbl">Enemies</div><div class="val" id="v-kills">0</div></div>
  <div id="hud-locked" class="hud-el">TARGET LOCKED</div>

  <div id="message"></div>
  <div id="pause-hint">[P] Pause · [C] Camera · [F] Cannons</div>
  <div id="crosshair"></div>
</div>

<div id="banner" class="hidden">
  <h2 id="banner-title">Paused</h2>
  <p id="banner-text"></p>
  <button id="banner-btn">Resume</button>
</div>

<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
<script>
/* ============================================================
   APEX AERO — single-file flight simulator
   Three.js scene + custom Newtonian aerodynamics
   ============================================================ */
'use strict';

/* ============================================================
   1. AIRCRAFT DEFINITIONS (differentiated physics)
   ============================================================ */
const AIRCRAFT = [
  {
    id:'falcon', name:'Falcon M-9', role:'Air Superiority',
    desc:'Balanced fighter. Strong thrust, agile, high top speed.',
    mass: 8200,          // kg
    wingArea: 28,        // m^2
    maxLift: 1.55,       // Cl max
    inducedFactor: 0.085,// CDi = ki * Cl^2
    profileDrag: 0.022,  // CD0
    maxThrust: 165000,   // N
    weightRatio: 0.19,   // thrust/mass
    liftSlope: 5.6,      // per radian
    stallAoA: 0.28,      // rad (~16°)
    rollRate: 2.4,       // rad/s at full control
    pitchRate: 1.5,      // rad/s
    yawRate: 1.1,
    dragCoeffBonus: 0.0, // flaps/gear extra
    color: 0x3a7bd5, accent: 0x8affc0,
  },
  {
    id:'viper', name:'Viper S', role:'Interceptor',
    desc:'Lightning-fast, low maneuverability. Built for speed & diving.',
    mass: 6100,
    wingArea: 19,
    maxLift: 1.25,
    inducedFactor: 0.055,
    profileDrag: 0.015,
    maxThrust: 195000,
    weightRatio: 0.31,
    liftSlope: 6.2,
    stallAoA: 0.24,
    rollRate: 3.0,
    pitchRate: 1.9,
    yawRate: 1.3,
    dragCoeffBonus: 0.0,
    color: 0xff7043, accent: 0xffd74f,
  },
  {
    id:'sentinel', name:'Sentinel G', role:'Glider',
    desc:'Ultra-light. Excellent lift & turn rate, low thrust. Rides thermals.',
    mass: 1150,
    wingArea: 16,
    maxLift: 1.95,
    inducedFactor: 0.042,
    profileDrag: 0.018,
    maxThrust: 18000,
    weightRatio: 0.015,
    liftSlope: 4.9,
    stallAoA: 0.34,
    rollRate: 2.1,
    pitchRate: 1.2,
    yawRate: 0.9,
    dragCoeffBonus: 0.0,
    color: 0xeceff1, accent: 0x4fc3ff,
  }
];

/* ============================================================
   2. ATMOSPHERE / CONSTANTS
   ============================================================ */
const G = 9.81;                    // gravity m/s^2
const SEA_DENSITY = 1.225;         // kg/m^3
// Exponential atmosphere model
function airDensity(alt){ return SEA_DENSITY * Math.exp(-alt/8500); }
function temperature(alt){ return 15 - alt/100*0.65; }

/* ============================================================
   3. NOISE (procedural terrain)
   ============================================================ */
function hash2(x,y){
  const s = Math.sin(x*127.1 + y*311.7)*43758.5453;
  return s - Math.floor(s);
}
function smoothNoise(x,y){
  const xi=Math.floor(x), yi=Math.floor(y);
  const xf=x-xi, yf=y-yi;
  const u=xf*xf*(3-2*xf), v=yf*yf*(3-2*yf);
  const a=hash2(xi,yi), b=hash2(xi+1,yi), c=hash2(xi,yi+1), d=hash2(xi+1,yi+1);
  return a*(1-u)*(1-v)+b*u*(1-v)+c*(1-u)*v+d*u*v;
}
function fbm(x,y){
  let amp=0.5, freq=1, sum=0, norm=0;
  for(let i=0;i<6;i++){ sum+=amp*smoothNoise(x*freq,y*freq); norm+=amp; amp*=0.5; freq*=2; }
  return sum/norm;
}
// Deterministic RNG for terrain seed
let TERRAIN_SEED = 1;
function seededNoise(x,y){
  const nx = x + TERRAIN_SEED*31.7, ny = y + TERRAIN_SEED*47.3;
  return fbm(nx*0.0009, ny*0.0009);
}
// Terrain height (meters). Flat runway plateau near origin; dramatic mountains farther out.
function terrainHeight(x,z){
  const d = Math.hypot(x,z);
  const fade = Math.min(1, d/7000);          // 0 near origin, 1 far away
  const flat = Math.max(0, 1 - d/3500);      // fully flat within 3.5km
  const n = seededNoise(x,z);
  const rid = seededNoise(x*1.7+100, z*1.7+100);
  let h = (n*2-1)*0.6 + rid*rid*0.8;         // -0.6..1.4
  const relief = 0.15 + 0.85*fade;           // gentle near origin, dramatic far
  let alt = h*1400*relief;
  const dome = Math.max(0, 1 - d/5000);
  alt += 30*dome*dome;                        // +30m runway plateau
  alt = alt*(1-flat) + 30*flat;               // blend to flat plateau near origin
  if(alt<SEA_LEVEL) alt *= 0.4;               // flatten ocean floor
  return alt;
}
function isOcean(x,z){ return terrainHeight(x,z) < 2; }
const SEA_LEVEL = 2;

/* ============================================================
   4. THREE.JS SETUP
   ============================================================ */
let scene, camera, renderer, clock;
let terrainMesh, skyMesh, sun;
const terrainVels = [];      // vertex positions reference
let aircraftGroup, enemyGroup;
let runwayStart, runwayDir, runwayLen=1400, runwayWidth=45;

function initThree(){
  scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0x9fbfe0, 0.00028);

  camera = new THREE.PerspectiveCamera(62, innerWidth/innerHeight, 1, 30000);

  renderer = new THREE.WebGLRenderer({ antialias:true, powerPreference:'high-performance' });
  renderer.setPixelRatio(Math.min(devicePixelRatio,2));
  renderer.setSize(innerWidth, innerHeight);
  renderer.shadowMap.enabled = true;
  renderer.shadowMap.type = THREE.PCFSoftShadowMap;
  renderer.outputColorSpace = THREE.SRGBColorSpace;
  document.getElementById('app').appendChild(renderer.domElement);

  clock = new THREE.Clock();

  // Lighting — PBR-ish
  const hemi = new THREE.HemisphereLight(0xbfd8ff, 0x4a6a45, 0.9);
  scene.add(hemi);
  sun = new THREE.DirectionalLight(0xfff2d6, 2.1);
  sun.position.set(400,600,200);
  sun.castShadow = true;
  sun.shadow.mapSize.set(2048,2048);
  const d=1200;
  sun.shadow.camera.left=-d; sun.shadow.camera.right=d;
  sun.shadow.camera.top=d; sun.shadow.camera.bottom=-d;
  sun.shadow.camera.far=6000;
  sun.shadow.bias=-0.0004;
  scene.add(sun);
  scene.add(new THREE.AmbientLight(0x405060, 0.4));

  buildSky();
  buildTerrain();
  buildRunway();
}

function buildSky(){
  const geo = new THREE.SphereGeometry(26000, 32, 16);
  const mat = new THREE.ShaderMaterial({
    side: THREE.BackSide, depthWrite:false,
    uniforms:{ top:{value:new THREE.Color(0x1b4fa8)}, bot:{value:new THREE.Color(0xbfe0ff)} },
    vertexShader:`varying vec3 vp; void main(){ vp=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
    fragmentShader:`varying vec3 vp; uniform vec3 top; uniform vec3 bot;
      void main(){ float h=clamp((normalize(vp).y+0.15)/1.1,0.0,1.0); gl_FragColor=vec4(mix(bot,top,h),1.0);}`
  });
  skyMesh = new THREE.Mesh(geo, mat);
  scene.add(skyMesh);
  // Sun sprite
  const c=document.createElement('canvas'); c.width=c.height=128;
  const g=c.getContext('2d');
  const grd=g.createRadialGradient(64,64,4,64,64,64);
  grd.addColorStop(0,'rgba(255,250,230,1)'); grd.addColorStop(0.2,'rgba(255,230,150,.9)');
  grd.addColorStop(1,'rgba(255,220,120,0)');
  g.fillStyle=grd; g.fillRect(0,0,128,128);
  const tex=new THREE.CanvasTexture(c);
  const sunSprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex,transparent:true,depthWrite:false,blending:THREE.AdditiveBlending}));
  sunSprite.scale.set(1600,1600,1);
  sunSprite.position.copy(sun.position).multiplyScalar(8);
  scene.add(sunSprite);
}

let terrainGeo, terrainPosAttr;
function buildTerrain(){
  const SEG=220, EXTENT=12000;
  terrainGeo = new THREE.PlaneGeometry(EXTENT*2, EXTENT*2, SEG, SEG);
  terrainGeo.rotateX(-Math.PI/2);
  terrainPosAttr = terrainGeo.attributes.position;
  const colors=new Float32Array(terrainPosAttr.count*3);
  const c=new THREE.Color();
  for(let i=0;i<terrainPosAttr.count;i++){
    const x=terrainPosAttr.getX(i), z=terrainPosAttr.getZ(i);
    const h=terrainHeight(x,z);
    terrainPosAttr.setY(i,h);
    // color by height & slope
    let col;
    if(h<SEA_LEVEL+1){ col=new THREE.Color(0xc2b280); }          // sand
    else if(h<200){ col=new THREE.Color(0x4a7a3a); }             // grass
    else if(h<700){ col=new THREE.Color(0x5a6b4a); }             // hills
    else if(h<1200){ col=new THREE.Color(0x7a7468); }            // rock
    else { col=new THREE.Color(0xf2f6ff); }                       // snow
    // blend toward water near ocean
    if(isOcean(x,z)){ col.setHex(0x2a5a86); col.lerp(new THREE.Color(0x123a63),0.5); }
    colors[i*3]=col.r; colors[i*3+1]=col.g; colors[i*3+2]=col.b;
  }
  terrainGeo.setAttribute('color',new THREE.BufferAttribute(colors,3));
  terrainGeo.computeVertexNormals();
  const mat=new THREE.MeshStandardMaterial({ vertexColors:true, roughness:0.95, metalness:0.0, flatShading:false });
  terrainMesh=new THREE.Mesh(terrainGeo, mat);
  terrainMesh.receiveShadow=true;
  scene.add(terrainMesh);

  // Water plane
  const waterMat=new THREE.MeshStandardMaterial({ color:0x1c4a78, transparent:true, opacity:0.82, roughness:0.25, metalness:0.4 });
  const water=new THREE.Mesh(new THREE.PlaneGeometry(26000,26000), waterMat);
  water.rotateX(-Math.PI/2); water.position.y=SEA_LEVEL;
  scene.add(water);
}

function buildRunway(){
  // Runway aligned along +X, at a flat clearing near origin (we flatten area)
  const geo = new THREE.PlaneGeometry(runwayLen, runwayWidth, 1, 1);
  geo.rotateX(-Math.PI/2);
  const mat = new THREE.MeshStandardMaterial({ color:0x2b2f36, roughness:0.9 });
  const mesh = new THREE.Mesh(geo, mat);
  const ry = terrainHeight(runwayLen/2+40, 0)+1.2;
  mesh.position.set(runwayLen/2+40, ry, 0);
  mesh.receiveShadow=true;
  scene.add(mesh);
  // Center line markings
  const lineMat=new THREE.MeshBasicMaterial({color:0xdfe7ef});
  for(let i=0;i<28;i++){
    const m=new THREE.Mesh(new THREE.PlaneGeometry(12,2), lineMat);
    m.rotateX(-Math.PI/2);
    m.position.set(-runwayLen/2+20+i*54, ry+0.15, 0);
    scene.add(m);
  }
  // Edge lights
  const edgeMat=new THREE.MeshBasicMaterial({color:0x8affc0});
  for(let i=0;i<=20;i++){
    const t=i/20; const x=-runwayLen/2+20+t*runwayLen;
    for(const s of [-1,1]){
      const l=new THREE.Mesh(new THREE.SphereGeometry(0.8,6,6), edgeMat);
      l.position.set(x, ry+1, s*runwayWidth/2);
      scene.add(l);
    }
  }
  runwayStart = new THREE.Vector3(-runwayLen/2+40, ry, 0);
  runwayDir = new THREE.Vector3(1,0,0);
}

// Draw a simple but recognizable plane mesh
function buildPlaneMesh(def, isEnemy){
  const g=new THREE.Group();
  const body=new THREE.MeshStandardMaterial({ color:def.color, roughness:0.4, metalness:0.5 });
  const accent=new THREE.MeshStandardMaterial({ color:def.accent, roughness:0.3, metalness:0.6, emissive:def.accent, emissiveIntensity:0.15 });
  const dark=new THREE.MeshStandardMaterial({ color:0x222833, roughness:0.6, metalness:0.3 });

  // Fuselage runs along +X (forward). Nose at +X, engine at -X.
  const fus=new THREE.Mesh(new THREE.CylinderGeometry(0.55,0.75,7,12), body);
  fus.rotation.z=-Math.PI/2; g.add(fus);
  // Nose cone at +X
  const nose=new THREE.Mesh(new THREE.ConeGeometry(0.55,2.2,12), accent);
  nose.rotation.y=Math.PI/2; nose.position.x=4.5; g.add(nose);
  // Cockpit
  const cock=new THREE.Mesh(new THREE.SphereGeometry(0.5,12,8), new THREE.MeshStandardMaterial({color:0x8fd0ff, roughness:0.1, metalness:0.8, transparent:true, opacity:0.75}));
  cock.scale.set(1.6,0.8,1); cock.position.set(1.2,0.5,0); g.add(cock);
  // Main wings (span along +Y/-Y, chord along X)
  const wspan=Math.max(4, Math.sqrt(def.wingArea)*1.6);
  const wingShape=new THREE.Mesh(new THREE.BoxGeometry(2.0,0.12,wspan), body);
  wingShape.position.set(0.2,0,0); g.add(wingShape);
  // Tail wing
  const tw=new THREE.Mesh(new THREE.BoxGeometry(1.0,0.1,wspan*0.42), body);
  tw.position.set(-3.2,0.1,0); g.add(tw);
  // Vertical stabilizer
  const tailV=new THREE.Mesh(new THREE.BoxGeometry(1.2,1.4,0.15), accent);
  tailV.position.set(-3.0,0.7,0); g.add(tailV);
  // Engine bell at -X
  const bell=new THREE.Mesh(new THREE.TorusGeometry(0.5,0.12,8,12), dark);
  bell.rotation.y=Math.PI/2; bell.position.x=-3.5; g.add(bell);
  // Engine glow at -X
  const glow=new THREE.Mesh(new THREE.CircleGeometry(0.45,12), new THREE.MeshBasicMaterial({color:0xff9030, transparent:true, opacity:0.85}));
  glow.rotation.y=Math.PI/2; glow.rotation.z=-Math.PI/2; glow.position.x=-3.55; glow.name='glow'; g.add(glow);

  g.traverse(o=>{ if(o.isMesh){ o.castShadow=true; }});
  return g;
}

/* ============================================================
   5. INPUT
   ============================================================ */
const input={ mx:0, my:0, throttle:0, throttleTarget:0, yaw:0, boost:false, brake:false, cannons:false, fire:false };
let mouseLocked=false;
function initInput(){
  addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); });
  addEventListener('mousemove',e=>{
    if(mouseLocked){ input.mx += (e.movementX||0); input.my += (e.movementY||0); }
  });
  addEventListener('keydown',e=>{
    switch(e.code){
      case 'KeyW': input.throttleTarget=Math.min(1,input.throttleTarget+0.08); break;
      case 'KeyS': input.throttleTarget=Math.max(0,input.throttleTarget-0.08); break;
      case 'KeyA': input.yaw=1; break;
      case 'KeyD': input.yaw=-1; break;
      case 'ShiftLeft': case 'ShiftRight': input.boost=true; break;
      case 'ControlLeft': case 'ControlRight': input.boost=false; break;
      case 'Space': input.brake=true; break;
      case 'KeyF': input.fire=true; break;
      case 'KeyC': switchCamera(); break;
      case 'KeyP': togglePause(); break;
    }
  });
  addEventListener('keyup',e=>{
    switch(e.code){
      case 'KeyW': case 'KeyS': input.throttleTarget=0.5; break; // momentary
      case 'KeyA': input.yaw=0; break;
      case 'KeyD': input.yaw=0; break;
      case 'ShiftLeft': case 'ShiftRight': input.boost=false; break;
      case 'ControlLeft': case 'ControlRight': input.boost=false; break;
      case 'Space': input.brake=false; break;
      case 'KeyF': input.fire=false; break;
    }
  });
  renderer && renderer.domElement.addEventListener('click',()=>{ if(game.state==='fly'&&!game.paused) renderer.domElement.requestPointerLock(); });
}

/* ============================================================
   6. FLIGHT CONTROLLER (physics state)
   ============================================================ */
function makePlane(def, isEnemy){
  return {
    def, isEnemy,
    pos:new THREE.Vector3(),
    vel:new THREE.Vector3(),
    quat:new THREE.Quaternion(),
    angularVel:new THREE.Vector3(), // body space (pqr)
    throttle:0, rpm:0, heat:0.2, fuel:1,
    onGround:false, stalled:false,
    alive:true,
    // enemy AI
    aiState:'hunt', aiTarget:null, aiTimer:0,
    // trail
    canFire:true, cooldown:0,
  };
}

// The physics core. dt in seconds.
function updatePlane(p, dt){
  const def=p.def;
  dt=Math.min(dt,0.033); // clamp for stability

  // ---- Orientation helpers ----
  const forward=new THREE.Vector3(1,0,0).applyQuaternion(p.quat);
  const up=new THREE.Vector3(0,1,0).applyQuaternion(p.quat);
  const right=new THREE.Vector3(0,0,1).applyQuaternion(p.quat);

  // ---- Angle of attack & sideslip ----
  // velocity in body frame
  const velBody=p.vel.clone().applyQuaternion(p.quat.conjugate());
  const vMag=velBody.length();
  const vx=velBody.x, vy=velBody.y, vz=velBody.z;
  // Angle of attack (pitch of velocity relative to fuselage)
  const aoa=Math.atan2(vy, Math.hypot(vx,vz));
  // sideslip
  const beta=Math.atan2(vz, Math.hypot(vx,vy));

  // ---- Air density ----
  const rho=airDensity(p.pos.y);

  // ---- Lift ----
  // Effective Cl = liftSlope * aoa (linear region), capped at stall
  let Cl;
  const aoaStall=def.stallAoA;
  if(Math.abs(aoa)<=aoaStall){
    Cl=def.liftSlope*aoa;
  } else {
    // Post-stall: lift drops
    const s=Math.sign(aoa);
    const overAoA=Math.max(0,Math.abs(aoa)-aoaStall);
    Cl= s*def.maxLift*Math.exp(-2.5*overAoA);
  }
  p.stalled = Math.abs(aoa)>aoaStall*0.9 && vMag>20;

  const q=0.5*rho*vMag*vMag;               // dynamic pressure
  const liftMag=q*def.wingArea*Cl;
  // Lift direction: perpendicular to velocity, in the plane of up & velocity (pull up)
  // Use "up" projected perpendicular to velocity
  const vNorm=velBody.clone().normalize();
  let liftDir=up.clone().sub(vNorm.clone().multiplyScalar(up.dot(vNorm))).normalize();
  if(liftDir.lengthSq()<1e-6) liftDir=up.clone();
  const lift=liftDir.multiplyScalar(liftMag);

  // ---- Drag ----
  // Profile drag (CD0) + induced drag (ki*Cl^2) + extra (speed brakes)
  let CD=def.profileDrag + def.inducedFactor*Cl*Cl + (p.brake?0.06:0);
  const dragDir=velBody.lengthSq()>1e-6? velBody.clone().normalize().multiplyScalar(-1):new THREE.Vector3();
  const dragMag=q*def.wingArea*CD;
  const drag=dragDir.multiplyScalar(dragMag);

  // ---- Thrust ----
  // Thrust falls off with air density (jet/prop)
  const thrustMag=p.throttle*def.maxThrust*Math.max(0.35, rho/SEA_DENSITY);
  const thrust=forward.clone().multiplyScalar(thrustMag);

  // ---- Gravity ----
  const weight=new THREE.Vector3(0,-def.mass*G,0);

  // ---- Sum forces -> acceleration ----
  const acc=lift.add(drag).add(thrust).add(weight).divideScalar(def.mass);
  // Integrate velocity
  p.vel.addScaledVector(acc,dt);

  // ---- Aerodynamic damping / control torques ----
  // Control effectiveness scales with airspeed (need airflow on surfaces)
  const controlEff=Math.min(1, vMag/40); // needs ~40 m/s for full authority
  const boostMul=input.boost?1.6:1;

  // Desired angular accel from input (enemy handled separately)
  // Body axes: x=forward(pitch about Y), z=right(roll about X)
  let pitchCmd=0, rollCmd=0, yawCmd=0;
  if(!p.isEnemy){
    pitchCmd=input.my*def.pitchRate;    // pull back (my>0) -> nose up (positive AOA)
    rollCmd=input.mx*def.rollRate;      // move right (mx>0) -> bank right
    yawCmd=input.yaw*def.yawRate;
  } else {
    applyEnemyControl(p, dt);
  }

  // Angular acceleration toward commanded rates, with inertia (damped 2nd-order)
  const targetOmega=new THREE.Vector3(
    rollCmd*controlEff*boostMul,     // roll about body X
    yawCmd*controlEff*boostMul,      // yaw about body Y
    pitchCmd*controlEff*boostMul     // pitch about body Z
  );
  // Yaw about body-y is small effect; add weathercock stability (align beta)
  // Damp angular velocity toward commanded (inertia lag)
  const damp=1-Math.exp(-6*dt);
  const newOm=new THREE.Vector3();
  newOm.x=p.angularVel.x+(targetOmega.x-p.angularVel.x)*damp;
  newOm.y=p.angularVel.y+(targetOmega.y-p.angularVel.y)*damp;
  newOm.z=p.angularVel.z+(targetOmega.z-p.angularVel.z)*damp;
  // Add sideslip weathercock (yaw to zero beta)
  newOm.y += -beta*2.5*controlEff*dt*(1-damp);
  p.angularVel.copy(newOm);
  // Natural damping of spin (gentle decay toward zero rotation)
  p.angularVel.multiplyScalar(Math.exp(-2*dt));

  // ---- Apply rotation (body rates) ----
  // qdot = 0.5 * omega ⊗ q
  const w=p.angularVel;
  const quat=p.quat;
  let qdot=new THREE.Quaternion(w.x*0.5, w.y*0.5, w.z*0.5, 0).multiply(quat);
  quat.x+=qdot.x*dt; quat.y+=qdot.y*dt; quat.z+=qdot.z*dt; quat.w+=qdot.w*dt;
  quat.normalize();

  // ---- Integrate position ----
  p.pos.addScaledVector(p.vel,dt);

  // ---- Terrain / ground collision ----
  const ground=terrainHeight(p.pos.x,p.pos.z)+ (p.onGround?1.5:0.5);
  if(p.pos.y<ground){
    p.pos.y=ground;
    // Ground interaction: kill downward velocity, apply friction
    if(p.vel.y<0){
      if(p.onGround){
        p.vel.y=0;
        // Rolling resistance (small) + speed brakes; thrust must overcome this to accelerate.
        const fric = p.brake ? 0.12 : 0.02;
        const hs = Math.hypot(p.vel.x, p.vel.z);
        if(hs > 0.05){
          const factor = Math.max(0, 1 - fric*G*dt/hs);
          p.vel.x *= factor;
          p.vel.z *= factor;
        }
      } else {
        p.vel.y=0;
        p.vel.multiplyScalar(0.7);
      }
    }
    if(!p.onGround){
      // Just touched ground
      p.onGround=true;
    }
  } else {
    p.onGround=false;
  }

  // ---- Fuel & heat & rpm ----
  p.rpm = Math.min(100, p.rpm + (p.throttle*100 - p.rpm)*(1-Math.exp(-3*dt)));
  p.heat = Math.min(1.6, p.heat + (p.throttle*0.9 + p.rpm*0.002 - 0.15)*(1-Math.exp(-2*dt)));
  const fuelUse = (0.00002 + p.throttle*0.00025)*def.mass*dt;
  p.fuel = Math.max(0, p.fuel - fuelUse);

  return { aoa, vMag, q, liftMag, dragMag, Cl, CD };
}

/* ============================================================
   7. ENEMY AI
   ============================================================ */
function applyEnemyControl(p, dt){
  const target=game.player;
  if(!target||!target.alive){ p.aiState='circle'; return; }
  p.aiTimer-=dt;
  const toEnemy=target.pos.clone().sub(p.pos);
  const dist=toEnemy.length();
  const ahead=toEnemy.normalize();

  // Compute desired orientation via lookAt
  const desired=new THREE.Object3D();
  desired.position.copy(p.pos);
  desired.lookAt(target.pos);
  const desiredQuat=new THREE.Quaternion().setFromEuler(new THREE.Euler(desired.rotation.x, desired.rotation.y, desired.rotation.z));

  // Blend current heading toward desired (turn limited by roll/pitch rate)
  const cur=new THREE.Object3D(); cur.quaternion.copy(p.quat);
  const curF=new THREE.Vector3(1,0,0).applyQuaternion(p.quat);

  // Lead target slightly
  const leadTime=dist/(game.playerBulletSpeed||900);
  // (simplified: aim at current pos with small lead)

  // Turn rate limit
  const turnRate=1.8*dt;
  const newF=curF.clone().lerp(ahead, Math.min(1,turnRate)); newF.normalize();
  // Rebuild quaternion: keep up roughly level
  const q=new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1,0,0), newF);
  // blend
  p.quat.copy(p.quat.clone().slerp(q, Math.min(1,turnRate*1.5))).normalize();

  // Maintain distance: climb/descend to stay in band
  const desiredAlt=600+Math.sin(game.time*0.3)*200;
  const altErr=desiredAlt-p.pos.y;
  // pitch up/down by adjusting target euler
  const up=new THREE.Vector3(0,1,0).applyQuaternion(p.quat);
  // banking for turn
  const dotF=curF.dot(ahead);
  const bank=Math.max(-1,Math.min(1,(dotF-0.7)/0.3));
  p.bank=bank;

  // Fire when roughly aimed & in range
  if(dotF>0.98 && dist<1200){
    p.fireTimer=(p.fireTimer||0)-dt;
    if(p.fireTimer<=0){ fireProjectile(p, false); p.fireTimer=1.2; }
  }
  // throttle: maintain speed
  if(p.vel.length()<120) p.throttle=Math.min(1,p.throttle+0.02*dt*30);
  else p.throttle=Math.max(0.4,p.throttle-0.01*dt*30);
}

/* ============================================================
   8. PROJECTILES (cannons)
   ============================================================ */
let projectiles=[];
const playerBulletSpeed=1100;
function fireProjectile(p, isPlayer){
  const f=new THREE.Vector3(1,0,0).applyQuaternion(p.quat);
  const origin=p.pos.clone().addScaledVector(f,6);
  const geo=new THREE.SphereGeometry(0.6,6,6);
  const mat=new THREE.MeshBasicMaterial({ color:isPlayer?0x8affc0:0xff5050 });
  const m=new THREE.Mesh(geo,mat);
  m.position.copy(origin);
  scene.add(m);
  projectiles.push({ mesh:m, vel:f.clone().multiplyScalar(playerBulletSpeed + p.vel.length()*0.3), life:2.5, isPlayer });
}

/* ============================================================
   9. GAME STATE / FLOW
   ============================================================ */
const game={
  state:'menu', // menu | fly | over
  mode:'takeoff', // takeoff | dogfight
  aircraftIndex:0,
  player:null,
  enemies:[],
  time:0,
  paused:false,
  camMode:0,
  kills:0,
  timeLeft:90,
  missionDone:false,
  playerBulletSpeed,
};

// Menu building
let selectedAircraft=0, selectedMode='takeoff';
function buildMenu(){
  const grid=document.getElementById('aircraft-grid');
  grid.innerHTML='';
  AIRCRAFT.forEach((a,i)=>{
    const c=document.createElement('div');
    c.className='card'+(i===0?' selected':'');
    c.dataset.i=i;
    const pct=v=>Math.round(v*100)+'%';
    const bar=(label,val)=>`<div class="stat"><span>${label}</span><span class="bar"><i style="width:${pct(val)}"></i></span><span class="v">${Math.round(val*100)}</span></div>`;
    c.innerHTML=`<h3>${a.name}</h3><div class="role">${a.role}</div>
      ${bar('Speed',a.maxThrust/a.mass)}
      ${bar('Agility',a.rollRate)}
      ${bar('Lift',a.maxLift)}
      ${bar('Thrust',a.weightRatio)}`;
    c.onclick=()=>{ document.querySelectorAll('#aircraft-grid .card').forEach(x=>x.classList.remove('selected')); c.classList.add('selected'); selectedAircraft=i; };
    grid.appendChild(c);
  });
  const mg=document.getElementById('mode-grid');
  mg.innerHTML='';
  const modes=[
    {id:'takeoff',t:'Takeoff & Landing',d:'Approach the runway in wind, rotate at the right speed, and touch down gently on the centerline.'},
    {id:'dogfight',t:'Dogfight',d:'Pursue and shoot down enemy fighters while managing speed, energy and G-load.'},
  ];
  modes.forEach(mo=>{
    const d=document.createElement('div');
    d.className='mode'+(mo.id===selectedMode?' selected':'');
    d.dataset.id=mo.id;
    d.innerHTML=`<h3>${mo.t}</h3><p>${mo.d}</p>`;
    d.onclick=()=>{ document.querySelectorAll('#mode-grid .mode').forEach(x=>x.classList.remove('selected')); d.classList.add('selected'); selectedMode=mo.id; };
    mg.appendChild(d);
  });
  document.getElementById('fly-btn').onclick=startMission;
}

function message(txt, dur){
  const el=document.getElementById('message');
  el.textContent=txt; el.classList.add('show');
  clearTimeout(message._t);
  message._t=setTimeout(()=>el.classList.remove('show'), dur||3000);
}

function startMission(){
  game.aircraftIndex=selectedAircraft;
  game.mode=selectedMode;
  game.state='fly';
  game.time=0; game.kills=0; game.timeLeft=90; game.missionDone=false; game.paused=false;

  document.getElementById('menu').classList.add('hidden');
  document.getElementById('hud').classList.remove('hidden');

  // Build player
  const def=AIRCRAFT[game.aircraftIndex];
  game.player=makePlane(def,false);
  if(game.mode==='takeoff'){
    game.player.pos.copy(runwayStart);
    game.player.vel.set(0,0,0);
  } else {
    game.player.pos.set(0, 700, 0);
    game.player.vel.set(150,0,0);
  }

  // Build mesh
  aircraftGroup=buildPlaneMesh(def,false);
  scene.add(aircraftGroup);

  // Enemies for dogfight
  game.enemies=[];
  if(game.mode==='dogfight'){
    enemyGroup=new THREE.Group(); scene.add(enemyGroup);
    const defs=[AIRCRAFT[1], AIRCRAFT[0], AIRCRAFT[1]];
    defs.forEach((d,i)=>{
      const e=makePlane(d,true);
      const ang=i*(Math.PI*2/3);
      e.pos.set(Math.cos(ang)*2200, 650+Math.random()*300, Math.sin(ang)*2200);
      e.vel.set(Math.cos(ang+1.5)*220,0,Math.sin(ang+1.5)*220);
      game.enemies.push(e);
      enemyGroup.add(buildPlaneMesh(d,true));
    });
    document.getElementById('hud-timer').classList.remove('hidden');
    document.getElementById('hud-kills').classList.remove('hidden');
    document.getElementById('crosshair').style.opacity='1';
    message('DOWN ENEMIES — 90s',3000);
  }

  // pointer lock + audio start
  setTimeout(()=>{ if(renderer.domElement) renderer.domElement.requestPointerLock(); },50);
  initAudio();
  audio.start();


}

function endMission(win, title, text){
  game.state='over';
  const b=document.getElementById('banner');
  document.getElementById('banner-title').textContent=title;
  document.getElementById('banner-text').textContent=text;
  document.getElementById('banner-btn').textContent='Play Again';
  b.classList.remove('hidden');
  document.exitPointerLock && document.exitPointerLock();
  document.getElementById('banner-btn').onclick=()=>{ b.classList.add('hidden'); document.getElementById('hud').classList.add('hidden'); document.getElementById('menu').classList.remove('hidden'); game.state='menu'; audio.stop(); };
}

function togglePause(){
  if(game.state!=='fly') return;
  game.paused=!game.paused;
  const b=document.getElementById('banner');
  if(game.paused){
    document.getElementById('banner-title').textContent='Paused';
    document.getElementById('banner-text').textContent='Take a breath, pilot.';
    document.getElementById('banner-btn').textContent='Resume';
    b.classList.remove('hidden');
  } else {
    b.classList.add('hidden');
  }
}
document.getElementById('banner-btn').addEventListener('click',()=>{ if(game.state==='fly') togglePause(); });

/* ============================================================
   10. CAMERA
   ============================================================ */
let camDist=22, camHeight=7;
function switchCamera(){ game.camMode=(game.camMode+1)%3; }
function updateCamera(dt){
  const p=game.player;
  const f=new THREE.Vector3(1,0,0).applyQuaternion(p.quat);
  const u=new THREE.Vector3(0,1,0).applyQuaternion(p.quat);
  const r=new THREE.Vector3(0,0,1).applyQuaternion(p.quat);

  if(game.camMode===0){ // chase
    const target=p.pos.clone().addScaledVector(f,camDist).addScaledVector(u,-camHeight*0.4);
    camera.position.lerp(target, 1-Math.exp(-6*dt));
    const look=p.pos.clone().addScaledVector(f,30);
    camera.lookAt(look);
  } else if(game.camMode===1){ // cockpit
    camera.position.copy(p.pos.clone().addScaledVector(f,1.2).addScaledVector(u,0.5));
    camera.quaternion.copy(p.quat);
  } else { // orbit-ish top
    const target=p.pos.clone().addScaledVector(u,40);
    camera.position.lerp(target,1-Math.exp(-4*dt));
    camera.lookAt(p.pos);
  }
  // Keep camera above terrain
  if(camera.position.y<terrainHeight(camera.position.x,camera.position.z)+8){
    camera.position.y=terrainHeight(camera.position.x,camera.position.z)+8;
  }
}

/* ============================================================
   11. AUDIO (WebAudio, dynamic)
   ============================================================ */
let audio=null;
function initAudio(){
  if(audio) return;
  const AC=window.AudioContext||window.webkitAudioContext;
  audio=new AC();
  const ctx=audio.ctx;
  audio.master=ctx.createGain(); audio.master.gain.value=0.9; audio.master.connect(ctx.destination);

  // --- Engine: 2 oscillators (base + harmonic) + sub, filtered ---
  const engineGain=ctx.createGain(); engineGain.gain.value=0.25; engineGain.connect(audio.master);
  const engineFilter=ctx.createBiquadFilter();
  engineFilter.type='lowpass'; engineFilter.frequency.value=900; engineFilter.Q.value=1.5;
  engineFilter.connect(engineGain);
  const osc1=ctx.createOscillator(); osc1.type='sawtooth'; osc1.frequency.value=60;
  const osc2=ctx.createOscillator(); osc2.type='square'; osc2.frequency.value=120;
  const sub=ctx.createOscillator(); sub.type='sine'; sub.frequency.value=40;
  const subGain=ctx.createGain(); subGain.gain.value=0.3; sub.connect(engineGain);
  osc1.connect(engineFilter); osc2.connect(engineFilter);
  osc1.start(); osc2.start(); sub.start();

  // --- Wind: noise through lowpass ---
  const bufferSize=2*ctx.sampleRate;
  const noiseBuffer=ctx.createBuffer(1,bufferSize,ctx.sampleRate);
  const nd=noiseBuffer.getChannelData(0);
  for(let i=0;i<bufferSize;i++) nd[i]=Math.random()*2-1;
  const windSrc=ctx.createBufferSource(); windSrc.buffer=noiseBuffer; windSrc.loop=true;
  const windFilter=ctx.createBiquadFilter(); windFilter.type='lowpass'; windFilter.frequency.value=500; windFilter.Q.value=0.5;
  const windGain=ctx.createGain(); windGain.gain.value=0.0;
  windSrc.connect(windFilter); windFilter.connect(windGain); windGain.connect(audio.master); windSrc.start();

  audio.engineGain=engineGain; audio.engineFilter=engineFilter;
  audio.osc1=osc1; audio.osc2=osc2; audio.sub=sub; audio.subGain=subGain;
  audio.windGain=windGain; audio.windFilter=windFilter;

  // --- Alarm / stall ---
  const alarm=ctx.createOscillator(); alarm.type='square'; alarm.frequency.value=1200;
  const alarmGain=ctx.createGain(); alarmGain.gain.value=0;
  alarm.connect(alarmGain); alarmGain.connect(audio.master); alarm.start();
  audio.alarm=alarm; audio.alarmGain=alarmGain;

  // --- Explosion ---
  audio.explosion=()=>{
    const n=ctx.createBufferSource();
    const b=ctx.createBuffer(1,ctx.sampleRate*0.6,ctx.sampleRate);
    const d=b.getChannelData(0);
    for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,2);
    n.buffer=b;
    const g=ctx.createGain(); g.gain.value=0.6;
    const f=ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=800;
    n.connect(f); f.connect(g); g.connect(audio.master); n.start();
  };

  audio._t=0;
}
function updateAudio(dt,p){
  if(!audio) return;
  const ctx=audio.ctx; const t=ctx.currentTime;
  const rpm=p.rpm/100, thr=p.throttle, spd=p.vel.length();
  // Engine pitch ~ idle 55Hz to redline ~ 320Hz scaled by rpm
  const baseFreq=55+rpm*280;
  audio.osc1.frequency.setTargetAtTime(baseFreq,t,0.08);
  audio.osc2.frequency.setTargetAtTime(baseFreq*2,t,0.08);
  audio.sub.frequency.setTargetAtTime(baseFreq*0.5,t,0.08);
  audio.engineFilter.frequency.setTargetAtTime(600+thr*1400,t,0.1);
  audio.engineGain.gain.setTargetAtTime(0.15+thr*0.35+rpm*0.05,t,0.1);
  // Wind with speed & altitude (thinner air = less wind even at speed)
  const rho=airDensity(p.pos.y);
  const windAmt=Math.min(0.5, (spd/250)*(rho/SEA_DENSITY)+0.03);
  audio.windGain.gain.setTargetAtTime(windAmt,t,0.15);
  audio.windFilter.frequency.setTargetAtTime(300+spd*6,t,0.15);
  // Stall alarm
  if(p.stalled){
    audio.alarmGain.gain.setTargetAtTime(0.12,t,0.05);
    audio.alarm.frequency.setTargetAtTime(1100+Math.sin(t*12)*300,t,0.05);
  } else {
    audio.alarmGain.gain.setTargetAtTime(0,t,0.1);
  }
}

/* ============================================================
   12. HUD UPDATE
   ============================================================ */
const attCanvas=document.getElementById('att-canvas');
const attCtx=attCanvas.getContext('2d');
function drawAttitude(p){
  const w=220,h=220,cx=w/2,cy=h/2;
  attCtx.clearRect(0,0,w,h);
  attCtx.save();
  attCtx.beginPath(); attCtx.arc(cx,cy,w/2,0,true); attCtx.clip();
  // rotate by roll, translate by pitch
  const roll=-p.quat.toEuler(new THREE.Euler(0,0,0,'YXZ')).z;
  const pitch=(p.quat.toEuler(new THREE.Euler('YXZ')).x);
  attCtx.translate(cx,cy);
  attCtx.rotate(roll);
  const off=pitch*140;
  // sky & ground
  attCtx.fillStyle='#3a7bd5'; attCtx.fillRect(-200,-200,400,200+off);
  attCtx.fillStyle='#6b4a2a'; attCtx.fillRect(-200,off,400,200);
  attCtx.strokeStyle='#ffffff'; attCtx.lineWidth=2;
  attCtx.beginPath(); attCtx.moveTo(-200,off); attCtx.lineTo(200,off); attCtx.stroke();
  attCtx.restore();
  // Fixed aircraft symbol
  attCtx.strokeStyle='#8affc0'; attCtx.lineWidth=2.5;
  attCtx.beginPath(); attCtx.moveTo(cx-40,cy); attCtx.lineTo(cx-12,cy); attCtx.moveTo(cx+12,cy); attCtx.lineTo(cx+40,cy); attCtx.stroke();
  attCtx.beginPath(); attCtx.moveTo(cx,cy-3); attCtx.lineTo(cx,cy+3); attCtx.stroke();
  // Roll ticks
  attCtx.fillStyle='#8affc0';
  for(const a of [-60,-30,30,60]){ attCtx.save(); attCtx.translate(cx,cy); attCtx.rotate(a*Math.PI/180); attCtx.fillRect(-1,-96,4,10); attCtx.restore(); }
}

function updateHUD(){
  const p=game.player;
  const spd=Math.round(p.vel.length()*3.6); // m/s -> km/h
  const alt=Math.max(0,Math.round(p.pos.y));
  document.getElementById('v-alt').textContent=alt;
  document.getElementById('v-spd').textContent=spd;
  // pitch in degrees
  const euler=new THREE.Euler().setFromQuaternion(p.quat,'YXZ');
  let degPitch=Math.round(THREE.MathUtils.radToDeg(euler.x));
  let degHdg=Math.round(THREE.MathUtils.radToDeg(euler.y));
  degHdg=((degHdg%360)+360)%360;
  document.getElementById('v-pitch').textContent=(degPitch>=0?'+':'')+degPitch;
  document.getElementById('v-hdg').textContent=String(degHdg).padStart(3,'0');
  // engine
  document.getElementById('v-rpm').textContent=Math.round(p.rpm);
  document.getElementById('rpm-bar').style.width=Math.min(100,p.rpm)+'%';
  document.getElementById('heat-bar').style.width=Math.min(100,p.heat*100)+'%';
  document.getElementById('fuel-bar').style.width=(p.fuel*100)+'%';
  document.getElementById('v-thr').textContent=Math.round(p.throttle*100);
  document.getElementById('thr-bar').style.width=(p.throttle*100)+'%';
  // stall
  document.getElementById('stall-warn').classList.toggle('show', p.stalled && !p.onGround);
  // timer / kills
  if(game.mode==='dogfight'){
    document.getElementById('v-timer').textContent=Math.max(0,Math.ceil(game.timeLeft));
    document.getElementById('v-kills').textContent=game.kills+'/'+game.enemies.length;
  }
  drawAttitude(p);
}

/* ============================================================
   13. MAIN LOOP
   ============================================================ */
let lastFireShot=0;
function animate(){
  requestAnimationFrame(animate);
  const dt=Math.min(clock.getDelta(),0.05);
  if(game.state==='fly' && !game.paused){
    game.time+=dt;

    // --- Player input smoothing (throttle) ---
    const p=game.player;
    p.throttleTarget=input.throttleTarget;
    p.throttle += (p.throttleTarget-p.throttle)*(1-Math.exp(-4*dt));

    // --- Fire cannons ---
    p.cooldown-=dt;
    if(p.fire && p.cooldown<=0 && p.fuel>0){
      fireProjectile(p,true);
      p.cooldown=0.12;
    }

    // --- Physics ---
    const info=updatePlane(p,dt);
    // sync mesh
    if(aircraftGroup){
      aircraftGroup.position.copy(p.pos);
      aircraftGroup.quaternion.copy(p.quat);
      const glow=aircraftGroup.getObjectByName('glow');
      if(glow) glow.material.opacity=0.3+p.throttle*0.7;
    }

    // --- Dogfight logic ---
    if(game.mode==='dogfight'){
      game.timeLeft-=dt;
      // update enemies
      game.enemies.forEach((e,idx)=>{
        if(!e.alive) return;
        updatePlane(e,dt);
        const m=enemyGroup.children[idx];
        if(m){ m.position.copy(e.pos); m.quaternion.copy(e.quat); }
      });
      // projectiles
      for(let i=projectiles.length-1;i>=0;i--){
        const pr=projectiles[i];
        pr.mesh.position.addScaledVector(pr.vel,dt);
        pr.life-=dt;
        let hit=false;
        if(pr.isPlayer){
          for(let j=0;j<game.enemies.length;j++){
            const e=game.enemies[j];
            if(e.alive && pr.mesh.position.distanceTo(e.pos)<12){
              e.alive=false; e.state='dead';
              game.kills++;
              if(game.enemies[j].isEnemy && enemyGroup.children[j]) enemyGroup.children[j].visible=false;
              audio&&audio.explosion();
              message('ENEMY DOWN!');
              hit=true;
              break;
            }
          }
        } else {
          if(pr.mesh.position.distanceTo(p.pos)<10){
            // player hit — reduce fuel, flash
            p.fuel=Math.max(0,p.fuel-0.15);
            audio&&audio.explosion();
            hit=true;
          }
        }
        if(pr.life<=0||hit){ scene.remove(pr.mesh); projectiles.splice(i,1); }
      }
      // lock-on display
      // (simplified)
      if(game.timeLeft<=0 && !game.missionDone){
        game.missionDone=true;
        endMission(game.kills>=game.enemies.length*0.6?1:0,
          game.kills>=Math.ceil(game.enemies.length*0.6)?'VICTORY':'MISSION OVER',
          `Enemies down: ${game.kills}/${game.enemies.length}`);
      }
    }

    // --- Takeoff/Landing checks ---
    if(game.mode==='takeoff'){
      if(!p.onGround && p.vel.length()>5){
        p.groundedJustLeft=true;
      }
      // landing scoring
      if(p.pos.y<=terrainHeight(p.pos.x,p.pos.z)+1.6 && p.vel.length()<45){
        // on runway & slow -> good
      }
      // Game over if crashed hard
      const eulerY=new THREE.Euler().setFromQuaternion(p.quat,'YXZ');
      const bank=Math.abs(eulerY.z);
      if(p.onGround && bank>1.4 && p.vel.length()>30){
        message('CRASHED — wing struck ground',4000);
        endMission(0,'CRASH','You spun out on the runway. Manage bank angle on touchdown.');
      }
    }

    // --- Out of bounds reset ---
    if(p.pos.y<-50){ p.pos.set(0,800,0); p.vel.set(120,0,0); message('Bail out — respawned'); }

    // --- Camera ---
    updateCamera(dt);
    // --- Audio ---
    updateAudio(dt,p);
    // --- HUD ---
    updateHUD();
  } else if(game.state==='fly' && game.paused){
    // keep camera static
  }
  renderer.render(scene,camera);
}

/* ============================================================
   14. BOOT
   ============================================================ */
function boot(){
  buildMenu();
  initThree();
  initInput();
  animate();
  message('Select your aircraft and mission.',4000);
}
window.addEventListener('DOMContentLoaded', boot);
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"a1e853c1-ad77-4abb-bd16-75f97d70fbcc","tokensIn":4472688,"tokensOut":134258,"tokensTotal":4606946,"cost":0,"turns":59,"toolCalls":58,"failedToolCalls":1,"timestamp":"2026-08-23T22:20:07.631Z"} -->