← Flight Simulator results

Flight Simulator

muse spark 1.2 contributor · 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 41,564 bytes · SHA-256 cd4bd6c0f63d
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>APEX AERO — High-Fidelity Flight Simulator</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;700&family=Orbitron:wght@500;800;900&family=Inter:wght@400;600&display=swap" rel="stylesheet">
<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>
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--cyan:#00e5ff;--amber:#ffb800;--red:#ff3b30}
body{background:#02060f;color:#e6f0ff;font-family:'Inter',sans-serif;overflow:hidden;height:100vh;width:100vw}
#canvas{position:fixed;inset:0;width:100%;height:100%;display:block}
#topbar{position:fixed;top:0;left:0;right:0;height:52px;background:rgba(6,12,28,0.92);backdrop-filter:blur(12px);border-bottom:1px solid rgba(0,229,255,0.15);display:flex;align-items:center;justify-content:space-between;padding:0 18px;z-index:20}
#logo{display:flex;align-items:center;gap:12px}
#logoMark{width:32px;height:32px;background:var(--cyan);display:grid;place-items:center;color:#001018;font-weight:900;font-family:'Orbitron',sans-serif;font-size:14px;transform:skew(-8deg)}
#logoText{font-family:'Orbitron',sans-serif;font-weight:900;letter-spacing:0.12em;font-size:15px}
#logoText span{color:var(--cyan)}
.navBtn{font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.12em;padding:8px 14px;border:1px solid rgba(255,255,255,0.12);background:rgba(255,255,255,0.06);color:#8a9ab5;cursor:pointer;transition:0.2s}
.navBtn.active{background:var(--cyan);color:#001018;border-color:var(--cyan);font-weight:700}
.navBtn:hover{border-color:var(--cyan);color:#fff}
#leftPanel{position:fixed;left:14px;top:64px;bottom:14px;width:300px;display:flex;flex-direction:column;gap:12px;z-index:15;pointer-events:none}
.panel{background:rgba(8,16,32,0.90);backdrop-filter:blur(16px);border:1px solid rgba(0,229,255,0.12);pointer-events:auto}
#fleetPanel{padding:14px}
.panelTitle{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:0.18em;color:var(--cyan);margin-bottom:10px;display:flex;align-items:center;gap:6px}
.panelTitle::before{content:'';width:6px;height:6px;background:var(--cyan);display:inline-block}
.acCard{display:flex;align-items:center;gap:10px;padding:10px;border:1px solid rgba(255,255,255,0.07);background:rgba(255,255,255,0.03);margin-bottom:8px;cursor:pointer;transition:0.2s;position:relative;overflow:hidden}
.acCard.active{border-color:var(--cyan);background:rgba(0,229,255,0.08)}
.acCard.active::after{content:'';position:absolute;left:0;top:0;bottom:0;width:3px;background:var(--cyan)}
.acIcon{width:44px;height:32px;background:rgba(0,229,255,0.12);border:1px solid rgba(0,229,255,0.2);display:grid;place-items:center;font-size:18px}
.acName{font-family:'Orbitron',sans-serif;font-size:11px;font-weight:800;letter-spacing:0.06em}
.acRole{font-family:'JetBrains Mono',monospace;font-size:9px;color:#6b7a99;letter-spacing:0.08em}
.acStats{margin-left:auto;text-align:right;font-family:'JetBrains Mono',monospace;font-size:8px;color:#6b7a99;line-height:1.5}
.acStats b{color:#cfe6ff}
#missionPanel{padding:14px}
.missionBtn{width:100%;text-align:left;padding:12px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.04);color:#8ea0c0;font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.1em;cursor:pointer;margin-bottom:8px;display:flex;justify-content:space-between;align-items:center;transition:0.2s}
.missionBtn.active{border-color:var(--amber);background:rgba(255,184,0,0.1);color:#fff}
.missionBtn .dot{width:8px;height:8px;border-radius:50%;background:currentColor;opacity:0.6}
#controlsPanel{padding:14px}
.ctrlGrid{display:grid;grid-template-columns:1fr 1fr;gap:6px;font-family:'JetBrains Mono',monospace;font-size:9px;color:#6b7a99}
.ctrlGrid kbd{background:rgba(255,255,255,0.08);border:1px solid rgba(255,255,255,0.12);padding:2px 5px;color:#cfe6ff;font-family:inherit;font-size:9px}
#bottomHUD{position:fixed;bottom:14px;left:50%;transform:translateX(-50%);display:flex;gap:10px;z-index:15;pointer-events:none}
.hudCard{background:rgba(8,16,32,0.90);backdrop-filter:blur(12px);border:1px solid rgba(0,229,255,0.12);padding:10px 14px;min-width:140px}
.hudLabel{font-family:'JetBrains Mono',monospace;font-size:8px;letter-spacing:0.14em;color:var(--cyan);margin-bottom:6px}
.hudValue{font-family:'Orbitron',sans-serif;font-size:18px;font-weight:800;letter-spacing:0.04em}
.hudSub{font-family:'JetBrains Mono',monospace;font-size:9px;color:#6b7a99}
.bar{height:4px;background:rgba(255,255,255,0.08);margin-top:6px;position:relative;overflow:hidden}
.barFill{height:100%;background:var(--cyan);transition:width 0.1s}
.barFill.amber{background:var(--amber)}
.barFill.red{background:var(--red)}
#centerHUD{position:fixed;inset:0;pointer-events:none;z-index:10;display:grid;place-items:center}
#reticle{width:120px;height:120px;border:1px solid rgba(0,229,255,0.35);border-radius:50%;position:relative;display:grid;place-items:center}
#reticle::before{content:'';position:absolute;inset:18px;border:1px solid rgba(0,229,255,0.15);border-radius:50%}
#reticleCross{width:20px;height:20px;position:relative}
#reticleCross::before,#reticleCross::after{content:'';position:absolute;background:var(--cyan)}
#reticleCross::before{left:50%;top:0;bottom:0;width:1px;transform:translateX(-50%)}
#reticleCross::after{top:50%;left:0;right:0;height:1px;transform:translateY(-50%)}
#horizonLine{position:absolute;left:-30px;right:-30px;top:50%;height:1px;background:rgba(0,229,255,0.6);transform:translateY(-50%)}
#horizonLabel{position:absolute;top:50%;left:50%;transform:translate(-50%,-28px);font-family:'JetBrains Mono',monospace;font-size:8px;letter-spacing:0.2em;color:var(--cyan)}
#stallWarn{position:absolute;top:22%;left:50%;transform:translateX(-50%);background:var(--red);color:#fff;font-family:'Orbitron',sans-serif;font-size:11px;letter-spacing:0.18em;padding:6px 14px;display:none;animation:blink 0.4s infinite alternate}
@keyframes blink{from{opacity:1}to{opacity:0.3}}
#gMeter{position:fixed;right:14px;top:64px;width:52px;background:rgba(8,16,32,0.90);border:1px solid rgba(0,229,255,0.12);z-index:15;padding:10px 8px;text-align:center}
#gMeter .hudLabel{margin-bottom:4px}
#gVal{font-family:'Orbitron',sans-serif;font-size:20px;font-weight:900}
#aoaBar{position:fixed;right:14px;top:220px;width:52px;background:rgba(8,16,32,0.90);border:1px solid rgba(0,229,255,0.12);z-index:15;padding:10px 8px;text-align:center}
#speedTape{position:fixed;left:328px;top:64px;bottom:90px;width:54px;background:rgba(8,16,32,0.72);border:1px solid rgba(0,229,255,0.12);z-index:15;display:flex;flex-direction:column;align-items:center;padding:8px 0}
#altTape{position:fixed;right:78px;top:64px;bottom:90px;width:64px;background:rgba(8,16,32,0.72);border:1px solid rgba(0,229,255,0.12);z-index:15;display:flex;flex-direction:column;align-items:center;padding:8px 0}
.tapeLabel{font-family:'JetBrains Mono',monospace;font-size:8px;letter-spacing:0.12em;color:var(--cyan)}
.tapeVal{font-family:'Orbitron',sans-serif;font-size:13px;font-weight:800;margin:4px 0}
#throttleHUD{position:fixed;left:328px;bottom:14px;width:54px;height:64px;background:rgba(8,16,32,0.90);border:1px solid rgba(0,229,255,0.12);z-index:15;display:flex;flex-direction:column;align-items:center;justify-content:center}
#msgBox{position:fixed;top:64px;left:50%;transform:translateX(-50%);background:rgba(0,229,255,0.14);border:1px solid rgba(0,229,255,0.35);color:#cfefff;font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.08em;padding:10px 18px;z-index:25;display:none}
#pauseOverlay{position:fixed;inset:0;background:rgba(2,6,15,0.72);backdrop-filter:blur(8px);z-index:30;display:none;place-items:center}
#pauseCard{background:rgba(10,18,36,0.95);border:1px solid rgba(0,229,255,0.2);padding:28px;min-width:380px;text-align:center}
.btnPrimary{background:var(--cyan);color:#001018;border:none;font-family:'Orbitron',sans-serif;font-weight:800;letter-spacing:0.12em;padding:12px 22px;cursor:pointer}
.btnGhost{background:transparent;color:#8ea0c0;border:1px solid rgba(255,255,255,0.12);font-family:'JetBrains Mono',monospace;font-size:11px;letter-spacing:0.12em;padding:10px 18px;cursor:pointer}
@media(max-width:900px){#leftPanel{width:260px}#speedTape,#altTape,#gMeter,#aoaBar{display:none}}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div id="topbar">
  <div id="logo"><div id="logoMark">A</div><div id="logoText">APEX <span>AERO</span> <span style="font-size:9px;letter-spacing:0.2em;color:#6b7a99;margin-left:8px">FLIGHT SIM v1.0</span></div></div>
  <div style="display:flex;gap:8px;align-items:center">
    <div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:#6b7a99;letter-spacing:0.1em" id="fpsLabel">— FPS</div>
    <button class="navBtn" id="btnPause">⏸ PAUSE [P]</button>
    <button class="navBtn active" id="btnSound">🔊 AUDIO</button>
    <button class="navBtn" id="btnReset">↺ RESET [R]</button>
  </div>
</div>
<div id="leftPanel">
  <div class="panel" id="fleetPanel">
    <div class="panelTitle">FLEET — SELECT AIRFRAME</div>
    <div class="acCard active" data-ac="0"><div class="acIcon">✈</div><div><div class="acName">F-16 VIPER</div><div class="acRole">MULTIROLE • AGILE</div></div><div class="acStats"><b>2,450</b> km/h<br><b>1.12</b> T/W • <b>9G</b></div></div>
    <div class="acCard" data-ac="1"><div class="acIcon">🛩</div><div><div class="acName">A-10 THUNDER</div><div class="acRole">ATTACK • HEAVY</div></div><div class="acStats"><b>706</b> km/h<br><b>0.58</b> T/W • <b>6G</b></div></div>
    <div class="acCard" data-ac="2"><div class="acIcon">🚀</div><div><div class="acName">SR-71 BLACKBIRD</div><div class="acRole">RECON • MACH 3+</div></div><div class="acStats"><b>3,540</b> km/h<br><b>0.92</b> T/W • <b>4G</b></div></div>
    <div style="display:grid;grid-template-columns:1fr 1fr;gap:6px;margin-top:8px;font-family:'JetBrains Mono',monospace;font-size:8px;color:#6b7a99">
      <div>WEIGHT <b id="statWeight" style="color:#cfe6ff">9,200 kg</b></div>
      <div>THRUST <b id="statThrust" style="color:#cfe6ff">76 kN</b></div>
      <div>WING AREA <b id="statWing" style="color:#cfe6ff">27.9 m²</b></div>
      <div>CL MAX <b id="statCl" style="color:#cfe6ff">1.45</b></div>
    </div>
  </div>
  <div class="panel" id="missionPanel">
    <div class="panelTitle">MISSION SELECT</div>
    <button class="missionBtn active" data-mission="precision"><span>◉ PRECISION — TAKEOFF / LANDING</span><span class="dot" style="background:var(--amber)"></span></button>
    <button class="missionBtn" data-mission="dogfight"><span>◎ DOGFIGHT — HIGH-G COMBAT</span><span class="dot" style="background:var(--red)"></span></button>
    <div id="missionDesc" style="font-family:'JetBrains Mono',monospace;font-size:9px;color:#6b7a99;line-height:1.6;margin-top:6px">Manage airspeed, AoA and descent rate. Touch down &lt; 180 kt, &lt; 3 m/s vertical. Stall above 15° AoA.</div>
  </div>
  <div class="panel" id="controlsPanel">
    <div class="panelTitle">CONTROLS</div>
    <div class="ctrlGrid">
      <div><kbd>W</kbd> <kbd>S</kbd> Pitch</div><div><kbd>A</kbd> <kbd>D</kbd> Roll</div>
      <div><kbd>Q</kbd> <kbd>E</kbd> Yaw</div><div><kbd>SHIFT</kbd> Throttle +</div>
      <div><kbd>CTRL</kbd> Throttle −</div><div><kbd>SPACE</kbd> Fire / Brake</div>
      <div><kbd>R</kbd> Reset</div><div><kbd>P</kbd> Pause</div>
    </div>
    <div style="margin-top:8px;display:flex;gap:6px">
      <button class="btnGhost" style="flex:1" id="btnTakeoff">TAKEOFF ASSIST</button>
      <button class="btnGhost" style="flex:1" id="btnGear">GEAR [G]</button>
    </div>
  </div>
</div>
<div id="speedTape"><div class="tapeLabel">AIRSPEED</div><div class="tapeVal" id="speedVal">0</div><div style="font-family:'JetBrains Mono',monospace;font-size:8px;color:#6b7a99">KNOTS</div><div class="bar" style="width:80%;margin-top:8px"><div class="barFill" id="speedBar" style="width:0%"></div></div></div>
<div id="altTape"><div class="tapeLabel">ALTITUDE</div><div class="tapeVal" id="altVal">0</div><div style="font-family:'JetBrains Mono',monospace;font-size:8px;color:#6b7a99">FEET • MSL</div><div style="font-family:'JetBrains Mono',monospace;font-size:8px;color:#6b7a99;margin-top:8px">VS <span id="vsVal">0</span> ft/min</div></div>
<div id="gMeter"><div class="hudLabel">G-FORCE</div><div id="gVal">1.0</div><div style="font-size:8px;color:#6b7a99;font-family:'JetBrains Mono',monospace">LOAD</div><div class="bar"><div class="barFill amber" id="gBar" style="width:15%"></div></div></div>
<div id="aoaBar"><div class="hudLabel">AoA</div><div id="aoaVal" style="font-family:'Orbitron',sans-serif;font-weight:800">0°</div><div class="bar"><div class="barFill" id="aoaFill" style="width:0%"></div></div><div style="font-family:'JetBrains Mono',monospace;font-size:7px;color:#6b7a99;margin-top:4px">STALL 15°</div></div>
<div id="throttleHUD"><div class="hudLabel">THRUST</div><div id="thrVal" style="font-family:'Orbitron',sans-serif;font-weight:800;font-size:16px">0%</div><div class="bar" style="width:70%"><div class="barFill" id="thrBar" style="width:0%"></div></div><div id="rpmVal" style="font-family:'JetBrains Mono',monospace;font-size:8px;color:#6b7a99;margin-top:4px">RPM 0%</div></div>
<div id="centerHUD"><div id="reticle"><div id="horizonLine"></div><div id="horizonLabel">HORIZON</div><div id="reticleCross"></div></div><div id="stallWarn">⚠ STALL — PUSH NOSE DOWN</div></div>
<div id="bottomHUD">
  <div class="hudCard"><div class="hudLabel">HEADING</div><div class="hudValue" id="hdgVal">090°</div><div class="hudSub"><span id="pitchVal">P 0°</span> • <span id="rollVal">R 0°</span></div></div>
  <div class="hudCard"><div class="hudLabel">ENGINE</div><div class="hudValue" id="engVal" style="font-size:14px">IDLE</div><div class="hudSub"><span id="fuelVal">FUEL 100%</span> • <span id="gearVal">GEAR DN</span></div><div class="bar"><div class="barFill amber" id="engBar" style="width:10%"></div></div></div>
  <div class="hudCard" id="scoreCard"><div class="hudLabel">MISSION</div><div class="hudValue" id="scoreVal" style="font-size:14px">ALIGN RWY 27</div><div class="hudSub" id="scoreSub">Distance 2.4 km</div></div>
  <div class="hudCard"><div class="hudLabel">PHYSICS</div><div class="hudSub" style="line-height:1.6">ρ <span id="rhoVal">1.22</span> kg/m³<br>Drag <span id="dragVal">0</span> N • Lift <span id="liftVal">0</span> N</div></div>
</div>
<div id="msgBox"></div>
<div id="pauseOverlay"><div id="pauseCard"><div style="font-family:'Orbitron',sans-serif;font-weight:900;letter-spacing:0.14em;font-size:18px;margin-bottom:8px">PAUSED</div><div style="font-family:'JetBrains Mono',monospace;font-size:10px;color:#6b7a99;margin-bottom:18px">Physics frozen — press P to resume</div><div style="display:flex;gap:10px;justify-content:center"><button class="btnPrimary" id="btnResume">RESUME</button><button class="btnGhost" id="btnReset2">RESET FLIGHT</button></div></div></div>

<script type="module">
import * as THREE from 'three';

const AIRCRAFT = [
  { name:'F-16 VIPER', mass:9200, thrust:76000, wingArea:27.9, clMax:1.45, cd0:0.022, k:0.045, maxG:9, rollRate:2.8, pitchRate:1.6, yawRate:0.7, color:0x8ec8ff, scale:1 },
  { name:'A-10 THUNDER', mass:11300, thrust:40000, wingArea:47.0, clMax:1.65, cd0:0.035, k:0.038, maxG:6, rollRate:1.2, pitchRate:1.0, yawRate:0.5, color:0x6b7a5a, scale:1.15 },
  { name:'SR-71 BLACKBIRD', mass:27000, thrust:145000, wingArea:55.0, clMax:1.10, cd0:0.018, k:0.055, maxG:4, rollRate:0.9, pitchRate:0.7, yawRate:0.4, color:0x1a1a1e, scale:1.35 }
];

const canvas = document.getElementById('canvas');
const renderer = new THREE.WebGLRenderer({canvas, antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.shadowMap.enabled = true;
renderer.setClearColor(0x7fb8ea,1);

const scene = new THREE.Scene();
// very light fog so terrain stays visible
scene.fog = new THREE.Fog(0x8bbfe8, 3500, 14000);

const camera = new THREE.PerspectiveCamera(62, innerWidth/innerHeight, 1, 50000);
camera.position.set(-50, 22, 0);

// lights
scene.add(new THREE.HemisphereLight(0xddeeff, 0x223344, 1.1));
const sun = new THREE.DirectionalLight(0xfff8ee, 1.4);
sun.position.set(600,900,300);
sun.castShadow = true;
sun.shadow.mapSize.set(2048,2048);
sun.shadow.camera.near=1; sun.shadow.camera.far=5000;
sun.shadow.camera.left=-1200; sun.shadow.camera.right=1200; sun.shadow.camera.top=1200; sun.shadow.camera.bottom=-1200;
scene.add(sun);

// sky dome
const skyGeo = new THREE.SphereGeometry(22000,32,16);
const skyMat = new THREE.ShaderMaterial({
  side:THREE.BackSide, depthWrite:false,
  uniforms:{top:{value:new THREE.Color(0x1a3a6a)}, bottom:{value:new THREE.Color(0x8ec6f0)}},
  vertexShader:`varying vec3 vP;void main(){vP=position;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.);}`,
  fragmentShader:`uniform vec3 top;uniform vec3 bottom;varying vec3 vP;void main(){float h=clamp((vP.y+5000.)/22000.,0.,1.);vec3 c=mix(bottom,top,pow(h,0.65));gl_FragColor=vec4(c,1.);}`
});
const skyMesh = new THREE.Mesh(skyGeo, skyMat); skyMesh.renderOrder=-100; scene.add(skyMesh);

// terrain helpers
function pseudoNoise(x,z){
  const a=Math.sin(x*0.001)*0.5+0.5;
  const b=Math.sin(z*0.0013+x*0.0007)*0.5+0.5;
  const c=Math.sin(x*0.0025 - z*0.002)*0.5+0.5;
  return (a*0.5 + b*0.35 + c*0.15);
}
function terrainHeight(x,z){
  let h = (pseudoNoise(x,z)-0.5)*180 + 18;
  // close ridge visible from runway
  h += Math.max(0, 340 - Math.hypot(x-900, z-600)/2.8)*1.15;
  h += Math.max(0, 280 - Math.hypot(x+600, z+800)/3.0)*0.9;
  h += Math.max(0, 520 - Math.hypot(x-2100, z+900)/2.2)*0.95;
  h += Math.max(0, 380 - Math.hypot(x+2200, z-1400)/3.2)*0.75;
  const runwayStrip = Math.abs(z)<90 && Math.abs(x)<1100;
  if(runwayStrip) h = THREE.MathUtils.lerp(h, 1.2, 0.94);
  else if(Math.hypot(x,z)<1100 && Math.abs(z)<180) h = THREE.MathUtils.lerp(h, 1.2, Math.max(0,1-Math.hypot(x,z)/1100)*0.88);
  const edge=Math.max(Math.abs(x),Math.abs(z)); if(edge>5000) h-=(edge-5000)*0.18;
  return Math.max(h, -12);
}
function getTerrainHeight(x,z){ return terrainHeight(x,z); }

// build terrain mesh
const TSize=12000, TSeg=160;
const tGeo = new THREE.PlaneGeometry(TSize,TSize,TSeg,TSeg);
const tPos=tGeo.attributes.position;
for(let i=0;i<tPos.count;i++){
  const x=tPos.getX(i), y=tPos.getY(i);
  tPos.setZ(i, terrainHeight(x,y));
}
tGeo.computeVertexNormals();
const tMat=new THREE.MeshStandardMaterial({color:0x6b8a5a, roughness:0.92, metalness:0.04, side:THREE.DoubleSide});
const terrain=new THREE.Mesh(tGeo,tMat);
terrain.rotation.x=-Math.PI/2;
terrain.receiveShadow=true;
scene.add(terrain);

// grid helper (visible reference)
const grid=new THREE.GridHelper(12000, 24, 0xffffff, 0x445533);
grid.position.y=3.5;
scene.add(grid);

// runway
const rwyGroup=new THREE.Group();
const rwyMesh=new THREE.Mesh(new THREE.PlaneGeometry(1900,88), new THREE.MeshStandardMaterial({color:0x1e2535, roughness:0.82}));
rwyMesh.rotation.x=-Math.PI/2; rwyMesh.position.y=2.2; rwyMesh.receiveShadow=true;
rwyGroup.add(rwyMesh);
for(let i=-850;i<=850;i+=110){
  const m=new THREE.Mesh(new THREE.PlaneGeometry(32,4), new THREE.MeshBasicMaterial({color:0xffffff}));
  m.rotation.x=-Math.PI/2; m.position.set(i,2.35,0); rwyGroup.add(m);
}
const thr=new THREE.Mesh(new THREE.PlaneGeometry(64,72), new THREE.MeshBasicMaterial({color:0xffffff}));
thr.rotation.x=-Math.PI/2; thr.position.set(-910,2.35,0); rwyGroup.add(thr);
const thr2=thr.clone(); thr2.position.x=910; rwyGroup.add(thr2);
for(let s of [-1,1]) for(let i=0;i<4;i++){
  const l=new THREE.Mesh(new THREE.SphereGeometry(2.2), new THREE.MeshBasicMaterial({color: s>0?0xff3333:0xffffff}));
  l.position.set(940,4,(i-1.5)*6); rwyGroup.add(l);
}
scene.add(rwyGroup);

// distant mountains as low poly cones for parallax
const mtnGroup=new THREE.Group();
for(let i=0;i<14;i++){
  const h=120+Math.random()*320, r=90+Math.random()*120;
  const m=new THREE.Mesh(new THREE.ConeGeometry(r,h,6), new THREE.MeshStandardMaterial({color:0x5a6e4a, flatShading:true}));
  const ang=Math.random()*Math.PI*2, dist=2800+Math.random()*3800;
  m.position.set(Math.cos(ang)*dist, h/2 - 10, Math.sin(ang)*dist);
  mtnGroup.add(m);
}
scene.add(mtnGroup);

// clouds
const cloudGroup=new THREE.Group();
for(let i=0;i<18;i++){
  const g=new THREE.Group(); const n=3+Math.floor(Math.random()*3);
  for(let j=0;j<n;j++){
    const m=new THREE.Mesh(new THREE.SphereGeometry(70+Math.random()*70,8,6), new THREE.MeshStandardMaterial({color:0xffffff,transparent:true,opacity:0.48}));
    m.position.set((Math.random()-0.5)*160,(Math.random()-0.5)*36,(Math.random()-0.5)*160); m.scale.set(1,0.6,1); g.add(m);
  }
  g.position.set((Math.random()-0.5)*7000, 900+Math.random()*700, (Math.random()-0.5)*7000);
  cloudGroup.add(g);
}
scene.add(cloudGroup);

// aircraft factory
function makeAircraftMesh(idx){
  const def=AIRCRAFT[idx];
  const g=new THREE.Group();
  const bodyMat=new THREE.MeshStandardMaterial({color:def.color, roughness:0.32, metalness:0.55});
  const darkMat=new THREE.MeshStandardMaterial({color:0x0f141e, roughness:0.7});
  const glassMat=new THREE.MeshStandardMaterial({color:0x6ec8ff, roughness:0.08, metalness:0.85, transparent:true, opacity:0.78});
  const fus=new THREE.Mesh(new THREE.CylinderGeometry(0.9,0.7,14,12), bodyMat); fus.rotation.z=Math.PI/2; fus.position.x=1; g.add(fus);
  const nose=new THREE.Mesh(new THREE.ConeGeometry(0.7,4,12), bodyMat); nose.rotation.z=-Math.PI/2; nose.position.x=10; g.add(nose);
  const cock=new THREE.Mesh(new THREE.CapsuleGeometry(0.7,2.5,4,8), glassMat); cock.rotation.z=Math.PI/2; cock.position.set(3,0.9,0); g.add(cock);
  const wingShape=new THREE.Shape(); wingShape.moveTo(0,0); wingShape.lineTo(3,-6); wingShape.lineTo(3.6,-6); wingShape.lineTo(0.8,0); wingShape.lineTo(0,0);
  const wingGeo=new THREE.ExtrudeGeometry(wingShape,{depth:0.22,bevelEnabled:false});
  const wingL=new THREE.Mesh(wingGeo, bodyMat); wingL.rotation.x=Math.PI/2; wingL.position.set(1,0,0.9); g.add(wingL);
  const wingR=wingL.clone(); wingR.scale.z=-1; wingR.position.z=-0.9; g.add(wingR);
  const tail=new THREE.Mesh(new THREE.BoxGeometry(2.2,2.0,0.2), bodyMat); tail.position.set(-5.5,1.1,0); tail.rotation.z=0.6; g.add(tail);
  const hStabL=new THREE.Mesh(new THREE.BoxGeometry(1.6,0.15,2.2), bodyMat); hStabL.position.set(-5.8,0.2,1.1); hStabL.rotation.y=0.5; g.add(hStabL);
  const hStabR=hStabL.clone(); hStabR.position.z=-1.1; hStabR.rotation.y=-0.5; g.add(hStabR);
  const eng=new THREE.Mesh(new THREE.CylinderGeometry(0.6,0.7,1.2,12), darkMat); eng.rotation.z=Math.PI/2; eng.position.set(-6.8,0,0); g.add(eng);
  const after=new THREE.Mesh(new THREE.CircleGeometry(0.62,12), new THREE.MeshBasicMaterial({color:0xff6a00})); after.rotation.y=Math.PI; after.position.set(-7.45,0,0); after.name='afterburner'; g.add(after);
  g.scale.setScalar(0.9*def.scale); g.castShadow=true;
  return g;
}

let acIdx=0;
let aircraftMesh=makeAircraftMesh(0); scene.add(aircraftMesh);
let mission='precision';
let paused=false, gearDown=true, crashed=false, landed=false;

const state={
  pos:new THREE.Vector3(-900, 9, 0),
  vel:new THREE.Vector3(82, 0, 0),
  quat:new THREE.Quaternion(),
  angVel:new THREE.Vector3(),
  euler:new THREE.Euler(0,0,0,'ZYX'),
  throttle:0.62, rpm:0.58, fuel:100, aoa:0, gLoad:1, stall:false, onGround:true
};

function setAircraft(i){
  acIdx=i;
  scene.remove(aircraftMesh); aircraftMesh=makeAircraftMesh(i); scene.add(aircraftMesh);
  document.querySelectorAll('.acCard').forEach(c=>c.classList.toggle('active', +c.dataset.ac===i));
  const d=AIRCRAFT[i];
  document.getElementById('statWeight').textContent=d.mass.toLocaleString()+' kg';
  document.getElementById('statThrust').textContent=(d.thrust/1000).toFixed(0)+' kN';
  document.getElementById('statWing').textContent=d.wingArea+' m²';
  document.getElementById('statCl').textContent=d.clMax.toFixed(2);
  state.fuel=100;
}
function resetFlight(){
  crashed=false; landed=false;
  state.pos.set(mission==='precision'? -900:0, mission==='precision'? 9: 620, mission==='dogfight'? 500:0);
  state.vel.set(mission==='precision'? 80: 150, 0, 0);
  state.euler.set(0, mission==='dogfight'? -0.6:0, 0);
  state.quat.setFromEuler(state.euler);
  state.angVel.set(0,0,0);
  state.throttle=mission==='precision'?0.62:0.85;
  state.rpm=0.6; state.fuel=100; state.onGround=mission==='precision';
  showMsg(mission==='precision'?'CLEARED FOR TAKEOFF — RWY 27':'DOGFIGHT — ENGAGE BANDIT',2200);
  if(mission==='dogfight') resetEnemies();
}
function showMsg(t,ms=1800){
  const el=document.getElementById('msgBox'); el.textContent=t; el.style.display='block';
  clearTimeout(showMsg._t); showMsg._t=setTimeout(()=>el.style.display='none',ms);
}

const keys={};
addEventListener('keydown',e=>{
  const k=e.key.toLowerCase();
  keys[k]=true;
  if(k==='p'){ paused=!paused; document.getElementById('pauseOverlay').style.display=paused?'grid':'none'; }
  if(k==='r') resetFlight();
  if(k==='g'){ gearDown=!gearDown; showMsg(gearDown?'GEAR DOWN':'GEAR UP'); }
  if(k===' '){ e.preventDefault(); if(mission==='dogfight') fireBullet(); else if(state.onGround) state.vel.x+=2; }
});
addEventListener('keyup',e=> keys[e.key.toLowerCase()]=false);
document.getElementById('btnPause').onclick=()=>{paused=!paused; document.getElementById('pauseOverlay').style.display=paused?'grid':'none'};
document.getElementById('btnResume').onclick=()=>{paused=false; document.getElementById('pauseOverlay').style.display='none'};
document.getElementById('btnReset').onclick=resetFlight;
document.getElementById('btnReset2').onclick=()=>{paused=false; document.getElementById('pauseOverlay').style.display='none'; resetFlight()};
document.getElementById('btnGear').onclick=()=>{gearDown=!gearDown; showMsg(gearDown?'GEAR DOWN':'GEAR UP')};
document.getElementById('btnTakeoff').onclick=()=>{state.throttle=0.95; state.vel.x=Math.max(state.vel.x, 90); showMsg('TAKEOFF POWER SET');};
document.querySelectorAll('.acCard').forEach(c=>c.onclick=()=>setAircraft(+c.dataset.ac));
document.querySelectorAll('.missionBtn').forEach(b=>b.onclick=()=>{
  document.querySelectorAll('.missionBtn').forEach(x=>x.classList.remove('active')); b.classList.add('active');
  mission=b.dataset.mission;
  document.getElementById('missionDesc').textContent = mission==='precision'
    ? 'Manage airspeed, AoA and descent rate. Touch down < 180 kt, < 3 m/s vertical. Stall above 15° AoA.'
    : 'High-G maneuvering — get behind the bandit (green) and fire. Watch AoA and airspeed!';
  resetFlight();
});
let soundOn=true;
document.getElementById('btnSound').onclick=(e)=>{
  soundOn=!soundOn; e.currentTarget.textContent=soundOn?'🔊 AUDIO':'🔇 MUTED';
  e.currentTarget.classList.toggle('active',soundOn);
  if(soundOn) ensureAudio(); else if(audioCtx) audioCtx.suspend();
};

// audio
let audioCtx, engOsc, engGain, windGain;
function ensureAudio(){
  if(audioCtx){ if(audioCtx.state==='suspended') audioCtx.resume(); return; }
  audioCtx=new (window.AudioContext||window.webkitAudioContext)();
  engOsc=audioCtx.createOscillator(); engOsc.type='sawtooth'; engOsc.frequency.value=80;
  const filt=audioCtx.createBiquadFilter(); filt.type='lowpass'; filt.frequency.value=900;
  engGain=audioCtx.createGain(); engGain.gain.value=0;
  engOsc.connect(filt).connect(engGain).connect(audioCtx.destination); engOsc.start();
  const bufSize=4096, buf=audioCtx.createBuffer(1,bufSize,audioCtx.sampleRate);
  const ch=buf.getChannelData(0); for(let i=0;i<bufSize;i++) ch[i]=Math.random()*2-1;
  const src=audioCtx.createBufferSource(); src.buffer=buf; src.loop=true;
  const wFilt=audioCtx.createBiquadFilter(); wFilt.type='bandpass'; wFilt.frequency.value=1200;
  windGain=audioCtx.createGain(); windGain.gain.value=0;
  src.connect(wFilt).connect(windGain).connect(audioCtx.destination); src.start();
}
addEventListener('pointerdown', ensureAudio, {once:true});

// enemies & bullets
const enemies=[], bullets=[]; let score=0;
function makeEnemy(){
  const m=makeAircraftMesh(0); m.scale.setScalar(0.9);
  m.traverse(o=>{ if(o.isMesh&&o.material&&o.material.color){ o.material=o.material.clone(); o.material.color.setHex(0xff3b30); }});
  scene.add(m);
  return {mesh:m, pos:new THREE.Vector3(800,700,400+Math.random()*400), vel:new THREE.Vector3(-90,0,0), yaw:Math.PI, hp:100};
}
function resetEnemies(){
  enemies.forEach(e=>scene.remove(e.mesh)); enemies.length=0;
  bullets.forEach(b=>scene.remove(b.mesh)); bullets.length=0;
  score=0; for(let i=0;i<2;i++) enemies.push(makeEnemy());
}
function fireBullet(){
  const mesh=new THREE.Mesh(new THREE.SphereGeometry(1.2), new THREE.MeshBasicMaterial({color:0xffff00}));
  const dir=new THREE.Vector3(1,0,0).applyQuaternion(state.quat);
  const p=state.pos.clone().add(dir.clone().multiplyScalar(12));
  const v=dir.clone().multiplyScalar(420).add(state.vel);
  scene.add(mesh); mesh.position.copy(p);
  bullets.push({mesh,pos:p,vel:v,life:1.8});
  state.vel.add(dir.clone().multiplyScalar(-0.6));
}

let lastTime=performance.now(), physAccum=0;
const FIXED_DT=1/240;
function clamp(v,a,b){return Math.max(a,Math.min(b,v));}

function physicsStep(dt){
  const def=AIRCRAFT[acIdx];
  const speed=state.vel.length();
  const altM=Math.max(0, state.pos.y);
  const rho=1.225*Math.exp(-altM/8500);
  const fwd=new THREE.Vector3(1,0,0).applyQuaternion(state.quat);
  const up=new THREE.Vector3(0,1,0).applyQuaternion(state.quat);
  const right=new THREE.Vector3(0,0,1).applyQuaternion(state.quat);
  let aoaDeg=0;
  if(speed>2){
    const pitch=state.euler.x*180/Math.PI;
    const fpa=Math.asin(clamp(state.vel.y/speed,-1,1))*180/Math.PI;
    aoaDeg=pitch - fpa;
  }
  state.aoa=aoaDeg;
  const aoaRad=aoaDeg*Math.PI/180;
  let Cl;
  const stallAoA=15*Math.PI/180;
  if(Math.abs(aoaRad)<stallAoA){ Cl=def.clMax*Math.sin(aoaRad*(Math.PI/2/stallAoA))+0.15; }
  else { const excess=Math.abs(aoaRad)-stallAoA; Cl=def.clMax*Math.cos(excess*2)*0.6; }
  Cl=clamp(Cl,-0.8,def.clMax);
  const Cd=def.cd0 + def.k*Cl*Cl + (state.stall?0.08:0);
  const q=0.5*rho*speed*speed;
  const liftMag=q*def.wingArea*Cl;
  const dragMag=q*def.wingArea*Cd;
  let liftVec=new THREE.Vector3();
  if(speed>1){
    const velN=state.vel.clone().normalize();
    const liftDir=up.clone().sub(velN.clone().multiplyScalar(up.dot(velN))).normalize();
    if(liftDir.lengthSq()<0.01) liftDir.copy(up);
    liftVec.copy(liftDir).multiplyScalar(liftMag);
    const rollFactor=Math.cos(state.euler.z);
    liftVec.multiplyScalar(clamp(rollFactor*0.5+0.5,0.15,1));
  }
  const dragVec=speed>0.1? state.vel.clone().normalize().multiplyScalar(-dragMag):new THREE.Vector3();
  if(!state.onGround && gearDown) dragVec.multiplyScalar(1.18);
  const maxThrust=def.thrust*(0.55+0.45*rho/1.225);
  const targetRpm=state.throttle;
  state.rpm+=(targetRpm-state.rpm)*dt*2.2;
  const thrustMag=maxThrust*(0.08+0.92*state.rpm);
  const thrustVec=fwd.clone().multiplyScalar(thrustMag);
  const weightVec=new THREE.Vector3(0,-def.mass*9.81,0);
  const terrainH=getTerrainHeight(state.pos.x, state.pos.z);
  const heightAGL=state.pos.y - terrainH;
  let groundNormalForce=new THREE.Vector3(), friction=new THREE.Vector3();
  state.onGround=heightAGL<=1.25;
  if(state.onGround){
    if(state.pos.y < terrainH+1.25){ state.pos.y=terrainH+1.25; if(state.vel.y<0) state.vel.y*=-0.15; }
    const penetration=Math.max(0,1.25-heightAGL);
    groundNormalForce.set(0, def.mass*9.81*Math.min(1, penetration*2+0.6), 0);
    const brake=keys[' ']?1:0.12;
    const horizSpeed=Math.hypot(state.vel.x,state.vel.z);
    if(horizSpeed>0.5){
      const fDir=new THREE.Vector3(state.vel.x,0,state.vel.z).normalize().multiplyScalar(-1);
      friction.copy(fDir).multiplyScalar(def.mass*9.81*brake*0.55);
      if(brake<0.5) friction.multiplyScalar(0.25);
    }
  }
  const totalForce=new THREE.Vector3().addVectors(liftVec,dragVec).add(thrustVec).add(weightVec).add(groundNormalForce).add(friction);
  const accel=totalForce.clone().divideScalar(def.mass);
  state.vel.add(accel.clone().multiplyScalar(dt));
  state.gLoad=accel.length()/9.81;
  state.stall=Math.abs(aoaDeg)>15 && speed<95 && !state.onGround;
  if(state.stall) state.angVel.x+=(Math.random()-0.5)*0.8*dt*10;
  const controlPitch=(keys['s']?1:0)+(keys['w']?-1:0);
  const controlRoll=(keys['d']?1:0)+(keys['a']?-1:0);
  const controlYaw=(keys['e']?1:0)+(keys['q']?-1:0);
  if(keys['shift']) state.throttle=clamp(state.throttle+dt*0.7,0,1);
  if(keys['control']) state.throttle=clamp(state.throttle-dt*0.7,0,1);
  const eff=clamp(q/1800,0.15,1.6);
  const pitchTorque=controlPitch*def.pitchRate*eff*(state.stall?0.35:1);
  const rollTorque=controlRoll*def.rollRate*eff;
  const yawTorque=controlYaw*def.yawRate*eff;
  state.angVel.x+=(pitchTorque - state.angVel.x*1.8)*dt*3;
  state.angVel.z+=(rollTorque - state.angVel.z*1.6)*dt*3;
  state.angVel.y+=(yawTorque - state.angVel.y*1.4)*dt*3;
  if(!state.stall && Math.abs(controlRoll)<0.01 && !state.onGround){
    state.angVel.z+=(-state.euler.z*0.35 - state.angVel.z)*dt*1.2;
  }
  state.euler.x+=state.angVel.x*dt;
  state.euler.y+=state.angVel.y*dt;
  state.euler.z+=state.angVel.z*dt;
  state.euler.x=clamp(state.euler.x, -Math.PI/2+0.1, Math.PI/2-0.1);
  state.quat.setFromEuler(state.euler);
  state.pos.add(state.vel.clone().multiplyScalar(dt));
  state.fuel=clamp(state.fuel - state.throttle*dt*0.18,0,100);
  if(state.fuel===0) state.throttle*=0.99;
  if(!crashed && !state.onGround && heightAGL<0.5 && speed>12 && state.vel.y < -7){ crashed=true; showMsg('CRASH — IMPACT '+Math.abs(state.vel.y).toFixed(1)+' m/s — PRESS R',3200); }
  if(state.onGround && Math.abs(state.vel.y)>8){ crashed=true; showMsg('HARD LANDING — GEAR COLLAPSE',2800); }
  if(mission==='precision' && state.onGround && !landed && !crashed){
    const onRunway=Math.abs(state.pos.z)<44 && state.pos.x>-960 && state.pos.x<960;
    const vs=Math.abs(state.vel.y), gsKts=Math.hypot(state.vel.x,state.vel.z)*1.94384;
    if(onRunway && vs<3.2 && gsKts<185 && gsKts>45){ landed=true; showMsg('✓ PERFECT LANDING — WELL DONE!',3000); }
  }
  state._rho=rho; state._lift=liftMag; state._drag=dragMag; state._speed=speed; state._altM=altM; state._heightAGL=heightAGL;
}

function updateEnemies(dt){
  for(const e of enemies){
    const toPlayer=state.pos.clone().sub(e.pos);
    const desiredYaw=Math.atan2(toPlayer.z,toPlayer.x);
    let yawDiff=((desiredYaw - e.yaw + Math.PI*3)%(Math.PI*2))-Math.PI;
    e.yaw+=clamp(yawDiff,-0.9*dt,0.9*dt);
    const altErr=(state.pos.y - e.pos.y)*0.002;
    e.vel.y+=altErr*dt*40; e.vel.y=clamp(e.vel.y,-30,30);
    const spd=110+Math.sin(performance.now()*0.0003+e.pos.x*0.001)*20;
    e.vel.x=Math.cos(e.yaw)*spd; e.vel.z=Math.sin(e.yaw)*spd;
    e.pos.add(e.vel.clone().multiplyScalar(dt));
    e.mesh.position.copy(e.pos); e.mesh.rotation.y=-e.yaw; e.mesh.rotation.z=clamp(-yawDiff*0.8,-0.6,0.6);
    const th=getTerrainHeight(e.pos.x,e.pos.z); if(e.pos.y < th+80){ e.pos.y=th+80; e.vel.y=Math.abs(e.vel.y)*0.3; }
  }
  for(let i=bullets.length-1;i>=0;i--){
    const b=bullets[i];
    b.pos.add(b.vel.clone().multiplyScalar(dt)); b.vel.y-=9.81*0.12*dt;
    b.mesh.position.copy(b.pos); b.life-=dt;
    if(b.life<=0){ scene.remove(b.mesh); bullets.splice(i,1); continue; }
    for(let j=enemies.length-1;j>=0;j--){
      const e=enemies[j];
      if(b.pos.distanceTo(e.pos)<18){
        e.hp-=55; scene.remove(b.mesh); bullets.splice(i,1);
        if(e.hp<=0){
          const exp=new THREE.Mesh(new THREE.SphereGeometry(18), new THREE.MeshBasicMaterial({color:0xff6600,transparent:true,opacity:0.9}));
          exp.position.copy(e.pos); scene.add(exp);
          let s=0; const anim=setInterval(()=>{s+=0.12; exp.scale.setScalar(1+s*1.2); exp.material.opacity-=0.07; if(exp.material.opacity<=0){clearInterval(anim); scene.remove(exp);}},32);
          scene.remove(e.mesh); enemies.splice(j,1); score+=100; showMsg('BANDIT SPLASHED! +100',1400);
          if(enemies.length<2) enemies.push(makeEnemy());
        }
        break;
      }
    }
  }
}

function updateHUD(){
  const spdKts=(state._speed||0)*1.94384;
  document.getElementById('speedVal').textContent=spdKts.toFixed(0);
  document.getElementById('altVal').textContent=((state._altM||0)*3.28084).toFixed(0);
  document.getElementById('vsVal').textContent=(state.vel.y*196.85).toFixed(0);
  document.getElementById('gVal').textContent=state.gLoad.toFixed(1);
  document.getElementById('gBar').style.width=clamp(state.gLoad/9*100,6,100)+'%';
  document.getElementById('aoaVal').textContent=state.aoa.toFixed(1)+'°';
  document.getElementById('aoaFill').style.width=clamp(Math.abs(state.aoa)/18*100,0,100)+'%';
  document.getElementById('aoaFill').style.background=Math.abs(state.aoa)>13?'var(--red)':Math.abs(state.aoa)>9?'var(--amber)':'var(--cyan)';
  document.getElementById('thrVal').textContent=(state.throttle*100).toFixed(0)+'%';
  document.getElementById('thrBar').style.width=(state.throttle*100)+'%';
  document.getElementById('rpmVal').textContent='RPM '+(state.rpm*100).toFixed(0)+'%';
  document.getElementById('engBar').style.width=(state.rpm*100)+'%';
  document.getElementById('rhoVal').textContent=(state._rho||0).toFixed(2);
  document.getElementById('dragVal').textContent=((state._drag||0)/1000).toFixed(1)+'k';
  document.getElementById('liftVal').textContent=((state._lift||0)/1000).toFixed(1)+'k';
  document.getElementById('speedBar').style.width=clamp(spdKts/520*100,0,100)+'%';
  document.getElementById('hdgVal').textContent=((state.euler.y*180/Math.PI+360)%360).toFixed(0).padStart(3,'0')+'°';
  document.getElementById('pitchVal').textContent='P '+(state.euler.x*180/Math.PI).toFixed(0)+'°';
  document.getElementById('rollVal').textContent='R '+(state.euler.z*180/Math.PI).toFixed(0)+'°';
  document.getElementById('fuelVal').textContent='FUEL '+state.fuel.toFixed(0)+'%';
  document.getElementById('gearVal').textContent=gearDown?'GEAR DN':'GEAR UP';
  document.getElementById('gearVal').style.color=gearDown?'#7CFF7C':'var(--amber)';
  const engEl=document.getElementById('engVal');
  if(crashed){ engEl.textContent='CRASHED'; engEl.style.color='var(--red)';}
  else if(state.stall){ engEl.textContent='STALL'; engEl.style.color='var(--red)';}
  else if(state.throttle>0.88){ engEl.textContent='MAX AB'; engEl.style.color='var(--amber)';}
  else if(state.throttle>0.5){ engEl.textContent='CRUISE'; engEl.style.color='#7CFF7C';}
  else { engEl.textContent='IDLE'; engEl.style.color='#8ea0c0'; }
  document.getElementById('stallWarn').style.display=state.stall?'block':'none';
  if(mission==='precision'){
    const gs=spdKts.toFixed(0);
    document.getElementById('scoreVal').textContent=landed?'LANDED ✓':crashed?'CRASHED ✕':state.onGround && state.pos.x<-600?'TAKEOFF ROLL':'FINAL RWY 27';
    document.getElementById('scoreSub').textContent=`AGL ${(state._heightAGL||0).toFixed(0)} m • GS ${gs} kt`;
  } else {
    const d=enemies[0]? state.pos.distanceTo(enemies[0].pos).toFixed(0):'—';
    document.getElementById('scoreVal').textContent=`SCORE ${score}`;
    document.getElementById('scoreSub').textContent=`Bandits ${enemies.length} • Nearest ${d} m • SPACE to fire`;
  }
  const hl=document.getElementById('horizonLine');
  hl.style.transform=`translateY(-50%) rotate(${-state.euler.z*180/Math.PI}deg) translateY(${-state.euler.x*180/Math.PI*1.2}px)`;
}

function updateCamera(dt){
  const fwd=new THREE.Vector3(1,0,0).applyQuaternion(state.quat);
  const up=new THREE.Vector3(0,1,0).applyQuaternion(state.quat);
  const targetPos=state.pos.clone().add(fwd.clone().multiplyScalar(-52).add(up.clone().multiplyScalar(18)));
  const th=getTerrainHeight(targetPos.x, targetPos.z);
  if(targetPos.y < th + 12) targetPos.y = th + 12;
  camera.position.lerp(targetPos, 1 - Math.pow(0.0006, dt*60));
  // look slightly downward so horizon sits at upper third and terrain fills view
  const lookAt=state.pos.clone().add(fwd.clone().multiplyScalar(300)).add(new THREE.Vector3(0,-28,0));
  camera.lookAt(lookAt);
  camera.fov=62 + clamp((state._speed||0)/150*8,0,10);
  camera.updateProjectionMatrix();
  const ab=aircraftMesh.getObjectByName('afterburner');
  if(ab){ ab.visible=state.throttle>0.82; ab.scale.setScalar(0.8+state.throttle*0.7+Math.random()*0.22); }
}

function animate(now){
  requestAnimationFrame(animate);
  const dt=Math.min(0.033,(now-lastTime)/1000); lastTime=now;
  if(!paused && !crashed){
    physAccum+=dt; let steps=0;
    while(physAccum>=FIXED_DT && steps<6){ physicsStep(FIXED_DT); if(mission==='dogfight') updateEnemies(FIXED_DT); physAccum-=FIXED_DT; steps++; }
  }
  aircraftMesh.position.copy(state.pos);
  aircraftMesh.quaternion.copy(state.quat);
  updateCamera(dt);
  updateHUD();
  if(soundOn && audioCtx){
    const thr=state.throttle, spd=state._speed||0;
    engOsc.frequency.value=55+thr*180+spd*0.35;
    engGain.gain.value=0.06+thr*0.14;
    windGain.gain.value=clamp(spd/140*0.18,0,0.22);
  }
  renderer.render(scene,camera);
  document.getElementById('fpsLabel').textContent=(1/dt).toFixed(0)+' FPS • PHYSICS 240Hz';
}
addEventListener('resize',()=>{ renderer.setSize(innerWidth,innerHeight); camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); });
renderer.setSize(innerWidth,innerHeight);
resetFlight();
animate(performance.now());
window.__apex={state, AIRCRAFT, resetFlight, getTerrainHeight};

</script>
</body>
</html>