← Flight Simulator results

Flight Simulator

ox alpha · 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 38,751 bytes · SHA-256 5a87893efb5b
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apex Aero — Flight Simulator</title>
<style>
  * { margin:0; padding:0; box-sizing:border-box; }
  html,body { width:100%; height:100%; overflow:hidden; background:#000; font-family:'Segoe UI',system-ui,sans-serif; }
  #app { position:fixed; inset:0; }
  canvas { display:block; }

  /* ---------- MENU ---------- */
  #menu { position:fixed; inset:0; z-index:50; display:flex; flex-direction:column; align-items:center;
    justify-content:center; gap:18px; background:radial-gradient(ellipse at 30% 20%, #16324f 0%, #060d16 70%);
    color:#cfe8ff; transition:opacity .5s; }
  #menu.hidden { opacity:0; pointer-events:none; }
  #menu h1 { font-size:52px; letter-spacing:10px; font-weight:800; color:#eaf6ff;
    text-shadow:0 0 30px rgba(90,180,255,.55), 0 2px 0 #123; }
  #menu h1 span { color:#57c7ff; }
  #menu .sub { font-size:13px; letter-spacing:4px; color:#7fa8c9; margin-top:-14px; }
  .section-label { margin-top:10px; font-size:11px; letter-spacing:3px; color:#6fa3cc; }
  #fleet { display:flex; gap:16px; }
  .card { width:250px; background:rgba(15,35,55,.75); border:1px solid rgba(90,170,255,.25);
    border-radius:10px; padding:14px 16px; cursor:pointer; transition:.18s; backdrop-filter:blur(4px); }
  .card:hover { border-color:#57c7ff; transform:translateY(-3px); box-shadow:0 8px 28px rgba(40,140,255,.25); }
  .card.sel { border-color:#ffd76a; box-shadow:0 0 22px rgba(255,210,90,.28); background:rgba(45,45,25,.7); }
  .card h3 { font-size:17px; color:#eaf6ff; margin-bottom:2px; }
  .card .role { font-size:11px; letter-spacing:2px; color:#57c7ff; margin-bottom:8px; }
  .bar { height:5px; background:#12283d; border-radius:3px; overflow:hidden; margin:3px 0 7px; }
  .bar i { display:block; height:100%; background:linear-gradient(90deg,#2e9bd6,#7fe3ff); border-radius:3px; }
  .stat { display:flex; justify-content:space-between; font-size:11px; color:#9fc4de; }
  .stat b { color:#dff2ff; font-weight:600; }
  #scenarios { display:flex; gap:14px; }
  .scn { padding:10px 22px; border:1px solid rgba(90,170,255,.3); border-radius:8px; cursor:pointer;
    font-size:13px; letter-spacing:1px; color:#bfe2ff; background:rgba(15,35,55,.6); transition:.15s; }
  .scn:hover { border-color:#57c7ff; }
  .scn.sel { background:#1d4d74; border-color:#57c7ff; color:#fff; }
  .scn small { display:block; font-size:10px; color:#7fa8c9; margin-top:3px; }
  #launchBtn { margin-top:6px; padding:13px 64px; font-size:17px; letter-spacing:5px; font-weight:700;
    color:#04121f; background:linear-gradient(180deg,#8fdcff,#37a8e8); border:none; border-radius:8px;
    cursor:pointer; transition:.15s; box-shadow:0 4px 24px rgba(60,170,255,.4); }
  #launchBtn:hover { filter:brightness(1.15); transform:scale(1.03); }
  #controlsHint { font-size:11px; color:#6f93ad; text-align:center; line-height:1.7; }

  /* ---------- HUD ---------- */
  #hud { position:fixed; inset:0; z-index:10; pointer-events:none; display:none; color:#bfffe0;
    font-family:'Consolas','Courier New',monospace; }
  #hud.on { display:block; }
  .hudtxt { position:absolute; font-size:14px; text-shadow:0 0 6px rgba(0,255,140,.6), 1px 1px 2px #000; white-space:pre; }
  #hudSpeed { left:6%; top:46%; text-align:right; }
  #hudAlt   { right:6%; top:42%; }
  #hudHead  { left:50%; top:6%; transform:translateX(-50%); }
  #hudThr   { left:6%; bottom:12%; }
  #hudState { left:50%; bottom:8%; transform:translateX(-50%); text-align:center; font-size:13px; color:#ffe9a8; }
  #hudMsg   { left:50%; top:34%; transform:translateX(-50%); font-size:26px; letter-spacing:4px;
    color:#ffd76a; text-align:center; text-shadow:0 0 18px rgba(255,200,80,.8); opacity:0; transition:opacity .4s; }
  #hudHp    { left:6%; top:6%; }
  #hudTarget{ right:6%; bottom:12%; }
  /* artificial horizon */
  #horizonWrap { position:absolute; left:50%; top:50%; width:230px; height:230px; transform:translate(-50%,-50%); }
  #horizonClip { position:absolute; inset:0; border-radius:50%; overflow:hidden; opacity:.85; }
  #horizon { position:absolute; left:-50%; top:-50%; width:200%; height:200%; }
  .sky { position:absolute; inset:0; background:linear-gradient(#1c6ec8,#79c2ef); }
  .gnd { position:absolute; left:0; right:0; top:50%; bottom:-10%; background:linear-gradient(#8a6234,#5c3f1e); }
  .hline { position:absolute; left:0; right:0; top:50%; height:3px; margin-top:-1px; background:#fff; }
  #pitchLadder { position:absolute; left:50%; top:50%; }
  .pl { position:absolute; left:-45px; width:90px; height:0; border-top:1.5px solid rgba(255,255,255,.9);
    text-align:center; font-size:10px; color:#fff; }
  .pl span{ position:relative; top:-14px; background:rgba(0,0,0,.25); padding:0 3px; }
  #reticle { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:36px; height:36px;
    border:2px solid rgba(120,255,180,.9); border-radius:50%; box-shadow:0 0 8px rgba(0,255,120,.5); }
  #reticle::before,#reticle::after { content:''; position:absolute; background:rgba(120,255,180,.9); }
  #reticle::before { left:50%; top:-12px; width:2px; height:8px; margin-left:-1px; }
  #reticle::after { left:-12px; top:50%; height:2px; width:8px; margin-top:-1px; }
  #vignette { position:fixed; inset:0; pointer-events:none; z-index:9;
    box-shadow:inset 0 0 220px rgba(0,0,0,.55); }
  #redflash { position:fixed; inset:0; pointer-events:none; z-index:11; background:radial-gradient(circle,transparent 40%,rgba(255,30,30,.55));
    opacity:0; transition:opacity .2s; }
</style>
</head>
<body>
<div id="app"></div>

<div id="hud">
  <div id="horizonWrap"><div id="horizonClip"><div id="horizon">
    <div class="sky"></div><div class="gnd"></div><div class="hline"></div></div></div></div>
  <div id="pitchLadder"></div>
  <div id="reticle"></div>
  <div class="hudtxt" id="hudSpeed"></div>
  <div class="hudtxt" id="hudAlt"></div>
  <div class="hudtxt" id="hudHead"></div>
  <div class="hudtxt" id="hudThr"></div>
  <div class="hudtxt" id="hudHp"></div>
  <div class="hudtxt" id="hudState"></div>
  <div class="hudtxt" id="hudTarget"></div>
  <div id="hudMsg"></div>
</div>
<div id="vignette"></div>
<div id="redflash"></div>

<div id="menu">
  <h1>APEX <span>AERO</span></h1>
  <div class="sub">HIGH-FIDELITY FLIGHT SIMULATOR</div>
  <div class="section-label">SELECT AIRCRAFT</div>
  <div id="fleet"></div>
  <div class="section-label">SELECT MISSION</div>
  <div id="scenarios">
    <div class="scn sel" data-s="takeoff">TAKEOFF<small>Climb to 1,000 ft</small></div>
    <div class="scn" data-s="landing">LANDING<small>Grease it on the runway</small></div>
    <div class="scn" data-s="dogfight">DOGFIGHT<small>3 hostiles inbound</small></div>
  </div>
  <button id="launchBtn">LAUNCH</button>
  <div id="controlsHint">
    W/S throttle &nbsp;·&nbsp; ↑↓ pitch &nbsp;·&nbsp; ←→ roll &nbsp;·&nbsp; A/D yaw &nbsp;·&nbsp; SPACE guns (dogfight)<br>
    C camera &nbsp;·&nbsp; ESC pause / return to hangar &nbsp;·&nbsp; G gear (visual)
  </div>
</div>

<script type="importmap">
{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } }
</script>
<script type="module">
import * as THREE from 'three';

/* ============================================================
   APEX AERO — custom Newtonian flight dynamics on Three.js
   ============================================================ */

const G = 9.81, RHO0 = 1.225;
const clamp = (v,a,b)=>Math.min(b,Math.max(a,v));
const lerp = (a,b,t)=>a+(b-a)*t;

/* ---------- procedural noise (value noise + fBm) ---------- */
function hash2(x,y){ let n = Math.sin(x*127.1+y*311.7)*43758.5453; return n-Math.floor(n); }
function vnoise(x,y){
  const xi=Math.floor(x), yi=Math.floor(y), xf=x-xi, yf=y-yi;
  const u=xf*xf*(3-2*xf), v=yf*yf*(3-2*yf);
  return lerp(lerp(hash2(xi,yi),hash2(xi+1,yi),u), lerp(hash2(xi,yi+1),hash2(xi+1,yi+1),u), v);
}
function fbm(x,y,oct=5){
  let s=0,a=.5,f=1;
  for(let i=0;i<oct;i++){ s+=a*vnoise(x*f,y*f); f*=2.03; a*=.5; }
  return s;
}
const WORLD = 24000;          // terrain square size (m)
function terrainH(x,z){
  const nx=x/WORLD, nz=z/WORLD;
  // continent mask: coastlines (ocean toward +x edge)
  let cont = clamp((nx+0.5)*1.15-0.25, 0, 1);
  let h = (fbm(nx*3+7.3, nz*3+2.1)-0.32)*900*cont + (fbm(nx*9,nz*9,4)-0.5)*160;
  // mountain ridge band
  const ridge = Math.pow(fbm(nx*2.2+31.7, nz*2.2+11.9), 2.2)*1500*cont;
  h += ridge;
  if(h<0) h*=0.12;                       // shallow sea floor
  // flatten runway corridor (along Z at x≈0)
  const dx=Math.abs(x), dzMax=900;
  const mx = 1-clamp((dx-70)/230,0,1);
  const mz = clamp((1300-Math.abs(z))/150,0,1);
  const mask = mx*mz;
  let out = lerp(h, 8, mask);
  // approach corridor: gentle ramp south of the runway
  if(z<-1150){
    const m=Math.pow(clamp((-z-1150)/4200,0,1),1.6);
    out = lerp(8, out, m);
  }
  return out;
}
function onRunway(x,z){ return Math.abs(x)<60 && Math.abs(z)<1150 && terrainH(x,z)<9.5; }

/* ---------- aircraft fleet ---------- */
const FLEET = [
  { id:'skylark', name:'SW-1 Skylark', role:'LIGHT TRAINER', color:0xf2f4f8, accent:0x2e86d6,
    mass:900, maxThrust:7200, wingArea:16.2, cl0:0.28, clSlope:5.2, stallAlpha:0.24, cd0:0.032,
    oswald:0.78, span:9.5, vRef:48, spool:1.6,
    pitchRate:0.45, rollRate:1.4, yawRate:0.3, stab:2.2, hp:60,
    stats:{SPD:.35,THR:.25,AGI:.85,ARM:.3}, desc:'Forgiving, slow, loves the pattern.' },
  { id:'vortex', name:'FX-9 Vortex', role:'AIR SUPERIORITY JET', color:0x8d99a6, accent:0xd23b3b,
    mass:11500, maxThrust:148000, wingArea:37.5, cl0:0.06, clSlope:3.9, stallAlpha:0.27, cd0:0.021,
    oswald:0.82, span:11.4, vRef:95, spool:2.6,
    pitchRate:0.7, rollRate:2.6, yawRate:0.35, stab:1.5, hp:100,
    stats:{SPD:1,THR:1,AGI:.9,ARM:.65}, desc:'Supercruise brute. Mind the Gs.' },
  { id:'titan', name:'AC-7 Titan', role:'HEAVY ATTACK', color:0x4a5d43, accent:0xe0c341,
    mass:26000, maxThrust:190000, wingArea:62, cl0:0.22, clSlope:4.6, stallAlpha:0.22, cd0:0.028,
    oswald:0.75, span:12.2, vRef:78, spool:3.4,
    pitchRate:0.35, rollRate:1.1, yawRate:0.25, stab:2.6, hp:200,
    stats:{SPD:.6,THR:.8,AGI:.4,ARM:1}, desc:'A flying fortress. Turns like a continent.' },
];

/* ---------- three.js scene ---------- */
const app = document.getElementById('app');
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
app.appendChild(renderer.domElement);

const scene = new THREE.Scene();
scene.background = new THREE.Color(0x9fc7e8);
scene.fog = new THREE.Fog(0xa8cce8, 3000, 16000);

const camera = new THREE.PerspectiveCamera(62, innerWidth/innerHeight, 0.5, 42000);

const hemi = new THREE.HemisphereLight(0xbdd8ff, 0x54492e, 0.85); scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff2dc, 2.2);
sun.position.set(-4000,5000,2500); scene.add(sun);
// sun glare sprite
{
  const c=document.createElement('canvas'); c.width=c.height=128;
  const g=c.getContext('2d'), gr=g.createRadialGradient(64,64,4,64,64,64);
  gr.addColorStop(0,'rgba(255,250,230,1)'); gr.addColorStop(.25,'rgba(255,235,190,.55)'); gr.addColorStop(1,'rgba(255,235,190,0)');
  g.fillStyle=gr; g.fillRect(0,0,128,128);
  const sm=new THREE.SpriteMaterial({map:new THREE.CanvasTexture(c), transparent:true, depthWrite:false});
  const sp=new THREE.Sprite(sm); sp.scale.setScalar(3500); sp.position.copy(sun.position).normalize().multiplyScalar(30000);
  scene.add(sp);
}

/* ---------- terrain mesh ---------- */
let terrain=null, water=null;
function buildTerrain(){
  if(terrain){ scene.remove(terrain); terrain.geometry.dispose(); }
  const seg=260, geo=new THREE.PlaneGeometry(WORLD,WORLD,seg,seg);
  geo.rotateX(-Math.PI/2);
  const pos=geo.attributes.position, col=new Float32Array(pos.count*3);
  const c=new THREE.Color();
  for(let i=0;i<pos.count;i++){
    const x=pos.getX(i), z=pos.getZ(i);
    const h=terrainH(x,z);
    pos.setY(i,h);
    const sl = Math.abs(terrainH(x+40,z)-h)+Math.abs(terrainH(x,z+40)-h);
    if(h<2.5)      c.setHex(0xc9bb84);                 // beach
    else if(h<180) c.setHex(0x4e7c3a);                 // grass
    else if(h<650) c.setHex(0x6b6f45).lerp(new THREE.Color(0x7a7a68),(h-180)/470);
    else if(h<1050)c.setHex(0x777168);                 // rock
    else           c.setHex(0xe8edf2);                 // snow
    if(sl>55&&h>60) c.lerp(new THREE.Color(0x5d564b), clamp((sl-55)/90,0,.8)); // cliffs
    col[i*3]=c.r; col[i*3+1]=c.g; col[i*3+2]=c.b;
  }
  geo.setAttribute('color', new THREE.BufferAttribute(col,3));
  geo.computeVertexNormals();
  const mat=new THREE.MeshStandardMaterial({vertexColors:true, roughness:.94, metalness:0, flatShading:false});
  terrain=new THREE.Mesh(geo,mat); scene.add(terrain);
  if(!water){
    water=new THREE.Mesh(new THREE.PlaneGeometry(WORLD*1.4,WORLD*1.4),
      new THREE.MeshStandardMaterial({color:0x1d5f8f, roughness:.15, metalness:.35, transparent:true, opacity:.93}));
    water.rotation.x=-Math.PI/2; water.position.y=2.2; scene.add(water);
  }
}
buildTerrain();

/* ---------- runway ---------- */
{
  const rw=new THREE.Mesh(new THREE.BoxGeometry(52,1.2,1760),
    new THREE.MeshStandardMaterial({color:0x33363b,roughness:.9}));
  rw.position.set(0,8.6,0); scene.add(rw);
  const mkStripe=(z)=>{ const s=new THREE.Mesh(new THREE.PlaneGeometry(2.4,26),
      new THREE.MeshBasicMaterial({color:0xf0f0f0})); s.rotation.x=-Math.PI/2; s.position.set(0,9.25,z); scene.add(s); };
  for(let z=-800;z<=800;z+=100) mkStripe(z);
  const tower=new THREE.Mesh(new THREE.CylinderGeometry(4,5,26,8),
    new THREE.MeshStandardMaterial({color:0x9aa3ab,roughness:.8}));
  tower.position.set(110,21,-300); scene.add(tower);
  const ttop=new THREE.Mesh(new THREE.BoxGeometry(14,6,14),
    new THREE.MeshStandardMaterial({color:0x3b4a58,roughness:.4,metalness:.4}));
  ttop.position.set(110,37,-300); scene.add(ttop);
}

/* ---------- aircraft 3D models (procedural, distinct silhouettes) ---------- */
function mat(c,r=.55,m=.35){ return new THREE.MeshStandardMaterial({color:c,roughness:r,metalness:m}); }
function buildModel(def, hostile=false){
  const g=new THREE.Group();
  const body=mat(hostile?0x8a2f2f:def.color), acc=mat(hostile?0x551111:def.accent,.4,.5),
        dark=mat(0x22262b,.5,.6), glass=new THREE.MeshStandardMaterial({color:0x9fd8ff,roughness:.08,metalness:.1,transparent:true,opacity:.85});
  const add=(geo,m,x,y,z,rx=0,ry=0,rz=0)=>{ const me=new THREE.Mesh(geo,m); me.position.set(x,y,z);
    me.rotation.set(rx,ry,rz); g.add(me); return me; };
  if(def.id==='skylark'){
    add(new THREE.CapsuleGeometry(.75,4.4,6,12),body,0,0,0,Math.PI/2);            // fuselage
    add(new THREE.BoxGeometry(11,.22,1.7),acc,0,.35,.4);                          // wing (high)
    add(new THREE.BoxGeometry(3.6,.16,1.1),acc,0,1.05,-2.6);                      // tail
    add(new THREE.BoxGeometry(.16,1.5,1.1),acc,0,.5,-2.7);
    add(new THREE.SphereGeometry(.62,12,10),glass,0,.55,.9);
    add(new THREE.CylinderGeometry(.16,.22,1.1,8),dark,0,0,2.7,Math.PI/2);        // spinner
    for(const s of[-1,1]) add(new THREE.CylinderGeometry(.09,.09,.7,6),dark,s*1.4,-1.05,.4); // gear legs
  } else if(def.id==='vortex'){
    add(new THREE.ConeGeometry(1.05,7.6,10),body,0,0,-.4,-Math.PI/2);             // nose cone fwd
    add(new THREE.CylinderGeometry(1.05,.85,5.2,10),body,0,0,2.6,Math.PI/2);
    add(new THREE.BoxGeometry(8.6,.16,3.4),acc,0,.1,1.2);                         // delta wing
    for(const s of[-1,1]){ add(new THREE.BoxGeometry(.14,2.3,2.8),acc,s*1.5,.9,-2.4,0,0,s*.35);  // twin tails
      add(new THREE.BoxGeometry(3.2,.14,1.9),acc,s*3.4,.1,-2.2); }                // stabs
    add(new THREE.SphereGeometry(.72,12,10),glass,0,.5,-1.2);
    add(new THREE.CylinderGeometry(.42,.55,1.4,10),dark,0,0,5.4,Math.PI/2);       // nozzle
    for(const s of[-1,1]) add(new THREE.BoxGeometry(1.5,.5,3),dark,s*1.1,-.55,1.6);// intakes
  } else {
    add(new THREE.CylinderGeometry(1.15,1.35,9,12),body,0,0,.6,Math.PI/2);        // fat fuselage
    add(new THREE.ConeGeometry(1.15,3.2,12),body,0,0,-5.3,-Math.PI/2);
    add(new THREE.BoxGeometry(17,.5,2.9),body,0,.9,1.2);                          // high wing
    for(const s of[-1,1]) add(new THREE.BoxGeometry(.2,3,2.2),acc,s*4.6,2.2,1.2); // struts
    add(new THREE.BoxGeometry(6.4,.3,2.2),acc,0,1.4,-4.6);
    add(new THREE.BoxGeometry(.2,2.2,2.2),acc,0,2.4,-4.6);
    add(new THREE.SphereGeometry(.85,12,10),glass,0,.85,-3.4);
    for(const s of[-1,1]) add(new THREE.CylinderGeometry(.5,.5,2.6,10),dark,s*2.6,.9,-5.6,Math.PI/2); // engines
    for(const s of[-1,1]) add(new THREE.CylinderGeometry(.09,.09,1.6,6),dark,s*1.8,-1.6,2);
  }
  g.traverse(o=>{ if(o.isMesh){ o.castShadow=false; } });
  return g;
}

/* ============================================================
   FLIGHT DYNAMICS — custom Newtonian 6-DOF-lite model
   ============================================================ */
const keys={};
addEventListener('keydown',e=>{ keys[e.code]=true; if(e.code==='Space')e.preventDefault(); });
addEventListener('keyup',e=>keys[e.code]=false);

class Plane {
  constructor(def, hostile=false){
    this.def=def; this.hostile=hostile;
    this.obj=buildModel(def,hostile); scene.add(this.obj);
    this.vel=new THREE.Vector3();
    this.angVel=new THREE.Vector3();       // body rates (x=pitch, y=yaw, z=roll)
    this.quat=new THREE.Quaternion();
    this.throttle=0; this.rpm=0; this.hp=def.hp; this.dead=false;
    this.gear=true; this.onGround=false; this.gForce=1; this.alpha=0; this.stalled=false;
    this.input={pitch:0,roll:0,yaw:0};
    this.cool=0;                            // gun cooldown
  }
  fwd(){ return new THREE.Vector3(0,0,-1).applyQuaternion(this.quat); }
  up(){ return new THREE.Vector3(0,1,0).applyQuaternion(this.quat); }
  right(){ return new THREE.Vector3(1,0,0).applyQuaternion(this.quat); }
  speed(){ return this.vel.length(); }

  step(dt){
    if(this.dead){ this.vel.set(0,0,0); return; }
    const d=this.def, q=this.quat;
    const alt=Math.max(0,this.obj.position.y);
    const rho=RHO0*Math.exp(-alt/8500);                 // ISA-ish density
    // --- engine (spool lag + altitude lapse) ---
    this.rpm += (this.throttle-this.rpm)*Math.min(1,dt/d.spool);
    const lapse = 0.6+0.4*(rho/RHO0);
    const thrust = d.maxThrust*this.rpm*lapse;
    // --- aerodynamics in body frame (fwd = -Z) ---
    const qInv=q.clone().invert();
    const vb=this.vel.clone().applyQuaternion(qInv);
    const V=vb.length();
    const alpha = V>1 ? Math.atan2(-vb.y, -vb.z) : 0;
    const beta  = V>1 ? Math.asin(clamp(vb.x/Math.max(V,1e-3),-1,1)) : 0;
    this.alpha=alpha;
    const qbar=0.5*rho*V*V;
    // lift coefficient w/ stall break
    const a=alpha, as=d.stallAlpha;
    let cl;
    if(Math.abs(a)<as) cl = d.cl0 + d.clSlope*a;
    else { const over=Math.abs(a)-as; cl = Math.sign(a)*(d.cl0+d.clSlope*as)*Math.max(0.25,1-over*3.2); }
    this.stalled = Math.abs(a)>as && V>15;
    const cd = d.cd0 + cl*cl/(Math.PI*d.oswald*(d.span*d.span/d.wingArea)) + (this.stalled?0.06:0);
    const L=qbar*d.wingArea*cl, D=qbar*d.wingArea*cd, S=qbar*d.wingArea*0.85*beta;
    const F=new THREE.Vector3(0,-G*d.mass,0);           // weight
    F.addScaledVector(this.fwd(), thrust);
    if(V>0.5){
      const vDir=this.vel.clone().normalize();
      // lift ⊥ velocity, in the body up-plane
      let liftDir=this.up().clone().sub(vDir.clone().multiplyScalar(this.up().dot(vDir)));
      if(liftDir.lengthSq()>1e-6){ liftDir.normalize(); F.addScaledVector(liftDir,L); }
      F.addScaledVector(vDir,-D);
      F.addScaledVector(this.right(),-S);
    }
    this.gForce = L/(d.mass*G) * Math.cos(0) + (this.onGround?1:0);
    this.vel.addScaledVector(F,dt/d.mass);
    // --- rotational dynamics: control authority ∝ dynamic pressure, inertia lag, stability ---
    const qRef=0.5*RHO0*d.vRef*d.vRef;
    const eff=clamp(qbar/qRef,0,1);
    const tgt=new THREE.Vector3(
      -this.input.pitch*d.pitchRate*eff,
      this.input.yaw*d.yawRate*eff,
      this.input.roll*d.rollRate*eff);
    // static stability: weathervane toward relative wind
    tgt.x += alpha*d.stab*eff*0.8;
    tgt.y += beta*d.stab*eff*0.5;
    // stall drops nose
    if(this.stalled) tgt.x += 0.5;
    const resp = 1-Math.exp(-dt*(2.5+eff*3));           // inertia: response lag
    this.angVel.lerp(tgt,resp);
    // integrate quaternion from body rates
    const w=this.angVel, wl=w.length();
    if(wl>1e-5){
      const dq=new THREE.Quaternion().setFromAxisAngle(w.clone().normalize(),wl*dt);
      q.premultiply(dq).normalize();
    }
    this.obj.position.addScaledVector(this.vel,dt);
    this.obj.quaternion.copy(q);
    // --- ground interaction ---
    const p=this.obj.position;
    const gh=terrainH(p.x,p.z);
    const clearance=this.gear?1.7:1.0;
    this.onGround=false;
    if(p.y<gh+clearance){
      const runway=onRunway(p.x,p.z);
      const vs=this.vel.y;
      const upDot=this.up().y;
      const gentle = vs>-6.5 && upDot>0.6 && runway;
      if(gentle || (this.onGroundPrev && runway)){
        this.onGround=true;
        p.y=gh+clearance;
        if(this.vel.y<0) this.vel.y=0;
        // constant rolling friction (μ·g) + steering
        const mu = this.throttle>0.05 ? 0.02 : 0.06;
        const frA = mu*G;
        const sp = Math.hypot(this.vel.x,this.vel.z);
        if(sp>0.01){ const dec=Math.min(sp,frA*dt);
          this.vel.x-=this.vel.x/sp*dec; this.vel.z-=this.vel.z/sp*dec; }
        const hdg=Math.atan2(this.fwd().x,this.fwd().z)+Math.PI;
        const steer=-this.input.yaw*0.5*clamp(this.speed()/20,0,1);
        const yawQ=new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0),steer*dt);
        q.premultiply(yawQ);
        // level the plane on its gear
        const e=new THREE.Euler().setFromQuaternion(q,'YXZ');
        e.z=lerp(e.z,0,dt*3);
        if(Math.abs(this.input.pitch)<0.05) e.x=lerp(e.x,0,dt*3);
        q.setFromEuler(e);
        if(!this.onGroundPrev && this.touchdownCb) this.touchdownCb(vs,this.speed());
      } else {
        this.crash();
      }
    }
    this.onGroundPrev=this.onGround;
    this.alt=alt; this.rho=rho; this.airspeed=V;
  }
  crash(){ if(this.dead)return; this.dead=true; explode(this.obj.position,2.2); this.hp=0; }
  fire(){ if(this.cool>0||this.dead)return false; this.cool=0.09; return true; }
}

/* ---------- projectiles & explosions ---------- */
const bullets=[], booms=[];
const bulletGeo=new THREE.SphereGeometry(.35,6,6);
const bulletMat=new THREE.MeshBasicMaterial({color:0xffd98a});
function shoot(p){
  const m=new THREE.Mesh(bulletGeo,bulletMat);
  const dir=p.fwd();
  m.position.copy(p.obj.position).addScaledVector(dir,6);
  scene.add(m);
  bullets.push({m, vel:dir.multiplyScalar(p.airspeed+900).add(p.vel), life:2.2, from:p});
}
function explode(pos,scale=1){
  const m=new THREE.Mesh(new THREE.SphereGeometry(3*scale,12,12),
    new THREE.MeshBasicMaterial({color:0xffa030,transparent:true,opacity:.95}));
  m.position.copy(pos); scene.add(m);
  booms.push({m,t:0,scale});
  boomSound();
}

/* ============================================================
   AUDIO — synthesized engine, airflow, wind (WebAudio)
   ============================================================ */
let AC=null, engineNodes=null, windGain=null, windFilter=null, masterGain=null;
function initAudio(){
  if(AC) return;
  AC=new (window.AudioContext||window.webkitAudioContext)();
  masterGain=AC.createGain(); masterGain.gain.value=0.7; masterGain.connect(AC.destination);
  // engine: sawtooth stack + rumble noise through lowpass
  const eg=AC.createGain(); eg.gain.value=0;
  const lp=AC.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=400;
  const o1=AC.createOscillator(); o1.type='sawtooth'; o1.frequency.value=60;
  const o2=AC.createOscillator(); o2.type='square';   o2.frequency.value=91;
  const og=AC.createGain(); og.gain.value=.5;
  o1.connect(og); o2.connect(og); og.connect(lp); lp.connect(eg); eg.connect(masterGain);
  // rumble noise
  const buf=AC.createBuffer(1,AC.sampleRate*2,AC.sampleRate);
  const dat=buf.getChannelData(0); for(let i=0;i<dat.length;i++)dat[i]=Math.random()*2-1;
  const ns=AC.createBufferSource(); ns.buffer=buf; ns.loop=true;
  const ng=AC.createGain(); ng.gain.value=.35;
  ns.connect(ng); ng.connect(lp);
  o1.start(); o2.start(); ns.start();
  engineNodes={eg,lp,o1,o2};
  // wind / airflow: bandpassed noise, volume ∝ q̄
  const wf=AC.createBiquadFilter(); wf.type='bandpass'; wf.frequency.value=700; wf.Q.value=.6;
  const wg=AC.createGain(); wg.gain.value=0;
  const ns2=AC.createBufferSource(); ns2.buffer=buf; ns2.loop=true; ns2.playbackRate.value=1.7;
  ns2.connect(wf); wf.connect(wg); wg.connect(masterGain); ns2.start();
  windGain=wg; windFilter=wf;
}
function boomSound(){
  if(!AC)return;
  const b=AC.createBuffer(1,AC.sampleRate*1.2,AC.sampleRate);
  const d=b.getChannelData(0);
  for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,2.2);
  const s=AC.createBufferSource(); s.buffer=b;
  const f=AC.createBiquadFilter(); f.type='lowpass'; f.frequency.value=220;
  const g=AC.createGain(); g.gain.value=1.2;
  s.connect(f); f.connect(g); g.connect(masterGain); s.start();
}
function gunSound(){
  if(!AC)return;
  const b=AC.createBuffer(1,AC.sampleRate*0.08,AC.sampleRate);
  const d=b.getChannelData(0);
  for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,3);
  const s=AC.createBufferSource(); s.buffer=b;
  const g=AC.createGain(); g.gain.value=.5;
  s.connect(g); g.connect(masterGain); s.start();
}
function updateAudio(p){
  if(!AC||!p)return;
  const jet=p.def.id!=='skylark';
  const rpm=p.rpm, rhoF=p.rho/RHO0;
  const base= jet? 90+rpm*260 : 45+rpm*95;
  engineNodes.o1.frequency.setTargetAtTime(base,AC.currentTime,.08);
  engineNodes.o2.frequency.setTargetAtTime(base*1.51,AC.currentTime,.08);
  engineNodes.lp.frequency.setTargetAtTime(300+rpm*2600,AC.currentTime,.1);
  engineNodes.eg.gain.setTargetAtTime((0.05+rpm*0.30)*rhoF*(jet?1.15:1),AC.currentTime,.1);
  const q=0.5*p.rho*p.airspeed*p.airspeed;
  const wv=clamp(q/18000,0,1);
  windGain.gain.setTargetAtTime(wv*wv*0.55,AC.currentTime,.15);
  windFilter.frequency.setTargetAtTime(400+p.airspeed*9+(p.stalled?300:0),AC.currentTime,.15);
}

/* ============================================================
   GAME STATE / SCENARIOS
   ============================================================ */
let player=null, enemies=[], mode=null, paused=false, missionState='flying', msgT=0, camMode=0;
let selPlane=1, selScn='takeoff';
const enemyModels=[];
function makeEnemy(){
  const def=FLEET[1];
  const e=new Plane(def,true);
  e.hp=45; e.aiT=0; e.aiMode='pursue';
  return e;
}
function spawnScenario(){
  // clear
  if(player){ scene.remove(player.obj); }
  enemies.forEach(e=>scene.remove(e.obj)); enemies=[];
  bullets.forEach(b=>scene.remove(b.m)); bullets.length=0;
  booms.forEach(b=>scene.remove(b.m)); booms.length=0;
  const def=FLEET[selPlane];
  player=new Plane(def);
  player.touchdownCb=(vs,spd)=>{
    if(mode==='landing'){
      if(onRunway(player.obj.position.x,player.obj.position.z)){
        if(Math.abs(vs)<3.2){ setMsg('BUTTER! LANDING SUCCESSFUL — '+vs.toFixed(1)+' m/s'); missionState='won'; }
        else if(Math.abs(vs)<5.5){ setMsg('FIRM LANDING — SURVIVABLE ('+vs.toFixed(1)+' m/s)'); missionState='won'; }
        else setMsg('HARD LANDING — GEAR DAMAGED');
      }
    }
  };
  if(selScn==='takeoff'){
    player.obj.position.set(0,terrainH(0,-600)+1.7,-600);
    player.quat.setFromEuler(new THREE.Euler(0,Math.PI,0)); // face +z... runway along z
    player.vel.set(0,0,0); player.throttle=0;
    mode='takeoff'; missionState='flying';
    setMsg('THROTTLE UP (W) — ROTATE AT '+Math.round(def.vRef*1.1)+' M/S',4);
  } else if(selScn==='landing'){
    player.obj.position.set(0,420,-5200);
    player.quat.setFromEuler(new THREE.Euler(0,Math.PI,0));
    player.vel.copy(player.fwd()).multiplyScalar(def.vRef*1.35);
    player.throttle=.45; player.rpm=.45;
    mode='landing'; missionState='flying';
    setMsg('RUNWAY AHEAD — MANAGE GLIDEPATH & ENERGY',4.5);
  } else {
    player.obj.position.set(0,900,-2000);
    player.quat.setFromEuler(new THREE.Euler(0,0,0));
    player.vel.copy(player.fwd()).multiplyScalar(def.vRef*1.6);
    player.throttle=.8; player.rpm=.8;
    mode='dogfight'; missionState='flying';
    for(let i=0;i<3;i++){
      const e=makeEnemy();
      e.obj.position.set(-600+i*600, 850+Math.random()*300, -1200+i*400);
      e.vel.copy(e.fwd()).multiplyScalar(120);
      enemies.push(e);
    }
    setMsg('BANDITS INBOUND — SPACE TO FIRE',4.5);
  }
  player.onGroundPrev = (selScn==='takeoff');
}
function setMsg(t,dur=3){ const el=document.getElementById('hudMsg'); el.textContent=t; el.style.opacity=1; msgT=dur; }

/* ---------- enemy AI ---------- */
function aiStep(e,dt){
  if(e.dead)return;
  e.aiT-=dt;
  const toP=player.obj.position.clone().sub(e.obj.position);
  const dist=toP.length();
  if(e.aiT<=0){ e.aiT=2+Math.random()*2; e.aiMode = dist<250?'flee':'pursue'; }
  let want;
  if(e.aiMode==='pursue') want=toP.normalize();
  else want=toP.normalize().negate();
  // convert desired direction into body-frame inputs
  const qInv=e.quat.clone().invert();
  const lw=want.applyQuaternion(qInv); // fwd=-z
  e.input.pitch = clamp(lw.y*3,-1,1);
  e.input.roll  = clamp(-lw.x*3,-1,1);
  e.input.yaw   = clamp(-lw.x*0.6,-1,1);
  e.throttle = dist>400?1:0.65;
  // guns
  if(dist<420 && e.aiMode==='pursue'){
    const aim=e.fwd().dot(toP.clone().normalize());
    if(aim>0.9975 && Math.random()<0.04) { if(e.fire()){ shoot(e); gunSound(0.2); } }
  }
}

/* ============================================================
   HUD
   ============================================================ */
const $=id=>document.getElementById(id);
const ladder=$('pitchLadder');
for(let a=-60;a<=60;a+=10){ if(!a)continue;
  const d=document.createElement('div'); d.className='pl';
  d.style.top=(-a*4)+'px'; d.innerHTML='<span>'+a+'</span>'; ladder.appendChild(d); }
function updateHUD(){
  const p=player; if(!p)return;
  const e=new THREE.Euler().setFromQuaternion(p.quat,'YXZ');
  const pitch=THREE.MathUtils.radToDeg(e.x), roll=THREE.MathUtils.radToDeg(e.z);
  const hdg=(THREE.MathUtils.radToDeg(Math.atan2(p.fwd().x,-p.fwd().z))+360)%360;
  $('horizon').style.transform=`rotate(${-roll}deg) translateY(${pitch*4}px)`;
  ladder.style.transform=`rotate(${-roll}deg)`;
  ladder.style.top='50%'; ladder.style.left='50%';
  $('hudSpeed').textContent=`KIAS ${(p.airspeed*1.944).toFixed(0)}\nMACH ${(p.airspeed/340).toFixed(2)}\nα ${(THREE.MathUtils.radToDeg(p.alpha)).toFixed(1)}°\nG ${p.gForce.toFixed(1)}`;
  $('hudAlt').textContent=`ALT ${Math.round(p.obj.position.y*3.281)} ft\nV/S ${Math.round(p.vel.y*196.85)} fpm\nρ ${(p.rho).toFixed(3)}`;
  $('hudHead').textContent=`HDG ${hdg.toFixed(0).padStart(3,'0')}°`;
  $('hudThr').textContent=`THR ${'▮'.repeat(Math.round(p.rpm*10)).padEnd(10,'▯')} ${Math.round(p.rpm*100)}%\nRPM ${Math.round(800+p.rpm*(p.def.id==='skylark'?1900:7200))}${p.stalled?'\n⚠ STALL':''}`;
  $('hudHp').textContent=`HULL ${'█'.repeat(Math.max(0,Math.round(p.hp/p.def.hp*10)))}${'░'.repeat(10-Math.max(0,Math.round(p.hp/p.def.hp*10)))}`;
  if(mode==='dogfight'){
    let n=enemies.filter(x=>!x.dead).length;
    let nearest=1e9;
    enemies.forEach(x=>{ if(!x.dead) nearest=Math.min(nearest,x.obj.position.distanceTo(p.obj.position)); });
    $('hudTarget').textContent=`BANDITS ${n}\nNEAREST ${nearest>9e8?'—':Math.round(nearest)+' m'}`;
  } else if(mode==='takeoff'){
    $('hudTarget').textContent=`CLIMB TO 1000 ft\n${Math.round(Math.min(100,p.obj.position.y*3.281/1000*100))}%`;
  } else {
    const d=Math.hypot(p.obj.position.x,p.obj.position.z);
    $('hudTarget').textContent=`RUNWAY ${onRunway(p.obj.position.x,p.obj.position.z)?'LOCKED':Math.round(d)+' m'}`;
  }
  if(missionState!=='flying'){ $('hudState').textContent='ESC — HANGAR'; }
  else $('hudState').textContent='';
  // G-stress redout
  $('redflash').style.opacity = p.gForce>7? clamp((p.gForce-7)/4,0,.9) : 0;
}

/* ============================================================
   CAMERA
   ============================================================ */
const camPos=new THREE.Vector3(), camLook=new THREE.Vector3();
function updateCamera(dt){
  const p=player; if(!p)return;
  if(camMode===0){ // chase
    const back=p.fwd().clone().multiplyScalar(-(14+p.def.span*0.6));
    const desired=p.obj.position.clone().add(back).add(new THREE.Vector3(0,4.5,0));
    camPos.lerp(desired,1-Math.exp(-dt*4));
    camera.position.copy(camPos);
    camLook.lerp(p.obj.position.clone().addScaledVector(p.vel,0.25),1-Math.exp(-dt*6));
    camera.lookAt(camLook);
  } else { // cockpit-ish
    camera.position.copy(p.obj.position).add(p.up().clone().multiplyScalar(1.1)).add(p.fwd().multiplyScalar(1.5));
    camera.quaternion.copy(p.quat);
  }
  const gh=terrainH(camera.position.x,camera.position.z)+3;
  if(camera.position.y<gh) camera.position.y=gh;
}

/* ============================================================
   MAIN LOOP
   ============================================================ */
let last=performance.now();
function loop(now){
  requestAnimationFrame(loop);
  const dt=Math.min(0.033,(now-last)/1000); last=now;
  if(paused||!player){ renderer.render(scene,camera); return; }
  // input
  const p=player;
  if(missionState==='flying'&&!p.dead){
    p.input.pitch=(keys['ArrowUp']?1:0)-(keys['ArrowDown']?1:0);
    p.input.roll =(keys['ArrowLeft']?1:0)-(keys['ArrowRight']?1:0);
    p.input.yaw  =(keys['KeyD']?1:0)-(keys['KeyA']?1:0);
    if(keys['KeyW']) p.throttle=clamp(p.throttle+dt*0.45,0,1);
    if(keys['KeyS']) p.throttle=clamp(p.throttle-dt*0.45,0,1);
    if(keys['Space']&&mode==='dogfight'){ if(p.fire()){ shoot(p); gunSound(); } }
  } else { p.input.pitch=p.input.roll=p.input.yaw=0; }
  p.cool-=dt;
  p.step(dt);
  // mission logic
  if(missionState==='flying'){
    if(mode==='takeoff'&&!p.dead&&p.obj.position.y*3.281>1000){ missionState='won'; setMsg('TAKEOFF COMPLETE — ALT 1000 ft'); }
    if(mode==='landing'&&p.dead){ missionState='lost'; setMsg('AIRFRAME LOST'); }
    if(mode==='dogfight'){
      enemies.forEach(e=>aiStep(e,dt));
      enemies.forEach(e=>{ if(!e.dead){ e.cool-=dt; e.step(dt);
        const gh=terrainH(e.obj.position.x,e.obj.position.z);
        if(e.obj.position.y<gh+2){ e.crash(); } } });
      // bullet hits
      for(let i=bullets.length-1;i>=0;i--){
        const b=bullets[i]; b.life-=dt;
        b.m.position.addScaledVector(b.vel,dt);
        let hit=false;
        const targets=b.from===p?enemies:[p];
        for(const t of targets){
          if(!t.dead && b.m.position.distanceTo(t.obj.position)<t.def.span*0.7){
            t.hp-=8; hit=true;
            if(t.hp<=0){ t.crash(); if(t===p){missionState='lost';setMsg('YOU WERE SHOT DOWN');} else setMsg('BANDIT DOWN!'); }
            break;
          }
        }
        const gh=terrainH(b.m.position.x,b.m.position.z);
        if(b.m.position.y<gh||b.life<=0) hit=true;
        if(hit){ scene.remove(b.m); bullets.splice(i,1); }
      }
      if(!enemies.some(e=>!e.dead)){ missionState='won'; setMsg('AIRSPACE CLEAR — ALL BANDITS DESTROYED'); }
    }
    if(p.dead&&mode!=='dogfight'){ missionState='lost'; setMsg('AIRFRAME LOST'); }
    if(p.dead&&mode==='dogfight'&&missionState==='flying'){ missionState='lost'; setMsg('YOU WERE DOWNED'); }
  }
  // explosions
  for(let i=booms.length-1;i>=0;i--){
    const b=booms[i]; b.t+=dt;
    b.m.scale.setScalar(1+b.t*9); b.m.material.opacity=Math.max(0,.95-b.t*1.4);
    if(b.t>0.8){ scene.remove(b.m); booms.splice(i,1); }
  }
  if(msgT>0){ msgT-=dt; if(msgT<=0) $('hudMsg').style.opacity=0; }
  updateCamera(dt);
  updateHUD();
  updateAudio(p);
  renderer.render(scene,camera);
}
requestAnimationFrame(loop);

/* ============================================================
   MENU WIRING
   ============================================================ */
function buildMenu(){
  const fleet=$('fleet');
  FLEET.forEach((d,i)=>{
    const c=document.createElement('div');
    c.className='card'+(i===selPlane?' sel':'');
    c.innerHTML=`<h3>${d.name}</h3><div class="role">${d.role}</div>
      ${['SPD','THR','AGI','ARM'].map(k=>`<div class="stat"><span>${k}</span><div class="bar" style="flex:1;margin:0 8px"><i style="width:${d.stats[k]*100}%"></i></div></div>`).join('')}
      <div class="stat"><span>MTOW</span><b>${d.mass.toLocaleString()} kg</b></div>
      <div class="stat"><span>T/W</span><b>${(d.maxThrust/(d.mass*G)).toFixed(2)}</b></div>
      <div class="stat" style="margin-top:6px;color:#7fa8c9">${d.desc}</div>`;
    c.onclick=()=>{ selPlane=i; document.querySelectorAll('.card').forEach((x,j)=>x.classList.toggle('sel',j===i)); };
    fleet.appendChild(c);
  });
  document.querySelectorAll('.scn').forEach(s=>{
    s.onclick=()=>{ selScn=s.dataset.s; document.querySelectorAll('.scn').forEach(x=>x.classList.toggle('sel',x===s)); };
  });
  $('launchBtn').onclick=launch;
}
function launch(){
  initAudio(); if(AC.state==='suspended')AC.resume();
  buildTerrain();
  spawnScenario();
  $('menu').classList.add('hidden');
  $('hud').classList.add('on');
  paused=false;
}
addEventListener('keydown',e=>{
  if(e.code==='Escape'){
    if($('menu').classList.contains('hidden')){ paused=false; backToMenu(); }
  }
  if(e.code==='KeyC') camMode=1-camMode;
});
function backToMenu(){
  $('menu').classList.remove('hidden');
  $('hud').classList.remove('on');
  if(engineNodes) engineNodes.eg.gain.setTargetAtTime(0,AC.currentTime,.1);
  if(windGain) windGain.gain.setTargetAtTime(0,AC.currentTime,.1);
  player=null;
}
addEventListener('resize',()=>{
  camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
  renderer.setSize(innerWidth,innerHeight);
});
buildMenu();
// test hook
window.__sim = { get player(){return player;}, set player(v){player=v;}, get enemies(){return enemies;},
  get missionState(){return missionState;}, set missionState(v){missionState=v;},
  get paused(){return paused;}, set paused(v){paused=v;}, get bullets(){return bullets;},
  shoot, explode };
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"e8dbf827-7c8f-4bdb-b7bc-4dbb0b869ac4","tokensIn":8108000,"tokensOut":41708,"tokensTotal":8149708,"cost":0,"turns":99,"toolCalls":98,"failedToolCalls":1,"timestamp":"2026-08-21T22:41:37.939Z"} -->