← Flight Simulator results

Flight Simulator

thinkingcap qwen3.6 27b · composer

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 78,493 bytes · SHA-256 279f0981cf68
<!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>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; font-family: 'Segoe UI', Arial, sans-serif; color: #fff; }
#menu { position: fixed; inset: 0; z-index: 100; display: flex; flex-direction: column; align-items: center; justify-content: center; background: linear-gradient(135deg, #0a0e27 0%, #1a1a3e 40%, #0d1b2a 100%); }
#menu h1 { font-size: 4em; letter-spacing: 0.3em; text-shadow: 0 0 30px rgba(0,150,255,0.5), 0 0 60px rgba(0,150,255,0.2); margin-bottom: -10px; }
.subtitle { font-size: 1.2em; color: #88aacc; letter-spacing: 0.5em; margin-bottom: 40px; }
#aircraft-select { display: flex; gap: 20px; margin-bottom: 30px; }
.aircraft-card { width: 200px; padding: 20px; border: 2px solid #334; border-radius: 12px; cursor: pointer; transition: all 0.3s; background: rgba(255,255,255,0.03); text-align: center; }
.aircraft-card:hover { border-color: #4af; transform: translateY(-5px); box-shadow: 0 10px 30px rgba(0,100,255,0.2); }
.aircraft-card.selected { border-color: #4af; background: rgba(0,100,255,0.1); box-shadow: 0 0 20px rgba(0,100,255,0.3); }
.aircraft-card h3 { color: #4af; margin-bottom: 8px; font-size: 1.1em; }
.aircraft-card p { font-size: 0.75em; color: #99aabb; line-height: 1.6; }
.stat-bar { height: 4px; background: #223; border-radius: 2px; margin-top: 4px; overflow: hidden; }
.stat-fill { height: 100%; background: linear-gradient(90deg, #4af, #4f8); border-radius: 2px; transition: width 0.5s; }
#scenario-select { display: flex; gap: 20px; margin-bottom: 30px; }
.scenario-card { padding: 15px 30px; border: 2px solid #334; border-radius: 8px; cursor: pointer; transition: all 0.3s; background: rgba(255,255,255,0.03); }
.scenario-card:hover { border-color: #fa4; }
.scenario-card.selected { border-color: #fa4; background: rgba(255,170,0,0.1); }
#start-btn { padding: 15px 60px; font-size: 1.3em; letter-spacing: 0.2em; border: 2px solid #4af; background: transparent; color: #fff; cursor: pointer; transition: all 0.3s; border-radius: 8px; }
#start-btn:not(:disabled) { cursor: pointer; }
#start-btn:hover:not(:disabled) { background: rgba(0,100,255,0.2); box-shadow: 0 0 30px rgba(0,100,255,0.3); }
#start-btn:disabled { opacity: 0.3; cursor: not-allowed; }
#controls-info { margin-top: 30px; padding: 15px 30px; background: rgba(255,255,255,0.05); border-radius: 8px; font-size: 0.8em; color: #7799aa; text-align: center; line-height: 1.8; }
#hud { position: fixed; inset: 0; z-index: 50; pointer-events: none; }
#crosshair { position: fixed; top: 50%; left: 50%; transform: translate(-50%,-50%); color: rgba(255,255,255,0.6); font-size: 24px; z-index: 51; pointer-events: none; }
#message-overlay { position: fixed; top: 20%; left: 50%; transform: translateX(-50%); color: #fff; font-size: 1.5em; text-shadow: 0 0 10px rgba(0,0,0,0.8); z-index: 60; pointer-events: none; opacity: 0; transition: opacity 0.5s; }
</style>
</head>
<body>
<div id="menu">
  <h1>APEX AERO</h1>
  <p class="subtitle">Flight Simulator</p>
  <div id="aircraft-select"></div>
  <div id="scenario-select"></div>
  <button id="start-btn" disabled="">LAUNCH SIMULATION</button>
  <div id="controls-info">
    <h3>Controls</h3>
    <p><b>W/S</b> Pitch | <b>A/D</b> Roll | <b>Q/E</b> Yaw | <b>Shift/Ctrl</b> Throttle Up/Down</p>
    <p><b>Space</b> Brakes | <b>R</b> Reset Position | <b>TAB</b> Toggle HUD</p>
  </div>
</div>
<div id="hud" style="display:none;">
  <canvas id="hud-canvas"></canvas>
</div>
<div id="crosshair">+</div>
<div id="message-overlay"></div>
<script>
'use strict';

// ── const: AIRCRAFT_DATA ──
const AIRCRAFT_DATA = {
  falcon: {
    name: "Falcon X1",
    description: "Light fighter - agile and fast",
    color: 0x4488ff,
    accentColor: 0xffaa00,
    mass: 5200,
    wingArea: 32,
    maxThrust: 18000,
    liftCoeffMax: 1.8,
    dragProfile: 0.025,
    dragInduced: 0.04,
    rollRate: 2.5,
    pitchRate: 1.8,
    yawRate: 1.2,
    stallSpeed: 65,
    maxG: 9,
    engineResponse: 0.03,
    stats: { speed: 75, agility: 90, power: 65 }
  },
  thunder: {
    name: "Thunder MK-III",
    description: "Heavy interceptor - powerful and stable",
    color: 0xff4444,
    accentColor: 0x88ff88,
    mass: 12500,
    wingArea: 55,
    maxThrust: 45000,
    liftCoeffMax: 1.6,
    dragProfile: 0.03,
    dragInduced: 0.035,
    rollRate: 1.8,
    pitchRate: 1.4,
    yawRate: 0.9,
    stallSpeed: 75,
    maxG: 7,
    engineResponse: 0.02,
    stats: { speed: 60, agility: 55, power: 95 }
  },
  phantom: {
    name: "Phantom Stealth",
    description: "Stealth bomber - balanced and efficient",
    color: 0x333344,
    accentColor: 0xaa44ff,
    mass: 8800,
    wingArea: 45,
    maxThrust: 28000,
    liftCoeffMax: 1.7,
    dragProfile: 0.02,
    dragInduced: 0.038,
    rollRate: 2.2,
    pitchRate: 1.6,
    yawRate: 1.0,
    stallSpeed: 70,
    maxG: 8,
    engineResponse: 0.025,
    stats: { speed: 70, agility: 75, power: 75 }
  }
};

// ── const: SCENARIOS ──
const SCENARIOS = {
  takeoff: { name: "Takeoff & Landing", description: "Precision flight on the runway" },
  dogfight: { name: "Dogfight", description: "High-speed aerial combat" }
};

// ── const: RHO0 ──
// Sea level air density (kg/m^3)
const RHO0 = 1.225;

// ── const: SCALE_HEIGHT ──
// Scale height for atmosphere model
const SCALE_HEIGHT = 8500;

// ── class: FlightPhysics ──
class FlightPhysics {
  constructor(acData) {
    // Aircraft properties
    this.mass = acData.mass;
    this.wingArea = acData.wingArea;
    this.maxThrust = acData.maxThrust;
    this.liftCoeffMax = acData.liftCoeffMax;
    this.dragProfile = acData.dragProfile;
    this.dragInduced = acData.dragInduced;
    this.rollRate = acData.rollRate;
    this.pitchRate = acData.pitchRate;
    this.yawRate = acData.yawRate;
    this.stallSpeed = acData.stallSpeed;
    this.maxG = acData.maxG;
    this.engineResponseTime = acData.engineResponse;

    // State
    this.position = new THREE.Vector3(0, 50, 0);
    this.velocity = new THREE.Vector3(0, 0, 0);
    this.quaternion = new THREE.Quaternion();
    this.angularVelocity = new THREE.Vector3(0, 0, 0);

    // Control inputs (normalized -1 to 1)
    this.pitchInput = 0;
    this.rollInput = 0;
    this.yawInput = 0;
    this.throttleTarget = 0;
    this.brakes = false;

    // Engine state
    this.currentThrust = 0;
    this.rpm = 0;

    // Derived values for HUD
    this.airspeed = 0;
    this.altitude = 0;
    this.verticalSpeed = 0;
    this.gLoad = 1;
    this.angleOfAttack = 0;
    this.airDensity = RHO0;
    this.isStalled = false;
  }

  reset(x, y, z, heading) {
    this.position.set(x, y, z);
    this.velocity.set(0, 0, 0);
    this.quaternion.setFromAxisAngle(new THREE.Vector3(0, 1, 0), -heading);
    this.angularVelocity.set(0, 0, 0);
    this.currentThrust = 0;
    this.rpm = 0;
    this.throttleTarget = 0;
  }

  getAirDensity(altitude) {
    return RHO0 * Math.exp(-Math.max(0, altitude) / SCALE_HEIGHT);
  }

  update(dt) {
    dt = Math.min(dt, 0.05); // cap delta to prevent physics explosions

    // Engine response (smooth throttle changes)
    const thrustDiff = this.throttleTarget - (this.currentThrust / this.maxThrust);
    const engineRate = this.engineResponseTime * 60;
    if (Math.abs(thrustDiff) > 0.001) {
      const step = Math.sign(thrustDiff) * Math.min(Math.abs(thrustDiff), engineRate * dt);
      this.currentThrust = (this.currentThrust / this.maxThrust + step) * this.maxThrust;
    }

    // RPM proportional to thrust percentage
    this.rpm = THREE.MathUtils.lerp(this.rpm, this.throttleTarget, 0.1);

    // Air density at current altitude
    this.airDensity = this.getAirDensity(this.position.y);

    // Transform velocity to aircraft local frame
    const bodyVel = new THREE.Vector3();
    this.velocity.clone().applyQuaternion(this.quaternion.clone().invert());

    // Airspeed (forward component magnitude)
    this.airspeed = Math.sqrt(
      bodyVel.x * bodyVel.x + bodyVel.y * bodyVel.y + bodyVel.z * bodyVel.z
    );

    // Angle of attack approximation
    if (this.airspeed > 1) {
      this.angleOfAttack = Math.atan2(bodyVel.y, Math.max(bodyVel.x, 0.1));
    } else {
      this.angleOfAttack = 0;
    }

    // Stall detection
    this.isStalled = this.airspeed < this.stallSpeed && this.position.y > 5;

    // --- Force calculations ---
    const rho = this.airDensity;
    const S = this.wingArea;
    const V = this.airspeed;
    const m = this.mass;
    const g = 9.81;

    // Lift coefficient based on angle of attack (simplified linear region)
    let Cl = Math.max(-0.5, Math.min(this.liftCoeffMax, this.angleOfAttack * 4));
    if (this.isStalled) {
      Cl *= 0.3; // drastic lift reduction in stall
    }

    // Lift force: L = 0.5 * rho * V^2 * S * Cl
    const liftForce = 0.5 * rho * V * V * S * Cl;

    // Drag: profile drag + induced drag
    const CdProfile = this.dragProfile;
    const CdInduced = this.dragInduced * Cl * Cl;
    const totalDrag = 0.5 * rho * V * V * S * (CdProfile + CdInduced);

    // Ground friction when on ground
    let groundFriction = 0;
    if (this.position.y < 3 && !this.brakes) {
      groundFriction = totalDrag * 2;
    } else if (this.brakes) {
      groundFriction = m * g * 0.6; // strong braking force
    }

    // Build force vectors in aircraft local frame then transform to world
    const forcesLocal = new THREE.Vector3();

    // Thrust along +X axis (forward)
    forcesLocal.x += this.currentThrust;

    // Drag along -X axis (backward)
    forcesLocal.x -= totalDrag;

    // Ground friction along -X
    forcesLocal.x -= groundFriction;

    // Lift along +Y axis (up in aircraft frame)
    forcesLocal.y += liftForce;

    // Weight along -Y world axis
    const weight = m * g;

    // Transform forces to world space
    const thrustWorld = new THREE.Vector3();
    forcesLocal.applyQuaternion(this.quaternion);

    // Total force in world space (weight is already in world space)
    const totalForce = new THREE.Vector3(
      thrustWorld.x,
      thrustWorld.y - weight + thrustWorld.z, // Y is up in our convention... 
      thrustWorld.z
    );

    // Actually let me redo this more carefully. In Three.js:
    // Aircraft forward = local +X (after rotation)
    // Aircraft up = local +Y (after rotation)  
    // Aircraft right = local +Z (after rotation)
    // World up = +Y

    const forwardDir = new THREE.Vector3(1, 0, 0).applyQuaternion(this.quaternion);
    const upDir = new THREE.Vector3(0, 1, 0).applyQuaternion(this.quaternion);

    // Thrust in forward direction
    const thrustVec = forwardDir.clone().multiplyScalar(this.currentThrust);

    // Drag opposite to velocity
    let dragVec = new THREE.Vector3();
    if (V > 0.1) {
      dragVec = this.velocity.clone().normalize().multiplyScalar(-totalDrag);
    }

    // Lift in aircraft up direction
    const liftVec = upDir.clone().multiplyScalar(liftForce);

    // Weight downward
    const weightVec = new THREE.Vector3(0, -weight, 0);

    // Ground friction opposite to velocity
    let frictionVec = new THREE.Vector3();
    if (groundFriction > 0 && V > 0.1) {
      frictionVec = this.velocity.clone().normalize().multiplyScalar(-groundFriction);
    }

    // Net force
    const netForce = new THREE.Vector3()
      .add(thrustVec)
      .add(dragVec)
      .add(liftVec)
      .add(weightVec)
      .add(frictionVec);

    // Acceleration = F/m
    const acceleration = netForce.divideScalar(m);

    // Update velocity
    this.velocity.add(acceleration.multiplyScalar(dt));

    // Ground collision
    if (this.position.y < 2) {
      this.position.y = 2;
      if (this.velocity.y < -5) {
        // Hard landing damage indicator
        this.gLoad = Math.abs(this.velocity.y) / g * 3;
      } else {
        this.velocity.y = Math.max(0, this.velocity.y);
      }
    }

    // Update position
    this.position.add(this.velocity.clone().multiplyScalar(dt));

    // --- Angular dynamics (attitude control) ---
    const rollTarget = -this.rollInput * this.rollRate;
    const pitchTarget = -this.pitchInput * this.pitchRate;
    const yawTarget = this.yawInput * this.yawRate;

    // Smooth angular velocity changes
    const angDamp = 0.92;
    const angResponse = Math.min(1, dt * 5);

    this.angularVelocity.x = THREE.MathUtils.lerp(this.angularVelocity.x, pitchTarget, angResponse);
    this.angularVelocity.y = THREE.MathUtils.lerp(this.angularVelocity.y, yawTarget, angResponse);
    this.angularVelocity.z = THREE.MathUtils.lerp(this.angularVelocity.z, rollTarget, angResponse);

    // Apply angular damping
    this.angularVelocity.multiplyScalar(angDamp);

    // Update orientation from angular velocity
    const deltaQuat = new THREE.Quaternion().setFromEuler(
      new THREE.Euler(
        this.angularVelocity.x * dt,
        this.angularVelocity.y * dt,
        this.angularVelocity.z * dt,
        'XYZ'
      )
    );
    this.quaternion.multiply(deltaQuat).normalize();

    // Auto-level when no input (gentle)
    if (Math.abs(this.rollInput) < 0.1 && Math.abs(this.pitchInput) < 0.1) {
      const levelQuat = new THREE.Quaternion().setFromAxisAngle(
        new THREE.Vector3(1, 0, 0), -this.angularVelocity.x * dt * 0.5
      );
      this.quaternion.multiply(levelQuat).normalize();
    }

    // G-load calculation (normal acceleration)
    const normalAccel = Math.sqrt(acceleration.y * acceleration.y + 
                                   acceleration.x * acceleration.x + 
                                   acceleration.z * acceleration.z);
    this.gLoad = 1 + normalAccel / g;
    if (this.isStalled) this.gLoad *= 0.5;

    // Vertical speed in m/s
    this.verticalSpeed = this.velocity.y;

    // Altitude
    this.altitude = Math.max(0, this.position.y);
  }
}

// ── class: TerrainNoise ──
// Simplex-like noise implementation for terrain generation
class TerrainNoise {
  constructor(seed) {
    this.seed = seed || Math.random() * 65536;
    this.perm = new Uint8Array(512);
    this.grad3 = [
      [1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
      [1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
      [0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]
    ];
    this._initPerm();
  }

  _initPerm() {
    const p = new Uint8Array(256);
    for (let i = 0; i < 256; i++) p[i] = i;
    // Fisher-Yates shuffle with seed
    let s = this.seed;
    for (let i = 255; i > 0; i--) {
      s = (s * 16807 + 0) % 2147483647;
      const j = s % (i + 1);
      [p[i], p[j]] = [p[j], p[i]];
    }
    for (let i = 0; i < 512; i++) this.perm[i] = p[i & 255];
  }

  _dot(g, x, y) { return g[0]*x + g[1]*y; }

  noise(xin, yin) {
    const F2 = 0.5 * (Math.sqrt(3) - 1);
    const G2 = (3 - Math.sqrt(3)) / 6;
    const s = (xin + yin) * F2;
    const i = Math.floor(xin + s);
    const j = Math.floor(yin + s);
    const t = (i + j) * G2;
    const X0 = i - t, Y0 = j - t;
    const x0 = xin - X0, y0 = yin - Y0;
    let i1, j1;
    if (x0 > y0) { i1 = 1; j1 = 0; } else { i1 = 0; j1 = 1; }
    const x1 = x0 - i1 + G2, y1 = y0 - j1 + G2;
    const x2 = x0 - 1 + 2*G2, y2 = y0 - 1 + 2*G2;
    const ii = i & 255, jj = j & 255;
    const gi0 = this.perm[ii + this.perm[jj]] % 12;
    const gi1 = this.perm[ii + i1 + this.perm[jj + j1]] % 12;
    const gi2 = this.perm[ii + 1 + this.perm[jj + 1]] % 12;
    let n0, n1, n2;
    let t0 = 0.5 - x0*x0 - y0*y0;
    if (t0 < 0) n0 = 0;
    else { t0 *= t0; n0 = t0 * t0 * this._dot(this.grad3[gi0], x0, y0); }
    let t1 = 0.5 - x1*x1 - y1*y1;
    if (t1 < 0) n1 = 0;
    else { t1 *= t1; n1 = t1 * t1 * this._dot(this.grad3[gi1], x1, y1); }
    let t2 = 0.5 - x2*x2 - y2*y2;
    if (t2 < 0) n2 = 0;
    else { t2 *= t2; n2 = t2 * t2 * this._dot(this.grad3[gi2], x2, y2); }
    return 70 * (n0 + n1 + n2);
  }

  fbm(x, y, octaves) {
    let val = 0, amp = 1, freq = 1, max = 0;
    for (let i = 0; i < octaves; i++) {
      val += this.noise(x * freq, y * freq) * amp;
      max += amp;
      amp *= 0.5;
      freq *= 2;
    }
    return val / max;
  }

  getHeight(worldX, worldZ) {
    const scale = 0.0015;
    let h = this.fbm(worldX * scale, worldZ * scale, 6);
    // Add ridges
    const ridge = 1 - Math.abs(this.fbm(worldX * scale * 2 + 5, worldZ * scale * 2 + 5, 4));
    h += ridge * ridge * 0.4;
    // Flatten near origin for runway area
    const distFromOrigin = Math.sqrt(worldX*worldX + worldZ*worldZ);
    if (distFromOrigin < 300) {
      const flattenFactor = distFromOrigin / 300;
      h *= flattenFactor * flattenFactor;
    }
    return Math.max(0, h * 250);
  }
}

// ── class: TerrainRenderer ──
class TerrainRenderer {
  constructor(scene, noise) {
    this.scene = scene;
    this.noise = noise;
    this.chunks = new Map();
    this.chunkSize = 500;
    this.chunkRes = 80;
    this.renderDistance = 6; // chunks in each direction
    this.lastPlayerChunk = null;
    this.meshes = [];
  }

  _chunkKey(cx, cz) { return `${cx},${cz}`; }

  _createChunkMesh(cx, cz) {
    const size = this.chunkSize;
    const res = this.chunkRes;
    const geometry = new THREE.PlaneGeometry(size, size, res, res);
    geometry.rotateX(-Math.PI / 2);

    const positions = geometry.attributes.position.array;
    const colors = new Float32Array(positions.length);
    const halfSize = size / 2;
    const worldOX = cx * size;
    const worldOZ = cz * size;

    for (let i = 0; i < positions.length; i += 3) {
      const wx = positions[i] + worldOX;
      const wz = positions[i + 2] + worldOZ;
      const h = this.noise.getHeight(wx, wz);
      positions[i + 1] = h;

      // Color based on height with more variation
      const t = Math.min(h / 200, 1);
      const noiseVal = (Math.sin(wx * 0.05) * Math.cos(wz * 0.07)) * 0.05;
      
      if (h < 3) {
        // Water/beach - sandy color
        colors[i] = 0.72 + noiseVal; 
        colors[i+1] = 0.68 + noiseVal; 
        colors[i+2] = 0.45 + noiseVal;
      } else if (h < 25) {
        // Low grass - vibrant green with variation
        const gVar = Math.sin(wx * 0.1 + wz * 0.08) * 0.05;
        colors[i] = 0.12 + noiseVal; 
        colors[i+1] = 0.35 + gVar; 
        colors[i+2] = 0.08 + noiseVal;
      } else if (h < 70) {
        // Forest - darker green/brown mix
        const fVar = Math.sin(wx * 0.06 - wz * 0.1) * 0.04;
        colors[i] = 0.08 + noiseVal; 
        colors[i+1] = 0.22 + fVar; 
        colors[i+2] = 0.05 + noiseVal;
      } else if (h < 130) {
        // Rock - gray with variation
        const r = 0.32 + Math.sin(wx * 0.08 + wz * 0.06) * 0.06;
        colors[i] = r; 
        colors[i+1] = r + 0.02; 
        colors[i+2] = r + 0.04;
      } else if (h < 175) {
        // Snow line transition
        const s = Math.min((h - 130) / 45, 1);
        colors[i] = 0.32 + s * 0.6 + noiseVal; 
        colors[i+1] = 0.32 + s * 0.6 + noiseVal; 
        colors[i+2] = 0.38 + s * 0.57 + noiseVal;
      } else {
        // Snow peak - bright white
        colors[i] = 0.92 + noiseVal; 
        colors[i+1] = 0.94 + noiseVal; 
        colors[i+2] = 0.96 + noiseVal;
      }
    }

    geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    geometry.computeVertexNormals();

    const material = new THREE.MeshStandardMaterial({
      vertexColors: true,
      roughness: 0.85,
      metalness: 0.1,
      flatShading: false
    });

    const mesh = new THREE.Mesh(geometry, material);
    mesh.position.set(worldOX, 0, worldOZ);
    return mesh;
  }

  update(playerX, playerZ) {
    const pcx = Math.round(playerX / this.chunkSize);
    const pcz = Math.round(playerZ / this.chunkSize);

    if (pcx === this.lastPlayerChunk?.cx && pcz === this.lastPlayerChunk?.cz) return;
    this.lastPlayerChunk = { cx: pcx, cz: pcz };

    // Determine needed chunks
    const needed = new Set();
    for (let dx = -this.renderDistance; dx <= this.renderDistance; dx++) {
      for (let dz = -this.renderDistance; dz <= this.renderDistance; dz++) {
        if (dx*dx + dz*dz > this.renderDistance * this.renderDistance + 1) continue;
        needed.add(this._chunkKey(pcx + dx, pcz + dz));
      }
    }

    // Remove far chunks
    for (const [key, mesh] of this.chunks) {
      if (!needed.has(key)) {
        this.scene.remove(mesh);
        mesh.geometry.dispose();
        mesh.material.dispose();
        this.chunks.delete(key);
      }
    }

    // Add new chunks
    for (const key of needed) {
      if (!this.chunks.has(key)) {
        const [cx, cz] = key.split(',').map(Number);
        const mesh = this._createChunkMesh(cx, cz);
        this.scene.add(mesh);
        this.chunks.set(key, mesh);
      }
    }
  }

  getHeightAt(x, z) {
    return this.noise.getHeight(x, z);
  }

  dispose() {
    for (const [, mesh] of this.chunks) {
      this.scene.remove(mesh);
      mesh.geometry.dispose();
      mesh.material.dispose();
    }
    this.chunks.clear();
  }
}

// ── class: AircraftModel ──
class AircraftModel {
  static createFalcon(color, accent) {
    const group = new THREE.Group();
    
    // Fuselage - sleek pointed body
    const fuselageGeo = new THREE.CylinderGeometry(0.4, 0.6, 8, 8);
    const fuselageMat = new THREE.MeshStandardMaterial({ color, roughness: 0.3, metalness: 0.7 });
    const fuselage = new THREE.Mesh(fuselageGeo, fuselageMat);
    fuselage.rotation.z = Math.PI / 2;
    group.add(fuselage);

    // Nose cone
    const noseGeo = new THREE.ConeGeometry(0.4, 2, 8);
    const nose = new THREE.Mesh(noseGeo, fuselageMat);
    nose.rotation.z = -Math.PI / 2;
    nose.position.x = 5;
    group.add(nose);

    // Cockpit canopy
    const cockpitGeo = new THREE.SphereGeometry(0.45, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
    const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x88ccff, roughness: 0.1, metalness: 0.9, transparent: true, opacity: 0.7 });
    const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
    cockpit.position.set(2, 0.35, 0);
    group.add(cockpit);

    // Main wings - swept back
    const wingShape = new THREE.Shape();
    wingShape.moveTo(0, 0);
    wingShape.lineTo(-1.5, 6);
    wingShape.lineTo(-2, 6);
    wingShape.lineTo(-0.5, 0);
    
    const wingExtrudeSettings = { depth: 0.08, bevelEnabled: true, bevelThickness: 0.02, bevelSize: 0.02 };
    const wingGeo = new THREE.ExtrudeGeometry(wingShape, wingExtrudeSettings);
    const wingMat = new THREE.MeshStandardMaterial({ color, roughness: 0.4, metalness: 0.6 });
    
    const leftWing = new THREE.Mesh(wingGeo, wingMat);
    leftWing.position.set(1, -0.3, 0);
    group.add(leftWing);

    const rightWing = new THREE.Mesh(wingGeo, wingMat);
    rightWing.position.set(1, -0.3, 0);
    rightWing.scale.z = -1;
    group.add(rightWing);

    // Tail wings (horizontal stabilizers)
    const tailShape = new THREE.Shape();
    tailShape.moveTo(0, 0);
    tailShape.lineTo(-0.8, 2.5);
    tailShape.lineTo(-1.2, 2.5);
    tailShape.lineTo(-0.3, 0);
    
    const tailGeo = new THREE.ExtrudeGeometry(tailShape, { depth: 0.06, bevelEnabled: false });
    const leftTail = new THREE.Mesh(tailGeo, wingMat);
    leftTail.position.set(-3.5, -0.2, 0);
    group.add(leftTail);

    const rightTail = new THREE.Mesh(tailGeo, wingMat);
    rightTail.position.set(-3.5, -0.2, 0);
    rightTail.scale.z = -1;
    group.add(rightTail);

    // Vertical stabilizer (tail fin)
    const vStabShape = new THREE.Shape();
    vStabShape.moveTo(0, 0);
    vStabShape.lineTo(-1.5, 2.5);
    vStabShape.lineTo(-2, 2);
    vStabShape.lineTo(-0.5, 0);
    
    const vStabGeo = new THREE.ExtrudeGeometry(vStabShape, { depth: 0.06, bevelEnabled: false });
    const vStab = new THREE.Mesh(vStabGeo, wingMat);
    vStab.position.set(-3.5, 0.2, -0.03);
    group.add(vStab);

    // Engine exhaust glow
    const exhaustGeo = new THREE.CylinderGeometry(0.15, 0.25, 0.5, 8);
    const exhaustMat = new THREE.MeshStandardMaterial({ color: accent, emissive: accent, emissiveIntensity: 0.5 });
    const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);
    exhaust.rotation.z = Math.PI / 2;
    exhaust.position.x = -4.2;
    group.add(exhaust);

    // Engine glow light
    const engineLight = new THREE.PointLight(accent, 0.5, 10);
    engineLight.position.set(-4.5, 0, 0);
    group.add(engineLight);
    group.userData.engineLight = engineLight;

    return group;
  }

  static createThunder(color, accent) {
    const group = new THREE.Group();

    // Larger fuselage
    const fuselageGeo = new THREE.CylinderGeometry(0.7, 1.0, 12, 8);
    const fuselageMat = new THREE.MeshStandardMaterial({ color, roughness: 0.35, metalness: 0.6 });
    const fuselage = new THREE.Mesh(fuselageGeo, fuselageMat);
    fuselage.rotation.z = Math.PI / 2;
    group.add(fuselage);

    // Nose
    const noseGeo = new THREE.ConeGeometry(0.7, 3, 8);
    const nose = new THREE.Mesh(noseGeo, fuselageMat);
    nose.rotation.z = -Math.PI / 2;
    nose.position.x = 7.5;
    group.add(nose);

    // Cockpit
    const cockpitGeo = new THREE.SphereGeometry(0.6, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
    const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x44aa44, roughness: 0.1, metalness: 0.9, transparent: true, opacity: 0.7 });
    const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
    cockpit.position.set(3.5, 0.5, 0);
    group.add(cockpit);

    // Broad wings
    const wingShape = new THREE.Shape();
    wingShape.moveTo(0, 0);
    wingShape.lineTo(-1, 8);
    wingShape.lineTo(-2.5, 8);
    wingShape.lineTo(-0.8, 0);
    
    const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.12, bevelEnabled: true, bevelThickness: 0.03, bevelSize: 0.03 });
    const wingMat = new THREE.MeshStandardMaterial({ color, roughness: 0.45, metalness: 0.5 });

    const leftWing = new THREE.Mesh(wingGeo, wingMat);
    leftWing.position.set(1, -0.5, 0);
    group.add(leftWing);

    const rightWing = new THREE.Mesh(wingGeo, wingMat);
    rightWing.position.set(1, -0.5, 0);
    rightWing.scale.z = -1;
    group.add(rightWing);

    // Twin vertical stabilizers
    for (let side of [-1, 1]) {
      const vStabShape = new THREE.Shape();
      vStabShape.moveTo(0, 0);
      vStabShape.lineTo(-2, 3.5);
      vStabShape.lineTo(-2.8, 3);
      vStabShape.lineTo(-0.8, 0);
      
      const vStabGeo = new THREE.ExtrudeGeometry(vStabShape, { depth: 0.08, bevelEnabled: false });
      const vStab = new THREE.Mesh(vStabGeo, wingMat);
      vStab.position.set(-5, 0.3, side * 1.2 - side * 0.04);
      group.add(vStab);
    }

    // Twin engines
    for (let side of [-1, 1]) {
      const engGeo = new THREE.CylinderGeometry(0.25, 0.35, 1, 8);
      const engMat = new THREE.MeshStandardMaterial({ color: accent, emissive: accent, emissiveIntensity: 0.4 });
      const eng = new THREE.Mesh(engGeo, engMat);
      eng.rotation.z = Math.PI / 2;
      eng.position.set(-6, -0.3, side * 1.5);
      group.add(eng);

      const elight = new THREE.PointLight(accent, 0.4, 8);
      elight.position.set(-6.5, -0.3, side * 1.5);
      group.add(elight);
    }
    group.userData.engineLight = group.children[group.children.length - 1];

    return group;
  }

  static createPhantom(color, accent) {
    const group = new THREE.Group();

    // Angular stealth fuselage
    const bodyGeo = new THREE.BoxGeometry(9, 0.8, 2);
    const bodyMat = new THREE.MeshStandardMaterial({ color, roughness: 0.5, metalness: 0.4 });
    const body = new THREE.Mesh(bodyGeo, bodyMat);
    group.add(body);

    // Pointed nose
    const noseGeo = new THREE.ConeGeometry(0.6, 3, 4);
    const nose = new THREE.Mesh(noseGeo, bodyMat);
    nose.rotation.z = -Math.PI / 2;
    nose.position.x = 6;
    group.add(nose);

    // Cockpit slit
    const cockpitGeo = new THREE.BoxGeometry(1.5, 0.3, 0.8);
    const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x6644aa, roughness: 0.1, metalness: 0.9, transparent: true, opacity: 0.7 });
    const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
    cockpit.position.set(3, 0.5, 0);
    group.add(cockpit);

    // Delta wings (large triangular)
    const wingShape = new THREE.Shape();
    wingShape.moveTo(2, 0);
    wingShape.lineTo(-4, 7);
    wingShape.lineTo(-3, 7.5);
    wingShape.lineTo(1, 0);

    const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.1, bevelEnabled: true, bevelThickness: 0.02, bevelSize: 0.02 });
    const wingMat = new THREE.MeshStandardMaterial({ color, roughness: 0.5, metalness: 0.4 });

    const leftWing = new THREE.Mesh(wingGeo, wingMat);
    leftWing.position.set(0, -0.4, 0);
    group.add(leftWing);

    const rightWing = new THREE.Mesh(wingGeo, wingMat);
    rightWing.position.set(0, -0.4, 0);
    rightWing.scale.z = -1;
    group.add(rightWing);

    // Canted vertical stabilizers
    for (let side of [-1, 1]) {
      const vStabShape = new THREE.Shape();
      vStabShape.moveTo(0, 0);
      vStabShape.lineTo(-2.5, 3);
      vStabShape.lineTo(-3, 2.8);
      vStabShape.lineTo(-0.5, 0);

      const vStabGeo = new THREE.ExtrudeGeometry(vStabShape, { depth: 0.06, bevelEnabled: false });
      const vStab = new THREE.Mesh(vStabGeo, wingMat);
      vStab.position.set(-3, 0.4, side * 2 - side * 0.03);
      group.add(vStab);
    }

    // Engine exhaust (centered)
    const exhaustGeo = new THREE.CylinderGeometry(0.2, 0.35, 0.6, 8);
    const exhaustMat = new THREE.MeshStandardMaterial({ color: accent, emissive: accent, emissiveIntensity: 0.5 });
    const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);
    exhaust.rotation.z = Math.PI / 2;
    exhaust.position.x = -4.8;
    group.add(exhaust);

    const engineLight = new THREE.PointLight(accent, 0.6, 10);
    engineLight.position.set(-5, 0, 0);
    group.add(engineLight);
    group.userData.engineLight = engineLight;

    return group;
  }

  static create(type, color, accent) {
    switch (type) {
      case 'falcon': return this.createFalcon(color, accent);
      case 'thunder': return this.createThunder(color, accent);
      case 'phantom': return this.createPhantom(color, accent);
      default: return this.createFalcon(color, accent);
    }
  }
}

// ── class: FlightAudio ──
class FlightAudio {
  constructor() {
    this.ctx = null;
    this.engineGain = null;
    this.windGain = null;
    this.stallOsc = null;
    this.initialized = false;
  }

  init() {
    if (this.initialized) return;
    try {
      this.ctx = new (window.AudioContext || window.webkitAudioContext)();
      
      // Engine sound - layered oscillators for realistic jet engine
      const masterGain = this.ctx.createGain();
      masterGain.gain.value = 0.15;
      masterGain.connect(this.ctx.destination);

      // Low rumble (engine core)
      this.engineOsc1 = this.ctx.createOscillator();
      this.engineOsc1.type = 'sawtooth';
      this.engineOsc1.frequency.value = 60;
      
      const engineFilter1 = this.ctx.createBiquadFilter();
      engineFilter1.type = 'lowpass';
      engineFilter1.frequency.value = 200;
      engineFilter1.Q.value = 2;

      this.engineGain = this.ctx.createGain();
      this.engineGain.gain.value = 0.3;

      this.engineOsc1.connect(engineFilter1);
      engineFilter1.connect(this.engineGain);
      this.engineGain.connect(masterGain);
      this.engineOsc1.start();

      // Mid frequency (turbine whine)
      this.engineOsc2 = this.ctx.createOscillator();
      this.engineOsc2.type = 'sawtooth';
      this.engineOsc2.frequency.value = 180;

      const engineFilter2 = this.ctx.createBiquadFilter();
      engineFilter2.type = 'bandpass';
      engineFilter2.frequency.value = 400;
      engineFilter2.Q.value = 3;

      this.engineGain2 = this.ctx.createGain();
      this.engineGain2.gain.value = 0.15;

      this.engineOsc2.connect(engineFilter2);
      engineFilter2.connect(this.engineGain2);
      this.engineGain2.connect(masterGain);
      this.engineOsc2.start();

      // High frequency (compressor whine)
      this.engineOsc3 = this.ctx.createOscillator();
      this.engineOsc3.type = 'sine';
      this.engineOsc3.frequency.value = 800;

      this.engineGain3 = this.ctx.createGain();
      this.engineGain3.gain.value = 0.05;

      this.engineOsc3.connect(this.engineGain3);
      this.engineGain3.connect(masterGain);
      this.engineOsc3.start();

      // Wind noise - filtered white noise
      const bufferSize = 2 * this.ctx.sampleRate;
      const windBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
      const windData = windBuffer.getChannelData(0);
      for (let i = 0; i < bufferSize; i++) {
        windData[i] = Math.random() * 2 - 1;
      }

      this.windSource = this.ctx.createBufferSource();
      this.windSource.buffer = windBuffer;
      this.windSource.loop = true;

      const windFilter = this.ctx.createBiquadFilter();
      windFilter.type = 'bandpass';
      windFilter.frequency.value = 500;
      windFilter.Q.value = 0.5;

      this.windGain = this.ctx.createGain();
      this.windGain.gain.value = 0;

      this.windSource.connect(windFilter);
      windFilter.connect(this.windGain);
      this.windGain.connect(masterGain);
      this.windSource.start();

      // Stall warning oscillator
      this.stallOsc = this.ctx.createOscillator();
      this.stallOsc.type = 'square';
      this.stallOsc.frequency.value = 800;
      
      const stallGain = this.ctx.createGain();
      stallGain.gain.value = 0;
      this.stallGainNode = stallGain;

      this.stallOsc.connect(stallGain);
      stallGain.connect(masterGain);
      this.stallOsc.start();

      // Ambient drone (low frequency rumble)
      const ambientOsc = this.ctx.createOscillator();
      ambientOsc.type = 'sine';
      ambientOsc.frequency.value = 30;
      
      const ambientGain = this.ctx.createGain();
      ambientGain.gain.value = 0.02;

      ambientOsc.connect(ambientGain);
      ambientGain.connect(masterGain);
      ambientOsc.start();

      this.initialized = true;
    } catch (e) {
      console.warn('Audio init failed:', e);
    }
  }

  update(throttle, airspeed, altitude, isStalled, dt) {
    if (!this.initialized || !this.ctx) return;

    const t = this.ctx.currentTime;

    // Engine pitch and volume based on throttle
    const baseFreq1 = 40 + throttle * 80;
    const baseFreq2 = 120 + throttle * 300;
    const baseFreq3 = 500 + throttle * 600;

    this.engineOsc1.frequency.setTargetAtTime(baseFreq1, t, 0.05);
    this.engineOsc2.frequency.setTargetAtTime(baseFreq2, t, 0.05);
    this.engineOsc3.frequency.setTargetAtTime(baseFreq3, t, 0.05);

    // Engine volume based on throttle and altitude (less dense air = quieter)
    const altFactor = Math.exp(-altitude / 10000);
    const engineVol = 0.05 + throttle * 0.4 * altFactor;
    this.engineGain.gain.setTargetAtTime(engineVol, t, 0.1);
    this.engineGain2.gain.setTargetAtTime(engineVol * 0.6, t, 0.1);
    this.engineGain3.gain.setTargetAtTime(engineVol * 0.3, t, 0.1);

    // Wind noise based on airspeed
    const windVol = Math.min(airspeed / 400, 1) * 0.2;
    this.windGain.gain.setTargetAtTime(windVol, t, 0.1);

    // Stall warning
    if (isStalled && airspeed > 5) {
      const stallPulse = Math.sin(t * 8) > 0 ? 0.15 : 0;
      this.stallGainNode.gain.setTargetAtTime(stallPulse, t, 0.02);
    } else {
      this.stallGainNode.gain.setTargetAtTime(0, t, 0.1);
    }
  }

  stop() {
    if (this.ctx) {
      try { this.ctx.close(); } catch(e) {}
      this.initialized = false;
    }
  }
}

// ── class: HUD ──
class HUD {
  constructor(canvas) {
    this.canvas = canvas;
    this.ctx = canvas.getContext('2d');
    this.visible = true;
    this.resize();
  }

  resize() {
    this.canvas.width = window.innerWidth;
    this.canvas.height = window.innerHeight;
  }

  draw(physics, throttle, scenario, enemies) {
    if (!this.visible) return;
    
    const ctx = this.ctx;
    const w = this.canvas.width;
    const h = this.canvas.height;
    const cx = w / 2;
    const cy = h / 2;

    ctx.clearRect(0, 0, w, h);
    ctx.save();

    // Color scheme
    const hudColor = '#4affaa';
    const warnColor = '#ff6644';
    const dimColor = 'rgba(74,255,170,0.3)';

    ctx.strokeStyle = hudColor;
    ctx.fillStyle = hudColor;
    ctx.lineWidth = 1.5;
    ctx.font = 'bold 14px monospace';
    ctx.textAlign = 'left';

    // --- Pitch ladder (centered) ---
    const pitchScale = 3; // pixels per degree
    const rollAngle = this._getRoll(physics.quaternion);
    
    ctx.save();
    ctx.translate(cx, cy);
    ctx.rotate(-rollAngle);

    // Pitch lines
    for (let deg = -45; deg <= 45; deg += 10) {
      if (deg === 0) continue;
      const y = -deg * pitchScale;
      const lineW = Math.abs(deg) % 20 === 0 ? 60 : 30;
      
      ctx.strokeStyle = dimColor;
      ctx.beginPath();
      ctx.moveTo(-lineW, y);
      ctx.lineTo(lineW, y);
      ctx.stroke();

      if (Math.abs(deg) % 20 === 0 && Math.abs(deg) > 0) {
        ctx.fillStyle = dimColor;
        ctx.font = '11px monospace';
        ctx.textAlign = 'center';
        ctx.fillText(Math.abs(deg).toString(), lineW + 15, y + 4);
      }
    }

    // Horizon line
    const pitchAngle = this._getPitch(physics.quaternion) * (180 / Math.PI);
    const horizonY = -pitchAngle * pitchScale;
    
    ctx.strokeStyle = hudColor;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(-150, horizonY);
    ctx.lineTo(150, horizonY);
    ctx.stroke();

    // Aircraft reference symbol (fixed)
    ctx.strokeStyle = hudColor;
    ctx.lineWidth = 2;
    ctx.beginPath();
    ctx.moveTo(-30, 0);
    ctx.lineTo(-12, 0);
    ctx.moveTo(12, 0);
    ctx.lineTo(30, 0);
    ctx.stroke();
    
    // Center dot
    ctx.fillStyle = hudColor;
    ctx.beginPath();
    ctx.arc(0, 0, 3, 0, Math.PI * 2);
    ctx.fill();

    ctx.restore();

    // --- Left side: Airspeed tape ---
    const speed = physics.airspeed * 1.944; // m/s to knots
    this._drawTape(80, cy - 150, 300, speed, 'kts', hudColor);

    // --- Right side: Altitude tape ---
    const alt = Math.round(physics.altitude * 3.281); // meters to feet
    this._drawTape(w - 140, cy - 150, 300, alt, 'ft', hudColor);

    // --- Bottom: G-meter ---
    const gVal = Math.max(-1, Math.min(12, physics.gLoad));
    ctx.fillStyle = dimColor;
    ctx.fillRect(w/2 - 80, h - 60, 160, 8);
    
    const gColor = Math.abs(gVal) > 7 ? warnColor : hudColor;
    ctx.fillStyle = gColor;
    const gWidth = ((gVal + 1) / 13) * 156;
    ctx.fillRect(w/2 - 80, h - 60, Math.max(0, gWidth), 8);
    
    ctx.fillStyle = hudColor;
    ctx.font = 'bold 12px monospace';
    ctx.textAlign = 'center';
    ctx.fillText(`G: ${gVal.toFixed(1)}`, w/2, h - 45);

    // --- Throttle bar (bottom left) ---
    const throttlePct = Math.round(throttle * 100);
    ctx.fillStyle = dimColor;
    ctx.fillRect(30, h - 80, 12, 60);
    ctx.fillStyle = hudColor;
    ctx.fillRect(30, h - 20 - throttlePct * 0.6, 12, throttlePct * 0.6);
    ctx.font = 'bold 11px monospace';
    ctx.textAlign = 'center';
    ctx.fillText(`${throttlePct}%`, 36, h - 85);

    // --- Stall warning ---
    if (physics.isStalled) {
      ctx.fillStyle = warnColor;
      ctx.font = 'bold 24px monospace';
      ctx.textAlign = 'center';
      const flash = Math.sin(Date.now() * 0.01) > 0;
      if (flash) ctx.fillText('⚠ STALL ⚠', cx, cy - 80);
    }

    // --- Ground proximity warning ---
    if (physics.altitude < 50 && physics.verticalSpeed > 2) {
      ctx.fillStyle = warnColor;
      ctx.font = 'bold 18px monospace';
      ctx.textAlign = 'center';
      const flash = Math.sin(Date.now() * 0.015) > 0;
      if (flash) ctx.fillText('PULL UP', cx, cy + 60);
    }

    // --- Heading indicator (top center) ---
    const heading = this._getHeading(physics.quaternion);
    ctx.fillStyle = hudColor;
    ctx.font = 'bold 14px monospace';
    ctx.textAlign = 'center';
    ctx.fillText(`HDG ${Math.round((heading + 360) % 360)}°`, cx, 30);

    // --- Air density indicator ---
    const densityRatio = (physics.airDensity / RHO0 * 100).toFixed(0);
    ctx.font = '11px monospace';
    ctx.fillStyle = dimColor;
    ctx.textAlign = 'left';
    ctx.fillText(`σ: ${densityRatio}%`, 30, h - 95);

    // --- Vertical speed indicator (right side) ---
    const vsi = Math.round(physics.verticalSpeed * 196.85); // m/s to ft/min
    ctx.fillStyle = hudColor;
    ctx.font = 'bold 12px monospace';
    ctx.textAlign = 'left';
    ctx.fillText(`V/S: ${vsi > 0 ? '+' : ''}${vsi}`, w - 140, cy + 170);

    // --- Player health (dogfight) ---
    if (window._gameHealth !== undefined) {
      const hp = Math.max(0, window._gameHealth);
      ctx.fillStyle = dimColor;
      ctx.fillRect(w/2 - 80, h - 35, 160, 8);
      ctx.fillStyle = hp > 50 ? hudColor : warnColor;
      ctx.fillRect(w/2 - 80, h - 35, (hp / 100) * 160, 8);
      ctx.font = 'bold 11px monospace';
      ctx.textAlign = 'center';
      ctx.fillText(`HP: ${Math.round(hp)}%`, w/2, h - 23);
    }

    // --- Mission score ---
    if (window._gameScore !== undefined) {
      ctx.fillStyle = hudColor;
      ctx.font = 'bold 16px monospace';
      ctx.textAlign = 'right';
      ctx.fillText(`SCORE: ${Math.round(window._gameScore)}`, w - 30, 30);
    }

    // --- Radar (dogfight mode only) ---
    if (scenario === 'dogfight' && enemies) {
      this._drawRadar(w - 100, h - 120, 70, physics.position, enemies);
    }

    ctx.restore();
  }

  _drawTape(x, tapeY, height, value, unit, color) {
    const ctx = this.ctx;
    
    // Tape background
    ctx.fillStyle = 'rgba(0,20,10,0.6)';
    ctx.fillRect(x - 5, tapeY, 70, height);
    ctx.strokeStyle = 'rgba(74,255,170,0.3)';
    ctx.strokeRect(x - 5, tapeY, 70, height);

    const tickSpacing = 20;
    const range = Math.ceil(height / (tickSpacing * 2));
    
    for (let i = -range; i <= range; i++) {
      const val = value + i * (unit === 'kts' ? 10 : 500);
      const tickY = tapeY + height/2 - i * tickSpacing;
      
      if (tickY < tapeY || tickY > tapeY + height) continue;

      ctx.strokeStyle = 'rgba(74,255,170,0.3)';
      ctx.beginPath();
      ctx.moveTo(x + 60, tickY);
      ctx.lineTo(x + 68, tickY);
      ctx.stroke();

      if (i % 2 === 0) {
        ctx.fillStyle = 'rgba(74,255,170,0.6)';
        ctx.font = '11px monospace';
        ctx.textAlign = 'left';
        ctx.fillText(Math.round(val).toString(), x + 3, tickY + 4);
      }
    }

    // Current value highlight bracket
    const midY = tapeY + height / 2;
    ctx.fillStyle = color;
    ctx.beginPath();
    ctx.moveTo(x - 10, midY - 8);
    ctx.lineTo(x - 3, midY);
    ctx.lineTo(x - 10, midY + 8);
    ctx.closePath();
    ctx.fill();

    // Current value text
    ctx.fillStyle = color;
    ctx.font = 'bold 16px monospace';
    ctx.textAlign = 'right';
    ctx.fillText(`${Math.round(value)} ${unit}`, x - 8, midY + 5);
  }

  _drawRadar(rx, ry, radius, playerPos, enemies) {
    const ctx = this.ctx;
    
    // Radar background circle
    ctx.strokeStyle = 'rgba(74,255,170,0.4)';
    ctx.lineWidth = 1;
    ctx.beginPath();
    ctx.arc(rx, ry, radius, 0, Math.PI * 2);
    ctx.stroke();

    // Range rings
    for (let r = 1; r <= 3; r++) {
      ctx.strokeStyle = 'rgba(74,255,170,0.15)';
      ctx.beginPath();
      ctx.arc(rx, ry, radius * r / 3, 0, Math.PI * 2);
      ctx.stroke();
    }

    // Cross lines (N/S/E/W)
    ctx.strokeStyle = 'rgba(74,255,170,0.2)';
    ctx.beginPath();
    ctx.moveTo(rx - radius, ry);
    ctx.lineTo(rx + radius, ry);
    ctx.moveTo(rx, ry - radius);
    ctx.lineTo(rx, ry + radius);
    ctx.stroke();

    // Player blip (center)
    ctx.fillStyle = '#4affaa';
    ctx.beginPath();
    ctx.arc(rx, ry, 3, 0, Math.PI * 2);
    ctx.fill();

    // Enemy blips
    const range = 1500; // meters per radar radius
    for (const enemy of enemies) {
      if (!enemy.alive || !enemy.mesh) continue;
      
      const dx = enemy.mesh.position.x - playerPos.x;
      const dz = enemy.mesh.position.z - playerPos.z;
      const dist = Math.sqrt(dx * dx + dz * dz);
      
      if (dist > range) continue;

      // Rotate by player heading to make radar relative
      const forwardX = Math.sin(this._getHeadingRaw(enemy.mesh.quaternion || new THREE.Quaternion()));
      const px = (dx / range) * radius;
      const pz = (dz / range) * radius;
      
      ctx.fillStyle = '#ff4444';
      ctx.beginPath();
      ctx.arc(rx + px, ry - pz, 3, 0, Math.PI * 2);
      ctx.fill();

      // Distance label
      ctx.fillStyle = 'rgba(255,68,68,0.7)';
      ctx.font = '9px monospace';
      ctx.textAlign = 'left';
      ctx.fillText(Math.round(dist), rx + px + 5, ry - pz + 3);
    }

    // N label
    ctx.fillStyle = 'rgba(74,255,170,0.6)';
    ctx.font = 'bold 10px monospace';
    ctx.textAlign = 'center';
    ctx.fillText('N', rx, ry - radius + 10);
    
    // Range label
    ctx.fillStyle = 'rgba(74,255,170,0.3)';
    ctx.font = '8px monospace';
    ctx.fillText(`${Math.round(range)}m`, rx, ry + radius - 5);
  }

  _getHeadingRaw(q) {
    return Math.atan2(2 * (q.w * q.y + q.x * q.z), 1 - 2 * (q.y * q.y + q.z * q.z));
  }

  _getPitch(q) {
    return Math.asin(-2 * (q.x * q.z - q.w * q.y));
  }

  _getRoll(q) {
    return Math.atan2(2 * (q.w * q.z + q.x * q.y), 1 - 2 * (q.y * q.y + q.z * q.z));
  }

  _getHeading(q) {
    const yaw = this._getHeadingRaw(q);
    return -(yaw * 180 / Math.PI);
  }

  toggle() {
    this.visible = !this.visible;
    if (!this.visible) {
      this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
    }
  }
}

// ── class: EnemyAircraft ──
class EnemyAircraft {
  constructor(scene, position) {
    this.scene = scene;
    this.mesh = AircraftModel.create('falcon', 0xff2222, 0xff8844);
    this.mesh.position.copy(position);
    this.scene.add(this.mesh);

    // Simple physics for enemy
    this.velocity = new THREE.Vector3(50, 0, -30);
    this.quaternion = new THREE.Quaternion();
    this.targetPosition = null;
    this.state = 'patrol'; // patrol, chase, evade
    this.health = 100;
    this.alive = true;

    // Patrol waypoints
    this.patrolAngle = Math.random() * Math.PI * 2;
    this.orbitRadius = 800 + Math.random() * 400;
    this.orbitAltitude = 300 + Math.random() * 500;

    // Attack cooldown
    this.attackCooldown = 0;
    
    // Reaction delay
    this.reactionTimer = 0;
  }

  update(dt, playerPos, playerVel) {
    if (!this.alive) return;

    const distToPlayer = this.mesh.position.distanceTo(playerPos);
    
    // State machine
    if (distToPlayer < 600 && this.health > 0) {
      this.state = 'chase';
    } else if (this.health <= 25) {
      this.state = 'evade';
    } else {
      this.state = 'patrol';
    }

    let targetDir;
    
    switch (this.state) {
      case 'patrol':
        this.patrolAngle += dt * 0.15;
        const tx = Math.cos(this.patrolAngle) * this.orbitRadius;
        const tz = Math.sin(this.patrolAngle) * this.orbitRadius;
        targetDir = new THREE.Vector3(tx - this.mesh.position.x, 
                                       this.orbitAltitude - this.mesh.position.y, 
                                       tz - this.mesh.position.z).normalize();
        break;

      case 'chase':
        // Lead the player slightly
        const leadPos = playerPos.clone().add(playerVel.clone().multiplyScalar(2));
        targetDir = leadPos.sub(this.mesh.position).normalize();
        
        // Attack when close enough and aligned
        this.attackCooldown -= dt;
        if (distToPlayer < 300 && this.attackCooldown <= 0) {
          const toPlayer = playerPos.clone().sub(this.mesh.position).normalize();
          const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(this.quaternion);
          if (forward.dot(toPlayer) > 0.85) {
            this.attackCooldown = 2;
            // Damage player (callback handled by game)
          }
        }
        break;

      case 'evade':
        const awayFromPlayer = this.mesh.position.clone().sub(playerPos).normalize();
        targetDir = awayFromPlayer;
        break;
    }

    // Smooth turning toward target direction
    const currentForward = new THREE.Vector3(1, 0, 0).applyQuaternion(this.quaternion);
    const crossProduct = new THREE.Vector3().crossVectors(currentForward, targetDir);
    
    const turnRate = Math.min(crossProduct.length(), dt * 2.5);
    if (turnRate > 0.01) {
      const axis = crossProduct.normalize();
      const deltaQuat = new THREE.Quaternion().setFromAxisAngle(axis, turnRate);
      this.quaternion.multiply(deltaQuat).normalize();
    }

    // Speed management
    const speed = this.velocity.length();
    const targetSpeed = 120 + Math.random() * 30;
    const accel = (targetSpeed - speed) * dt * 0.5;
    
    const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(this.quaternion);
    this.velocity.add(forward.multiplyScalar(accel));

    // Altitude control
    if (this.mesh.position.y < 100) {
      this.velocity.y += dt * 20;
    } else if (this.mesh.position.y > 800) {
      this.velocity.y -= dt * 15;
    }

    // Move
    this.mesh.position.add(this.velocity.clone().multiplyScalar(dt));

    // Update mesh orientation
    this.mesh.quaternion.copy(this.quaternion);
    
    // Engine glow based on speed
    if (this.mesh.userData.engineLight) {
      const intensity = Math.min(speed / 200, 1) * 0.8;
      this.mesh.userData.engineLight.intensity = intensity;
    }
  }

  takeDamage(amount) {
    this.health -= amount;
    if (this.health <= 0 && this.alive) {
      this.alive = false;
      // Explosion effect handled by game
    }
  }

  destroy() {
    this.scene.remove(this.mesh);
    this.mesh.traverse(child => {
      if (child.geometry) child.geometry.dispose();
      if (child.material) child.material.dispose();
    });
  }
}

// ── class: ExplosionEffect ──
class ExplosionEffect {
  constructor(scene, position) {
    this.particles = [];
    this.alive = true;
    
    // Create fireball particles
    for (let i = 0; i < 30; i++) {
      const size = 1 + Math.random() * 4;
      const geo = new THREE.SphereGeometry(size, 6, 6);
      const mat = new THREE.MeshBasicMaterial({ 
        color: new THREE.Color().setHSL(0.05 + Math.random() * 0.1, 1, 0.5),
        transparent: true, opacity: 1
      });
      const mesh = new THREE.Mesh(geo, mat);
      mesh.position.copy(position);
      
      const vel = new THREE.Vector3(
        (Math.random() - 0.5) * 40,
        Math.random() * 30 + 10,
        (Math.random() - 0.5) * 40
      );

      scene.add(mesh);
      this.particles.push({ mesh, vel, life: 1 });
    }

    // Flash light
    const flash = new THREE.PointLight(0xff6622, 5, 100);
    flash.position.copy(position);
    scene.add(flash);
    this.flash = flash;
    this.flashLife = 1;
  }

  update(dt) {
    if (!this.alive) return;

    // Update particles
    for (const p of this.particles) {
      p.life -= dt * 0.8;
      p.vel.y -= dt * 9.81;
      p.mesh.position.add(p.vel.clone().multiplyScalar(dt));
      p.mesh.material.opacity = Math.max(0, p.life);
      p.mesh.scale.multiplyScalar(1 + dt * 2);

      if (p.life <= 0) {
        p.mesh.visible = false;
      }
    }

    // Flash decay
    this.flashLife -= dt * 3;
    this.flash.intensity = Math.max(0, this.flashLife * 5);

    if (this.particles.every(p => p.life <= 0)) {
      this.alive = false;
      for (const p of this.particles) {
        p.mesh.geometry.dispose();
        p.mesh.material.dispose();
        p.mesh.parent.remove(p.mesh);
      }
      this.flash.parent.remove(this.flash);
    }
  }
}

// ── class: Runway ──
class Runway {
  constructor(scene) {
    this.scene = scene;
    this.group = new THREE.Group();
    
    // Main runway surface
    const rwGeo = new THREE.PlaneGeometry(40, 2000);
    rwGeo.rotateX(-Math.PI / 2);
    const rwMat = new THREE.MeshStandardMaterial({ 
      color: 0x333333, roughness: 0.9, metalness: 0.1 
    });
    this.runway = new THREE.Mesh(rwGeo, rwMat);
    this.runway.position.y = 0.5;
    this.group.add(this.runway);

    // Runway markings - center line dashes
    for (let z = -950; z < 950; z += 40) {
      const dashGeo = new THREE.PlaneGeometry(1, 20);
      dashGeo.rotateX(-Math.PI / 2);
      const dashMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
      const dash = new THREE.Mesh(dashGeo, dashMat);
      dash.position.set(0, 0.6, z);
      this.group.add(dash);
    }

    // Edge lines
    for (let side of [-18, 18]) {
      const edgeGeo = new THREE.PlaneGeometry(1, 2000);
      edgeGeo.rotateX(-Math.PI / 2);
      const edgeMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
      const edge = new THREE.Mesh(edgeGeo, edgeMat);
      edge.position.set(side, 0.6, 0);
      this.group.add(edge);
    }

    // Threshold markings
    for (let z of [-980, 980]) {
      for (let x = -12; x <= 12; x += 4) {
        const tGeo = new THREE.PlaneGeometry(2, 30);
        tGeo.rotateX(-Math.PI / 2);
        const tMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
        const tMesh = new THREE.Mesh(tGeo, tMat);
        tMesh.position.set(x, 0.6, z);
        this.group.add(tMesh);
      }
    }

    // Runway lights (emissive)
    for (let side of [-20, 20]) {
      for (let z = -980; z <= 980; z += 30) {
        const lightGeo = new THREE.SphereGeometry(0.3, 6, 6);
        const lightMat = new THREE.MeshBasicMaterial({ color: 0x44ff44 });
        const lightMesh = new THREE.Mesh(lightGeo, lightMat);
        lightMesh.position.set(side, 1, z);
        this.group.add(lightMesh);
      }
    }

    // PAPI lights (approach slope indicators)
    for (let i = 0; i < 4; i++) {
      const pGeo = new THREE.SphereGeometry(0.5, 6, 6);
      const pMat = new THREE.MeshBasicMaterial({ color: i < 2 ? 0xff0000 : 0xffffff });
      const pMesh = new THREE.Mesh(pGeo, pMat);
      pMesh.position.set(-25, 3, -980);
      this.group.add(pMesh);
    }

    // Landing zone indicator (for scoring)
    const lzGeo = new THREE.PlaneGeometry(30, 100);
    lzGeo.rotateX(-Math.PI / 2);
    const lzMat = new THREE.MeshBasicMaterial({ 
      color: 0x44ffaa, transparent: true, opacity: 0.15 
    });
    this.landingZone = new THREE.Mesh(lzGeo, lzMat);
    this.landingZone.position.set(0, 0.7, -200);
    this.group.add(this.landingZone);

    // Touchdown zone markings
    for (let x of [-8, 0, 8]) {
      const tzGeo = new THREE.PlaneGeometry(4, 60);
      tzGeo.rotateX(-Math.PI / 2);
      const tzMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
      const tzMesh = new THREE.Mesh(tzGeo, tzMat);
      tzMesh.position.set(x, 0.65, -200);
      this.group.add(tzMesh);
    }

    scene.add(this.group);
  }

  isInLandingZone(pos) {
    return Math.abs(pos.x) < 18 && pos.z > -250 && pos.z < -150;
  }

  destroy() {
    this.scene.remove(this.group);
    this.group.traverse(child => {
      if (child.geometry) child.geometry.dispose();
      if (child.material) child.material.dispose();
    });
  }
}

// ── class: Game ──
class Game {
  constructor() {
    this.state = 'menu'; // menu, playing, paused, crashed
    this.selectedAircraft = null;
    this.selectedScenario = null;
    
    // Three.js core
    this.scene = new THREE.Scene();
    this.camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 1, 20000);
    this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
    
    // Game objects
    this.physics = null;
    this.aircraftMesh = null;
    this.terrainNoise = null;
    this.terrainRenderer = null;
    this.audio = null;
    this.hud = null;
    this.runway = null;
    this.enemies = [];
    this.explosions = [];
    this.contrails = null;

    // Input state
    this.keys = {};
    
    // Camera
    this.cameraMode = 'chase'; // chase, cockpit, free
    this.cameraOffset = new THREE.Vector3(-25, 8, 0);
    this.cameraSmoothness = 0.06;

    // Mission state
    this.missionScore = 0;
    this.playerHealth = 100;
    this.enemiesDestroyed = 0;
    this.landingGrade = null;
    this.messageTimer = 0;

    // Time tracking
    this.lastTime = 0;
    this.elapsed = 0;

    // Expose for HUD
    window._gameHealth = 100;
    window._gameScore = 0;

    this._initRenderer();
    this._setupScene();
    this._setupInput();
  }

  _initRenderer() {
    this.renderer.setSize(window.innerWidth, window.innerHeight);
    this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    this.renderer.shadowMap.enabled = true;
    this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
    this.renderer.toneMapping = THREE.ACESFilmicToneMapping;
    this.renderer.toneMappingExposure = 1.0;
    document.getElementById('hud').appendChild(this.renderer.domElement);
  }

  _setupScene() {
    // Sky gradient using a large sphere with improved shader
    const skyGeo = new THREE.SphereGeometry(9000, 32, 32);
    const skyMat = new THREE.ShaderMaterial({
      uniforms: {
        topColor: { value: new THREE.Color(0x0a1a4a) },
        midColor: { value: new THREE.Color(0x3366aa) },
        bottomColor: { value: new THREE.Color(0xddeeff) },
        offset: { value: 20 },
        exponent: { value: 0.5 }
      },
      vertexShader: `
        varying vec3 vWorldPosition;
        void main() {
          vec4 worldPos = modelMatrix * vec4(position, 1.0);
          vWorldPosition = worldPos.xyz;
          gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
        }
      `,
      fragmentShader: `
        uniform vec3 topColor;
        uniform vec3 midColor;
        uniform vec3 bottomColor;
        uniform float offset;
        uniform float exponent;
        varying vec3 vWorldPosition;
        void main() {
          float h = normalize(vWorldPosition + offset).y;
          vec3 color;
          if (h > 0.0) {
            color = mix(midColor, topColor, pow(h, exponent));
          } else {
            color = mix(bottomColor, midColor, pow(-h * 2.0, exponent));
          }
          gl_FragColor = vec4(color, 1.0);
        }
      `,
      side: THREE.BackSide
    });
    this.scene.add(new THREE.Mesh(skyGeo, skyMat));

    // Fog for depth - color matches horizon
    this.scene.fog = new THREE.FogExp2(0xaabbcc, 0.00012);

    // Lighting
    const ambientLight = new THREE.AmbientLight(0x5577bb, 0.5);
    this.scene.add(ambientLight);

    const sunLight = new THREE.DirectionalLight(0xffeedd, 1.3);
    sunLight.position.set(500, 800, 300);
    sunLight.castShadow = true;
    sunLight.shadow.mapSize.width = 2048;
    sunLight.shadow.mapSize.height = 2048;
    sunLight.shadow.camera.near = 10;
    sunLight.shadow.camera.far = 3000;
    sunLight.shadow.camera.left = -500;
    sunLight.shadow.camera.right = 500;
    sunLight.shadow.camera.top = 500;
    sunLight.shadow.camera.bottom = -500;
    this.scene.add(sunLight);

    const hemiLight = new THREE.HemisphereLight(0x87ceeb, 0x445522, 0.35);
    this.scene.add(hemiLight);

    // Clouds layer
    this._createClouds();
  }

  _createClouds() {
    const cloudGroup = new THREE.Group();
    for (let i = 0; i < 60; i++) {
      const size = 50 + Math.random() * 150;
      const geo = new THREE.SphereGeometry(size, 8, 6);
      const mat = new THREE.MeshStandardMaterial({
        color: 0xffffff,
        transparent: true,
        opacity: 0.4 + Math.random() * 0.3,
        roughness: 1,
        metalness: 0
      });
      const cloud = new THREE.Mesh(geo, mat);
      cloud.position.set(
        (Math.random() - 0.5) * 8000,
        400 + Math.random() * 600,
        (Math.random() - 0.5) * 8000
      );
      cloud.scale.y = 0.3;
      cloudGroup.add(cloud);
    }
    this.scene.add(cloudGroup);
    this.clouds = cloudGroup;
  }

  _setupInput() {
    window.addEventListener('keydown', e => {
      this.keys[e.code] = true;
      
      if (e.code === 'Tab') {
        e.preventDefault();
        if (this.hud) this.hud.toggle();
      }
      if (e.code === 'KeyR' && this.state === 'playing') {
        this._resetAircraft();
      }
    });

    window.addEventListener('keyup', e => {
      this.keys[e.code] = false;
    });

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

  _cleanup() {
    // Clean up previous game state before starting new one
    if (this.aircraftMesh) {
      this.scene.remove(this.aircraftMesh);
      this.aircraftMesh.traverse(child => {
        if (child.geometry) child.geometry.dispose();
        if (child.material) child.material.dispose();
      });
      this.aircraftMesh = null;
    }
    if (this.terrainRenderer) {
      this.terrainRenderer.dispose();
      this.terrainRenderer = null;
    }
    if (this.runway) {
      this.runway.destroy();
      this.runway = null;
    }
    for (const e of this.enemies) e.destroy();
    this.enemies = [];
    this.explosions.forEach(e => { try { e.alive = false; } catch(ex){} });
    this.explosions = [];
    if (this.contrails) {
      this.contrails.dispose();
      this.contrails = null;
    }
    if (this.audio) {
      this.audio.stop();
      this.audio = null;
    }
  }

  start(aircraftType, scenario) {
    // Allow re-launching - always clean up first
    this._cleanup();
    
    this.selectedAircraft = aircraftType;
    this.selectedScenario = scenario;
    
    const acData = AIRCRAFT_DATA[aircraftType];
    
    // Initialize physics
    this.physics = new FlightPhysics(acData);
    
    // Create terrain
    this.terrainNoise = new TerrainNoise(Math.random() * 10000);
    this.terrainRenderer = new TerrainRenderer(this.scene, this.terrainNoise);

    // Create aircraft mesh
    const acModel = AircraftModel.create(aircraftType, acData.color, acData.accentColor);
    this.aircraftMesh = acModel;
    this.scene.add(acModel);

    // Contrails
    this.contrails = new ContrailSystem(this.scene);

    // Initialize audio
    this.audio = new FlightAudio();
    this.audio.init();

    // Initialize HUD
    const hudCanvas = document.getElementById('hud-canvas');
    this.hud = new HUD(hudCanvas);

    if (scenario === 'takeoff') {
      this._setupTakeoffScenario();
    } else {
      this._setupDogfightScenario();
    }

    // Switch UI
    document.getElementById('menu').style.display = 'none';
    document.getElementById('hud').style.display = 'block';
    document.getElementById('crosshair').style.display = 'block';

    this.state = 'playing';
    this.lastTime = performance.now();
    
    window._gameHealth = 100;
    window._gameScore = 0;
    
    this._showMessage(scenario === 'takeoff' ? 'Takeoff & Landing - Clear for takeoff!' : 'Dogfight - Hostiles detected!');
  }

  _setupTakeoffScenario() {
    // Place aircraft on runway
    this.physics.reset(0, 2, 500, Math.PI); // facing negative Z (down runway)
    
    // Create runway
    this.runway = new Runway(this.scene);
    
    this.missionScore = 0;
    this.playerHealth = 100;
  }

  _setupDogfightScenario() {
    // Place aircraft in the air
    this.physics.reset(0, 400, 0, 0);
    this.physics.velocity.set(80, 0, 50);
    
    // Spawn enemies
    for (let i = 0; i < 3; i++) {
      const angle = (i / 3) * Math.PI * 2 + Math.random();
      const dist = 600 + Math.random() * 400;
      const pos = new THREE.Vector3(
        Math.cos(angle) * dist,
        300 + Math.random() * 300,
        Math.sin(angle) * dist
      );
      this.enemies.push(new EnemyAircraft(this.scene, pos));
    }

    this.missionScore = 0;
    this.playerHealth = 100;
    this.enemiesDestroyed = 0;
  }

  _resetAircraft() {
    if (this.selectedScenario === 'takeoff') {
      this.physics.reset(0, 2, 500, Math.PI);
    } else {
      this.physics.reset(0, 400, 0, 0);
      this.physics.velocity.set(80, 0, 50);
    }
    this.playerHealth = 100;
    window._gameHealth = 100;
    this._showMessage('Position reset');
  }

  _showMessage(text) {
    const overlay = document.getElementById('message-overlay');
    overlay.textContent = text;
    overlay.style.opacity = '1';
    this.messageTimer = 3;
  }

  update() {
    if (this.state !== 'playing') return;

    const now = performance.now();
    let dt = (now - this.lastTime) / 1000;
    dt = Math.min(dt, 0.05);
    this.lastTime = now;
    this.elapsed += dt;

    // --- Input processing ---
    if (this.keys['KeyW']) this.physics.pitchInput = Math.max(-1, this.physics.pitchInput - dt * 3);
    else if (this.keys['KeyS']) this.physics.pitchInput = Math.min(1, this.physics.pitchInput + dt * 3);
    else this.physics.pitchInput *= Math.pow(0.05, dt);

    if (this.keys['KeyA']) this.physics.rollInput = Math.max(-1, this.physics.rollInput - dt * 3);
    else if (this.keys['KeyD']) this.physics.rollInput = Math.min(1, this.physics.rollInput + dt * 3);
    else this.physics.rollInput *= Math.pow(0.05, dt);

    if (this.keys['KeyQ']) this.physics.yawInput = Math.max(-1, this.physics.yawInput - dt * 2);
    else if (this.keys['KeyE']) this.physics.yawInput = Math.min(1, this.physics.yawInput + dt * 2);
    else this.physics.yawInput *= Math.pow(0.05, dt);

    if (this.keys['ShiftLeft'] || this.keys['ShiftRight']) {
      this.physics.throttleTarget = Math.min(1, this.physics.throttleTarget + dt * 0.8);
    }
    if (this.keys['ControlLeft'] || this.keys['ControlRight']) {
      this.physics.throttleTarget = Math.max(0, this.physics.throttleTarget - dt * 0.8);
    }

    this.physics.brakes = !!this.keys['Space'];

    // --- Physics update ---
    this.physics.update(dt);

    // --- Update aircraft mesh ---
    if (this.aircraftMesh) {
      this.aircraftMesh.position.copy(this.physics.position);
      this.aircraftMesh.quaternion.copy(this.physics.quaternion);

      // Engine glow based on throttle
      if (this.aircraftMesh.userData.engineLight) {
        const intensity = this.physics.throttleTarget * 1.5;
        this.aircraftMesh.userData.engineLight.intensity = intensity;
      }
    }

    // --- Contrails update ---
    if (this.contrails && this.aircraftMesh) {
      this.contrails.addPoint(this.physics.position);
      this.contrails.update(dt, this.physics.altitude);
    }

    // --- Terrain update ---
    if (this.terrainRenderer) {
      this.terrainRenderer.update(this.physics.position.x, this.physics.position.z);
    }

    // --- Ground collision check ---
    const terrainHeight = this.terrainNoise ? this.terrainNoise.getHeight(
      this.physics.position.x, this.physics.position.z
    ) : 0;
    
    if (this.physics.position.y < terrainHeight + 2 && this.state === 'playing') {
      // Crash into terrain
      if (this.physics.velocity.length() > 15) {
        this._crash('Terrain collision!');
      } else {
        this.physics.position.y = terrainHeight + 2;
        this.physics.velocity.y = Math.max(0, this.physics.velocity.y);
      }
    }

    // --- Camera update ---
    this._updateCamera(dt);

    // --- Mission-specific updates ---
    if (this.selectedScenario === 'dogfight') {
      this._updateDogfight(dt);
    } else {
      this._updateTakeoffMission(dt);
    }

    // --- Audio update ---
    if (this.audio) {
      this.audio.update(
        this.physics.throttleTarget,
        this.physics.airspeed,
        this.physics.altitude,
        this.physics.isStalled,
        dt
      );
    }

    // --- Message timer ---
    if (this.messageTimer > 0) {
      this.messageTimer -= dt;
      if (this.messageTimer <= 0) {
        document.getElementById('message-overlay').style.opacity = '0';
      }
    }

    // --- HUD update ---
    if (this.hud) {
      window._gameHealth = this.playerHealth;
      window._gameScore = this.missionScore;
      const aliveEnemies = this.selectedScenario === 'dogfight' ? this.enemies : null;
      this.hud.draw(this.physics, this.physics.throttleTarget, this.selectedScenario, aliveEnemies);
    }

    // --- Explosion effects ---
    for (let i = this.explosions.length - 1; i >= 0; i--) {
      this.explosions[i].update(dt);
      if (!this.explosions[i].alive) {
        this.explosions.splice(i, 1);
      }
    }

    // --- Clouds slow drift ---
    if (this.clouds) {
      this.clouds.position.x += dt * 2;
    }
  }

  _updateCamera(dt) {
    const pos = this.physics.position;
    const quat = this.physics.quaternion;

    // Calculate chase camera position behind aircraft
    const backDir = new THREE.Vector3(-1, 0, 0).applyQuaternion(quat);
    const upDir = new THREE.Vector3(0, 1, 0).applyQuaternion(quat);
    
    const desiredPos = pos.clone()
      .add(backDir.multiplyScalar(this.cameraOffset.x))
      .add(upDir.multiplyScalar(this.cameraOffset.y));

    // Smooth camera follow
    this.camera.position.lerp(desiredPos, Math.min(1, this.cameraSmoothness + dt * 2));

    // Look at aircraft (slightly ahead)
    const lookTarget = pos.clone().add(
      new THREE.Vector3(1, 0, 0).applyQuaternion(quat).multiplyScalar(20)
    );
    this.camera.lookAt(lookTarget);
  }

  _updateDogfight(dt) {
    // Update enemies
    for (const enemy of this.enemies) {
      if (!enemy.alive) continue;
      
      enemy.update(dt, this.physics.position, this.physics.velocity);

      // Check if player is shooting (F key)
      const dist = this.physics.position.distanceTo(enemy.mesh.position);
      if (dist < 200 && this.keys['KeyF']) {
        const toEnemy = enemy.mesh.position.clone().sub(this.physics.position).normalize();
        const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(this.physics.quaternion);
        
        if (forward.dot(toEnemy) > 0.85) {
          this.missionScore += 1;
          
          // Check hit
          enemy.takeDamage(25 + Math.random() * 15);
          
          if (!enemy.alive) {
            this.enemiesDestroyed++;
            this.explosions.push(new ExplosionEffect(this.scene, enemy.mesh.position.clone()));
            enemy.destroy();
            this.missionScore += 50;
            this._showMessage(`Enemy destroyed! Score: ${this.missionScore}`);
          } else {
            // Hit spark effect
            this.explosions.push(new ExplosionEffect(this.scene, 
              enemy.mesh.position.clone().add(new THREE.Vector3(0, 1, 0))));
          }
        }
      }

      // Enemy damage to player when very close
      if (dist < 50) {
        this.playerHealth -= dt * 20;
        if (this.playerHealth <= 0) {
          this._crash('Shot down!');
        }
      }
    }

    // Check mission complete
    if (this.enemies.every(e => !e.alive)) {
      this.missionScore += Math.round(this.playerHealth);
      this._showMessage(`Mission Complete! Final Score: ${this.missionScore}`);
    }
  }

  _updateTakeoffMission(dt) {
    // Landing evaluation
    if (this.runway && this.physics.position.y < 5 && 
        Math.abs(this.physics.velocity.y) < 10 &&
        this.runway.isInLandingZone(this.physics.position)) {
      
      const speed = this.physics.airspeed;
      const vsi = Math.abs(this.physics.verticalSpeed);
      
      if (speed < 60 && vsi < 5) {
        let grade = 'A';
        let score = 100;
        
        if (vsi > 3 || speed > 50) { grade = 'B'; score = 80; }
        if (vsi > 6 || speed > 70) { grade = 'C'; score = 60; }
        if (vsi > 10 || speed > 90) { grade = 'D'; score = 40; }
        
        this.landingGrade = grade;
        this.missionScore = score;
        this._showMessage(`Landing Grade: ${grade} - Score: ${score}`);
      }
    }

    // Takeoff bonus
    if (this.physics.altitude > 100 && this.missionScore === 0) {
      this.missionScore = 25;
      this._showMessage('Takeoff successful!');
    }
  }

  _crash(reason) {
    this.state = 'crashed';
    
    // Create explosion at aircraft position
    if (this.aircraftMesh) {
      this.explosions.push(new ExplosionEffect(this.scene, this.physics.position.clone()));
      this.aircraftMesh.visible = false;
    }

    this._showMessage(`CRASHED: ${reason}`);
    
    // Allow restart after delay
    setTimeout(() => {
      if (this.state === 'crashed') {
        this._returnToMenu();
      }
    }, 5000);
  }

  _returnToMenu() {
    this.state = 'menu';
    
    // Cleanup
    if (this.aircraftMesh) this.scene.remove(this.aircraftMesh);
    if (this.terrainRenderer) this.terrainRenderer.dispose();
    if (this.runway) this.runway.destroy();
    for (const e of this.enemies) e.destroy();
    this.enemies = [];
    this.explosions.forEach(e => { try { e.alive = false; } catch(ex){} });
    this.explosions = [];
    if (this.contrails) this.contrails.dispose();
    if (this.audio) this.audio.stop();

    document.getElementById('menu').style.display = 'flex';
    document.getElementById('hud').style.display = 'none';
    document.getElementById('crosshair').style.display = 'none';
  }

  render() {
    this.renderer.render(this.scene, this.camera);
  }
}

// ── class: ContrailSystem ──
class ContrailSystem {
  constructor(scene) {
    this.scene = scene;
    this.trails = []; // Array of trail points per engine
    this.maxTrailLength = 200;
    this.alive = true;
    
    // Create a line geometry that we'll update each frame
    const geo = new THREE.BufferGeometry();
    const positions = new Float32Array(this.maxTrailLength * 3);
    geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
    
    const mat = new THREE.LineBasicMaterial({ 
      color: 0xffffff, 
      transparent: true, 
      opacity: 0.4 
    });
    
    this.line = new THREE.Line(geo, mat);
    this.line.frustumCulled = false; // Always render even if far away
    scene.add(this.line);
    
    this.trailPoints = [];
  }

  addPoint(position) {
    this.trailPoints.push({
      pos: position.clone(),
      age: 0,
      opacity: 1
    });
    
    if (this.trailPoints.length > this.maxTrailLength) {
      this.trailPoints.shift();
    }
  }

  update(dt, altitude) {
    // Contrails only form at higher altitudes where air is cold enough
    const contrailAltitude = 200; // meters
    
    if (altitude > contrailAltitude) {
      this.trailPoints.forEach(p => p.age += dt);
      
      // Fade old points
      for (let i = this.trailPoints.length - 1; i >= 0; i--) {
        const p = this.trailPoints[i];
        p.opacity = Math.max(0, 1 - p.age / 8);
        if (p.opacity <= 0) {
          this.trailPoints.splice(i, 1);
        }
      }
    } else {
      // Fade out when below contrail altitude
      for (let i = this.trailPoints.length - 1; i >= 0; i--) {
        const p = this.trailPoints[i];
        p.opacity -= dt * 2;
        if (p.opacity <= 0) {
          this.trailPoints.splice(i, 1);
        }
      }
    }

    // Update line geometry
    const positions = this.line.geometry.attributes.position.array;
    for (let i = 0; i < this.maxTrailLength; i++) {
      if (i < this.trailPoints.length) {
        positions[i * 3] = this.trailPoints[i].pos.x;
        positions[i * 3 + 1] = this.trailPoints[i].pos.y;
        positions[i * 3 + 2] = this.trailPoints[i].pos.z;
      } else {
        positions[i * 3] = 0;
        positions[i * 3 + 1] = -9999; // Hide unused vertices
        positions[i * 3 + 2] = 0;
      }
    }
    
    this.line.geometry.attributes.position.needsUpdate = true;
    this.line.material.opacity = 0.4;
  }

  dispose() {
    this.scene.remove(this.line);
    this.line.geometry.dispose();
    this.line.material.dispose();
  }
}

// ── main ──
(function() {
  const game = new Game();
  
  // Expose for testing/debugging
  window.gameRef = game;
  
  // Build aircraft selection cards
  const acSelect = document.getElementById('aircraft-select');
  Object.entries(AIRCRAFT_DATA).forEach(([key, data]) => {
    const card = document.createElement('div');
    card.className = 'aircraft-card';
    card.dataset.type = key;
    card.innerHTML = `
      <h3>${data.name}</h3>
      <p>${data.description}</p>
      <p style="margin-top:8px;">Speed</p><div class="stat-bar"><div class="stat-fill" style="width:${data.stats.speed}%"></div></div>
      <p>Agility</p><div class="stat-bar"><div class="stat-fill" style="width:${data.stats.agility}%"></div></div>
      <p>Power</p><div class="stat-bar"><div class="stat-fill" style="width:${data.stats.power}%"></div></div>
    `;
    card.addEventListener('click', () => {
      document.querySelectorAll('.aircraft-card').forEach(c => c.classList.remove('selected'));
      card.classList.add('selected');
      checkReady();
    });
    acSelect.appendChild(card);
  });

  // Build scenario selection cards
  const scSelect = document.getElementById('scenario-select');
  Object.entries(SCENARIOS).forEach(([key, data]) => {
    const card = document.createElement('div');
    card.className = 'scenario-card';
    card.dataset.scenario = key;
    card.innerHTML = `<strong>${data.name}</strong><br><small style="color:#99aabb">${data.description}</small>`;
    card.addEventListener('click', () => {
      document.querySelectorAll('.scenario-card').forEach(c => c.classList.remove('selected'));
      card.classList.add('selected');
      checkReady();
    });
    scSelect.appendChild(card);
  });

  let selectedAC = null, selectedSC = null;
  
  function checkReady() {
    const acCard = document.querySelector('.aircraft-card.selected');
    const scCard = document.querySelector('.scenario-card.selected');
    selectedAC = acCard ? acCard.dataset.type : null;
    selectedSC = scCard ? scCard.dataset.scenario : null;
    document.getElementById('start-btn').disabled = !(selectedAC && selectedSC);
  }

  // Start button
  document.getElementById('start-btn').addEventListener('click', () => {
    if (selectedAC && selectedSC) {
      game.start(selectedAC, selectedSC);
    }
  });

  // Game loop
  function loop() {
    requestAnimationFrame(loop);
    game.update();
    game.render();
  }
  
  loop();
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"composer","sessionId":"300489a3-a92c-4ced-a4c6-5fc4cd1df0a1","tokensIn":2546106,"tokensOut":61671,"tokensTotal":2607777,"turns":55,"toolCalls":56,"failedToolCalls":0,"timestamp":"2026-07-18T15:24:09.282Z"} -->