← Flight Simulator results

Flight Simulator

Qwen 3.8 · 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 76,400 bytes · SHA-256 1233d3c9ed97
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>APEX AERO — Flight Simulator</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@500;700;900&family=Rajdhani:wght@400;500;600;700&display=swap" rel="stylesheet">
<style>
:root{
  --hud:#9ff2ea; --hud-dim:rgba(159,242,234,.55); --warn:#ff5f4a; --ok:#59ffa8;
  --gold:#ffd166; --panel:rgba(6,14,20,.55); --line:rgba(159,242,234,.28);
  --disp:'Orbitron','Rajdhani',sans-serif; --body:'Rajdhani','Segoe UI',sans-serif;
}
*{margin:0;padding:0;box-sizing:border-box}
html,body{width:100%;height:100%;overflow:hidden;background:#04090e;color:#dfeef2;font-family:var(--body);-webkit-user-select:none;user-select:none}
canvas#gl{position:fixed;inset:0;display:block}
#vignette{position:fixed;inset:0;pointer-events:none;z-index:3;
  background:radial-gradient(ellipse at center,transparent 52%,rgba(2,8,12,.55) 100%)}
#dmg{position:fixed;inset:0;pointer-events:none;z-index:4;opacity:0;transition:opacity .25s;
  background:radial-gradient(ellipse at center,transparent 40%,rgba(255,30,10,.55) 100%)}
#flash{position:fixed;inset:0;pointer-events:none;z-index:5;opacity:0;background:#fff}
/* ============ HUD ============ */
#hud{position:fixed;inset:0;z-index:6;pointer-events:none;opacity:0;transition:opacity .4s}
#hud.on{opacity:1}
#hud .lbl{font-family:var(--disp);font-size:10px;letter-spacing:.22em;color:var(--hud-dim)}
#hud .val{font-family:var(--disp);font-weight:700;color:var(--hud);text-shadow:0 0 12px rgba(159,242,234,.35);line-height:1.05}
#hud .unit{font-family:var(--body);font-weight:600;font-size:11px;color:var(--hud-dim);letter-spacing:.08em}
#topbar{position:absolute;top:14px;left:0;right:0;display:flex;justify-content:space-between;align-items:flex-start;padding:0 22px}
#topbar .left,#topbar .right{display:flex;gap:26px;align-items:center}
#modeTag{font-family:var(--disp);font-size:12px;letter-spacing:.3em;color:var(--gold);text-shadow:0 0 14px rgba(255,209,102,.4)}
#aircraftTag{font-family:var(--disp);font-size:13px;letter-spacing:.18em;color:var(--hud)}
#score{font-family:var(--disp);font-size:22px;color:var(--gold);text-shadow:0 0 16px rgba(255,209,102,.45)}
#score .lbl{display:block;font-size:9px;text-align:right}
#fuelwrap{width:150px}
#fuelbar{height:8px;border:1px solid var(--line);background:rgba(0,0,0,.4);margin-top:4px;position:relative;overflow:hidden}
#fuelbar i{position:absolute;inset:0;width:100%;background:linear-gradient(90deg,#ff9d4a,#ffd166);transition:width .2s}
#fuelbar.low i{background:linear-gradient(90deg,#ff3b2a,#ff9d4a)}
#fuelbar::after{content:'';position:absolute;inset:0;background:repeating-linear-gradient(90deg,transparent 0 14px,rgba(4,9,14,.9) 14px 15px)}
#healthwrap{width:150px}
#healthbar{height:8px;border:1px solid var(--line);background:rgba(0,0,0,.4);margin-top:4px;position:relative;overflow:hidden}
#healthbar i{position:absolute;inset:0;width:100%;background:linear-gradient(90deg,#2affb0,#baff7a);transition:width .15s}
#healthbar.low i{background:linear-gradient(90deg,#ff3b2a,#ff9d4a)}
#healthbar::after{content:'';position:absolute;inset:0;background:repeating-linear-gradient(90deg,transparent 0 14px,rgba(4,9,14,.9) 14px 15px)}
#enginebox{display:flex;flex-direction:column;gap:2px;min-width:120px}
#throttlebar{width:120px;height:6px;border:1px solid var(--line);background:rgba(0,0,0,.4);margin-top:3px;position:relative}
#throttlebar i{position:absolute;left:0;top:0;bottom:0;width:100%;background:linear-gradient(90deg,#4ad0ff,#9ff2ea)}
#gbox{position:absolute;left:22px;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;gap:10px;align-items:flex-start}
.gpill{display:flex;align-items:center;gap:8px;font-family:var(--disp);font-size:15px;color:var(--hud)}
.gpill b{width:34px;height:34px;border:1px solid var(--line);display:flex;align-items:center;justify-content:center;background:var(--panel);border-radius:4px}
.gpill.hot b{color:var(--warn);border-color:var(--warn);box-shadow:0 0 14px rgba(255,95,74,.5)}
#rightbox{position:absolute;right:22px;top:50%;transform:translateY(-50%);display:flex;flex-direction:column;align-items:flex-end;gap:14px;text-align:right}
#altcol .val{font-size:26px}
#spdcol .val{font-size:26px}
#vsi .val{font-size:18px}
#aoacol .val{font-size:18px}
#warnbox{position:absolute;left:50%;bottom:150px;transform:translateX(-50%);display:flex;flex-direction:column;gap:6px;align-items:center}
.warn{font-family:var(--disp);font-size:15px;letter-spacing:.28em;padding:5px 18px;border:1px solid currentColor;
  background:rgba(0,0,0,.35);animation:blink 0.7s steps(2) infinite}
.warn.stall{color:var(--warn)} .warn.g{color:#ffb347} .warn.aoa{color:#ffd166} .warn.gear{color:var(--gold)} .warn.low{color:#ffb347}
@keyframes blink{50%{opacity:.25}}
#crosshair{position:absolute;left:50%;top:50%;width:0;height:0}
#crosshair::before{content:'';position:absolute;left:-1px;top:-1px;width:2px;height:2px;background:var(--hud);box-shadow:0 0 6px var(--hud)}
#crosshair .tick{position:absolute;background:var(--hud);opacity:.85;box-shadow:0 0 5px var(--hud)}
#crosshair .t{left:-14px;top:0;width:8px;height:1px} #crosshair .r{left:6px;top:0;width:8px;height:1px}
#crosshair .l{left:0;top:-14px;width:1px;height:8px} #crosshair .b{left:0;top:6px;width:1px;height:8px}
#apd{position:absolute;left:0;top:0;width:26px;height:26px;margin:-13px 0 0 -13px;opacity:0;transition:opacity .1s}
#apd svg{width:100%;height:100%}
#apd .lbl2{position:absolute;left:30px;top:8px;font-family:var(--disp);font-size:10px;color:var(--warn);letter-spacing:.1em}
#apd.locked{opacity:1} #apd.locked svg{filter:drop-shadow(0 0 6px var(--warn))}
#apd.locked .lbl2{color:var(--warn)}
#ads{position:absolute;left:50%;bottom:96px;transform:translateX(-50%);display:flex;gap:10px;align-items:flex-end}
#ads .lbl{width:100%;text-align:center}
#adsbar{width:220px;height:4px;background:rgba(0,0,0,.5);border:1px solid var(--line);position:relative;margin-bottom:6px}
#adsbar i{position:absolute;top:0;bottom:0;background:var(--hud);box-shadow:0 0 8px var(--hud)}
#adsdot{position:absolute;top:-4px;width:8px;height:8px;margin-left:-4px;border-radius:50%;background:var(--warn);box-shadow:0 0 10px var(--warn)}
#adsval{font-family:var(--disp);font-size:16px;color:var(--hud)}
#targetInfo{position:absolute;left:50%;bottom:70px;transform:translateX(-50%);font-family:var(--disp);font-size:11px;letter-spacing:.2em;color:var(--warn);opacity:0;transition:opacity .2s}
#targetInfo.on{opacity:1}
#killfeed{position:absolute;right:22px;top:120px;display:flex;flex-direction:column;gap:5px;align-items:flex-end}
.kill{font-family:var(--disp);font-size:11px;letter-spacing:.12em;color:var(--gold);background:var(--panel);border:1px solid rgba(255,209,102,.35);padding:4px 10px;animation:killin .3s ease-out}
@keyframes killin{from{transform:translateX(30px);opacity:0}}
#toast{position:absolute;left:50%;top:110px;transform:translateX(-50%);font-family:var(--disp);font-size:13px;letter-spacing:.24em;color:var(--hud);background:var(--panel);border:1px solid var(--line);padding:8px 22px;opacity:0;transition:opacity .3s}
#toast.on{opacity:1}
#minimap{position:absolute;left:22px;bottom:22px;width:170px;height:170px;border:1px solid var(--line);background:rgba(4,10,15,.6);border-radius:4px;overflow:hidden}
#minimap canvas{width:100%;height:100%;display:block}
#controlsHint{position:absolute;left:50%;bottom:26px;transform:translateX(-50%);font-family:var(--body);font-weight:600;font-size:11px;letter-spacing:.14em;color:var(--hud-dim);white-space:nowrap}
#controlsHint b{color:var(--hud);font-weight:700}
#pauseOverlay{position:fixed;inset:0;z-index:8;display:none;align-items:center;justify-content:center;flex-direction:column;gap:18px;background:rgba(3,8,12,.72);backdrop-filter:blur(3px)}
#pauseOverlay.on{display:flex}
#pauseOverlay h2{font-family:var(--disp);font-size:34px;letter-spacing:.4em;color:var(--hud);text-shadow:0 0 24px rgba(159,242,234,.4)}
#pauseOverlay p{font-size:14px;letter-spacing:.18em;color:var(--hud-dim)}
/* ============ MENU ============ */
#menu{position:fixed;inset:0;z-index:10;display:flex;flex-direction:column;overflow-y:auto;
  background:linear-gradient(160deg,rgba(4,10,16,.88),rgba(6,16,24,.72)),radial-gradient(ellipse at 70% 20%,rgba(30,90,120,.25),transparent 60%)}
#menu.hidden{display:none}
#menuTop{padding:42px 6vw 10px;display:flex;justify-content:space-between;align-items:flex-start;gap:20px}
#brand h1{font-family:var(--disp);font-weight:900;font-size:clamp(38px,6.4vw,76px);letter-spacing:.14em;line-height:.95;
  background:linear-gradient(180deg,#eafffb 20%,#7fd8e8 60%,#2a7f96 100%);-webkit-background-clip:text;background-clip:text;color:transparent;
  text-shadow:0 0 44px rgba(120,220,240,.25)}
