← Flight Simulator results

Flight Simulator

inkling · basic

Initial view of Flight Simulator

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 29,988 bytes · SHA-256 c1bc935982c7
<!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>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
  <style>
    @import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap');

    * { margin: 0; padding: 0; box-sizing: border-box; }
    body { background: #030507; overflow: hidden; font-family: 'Orbitron', sans-serif; color: #e0f7ff; user-select: none; }

    /* HUD Layout */
    #hud { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
    .hud-panel { position: absolute; background: rgba(0, 10, 20, 0.55); border: 1px solid rgba(0, 255, 255, 0.15); backdrop-filter: blur(4px); border-radius: 6px; }
    .hud-value { font-family: 'Share Tech Mono', monospace; font-size: 1.6rem; color: #00ffaa; text-shadow: 0 0 8px rgba(0, 255, 170, 0.5); letter-spacing: 2px; }

    /* Top-Left: Aircraft Info */
    #info-panel { top: 20px; left: 20px; padding: 16px 22px; width: 260px; }
    #info-panel h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 2px; color: #00ffaa; margin-bottom: 6px; opacity: 0.9; }
    #info-panel .spec { font-family: 'Share Tech Mono', monospace; font-size: 0.75rem; color: #88ccff; margin: 2px 0; opacity: 0.8; }

    /* Top-Right: Telemetry */
    #telemetry { top: 20px; right: 20px; padding: 16px 22px; width: 240px; text-align: right; }
    #telemetry div { margin-bottom: 6px; }
    #telemetry .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px; color: #88ccff; opacity: 0.7; display: block; }
    #telemetry .value { font-family: 'Share Tech Mono', monospace; font-size: 1.2rem; color: #fff; text-shadow: 0 0 6px rgba(255,255,255,0.3); }

    /* Center Crosshair / Pitch Ladder */
    #center-hud { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; }
    .crosshair { width: 40px; height: 40px; border: 2px solid rgba(0,255,170,0.3); border-radius: 50%; position: relative; }
    .crosshair::before, .crosshair::after { content: ''; position: absolute; background: rgba(0,255,170,0.5); }
    .crosshair::before { width: 2px; height: 24px; left: 50%; top: 50%; transform: translate(-50%, -50%); }
    .crosshair::after { width: 24px; height: 2px; top: 50%; left: 50%; transform: translate(-50%, -50%); }

    /* Bottom Center: Mode & Status */
    #status-bar { bottom: 30px; left: 50%; transform: translateX(-50%); padding: 14px 28px; text-align: center; min-width: 320px; }
    #status-bar h3 { font-size: 0.9rem; letter-spacing: 3px; color: #00ffaa; text-transform: uppercase; margin-bottom: 4px; }
    #status-bar p { font-family: 'Share Tech Mono', monospace; font-size: 0.85rem; color: #88ccff; }

    /* Menu Overlay */
    #main-menu { position: fixed; inset: 0; z-index: 100; background: linear-gradient(180deg, rgba(3,8,15,0.95) 0%, rgba(3,15,30,0.98) 100%); display: flex; flex-direction: column; align-items: center; justify-content: center; transition: opacity 0.8s ease; }
    #main-menu.hidden { opacity: 0; pointer-events: none; }

    .menu-title { font-size: 5rem; font-weight: 900; letter-spacing: 12px; color: transparent; background: linear-gradient(90deg, #00ffaa 0%, #0088ff 50%, #aa00ff 100%); -webkit-background-clip: text; background-clip: text; text-shadow: 0 0 60px rgba(0,255,170,0.2); margin-bottom: 10px; animation: pulseTitle 3s infinite alternate; }
    @keyframes pulseTitle { from { filter: drop-shadow(0 0 20px rgba(0,255,170,0.2)); } to { filter: drop-shadow(0 0 40px rgba(0,136,255,0.4)); } }

    .menu-sub { font-family: 'Share Tech Mono', monospace; font-size: 1.2rem; color: #88ccff; letter-spacing: 6px; margin-bottom: 60px; opacity: 0.7; }

    .fleet-grid { display: flex; gap: 30px; margin-bottom: 50px; }
    .plane-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(0,255,170,0.15); border-radius: 12px; padding: 24px; width: 220px; text-align: center; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; }
    .plane-card:hover { border-color: rgba(0,255,170,0.6); background: rgba(0,255,170,0.05); transform: translateY(-8px); box-shadow: 0 20px 40px rgba(0,255,170,0.1); }
    .plane-card.active { border-color: #00ffaa; box-shadow: 0 0 30px rgba(0,255,170,0.3); }
    .plane-card h4 { font-size: 1.1rem; letter-spacing: 2px; margin-bottom: 4px; color: #fff; }
    .plane-card .role { font-family: 'Share Tech Mono', monospace; font-size: 0.7rem; color: #00ffaa; margin-bottom: 14px; text-transform: uppercase; }
    .plane-card .stat-row { display: flex; justify-content: space-between; font-family: 'Share Tech Mono', monospace; font-size: 0.75rem; color: #ccc; margin: 3px 0; padding-bottom: 2px; border-bottom: 1px solid rgba(255,255,255,0.05); }

    .scenario-grid { display: flex; gap: 20px; }
    .scenario-btn { padding: 16px 32px; border: 1px solid rgba(0,136,255,0.4); background: rgba(0,136,255,0.05); color: #88ccff; font-family: 'Orbitron', sans-serif; font-size: 0.9rem; letter-spacing: 3px; text-transform: uppercase; cursor: pointer; border-radius: 6px; transition: all 0.3s; }
    .scenario-btn:hover { background: rgba(0,136,255,0.2); color: #00ffaa; border-color: #00ffaa; }

    /* Canvas */
    #sim-canvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
  </style>
</head>
<body>

  <canvas id="sim-canvas"></canvas>

  <!-- HUD -->
  <div id="hud">
    <div id="info-panel" class="hud-panel">
      <h2 id="plane-name">Apex Striker</h2>
      <div class="spec" id="spec-thrust">Thrust: 45,000 N</div>
      <div class="spec" id="spec-weight">Weight: 8,200 kg</div>
      <div class="spec" id="spec-lift">Lift Coeff: 0.42</div>
      <div class="spec" id="spec-drag">Drag Coeff: 0.08</div>
    </div>

    <div id="telemetry" class="hud-panel">
      <div><span class="label">Airspeed</span><span class="hud-value" id="val-speed">0</span><span style="font-size:0.8rem; color:#888;"> KTS</span></div>
      <div><span class="label">Altitude</span><span class="hud-value" id="val-alt">120</span><span style="font-size:0.8rem; color:#888;"> M</span></div>
      <div><span class="label">Pitch</span><span class="hud-value" id="val-pitch">0</span><span style="font-size:0.8rem; color:#888;"> °</span></div>
      <div><span class="label">Roll</span><span class="hud-value" id="val-roll">0</span><span style="font-size:0.8rem; color:#888;"> °</span></div>
      <div><span class="label">AOA</span><span class="hud-value" id="val-aoa">0</span><span style="font-size:0.8rem; color:#888;"> °</span></div>
      <div><span class="label">Throttle</span><span class="hud-value" id="val-throttle">30</span><span style="font-size:0.8rem; color:#888;">%</span></div>
    </div>

    <div id="center-hud">
      <div class="crosshair"></div>
    </div>

    <div id="status-bar" class="hud-panel">
      <h3 id="scenario-title">Mission: Takeoff & Landing</h3>
      <p id="scenario-desc">Manage throttle and pitch for a clean lift-off from the valley runway.</p>
    </div>
  </div>

  <!-- Main Menu -->
  <div id="main-menu">
    <h1 class="menu-title">APEX AERO</h1>
    <p class="menu-sub">HIGH-FIDELITY FLIGHT SIMULATION</p>

    <div class="fleet-grid">
      <div class="plane-card active" onclick="selectPlane(0)">
        <h4>F-77 Striker</h4>
        <div class="role">Multirole Fighter</div>
        <div class="stat-row"><span>Max Speed</span><span>920 kts</span></div>
        <div class="stat-row"><span>Thrust</span><span>52k N</span></div>
        <div class="stat-row"><span>Weight</span><span>9,200 kg</span></div>
        <div class="stat-row"><span>Lift</span><span>High</span></div>
      </div>
      <div class="plane-card" onclick="selectPlane(1)">
        <h4>CS-200 Heavy</h4>
        <div class="role">Heavy Interceptor</div>
        <div class="stat-row"><span>Max Speed</span><span>780 kts</span></div>
        <div class="stat-row"><span>Thrust</span><span>68k N</span></div>
        <div class="stat-row"><span>Weight</span><span>14,500 kg</span></div>
        <div class="stat-row"><span>Lift</span><span>Medium</span></div>
      </div>
      <div class="plane-card" onclick="selectPlane(2)">
        <h4>VX-9 Glider</h4>
        <div class="role">Light Recon</div>
        <div class="stat-row"><span>Max Speed</span><span>650 kts</span></div>
        <div class="stat-row"><span>Thrust</span><span>28k N</span></div>
        <div class="stat-row"><span>Weight</span><span>4,800 kg</span></div>
        <div class="stat-row"><span>Lift</span><span>Very High</span></div>
      </div>
    </div>

    <div class="scenario-grid">
      <button class="scenario-btn" onclick="startScenario('landing')">Takeoff / Landing</button>
      <button class="scenario-btn" onclick="startScenario('dogfight')">Dogfight</button>
    </div>
  </div>

  <script>
    /* =========================================================
       THREE.JS FLIGHT SIMULATOR ENGINE — APEX AERO
       ========================================================= */

    // Global references
    let renderer, scene, camera, clock;
    let planeMesh, planePhysics;
    let terrain;
    let audioCtx, engineOsc, engineGain, windOsc, windGain, audioStarted = false;
    let selectedPlaneIndex = 0;
    let currentScenario = 'landing';
    let controls = { thrust: 0.3, pitch: 0, roll: 0, yaw: 0, throttleDelta: 0 };
    let keys = {};

    // Aircraft database with physics profiles
    const AIRCRAFT_DB = [
      {
        name: "F-77 Striker", role: "Multirole Fighter",
        mass: 9200, thrustMax: 52000, thrustAcc: 24000, dragCoeff: 0.08, liftCoeff: 0.72,
        wingArea: 42, maxSpeed: 450, stallAoa: 18,
        specsText: ["Thrust: 52,000 N", "Weight: 9,200 kg", "Lift Coeff: 0.72", "Drag Coeff: 0.08"]
      },
      {
        name: "CS-200 Heavy", role: "Heavy Interceptor",
        mass: 14500, thrustMax: 68000, thrustAcc: 18000, dragCoeff: 0.10, liftCoeff: 0.55,
        wingArea: 55, maxSpeed: 380, stallAoa: 14,
        specsText: ["Thrust: 68,000 N", "Weight: 14,500 kg", "Lift Coeff: 0.55", "Drag Coeff: 0.10"]
      },
      {
        name: "VX-9 Glider", role: "Light Recon",
        mass: 4800, thrustMax: 28000, thrustAcc: 32000, dragCoeff: 0.05, liftCoeff: 0.95,
        wingArea: 32, maxSpeed: 320, stallAoa: 22,
        specsText: ["Thrust: 28,000 N", "Weight: 4,800 kg", "Lift Coeff: 0.95", "Drag Coeff: 0.05"]
      }
    ];

    // Scene init
    function init() {
      const canvas = document.getElementById('sim-canvas');
      renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
      renderer.setSize(window.innerWidth, window.innerHeight);
      renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
      renderer.outputEncoding = THREE.sRGBEncoding;
      renderer.toneMapping = THREE.ACESFilmicToneMapping;
      renderer.toneMappingExposure = 1.2;

      scene = new THREE.Scene();
      scene.background = new THREE.Color(0x050a14);
      scene.fog = new THREE.FogExp2(0x0a1225, 0.012);

      // Camera
      camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 5000);
      camera.position.set(0, 30, 60);

      clock = new THREE.Clock();

      // Lights
      const hemi = new THREE.HemisphereLight(0x8899cc, 0x223344, 1.2);
      scene.add(hemi);

      const sun = new THREE.DirectionalLight(0xffffff, 1.6);
      sun.position.set(-200, 300, -100);
      sun.castShadow = true;
      sun.shadow.mapSize.width = 2048;
      sun.shadow.mapSize.height = 2048;
      sun.shadow.bias = -0.001;
      scene.add(sun);

      const fill = new THREE.DirectionalLight(0x5588cc, 0.5);
      fill.position.set(100, -50, 50);
      scene.add(fill);

      // Procedural Terrain
      generateTerrain();

      // Skybox (procedural gradient sphere)
      const skyGeo = new THREE.SphereGeometry(800, 32, 32);
      const skyMat = new THREE.MeshBasicMaterial({
        color: 0x020a14,
        side: THREE.BackSide,
        fog: false
      });
      const skyMesh = new THREE.Mesh(skyGeo, skyMat);
      scene.add(skyMesh);

      // Stars
      addStars();

      // Create aircraft geometry
      buildAircraft(selectedPlaneIndex);

      // Initialize physics state
      planePhysics = {
        velocity: new THREE.Vector3(0, 0, -20), // Start moving slightly forward
        velocityLocal: new THREE.Vector3(0, 0, -20),
        position: new THREE.Vector3(0, 30, 0),
        rotation: new THREE.Euler(0, 0, 0),
        pitchRate: 0, rollRate: 0, yawRate: 0,
        altitude: 30,
        airspeed: 20,
        aoa: 0, // Angle of attack
        stall: false
      };

      // Place mesh
      planeMesh.position.copy(planePhysics.position);
      planeMesh.rotation.copy(planePhysics.rotation);

      // Input listeners
      window.addEventListener('resize', onResize);
      window.addEventListener('keydown', e => { keys[e.code] = true; if(e.code === 'Space') startAudio(); });
      window.addEventListener('keyup', e => keys[e.code] = false);

      // Start loop
      animate();
    }

    // Procedural Terrain using Simplex-like noise approximation
    function generateTerrain() {
      const segments = 120;
      const size = 600;
      const geo = new THREE.PlaneGeometry(size, size, segments, segments);
      geo.rotateX(-Math.PI / 2);

      const pos = geo.attributes.position;
      const colors = [];
      for (let i = 0; i < pos.count; i++) {
        const x = pos.getX(i);
        const z = pos.getZ(i);
        // Procedural height using multiple octaves of sine/noise approximation
        const dist = Math.sqrt(x*x + z*z);
        let h = 0;
        h += Math.sin(x * 0.025) * 15 + Math.sin(z * 0.03) * 15;
        h += Math.sin(x * 0.05 + z * 0.03) * 8;
        h += Math.sin(x * 0.012) * Math.sin(z * 0.012) * 25;
        h += Math.sin(dist * 0.02) * 10;
        // Central valley / runway area
        if (Math.abs(x) < 40 && z > -100 && z < 100) {
          h = 2 + Math.sin(x * 0.05) * 1;
        } else {
          h = Math.max(h, 2); // Keep base above water-ish
        }
        // Add some mountains further out
        if (dist > 150) {
          h += Math.sin(dist * 0.04) * 30 + 20;
        }
        pos.setY(i, h);

        // Vertex colors for terrain shading
        const color = new THREE.Color();
        if (h < 10) {
          color.set(0x2a5a3a); // Grass/low
        } else if (h < 40) {
          color.set(0x6b5e4a); // Dirt/rock mid
        } else {
          color.set(0x8899aa); // Snow/peak
        }
        colors.push(color.r, color.g, color.b);
      }

      geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
      geo.computeVertexNormals();

      const terrainMat = new THREE.MeshStandardMaterial({
        vertexColors: true,
        roughness: 0.9,
        metalness: 0.05,
        flatShading: false
      });

      terrain = new THREE.Mesh(geo, terrainMat);
      terrain.receiveShadow = true;
      scene.add(terrain);

      // Add runway marker in valley
      const runwayMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 });
      const runway = new THREE.Mesh(new THREE.BoxGeometry(60, 0.2, 300), runwayMat);
      runway.position.set(0, 0.1, 0);
      runway.receiveShadow = true;
      scene.add(runway);
    }

    // Add starfield
    function addStars() {
      const starGeo = new THREE.BufferGeometry();
      const starCount = 3000;
      const posArray = new Float32Array(starCount * 3);
      for (let i = 0; i < starCount * 3; i++) {
        posArray[i] = (Math.random() - 0.5) * 800;
      }
      starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
      const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 1.5, sizeAttenuation: true });
      const stars = new THREE.Points(starGeo, starMat);
      scene.add(stars);
    }

    // Aircraft builder
    function buildAircraft(index) {
      const ac = AIRCRAFT_DB[index];
      const group = new THREE.Group();

      // Fuselage
      const fuselageGeo = new THREE.BoxGeometry(1, 1.5, 8);
      const fuselageMat = new THREE.MeshStandardMaterial({ color: 0x334455, roughness: 0.3, metalness: 0.7 });
      const fuselage = new THREE.Mesh(fuselageGeo, fuselageMat);
      group.add(fuselage);

      // Wings (angled for lift effect)
      const wingGeo = new THREE.BoxGeometry(16, 0.2, 4);
      const wingMat = new THREE.MeshStandardMaterial({ color: 0x556677, roughness: 0.4, metalness: 0.6 });
      const wings = new THREE.Mesh(wingGeo, wingMat);
      wings.position.set(0, 0.2, 0);
      group.add(wings);

      // Tail
      const tailGeo = new THREE.BoxGeometry(0.1, 3, 2);
      const tail = new THREE.Mesh(tailGeo, wingMat);
      tail.position.set(0, 1.5, -3.5);
      group.add(tail);

      // Nose cone (pointy)
      const noseGeo = new THREE.ConeGeometry(0.8, 3, 8);
      const nose = new THREE.Mesh(noseGeo, fuselageMat);
      nose.position.set(0, 0, 3.5);
      nose.rotation.x = -Math.PI / 2;
      group.add(nose);

      // Engines (cylinders)
      const engGeo = new THREE.CylinderGeometry(0.4, 0.5, 1.5, 8);
      const engMat = new THREE.MeshStandardMaterial({ color: 0x222, roughness: 0.5, metalness: 0.8 });
      const leftEng = new THREE.Mesh(engGeo, engMat);
      leftEng.position.set(-3, -0.8, -1);
      leftEng.rotation.x = Math.PI / 2;
      group.add(leftEng);
      const rightEng = new THREE.Mesh(engGeo, engMat);
      rightEng.position.set(3, -0.8, -1);
      rightEng.rotation.x = Math.PI / 2;
      group.add(rightEng);

      group.scale.set(0.8, 0.8, 0.8);
      group.rotation.y = Math.PI;

      if (planeMesh) scene.remove(planeMesh);
      planeMesh = group;
      scene.add(planeMesh);
    }

    // Physics engine
    function updatePhysics(delta) {
      const ac = AIRCRAFT_DB[selectedPlaneIndex];
      const p = planePhysics;

      // Inputs
      if (keys['ArrowUp']) controls.pitch += 0.04;
      if (keys['ArrowDown']) controls.pitch -= 0.04;
      if (keys['ArrowLeft']) controls.roll += 0.03;
      if (keys['ArrowRight']) controls.roll -= 0.03;
      if (keys['KeyW']) controls.thrust += 0.02;
      if (keys['KeyS']) controls.thrust -= 0.02;

      // Clamp
      controls.pitch = Math.max(-0.8, Math.min(0.8, controls.pitch));
      controls.roll = Math.max(-1.2, Math.min(1.2, controls.roll));
      controls.thrust = Math.max(0, Math.min(1.2, controls.thrust));

      // Atmospheric density based on altitude
      const altitudeFactor = Math.exp(-p.altitude / 8500); // Simple exponential decay
      const airDensity = 1.225 * altitudeFactor;

      // Thrust
      const thrustForce = ac.thrustMax * controls.thrust;

      // Velocity magnitude
      const speed = p.velocity.length();
      p.airspeed = Math.round(speed * 1.94384); // Knots approx

      // Angle of Attack (pitch relative to velocity vector)
      const pitchAngle = p.rotation.x;
      const aoa = pitchAngle - Math.atan2(p.velocity.y, Math.abs(p.velocity.z));
      p.aoa = THREE.MathUtils.clamp(aoa, -Math.PI, Math.PI);
      const aoaDeg = THREE.MathUtils.radToDeg(p.aoa);
      p.stall = Math.abs(aoaDeg) > ac.stallAoa || speed < ac.maxSpeed * 0.15;

      // Lift
      // L = 0.5 * rho * v^2 * S * Cl
      // Cl depends roughly linearly on AoA up to stall
      let clFactor = 0;
      if (!p.stall) {
        clFactor = 0.1 + (Math.abs(aoaDeg) / ac.stallAoa) * ac.liftCoeff;
        clFactor = THREE.MathUtils.clamp(clFactor, 0, ac.liftCoeff * 1.5);
      } else {
        clFactor = ac.liftCoeff * 0.3; // Post-stall reduced lift
      }

      // Induced drag increases at high lift
      const inducedDragCoeff = (clFactor * clFactor) / (Math.PI * 6); // Approx
      const profileDragCoeff = ac.dragCoeff;

      // Drag
      // D = 0.5 * rho * v^2 * S * Cd
      const dragCoeffTotal = profileDragCoeff + inducedDragCoeff;
      const dragForceMag = 0.5 * airDensity * speed * speed * ac.wingArea * dragCoeffTotal;

      // Direction vectors
      const forward = new THREE.Vector3(0, 0, -1);
      forward.applyQuaternion(p.rotation); // Incorrect for world direction, let's use simpler model.

      // Simplified world-space physics approximation:
      // Apply thrust along plane's forward axis
      const thrustVec = new THREE.Vector3(0, 0, -1); // Local forward
      thrustVec.applyEuler(p.rotation);
      thrustVec.multiplyScalar(thrustForce);

      // Drag opposes velocity
      const dragVec = p.velocity.clone().normalize().multiplyScalar(-dragForceMag);

      // Lift is perpendicular to velocity, in pitch plane (simplified)
      const velocityDir = p.velocity.clone().normalize();
      // Perpendicular to velocity in vertical plane
      const liftDir = new THREE.Vector3(-velocityDir.y, velocityDir.x, 0).normalize();
      // Adjust lift direction relative to pitch/roll
      const liftVec = new THREE.Vector3(0, 1, 0);
      liftVec.applyQuaternion(p.rotation); // Rough approximation
      // Better lift approximation: lift acts "up" relative to aircraft, scaled by AoA
      const liftForceMag = 0.5 * airDensity * speed * speed * ac.wingArea * clFactor;
      const liftVecSimple = new THREE.Vector3(0, liftForceMag, 0);
      liftVecSimple.applyQuaternion(p.rotation);

      // Weight / Gravity
      const weightVec = new THREE.Vector3(0, -ac.mass * 9.81, 0);

      // Combine forces
      const totalForce = new THREE.Vector3();
      totalForce.add(thrustVec);
      totalForce.add(dragVec);
      totalForce.add(liftVecSimple);
      totalForce.add(weightVec);

      // Accelerate (F=ma)
      const accel = totalForce.divideScalar(ac.mass);
      p.velocity.add(accel.multiplyScalar(delta));

      // Update position
      p.position.add(p.velocity.clone().multiplyScalar(delta));

      // Inertia / rotational dynamics
      // Roll and pitch rates based on inputs, dampened
      const rollDamp = 0.85, pitchDamp = 0.85;
      p.rollRate = THREE.MathUtils.lerp(p.rollRate, controls.roll * 1.5, 0.08);
      p.pitchRate = THREE.MathUtils.lerp(p.pitchRate, controls.pitch * 1.5, 0.08);
      p.yawRate = THREE.MathUtils.lerp(p.yawRate, controls.roll * 0.3, 0.05);

      const rotationStep = new THREE.Euler(p.pitchRate * delta, 0, p.rollRate * delta, 'YXZ');
      // Actually apply continuous rotation based on rates
      p.rotation.y += p.yawRate * delta * 2;
      p.rotation.x += p.pitchRate * delta * 2;
      p.rotation.z += p.rollRate * delta * 2;

      // Auto-level tendency (stability)
      if (Math.abs(controls.roll) < 0.01) {
        p.rotation.z *= 0.98;
        p.rollRate *= 0.95;
      }
      if (Math.abs(controls.pitch) < 0.01) {
        p.rotation.x *= 0.98;
        p.pitchRate *= 0.95;
      }

      // Update plane mesh
      p.altitude = p.position.y;
      planeMesh.position.copy(p.position);
      planeMesh.rotation.copy(p.rotation);

      // Check terrain collision (simple)
      // Look up terrain height at plane position
      // Note: we don't have exact terrain lookup here but we approximate with distance-based height or raycast for simplicity.
      // For fidelity, we'll do a simple raycast down.
      const raycaster = new THREE.Raycaster();
      const down = new THREE.Vector3(0, -1, 0);
      raycaster.set(p.position, down);
      const hits = raycaster.intersectObject(terrain);
      if (hits.length > 0) {
        const groundHeight = hits[0].point.y;
        if (p.position.y < groundHeight + 3) {
          p.position.y = groundHeight + 3;
          p.velocity.y = 0;
          p.velocity.multiplyScalar(0.95); // Ground friction
        }
      }

      // Update camera (follow plane with lag)
      const offset = new THREE.Vector3(0, 10, 30);
      offset.applyQuaternion(p.rotation);
      const targetCamPos = p.position.clone().add(offset);
      camera.position.lerp(targetCamPos, 0.1);
      camera.lookAt(p.position);
    }

    // Update HUD text
    function updateHUD() {
      const ac = AIRCRAFT_DB[selectedPlaneIndex];
      document.getElementById('plane-name').textContent = ac.name;
      document.getElementById('spec-thrust').textContent = "Thrust: " + (ac.thrustMax/1000).toFixed(0) + " kN";
      document.getElementById('spec-weight').textContent = "Weight: " + (ac.mass/1000).toFixed(1) + " t";
      document.getElementById('spec-lift').textContent = "Lift Coeff: " + ac.liftCoeff.toFixed(2);
      document.getElementById('spec-drag').textContent = "Drag Coeff: " + ac.dragCoeff.toFixed(2);

      document.getElementById('val-speed').textContent = planePhysics ? Math.round(planePhysics.airspeed) : 0;
      document.getElementById('val-alt').textContent = planePhysics ? Math.round(planePhysics.altitude) : 0;
      document.getElementById('val-pitch').textContent = planePhysics ? THREE.MathUtils.radToDeg(planePhysics.rotation.x).toFixed(0) : 0;
      document.getElementById('val-roll').textContent = planePhysics ? THREE.MathUtils.radToDeg(planePhysics.rotation.z).toFixed(0) : 0;
      document.getElementById('val-aoa').textContent = planePhysics ? THREE.MathUtils.radToDeg(planePhysics.aoa).toFixed(0) : 0;
      document.getElementById('val-throttle').textContent = Math.round(controls.thrust * 100);
    }

    // Audio generation using Web Audio API
    function startAudio() {
      if (audioStarted) return;
      audioStarted = true;

      try {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
      } catch(e) { return; }

      // Engine sound: multiple detuned oscillators for richness
      engineOsc = [];
      engineGain = [];
      for(let i=0; i<3; i++) {
        const osc = audioCtx.createOscillator();
        const gain = audioCtx.createGain();
        osc.type = 'sawtooth';
        osc.frequency.setValueAtTime(60 + i*10, audioCtx.currentTime);
        osc.connect(gain);
        gain.connect(audioCtx.destination);
        osc.start();
        engineOsc.push(osc);
        engineGain.push(gain);
        gain.gain.setValueAtTime(0, audioCtx.currentTime);
      }

      // Wind noise
      windOsc = audioCtx.createOscillator();
      windGain = audioCtx.createGain();
      windOsc.type = 'triangle';
      windOsc.frequency.setValueAtTime(120, audioCtx.currentTime);
      windOsc.connect(windGain);
      windGain.connect(audioCtx.destination);
      windOsc.start();
      windGain.gain.setValueAtTime(0.05, audioCtx.currentTime);
    }

    function updateAudio() {
      if (!audioStarted || !audioCtx) return;
      const ac = AIRCRAFT_DB[selectedPlaneIndex];
      const p = planePhysics;

      // Engine pitch and volume based on throttle and airspeed
      const throttleFactor = controls.thrust;
      const rpmFactor = throttleFactor * 0.8 + (p.airspeed / ac.maxSpeed) * 0.4;
      const baseFreq = 60 + rpmFactor * 220;
      const volume = Math.min(0.4, throttleFactor * 0.6 + (p.airspeed / 500) * 0.1);

      engineOsc.forEach((osc, i) => {
        osc.frequency.linearRampToValueAtTime(baseFreq + i*15, audioCtx.currentTime + 0.05);
        engineGain[i].gain.linearRampToValueAtTime(volume * (0.7 + i*0.15), audioCtx.currentTime + 0.05);
      });

      // Wind noise increases with speed and altitude
      const windVol = Math.min(0.3, (p.airspeed / 600) * 0.2 + (p.altitude / 200) * 0.05);
      windGain.gain.linearRampToValueAtTime(windVol, audioCtx.currentTime + 0.05);
    }

    // Menu logic
    function selectPlane(index) {
      selectedPlaneIndex = index;
      document.querySelectorAll('.plane-card').forEach((el, i) => {
        el.classList.toggle('active', i === index);
      });
      // Update aircraft if already running
      if (planeMesh) {
        buildAircraft(index);
        planePhysics.velocity.set(0,0,-20);
      }
    }

    function startScenario(type) {
      currentScenario = type;
      document.getElementById('main-menu').classList.add('hidden');

      if (type === 'landing') {
        document.getElementById('scenario-title').textContent = "Mission: Precision Landing";
        document.getElementById('scenario-desc').textContent = "Maintain 15° pitch on final approach. Throttle below 30% for touchdown.";
        planePhysics.position.set(0, 120, -300);
        planePhysics.velocity.set(0, -5, 60);
        planePhysics.rotation.set(0, 0, 0);
        controls.thrust = 0.25;
        controls.pitch = 0.15;
      } else {
        document.getElementById('scenario-title').textContent = "Mission: Dogfight Maneuvers";
        document.getElementById('scenario-desc').textContent = "Execute high-G turns. Watch angle of attack to avoid stall.";
        planePhysics.position.set(-100, 200, 0);
        planePhysics.velocity.set(60, 0, 0);
        planePhysics.rotation.set(0, Math.PI/2, 0);
        controls.thrust = 0.8;
      }

      buildAircraft(selectedPlaneIndex);
    }

    function onResize() {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    }

    // Main loop
    function animate() {
      requestAnimationFrame(animate);
      const dt = Math.min(clock.getDelta(), 0.05); // Cap delta to avoid physics explosions

      updatePhysics(dt);
      updateHUD();
      updateAudio();
      renderer.render(scene, camera);
    }

    // Initialize after DOM load
    window.addEventListener('DOMContentLoaded', () => {
      init();
    });

    // Expose functions for HTML onclick
    window.selectPlane = selectPlane;
    window.startScenario = startScenario;

  </script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"basic","sessionId":"8e32c323-fcfe-4c95-97c3-da99e738f6a5","tokensIn":1205,"tokensOut":9083,"tokensTotal":10288,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T20:39:12.011Z"} -->