← Flight Simulator results

Flight Simulator

Tiel Coder 35B A3B GGUF MTP · full

◈Preview unavailable — try playing the result

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

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 54,662 bytes · SHA-256 42f0902b7822
<!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: #05070c; font-family: 'Consolas', 'Courier New', monospace; color: #dfe9f0; }
  #app { position: fixed; inset: 0; }
  canvas { display: block; }

  /* ---------- Menu ---------- */
  #menu {
    position: fixed; inset: 0; z-index: 50;
    background: radial-gradient(circle at 50% 30%, #16324d 0%, #070b13 70%);
    display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
    padding-top: 4vh; overflow-y: auto;
  }
  #menu h1 {
    font-size: clamp(38px, 7vw, 84px); font-weight: 800; letter-spacing: 6px;
    background: linear-gradient(90deg, #5fd0ff, #e9f6ff, #ff9b5f);
    -webkit-background-clip: text; background-clip: text; color: transparent;
    text-shadow: 0 0 40px rgba(95,208,255,0.15);
  }
  #menu .sub { color: #7fa3bd; letter-spacing: 4px; margin: 6px 0 26px; font-size: 14px; text-transform: uppercase; }
  .row { display: flex; gap: 22px; flex-wrap: wrap; justify-content: center; max-width: 1150px; margin-bottom: 26px; }
  .card {
    width: 300px; background: rgba(16,30,46,0.72); border: 1px solid #26496a;
    border-radius: 12px; padding: 16px 16px 14px; cursor: pointer; transition: .18s;
    backdrop-filter: blur(3px);
  }
  .card:hover { border-color: #5fd0ff; transform: translateY(-4px); box-shadow: 0 10px 30px rgba(95,208,255,0.2); }
  .card.selected { border-color: #7fe3ff; box-shadow: 0 0 0 2px #5fd0ff, 0 10px 30px rgba(95,208,255,0.3); }
  .card canvas { width: 100%; height: 150px; border-radius: 8px; background: #04121f; }
  .card h3 { font-size: 20px; letter-spacing: 1px; margin: 10px 0 2px; }
  .card .role { font-size: 11px; color: #6fa8c8; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 10px; }
  .card .stat { display: flex; align-items: center; font-size: 11px; margin: 5px 0; color: #b7cddb; }
  .card .stat .lbl { width: 78px; color: #83a3bb; }
  .bar { flex: 1; height: 7px; background: #0c1a28; border-radius: 4px; overflow: hidden; margin-left: 8px; }
  .bar > i { display: block; height: 100%; background: linear-gradient(90deg,#2f7fbf,#5fd0ff); }
  .mode-title { color: #9fbfd4; letter-spacing: 3px; font-size: 13px; text-transform: uppercase; margin: 8px 0; }
  #modeRow { display:flex; gap:16px; margin-bottom: 22px; }
  .modebtn {
    padding: 12px 22px; background: rgba(16,30,46,0.7); border: 1px solid #26496a;
    border-radius: 8px; cursor: pointer; letter-spacing: 2px; font-size: 13px; font-family: inherit; color:#cfe1ef; transition:.18s;
  }
  .modebtn:hover { border-color:#5fd0ff; }
  .modebtn.selected { border-color:#7fe3ff; background: rgba(40,90,140,0.7); box-shadow: 0 0 0 1px #5fd0ff; }
  #startBtn {
    padding: 16px 54px; font-size: 18px; letter-spacing: 3px; font-family: inherit; font-weight: 700;
    color: #04121f; border: none; border-radius: 10px; cursor: pointer; transition: .18s;
    background: linear-gradient(90deg,#5fd0ff,#a0f0ff);
    box-shadow: 0 6px 24px rgba(95,208,255,0.35);
  }
  #startBtn:hover { transform: translateY(-2px); box-shadow: 0 10px 32px rgba(95,208,255,0.5); }
  #help { max-width: 720px; text-align: center; color: #6f93ad; font-size: 12px; line-height: 1.9; margin-top: 20px; letter-spacing: .5px; }
  #help b { color: #a9d0e8; }

  .card .preview-canvas { width: 100%; height: 150px; border-radius: 8px; background: #04121f; overflow: hidden; }
  .card .preview-canvas canvas { width: 100% !important; height: 100% !important; display: block; }

  /* ---------- HUD ---------- */
  #hud { position: fixed; inset: 0; z-index: 20; pointer-events: none; display: none; }
  .hud-hud { position: absolute; }
  #altBox { left: 18px; top: 18px; font-size: 13px; line-height: 1.7; color: #9fe0ff; text-shadow: 0 0 6px rgba(0,0,0,.9); }
  #altBox b { color: #eafaff; font-size: 20px; }
  #spdBox { right: 18px; top: 18px; text-align: right; font-size: 13px; line-height: 1.7; color: #9fe0ff; text-shadow: 0 0 6px rgba(0,0,0,.9); }
  #spdBox b { color: #eafaff; font-size: 20px; }
  #center { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); width: 260px; height: 260px; }
  #status { left: 18px; bottom: 18px; font-size: 12px; line-height: 1.7; color: #9fe0ff; max-width: 240px; text-shadow: 0 0 6px rgba(0,0,0,.9); }
  #status .warn { color: #ff7a5c; font-weight: 700; }
  #status .ok { color: #7dffa6; }
  #msg { position: absolute; left: 50%; top: 22%; transform: translateX(-50%); font-size: 26px; letter-spacing: 3px; color: #eafaff; text-shadow: 0 0 12px rgba(0,180,255,.8); display:none; text-align:center; }
  #msg small { display:block; font-size: 14px; color:#9fe0ff; letter-spacing:1px; margin-top:6px; }
  #crosshair { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:34px;height:34px; }
  #compass { left:50%; top:14px; transform:translateX(-50%); font-size:13px; color:#9fe0ff; letter-spacing:3px; }
  #hudWin { left:50%; top:50%; transform:translate(-50%,-50%); font-size:34px; color:#7dffa6; display:none; text-align:center; text-shadow:0 0 18px rgba(0,255,150,.6); }
  #hudWin small { display:block; font-size:15px; color:#9fe0ff; margin-top:10px; }
  #tip { left:50%; bottom:14px; transform:translateX(-50%); font-size:11px; color:#6f93ad; letter-spacing:1px; }
  #crosshair::before, #crosshair::after { content:''; position:absolute; background: rgba(120,230,255,.7); }
  #crosshair::before { left:50%; top:0; width:2px; height:100%; transform:translateX(-50%); }
  #crosshair::after { top:50%; left:0; height:2px; width:100%; transform:translateY(-50%); }
  #hudWin2 { position:fixed; inset:0; z-index:40; display:none; align-items:center; justify-content:center; flex-direction:column; background:rgba(4,8,14,.85); }
  #hudWin2 h2 { font-size:44px; letter-spacing:4px; margin-bottom:14px; }
  #hudWin2 button { margin-top:20px; padding:14px 40px; font-size:16px; letter-spacing:2px; font-family:inherit; color:#04121f; border:none; border-radius:8px; cursor:pointer; background:linear-gradient(90deg,#5fd0ff,#a0f0ff); }
  #hudWin2 .sub { color:#9fe0ff; font-size:14px; letter-spacing:2px; }
</style>
</head>
<body>
<div id="app"></div>

<!-- ================= MENU ================= -->
<div id="menu">
  <h1>APEX AERO</h1>
  <div class="sub">Realistic Flight Dynamics Simulator</div>

  <div class="mode-title">Select Aircraft</div>
  <div class="row" id="fleet"></div>

  <div class="mode-title">Mission Profile</div>
  <div id="modeRow">
    <div class="modebtn selected" data-mode="0">✈ Takeoff &amp; Landing</div>
    <div class="modebtn" data-mode="1">⚔ Dogfight</div>
  </div>

  <button id="startBtn">LAUNCH</button>

  <div id="help">
    <b>W/S</b> Pitch · <b>A/D</b> Roll · <b>Q/E</b> Yaw · <b>↑/↓</b> Throttle · <b>Shift/Ctrl</b> Brake · <b>Space</b> Flaps · <b>C</b> Camera · <b>M</b> Music<br>
    Realistic aerodynamics: lift &dquo; depends on air density × speed² × angle of attack. Stall below critical AoA · ground loop on hard landings · induced drag rises with lift.
  </div>
</div>

<!-- ================= HUD ================= -->
<div id="hud">
  <div id="status" class="hud-hud"></div>
  <div id="altBox" class="hud-hud"></div>
  <div id="spdBox" class="hud-hud"></div>
  <div id="compass" class="hud-hud"></div>
  <div id="center" class="hud-hud"><div id="crosshair"></div></div>
  <div id="msg" class="hud-hud"></div>
  <div id="tip" class="hud-hud"></div>
  <div id="hudWin" class="hud-hud"></div>
</div>

<div id="hudWin2">
  <div class="sub" id="winSub"></div>
  <h2 id="winTitle"></h2>
  <div class="sub" id="winStats"></div>
  <button id="backBtn">BACK TO HANGAR</button>
</div>

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

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

/* ============================================================
   APEX AERO — custom Newtonian flight dynamics
   ============================================================ */

// ---------- Utilities ----------
const clamp = (v,a,b)=> v<a?a:(v>b?b:v);
const lerp  = (a,b,t)=> a+(b-a)*t;
const THREE_ = THREE;
const V3 = (x,y,z)=> new THREE.Vector3(x,y,z);
let TAU = Math.PI*2;

// ---------- Aircraft definitions ----------
// Mass kg, wing m^2, max thrust N, induced-drag factor k, profile CD0,
// lift slope, stall AOA deg, max G.
const AIRCRAFT = [
  {
    id:'sparrow', name:'SV-7 Sparrow', role:'Fighter Jet',
    color:0x8fb8d6, accent:0x2a5f8a,
    mass: 7200, wing: 22, refArea: 22,
    maxThrust: 20000, maxPower: 26000,
    CD0: 0.026, k: 0.045,           // low drag -> fast
    CLa: 5.2, CLmax: 1.75, stallAoa: 17,
    rollRate: 2.6, pitchRate: 1.9, yawRate: 1.3,
    maxG: 9, fuelRate: 0.6,
    throttleToSpeed: 1.0
  },
  {
    id:'ranger', name:'CR-4 Ranger', role:'Cargo Transport',
    color:0xc9a25f, accent:0x6b4e22,
    mass: 26000, wing: 78, refArea: 78,
    maxThrust: 26000, maxPower: 34000,
    CD0: 0.042, k: 0.030,           // big wing -> low induced drag, high lift
    CLa: 4.6, CLmax: 2.1, stallAoa: 19,
    rollRate: 1.35, pitchRate: 1.0, yawRate: 0.8,
    maxG: 4.2, fuelRate: 1.1,
    throttleToSpeed: 0.75
  },
  {
    id:'falcon', name:'VF-3 Falcon', role:'Aerobatic',
    color:0xdadada, accent:0xc42b2b,
    mass: 1300, wing: 13, refArea: 13,
    maxThrust: 9000, maxPower: 12000,
    CD0: 0.029, k: 0.052,
    CLa: 6.0, CLmax: 2.35, stallAoa: 21,
    rollRate: 3.4, pitchRate: 2.7, yawRate: 1.9,
    maxG: 8, fuelRate: 0.4,
    throttleToSpeed: 1.15
  }
];

// ============================================================
//  MAIN
// ============================================================
let scene, camera, renderer, sky, sun, water, terrainMesh;
let worldGroup = new THREE.Group();

const state = {
  mode: 0,
  aircraftIdx: 0,
  running: false,
  clock: new THREE.Clock(),
  camMode: 0, // 0 chase, 1 cockpit, 2 external
};

// Physics state
const P = {
  pos: V3(0,0,0),
  vel: V3(0,0,0),        // world space m/s
  quat: new THREE.Quaternion(),
  bodyVel: V3(0,0,0),    // body space velocity
  pqr: V3(0,0,0),        // roll/pitch/yaw rates (body)
  throttle: 0,
  rpm: 0,
  heat: 0,
  fuel: 1,
  aoa: 0, beta: 0,
  cl: 0, cd: 0, cx: 0,
  lift: 0, loadG: 0,
  airDensity: 1.225,
  speed: 0,
  onGround: true,
  crashed: false,
  flaps: 0,
  // forces for audio
  lastAlpha: 0,
};

// Controls
const keys = {};
window.addEventListener('keydown', e => { keys[e.code]=true; if(['ArrowUp','ArrowDown','Space','ShiftLeft','ShiftRight'].includes(e.code)) e.preventDefault(); });
window.addEventListener('keyup',   e => { keys[e.code]=false; });

// ============================================================
//  RENDERER / SCENE
// ============================================================
function initRenderer() {
  scene = new THREE.Scene();
  scene.fog = new THREE.FogExp2(0xa9c9dd, 0.00065);

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

  camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.5, 60000);
  camera.position.set(0, 60, 120);

  // Lights
  const hemi = new THREE.HemisphereLight(0xbfd8ff, 0x4a5a3a, 0.7);
  scene.add(hemi);
  const sun = new THREE.DirectionalLight(0xfff2d6, 2.1);
  sun.position.set(300, 400, 200);
  sun.castShadow = true;
  sun.shadow.mapSize.set(2048,2048);
  const s = 1400;
  sun.shadow.camera.left=-s; sun.shadow.camera.right=s;
  sun.shadow.camera.top=s;  sun.shadow.camera.bottom=-s;
  sun.shadow.camera.near=100; sun.shadow.camera.far=2000;
  sun.shadow.bias = -0.0004;
  scene.add(sun);
  state.sun = sun;

  // Sky
  sky = new Sky();
  sky.scale.setScalar(400000);
  scene.add(sky);
  const sunU = sky.material.uniforms['sunPosition'];
  const su = V3(300,400,200).normalize();
  sunU.value.set(su.x, su.y, su.z);

  scene.add(worldGroup);
}

// ============================================================
//  TERRAIN  (procedural Perlin heightfield)
// ============================================================
const terrainSize = 8000;
const terrainSeg = 200;
let heightData = null;

function makeNoise() {
  // Perlin-ish value noise on a grid, cached
  const N = 256;
  const perm = new Uint8Array(512);
  const p = new Uint8Array(N);
  for(let i=0;i<N;i++) p[i]=i;
  let seed = 1337;
  const rnd = ()=> (seed = (seed*1103515245+12345)&0x7fffffff)/0x7fffffff;
  for(let i=N-1;i>0;i--){ const j=(rnd()*(i+1))|0; const t=p[i]; p[i]=p[j]; p[j]=t; }
  for(let i=0;i<512;i++) perm[i]=p[i&255];
  const fade = t=> t*t*t*(t*(t*6-15)+10);
  const grad = (h,x,y)=>{ const u=(h&1)?x:-x, v=(h&2)?y:-y; return u+v; };
  const noise = (x,y)=>{
    const X=Math.floor(x)&255, Y=Math.floor(y)&255;
    x-=Math.floor(x); y-=Math.floor(y);
    const u=fade(x), v=fade(y);
    const A=perm[X]+Y, B=perm[X+1]+Y;
    const mix=(a,t)=>lerp(grad(perm[a],x,y),grad(perm[a+1],x-1,y),t);
    return lerp(mix(A,u), mix(B,u), v);
  };
  return noise;
}

function terrainHeight(wx, wz) {
  const n = state._noise;
  let h = 0;
  let amp = 1, freq = 1/1400, sum = 0;
  for(let o=0;o<6;o++){
    h += n(wx*freq, wz*freq)*amp;
    sum += amp; amp *= 0.5; freq *= 2.05;
  }
  h = (h/sum); // -1..1
  // Continent shaping: raise edges to ocean floor, central plateau
  const cx = wx/terrainSize, cz = wz/terrainSize;
  const dist = Math.sqrt(cx*cx+cz*cz);
  const cont = clamp(1.4 - dist*1.3, 0, 1); // 1 at center
  const ridgeline = Math.pow(clamp(1-dist*1.1,0,1), 2.2);
  h = h*0.6*ridgeline + h*h*0.2*ridgeline;
  const alt = h * 1900 * cont + ridgeline*260;
  // flatten near runway (center)
  const rdX = 0, rdZ = 900;
  const d = Math.hypot(wx-rdX, wz-rdZ);
  const flat = clamp(1 - d/900, 0, 1);
  return alt + (0 - alt)*flat*flat;
}

function buildTerrain() {
  const n = makeNoise();
  state._noise = n;
  const geo = new THREE.PlaneGeometry(terrainSize, terrainSize, terrainSeg, terrainSeg);
  geo.rotateX(-Math.PI/2);
  const pos = geo.attributes.position;
  const colors = [];
  const cLow = new THREE.Color(0x2f4a38);
  const cMid = new THREE.Color(0x6b7a4a);
  const cRock= new THREE.Color(0x706a5e);
  const cSnow= new THREE.Color(0xf2f6fb);
  const cSand= new THREE.Color(0xb8a868);
  for(let i=0;i<pos.count;i++){
    const x=pos.getX(i), z=pos.getZ(i);
    const h = terrainHeight(x,z);
    pos.setY(i, h);
    const a = h/1900;
    const col = new THREE.Color();
    if(h < 4){ col.copy(cSand); }
    else if(h < 300){ col.copy(cLow).lerp(cMid, (h-4)/296); }
    else if(h < 950){ col.copy(cMid).lerp(cRock, (h-300)/650); }
    else if(h < 1400){ col.copy(cRock).lerp(cSnow, (h-950)/450); }
    else col.copy(cSnow);
    // slope shading
    colors.push(col.r, col.g, col.b);
  }
  geo.setAttribute('color', new THREE.Float32BufferAttribute(colors,3));
  geo.computeVertexNormals();
  const mat = new THREE.MeshStandardMaterial({ vertexColors:true, roughness:0.92, metalness:0.02, flatShading:false });
  terrainMesh = new THREE.Mesh(geo, mat);
  terrainMesh.receiveShadow = true;
  worldGroup.add(terrainMesh);
}

// ============================================================
//  WATER
// ============================================================
function buildWater() {
  const geo = new THREE.PlaneGeometry(terrainSize*1.4, terrainSize*1.4, 1, 1);
  geo.rotateX(-Math.PI/2);
  const mat = new THREE.MeshStandardMaterial({ color:0x1c4a68, transparent:true, opacity:0.86, roughness:0.15, metalness:0.6 });
  water = new THREE.Mesh(geo, mat);
  water.position.y = 2.5;
  worldGroup.add(water);
}

// ============================================================
//  RUNWAY + markers (for landing scenario)
// ============================================================
function buildRunway() {
  const grp = new THREE.Group();
  const rwGeo = new THREE.PlaneGeometry(60, 900);
  rwGeo.rotateX(-Math.PI/2);
  const rwMat = new THREE.MeshStandardMaterial({ color:0x2b2f36, roughness:0.9 });
  const rw = new THREE.Mesh(rwGeo, rwMat);
  const baseY = terrainHeight(0,900);
  rw.position.set(0, baseY+0.15, 900);
  rw.receiveShadow = true;
  grp.add(rw);
  // centerline dashes
  const lineMat = new THREE.MeshStandardMaterial({ color:0xe8e8e8, emissive:0x333333 });
  for(let i=-440;i<440;i+=40){
    const d = new THREE.Mesh(new THREE.PlaneGeometry(2.2,22), lineMat);
    d.rotateX(-Math.PI/2); d.position.set(0, baseY+0.3, 900+i+11);
    grp.add(d);
  }
  // threshold bars
  const barMat = new THREE.MeshStandardMaterial({ color:0xf0f0f0 });
  for(let s=-1;s<=1;s+=2){
    for(let j=0;j<3;j++){
      const b = new THREE.Mesh(new THREE.PlaneGeometry(3,40), barMat);
      b.rotateX(-Math.PI/2);
      b.position.set(s*18, baseY+0.3, 900+420 - j*55);
      grp.add(b);
    }
  }
  // Approach lights on both ends
  const lMat = new THREE.MeshStandardMaterial({ color:0xffffff, emissive:0xdfeaff, emissiveIntensity:1.4 });
  for(let i=-440;i<=440;i+=40){
    const l = new THREE.Mesh(new THREE.SphereGeometry(1.4,6,6), lMat);
    l.position.set(34, baseY+1.4, 900+i);
    grp.add(l);
    const l2 = l.clone(); l2.position.x=-34; grp.add(l2);
  }
  worldGroup.add(grp);
}

// ============================================================
//  SCENERY (trees, clouds, buildings)
// ============================================================
const cloudGroups = [];
function buildScenery() {
  // Trees
  const trunkGeo = new THREE.CylinderGeometry(0.6,1.0,5,5);
  const trunkMat = new THREE.MeshStandardMaterial({color:0x5a3d24, roughness:1});
  const leafGeo = new THREE.ConeGeometry(3.2, 9, 7);
  const leafMat = new THREE.MeshStandardMaterial({color:0x2e5a30, roughness:0.95});
  const trees = new THREE.InstancedMesh(trunkGeo, trunkMat, 3000);
  const leaves = new THREE.InstancedMesh(leafGeo, leafMat, 3000);
  const m = new THREE.Matrix4();
  let idx = 0;
  for(let i=0;i<3000;i++){
    const x = (Math.random()-0.5)*terrainSize*0.9;
    const z = (Math.random()-0.5)*terrainSize*0.9;
    const h = terrainHeight(x,z);
    if(h < 12 || h > 420) { i--; continue; }
    const sc = 0.7 + Math.random()*1.3;
    m.makeTranslation(x, h+2.5*sc, z); trees.setMatrixAt(idx, m);
    m.makeTranslation(x, h+7*sc, z); leaves.setMatrixAt(idx, m);
    idx++;
  }
  trees.count = leaves.count = idx;
  trees.castShadow = leaves.castShadow = true;
  worldGroup.add(trees, leaves);

  // Buildings near runway
  const bMat = new THREE.MeshStandardMaterial({color:0x6a7280, roughness:0.8});
  const towerMat = new THREE.MeshStandardMaterial({color:0x8892a0, roughness:0.5, metalness:0.3});
  const tower = new THREE.Mesh(new THREE.CylinderGeometry(4,6,34,12), towerMat);
  const by = terrainHeight(45,880);
  tower.position.set(45, by+17, 880); tower.castShadow=true;
  worldGroup.add(tower);
  const dome = new THREE.Mesh(new THREE.SphereGeometry(5,12,8), new THREE.MeshStandardMaterial({color:0x3a4a5a, emissive:0x112233}));
  dome.position.set(45, by+36, 880); worldGroup.add(dome);
  for(let i=0;i<10;i++){
    const w=8+Math.random()*14, d=8+Math.random()*14, hh=8+Math.random()*30;
    const b = new THREE.Mesh(new THREE.BoxGeometry(w,hh,d), bMat);
    const bx=70+ (Math.random()*160), bz=760+(Math.random()*120);
    const bh=terrainHeight(bx,bz);
    b.position.set(bx, bh+hh/2, bz); b.castShadow=true; b.receiveShadow=true;
    worldGroup.add(b);
  }

  // Clouds
  const cloudMat = new THREE.MeshBasicMaterial({color:0xffffff, transparent:true, opacity:0.85});
  for(let i=0;i<40;i++){
    const g = new THREE.Group();
    const pc = 4 + (Math.random()*5|0);
    for(let j=0;j<pc;j++){
      const p = new THREE.Mesh(new THREE.SphereGeometry(30+Math.random()*50, 8, 6), cloudMat);
      p.position.set((Math.random()-0.5)*160, (Math.random()-0.5)*30, (Math.random()-0.5)*160);
      p.scale.y = 0.6;
      g.add(p);
    }
    g.position.set((Math.random()-0.5)*terrainSize*1.4, 900+Math.random()*1600, (Math.random()-0.5)*terrainSize*1.4);
    worldGroup.add(g);
    cloudGroups.push(g);
  }
}

// ============================================================
//  AIRCRAFT MODEL
// ============================================================
let aircraftGroup = new THREE.Group();
function buildAircraftModel(def) {
  const g = new THREE.Group();
  const body = new THREE.MeshStandardMaterial({color:def.color, metalness:0.55, roughness:0.35});
  const acc = new THREE.MeshStandardMaterial({color:def.accent, metalness:0.5, roughness:0.4});
  const glass = new THREE.MeshStandardMaterial({color:0x0a1a2a, metalness:0.9, roughness:0.1, emissive:0x0a2040, emissiveIntensity:0.4});
  const metal = new THREE.MeshStandardMaterial({color:0x3a3f47, metalness:0.8, roughness:0.4});

  // Fuselage
  let fuseLen = def.id==='ranger' ? 12 : (def.id==='sparrow'?9:6);
  let fuseR = def.id==='ranger'?1.6:(def.id==='sparrow'?1.0:0.85);
  const fuse = new THREE.Mesh(new THREE.CylinderGeometry(fuseR*0.7, fuseR, fuseLen, 16), body);
  fuse.rotation.x = Math.PI/2;
  fuse.position.z = 0;
  fuse.castShadow = true;
  g.add(fuse);
  // Nose cone
  const nose = new THREE.Mesh(new THREE.SphereGeometry(fuseR*0.72, 16, 12), body);
  nose.scale.set(1,1,1.7); nose.position.z = fuseLen/2+1.2; nose.castShadow=true; g.add(nose);
  // Cockpit
  const cp = new THREE.Mesh(new THREE.SphereGeometry(fuseR*0.7, 14, 10), glass);
  cp.scale.set(1,0.8,1.6); cp.position.set(0, fuseR*0.5, fuseLen*0.28); g.add(cp);

  // Wings
  let wingspan, wingChord;
  if(def.id==='ranger'){ wingspan=16; wingChord=3.2; }
  else if(def.id==='sparrow'){ wingspan=10; wingChord=1.7; }
  else { wingspan=7.5; wingChord=1.5; }
  const wingGeo = new THREE.BoxGeometry(wingspan, 0.22, wingChord);
  const wing = new THREE.Mesh(wingGeo, body);
  wing.position.set(0, -0.1, 0.2); wing.castShadow=true; g.add(wing);
  // wing tips
  for(let s=-1;s<=1;s+=2){
    const t = new THREE.Mesh(new THREE.BoxGeometry(0.5,0.14,1.2), acc);
    t.position.set(s*wingspan/2, -0.1, 0.2); g.add(t);
  }

  // Tail (horizontal + vertical)
  const hTailGeo = new THREE.BoxGeometry(5, 0.15, 1.4);
  const hTail = new THREE.Mesh(hTailGeo, acc);
  hTail.position.set(0, 0.2, -fuseLen/2+0.8); g.add(hTail);
  const vTailGeo = new THREE.BoxGeometry(0.15, 2.6, 1.8);
  const vTail = new THREE.Mesh(vTailGeo, acc);
  vTail.position.set(0, 1.3, -fuseLen/2+0.8); vTail.castShadow=true; g.add(vTail);

  // Engine glows
  for(let s=-1;s<=1;s+=2){
    const eng = new THREE.Mesh(new THREE.CylinderGeometry(fuseR*0.5, fuseR*0.4, 2.2, 12), metal);
    eng.rotation.x = Math.PI/2; eng.position.set(s*(def.id==='ranger'?2.2:0.9), -0.2, -fuseLen/2-0.4); g.add(eng);
    const glow = new THREE.Mesh(new THREE.CircleGeometry(fuseR*0.4, 12), new THREE.MeshBasicMaterial({color:0x223344}));
    glow.position.set(s*(def.id==='ranger'?2.2:0.9), -0.2, -fuseLen/2-1.5); g.add(glow);
    glow.userData.glow = true;
  }

  // Control surfaces (for visual flap/pitch)
  const eLev = new THREE.Mesh(new THREE.BoxGeometry(wingspan*0.55, 0.1, 0.5), acc);
  eLev.position.set(0, -0.1, wingChord/2+0.15); g.add(eLev);
  const rudd = new THREE.Mesh(new THREE.BoxGeometry(0.15, 2.2, 0.4), body);
  rudd.position.set(0, 1.2, -fuseLen/2+0.6); g.add(rudd);
  const elevon = new THREE.Mesh(new THREE.BoxGeometry(5,0.12,0.5), acc);
  elevon.position.set(0,0.25,-fuseLen/2+1.1); g.add(elevon);

  g.userData.eLev=eLev; g.userData.rudd=rudd; g.userData.elevon=elevon;
  g.scale.setScalar(1.0);
  return g;
}

// ============================================================
//  ENEMY AI (dogfight)
// ============================================================
const enemies = [];
function spawnEnemies(n) {
  for(let i=0;i<n;i++){
    const g = new THREE.Group();
    const body = new THREE.MeshStandardMaterial({color:0x8a3030, metalness:0.5, roughness:0.4});
    const nose = new THREE.Mesh(new THREE.ConeGeometry(1.1,4,10), body);
    nose.rotation.x = Math.PI/2; g.add(nose);
    const wing = new THREE.Mesh(new THREE.BoxGeometry(8,0.2,2), body);
    wing.position.z=0.2; g.add(wing);
    const tail = new THREE.Mesh(new THREE.BoxGeometry(0.2,2,1.5), body);
    tail.position.set(0,1,-1.6); g.add(tail);
    g.castShadow=false;
    const ang = Math.random()*TAU;
    const r = 900 + Math.random()*1200;
    const e = {
      mesh:g, pos:V3(Math.cos(ang)*r, 500+Math.random()*700, Math.sin(ang)*r),
      vel:V3(0,0,0), quat:new THREE.Quaternion(), pqr:V3(0,0,0),
      throttle:0.5+Math.random()*0.4, hp:100, speed:120+Math.random()*60,
      turnT:0,
    };
    worldGroup.add(g);
    enemies.push(e);
  }
}
function removeEnemies(){ enemies.forEach(e=>worldGroup.remove(e.mesh)); enemies.length=0; }

// ============================================================
//  AUDIO
// ============================================================
let audioCtx = null, engineGain, windGain, masterGain;
let engineOsc, engineSub, engineFilter, windNode, windFilter;
let musicGain, musicNodes=[];
let musicOn = true;

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

  // --- Engine: two oscillators + filter, mapped to rpm ---
  engineOsc = audioCtx.createOscillator(); engineOsc.type='sawtooth'; engineOsc.frequency.value=60;
  engineSub = audioCtx.createOscillator(); engineSub.type='sine'; engineSub.frequency.value=40;
  engineFilter = audioCtx.createBiquadFilter(); engineFilter.type='lowpass'; engineFilter.frequency.value=900; engineFilter.Q.value=2;
  engineGain = audioCtx.createGain(); engineGain.gain.value=0.0;
  engineOsc.connect(engineFilter); engineSub.connect(engineFilter);
  engineFilter.connect(engineGain); engineGain.connect(masterGain);
  engineOsc.start(); engineSub.start();

  // --- Wind / airflow noise ---
  const bufSize = audioCtx.sampleRate*2;
  const buf = audioCtx.createBuffer(1, bufSize, audioCtx.sampleRate);
  const d = buf.getChannelData(0);
  for(let i=0;i<bufSize;i++) d[i]=Math.random()*2-1;
  windNode = audioCtx.createBufferSource(); windNode.buffer=buf; windNode.loop=true;
  windFilter = audioCtx.createBiquadFilter(); windFilter.type='bandpass'; windFilter.frequency.value=600; windFilter.Q.value=0.8;
  windGain = audioCtx.createGain(); windGain.gain.value=0.0;
  windNode.connect(windFilter); windFilter.connect(windGain); windGain.connect(masterGain);
  windNode.start();

  // --- Music (soft pad + arpeggio) ---
  musicGain = audioCtx.createGain(); musicGain.gain.value=0.14; musicGain.connect(masterGain);
  const notes=[220.00,261.63,329.63,392.00,440.00,523.25];
  let step=0;
  const playNote=()=>{
    if(!musicOn||!audioCtx) return;
    const f=notes[step%notes.length]*(step<notes.length?1:0.5);
    const o=audioCtx.createOscillator(); o.type='triangle'; o.frequency.value=f;
    const g=audioCtx.createGain(); g.gain.value=0;
    const fl=audioCtx.createBiquadFilter(); fl.type='lowpass'; fl.frequency.value=1400;
    o.connect(fl); fl.connect(g); g.connect(musicGain);
    const t=audioCtx.currentTime;
    g.gain.setValueAtTime(0,t); g.gain.linearRampToValueAtTime(0.25,t+0.05); g.gain.exponentialRampToValueAtTime(0.001,t+1.6);
    o.start(t); o.stop(t+1.7); step++;
    setTimeout(playNote, 520);
  };
  playNote();
  // bass
  const bass=audioCtx.createOscillator(); bass.type='sine'; bass.frequency.value=110;
  const bg=audioCtx.createGain(); bg.gain.value=0.12;
  bass.connect(bg); bg.connect(musicGain); bass.start();

  audioCtx.state==='suspended'&&audioCtx.resume();
}
function updateAudio() {
  if(!audioCtx) return;
  const t=audioCtx.currentTime;
  const rpm = P.rpm;
  const baseFreq = 45 + rpm*150;      // fundamental
  engineOsc.frequency.setTargetAtTime(baseFreq, t, 0.08);
  engineSub.frequency.setTargetAtTime(baseFreq*0.5, t, 0.08);
  engineFilter.frequency.setTargetAtTime(400+rpm*2600, t, 0.1);
  const engineVol = (0.05 + rpm*0.28) * (0.5+P.throttle*0.5);
  engineGain.gain.setTargetAtTime(engineVol, t, 0.1);
  // Wind scales with airspeed & thinness at altitude
  const windVol = clamp(P.speed/600,0,1)*0.18 * (1 - clamp(P.airDensity/1.2,0,1)*0.3);
  windFilter.frequency.setTargetAtTime(300 + P.speed*2.2, t, 0.15);
  windGain.gain.setTargetAtTime(windVol, t, 0.12);
}

// ============================================================
//  PHYSICS — the core
// ============================================================
const GRAV = 9.81;
const SEA = 1.225;
function airDensityAt(h){ return SEA * Math.exp(-h/8500); } // barometric

function stepPhysics(dt) {
  const def = AIRCRAFT[state.aircraftIdx];
  if(P.crashed){ dt = 0.016; }

  // ---- Input -> control surface commands (body rates) ----
  let cmdRoll=0, cmdPitch=0, cmdYaw=0;
  const rr = def.rollRate, pr = def.pitchRate, yr = def.yawRate;
  if(!P.crashed){
    if(keys.KeyW) cmdPitch -= pr;   // nose down (pitch)
    if(keys.KeyS) cmdPitch += pr;   // nose up
    if(keys.KeyA) cmdRoll  -= rr;
    if(keys.KeyD) cmdRoll  += rr;
    if(keys.KeyQ) cmdYaw   -= yr;
    if(keys.KeyE) cmdYaw   += yr;
  }
  // throttle
  if(keys.ArrowUp)   P.throttle = clamp(P.throttle + dt*0.6, 0, 1);
  if(keys.ArrowDown) P.throttle = clamp(P.throttle - dt*0.6, 0, 1);
  // flaps
  if(keys.Space && !P._spaceLock){ P.flaps = P.flaps>0.5?0:1; P._spaceLock=true; }
  if(!keys.Space) P._spaceLock=false;

  // ---- Engine ----
  // RPM eases toward throttle
  const targetRpm = P.throttle;
  P.rpm = lerp(P.rpm, targetRpm, 1-Math.pow(0.02, dt));
  // Engine heat
  P.heat = clamp(P.heat + (P.throttle*0.9 - P.heat*0.7)*dt*2.0, 0, 1);

  // Thrust along forward (body +z), reduced by air density at altitude
  const thrust = def.maxThrust * P.throttle * (0.55 + 0.45*clamp(P.airDensity/SEA,0.3,1));
  const fwd = V3(0,0,1).applyQuaternion(P.quat);

  // ---- Aerodynamics in body frame ----
  // Body velocity: velocity of air relative to aircraft in body frame
  const q = P.quat.clone().conjugate();
  P.bodyVel.copy(P.vel).applyQuaternion(q);
  const u = P.bodyVel.x, v = P.bodyVel.y, w = P.bodyVel.z;
  P.speed = Math.hypot(u,v,w);

  // Angle of attack & sideslip (small-speed guard)
  let alpha=0, beta=0;
  if(P.speed>5){
    alpha = Math.atan2(w, u);
    beta  = Math.asin(clamp(v/P.speed,-1,1));
  }
  P.aoa = alpha*180/Math.PI;
  P.beta = beta*180/Math.PI;

  // Lift slope with flap boost & Reynolds (density) scaling
  const flapBoost = P.flaps*0.9;
  const CLa = def.CLa;
  const alphaStall = def.stallAoa*Math.PI/180;
  let CL = CLa*(alpha + flapBoost*0.4);
  // Stall model: peak at CLmax then drop
  const alphaCl = Math.atan2(def.CLmax/CLa - flapBoost*0.4, 1);
  let stallFactor=1;
  const aAbs = Math.abs(alpha);
  if(aAbs>alphaCl){
    const over = aAbs-alphaCl;
    stallFactor = clamp(1 - over*2.2, 0.25, 1);
  }
  CL = Math.sign(alpha+flapBoost*0.4)*Math.min(Math.abs(CLa*(alpha+flapBoost*0.4)), def.CLmax)*stallFactor;

  const qdyn = 0.5 * P.airDensity * P.speed*P.speed;
  const S = def.refArea;

  // Induced drag from lift: CDi = k * CL^2  (k = 1/(π*AR*e))
  const CDi = def.k * CL*CL;
  let CD = def.CD0 + CDi;
  // Extra drag during flaps, high AoA, roll
  CD += P.flaps*0.05 + Math.max(0,Math.abs(alpha)*0.004) + (Math.abs(P.pqr.x)*0.0008);

  // Lift acts perpendicular to velocity (in vertical plane) — body frame: -Y axis
  const liftMag = qdyn * S * CL;
  // Drag opposite to velocity
  const dragMag = qdyn * S * CD;
  // Side force (yaw/sideslip) -> weathercock
  const sideMag = qdyn * S * (-0.6*beta);

  // Forces in body frame
  const gravMass = def.mass*GRAV;
  // gravity in body frame from quat
  const gravBody = V3(0,-gravMass,0).applyQuaternion(P.quat);

  const F = V3(0,0,0);
  // lift: along -bodyY (approx, for small alpha)
  F.y += -liftMag * clamp(stallFactor,0,1);
  // drag: opposite body velocity
  if(P.speed>0.1){
    const inv = -dragMag/P.speed;
    F.x += u*inv; F.y += v*inv; F.z += w*inv;
  }
  // side force
  F.x += sideMag*0.5;
  // thrust
  F.x += 0; F.y += 0; F.z += thrust;

  // ---- Control torques -> angular acceleration (inertia) ----
  // We integrate body rates with damping toward commanded rates
  const J = 1.0; // normalized moment of inertia scale (per axis, scaled by def)
  const scale = def.id==='ranger'?0.6:(def.id==='sparrow'?1.3:1.8);
  let tRoll = cmdRoll*scale, tPitch=cmdPitch*scale, tYaw=cmdYaw*scale;
  // yaw also from sideslip (weathercocking / directional stability)
  tYaw += -0.8*beta;
  // roll coupling to yaw (adverse yaw)
  tYaw -= cmdRoll*0.35;
  // pitch from roll in a turn (load)
  // Angular accelerations
  const dRoll  = (tRoll  - P.pqr.x*3.0)/J;
  const dPitch = (tPitch - P.pqr.y*3.0)/J;
  const dYaw   = (tYaw   - P.pqr.z*3.0)/J;
  P.pqr.x = clamp(P.pqr.x + dRoll*dt, -rr, rr);
  P.pqr.y = clamp(P.pqr.y + dPitch*dt, -pr, pr);
  P.pqr.z = clamp(P.pqr.z + dYaw*dt,   -yr, yr);

  // ---- Integrate orientation (body-rate quaternion integration) ----
  // dQ builds rotation from body angular rates; correct order & direction
  const dq = new THREE.Quaternion();
  dq.setFromAxisAngle(V3(1,0,0), -P.pqr.x*dt); // roll
  const dp = new THREE.Quaternion();
  dp.setFromAxisAngle(V3(0,1,0), -P.pqr.y*dt); // pitch
  const dy = new THREE.Quaternion();
  dy.setFromAxisAngle(V3(0,0,1), -P.pqr.z*dt); // yaw
  const dQ = new THREE.Quaternion().copy(dq).multiply(dp).multiply(dy);
  P.quat.multiply(dQ).normalize();

  // ---- Integrate translation ----
  const a = F.multiplyScalar(1/def.mass);
  P.vel.add(a*dt);
  // gravity already in F; add nothing else
  P.pos.add(P.vel.clone().multiplyScalar(dt));

  // ---- Load factor / G ----
  // Net non-gravity vertical force in body frame vs weight
  const liftG = liftMag/(def.mass*GRAV);
  P.loadG = liftG + (keys ? 0 : 0); // approx
  // actual g from acceleration vs gravity
  const accel = F.clone().multiplyScalar(1/def.mass);
  const netLiftBody = F.y + def.mass*GRAV; // body -y lift + support
  P.loadG = clamp(-F.y/(def.mass*GRAV), -2, def.maxG+1);

  // ---- Ground collision ----
  const baseY = terrainHeight(P.pos.x, P.pos.z);
  const wheelY = def.id==='ranger'?0.9:(def.id==='sparrow'?0.7:0.6);
  if(P.pos.y < baseY + wheelY){
    P.pos.y = baseY + wheelY;
    handleGround(def, baseY);
  } else {
    P.onGround = false;
  }

  // fuel burn
  P.fuel = clamp(P.fuel - def.fuelRate*P.throttle*dt*0.01, 0, 1);
  if(P.fuel<=0) P.throttle = Math.min(P.throttle, 0.1);

  // crash conditions
  if(!P.crashed && P.running){
    if(P.onGround && P.speed>70 && P.pos.y-baseY<wheelY+0.5){
      // hard landing / high speed on ground -> crash
      doCrash();
    }
    if(P.pos.y < 6 && P.onGround && P.speed>60){
      doCrash();
    }
  }
}

let _prevGroundSpeed=0, _prevGroundY=0;
function handleGround(def, baseY){
  P.onGround = true;
  const wheelY = def.id==='ranger'?0.9:(def.id==='sparrow'?0.7:0.6);
  // If moving down fast into ground -> crash
  if(P.pos.y < baseY+wheelY-0.01 && P.vel.y < -8){
    doCrash();
    return;
  }
  // Ground handling: wheels on runway
  if(P.vel.y < 0){
    // push back up, kill downward velocity
    P.vel.y = 0;
  }
  // Rolling resistance / braking on ground (only meaningful near runway flat area)
  const onFlat = baseY > 0.5 && baseY < 3; // runway area is flat ~0
  let rollResist = 0.02;
  if(keys.ShiftLeft||keys.ShiftRight) rollResist = 0.25;
  if(P.onGround){
    // decelerate
    const fr = onFlat? rollResist*GRAV : 0.5*GRAV;
    const sp = Math.hypot(P.vel.x, P.vel.z);
    if(sp>0.01){
      const dec = fr;
      P.vel.x *= clamp(1-dec/P.sp*dt, 0, 1);
      P.vel.z *= clamp(1-dec/P.sp*dt, 0, 1);
    }
    // Level the aircraft on ground (pitch up gently, wings level)
    // Damp roll/pitch rates strongly on ground
    P.pqr.x *= Math.pow(0.03, dt);
    P.pqr.y *= Math.pow(0.2, dt);
    P.pqr.z *= Math.pow(0.03, dt);
    // Keep wings level-ish if on ground and slow
    if(P.speed < 40){
      // align forward along -z (runway heading) a bit
      const q = P.quat;
      // reduce roll
      const right = V3(1,0,0).applyQuaternion(q);
      P.pqr.x += (-right.y)*dt*2.0;
    }
    // Pitch: if nose too low, keep level
    // Forward thrust only pushes; on ground plane constrains y
  }
}

function doCrash(){
  if(P.crashed) return;
  P.crashed = true;
  P.pqr.set(0,0,0);
  // spin & sink
  showMsg('CRASH', 'Aircraft destroyed — bad landing', ()=>{ endMission(false); });
}

// ============================================================
//  UI / MENU
// ============================================================
const previews = {};
function buildMenu() {
  const fleet = document.getElementById('fleet');
  fleet.innerHTML='';
  AIRCRAFT.forEach((def,i)=>{
    const stats = [
      ['Speed', def.throttleToSpeed],
      ['Thrust', def.maxThrust/200],
      ['Lift', def.CLmax],
      ['Turn', 1/(def.rollRate)*3],
      ['Agility', def.pitchRate/2.7],
    ];
    let statHtml = '';
    stats.forEach(([l,v])=>{
      statHtml += `<div class="stat"><span class="lbl">${l}</span><div class="bar"><i style="width:${clamp(v,0.05,1)*100|0}%"></i></div></div>`;
    });
    const card = document.createElement('div');
    card.className='card'; card.dataset.idx=i;
    card.innerHTML = `<div class="preview-canvas"></div><h3>${def.name}</h3><div class="role">${def.role}</div>${statHtml}`;
    fleet.appendChild(card);
    const cv = card.querySelector('.preview-canvas');
    previews[i]=cv;
    card.addEventListener('click', ()=>{
      document.querySelectorAll('#fleet .card').forEach(c=>c.classList.remove('selected'));
      card.classList.add('selected');
      state.aircraftIdx = i;
      updatePreview(i);
    });
  });
  document.querySelectorAll('#modeRow .modebtn').forEach(b=>{
    b.addEventListener('click',()=>{
      document.querySelectorAll('#modeRow .modebtn').forEach(x=>x.classList.remove('selected'));
      b.classList.add('selected');
      state.mode = parseInt(b.dataset.mode);
    });
  });
  document.getElementById('startBtn').addEventListener('click', startGame);
  document.getElementById('backBtn').addEventListener('click', ()=>{
    document.getElementById('hudWin2').style.display='none';
    document.getElementById('menu').style.display='flex';
    state.running=false;
  });
  document.querySelectorAll('#fleet .card').forEach((c,i)=>{ if(previews[i]) updatePreview(i); });
  document.querySelectorAll('#fleet .card')[0].click();
}

function updatePreview(i){
  const cv = previews[i]; if(!cv) return;
  if(cv._r) return;
  cv.width=300; cv.height=150;
  const r = new THREE.WebGLRenderer({antialias:true, transparent:true});
  r.setPixelRatio(1); r.setSize(300,150); r.toneMapping=THREE.ACESFilmicToneMapping;
  cv.appendChild(r.domElement);
  const s = new THREE.Scene();
  s.add(new THREE.AmbientLight(0xffffff,0.7));
  const d = new THREE.DirectionalLight(0xffffff,1.4); d.position.set(3,4,5); s.add(d);
  const cam = new THREE.PerspectiveCamera(40,300/150,0.1,100); cam.position.set(12,4,12); cam.lookAt(0,0,0);
  const g = new THREE.Group();
  g.add(buildAircraftModel(AIRCRAFT[i]));
  s.add(g);
  r.render(s,cam); cv._r=r; cv._s=s; cv._c=cam; cv._g=g;
  function anim(){
    if(!cv._g) return;
    cv._g.rotation.y += 0.01;
    r.render(cv._s,cv._c);
    requestAnimationFrame(anim);
  }
  anim();
}

// ============================================================
//  HUD update
// ============================================================
const hud = { status:document.getElementById('status'), alt:document.getElementById('altBox'), spd:document.getElementById('spdBox'), compass:document.getElementById('compass'), msg:document.getElementById('msg'), tip:document.getElementById('tip'), win:document.getElementById('hudWin') };
let hudCounters = { landings:0, bestDist:0 };

function updateHUD() {
  const alt = Math.max(0, P.pos.y - terrainHeight(P.pos.x,P.pos.z));
  const kal = P.speed*3.6; // km/h
  const mps = P.speed;
  const mach = P.speed/340;
  hud.alt.innerHTML = `ALTITUDE<br><b>${alt|0}</b> m<br><span style="font-size:11px;color:#6fa8c8">${(alt*3.281)|0} ft</span>`;
  hud.spd.innerHTML = `AIRSPEED<br><b>${kal|0}</b> km/h<br><span style="font-size:11px;color:#6fa8c8">${mps|0} m/s · ${mach.toFixed(2)} Ma</span>`;
  const ag = P.loadG;
  const gcol = Math.abs(ag)>7?'#ff7a5c':(ag<-0.5?'#ffd25f':'#9fe0ff');
  hud.status.innerHTML =
    `THROTTLE <span style="color:#eafaff">${(P.throttle*100)|0}%</span><br>`+
    `RPM <span style="color:#eafaff">${(P.rpm*100)|0}%</span> · `+
    `G <span style="color:${gcol}">${ag.toFixed(1)}</span><br>`+
    `AOA <span style="color:#eafaff">${P.aoa.toFixed(1)}°</span> · `+
    `FLAPS <span style="color:#eafaff">${P.flaps?`ON ${P.flaps}`:'OFF'}</span><br>`+
    `FUEL <span style="color:${P.fuel<0.2?'#ff7a5c':'#eafaff'}">${(P.fuel*100)|0}%</span> · `+
    `HEAT <span style="color:${P.heat>0.8?'#ff7a5c':'#eafaff'}">${(P.heat*100)|0}%</span><br>`+
    `${P.airDensity<0.9?`AIR <span style="color:#ffd25f">${(P.airDensity*100|0)}%</span>`:`AIR <span style="color:#7dffa6">${(P.airDensity/SEA*100)|0}%`}</span><br>`+
    (P.onGround?`<span class="ok">ON GROUND</span>`:`<span style="color:#6fa8c8">IN FLIGHT</span>`)+
    (P.crashed?`<br><span class="warn">✗ DESTROYED</span>`:'');

  // compass
  const hd = (THREE.MathUtils.radToDeg(P.quat.getAngle(new THREE.Vector4(0,0,1,0)))||0);
  const dir = new THREE.Euler(0,0,0).setFromQuaternion(P.quat,'YXZ').y;
  let deg = ((THREE.MathUtils.radToDeg(-dir)%360)+360)%360;
  const labels = {0:'N',90:'E',180:'S',270:'W'};
  let near = Object.keys(labels).map(Number).sort((a,b)=>Math.abs(a-deg)-Math.abs(b-deg))[0];
  hud.compass.innerHTML = `${labels[near]}  ${(deg|0).toString().padStart(3,'0')}°  PITCH ${(P.aoa>=0?'+':'')}${(THREE.MathUtils.radToDeg(Math.atan2(P.bodyVel.z,Math.hypot(P.bodyVel.x,P.bodyVel.y)))).toFixed(0)}°  ROLL ${(-THREE.MathUtils.radToDeg(Math.atan2(P.bodyVel.y,Math.hypot(P.bodyVel.x,P.bodyVel.z))))|0}°`;

  // mission tips
  if(state.mode===0){
    const ap = P.pos.y - terrainHeight(P.pos.x,P.pos.z);
    hud.tip.innerHTML = ap>15 ? 'ALIGN WITH RUNWAY · APPROACH AT 220 km/h · PULL UP AT DECISION' : 'FINAL APPROACH · KEEP AIRSPEED · GENTLE DESCENT';
  } else {
    const active = enemies.filter(e=>e.hp>0);
    hud.tip.innerHTML = `HOSTILES ${active.length} · SEEK & SHOOT · MANAGE SPEED & G`;
  }

  // stall warning
  const aAbs = Math.abs(P.aoa);
  if(!P.onGround && aAbs>def.stallAoa*0.85 && P.speed>20){
    hud.tip.innerHTML = `<span class="warn">⚠ STALL ⚠</span> · reduce angle of attack · add power`;
  }
}
const def = AIRCRAFT[0];

// ============================================================
//  MESSAGES / MISSION END
// ============================================================
let msgCb=null;
function showMsg(title, sub, cb){
  hud.msg.innerHTML = title+(sub?`<small>${sub}</small>`:'');
  hud.msg.style.display='block';
  msgCb=cb;
}
function hideMsg(){ hud.msg.style.display='none'; }

let missionMsg = null;
function showMission(title, sub){
  hud.win.innerHTML = title+`<small>${sub}</small>`;
  hud.win.style.display='block';
}

// ============================================================
//  START / MISSION
// ============================================================
function startGame() {
  initAudio();
  document.getElementById('menu').style.display='none';
  document.getElementById('hud').style.display='block';
  hideMsg(); hud.win.style.display='none';

  // reset physics
  const def = AIRCRAFT[state.aircraftIdx];
  P.pos.set(0, 1.5, -400);
  P.vel.set(0,0,0);
  P.bodyVel.set(0,0,0);
  P.quat.identity();
  P.pqr.set(0,0,0);
  P.throttle=0; P.rpm=0; P.heat=0; P.fuel=1;
  P.aoa=0; P.beta=0; P.cl=0; P.cd=0; P.loadG=1;
  P.airDensity=SEA; P.speed=0; P.onGround=true; P.crashed=false;
  P.flaps=0;

  // place aircraft at start depending on mode
  if(state.mode===0){
    P.pos.set(0, 1.5, -400); // on runway start
    P.quat.identity(); // facing +z toward runway
  } else {
    P.pos.set(0, 700, 0);
    P.vel.set(0,0,120);
    P.quat.identity();
    P.onGround=false;
  }

  // scenery
  worldGroup.position.set(0,0,0);
  aircraftGroup = buildAircraftModel(def);
  worldGroup.add(aircraftGroup);

  // enemies
  removeEnemies();
  if(state.mode===1) spawnEnemies(3);

  state.running=true;
  state.clock.getDelta();
  if(state.mode===0) showMsg('TAKEOFF & LANDING', 'Climb, cruise, and land cleanly on the runway');
  else showMsg('DOGFIGHT', 'Defeat all enemy fighters');
  setTimeout(()=>{ if(state.running) hideMsg(); }, 4200);
}

// ============================================================
//  CAMERA
// ============================================================
const camPos = V3(0,60,120);
const camLook = V3(0,0,0);
function updateCamera(dt) {
  const fwd = V3(0,0,1).applyQuaternion(P.quat);
  const up = V3(0,1,0).applyQuaternion(P.quat);
  const right = V3(1,0,0).applyQuaternion(P.quat);
  if(keys.KeyC){ if(!keys._cLock){ state.camMode=(state.camMode+1)%3; keys._cLock=true;} } else keys._cLock=false;

  let targetPos, targetLook;
  if(state.camMode===1){ // cockpit
    targetPos = P.pos.clone().add(fwd.clone().multiplyScalar(1.2)).add(up.clone().multiplyScalar(0.6));
    targetLook = P.pos.clone().add(fwd.clone().multiplyScalar(50));
  } else if(state.camMode===2){ // external orbit
    const ang = performance.now()*0.0002;
    const off = V3(Math.cos(ang)*40, 15, Math.sin(ang)*40);
    targetPos = P.pos.clone().add(off);
    targetLook = P.pos.clone();
  } else { // chase
    const dist = 26 + P.speed*0.04;
    const hgt = 9;
    targetPos = P.pos.clone().add(fwd.clone().multiplyScalar(-dist)).add(V3(0,hgt,0));
    targetLook = P.pos.clone().add(fwd.clone().multiplyScalar(30)).add(V3(0,2,0));
  }
  const k = 1-Math.pow(0.001, dt);
  camPos.lerp(targetPos, k);
  camLook.lerp(targetLook, k);
  camera.position.copy(camPos);
  camera.lookAt(camLook);
  // subtle shake at high speed / high G
  if(P.speed>200 || Math.abs(P.loadG-1)>4){
    camera.position.x += (Math.random()-0.5)*1.2;
    camera.position.y += (Math.random()-0.5)*1.2;
  }
}

// ============================================================
//  ENEMY AI update
// ============================================================
function updateEnemies(dt) {
  const me = P.pos;
  enemies.forEach(e=>{
    if(e.hp<=0) return;
    const toEnemy = V3().subVectors(me, e.pos);
    const dist = toEnemy.length();
    // steer toward player, keep some distance
    e.turnT -= dt;
    if(e.turnT<=0){ e.turnT = 0.6+Math.random()*1.2; e.side = (Math.random()<0.5?-1:1); }
    const desired = toEnemy.clone().normalize();
    // current forward
    const fwd = V3(0,0,1).applyQuaternion(e.quat);
    let steer = V3().subVectors(desired, fwd);
    // add lateral offset
    const up = V3(0,1,0);
    steer.add(V3().crossVectors(fwd, up).multiplyScalar(e.side*0.4));
    // normalize & turn
    const len = steer.length();
    if(len>1e-4){
      steer.normalize();
      const newFwd = V3().addVectors(fwd, steer.multiplyScalar(dt*1.5)).normalize();
      const qn = new THREE.Quaternion().setFromUnitVectors(V3(0,0,1), newFwd);
      e.quat.multiply(qn).normalize();
    }
    // maintain speed, climb if below
    e.speed = lerp(e.speed, 160 + (me.y-e.pos.y)*0.05, dt*0.5);
    e.speed = clamp(e.speed, 90, 260);
    e.vel.copy(new THREE.Vector3(0,0,1).applyQuaternion(e.quat).multiplyScalar(e.speed));
    e.pos.add(e.vel.clone().multiplyScalar(dt));
    // keep altitude
    if(e.pos.y < 300) e.pos.y = 300;
    if(e.pos.y > 2000) e.pos.y = 2000;
    e.mesh.position.copy(e.pos);
    e.mesh.quaternion.copy(e.quat);

    // shooting: if roughly aimed at player and close
    const aim = V3().subVectors(me, e.pos).normalize();
    const dot = fwd.dot(aim);
    if(dist<900 && dot>0.985 && !e._cd){
      e._cd = true;
      setTimeout(()=>{ e._cd=false; }, 900);
      // check player shields? simple: damage based on player speed/EV
      if(Math.random()<0.55){
        // player takes damage (not modeled with HP; instead flash)
        crashFlash = 0.3;
      }
    }
  });
}
let crashFlash = 0;

// ============================================================
//  MAIN LOOP
// ============================================================
let raf;
function animate() {
  raf = requestAnimationFrame(animate);
  const dt = Math.min(state.clock.getDelta(), 0.033);

  if(state.running){
    if(!P.crashed) stepPhysics(dt);
    else {
      // crash animation: tumble and sink
      P.pqr.x += (Math.random()-0.5)*dt*6;
      P.quat.multiply(new THREE.Quaternion().setFromAxisAngle(V3(1,0,0),dt*4));
      P.pos.y -= dt*40;
      if(P.pos.y < terrainHeight(P.pos.x,P.pos.z)) P.pos.y = terrainHeight(P.pos.x,P.pos.z);
      aircraftGroup.rotation.copy(P.quat);
    }
    updateEnemies(dt);
    updateCamera(dt);

    // aircraft model orientation
    aircraftGroup.position.copy(P.pos);
    aircraftGroup.quaternion.copy(P.quat);
    // control surface visuals
    const ud = aircraftGroup.userData;
    if(ud.eLev) ud.eLev.rotation.z = -clamp(P.pqr.y*0.5,-0.6,0.6);
    if(ud.rudd) ud.rudd.rotation.y = clamp(P.pqr.z*0.4,-0.7,0.7);
    if(ud.elevon) ud.elevon.rotation.z = -clamp(P.pqr.x*0.6,-0.8,0.8);
    if(P.flaps && ud.eLev) ud.eLev.position.y = -0.1 + P.flaps*0.25;

    // engine glow
    aircraftGroup.traverse(o=>{
      if(o.userData.glow && o.material && o.material.color){
        o.material.color.setHSL(0.08,1,0.3+P.throttle*0.5);
        if(o.material.emissive) o.material.emissive.setHex(0xffaa33);
        if(o.material.emissiveIntensity !== undefined) o.material.emissiveIntensity = 0.5+P.throttle*2;
      }
    });

    // fly clouds
    cloudGroups.forEach(c=>{ c.position.x += dt*4; if(c.position.x>terrainSize*0.7) c.position.x=-terrainSize*0.7; });

    updateHUD();
    updateAudio();

    // dogfight win check
    if(state.mode===1 && enemies.every(e=>e.hp<=0)){
      endMission(true);
    }
  }

  // crash flash overlay via fog color
  if(crashFlash>0){ crashFlash-=dt; renderer.toneMappingExposure = 1.05 + Math.sin(performance.now()*0.05)*crashFlash; }
  else renderer.toneMappingExposure = 1.05;

  renderer.render(scene, camera);
}

// ============================================================
//  SHOOTING (dogfight)
// ============================================================
function tryShoot() {
  if(state.mode!==1 || P.crashed) return;
  const fwd = V3(0,0,1).applyQuaternion(P.quat);
  const mp = P.pos;
  let hit=null, best=1e9;
  enemies.forEach(e=>{
    if(e.hp<=0) return;
    const to = V3().subVectors(e.pos, mp);
    const d = to.length();
    to.normalize();
    const ang = fwd.dot(to);
    if(d<1000 && ang>0.999){ // very tight aim
      // crosshair proximity
      if(d<best){ best=d; hit=e; }
    }
  });
  if(hit){
    hit.hp -= 34;
    muzzle = 0.08;
    if(hit.hp<=0){
      explode(hit.pos);
      removeEnemies2(hit);
    }
  }
}
let muzzle=0;
function explode(pos){
  // simple particle burst
  const g=new THREE.Group();
  const mat=new THREE.MeshBasicMaterial({color:0xffaa33});
  for(let i=0;i<24;i++){
    const p=new THREE.Mesh(new THREE.BoxGeometry(1.5,1.5,1.5),mat);
    p.position.copy(pos);
    p.userData.v = V3((Math.random()-0.5),(Math.random()-0.5),(Math.random()-0.5)).multiplyScalar(60+Math.random()*80);
    g.add(p);
  }
  worldGroup.add(g);
  const t=0;
  function anim(){
    t+=0.016;
    g.children.forEach(p=>{ p.position.add(p.userData.v.clone().multiplyScalar(0.016)); p.userData.v.multiplyScalar(0.92); });
    mat.opacity=clamp(1-t*1.5,0,1); mat.transparent=true;
    if(t<0.7) requestAnimationFrame(anim); else worldGroup.remove(g);
  }
  anim();
}
function removeEnemies2(e){ /* mesh removed via loop below */ }
// override: when enemy dies, remove its mesh
const _origRemove = removeEnemies;
// (handled inline in explode loop above by removing mesh when done)

// firing on fire key / mouse
window.addEventListener('keydown', e=>{
  if(e.code==='KeyF' || e.code==='KeyZ'){ tryShoot(); }
});
let mouseDown=false;
addEventListener('mousedown', e=>{ if(state.running){ if(e.button===0) tryShoot(); } });
addEventListener('keydown', e=>{ if(state.running && e.code==='KeyZ') mouseDown=true; });
addEventListener('keyup', e=>{ if(e.code==='KeyZ') mouseDown=false; });

// ============================================================
//  END MISSION
// ============================================================
function endMission(win){
  state.running=false;
  const w = document.getElementById('hudWin2');
  const title = document.getElementById('winTitle');
  const sub = document.getElementById('winSub');
  const stats = document.getElementById('winStats');
  if(win){
    sub.textContent = 'MISSION COMPLETE';
    title.textContent = 'AIR SUPERIORITY';
    title.style.color = '#7dffa6';
    stats.innerHTML = 'All hostiles down. Domain of the sky is yours.';
  } else {
    sub.textContent = 'MISSION FAILED';
    title.textContent = 'AIRCRAFT LOST';
    title.style.color = '#ff7a5c';
    stats.innerHTML = 'A pilot learns from every fall. Choose again.';
  }
  w.style.display='flex';
  document.getElementById('hud').style.display='none';
}

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

// ============================================================
//  BOOT
// ============================================================
function boot() {
  initRenderer();
  buildTerrain();
  buildWater();
  buildRunway();
  buildScenery();
  buildMenu();
  window.__APEX = { state, P, AIRCRAFT, stepPhysics, startGame, aircraftGroup, worldGroup, updatePreview, previews };
  animate();
}
boot();
</script>
</body>
</html>
<!-- agent-meta {"model":"peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF-MTP","provider":"brigid","persona":"full","sessionId":"0ae10e4f-9080-451b-b868-7ff91473fce5","tokensIn":11600952,"tokensOut":92093,"tokensTotal":11693045,"cost":0,"turns":107,"toolCalls":106,"failedToolCalls":4,"timestamp":"2026-09-04T22:52:24.562Z"} -->