#brand .sub{font-family:var(--disp);font-size:11px;letter-spacing:.5em;color:var(--hud-dim);margin-top:10px}
#brand .sub b{color:var(--gold)}
#topStats{display:flex;gap:26px;text-align:right}
#topStats .st .v{font-family:var(--disp);font-size:20px;color:var(--hud)}
#topStats .st .k{font-size:10px;letter-spacing:.24em;color:var(--hud-dim)}
#secTitle{font-family:var(--disp);font-size:12px;letter-spacing:.42em;color:var(--gold);margin:26px 6vw 14px;display:flex;align-items:center;gap:14px}
#secTitle::after{content:'';flex:1;height:1px;background:linear-gradient(90deg,rgba(255,209,102,.5),transparent)}
#cards{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:16px;padding:0 6vw}
.card{position:relative;border:1px solid var(--line);background:var(--panel);backdrop-filter:blur(6px);padding:18px 18px 14px;cursor:pointer;overflow:hidden;transition:transform .18s,border-color .18s,box-shadow .18s}
.card:hover{transform:translateY(-3px);border-color:rgba(159,242,234,.6);box-shadow:0 12px 34px rgba(0,0,0,.45),0 0 24px rgba(159,242,234,.12)}
.card.sel{border-color:var(--gold);box-shadow:0 0 0 1px var(--gold),0 0 30px rgba(255,209,102,.25)}
.card .role{font-family:var(--disp);font-size:9px;letter-spacing:.34em;color:var(--hud-dim)}
.card h3{font-family:var(--disp);font-size:21px;letter-spacing:.1em;color:#eafffb;margin:4px 0 2px}
.card .mfr{font-size:12px;letter-spacing:.14em;color:var(--hud-dim);margin-bottom:12px}
.card canvas{width:100%;height:118px;display:block;border:1px solid rgba(159,242,234,.15);background:radial-gradient(ellipse at 50% 120%,rgba(40,120,150,.25),rgba(4,10,16,.5));margin-bottom:12px}
#statsGrid{display:grid;grid-template-columns:1fr 1fr;gap:7px 14px}
.stat{display:flex;justify-content:space-between;align-items:baseline;font-size:12px;letter-spacing:.06em;color:#b9d8de;border-bottom:1px dotted rgba(159,242,234,.18);padding-bottom:2px}
.stat b{font-family:var(--disp);font-size:13px;color:var(--hud)}
.stat .u{font-size:10px;color:var(--hud-dim);margin-left:2px}
.card .desc{font-size:12.5px;line-height:1.45;color:#9fc3cc;margin-top:12px;min-height:36px}
.card .selTag{position:absolute;top:12px;right:12px;font-family:var(--disp);font-size:9px;letter-spacing:.2em;color:var(--gold);opacity:0}
.card.sel .selTag{opacity:1}
#modes{display:grid;grid-template-columns:1fr 1fr;gap:16px;padding:0 6vw}
@media(max-width:760px){#modes{grid-template-columns:1fr}}
.mode{border:1px solid var(--line);background:var(--panel);backdrop-filter:blur(6px);padding:18px;cursor:pointer;transition:transform .18s,border-color .18s}
.mode:hover{transform:translateY(-3px);border-color:rgba(159,242,234,.6)}
.mode.sel{border-color:var(--gold);box-shadow:0 0 0 1px var(--gold),0 0 26px rgba(255,209,102,.22)}
.mode .mtag{font-family:var(--disp);font-size:9px;letter-spacing:.3em;color:var(--hud-dim)}
.mode h4{font-family:var(--disp);font-size:19px;letter-spacing:.12em;color:#eafffb;margin:5px 0 8px}
.mode p{font-size:12.5px;line-height:1.5;color:#9fc3cc}
.mode .obj{margin-top:10px;font-size:11.5px;color:var(--gold);letter-spacing:.06em}
#menuFoot{padding:26px 6vw 40px;display:flex;justify-content:space-between;align-items:center;gap:20px;flex-wrap:wrap}
#ctrls{font-size:11.5px;letter-spacing:.1em;color:var(--hud-dim);line-height:1.8}
#ctrls b{color:var(--hud)}
#launch{font-family:var(--disp);font-size:17px;letter-spacing:.3em;padding:18px 46px;background:linear-gradient(180deg,rgba(159,242,234,.16),rgba(159,242,234,.05));
  border:1px solid var(--hud);color:var(--hud);cursor:pointer;transition:all .18s;text-shadow:0 0 14px rgba(159,242,234,.5)}
#launch:hover{background:linear-gradient(180deg,rgba(159,242,234,.3),rgba(159,242,234,.1));box-shadow:0 0 34px rgba(159,242,234,.35);transform:translateY(-2px)}
#launch:active{transform:translateY(0)}
#errbox{position:fixed;left:16px;bottom:16px;z-index:99;max-width:420px;background:rgba(60,8,4,.9);border:1px solid var(--warn);
  color:#ffd9d2;font-size:12px;padding:10px 14px;display:none;font-family:monospace}
</style>
<script type="importmap">
{ "imports": { "three": "https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.module.js" } }
</script>
</head>
<body>
<canvas id="gl"></canvas>
<div id="vignette"></div>
<div id="dmg"></div>
<div id="flash"></div>

<div id="hud">
  <div id="topbar">
    <div class="left">
      <div id="modeTag">—</div>
      <div id="aircraftTag">—</div>
    </div>
    <div class="right">
      <div id="score">0<span class="lbl">SCORE</span></div>
      <div id="fuelwrap"><div class="lbl">FUEL</div><div id="fuelbar"><i></i></div></div>
      <div id="healthwrap"><div class="lbl">HULL</div><div id="healthbar"><i></i></div></div>
      <div id="enginebox">
        <div class="lbl">THROTTLE</div>
        <div id="throttlebar"><i></i></div>
        <div class="lbl" style="margin-top:5px">RPM <span id="rpm" class="val" style="font-size:13px">—</span></div>
      </div>
    </div>
  </div>

  <div id="gbox">
    <div class="gpill" id="gpill"><b id="gval">1.0</b><span class="lbl">G-LOAD</span></div>
  </div>

  <div id="rightbox">
    <div id="altcol"><div class="lbl">ALTITUDE</div><div class="val" id="altval">0</div><div class="unit">FT</div></div>
    <div id="spdcol"><div class="lbl">AIRSPEED</div><div class="val" id="spdval">0</div><div class="unit">KT</div></div>
    <div id="vsi"><div class="lbl">V/S</div><div class="val" id="vsival">0</div><div class="unit">FT/MIN</div></div>
    <div id="aoacol"><div class="lbl">AOA</div><div class="val" id="aoaval">0.0</div><div class="unit">DEG</div></div>
  </div>

  <div id="crosshair"><i class="tick t"></i><i class="tick r"></i><i class="tick l"></i><i class="tick b"></i></div>
  <div id="apd"><svg viewBox="0 0 26 26"><path d="M13 2 L13 8 M13 18 L13 24 M2 13 L8 13 M18 13 L24 13" stroke="#ff5f4a" stroke-width="2" fill="none"/></svg><span class="lbl2">AP</span></div>
  <div id="ads"><div class="lbl">ANGLE-ON · TARGET</div><div id="adsbar"><i id="adsneedle"></i><span id="adsdot"></span></div><div id="adsval">—</div></div>
  <div id="targetInfo">TARGET —</div>
  <div id="warnbox"></div>
  <div id="killfeed"></div>
  <div id="toast"></div>
  <div id="minimap"><canvas id="mm" width="340" height="340"></canvas></div>
  <div id="controlsHint">—</div>
</div>

<div id="pauseOverlay"><h2>PAUSED</h2><p>PRESS ESC TO RESUME · M FOR MENU</p></div>

<div id="menu">
  <div id="menuTop">
    <div id="brand">
      <h1>APEX AERO</h1>
      <div class="sub">HIGH-FIDELITY FLIGHT SIMULATOR · <b>NEWTONIAN PHYSICS CORE</b></div>
    </div>
    <div id="topStats">
      <div class="st"><div class="v" id="statAircraft">3</div><div class="k">AIRCRAFT</div></div>
      <div class="st"><div class="v" id="statModes">2</div><div class="k">SCENARIOS</div></div>
      <div class="st"><div class="v" id="statTerrain">∞</div><div class="k">TERRAIN</div></div>
    </div>
  </div>

  <div id="secTitle">SELECT AIRCRAFT</div>
  <div id="cards"></div>

  <div id="secTitle">SELECT SCENARIO</div>
  <div id="modes">
    <div class="mode sel" data-mode="tl">
      <div class="mtag">SCENARIO A · PRECISION FLIGHT</div>
      <h4>TAKEOFF / LANDING</h4>
      <p>Roll out, manage rotation speed and climb-out, then fly a traffic pattern and put the wheels down inside the landing zone. Every aircraft has its own stall speed, sink rate and runway demand.</p>
      <div class="obj">OBJECTIVE: LIFT OFF · CLIMB 400 FT · LAND IN ZONE</div>
    </div>
    <div class="mode" data-mode="df">
      <div class="mtag">SCENARIO B · COMBAT</div>
      <h4>DOGFIGHT</h4>
      <p>High-G turning, energy management and slipstream combat against AI fighters. Watch your G-load, your angle-of-attack and your speed — the physics will not forgive a lazy turn.</p>
      <div class="obj">OBJECTIVE: DOWN 3 HOSTILES · SURVIVE</div>
    </div>
  </div>

  <div id="menuFoot">
    <div id="ctrls">
      <b>MOUSE</b> pitch / roll · <b>Q/E</b> rudder · <b>W/S</b> throttle · <b>SPACE</b> fire / <b>SHIFT</b> boost ·
      <b>G</b> gear · <b>B</b> brakes · <b>F</b> flap · <b>R</b> reset · <b>M</b> menu · <b>ESC</b> pause
    </div>
    <button id="launch">LAUNCH ▸</button>
  </div>
</div>

<div id="errbox"></div>
<script type="module">
import * as THREE from 'three';

/* ================= utilities ================= */
const clamp=(v,a,b)=>v<a?a:v>b?b:v;
const lerp=(a,b,t)=>a+(b-a)*t;
const KT=1.94384, FT=3.28084, D2R=0.0174533, R2D=57.2958;
const $=id=>document.getElementById(id);
function mulberry32(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296}}
function hash2(x,y){let h=Math.imul(x|0,374761393)+Math.imul(y|0,668265263);h=Math.imul(h^(h>>>13),1274126177);return((h^(h>>>16))>>>0)/4294967296}
function smooth(t){return t*t*(3-2*t)}
function fbm2(x,y,oct=5){let s=0,a=0.5,f=1,n=0;for(let i=0;i<oct;i++){s+=a*(hash2(Math.floor(x*f)+Math.sin(y*f)*100|0,Math.floor(y*f)))*2-0.5;a*=0.5;f*=2;n+=1}return s}
/* value-noise fBm with bilinear interpolation */
function vnoise(x,y){const xi=Math.floor(x),yi=Math.floor(y),xf=x-xi,yf=y-yi;
  const a=hash2(xi,yi),b=hash2(xi+1,yi),c=hash2(xi,yi+1),d=hash2(xi+1,yi+1);
  const u=smooth(xf),v=smooth(yf);
  return lerp(lerp(a,b,u),lerp(c,d,u),v)}
function fbm(x,y,oct=5,lac=2,gain=0.5){let s=0,a=1,f=1,n=0;for(let i=0;i<oct;i++){s+=a*vnoise(x*f,y*f);n+=a;a*=gain;f*=lac}return s/n}
function ridge(x,y,oct=4){let s=0,a=0.55,f=1;for(let i=0;i<oct;i++){s+=a*(1-Math.abs(2*fbm(x*f+7.3,y*f+3.1,3)-1));a*=0.5;f*=2.1}return s}

/* ================= atmosphere (ISA) ================= */
const SEA_P=101325, SEA_RHO=1.225, G0=9.80665;
function airDensity(altM){const a=clamp(altM,0,12000);return SEA_RHO*Math.pow(1-2.25577e-5*a,4.2561)}
function seaLevelSpeedOfSound(){return 340.294}

/* ================= aircraft definitions ================= */
const AIRCRAFT={
  falcon:{
    id:'falcon',name:'VF-21 FALCON',mfr:'Meridian Dynamics · Multirole',role:'MULTIROLE FIGHTER',
    desc:'Balanced all-rounder. Moderate wing loading, responsive roll, dependable energy handling across the envelope.',
    mass:8200, wingArea:28, CLmax:1.65, CL0:0.06, CD0:0.022, AR:5.6, kOswald:0.82,
    thrustMax:88000, fuelMax:1600,
    ix:185000, iy:265000, iz:38000,
    pEff:5.2, rEff:7.5, yEff:2.4, pDamp:3.4, rDamp:5.2, yDamp:1.6,
    vStall:142, vMax:720,
    colors:{body:0x8fa8b8,accent:0x2e6f86,dark:0x22303a,canopy:0x9fdcff},
    stats:[['MAX SPD','720','KT'],['THRUST','88','kN'],['CL MAX','1.65',''],['WING LOAD','293','KG/M²'],['STALL','142','KT'],['TURN','MED',''],['RANGE','LONG','']]
  },
  tempest:{
    id:'tempest',name:'TD-9 TEMPEST',mfr:'Kestrel Works · Heavy Interceptor',role:'HEAVY INTERCEPTOR',
    desc:'A flying brick with a rocket heart. Enormous thrust, heavy wing loading — slow to turn, devastating in a dive.',
    mass:14500, wingArea:42, CLmax:1.45, CL0:0.05, CD0:0.026, AR:4.4, kOswald:0.8,
    thrustMax:172000, fuelMax:2600,
    ix:340000, iy:520000, iz:70000,
    pEff:3.4, rEff:4.6, yEff:1.6, pDamp:2.6, rDamp:4.0, yDamp:1.4,
    vStall:178, vMax:820,
    colors:{body:0x6d7f8a,accent:0x9a4a1e,dark:0x1c242c,canopy:0xbfe6ff},
    stats:[['MAX SPD','820','KT'],['THRUST','172','kN'],['CL MAX','1.45',''],['WING LOAD','345','KG/M²'],['STALL','178','KT'],['TURN','HEAVY',''],['RANGE','LONG','']]
  },
  viper:{
    id:'viper',name:'CV-3 VIPER',mfr:'Atlas Sport · High-Performance Trainer',role:'AGILE TRAINER',
    desc:'Featherweight with a low stall and twitchy control response. The dogfighter scalpel — light, nimble, fragile.',
    mass:3100, wingArea:19, CLmax:1.75, CL0:0.05, CD0:0.020, AR:6.4, kOswald:0.84,
    thrustMax:46000, fuelMax:800,
    ix:62000, iy:94000, iz:14000,
    pEff:8.5, rEff:11.0, yEff:3.6, pDamp:4.2, rDamp:6.0, yDamp:1.9,
    vStall:104, vMax:560,
    colors:{body:0xc8ccd2,accent:0x1a7a4a,dark:0x2a343c,canopy:0xa8e8ff},
    stats:[['MAX SPD','560','KT'],['THRUST','46','kN'],['CL MAX','1.75',''],['WING LOAD','163','KG/M²'],['STALL','104','KT'],['TURN','LIGHT',''],['RANGE','SHORT','']]
  }
};
const AIR_LIST=['falcon','tempest','viper'];

/* ================= shared math state ================= */
const UP=new THREE.Vector3(0,1,0);
let _q1=new THREE.Quaternion(),_q2=new THREE.Quaternion(),_q3=new THREE.Quaternion();
let _v1=new THREE.Vector3(),_v2=new THREE.Vector3(),_v3=new THREE.Vector3(),_v4=new THREE.Vector3();
let _m1=new THREE.Matrix4();

/* ================= globals ================= */
let renderer,scene,camera,sun,skyMesh,water,cloudLayer;
let terrain=null, aircraft=null, mode=null, enemies=[], tracers=[], explosions=[], particles=[];
let menuScene=null,menuCam=null;
let locked=false, paused=false, menuOpen=true, running=false;
let score=0, kills=0, fuel=0, health=100, throttle=0.35, gearDown=false, brakes=false, flap=0, boost=false, boostT=0;
let input={pitch:0,roll:0,yaw:0,throttle:0.35,fire:false,boost:false,gear:false,brakes:false,flap:false,reset:false,menu:false};
let selAircraft='falcon', selMode='tl';
let lastT=performance.now(), hudAcc=0, adiT=0;
let groundContact=false, onRunway=false;
let worldSeed=1337;
const WORLD={chunk:240,res:48,range:9,radius:5};
let runways=[], lZones=[], spawnPoint=null;
let targetEnemy=null, apAngleOn=0, apLock=false;
let fireCd=0, shakeMag=0, shakeT=0;
let menuAircraftMesh=null, menuAngle=0;

window.addEventListener('error',e=>{const b=$('errbox');b.style.display='block';b.textContent='ERR: '+(e.message||e.error);});

/* ================= renderer & scene ================= */
function initRenderer(){
  renderer=new THREE.WebGLRenderer({canvas:$('gl'),antialias:true,powerPreference:'high-performance'});
  renderer.setPixelRatio(Math.min(devicePixelRatio,1.75));
  renderer.setSize(innerWidth,innerHeight);
  renderer.toneMapping=THREE.ACESFilmicToneMapping;
  renderer.toneMappingExposure=1.05;
  renderer.shadowMap.enabled=true;
  renderer.shadowMap.type=THREE.PCFSoftShadowMap;
  scene=new THREE.Scene();
  scene.fog=new THREE.Fog(0xbdd7e8,900,9500);
  camera=new THREE.PerspectiveCamera(62,innerWidth/innerHeight,0.5,30000);
  addEventListener('resize',()=>{camera.aspect=innerWidth/innerHeight;camera.updateProjectionMatrix();renderer.setSize(innerWidth,innerHeight)});
}
function makeSky(){
  const geo=new THREE.SphereGeometry(15000,32,16);
  const mat=new THREE.ShaderMaterial({
    side:THREE.BackSide,depthWrite:false,fog:false,
    uniforms:{top:{value:new THREE.Color(0x2f6fbe)},mid:{value:new THREE.Color(0x9cc4e4)},bot:{value:new THREE.Color(0xdcebf5)},
      sunDir:{value:new THREE.Vector3(0.5,0.35,0.2).normalize()}},
    vertexShader:`varying vec3 vW;void main(){vW=(modelMatrix*vec4(position,1.0)).xyz;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
    fragmentShader:`varying vec3 vW;uniform vec3 top;uniform vec3 mid;uniform vec3 bot;uniform vec3 sunDir;
      void main(){vec3 d=normalize(vW-cameraPosition);float h=clamp(d.y,-0.05,1.0);
      vec3 c=mix(bot,mid,smoothstep(0.0,0.12,h));c=mix(c,top,smoothstep(0.12,0.65,h));
      float s=max(dot(d,sunDir),0.0);c+=vec3(1.0,0.85,0.6)*pow(s,600.0)*3.0;c+=vec3(1.0,0.75,0.5)*pow(s,8.0)*0.12;
      gl_FragColor=vec4(c,1.0);}`
  });
  skyMesh=new THREE.Mesh(geo,mat);skyMesh.frustumCulled=false;scene.add(skyMesh);
}
function makeLights(){
  const hemi=new THREE.HemisphereLight(0xbfd9ee,0x3a4a3a,0.75);scene.add(hemi);
  sun=new THREE.DirectionalLight(0xfff1dc,1.6);
  sun.position.set(2400,1700,1100);sun.castShadow=true;
  sun.shadow.mapSize.set(2048,2048);
  const s=sun.shadow.camera;s.left=-600;s.right=600;s.top=600;s.bottom=-600;s.near=200;s.far=8000;
  sun.shadow.bias=-0.0004;
  scene.add(sun);scene.add(sun.target);
}
function makeWater(){
  water=new THREE.Mesh(new THREE.PlaneGeometry(30000,30000),
    new THREE.MeshStandardMaterial({color:0x1d5a7a,metalness:0.65,roughness:0.22}));
  water.rotation.x=-Math.PI/2;water.position.y=-2;scene.add(water);
}
function makeClouds(){
  const n=46;cloudLayer=new THREE.Group();
  const rnd=mulberry32(99);
  const mat=new THREE.MeshBasicMaterial({color:0xffffff,transparent:true,opacity:0.5,depthWrite:false,fog:false});
  const mat2=new THREE.MeshBasicMaterial({color:0xe8f0f8,transparent:true,opacity:0.42,depthWrite:false,fog:false});
  for(let i=0;i<n;i++){
    const m=(rnd()<0.5?mat:mat2);
    const cl=new THREE.Mesh(new THREE.SphereGeometry(1,6,4),m);
    const sx=120+rnd()*260,sy=10+rnd()*16,sz=60+rnd()*140;
    cl.scale.set(sx,sy,sz);
    cl.position.set((rnd()-0.5)*9000,1750+rnd()*1200,(rnd()-0.5)*9000);
    cl.userData.drift=2+rnd()*4;
    cloudLayer.add(cl);
  }
  scene.add(cloudLayer);
}
function updateSkyFollow(dt){
  if(skyMesh)skyMesh.position.copy(camera.position);
  if(cloudLayer)for(const c of cloudLayer.children){c.position.x+=c.userData.drift*dt;if(c.position.x>4600)c.position.x=-4600;}
  const alt=camera.position.y;
  const k=clamp(alt/4000,0,1);
  if(skyMesh){const u=skyMesh.material.uniforms;
    u.top.value.set(0x2f6fbe).lerp(new THREE.Color(0x0a1e4e),k*0.8);
    u.bot.value.set(0xdcebf5).lerp(new THREE.Color(0xa8c8e8),k*0.5);
    scene.fog.color.set(0xbdd7e8).lerp(new THREE.Color(0x9db8d8),k*0.5);
  }
}

/* ================= procedural terrain ================= */
let flattenSpots=[];
const TL_ANCHOR={x:0,z:-2600,r:1500}, LZ_ANCHOR={x:900,z:-3300,r:1100};
function heightAt(x,z){
  const nx=x*0.00035, nz=z*0.00035;
  const cont=fbm(nx*0.5+11.2,nz*0.5+7.7,4);
  const m=ridge(nx*1.4+3.3,nz*1.4+9.1,4);
  const hills=fbm(nx*3.1+5.5,nz*3.1+2.2,4);
  let h=(cont*60-30)+m*m*430+hills*26;
  if(h<12)h*=0.55;
  // coastal plain around the airfield anchors (gentle fade to flat)
  for(const A of [TL_ANCHOR,LZ_ANCHOR]){
    const dx=x-A.x,dz=z-A.z,d=Math.sqrt(dx*dx+dz*dz);
    if(d<A.r){const k=smooth(clamp(d/A.r,0,1));h=lerp(6,h,k)}
  }
  for(const f of flattenSpots){
    const dx=x-f.x,dz=z-f.z,d=Math.sqrt(dx*dx+dz*dz);
    if(d<f.r){const t=smooth(clamp(1-d/f.r,0,1));h=lerp(h,f.h,t)}
  }
  return h;
}
function slopeAt(x,z){const e=6;const hx=heightAt(x+e,z)-heightAt(x-e,z),hz=heightAt(x,z+e)-heightAt(x,z-e);return Math.sqrt(hx*hx+hz*hz)/(2*e)}
function initTerrain(){
  terrain={chunks:new Map(),lastUpdate:0};
}
function buildChunk(cx,cz){
  const C=WORLD.chunk, R=WORLD.res;
  const geo=new THREE.PlaneGeometry(C,C,R-1,R-1);
  geo.rotateX(-Math.PI/2);
  const pos=geo.attributes.position;
  const col=new Float32Array(pos.count*3);
  const c=new THREE.Color();
  const mo=mulberry32(cx*73856093^cz*19349663);
  for(let i=0;i<pos.count;i++){
    const wx=cx*C+pos.getX(i), wz=cz*C+pos.getZ(i);
    const h=heightAt(wx,wz);
    pos.setY(i,h);
    const s=slopeAt(wx,wz);
    const wet=fbm(wx*0.002+40,wz*0.002+17,3);
    const v=0.85+mo()*0.3;
    if(h<4)c.setRGB(0.79*v,0.72*v,0.52*v);
    else if(h<16)c.setRGB(0.42*v,0.58*v,0.30*v).multiplyScalar(lerp(1,0.8,wet));
    else if(h<60)c.setRGB(0.27*v,0.44*v,0.24*v);
    else if(h<150)c.setRGB(0.48*v,0.46*v,0.41*v);
    else if(h<240)c.setRGB(0.58*v,0.57*v,0.53*v);
    else c.setRGB(0.90*v,0.92*v,0.94*v);
    if(s>0.55&&h>8)c.setRGB(0.45*v,0.43*v,0.40*v);
    col[i*3]=c.r;col[i*3+1]=c.g;col[i*3+2]=c.b;
  }
  geo.computeVertexNormals();
  geo.setAttribute('color',new THREE.BufferAttribute(col,3));
  const mat=new THREE.MeshStandardMaterial({vertexColors:true,roughness:0.95,metalness:0.02});
  const mesh=new THREE.Mesh(geo,mat);
  mesh.position.set(cx*C,0,cz*C);
  mesh.receiveShadow=true;
  scene.add(mesh);
  terrain.chunks.set(cx+','+cz,mesh);
}
function removeChunk(key){
  const m=terrain.chunks.get(key);
  if(m){scene.remove(m);m.geometry.dispose();m.material.dispose();terrain.chunks.delete(key)}
}
function updateTerrain(t){
  if(t-terrain.lastUpdate<0.4)return;
  terrain.lastUpdate=t;
  const px=aircraft?aircraft.group.position.x:camera.position.x;
  const pz=aircraft?aircraft.group.position.z:camera.position.z;
  const ccx=Math.round(px/WORLD.chunk), ccz=Math.round(pz/WORLD.chunk);
  const need=new Set();
  for(let dx=-WORLD.range;dx<=WORLD.range;dx++)for(let dz=-WORLD.range;dz<=WORLD.range;dz++){
    if(dx*dx+dz*dz>WORLD.range*WORLD.range)continue;
    need.add((ccx+dx)+','+(ccz+dz));
  }
  let budget=3;
  for(const key of need){
    if(!terrain.chunks.has(key)){if(budget<=0)break;buildChunk(...key.split(',').map(Number));budget--}
  }
  for(const key of terrain.chunks.keys()){
    const [cx,cz]=key.split(',').map(Number);
    if(!need.has(key))removeChunk(key);
  }
}
function makeRunway(x,z,yaw,len=1500,w=46){
  const g=new THREE.Group();
  const y=heightAt(x,z)+0.6;
  const road=new THREE.Mesh(new THREE.PlaneGeometry(w,len),
    new THREE.MeshStandardMaterial({color:0x2a2e33,roughness:0.9}));
  road.rotation.x=-Math.PI/2;road.receiveShadow=true;g.add(road);
  const dashMat=new THREE.MeshBasicMaterial({color:0xd8d8d0});
  for(let d=-len/2+30;d<len/2-30;d+=42){
    const dash=new THREE.Mesh(new THREE.PlaneGeometry(1.4,16),dashMat);
    dash.rotation.x=-Math.PI/2;dash.position.set(0,0.05,d);g.add(dash);
  }
  for(const s of[-1,1])for(let d=-len/2+12;d<len/2-12;d+=28){
    const lt=new THREE.Mesh(new THREE.SphereGeometry(0.9,6,6),
      new THREE.MeshBasicMaterial({color:s<0?0xffb347:0x66e0ff}));
    lt.position.set(s*(w/2+2),1.2,d);g.add(lt);
  }
  for(let i=0;i<5;i++){
    const lt=new THREE.Mesh(new THREE.SphereGeometry(1.1,6,6),new THREE.MeshBasicMaterial({color:0xfff2cc}));
    lt.position.set(0,1.6,-len/2-20-i*18);g.add(lt);
  }
  g.position.set(x,y,z);g.rotation.y=yaw;
  scene.add(g);
  runways.push({x,z,y:yaw,len,w,heading:new THREE.Vector3(Math.sin(yaw),0,Math.cos(yaw))});
  return g;
}
function makeLandZone(x,z,r=120){
  const y=heightAt(x,z)+0.8;
  const ring=new THREE.Mesh(new THREE.RingGeometry(r-6,r,48),
    new THREE.MeshBasicMaterial({color:0xffd166,transparent:true,opacity:0.55,side:THREE.DoubleSide}));
  ring.rotation.x=-Math.PI/2;ring.position.set(x,y,z);scene.add(ring);
  for(let i=0;i<8;i++){
    const a=i/8*Math.PI*2;
    const p=new THREE.Mesh(new THREE.SphereGeometry(1.6,6,6),new THREE.MeshBasicMaterial({color:0xffd166}));
    p.position.set(x+Math.cos(a)*r,y+2,z+Math.sin(a)*r);scene.add(p);
  }
  lZones.push({x,z,y,r});
}
function setupWorld(){
  flattenSpots=[];runways=[];lZones=[];
  for(const k of terrain.chunks.keys())removeChunk(k);
  if(mode==='tl'){
    spawnPoint=new THREE.Vector3(0,0,-2600);
    const hy=heightAt(0,-2600);
    flattenSpots.push({x:0,z:-2600,r:420,h:Math.max(hy,9)});
    makeRunway(0,-2600,0);
    const lz={x:900,z:-3300};
    flattenSpots.push({x:lz.x,z:lz.z,r:300,h:Math.max(heightAt(lz.x,lz.z),9)});
    makeLandZone(lz.x,lz.z,130);
  }else{
    spawnPoint=new THREE.Vector3(0,950,-3800);
  }
}
function resetPlaneToSpawn(){
  const a=aircraft;
  if(mode==='tl'){
    a.group.position.set(0,heightAt(0,-2600)+1.6,-2600);
    a.quat.identity(); // +Z forward aligns with runway heading
  }else{
    a.group.position.copy(spawnPoint);
    a.quat.identity();
  }
  a.vel.set(0,0,0);a.angVel.set(0,0,0);
  a.gearDeployed=mode==='tl';a.gearT=mode==='tl'?1:0;a.flapT=0;
}

/* ================= aircraft 3D models ================= */
function matStd(color,rough=0.45,metal=0.55){return new THREE.MeshStandardMaterial({color,roughness:rough,metalness:metal})}
function buildAircraft(def,forMenu=false){
  const g=new THREE.Group();
  const C=def.colors;
  const bodyM=matStd(C.body,0.42,0.6), accM=matStd(C.accent,0.5,0.4), darkM=matStd(C.dark,0.6,0.3);
  const canopyM=new THREE.MeshStandardMaterial({color:C.canopy,roughness:0.08,metalness:0.9,transparent:true,opacity:0.92});
  const heavy=def.id==='tempest', light=def.id==='viper';
  const fusLen=heavy?16.5:14.5, fusR=heavy?1.55:1.25;
  // fuselage
  const fus=new THREE.Mesh(new THREE.CapsuleGeometry(fusR,fusLen,6,14),bodyM);
  fus.rotation.x=Math.PI/2;fus.scale.set(1,0.72,1);fus.castShadow=true;g.add(fus);
  // nose cone
  const nose=new THREE.Mesh(new THREE.ConeGeometry(fusR*0.85,heavy?3.4:2.8,14),darkM);
  nose.rotation.x=Math.PI/2;nose.position.z=fusLen/2+1.6;g.add(nose);
  // canopy
  const can=new THREE.Mesh(new THREE.SphereGeometry(fusR*0.8,14,10),canopyM);
  can.scale.set(1,0.75,2.1);can.position.set(0,fusR*0.55,1.2);g.add(can);
  // intake
  const intake=new THREE.Mesh(new THREE.BoxGeometry(fusR*1.5,fusR*1.1,fusR*1.6),darkM);
  intake.position.set(0,-fusR*0.15,fusLen/2+0.4);g.add(intake);
  // tailpipe + afterburner glow
  const pipe=new THREE.Mesh(new THREE.CylinderGeometry(fusR*0.5,fusR*0.62,2.6,12),darkM);
  pipe.rotation.x=Math.PI/2;pipe.position.z=-fusLen/2-0.6;g.add(pipe);
  const burner=new THREE.Mesh(new THREE.ConeGeometry(fusR*0.42,1.6,12),
    new THREE.MeshBasicMaterial({color:0x66ccff,transparent:true,opacity:0.0}));
  burner.rotation.x=-Math.PI/2;burner.position.z=-fusLen/2-2.2;g.add(burner);
  // wings
  const wingSpan=heavy?13.5:(light?10.5:12), wingChord=heavy?4.2:3.6, wingY=0.1, wingZ=-0.6;
  const wingGeo=new THREE.BoxGeometry(1,0.34,wingChord);
  for(const s of[-1,1]){
    const wing=new THREE.Mesh(wingGeo,bodyM);
    wing.scale.x=wingSpan/2-0.5;
    wing.position.set(s*(wingSpan/4+0.5),wingY,wingZ);
    wing.rotation.z=s*0.06; // slight dihedral
    wing.castShadow=true;
    const flapGroup=new THREE.Group();
    const flap=new THREE.Mesh(new THREE.BoxGeometry(1,0.22,wingChord*0.32),accM);
    flap.scale.x=wingSpan/2-0.6;
    flapGroup.add(flap);
    flapGroup.position.set(0,-0.12,-wingChord*0.62);
    wing.add(flapGroup);
    g.add(wing);
    if(s<0)g.userData.flapsL=flapGroup; else g.userData.flapsR=flapGroup;
    // wingtip light
    const tip=new THREE.Mesh(new THREE.SphereGeometry(0.28,6,6),
      new THREE.MeshBasicMaterial({color:s<0?0xff4444:0x44ff88}));
    tip.position.set(s*(wingSpan/2-0.4),wingY+0.3,wingZ);g.add(tip);
  }
  // tail
  const hstab=new THREE.Mesh(new THREE.BoxGeometry(heavy?6.4:5.2,0.28,2.4),bodyM);
  hstab.position.set(0,0.5,-fusLen/2+0.6);g.add(hstab);
  for(const s of[-1,1]){
    const vstab=new THREE.Mesh(new THREE.BoxGeometry(0.26,heavy?3.6:2.9,2.6),accM);
    vstab.position.set(s*(fusR*0.55),fusR*0.9,-fusLen/2+0.8);
    vstab.rotation.y=s*0.12;g.add(vstab);
  }
  // rudder reference
  const rudder=new THREE.Mesh(new THREE.BoxGeometry(0.22,heavy?2.6:2.2,1.6),darkM);
  rudder.position.set(0,fusR*0.9+1.2,-fusLen/2+0.8);g.add(rudder);
  g.userData.rudder=rudder;
  // propeller (visual only, for trainer) or none
  let prop=null;
  if(light){
    prop=new THREE.Group();
    for(let i=0;i<3;i++){
      const bl=new THREE.Mesh(new THREE.BoxGeometry(0.22,3.6,0.5),darkM);
      bl.position.y=1.9;
      const holder=new THREE.Group();holder.add(bl);holder.rotation.z=i*Math.PI*2/3;prop.add(holder);
    }
    prop.position.set(0,0,fusLen/2+3.2);g.add(prop);
  }
  // landing gear
  const gearParts=[];
  const gearMat=matStd(0x11151a,0.8,0.2), tireMat=matStd(0x0a0c0e,0.95,0);
  const mkWheel=(x,y,z,r=0.9)=>{
    const gr=new THREE.Group();
    const strut=new THREE.Mesh(new THREE.CylinderGeometry(0.12,0.12,1.1,8),gearMat);
    strut.position.y=0.55;gr.add(strut);
    const tire=new THREE.Mesh(new THREE.CylinderGeometry(r,r,r*0.5,12),tireMat);
    tire.rotation.z=Math.PI/2;tire.position.y=-0.2;gr.add(tire);
    gr.position.set(x,y,z);g.add(gr);gearParts.push(gr);
  };
  mkWheel(-fusR*0.9,-fusR*0.55,fusLen/4);
  mkWheel(fusR*0.9,-fusR*0.55,fusLen/4);
  mkWheel(0,-fusR*0.55,-fusLen/3);
  g.userData.gearParts=gearParts;
  g.userData.burner=burner;
  g.userData.prop=prop;
  g.userData.def=def;
  if(!forMenu){g.traverse(o=>{if(o.isMesh)o.castShadow=true})}
  return g;
}
function makeAircraft(id){
  const def=AIRCRAFT[id];
  const group=buildAircraft(def);
  return {
    def, group, pos:new THREE.Vector3(), quat:new THREE.Quaternion(),
    vel:new THREE.Vector3(), angVel:new THREE.Vector3(),
    gearDeployed:false, gearT:0, flapT:0,
    fuel:def.fuelMax, health:100,
    forward:new THREE.Vector3(0,0,1), right:new THREE.Vector3(1,0,0), upW:new THREE.Vector3(0,1,0)
  };
}
function setGearVisual(a,deployed){
  a.gearDeployed=deployed;
}
function setFlapVisual(a,flapDeg){
  if(a.group.userData.flapsL)a.group.userData.flapsL.rotation.x=flapDeg*0.0174;
  if(a.group.userData.flapsR)a.group.userData.flapsR.rotation.x=flapDeg*0.0174;
}

/* ================= FLIGHT PHYSICS CORE (Newtonian) =================
   Forces: Lift (0.5·ρ·V²·S·CL), Drag (profile + induced), Thrust (rpm·power·ρ),
   Weight. Attitude integrated via angular velocity with damping, control
   authority scaled by airspeed, prograde stability and stall buffet.
============================================================= */
const PHYS={alphaStall:0.30, slope:(AIRCRAFT.falcon.CLmax-AIRCRAFT.falcon.CL0)/0.30};
function physStep(a,dt,input,isP){
  const d=a.def, m=d.mass, S=d.wingArea;
  a.forward.set(0,0,1).applyQuaternion(a.quat);
  a.right.set(1,0,0).applyQuaternion(a.quat);
  a.upW.set(0,1,0).applyQuaternion(a.quat);
  const V=a.vel.length();
  const rho=airDensity(Math.max(a.pos.y,0));
  /* --- angle of attack & sideslip --- */
  const vFwd=a.vel.dot(a.forward), vUp=a.vel.dot(a.upW), vRight=a.vel.dot(a.right);
  let alpha=Math.atan2(vUp,Math.max(vFwd,0.01));
  let beta=Math.atan2(vRight,Math.max(vFwd,0.01));
  a.lastV=V; a.lastAlpha=alpha;
  /* --- engine (rpm lags throttle -> realistic response) --- */
  const rpmT=clamp(input.throttle,0,1);
  a.rpm=lerp(a.rpm??0.35,rpmT,1-Math.exp(-dt*1.9));
  const altFactor=clamp(rho/1.225,0.30,1);
  const fuelOK=(isP? a.fuel>0 : true);
  const boostF=(a.boostT>0)?1.35:1;
  const thrust=fuelOK? a.rpm*d.thrustMax*altFactor*boostF : 0;
  if(isP&&fuelOK){a.fuel=Math.max(0,a.fuel-(a.rpm*d.fuelMax*0.0011+(a.boostT>0?d.fuelMax*0.0012:0))*dt); a.boostT=Math.max(0,a.boostT-dt*0.5); if(a.boostT<=0)a.boostOn=false;}
  /* --- lift / stall --- */
  const vStall=d.vStall/KT;
  const flapBonus=a.flapT*0.0085;
  let CL=d.CL0+PHYS.slope*alpha;
  let stalled=false;
  if(alpha>PHYS.alphaStall){CL=d.CLmax-(alpha-PHYS.alphaStall)*PHYS.slope*2.6; stalled=V>vStall*0.55;}
  if(V<vStall*0.92&&alpha>0.12){CL=Math.min(CL,d.CLmax*0.82); stalled=true;}
  CL+=flapBonus;
  a.lastCL=CL; a.lastStall=stalled;
  const qDyn=0.5*rho*V*V;
  const L=qDyn*S*CL;
  /* --- drag: profile + induced + config --- */
  let CD=d.CD0+(CL*CL)/(Math.PI*d.AR*d.kOswald);
  CD+=a.gearT*0.022+a.flapT*0.0011;
  const D=qDyn*S*CD;
  a.lastCD=CD;
  /* --- force sum --- */
  _v1.copy(a.upW).multiplyScalar(L);
  if(V>0.01)_v2.copy(a.vel).multiplyScalar(-D/V); else _v2.set(0,0,0);
  _v3.copy(a.forward).multiplyScalar(thrust);
  _v1.add(_v2).add(_v3);
  _v1.y-=m*G0;
  /* --- linear integration --- */
  a.vel.addScaledVector(_v1,(dt/m));
  a.pos.addScaledVector(a.vel,dt);
  a.lastG=(_v1.dot(a.upW))/(m*G0);
  /* --- angular dynamics --- */
  let pR=a.angVel.dot(a.right), qR=a.angVel.dot(a.upW), rR=a.angVel.dot(a.forward);
  const eff=clamp(V/(vStall*1.05),0.28,1.25)*(V<3?0.4:1);
  const st=stalled?0.45:1;
  qR+=input.pitch*d.pEff*eff*st*dt;
  qR+=-alpha*1.1*dt;            // prograde: nose chases relative wind
  pR+=input.roll*d.rEff*eff*st*dt;
  pR+=-beta*1.5*dt;             // weathervane roll stability
  rR+=input.yaw*d.yEff*eff*dt;
  qR-=qR*d.pDamp*dt; pR-=pR*d.rDamp*dt; rR-=rR*d.yDamp*dt;
  if(stalled){ // buffet
    qR+=(Math.random()-0.5)*1.6*dt*3; pR+=(Math.random()-0.5)*1.4*dt*3;
    if(isP)shakeMag=Math.max(shakeMag,0.25);
  }
  pR=clamp(pR,-2.7,2.7); qR=clamp(qR,-2.4,2.4); rR=clamp(rR,-1.8,1.8);
  a.angVel.set(0,0,0).addScaledVector(a.right,pR).addScaledVector(a.upW,qR).addScaledVector(a.forward,rR);
  _q1.setFromAxisAngle(_v1.set(1,0,0),pR*dt);
  _q2.setFromAxisAngle(_v2.set(0,1,0),qR*dt);
  _q3.setFromAxisAngle(_v3.set(0,0,1),rR*dt);
  a.quat.multiply(_q1).multiply(_q2).multiply(_q3).normalize();
}
function groundStep(a,dt){
  const d=a.def;
  const gy=heightAt(a.pos.x,a.pos.z);
  const sea=gy<-0.5;
  const gearOff=1.35;
  if(sea){ if(a.pos.y-gearOff<-1.5){ if(!a.dead){a.dead=true;explode(a.pos,true); if(a===aircraft)toast('LOST IN THE SEA');} a.pos.y=-9999; return;} }
  const contact=!sea&&(a.pos.y-gearOff<=gy+0.06);
  a.onGround=contact;
  if(contact){
    a.pos.y=gy+gearOff;
    if(a.vel.y<0){
      const impact=-a.vel.y; a.vel.y=0;
      if(impact>16&&a.gearT<0.5){a.health-=(impact-16)*2.2; if(a===aircraft)shake(0.8,0.5);}
      else if(impact>30){a.health-=(impact-30)*1.2;}
    }
    /* friction: runway vs rough ground, brakes */
    let Vh=Math.hypot(a.vel.x,a.vel.z);
    if(Vh>0.01){
      const mu=(onRunwayAt(a.pos)?0.42:0.28)+(a.brake?0.95:0);
      const nV=Math.max(0,Vh-(mu*G0+0.35)*dt);
      const k=nV/Vh; a.vel.x*=k; a.vel.z*=k;
    }
    /* gear constrains attitude */
    a.angVel.multiplyScalar(Math.max(0,1-7*dt));
    /* liftoff: lift > weight */
    const vStall=d.vStall/KT;
    const L=0.5*airDensity(Math.max(a.pos.y,0))*a.vel.length()**2*d.wingArea*(d.CL0+PHYS.slope*clamp(a.lastAlpha,0,0.5)+a.flapT*0.0085);
    if(L>d.mass*G0*0.96&&a.vel.length()>vStall*0.9){
      a.pos.y+=0.02; a.vel.y=Math.max(a.vel.y,1.5);
      if(a===aircraft){groundContact=false; toast('LIFT-OFF · +500'); score+=500; sfx('liftoff');}
    }
  }else{
    if(a===aircraft&&groundContact&&a.vel.y<0&&a.pos.y-gearOff<=gy+0.2){
      /* touchdown */
      const impact=-a.vel.y; a.vel.y=0;
      if(a.gearT<0.5){a.health-=28; toast('HARD GEAR IMPACT');}
      else if(impact>18){a.health-=(impact-18)*2.5; shake(0.5,0.4);}
      else if(impact<4){ if(mode==='tl'){score+=750; toast('SOFT TOUCHDOWN · +750'); sfx('goodland');} }
      if(mode==='tl'&&inLandZone(a.pos)){score+=1000; missionComplete('PRECISION LANDING');}
    }
  }
  if(a.health<=0&&!a.dead){a.dead=true;explode(a.pos,true); if(a===aircraft)toast('AIRCRAFT LOST');}
}
function isPlayer(a){return a===aircraft}
function onRunwayAt(p){for(const r of runways){const dx=p.x-r.x,dz=p.z-r.z;const along=dx*r.heading.x+dz*r.heading.z;if(Math.abs(along)>r.len/2+8)return false;const lat=dx*r.heading.z-dz*r.heading.x;if(Math.abs(lat)<r.w/2+4)return true}return false}
function inLandZone(p){for(const z of lZones){const dx=p.x-z.x,dz=p.z-z.z;if(dx*dx+dz*dz<z.r*z.r)return true}return false}
function shake(mag,t){shakeMag=Math.max(shakeMag,mag);shakeT=Math.max(shakeT,t)}

/* ================= AUDIO (synthesized, physics-linked) ================= */
let AC=null, master=null, engineGain=null, engineFilter=null, oscs=[], noiseGain=null, noiseFilter=null,
    windGain=null, windFilter=null, ambGain=null, ambFilter=null, creakGain=null;
function initAudio(){
  if(AC)return;
  try{
    AC=new (window.AudioContext||window.webkitAudioContext)();
  }catch(e){return}
  master=AC.createGain();master.gain.value=0.85;master.connect(AC.destination);
  // noise buffer (2s white)
  const nb=AC.createBuffer(1,AC.sampleRate*2,AC.sampleRate);
  const nd=nb.getChannelData(0);for(let i=0;i<nd.length;i++)nd[i]=Math.random()*2-1;
  const mkNoise=()=>{const s=AC.createBufferSource();s.buffer=nb;s.loop=true;s.start();return s};
  // --- engine: 3 detuned saws -> lowpass -> gain ---
  engineGain=AC.createGain();engineGain.gain.value=0;
  engineFilter=AC.createBiquadFilter();engineFilter.type='lowpass';engineFilter.frequency.value=900;engineFilter.Q.value=1.4;
  const sub=AC.createGain();sub.gain.value=0.5;
  const o1=AC.createOscillator();o1.type='sawtooth';o1.frequency.value=60;
  const o2=AC.createOscillator();o2.type='sawtooth';o2.frequency.value=61.5;
  const o3=AC.createOscillator();o3.type='square';o3.frequency.value=122;
  const o3g=AC.createGain();o3g.gain.value=0.25;
  o1.connect(sub);o2.connect(sub);o3.connect(o3g);o3g.connect(sub);
  sub.connect(engineFilter);engineFilter.connect(engineGain);engineGain.connect(master);
  o1.start();o2.start();o3.start();oscs=[o1,o2,o3];
  // --- aero whine: bandpass noise ---
  noiseGain=AC.createGain();noiseGain.gain.value=0;
  noiseFilter=AC.createBiquadFilter();noiseFilter.type='bandpass';noiseFilter.frequency.value=1200;noiseFilter.Q.value=0.8;
  const n1=mkNoise();n1.connect(noiseFilter);noiseFilter.connect(noiseGain);noiseGain.connect(master);
  // --- wind: lowpass noise ---
  windGain=AC.createGain();windGain.gain.value=0;
  windFilter=AC.createBiquadFilter();windFilter.type='lowpass';windFilter.frequency.value=320;
  const n2=mkNoise();n2.connect(windFilter);windFilter.connect(windGain);windGain.connect(master);
  // --- ambient: very low rumble ---
  ambGain=AC.createGain();ambGain.gain.value=0.02;
  ambFilter=AC.createBiquadFilter();ambFilter.type='lowpass';ambFilter.frequency.value=90;
  const n3=mkNoise();n3.connect(ambFilter);ambFilter.connect(ambGain);ambGain.connect(master);
  // --- gear creak: short filtered noise burst (triggered) ---
  creakGain=AC.createGain();creakGain.gain.value=0;
  const crF=AC.createBiquadFilter();crF.type='bandpass';crF.frequency.value=420;crF.Q.value=2;
  const n4=mkNoise();n4.connect(crF);crF.connect(creakGain);creakGain.connect(master);
}
function gearSfx(){if(!AC)return;const t=AC.currentTime;creakGain.gain.cancelScheduledValues(t);creakGain.gain.setValueAtTime(0.35,t);creakGain.gain.exponentialRampToValueAtTime(0.001,t+0.5);}
function sfx(kind){
  if(!AC)return;const t=AC.currentTime;
  const tone=(f,dur,vol,type='sine',slide=0)=>{const o=AC.createOscillator(),g=AC.createGain();o.type=type;o.frequency.setValueAtTime(f,t);if(slide)o.frequency.exponentialRampToValueAtTime(Math.max(30,f+slide),t+dur);g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(0.001,t+dur);o.connect(g);g.connect(master);o.start(t);o.stop(t+dur+0.02);};
  const burst=(f,dur,vol,q=1)=>{const s=AC.createBufferSource();s.buffer=engineGain?nbBuf():null;const fl=AC.createBiquadFilter();fl.type='bandpass';fl.frequency.value=f;fl.Q.value=q;const g=AC.createGain();g.gain.setValueAtTime(vol,t);g.gain.exponentialRampToValueAtTime(0.001,t+dur);s.connect(fl);fl.connect(g);g.connect(master);s.start(t);s.stop(t+dur+0.02);};
  if(kind==='shot'){tone(340,0.09,0.3,'square',-260);burst(900,0.07,0.22,1.2);}
  if(kind==='boom'){tone(70,0.9,0.5,'sine',-40);burst(180,0.8,0.5,0.6);}
  if(kind==='lock'){tone(1400,0.06,0.16,'square');}
  if(kind==='liftoff'){tone(220,0.5,0.2,'sine',160);}
  if(kind==='goodland'){tone(880,0.12,0.2);tone(1320,0.25,0.18);}
  if(kind==='warn'){tone(660,0.14,0.18,'square');}
}
let _nbCache=null;
function nbBuf(){if(_nbCache)return _nbCache;const nb=AC.createBuffer(1,AC.sampleRate,AC.sampleRate);const d=nb.getChannelData(0);for(let i=0;i<d.length;i++)d[i]=Math.random()*2-1;_nbCache=nb;return nb}
function updateAudio(dt){
  if(!AC||!aircraft)return;
  const a=aircraft, V=a.lastV||0, m=aircraft.def.mass;
  const t=AC.currentTime;
  const rpm=a.rpm||0.35;
  // engine: pitch & volume from rpm + altitude (air density thins -> thinner tone)
  const rho=airDensity(Math.max(a.pos.y,0));
  const densF=clamp(rho/1.225,0.3,1);
  const fBase=42+rpm*95*densF;
  oscs[0].frequency.setTargetAtTime(fBase,t,0.05);
  oscs[1].frequency.setTargetAtTime(fBase*1.012,t,0.05);
  oscs[2].frequency.setTargetAtTime(fBase*2.02,t,0.05);
  engineFilter.frequency.setTargetAtTime(300+rpm*1900+densF*400,t,0.08);
  const vol=aircraft.dead?0:clamp(0.05+rpm*0.5,0,0.6)*(a.onGround?0.8:1)*(a.fuel<=0?0.05:1);
  engineGain.gain.setTargetAtTime(vol,t,0.09);
  // aero whine: q_dyn
  const qD=0.5*rho*V*V;
  noiseFilter.frequency.setTargetAtTime(500+V*2.2,t,0.1);
  noiseGain.gain.setTargetAtTime(clamp(qD/26000,0,0.4)*(aircraft.dead?0:1),t,0.12);
  // wind: relative speed, stronger at altitude
  windFilter.frequency.setTargetAtTime(180+V*1.1+Math.max(0,a.pos.y)*0.05,t,0.15);
  windGain.gain.setTargetAtTime(clamp(V/520,0,0.5)*(aircraft.dead?0:1),t,0.15);
  // ambient rumble: proximity to terrain
  const gy=heightAt(a.pos.x,a.pos.z);
  const prox=clamp(1-(a.pos.y-gy)/900,0,1);
  ambGain.gain.setTargetAtTime(0.015+prox*0.05,t,0.3);
}

/* ================= HUD ================= */
let adi=null;
function initHUD(){
  const c=document.createElement('canvas');
  c.width=340;c.height=340;
  c.style.cssText='position:absolute;left:50%;top:50%;width:340px;height:340px;transform:translate(-50%,-50%);opacity:.9;pointer-events:none';
  $('hud').appendChild(c);
  adi={canvas:c,ctx:c.getContext('2d')};
}
function drawADI(dt){
  const {ctx}=adi, S=340, cx=S/2, cy=S/2;
  const a=aircraft;
  // pitch from velocity: angle between forward and vel
  let pitch=0;
  if(a.lastV>1){const vF=a.vel.dot(a.forward),vU=a.vel.dot(a.upW);pitch=Math.atan2(vU,Math.max(vF,0.01));}
  const upW=_v1.set(0,1,0).applyQuaternion(a.quat);
  const roll=Math.atan2(a.right.dot(UP),upW.dot(UP));
  const pitchPx=pitch*R2D*3.4;
  ctx.clearRect(0,0,S,S);
  ctx.save();
  ctx.translate(cx,cy);
  ctx.beginPath();ctx.arc(0,0,168,0,7);ctx.clip();
  // --- sky/ground ---
  ctx.save();
  ctx.rotate(-roll);
  const g1=ctx.createLinearGradient(0,-200,0,0);g1.addColorStop(0,'#2f6fbe');g1.addColorStop(1,'#7fb2dd');
  const g2=ctx.createLinearGradient(0,0,0,200);g2.addColorStop(0,'#7a5a3a');g2.addColorStop(1,'#4a3524');
  ctx.fillStyle=g1;ctx.fillRect(-220,-260,440,260+pitchPx);
  ctx.fillStyle=g2;ctx.fillRect(-220,pitchPx,440,260);
  // pitch ladder
  ctx.strokeStyle='rgba(220,245,255,.75)';ctx.fillStyle='rgba(220,245,255,.9)';
  ctx.lineWidth=1.4;ctx.font='11px Orbitron, sans-serif';ctx.textAlign='left';
  for(let p=-60;p<=60;p+=10){
    if(p===0)continue;
    const y=pitchPx-p*R2D*3.4;
    if(y<-240||y>240)continue;
    const w=p%30===0?56:34;
    ctx.beginPath();ctx.moveTo(-w,y);ctx.lineTo(-14,y);ctx.moveTo(14,y);ctx.lineTo(w,y);ctx.stroke();
    if(p%30===0){ctx.fillText(Math.abs(p),w+6,y+4);ctx.fillText(Math.abs(p),-w-6,y+4);}
  }
  ctx.restore();
  // --- horizon line ---
  ctx.strokeStyle='#9ff2ea';ctx.lineWidth=2;
  ctx.beginPath();ctx.moveTo(cx-120,cy);ctx.lineTo(cx-24,cy);ctx.moveTo(cx+24,cy);ctx.lineTo(cx+120,cy);ctx.stroke();
  // --- bank scale ---
  ctx.save();ctx.translate(cx,cy);
  ctx.strokeStyle='rgba(159,242,234,.8)';
  for(let b=-60;b<=60;b+=30){
    if(b===0)continue;
    ctx.save();ctx.rotate(-b*R2D);
    ctx.beginPath();ctx.moveTo(0,-118);ctx.lineTo(0,-104);ctx.stroke();
    ctx.restore();
  }
  ctx.restore();
  // --- bank pointer (triangle top) ---
  ctx.save();ctx.translate(cx,cy);ctx.rotate(-roll);
  ctx.fillStyle='#ffd166';
  ctx.beginPath();ctx.moveTo(0,-118);ctx.lineTo(-7,-132);ctx.lineTo(7,-132);ctx.closePath();ctx.fill();
  ctx.restore();
  // --- fixed aircraft symbol ---
  ctx.strokeStyle='#eafffb';ctx.lineWidth=3;
  ctx.beginPath();
  ctx.moveTo(cx-64,cy);ctx.lineTo(cx-22,cy);ctx.lineTo(cx-12,cy+8);
  ctx.moveTo(cx+12,cy+8);ctx.lineTo(cx+22,cy);ctx.lineTo(cx+64,cy);ctx.stroke();
  ctx.fillStyle='#eafffb';
  ctx.beginPath();ctx.arc(cx-70,cy,3.4,0,7);ctx.fill();
  ctx.beginPath();ctx.arc(cx+70,cy,3.4,0,7);ctx.fill();
  // --- stall flash ---
  if(a.lastStall){
    ctx.fillStyle=`rgba(255,60,40,${0.25+0.2*Math.sin(performance.now()*0.02)})`;
    ctx.fillRect(0,0,S,S);
  }
  ctx.restore();
  // bezel ring
  ctx.save();ctx.translate(cx,cy);
  ctx.strokeStyle='rgba(159,242,234,.35)';ctx.lineWidth=1.5;
  ctx.beginPath();ctx.arc(0,0,170,0,7);ctx.stroke();
  ctx.restore();
}
function updateHUD(){
  const a=aircraft;
  const kt=a.lastV*KT, ft=Math.max(0,a.pos.y)*FT;
  $('altval').textContent=Math.round(ft).toLocaleString();
  $('spdval').textContent=Math.round(kt);
  $('aoaval').textContent=(a.lastAlpha*R2D).toFixed(1);
  $('vsival').textContent=Math.round(a.vel.y*FT*60);
  $('gval').textContent=Math.abs(a.lastG||1).toFixed(1);
  $('gpill').classList.toggle('hot',Math.abs(a.lastG||1)>6);
  $('rpm').textContent=Math.round(2000+(a.rpm||0)*6500);
  $('throttlebar').firstElementChild.style.width=(input.throttle*100).toFixed(0)+'%';
  $('fuelbar').firstElementChild.style.width=(a.fuel/a.def.fuelMax*100).toFixed(0)+'%';
  $('fuelbar').classList.toggle('low',a.fuel/a.def.fuelMax<0.2);
  $('healthbar').firstElementChild.style.width=clamp(a.health,0,100)+'%';
  $('healthbar').classList.toggle('low',a.health<35);
  $('dmg').style.opacity=clamp((100-a.health)/130,0,0.9);
  // warnings
  let w='';
  if(a.lastStall)w+='<div class="warn stall">STALL</div>';
  if(Math.abs(a.lastG||1)>6)w+='<div class="warn g">G-LIMIT</div>';
  if((a.lastAlpha*R2D)>18)w+='<div class="warn aoa">AOA</div>';
  if(a.gearDeployed&&!a.onGround)w+='<div class="warn gear">GEAR DOWN</div>';
  if(a.fuel/a.def.fuelMax<0.15)w+='<div class="warn low">LOW FUEL</div>';
  if(a.health<35)w+='<div class="warn g">HULL CRITICAL</div>';
  $('warnbox').innerHTML=w;
  // AP / angle-on
  if(mode==='df'&&targetEnemy&&!targetEnemy.dead){
    _v1.copy(targetEnemy.pos).sub(a.pos);
    const dist=_v1.length();
    _v1.normalize();
    const cos=_v1.dot(a.forward);
    const off=Math.acos(clamp(cos,-1,1));
    const rate=(targetEnemy.vel.clone().sub(a.vel)).dot(_v2.copy(a.forward).cross(_v1).normalize())/(dist||1);
    apAngleOn=rate*off;
    apLock=apLock?Math.abs(apAngleOn)<0.05:Math.abs(apAngleOn)<0.03;
    $('apd').classList.toggle('locked',apLock);
    const px=clamp(apAngleOn*900,-160,160);
    $('adsneedle').style.left=`calc(50% + ${px}px - 11px)`;
    $('adsdot').style.left=`calc(50% + ${px}px)`;
    $('adsval').textContent=apLock?'LOCKED':(apAngleOn>0.001?'L':'R');
    $('targetInfo').textContent=`TARGET · ${Math.round(dist*FT)} FT · ${Math.round(kt)} KT`;
    $('targetInfo').classList.add('on');
  }else{
    $('apd').classList.remove('locked');$('targetInfo').classList.remove('on');
    $('adsval').textContent='—';
  }
  drawMinimap();
}
function drawMinimap(){
  const c=$('mm'),ctx=c.getContext('2d'),S=c.width,a=aircraft;
  ctx.clearRect(0,0,S,S);
  ctx.save();ctx.translate(S/2,S/2);
  const scale=0.045;
  // terrain tint
  for(let i=0;i<24;i++){
    const ang=i/24*Math.PI*2;
    const gx=a.pos.x+Math.cos(ang)*1400, gz=a.pos.z+Math.sin(ang)*1400;
    const h=heightAt(gx,gz);
    ctx.fillStyle=h>120?'rgba(120,125,130,.14)':h>20?'rgba(80,110,70,.12)':'rgba(40,90,130,.10)';
    ctx.beginPath();ctx.arc(Math.cos(ang)*1400*scale,Math.sin(ang)*1400*scale,26,0,7);ctx.fill();
  }
  // runways / zones
  for(const r of runways){
    const x=(r.x-a.pos.x)*scale,z=(r.z-a.pos.z)*scale;
    ctx.save();ctx.translate(x,z);ctx.rotate(Math.atan2(r.heading.x,-r.heading.z));
    ctx.fillStyle='rgba(220,220,210,.5)';ctx.fillRect(-r.w*scale/2,-r.len*scale/2,r.w*scale,r.len*scale);ctx.restore();
  }
  for(const z of lZones){
    ctx.strokeStyle='rgba(255,209,102,.7)';ctx.lineWidth=1.5;
    ctx.beginPath();ctx.arc((z.x-a.pos.x)*scale,(z.z-a.pos.z)*scale,z.r*scale,0,7);ctx.stroke();
  }
  // enemies
  for(const e of enemies){
    if(e.dead)continue;
    const x=(e.pos.x-a.pos.x)*scale,z=(e.pos.z-a.pos.z)*scale;
    if(Math.abs(x)>S/2-6||Math.abs(z)>S/2-6)continue;
    ctx.save();ctx.translate(x,z);ctx.rotate(Math.atan2(e.forward.x,-e.forward.z));
    ctx.fillStyle='#ff5f4a';ctx.beginPath();ctx.moveTo(0,-6);ctx.lineTo(4,5);ctx.lineTo(-4,5);ctx.closePath();ctx.fill();ctx.restore();
  }
  // self
  ctx.fillStyle='#9ff2ea';ctx.beginPath();ctx.moveTo(0,-7);ctx.lineTo(5,6);ctx.lineTo(-5,6);ctx.closePath();ctx.fill();
  ctx.restore();
  // border ring
  ctx.strokeStyle='rgba(159,242,234,.25)';ctx.lineWidth=1;
  ctx.beginPath();ctx.arc(S/2,S/2,S/2-4,0,7);ctx.stroke();
}

/* ================= weapons & effects ================= */
const tracerGeo=new THREE.BufferGeometry();
const MAXT=64;
const tPos=new Float32Array(MAXT*2*3), tCol=new Float32Array(MAXT*2*3);
tracerGeo.setAttribute('position',new THREE.BufferAttribute(tPos,3));
tracerGeo.setAttribute('color',new THREE.BufferAttribute(tCol,3));
let tracerLines=null;
function initFX(){
  tracerLines=new THREE.LineSegments(tracerGeo,new THREE.LineBasicMaterial({vertexColors:true,transparent:true,opacity:0.95}));
  tracerLines.frustumCulled=false;scene.add(tracerLines);
}
function fireTracer(from,dir,speed,fromEnemy){
  if(tracers.length>=MAXT)return;
  tracers.push({pos:from.clone(),vel:dir.clone().multiplyScalar(speed),life:1.1,fromEnemy});
}
function updateTracers(dt){
  for(let i=tracers.length-1;i>=0;i--){
    const t=tracers[i];
    t.life-=dt;t.pos.addScaledVector(t.vel,dt);
    const gy=heightAt(t.pos.x,t.pos.z);
    if(t.life<=0||t.pos.y<=gy+0.3||t.pos.y>12000){
      spark(t.pos,1);
      tracers.splice(i,1);
    }
  }
  // hit test
  for(let i=tracers.length-1;i>=0;i--){
    const t=tracers[i];
    let hit=false;
    if(!t.fromEnemy&&targetEnemy&&!targetEnemy.dead){
      const d=t.pos.distanceTo(targetEnemy.pos);
      if(d<14){targetEnemy.health-=26+Math.random()*14;hit=true;shake(0.15,0.1);
        if(targetEnemy.health<=0&&!targetEnemy.dead){targetEnemy.dead=true;explode(targetEnemy.pos,false);score+=1000;kills++;killMsg('HOSTILE DESTROYED · +1000');sfx('boom');spawnEnemy();}}
    }
    if(t.fromEnemy&&aircraft&&!aircraft.dead){
      const d=t.pos.distanceTo(aircraft.pos);
      if(d<12){aircraft.health-=14+Math.random()*8;hit=true;shake(0.3,0.2);
        if(aircraft.health<=0&&!aircraft.dead){aircraft.dead=true;explode(aircraft.pos,true);toast('AIRCRAFT DESTROYED');}}
    }
    if(hit){spark(t.pos,2);tracers.splice(i,1);}
  }
  // buffer
  let n=0;
  for(const t of tracers){
    const i=n*6;
    tPos[i]=t.pos.x;tPos[i+1]=t.pos.y;tPos[i+2]=t.pos.z;
    const tail=_v1.copy(t.pos).addScaledVector(t.vel,-0.05);
    tPos[i+3]=tail.x;tPos[i+4]=tail.y;tPos[i+5]=tail.z;
    const c=t.fromEnemy?1:0.35;
    tCol[i]=c;tCol[i+1]=t.fromEnemy?0.2:0.95;tCol[i+2]=t.fromEnemy?0.15:0.9;
    tCol[i+3]=c*0.7;tCol[i+4]=t.fromEnemy?0.05:0.3;tCol[i+5]=t.fromEnemy?0.02:0.3;
    n++;
  }
  tracerGeo.attributes.position.needsUpdate=true;
  tracerGeo.attributes.color.needsUpdate=true;
  tracerGeo.setDrawRange(0,n*2);
  tracerLines.visible=n>0;
}
function spark(p,k=1){
  for(let i=0;i<6*k;i++){
    particles.push({pos:p.clone(),vel:_v1.set(Math.random()-0.5,Math.random()-0.2,Math.random()-0.5).normalize().multiplyScalar(8+Math.random()*14).clone(),life:0.35+Math.random()*0.3,size:0.5,col:1});
  }
}
function explode(p,big){
  const n=big?46:30;
  for(let i=0;i<n;i++){
    const v=_v1.set(Math.random()-0.5,Math.random()-0.5,Math.random()-0.5).normalize().multiplyScalar((big?26:16)+Math.random()*(big?30:18));
    particles.push({pos:p.clone(),vel:v,life:0.7+Math.random()*0.8,size:big?2.2:1.4,col:2});
  }
  explosions.push({pos:p.clone(),t:0,dur:big?1.6:0.9,scale:big?42:24});
  const light=new THREE.PointLight(0xffa050,big?260:120,big?700:300);
  light.position.copy(p);scene.add(light);
  explosions.push({light,fade:1});
  sfx('boom');shake(big?1.2:0.5,big?0.9:0.4);
  const f=$('flash');f.style.opacity=big?0.8:0.35;setTimeout(()=>f.style.opacity=0,90);
}
function updateFX(dt){
  for(let i=particles.length-1;i>=0;i--){
    const p=particles[i];
    p.life-=dt;p.pos.addScaledVector(p.vel,dt);
    p.vel.y-=G0*0.5*dt;p.vel.multiplyScalar(1-1.2*dt);
    if(p.life<=0)particles.splice(i,1);
  }
  for(let i=explosions.length-1;i>=0;i--){
    const e=explosions[i];
    if(e.t!==undefined){
      e.t+=dt;
      if(e.t>e.dur){
        if(e.mesh){scene.remove(e.mesh);e.mesh.material.dispose();e.mesh.geometry.dispose();}
        explosions.splice(i,1);
      }else{
        if(!e.mesh){
          const m=new THREE.Sprite(new THREE.SpriteMaterial({map:fireballTex(),transparent:true,opacity:0.95,depthWrite:false}));
          m.position.copy(e.pos);scene.add(m);e.mesh=m;
        }
        const k=e.t/e.dur;
        e.mesh.scale.setScalar(e.scale*(0.4+k*1.3));
        e.mesh.material.opacity=0.95*(1-k);
      }
    }else if(e.fade!==undefined){
      e.fade-=dt*2.2;e.light.intensity=Math.max(0,e.light.intensity*0.86);
      if(e.fade<=0){scene.remove(e.light);e.light.dispose&&e.light.dispose();explosions.splice(i,1);}
    }
  }
}
let _fireTex=null;
function fireballTex(){
  if(_fireTex)return _fireTex;
  const c=document.createElement('canvas');c.width=c.height=128;
  const x=c.getContext('2d');
  const g=x.createRadialGradient(64,64,4,64,64,64);
  g.addColorStop(0,'rgba(255,255,230,1)');g.addColorStop(0.3,'rgba(255,180,80,0.9)');g.addColorStop(0.7,'rgba(200,60,20,0.5)');g.addColorStop(1,'rgba(60,20,10,0)');
  x.fillStyle=g;x.fillRect(0,0,128,128);
  _fireTex=new THREE.CanvasTexture(c);
  return _fireTex;
}
let particlePoints=null,pPosArr=null,pColArr=null,pSizeArr=null;
const MAXP=400;
function initParticles(){
  pPosArr=new Float32Array(MAXP*3);pColArr=new Float32Array(MAXP*3);pSizeArr=new Float32Array(MAXP);
  const g=new THREE.BufferGeometry();
  g.setAttribute('position',new THREE.BufferAttribute(pPosArr,3));
  g.setAttribute('color',new THREE.BufferAttribute(pColArr,3));
  particlePoints=new THREE.Points(g,new THREE.PointsMaterial({size:2.2,vertexColors:true,transparent:true,opacity:0.95,depthWrite:false,sizeAttenuation:true}));
  particlePoints.frustumCulled=false;scene.add(particlePoints);
}
function updateParticlePoints(){
  let n=0;
  for(const p of particles){
    const i=n*3;
    pPosArr[i]=p.pos.x;pPosArr[i+1]=p.pos.y;pPosArr[i+2]=p.pos.z;
    if(p.col===1){pColArr[i]=1;pColArr[i+1]=0.95;pColArr[i+2]=0.7;}
    else{pColArr[i]=1;pColArr[i+1]=0.55-0.4*(1-p.life);pColArr[i+2]=0.15;}
    pSizeArr[n]=p.size;
    n++;if(n>=MAXP)break;
  }
  particlePoints.geometry.attributes.position.needsUpdate=true;
  particlePoints.geometry.attributes.color.needsUpdate=true;
  particlePoints.geometry.setDrawRange(0,n);
  particlePoints.visible=n>0;
}

/* ================= AI enemies ================= */
function enemyDef(){
  const base=AIRCRAFT.falcon;
  return Object.assign({},base,{id:'hostile',name:'HOSTILE',mfr:'unknown',role:'HOSTILE',
    colors:{body:0x7a4a4a,accent:0x8a2a1a,dark:0x241512,canopy:0xffb0a0}});
}
function spawnEnemy(){
  const e=makeAircraft('falcon');
  e.def=enemyDef();
  e.group.userData.def=e.def;
  // rebuild visuals with enemy colors
  const old=e.group;e.group=buildAircraft(e.def);
  e.pos.copy(aircraft.pos).add(_v1.set(Math.random()-0.5,Math.random()-0.2,Math.random()-0.5).normalize().multiplyScalar(1600+Math.random()*900));
  e.pos.y=Math.max(e.pos.y,heightAt(e.pos.x,e.pos.z)+120);
  e.group.position.copy(e.pos);
  e.vel.copy(aircraft.vel).add(_v1.set(Math.random()-0.5,0,Math.random()-0.5).multiplyScalar(60));
  e.health=100;e.rpm=0.8;
  e.state='engage';e.think=0;e.fireCd=1+Math.random()*2;
  scene.add(e.group);
  enemies.push(e);
  return e;
}
function aiThink(e,dt){
  e.think-=dt;
  const a=aircraft;
  if(e.think>0)return;
  e.think=0.12;
  e.forward.set(0,0,1).applyQuaternion(e.quat);
  if(a.dead||e.health<25){
    e.state='disengage';
  }else if(e.state==='disengage'&&e.health>60){
    e.state='engage';
  }
  let desired;
  if(e.state==='disengage'){
    desired=_v2.copy(e.pos).add(_v1.copy(a.pos).sub(e.pos).normalize().multiplyScalar(-1)).add(_v3.set(0,1,0).multiplyScalar(80));
    _v1.copy(desired).sub(e.pos).normalize();
  }else{
    const dist=e.pos.distanceTo(a.pos);
    const lead=_v3.copy(a.vel).clone().multiplyScalar(clamp(dist/700,0,1.4));
    _v1.copy(a.pos).add(lead).sub(e.pos).normalize();
    // keep some speed: avoid pure pursuit at low energy
    if(e.lastV<200&&dist<500){e.state='disengage';}
  }
  // control from local frame error
  _q1.copy(e.quat).invert();
  _v2.copy(_v1).applyQuaternion(_q1);
  const pitchC=clamp(Math.atan2(_v2.y,Math.max(_v2.z,0.05))*2.2,-1,1);
  const rollC=clamp(-Math.atan2(_v2.x,Math.max(_v2.z,0.05))*2.6,-1,1);
  e.input={pitch:pitchC,roll:rollC,yaw:0,throttle:clamp(0.55+(e.lastV? clamp((260-e.lastV)/300,0,0.45):0.3),0.3,1),boost:e.lastV<220&&e.fuel>50};
  // fire
  e.fireCd-=dt;
  const toT=_v3.copy(a.pos).sub(e.pos);
  const d=toT.length();toT.normalize();
  const cos=toT.dot(e.forward);
  if(e.state==='engage'&&e.fireCd<=0&&d<1400&&d>180&&cos>0.995&&a.lastV>100){
    // lead shot
    const tt=d/820;
    const aim=_v1.copy(a.pos).addScaledVector(a.vel,tt).sub(e.pos).normalize();
    if(aim.dot(e.forward)>0.985){
      for(let i=0;i<4;i++)fireTracer(_v2.copy(e.pos).addScaledVector(e.forward,10).add(_v4.set((Math.random()-0.5)*1.2,Math.random()*0.6,0)),aim,820,true);
      e.fireCd=0.55+Math.random()*0.5;
    }
  }
}
function updateEnemies(dt){
  for(let i=enemies.length-1;i>=0;i--){
    const e=enemies[i];
    if(e.dead){
      e.deadT=(e.deadT||0)+dt;
      e.group.position.y-=dt*8; // sink
      if(e.deadT>3){scene.remove(e.group);e.group.traverse(o=>{if(o.isMesh){o.geometry.dispose();o.material.dispose();}});enemies.splice(i,1);}
      continue;
    }
    aiThink(e,dt);
    if(e.input.boost&&e.boostT<=0){e.boostT=2;}
    physStep(e,dt,e.input,false);
    groundStep(e,dt);
    e.group.position.copy(e.pos);
    e.group.quaternion.copy(e.quat);
    // pick target
    if(!targetEnemy&&enemies.length){
      let best=null,bd=1e9;
      for(const en of enemies){if(en.dead)continue;const d=en.pos.distanceTo(aircraft.pos);if(d<bd){bd=d;best=en;}}
      targetEnemy=best;
    }
  }
  if(targetEnemy&&(targetEnemy.dead||!enemies.includes(targetEnemy))){targetEnemy=null;}
}

/* ================= input ================= */
const keys=new Set();
function initInput(){
  addEventListener('keydown',e=>{
    if(e.code==='Space')e.preventDefault();
    keys.add(e.code);
    if(!running)return;
    if(e.code==='KeyG'){if(aircraft){aircraft.gearDeployed=!aircraft.gearDeployed;gearSfx();}}
    if(e.code==='KeyB'){aircraft.brake=!aircraft.brake;}
    if(e.code==='KeyF'){if(aircraft){aircraft.flapOn=!aircraft.flapOn;}}
    if(e.code==='KeyR'){resetRun();}
    if(e.code==='KeyM'){backToMenu();}
  });
  addEventListener('keyup',e=>keys.delete(e.code));
  addEventListener('mousemove',e=>{
    if(!locked||!running)return;
    const s=0.0022;
    input.pitch+=-e.movementY*s;
    input.roll+=e.movementX*s;
    input.pitch=clamp(input.pitch,-1,1);input.roll=clamp(input.roll,-1,1);
  });
  document.addEventListener('pointerlockchange',()=>{
    locked=document.pointerLockElement===$('gl');
    if(!locked&&running&&!menuOpen){paused=true;$('pauseOverlay').classList.add('on');}
  });
  $('gl').addEventListener('click',()=>{
    if(running&&!menuOpen&&!locked){$('gl').requestPointerLock();paused=false;$('pauseOverlay').classList.remove('on');}
  });
  $('pauseOverlay').addEventListener('click',()=>{
    if(paused){$('gl').requestPointerLock();paused=false;$('pauseOverlay').classList.remove('on');}
  });
}
function readInput(dt){
  if(!running)return;
  const a=aircraft;
  input.pitch*=Math.exp(-6*dt);input.roll*=Math.exp(-6*dt);
  if(keys.has('KeyQ')||keys.has('ArrowLeft'))input.yaw=1;
  else if(keys.has('KeyE')||keys.has('ArrowRight'))input.yaw=-1;
  else input.yaw*=Math.exp(-8*dt);
  if(keys.has('KeyW')||keys.has('ArrowUp'))input.throttle=clamp(input.throttle+0.55*dt,0,1);
  if(keys.has('KeyS')||keys.has('ArrowDown'))input.throttle=clamp(input.throttle-0.55*dt,0,1);
  throttle=input.throttle;
  a.boostT>0?0:(boost=keys.has('ShiftLeft')&&mode==='df');
  if(boost&&a.boostT<=0&&keys.has('ShiftLeft'))a.boostT=2.6;
  input.fire=keys.has('Space')&&mode==='df';
  if(a.gearT!==undefined){a.gearT=lerp(a.gearT||0,a.gearDeployed?1:0,1-Math.exp(-4*dt));}
  a.flapT=lerp(a.flapT||0,a.flapOn?1:0,1-Math.exp(-3*dt));
  setFlapVisual(a,a.flapT*35);
  // gear visual
  if(a.group.userData.gearParts){
    const t=a.gearT||0;
    a.group.userData.gearParts.forEach((gr,i)=>{gr.position.y=(i<2?-0.55:-0.55)+t*0.95;});
  }
  // burner glow
  const b=a.group.userData.burner;
  if(b){const k=clamp((a.boostT>0?0.9:0)+a.rpm*0.25,0,1);b.material.opacity=k*0.85;}
  if(a.group.userData.prop){a.group.userData.prop.rotation.z+=dt*(6+a.rpm*40);}
}
function resetRun(){
  if(!aircraft)return;
  resetPlaneToSpawn();
  aircraft.health=100;aircraft.fuel=aircraft.def.fuelMax;
  aircraft.gearDeployed=mode==='tl';aircraft.gearT=aircraft.gearDeployed?1:0;
  if(mode==='df'){for(const e of enemies){if(!e.dead)spawnEnemyAt(e);} }
  toast('RESET');
}
function spawnEnemyAt(e){
  e.pos.copy(aircraft.pos).add(_v1.set(Math.random()-0.5,0.3,Math.random()-0.5).normalize().multiplyScalar(1800));
  e.pos.y=Math.max(e.pos.y,heightAt(e.pos.x,e.pos.z)+150);
  e.health=100;e.dead=false;e.state='engage';
  e.group.position.copy(e.pos);
}

/* ================= menu ================= */
const menuPreviews=[];
function buildMenu(){
  const wrap=$('cards');
  for(const id of AIR_LIST){
    const def=AIRCRAFT[id];
    const card=document.createElement('div');
    card.className='card'+(id===selAircraft?' sel':'');
    card.dataset.id=id;
    let stats='';
    for(const [k,v,u] of def.stats)stats+=`<div class="stat"><span>${k}</span><b>${v}<span class="u">${u||''}</span></b></div>`;
    card.innerHTML=`<div class="selTag">▸ SELECTED</div><div class="role">${def.role}</div><h3>${def.name}</h3><div class="mfr">${def.mfr}</div><canvas></canvas><div id="statsGrid">${stats}</div><div class="desc">${def.desc}</div>`;
    card.addEventListener('click',()=>{
      selAircraft=id;
      for(const c of wrap.children)c.classList.toggle('sel',c.dataset.id===id);
    });
    wrap.appendChild(card);
    const cv=card.querySelector('canvas');
    const pr=new THREE.WebGLRenderer({canvas:cv,alpha:true,antialias:true});
    pr.setPixelRatio(Math.min(devicePixelRatio,1.5));
    const ms=new THREE.Scene();
    const mc=new THREE.PerspectiveCamera(38,2,0.1,100);
    mc.position.set(0,2.6,13.5);mc.lookAt(0,0.4,0);
    const key=new THREE.DirectionalLight(0xffffff,2.2);key.position.set(4,6,5);ms.add(key);
    ms.add(new THREE.HemisphereLight(0xbfd9ee,0x223040,1.1));
    const m=buildAircraft(def,true);
    m.position.set(0,0.4,0);m.rotation.y=-0.6;
    ms.add(m);
    menuPreviews.push({pr,ms,mc,m,card});
  }
  for(const el of document.querySelectorAll('.mode')){
    el.addEventListener('click',()=>{
      selMode=el.dataset.mode;
      document.querySelectorAll('.mode').forEach(x=>x.classList.toggle('sel',x===el));
    });
  }
  $('launch').addEventListener('click',startGame);
  $('statAircraft').textContent=AIR_LIST.length;
  $('statModes').textContent=2;
}
function animateMenuPreviews(){
  if(!menuOpen)return;
  menuAngle+=0.008;
  for(const p of menuPreviews){
    const w=p.card.clientWidth||300,h=118;
    if(p.pr.getSize(new THREE.Vector2()).x!==w){p.pr.setSize(w,h,false);p.mc.aspect=w/h;p.mc.updateProjectionMatrix();}
    p.m.rotation.y=-0.6+Math.sin(menuAngle)*0.5;
    p.pr.render(p.ms,p.mc);
  }
  requestAnimationFrame(animateMenuPreviews);
}

/* ================= game flow ================= */
function toast(msg){
  const t=$('toast');t.textContent=msg;t.classList.add('on');
  clearTimeout(t._to);t._to=setTimeout(()=>t.classList.remove('on'),2600);
}
function killMsg(msg){
  const d=document.createElement('div');d.className='kill';d.textContent=msg;
  $('killfeed').appendChild(d);
  setTimeout(()=>d.remove(),4200);
}
let climbDone=false, missionDone=false;
function missionComplete(title){
  if(missionDone)return;
  missionDone=true;
  score+=2000;
  toast(title+' · +2000 · FREE FLIGHT — PRESS M FOR MENU');
}
function startGame(){
  menuOpen=false;paused=false;missionDone=false;climbDone=false;
  $('menu').classList.add('hidden');
  $('pauseOverlay').classList.remove('on');
  initAudio();
  if(AC&&AC.state==='suspended')AC.resume();
  mode=selMode;
  setupWorld();
  // clear old
  if(aircraft){scene.remove(aircraft.group);}
  for(const e of enemies){scene.remove(e.group);}
  enemies.length=0;tracers.length=0;particles.length=0;explosions.length=0;
  targetEnemy=null;score=0;kills=0;
  aircraft=makeAircraft(selAircraft);
  scene.add(aircraft.group);
  resetPlaneToSpawn();
  aircraft.rpm=0.35;
  throttle=input.throttle=0.35;
  if(mode==='df'){for(let i=0;i<3;i++)spawnEnemy();}
  $('hud').classList.add('on');
  $('modeTag').textContent=mode==='tl'?'SCENARIO A · TAKEOFF/LANDING':'SCENARIO B · DOGFIGHT';
  $('aircraftTag').textContent=aircraft.def.name;
  $('controlsHint').innerHTML=mode==='tl'
    ?'<b>W/S</b> THROTTLE · <b>MOUSE</b> PITCH/ROLL · <b>F</b> FLAP · <b>G</b> GEAR · <b>B</b> BRAKE — ROTATE AT '+Math.round(aircraft.def.vStall*1.15)+' KT'
    :'<b>MOUSE</b> AIM · <b>SPACE</b> FIRE · <b>SHIFT</b> BOOST · <b>W/S</b> THROTTLE · WATCH YOUR G';
  running=true;
  $('gl').requestPointerLock();
}
function backToMenu(){
  running=false;menuOpen=true;paused=false;
  document.exitPointerLock&&document.exitPointerLock();
  $('menu').classList.remove('hidden');
  $('hud').classList.remove('on');
  $('pauseOverlay').classList.remove('on');
  if(aircraft){scene.remove(aircraft.group);aircraft=null;}
  for(const e of enemies){scene.remove(e.group);}
  enemies.length=0;
  animateMenuPreviews();
}

/* ================= main loop ================= */
function cameraFollow(dt){
  const a=aircraft;
  const back=_v1.copy(a.forward).multiplyScalar(-11.5);
  const up=_v2.copy(a.upW).multiplyScalar(3.4);
  const target=_v3.copy(a.pos).add(back).add(up);
  // keep camera above terrain
  const gy=heightAt(target.x,target.z);
  if(target.y<gy+2)target.y=gy+2;
  camera.position.lerp(target,1-Math.exp(-5.5*dt));
  const tq=_q1.copy(a.quat);
  camera.quaternion.slerp(tq,1-Math.exp(-7*dt));
  // shake
  if(shakeT>0){
    shakeT-=dt;
    const s=shakeMag*shakeT;
    camera.position.x+=(Math.random()-0.5)*s;camera.position.y+=(Math.random()-0.5)*s;camera.position.z+=(Math.random()-0.5)*s;
    if(shakeT<=0)shakeMag=0;
  }
  const fovT=62+clamp((a.lastV||0)/40,0,14)*(a.boostT>0?1.6:1);
  camera.fov=lerp(camera.fov,fovT,1-Math.exp(-3*dt));
  camera.updateProjectionMatrix();
}
function step(dt){
  const a=aircraft;
  // firing
  if(mode==='df'&&input.fire&&!a.dead){
    fireCd-=dt;
    if(fireCd<=0){
      fireCd=0.14;
      const dir=_v1.copy(a.forward);
      for(let i=0;i<3;i++){
        const off=_v2.set((i-1)*0.9+(Math.random()-0.5)*0.3,(Math.random()-0.5)*0.3,0).applyQuaternion(a.quat);
        fireTracer(_v3.copy(a.pos).addScaledVector(a.forward,10).add(off),dir,820,false);
      }
      sfx('shot');
      shake(0.06,0.08);
    }
  }
  physStep(a,dt,input,true);
  groundStep(a,dt);
  a.group.position.copy(a.pos);
  a.group.quaternion.copy(a.quat);
  updateEnemies(dt);
  updateTracers(dt);
  updateFX(dt);
  updateParticlePoints();
  // mission logic
  if(mode==='tl'&&!a.dead){
    const spawnY=heightAt(0,-2600);
    if(!climbDone&&a.pos.y>spawnY+122){climbDone=true;score+=750;toast('CLIMB-OUT COMPLETE · +750');sfx('goodland');}
  }
  if(mode==='df'&&kills>=3&&!missionDone){missionComplete('ENGAGEMENT CLEAR');}
}
let frameId=0;
function frame(now){
  frameId=requestAnimationFrame(frame);
  let dt=(now-lastT)/1000;lastT=now;
  if(dt>0.1)dt=0.1;
  if(running&&!paused&&aircraft){
    readInput(dt);
    // fixed substeps for stable integration
    let rem=dt;const h=1/120;
    while(rem>1e-4){const s=Math.min(h,rem);step(s);rem-=s;}
    updateTerrain(now/1000);
    cameraFollow(dt);
    updateSkyFollow(dt);
    updateAudio(dt);
    hudAcc+=dt;
    drawADI(dt);
    if(hudAcc>0.1){updateHUD();hudAcc=0;}
  }else if(menuOpen){
    updateSkyFollow(dt);
    // idle slow orbit of the world as menu backdrop
    camera.position.lerp(_v1.set(600,420,-2200),1-Math.exp(-0.5*dt));
    camera.lookAt(0,120,-2600);
  }
  renderer.render(scene,camera);
}
/* ================= boot ================= */
initRenderer();
makeSky();makeLights();makeWater();makeClouds();
initTerrain();initFX();initParticles();initHUD();initInput();buildMenu();
// pre-build terrain around TL spawn so the backdrop is alive
flattenSpots=[{x:0,z:-2600,r:420,h:9}];
for(let dx=-2;dx<=2;dx++)for(let dz=-2;dz<=2;dz++)buildChunk(dx,dz);
camera.position.set(600,420,-2200);
camera.lookAt(0,120,-2600);
animateMenuPreviews();
lastT=performance.now();
frameId=requestAnimationFrame(frame);
window.__fx={get aircraft(){return aircraft},camera,terrain,runways,heightAt,flattenSpots,scene,physStep,groundStep,mode:()=>mode,score:()=>score,enemies};
</script>
</html>