← Flight Simulator results

Flight Simulator

Qwen 3.8 27B GGUF · typescript

◈Preview unavailable — try playing the result

Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

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

Generation prompt · exact text unavailable

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

Inspect original source 101,915 bytes · SHA-256 9247078b7024
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>APEX AERO — Realistic Flight Simulator</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
<style>
/* ── css: all ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #04070c; }
body {
  font-family: "Segoe UI", "Helvetica Neue", Arial, sans-serif;
  color: #dfe9f2;
  -webkit-user-select: none; user-select: none;
}
.hidden { display: none !important; }
#view { position: fixed; inset: 0; }
#view canvas { display: block; }
.mono { font-family: "Consolas", "Menlo", monospace; }

/* ---------- HUD ---------- */
#hud {
  position: fixed; inset: 0; pointer-events: none;
  font-family: "Consolas", "Menlo", monospace;
  text-shadow: 0 0 6px rgba(0,0,0,.8), 0 1px 2px rgba(0,0,0,.9);
  z-index: 5;
}
#hud .hud-num { font-size: 30px; font-weight: 600; line-height: 1.05; color: #f2f8ff; }
#hud .hud-num.sm { font-size: 19px; }
#hud .hud-lab { font-size: 10px; letter-spacing: .18em; color: #9fc0d8; }
#hud .hud-mini { font-size: 13px; color: #f2f8ff; margin: 0 6px 0 2px; }

#hud-top-left { position: absolute; top: 14px; left: 16px; }
#hud-callsign { font-size: 15px; letter-spacing: .3em; color: #bfe3ff; }
#hud-mode { font-size: 11px; letter-spacing: .22em; color: #7fa8c4; margin-top: 2px; }

#banner-box {
  position: absolute; top: 13%; left: 50%; transform: translateX(-50%);
  text-align: center; display: none;
}
#hud-banner-t { display: block; font-size: 26px; font-weight: 700; letter-spacing: .3em; color: #ffe9a8; }
#hud-banner-sub { display: block; font-size: 12px; letter-spacing: .2em; color: #bcd4e6; margin-top: 6px; }

#hud-msg {
  position: absolute; top: 20%; left: 50%; transform: translateX(-50%);
  font-size: 15px; letter-spacing: .14em; color: #d9ecff;
  opacity: 0; transition: opacity .35s; white-space: nowrap;
}
#hud-warn {
  position: absolute; top: 26%; left: 50%; transform: translateX(-50%);
  display: flex; gap: 8px;
}
.warnchip {
  font-size: 12px; letter-spacing: .18em; padding: 3px 10px;
  border: 1px solid #c8d8e8; color: #eaf4ff; background: rgba(20,30,44,.45);
}
.warnchip.hot {
  border-color: #ff5b4d; color: #ffb4ac; background: rgba(90,10,6,.5);
  animation: blink .5s steps(2) infinite;
}
@keyframes blink { 50% { opacity: .25; } }

#speed-box { position: absolute; left: 20px; top: 46%; transform: translateY(-50%); text-align: right; }
#vsi-box { position: absolute; left: 20px; top: 46%; transform: translateY(46px); text-align: right; }
#alt-box { position: absolute; right: 20px; top: 46%; transform: translateY(-50%); text-align: left; }
#hud-eng { position: absolute; right: 20px; top: 46%; transform: translateY(46px); text-align: left; }
#hud-thr-wrap {
  width: 120px; height: 8px; border: 1px solid #9fc0d8; margin: 4px 0 8px 0; background: rgba(10,16,24,.5);
}
#hud-thr { height: 100%; width: 50%; background: #4fc3f7; }
#hud-engline { white-space: nowrap; }

#hud-att { position: absolute; left: 20px; bottom: 22px; border: 1px solid rgba(159,192,216,.5); border-radius: 6px; background: rgba(8,14,22,.35); }
#hud-hdg { position: absolute; left: 50%; bottom: 22px; transform: translateX(-50%); }
#hud-aim { position: absolute; left: 50%; top: 44%; transform: translate(-50%,-50%); display: none; }
#score-box { position: absolute; right: 20px; top: 14px; font-size: 13px; color: #ffe9a8; letter-spacing: .12em; }
#score-box .hud-lab { color: #7fa8c4; }
#hud-ammo { position: absolute; right: 20px; top: 38px; font-size: 13px; color: #ff8f88; letter-spacing: .12em; }
#crosshair {
  position: absolute; left: 50%; top: 50%; width: 4px; height: 4px;
  margin: -2px 0 0 -2px; border-radius: 50%;
  background: rgba(255,255,255,.85); box-shadow: 0 0 5px rgba(0,0,0,.9);
}

/* ---------- Menu ---------- */
#menu {
  position: fixed; inset: 0; z-index: 10;
  background:
    radial-gradient(1200px 700px at 70% -10%, rgba(38,84,138,.55), transparent 60%),
    radial-gradient(900px 600px at 10% 110%, rgba(190,90,40,.22), transparent 60%),
    linear-gradient(160deg, #050a12 0%, #0a1522 55%, #060d16 100%);
  display: flex; flex-direction: column; align-items: center;
  overflow-y: auto; padding: 26px 16px 30px;
}
#menu::before {
  content: ""; position: absolute; inset: 0;
  background-image: repeating-linear-gradient(0deg, rgba(255,255,255,.03) 0 1px, transparent 1px 42px),
    repeating-linear-gradient(90deg, rgba(255,255,255,.03) 0 1px, transparent 1px 42px);
  pointer-events: none;
}
#menu-title { font-size: 54px; font-weight: 800; letter-spacing: .34em; color: #f2f8ff; position: relative; }
#menu-title span { color: #ffb443; }
#menu-sub { font-size: 11px; letter-spacing: .3em; color: #7fa8c4; margin: 10px 0 22px; position: relative; }
#menu-planes { display: flex; gap: 14px; flex-wrap: wrap; justify-content: center; position: relative; }
.plane-card {
  width: 240px; border: 1px solid #33506e; background: rgba(10,18,28,.72);
  padding: 14px 14px 12px; cursor: pointer; position: relative; transition: border-color .15s, transform .1s;
}
.plane-card:hover { border-color: #6ea3d0; transform: translateY(-2px); }
.plane-card.sel { border-color: #ffb443; box-shadow: 0 0 0 1px #ffb443, 0 0 24px rgba(255,180,67,.25); }
.plane-card canvas { width: 100%; height: 108px; display: block; }
.plane-name { font-size: 16px; font-weight: 700; letter-spacing: .12em; margin-top: 8px; color: #eaf4ff; }
.plane-tag { font-size: 10.5px; color: #8fb2cc; margin: 3px 0 9px; min-height: 26px; line-height: 1.35; }
.stat-row { display: flex; align-items: center; gap: 8px; margin: 4px 0; }
.stat-row .lab { width: 52px; font-size: 9px; letter-spacing: .14em; color: #7fa8c4; }
.stat-bar { flex: 1; height: 5px; background: #16232f; border: 1px solid #26394a; position: relative; }
.stat-bar i { position: absolute; left: 0; top: 0; bottom: 0; background: linear-gradient(90deg, #3d7cb4, #63b8f0); }
#menu-modes { display: flex; gap: 16px; margin-top: 22px; flex-wrap: wrap; justify-content: center; position: relative; }
.modebtn {
  width: 300px; padding: 16px 18px; cursor: pointer; text-align: left;
  background: linear-gradient(180deg, rgba(28,44,62,.9), rgba(14,24,36,.9));
  border: 1px solid #33506e; color: #dfe9f2; transition: all .15s;
}
.modebtn:hover { border-color: #ffb443; transform: translateY(-2px); box-shadow: 0 8px 24px rgba(0,0,0,.4); }
.mode-name { font-size: 17px; font-weight: 700; letter-spacing: .18em; color: #ffe9a8; }
.mode-desc { font-size: 11px; color: #8fb2cc; margin-top: 6px; line-height: 1.4; }
#menu-controls {
  display: flex; flex-wrap: wrap; gap: 8px 18px; justify-content: center; margin-top: 26px;
  font-size: 11px; color: #7fa8c4; position: relative;
}
#menu-controls b { color: #dfe9f2; background: #1b2c3d; border: 1px solid #33506e; padding: 1px 6px; margin-right: 5px; font-weight: 600; }
#menu-foot { margin-top: 22px; position: relative; }
#btn-mute {
  pointer-events: auto; background: none; border: 1px solid #33506e; color: #7fa8c4;
  font-size: 11px; letter-spacing: .2em; padding: 7px 16px; cursor: pointer;
}
#btn-mute:hover { border-color: #6ea3d0; color: #dfe9f2; }

/* ---------- Result overlay ---------- */
#result {
  position: fixed; inset: 0; z-index: 12; display: flex; flex-direction: column;
  align-items: center; justify-content: center;
  background: rgba(3,7,12,.66); backdrop-filter: blur(3px);
  font-family: "Consolas", "Menlo", monospace;
}
#result-title { font-size: 34px; font-weight: 800; letter-spacing: .3em; color: #ffe9a8; }
#result-lines { margin: 22px 0 28px; font-size: 15px; line-height: 2; text-align: center; color: #dfe9f2; letter-spacing: .1em; }
#result-btns { display: flex; gap: 14px; }
#result-btns button {
  pointer-events: auto; cursor: pointer; font-size: 13px; letter-spacing: .2em;
  padding: 11px 26px; color: #dfe9f2; background: rgba(20,32,46,.9); border: 1px solid #33506e;
}
#result-btns button:hover { border-color: #ffb443; color: #ffe9a8; }
</style>
</head>
<body>
<div id="view"></div>
<div id="hud" class="hidden">
  <div id="hud-top-left">
    <div id="hud-callsign">APEX</div>
    <div id="hud-mode">PRECISION</div>
  </div>
  <div id="banner-box"><span id="hud-banner-t"></span><span id="hud-banner-sub"></span></div>
  <div id="hud-msg"></div>
  <div id="hud-warn"></div>
  <div id="speed-box"><div class="hud-num" id="hud-spd">0</div><div class="hud-lab">KTS</div></div>
  <div id="vsi-box"><div class="hud-num sm" id="hud-vsi">0</div><div class="hud-lab">FPM</div></div>
  <div id="alt-box"><div class="hud-num" id="hud-alt">0</div><div class="hud-lab">FT</div></div>
  <div id="hud-eng">
    <div class="hud-lab">THR</div>
    <div id="hud-thr-wrap"><div id="hud-thr"></div></div>
    <div id="hud-engline"><span class="hud-lab">AOA</span><span id="hud-aoa" class="hud-mini">0.0</span><span class="hud-lab">G</span><span id="hud-g" class="hud-mini">1.0</span></div>
  </div>
  <div id="hud-att"><canvas id="att-canvas" width="220" height="150"></canvas></div>
  <div id="hud-hdg"><canvas id="hdg-canvas" width="260" height="30"></canvas></div>
  <div id="hud-aim"><canvas id="aim-canvas" width="130" height="130"></canvas></div>
  <div id="score-box"><span class="hud-lab">SCORE</span> <span id="hud-score-n">0</span></div>
  <div id="hud-ammo" class="hidden"><span class="hud-lab">AMMO</span> <span id="hud-ammo-n">42</span></div>
  <div id="crosshair"></div>
</div>
<div id="menu">
  <div id="menu-title">APEX<span> AERO</span></div>
  <div id="menu-sub">REALISTIC FLIGHT DYNAMICS · PROCEDURAL WORLD</div>
  <div id="menu-planes"></div>
  <div id="menu-modes">
    <button id="btn-precision" class="modebtn"><div class="mode-name">PRECISION FLIGHT</div><div class="mode-desc">Takeoff &amp; landing · stall limits · sink-rate scoring</div></button>
    <button id="btn-dogfight" class="modebtn"><div class="mode-name">DOGFIGHT</div><div class="mode-desc">High-G maneuvering · energy management · 3 hostiles</div></button>
  </div>
  <div id="menu-controls">
    <span><b>W/S</b> pitch</span><span><b>A/D</b> roll</span><span><b>Q/E</b> yaw</span>
    <span><b>Shift/Ctrl</b> throttle</span><span><b>G</b> gear</span><span><b>F</b> flaps</span>
    <span><b>Space</b> fire/brake</span><span><b>R</b> reset</span><span><b>Esc</b> menu</span>
  </div>
  <div id="menu-foot"><button id="btn-mute">SOUND: ON</button></div>
</div>
<div id="result" class="hidden">
  <div id="result-title">MISSION COMPLETE</div>
  <div id="result-lines"></div>
  <div id="result-btns">
    <button id="btn-again">FLY AGAIN</button>
    <button id="btn-menu">MAIN MENU</button>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./specs":"src/specs.ts","./physics":"src/physics.ts","./world":"src/world.ts","./planes":"src/planes.ts","./audio":"src/audio.ts","./hud":"src/hud.ts","./scenarios":"src/scenarios.ts","./math3":"src/math3.ts"},"src/physics.ts":{"./specs":"src/specs.ts","./math3":"src/math3.ts"},"src/world.ts":{"./specs":"src/specs.ts"},"src/planes.ts":{"./specs":"src/specs.ts"},"src/hud.ts":{"./physics":"src/physics.ts","./math3":"src/math3.ts"},"src/scenarios.ts":{"./physics":"src/physics.ts","./math3":"src/math3.ts","./specs":"src/specs.ts","./world":"src/world.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const specs_1 = require("./specs");
const physics_1 = require("./physics");
const world_1 = require("./world");
const planes_1 = require("./planes");
const audio_1 = require("./audio");
const hud_1 = require("./hud");
const scenarios_1 = require("./scenarios");
const math3_1 = require("./math3");
// ---- test/probe hooks (available even if the 3D engine fails to load) ----
const W = window;
W.__apex = {
    AIRCRAFT: specs_1.AIRCRAFT, createState: physics_1.createState, updatePhysics: physics_1.updatePhysics, terrainHeight: world_1.terrainHeight,
    spawnPrecision: scenarios_1.spawnPrecision, updatePrecision: scenarios_1.updatePrecision, spawnDogfight: scenarios_1.spawnDogfight, updateDogfight: scenarios_1.updateDogfight, DOGFIGHT_TIME: scenarios_1.DOGFIGHT_TIME,
    playerSt: null,
    dog: null,
    stepOnce: null,
    start: null,
};
window.__capErrors = [];
window.addEventListener('error', (e) => {
    window.__capErrors.push({ m: String(e.message), s: String((e.error && e.error.stack) || e.stack || '').slice(0, 1500) });
});
if (typeof THREE === 'undefined') {
    const m = document.getElementById('menu');
    if (m) {
        const d = document.createElement('div');
        d.style.cssText = 'color:#ff8f88;font:13px Consolas,monospace;margin-top:16px;text-align:center;max-width:520px';
        d.textContent = '3D engine (Three.js) failed to load from CDN — check network and reload. Flight-model diagnostics remain active.';
        m.appendChild(d);
    }
}
else {
    main();
}
function main() {
    const view = document.getElementById('view');
    const hudRoot = document.getElementById('hud');
    const menuEl = document.getElementById('menu');
    const resultEl = document.getElementById('result');
    const hud = new hud_1.Hud((0, hud_1.makeHudRefs)(hudRoot));
    const renderer = new THREE.WebGLRenderer({ antialias: true });
    renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
    renderer.setSize(window.innerWidth, window.innerHeight);
    view.appendChild(renderer.domElement);
    const scene = new THREE.Scene();
    (0, world_1.buildWorld)(scene);
    const camMain = new THREE.PerspectiveCamera(62, window.innerWidth / window.innerHeight, 0.5, specs_1.FAR_PLANE);
    const camMenu = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.5, specs_1.FAR_PLANE);
    scene.add(camMain);
    scene.add(camMenu);
    window.addEventListener('resize', () => {
        renderer.setSize(window.innerWidth, window.innerHeight);
        camMain.aspect = window.innerWidth / window.innerHeight;
        camMain.updateProjectionMatrix();
        camMenu.aspect = window.innerWidth / window.innerHeight;
        camMenu.updateProjectionMatrix();
    });
    // ---------- state ----------
    let mode = 'menu';
    let lastMode = 'precision';
    let spec = specs_1.AIRCRAFT[0];
    let playerSt = null;
    let plane = null;
    let prec = null;
    let dog = null;
    let gearOn = true;
    let flapsOn = false;
    let fireHeld = false;
    let crashHandled = false;
    let resultPending = -1;
    let resultReason = '';
    let camPos = (0, math3_1.v)(0, 60, 1000);
    let menuAngle = 0;
    let selectedIdx = 0;
    const enemyVisuals = [];
    const zeroWind = (0, math3_1.v)(0, 0, 0);
    // ---------- menu plane cards (each with its own tiny renderer) ----------
    const planeCards = [];
    {
        const miniRoot = document.getElementById('menu-planes');
        specs_1.AIRCRAFT.forEach((s, i) => {
            const card = document.createElement('div');
            card.className = 'plane-card' + (i === 0 ? ' sel' : '');
            const cv = document.createElement('canvas');
            cv.width = 240;
            cv.height = 108;
            const nm = document.createElement('div');
            nm.className = 'plane-name';
            nm.textContent = s.name;
            const tg = document.createElement('div');
            tg.className = 'plane-tag';
            tg.textContent = s.tagline;
            const stats = [
                ['SPEED', s.maxV / 460],
                ['THRUST', s.maxThrust / 148000],
                ['AGILITY', s.phiB / 0.6],
                ['CEILING', s.maxAlt / 16800],
            ];
            const st = document.createElement('div');
            for (const [lab, val] of stats) {
                const row = document.createElement('div');
                row.className = 'stat-row';
                row.innerHTML = `<span class="lab">${lab}</span><span class="stat-bar"><i style="width:${Math.round((0, math3_1.clamp)(val, 0.06, 1) * 100)}%"></i></span>`;
                st.appendChild(row);
            }
            card.appendChild(cv);
            card.appendChild(nm);
            card.appendChild(tg);
            card.appendChild(st);
            card.addEventListener('click', () => {
                selectedIdx = i;
                for (const c of planeCards)
                    c.card.classList.remove('sel');
                card.classList.add('sel');
            });
            miniRoot.appendChild(card);
            const model = (0, planes_1.buildAircraft)(s).root;
            const gs = new THREE.Scene();
            gs.add(model);
            const hl = new THREE.HemisphereLight(0xdfeaff, 0x3a4a58, 1.15);
            gs.add(hl);
            const dl = new THREE.DirectionalLight(0xfff2e0, 1.5);
            dl.position.set(4, 6, 3);
            gs.add(dl);
            const cam = new THREE.PerspectiveCamera(40, 240 / 108, 0.5, 500);
            cam.position.set(-17, 6.5, 26);
            cam.lookAt(2, 0, 0);
            const mini = new THREE.WebGLRenderer({ canvas: cv, antialias: true, alpha: true });
            mini.setSize(240, 108, false);
            mini.setClearColor(0x000000, 0);
            planeCards.push({ card, model, scene: gs, cam, spin: Math.random() * 6, mini });
        });
    }
    // ---------- bullets / booms / trails ----------
    const bulletPool = [];
    const bulletMat = new THREE.MeshBasicMaterial({ color: 0xffcf8a });
    const bulletGeo = new THREE.BoxGeometry(0.5, 0.5, 2.4);
    for (let i = 0; i < 64; i++) {
        const bm = new THREE.Mesh(bulletGeo, bulletMat);
        bm.visible = false;
        scene.add(bm);
        bulletPool.push(bm);
    }
    const boomTexC = document.createElement('canvas');
    boomTexC.width = boomTexC.height = 64;
    {
        const bc = boomTexC.getContext('2d');
        const g = bc.createRadialGradient(32, 32, 2, 32, 32, 30);
        g.addColorStop(0, 'rgba(255,255,255,1)');
        g.addColorStop(0.4, 'rgba(255,180,80,0.8)');
        g.addColorStop(1, 'rgba(255,80,20,0)');
        bc.fillStyle = g;
        bc.fillRect(0, 0, 64, 64);
    }
    const boomTex = new THREE.CanvasTexture(boomTexC);
    const booms = [];
    const boomMats = [];
    for (let i = 0; i < 4; i++) {
        const geo = new THREE.BufferGeometry();
        const arr = new Float32Array(24 * 3);
        geo.setAttribute('position', new THREE.BufferAttribute(arr, 3));
        const mat = new THREE.PointsMaterial({
            size: 150, map: boomTex, transparent: true, opacity: 0,
            depthWrite: false, sizeAttenuation: true, fog: false,
        });
        const pts = new THREE.Points(geo, mat);
        pts.visible = false;
        scene.add(pts);
        booms.push(pts);
        boomMats.push(mat);
    }
    class Trail {
        constructor(color, size) {
            this.head = 0;
            this.last = (0, math3_1.v)(1e9, 0, 0);
            const geo = new THREE.BufferGeometry();
            this.arr = new Float32Array(120 * 3);
            for (let i = 0; i < 120; i++)
                this.arr[i * 3 + 1] = -100000;
            geo.setAttribute('position', new THREE.BufferAttribute(this.arr, 3));
            const mat = new THREE.PointsMaterial({
                color, size, transparent: true, opacity: 0.4,
                depthWrite: false, sizeAttenuation: true, map: boomTex,
            });
            this.pts = new THREE.Points(geo, mat);
            scene.add(this.pts);
        }
        push(p) {
            const dx = p.x - this.last.x, dy = p.y - this.last.y, dz = p.z - this.last.z;
            if (dx * dx + dy * dy + dz * dz < 25)
                return;
            this.last = { x: p.x, y: p.y, z: p.z };
            const i = this.head * 3;
            this.arr[i] = p.x;
            this.arr[i + 1] = p.y;
            this.arr[i + 2] = p.z;
            this.head = (this.head + 1) % 120;
            this.pts.geometry.getAttribute('position').needsUpdate = true;
        }
        clear() {
            for (let i = 0; i < 120; i++)
                this.arr[i * 3 + 1] = -100000;
            this.pts.geometry.getAttribute('position').needsUpdate = true;
            this.head = 0;
            this.last = (0, math3_1.v)(1e9, 0, 0);
        }
    }
    const trails = [new Trail(0x99a0aa, 7), new Trail(0x99a0aa, 7), new Trail(0x99a0aa, 7), new Trail(0x99a0aa, 7), new Trail(0x99a0aa, 7)];
    // ---------- input ----------
    const keys = {};
    window.addEventListener('keydown', (e) => {
        const k = e.key.toLowerCase();
        if (k === ' ' || k.startsWith('arrow'))
            e.preventDefault();
        if (e.repeat)
            return;
        keys[k] = true;
        if (k === ' ')
            fireHeld = true;
        if (mode !== 'menu') {
            if (k === 'g')
                gearOn = !gearOn;
            if (k === 'f')
                flapsOn = !flapsOn;
            if (k === 'r')
                start(lastMode);
            if (k === 'escape')
                toMenu();
        }
        if (k === 'm')
            toggleMute();
    });
    window.addEventListener('keyup', (e) => {
        const k = e.key.toLowerCase();
        keys[k] = false;
        if (k === ' ')
            fireHeld = false;
    });
    function readControls() {
        return {
            pitchIn: (keys['w'] || keys['arrowup'] ? 1 : 0) + (keys['s'] || keys['arrowdown'] ? -1 : 0),
            rollIn: (keys['d'] || keys['arrowright'] ? 1 : 0) + (keys['a'] || keys['arrowleft'] ? -1 : 0),
            yawIn: (keys['e'] ? 1 : 0) + (keys['q'] ? -1 : 0),
            throttleIn: (keys['shift'] ? 1 : 0) + (keys['control'] ? -1 : 0),
            trimPitch: 0,
            gear: gearOn,
            flaps: flapsOn,
            brakes: fireHeld && mode === 'precision' && !!(playerSt && playerSt.onGround),
            fire: false,
            reset: false,
        };
    }
    // ---------- mode management ----------
    function start(m) {
        audio_1.audio.init();
        audio_1.audio.resume();
        if (plane)
            scene.remove(plane.root);
        for (const ev of enemyVisuals)
            scene.remove(ev.root);
        enemyVisuals.length = 0;
        for (const bm of bulletPool)
            bm.visible = false;
        trails.forEach(t => t.clear());
        for (let i = 0; i < booms.length; i++) {
            booms[i].visible = false;
            boomMats[i].opacity = 0;
            booms[i].boom = null;
        }
        lastMode = m;
        spec = specs_1.AIRCRAFT[selectedIdx];
        mode = m;
        crashHandled = false;
        resultPending = -1;
        resultReason = '';
        gearOn = true;
        flapsOn = false;
        resultEl.classList.add('hidden');
        menuEl.classList.add('hidden');
        hudRoot.classList.remove('hidden');
        hud.setMode(m === 'precision' ? 'PRECISION FLIGHT' : 'DOGFIGHT');
        hud.setCallsign(spec.name);
        hud.refs.scoreBox.style.display = m === 'dogfight' ? 'block' : 'none';
        hud.refs.ammoBox.classList.toggle('hidden', m !== 'dogfight');
        if (m === 'precision') {
            const sp = (0, scenarios_1.spawnPrecision)();
            playerSt = (0, physics_1.createState)(spec, sp.p, sp.yaw, 0, 0, 0);
            prec = (0, scenarios_1.precisionInitial)();
            dog = null;
            hud.banner('LINE UP — FULL THROTTLE', 'SHIFT/CTRL up · rotate above ' + Math.round(spec.vRef * 1.15 * 3.6) + ' km/h · land gently');
        }
        else {
            const center = (0, math3_1.v)(9000, 1800, 9000);
            const p = (0, math3_1.v)(6200, 2300, 6200);
            const yaw = Math.atan2(-(center.z - p.z), center.x - p.x);
            playerSt = (0, physics_1.createState)(spec, p, yaw, 0.06, 0, 210);
            playerSt.b.throttle = 0.85;
            dog = (0, scenarios_1.spawnDogfight)(spec, 3);
            prec = null;
            for (let i = 0; i < dog.enemies.length; i++) {
                const ev = (0, planes_1.buildAircraft)(dog.enemies[i].spec);
                scene.add(ev.root);
                enemyVisuals.push(ev);
            }
            hud.banner('ENGAGE — 3 HOSTILES', 'SPACE fire · manage energy · R respawn · survive ' + scenarios_1.DOGFIGHT_TIME + 's');
        }
        plane = (0, planes_1.buildAircraft)(spec);
        for (const g of [plane.gearL, plane.gearR, plane.gearC]) {
            if (g)
                g.userData.by = g.position.y;
        }
        scene.add(plane.root);
        const m4 = basisOf(playerSt);
        const tgt = (0, math3_1.add)((0, math3_1.add)(playerSt.b.p, (0, math3_1.scale)(m4.fwd, -10.5)), (0, math3_1.add)((0, math3_1.scale)(m4.up, 2.0), (0, math3_1.scale)(m4.right, -2.7)));
        camPos = { x: tgt.x, y: tgt.y, z: tgt.z };
        W.__apex.dog = dog;
    }
    function toMenu() {
        mode = 'menu';
        menuEl.classList.remove('hidden');
        hudRoot.classList.add('hidden');
        resultEl.classList.add('hidden');
        if (plane)
            plane.root.visible = false;
    }
    function toggleMute() {
        audio_1.audio.setMuted(!audio_1.audio.muted);
        const b = document.getElementById('btn-mute');
        b.textContent = 'SOUND: ' + (audio_1.audio.muted ? 'OFF' : 'ON');
    }
    document.getElementById('btn-mute').addEventListener('click', toggleMute);
    document.getElementById('btn-precision').addEventListener('click', () => start('precision'));
    document.getElementById('btn-dogfight').addEventListener('click', () => start('dogfight'));
    document.getElementById('btn-again').addEventListener('click', () => start(lastMode));
    document.getElementById('btn-menu').addEventListener('click', toMenu);
    // ---------- weapons / fx ----------
    function onFire(f) {
        if (!dog)
            return;
        if (dog.bullets.length > 60)
            return;
        const dir = (0, math3_1.v)(f.dir.x + (Math.random() - 0.5) * 2 * f.spread, f.dir.y + (Math.random() - 0.5) * 2 * f.spread, f.dir.z + (Math.random() - 0.5) * 2 * f.spread);
        const l = (0, math3_1.len)(dir) || 1;
        dog.bullets.push({
            p: { x: f.origin.x, y: f.origin.y, z: f.origin.z },
            dir: (0, math3_1.v)(dir.x / l, dir.y / l, dir.z / l),
            life: 1.7, fromPlayer: f.fromPlayer, speed: 640,
        });
        audio_1.audio.gunshot();
    }
    const boomQueue = [];
    function onBoom(p, big) {
        boomQueue.push({ p: { x: p.x, y: p.y, z: p.z }, t: 0, big });
        audio_1.audio.explosion(big);
    }
    // ---------- simulation step (shared with probes) ----------
    function simStep(dt) {
        if (mode === 'menu' || !playerSt)
            return;
        const st = playerSt;
        const ctl = readControls();
        const windFn = (p) => (mode === 'dogfight' && dog ? (0, scenarios_1.dogfightWind)(p, dog) : zeroWind);
        (0, physics_1.updatePhysics)(st, {
            controls: ctl, dt,
            terrainHeight: (x, z) => (0, world_1.terrainHeight)(x, z),
            wind: windFn,
        }, dt);
        if (mode === 'precision' && prec) {
            const wasLanded = prec.phase;
            (0, scenarios_1.updatePrecision)(st, prec, dt, (t) => hud.msg(t));
            if (wasLanded !== 'landed' && prec.phase === 'landed') {
                resultReason = 'landed';
                resultPending = 1.4;
            }
        }
        if (mode === 'dogfight' && dog) {
            (0, scenarios_1.updateDogfight)(st, dog, dt, fireHeld, onFire, onBoom, (t) => hud.msg(t));
            if (dog.result !== 'none' && dog.resultT > 1.8) {
                resultReason = dog.result;
                resultPending = 0.0001;
            }
        }
        if (st.crashed && !crashHandled) {
            crashHandled = true;
            if (mode === 'precision') {
                resultReason = 'lost';
                resultPending = 1.6;
            }
        }
        if (resultPending > 0) {
            resultPending -= dt;
            if (resultPending <= 0)
                showResult();
        }
        W.__apex.playerSt = st;
        W.__apex.dog = dog;
    }
    W.__apex.stepOnce = (dt) => simStep(Math.min(dt, 0.05));
    W.__apex.start = (m) => start(m);
    function showResult() {
        if (!playerSt)
            return;
        resultEl.classList.remove('hidden');
        const title = document.getElementById('result-title');
        const lines = document.getElementById('result-lines');
        const st = playerSt;
        const rows = [];
        if (resultReason === 'landed' && prec) {
            title.textContent = 'LANDING COMPLETE';
            rows.push('LANDED  +' + prec.score + ' pts');
            rows.push('ALTITUDE ' + Math.round(st.b.p.y * 3.28084).toLocaleString() + ' FT');
        }
        else if (resultReason === 'win' && dog) {
            title.textContent = 'MISSION ACCOMPLISHED';
            rows.push('SCORE ' + dog.score.toLocaleString());
            rows.push('KILLS ' + dog.kills + ' / ' + dog.enemies.length);
            rows.push('TIME ' + Math.round(dog.time) + 's  ·  AMMO LEFT ' + dog.playerAmmo);
        }
        else if (resultReason === 'timeout' && dog) {
            title.textContent = 'SORTIE ENDED';
            rows.push('SCORE ' + dog.score.toLocaleString());
            rows.push('KILLS ' + dog.kills + ' / ' + dog.enemies.length);
            rows.push('HOSTILES REMAINING ' + dog.alive);
        }
        else {
            title.textContent = 'AIRCRAFT LOST';
            if (dog) {
                rows.push('SCORE ' + dog.score.toLocaleString());
                rows.push('KILLS ' + dog.kills + ' / ' + dog.enemies.length);
            }
            rows.push('FINAL ALTITUDE ' + Math.round(st.b.p.y * 3.28084).toLocaleString() + ' FT');
        }
        lines.innerHTML = rows.map(r => `<div>${r}</div>`).join('');
    }
    // ---------- basis helper (body axes in world frame) ----------
    function basisOf(st) {
        const b = st.b;
        const cy = Math.cos(b.yaw), sy = Math.sin(b.yaw);
        const cp = Math.cos(b.pitch), sp = Math.sin(b.pitch);
        const cr = Math.cos(b.roll), sr = Math.sin(b.roll);
        return {
            fwd: (0, math3_1.v)(cp * cy, sp * sr + cp * sy * cr, -sp * cr + cp * sy * sr),
            right: (0, math3_1.v)(sy, cp * cy * cr - sp * sr, sp * cr + cp * cy * sr),
            up: (0, math3_1.v)(-sy * sr, cp * cy * sr + sp * cr, sp * sr + cp * cy * cr),
        };
    }
    // ---------- render ----------
    function render(dt) {
        if (mode === 'menu') {
            for (const pc of planeCards) {
                pc.spin += dt * 0.3;
                pc.model.rotation.y = -0.7 + Math.sin(pc.spin) * 0.55;
                pc.mini.render(pc.scene, pc.cam);
            }
            menuAngle += dt * 0.045;
            const focus = playerSt ? playerSt.b.p : (0, math3_1.v)(world_1.RUNWAY.cx, world_1.RUNWAY.y + 120, world_1.RUNWAY.cz);
            const r = playerSt ? 55 : 900;
            const h = playerSt ? 16 : 240;
            camMenu.position.set(focus.x + Math.cos(menuAngle) * r, focus.y + h, focus.z + Math.sin(menuAngle) * r);
            camMenu.lookAt(focus.x, focus.y, focus.z);
            renderer.render(scene, camMenu);
            return;
        }
        if (!playerSt || !plane) {
            renderer.render(scene, camMain);
            return;
        }
        const st = playerSt;
        const b = st.b;
        const m4 = basisOf(st);
        plane.root.visible = true;
        plane.root.position.set(b.p.x, b.p.y, b.p.z);
        plane.root.quaternion.setFromRotationMatrix(new THREE.Matrix4().makeBasis(new THREE.Vector3(m4.fwd.x, m4.fwd.y, m4.fwd.z), new THREE.Vector3(m4.up.x, m4.up.y, m4.up.z), new THREE.Vector3(m4.right.x, m4.right.y, m4.right.z)));
        if (plane.prop)
            plane.prop.rotation.z += (20 + 140 * b.throttle) * dt;
        if (plane.afterburner) {
            const on = b.throttle > 0.5 && !st.onGround;
            plane.afterburner.material.opacity = on ? 0.55 + 0.45 * b.throttle : 0;
            plane.afterburner.visible = on;
        }
        for (const g of [plane.gearL, plane.gearR, plane.gearC]) {
            if (!g)
                continue;
            g.visible = b.gearExt > 0.04;
            g.position.y = (g.userData.by ?? g.position.y) + 2.4 * b.gearExt;
        }
        if (dog) {
            dog.enemies.forEach((e, i) => {
                const ev = enemyVisuals[i];
                if (!ev)
                    return;
                ev.root.visible = e.alive;
                if (!e.alive)
                    return;
                const eb = e.st.b;
                ev.root.position.set(eb.p.x, eb.p.y, eb.p.z);
                const em = basisOf(e.st);
                ev.root.quaternion.setFromRotationMatrix(new THREE.Matrix4().makeBasis(new THREE.Vector3(em.fwd.x, em.fwd.y, em.fwd.z), new THREE.Vector3(em.up.x, em.up.y, em.up.z), new THREE.Vector3(em.right.x, em.right.y, em.right.z)));
                if (ev.gearL) {
                    ev.gearL.visible = false;
                    if (ev.gearR)
                        ev.gearR.visible = false;
                    if (ev.gearC)
                        ev.gearC.visible = false;
                }
            });
        }
        trails[0].push((0, math3_1.v)(b.p.x - m4.fwd.x * 9, b.p.y - m4.fwd.y * 9 + 0.5, b.p.z - m4.fwd.z * 9));
        if (dog) {
            dog.enemies.forEach((e, i) => {
                if (e.alive)
                    trails[i + 1].push((0, math3_1.v)(e.st.b.p.x, e.st.b.p.y, e.st.b.p.z));
            });
        }
        const nB = dog ? dog.bullets.length : 0;
        for (let i = 0; i < bulletPool.length; i++) {
            const bm = bulletPool[i];
            if (i < nB) {
                const bl = dog.bullets[i];
                bm.visible = true;
                bm.position.set(bl.p.x, bl.p.y, bl.p.z);
                bm.quaternion.setFromUnitVectors(new THREE.Vector3(0, 0, 1), new THREE.Vector3(bl.dir.x, bl.dir.y, bl.dir.z));
            }
            else
                bm.visible = false;
        }
        while (boomQueue.length > 0 && booms.some(bm => !bm.visible)) {
            const bd = boomQueue.shift();
            const bm = booms.find(b => !b.visible);
            bm.visible = true;
            const dirs = [];
            for (let i = 0; i < 24; i++) {
                const th = Math.random() * Math.PI * 2;
                const ph = Math.acos(2 * Math.random() - 1);
                const s = bd.big ? 1 : 0.45;
                dirs.push({ x: Math.sin(ph) * Math.cos(th) * s, y: Math.cos(ph) * 0.7 * s, z: Math.sin(ph) * Math.sin(th) * s });
            }
            bd.dirs = dirs;
            bm.boom = bd;
        }
        for (let i = 0; i < booms.length; i++) {
            const bm = booms[i];
            if (!bm.visible)
                continue;
            const bd = bm.boom;
            const arr = bm.geometry.getAttribute('position').array;
            const dirs = bd.dirs;
            const sp = bd.big ? 240 : 100;
            for (let k = 0; k < 24; k++) {
                arr[k * 3] = bd.p.x + dirs[k].x * bd.t * sp;
                arr[k * 3 + 1] = bd.p.y + dirs[k].y * bd.t * sp - 30 * bd.t * bd.t;
                arr[k * 3 + 2] = bd.p.z + dirs[k].z * bd.t * sp;
            }
            bm.geometry.getAttribute('position').needsUpdate = true;
            boomMats[i].opacity = Math.max(0, 1 - bd.t / 1.2);
            if (bd.t > 1.5) {
                bm.visible = false;
                boomMats[i].opacity = 0;
                bm.boom = null;
            }
        }
        // chase camera
        if (!st.crashed) {
            const tgt = (0, math3_1.add)((0, math3_1.add)(st.b.p, (0, math3_1.scale)(m4.fwd, -10.5)), (0, math3_1.add)((0, math3_1.scale)(m4.up, 2.0), (0, math3_1.scale)(m4.right, -2.7)));
            camPos.x = (0, math3_1.expDamp)(camPos.x, tgt.x, 6, dt);
            camPos.y = (0, math3_1.expDamp)(camPos.y, tgt.y, 6, dt);
            camPos.z = (0, math3_1.expDamp)(camPos.z, tgt.z, 6, dt);
            camMain.position.set(camPos.x, camPos.y, camPos.z);
            const look = (0, math3_1.add)((0, math3_1.add)(st.b.p, (0, math3_1.scale)(m4.fwd, 40)), (0, math3_1.scale)(m4.up, -2.5));
            camMain.lookAt(look.x, look.y, look.z);
            const fov = 58 + 14 * (0, math3_1.clamp)(st.V / 460, 0, 1);
            if (Math.abs(camMain.fov - fov) > 0.15) {
                camMain.fov = fov;
                camMain.updateProjectionMatrix();
            }
        }
        else {
            const a = (W.__apex._orbitA = ((W.__apex._orbitA ?? 0) + dt * 0.35));
            camMain.position.set(b.p.x + Math.cos(a) * 26, b.p.y + 9, b.p.z + Math.sin(a) * 26);
            camMain.lookAt(b.p.x, b.p.y + 2, b.p.z);
        }
        renderer.render(scene, camMain);
        // HUD
        let aimInfo = null;
        if (dog && !st.crashed) {
            let best = null;
            for (const e of dog.enemies) {
                if (!e.alive)
                    continue;
                const to = (0, math3_1.sub)((0, math3_1.v)(e.st.b.p.x, e.st.b.p.y, e.st.b.p.z), camPos);
                const dl = (0, math3_1.len)(to);
                const fwdN = (0, math3_1.scale)(to, 1 / (dl || 1));
                const offDeg = Math.acos((0, math3_1.clamp)((0, math3_1.dot)(fwdN, m4.fwd), -1, 1)) * 57.2958;
                if (!best || offDeg < best.offDeg)
                    best = { offDeg, dist: dl };
            }
            if (best)
                aimInfo = { offDeg: best.offDeg, locked: best.offDeg < 4.2, dist: best.dist };
        }
        hud.tick({
            st,
            score: dog ? dog.score : (prec ? prec.score : 0),
            ammo: dog ? dog.playerAmmo : 0,
            aimInfo,
        }, dt);
    }
    // ---------- audio ----------
    function updateAudio(dt) {
        if (!playerSt || mode === 'menu')
            return;
        const st = playerSt;
        audio_1.audio.update({
            throttle: st.b.throttle,
            V: st.V,
            alt: Math.max(0, st.b.p.y),
            onGround: st.onGround,
            gear: st.b.gearExt,
            crashed: st.crashed,
        }, dt);
    }
    // ---------- loop ----------
    let last = performance.now();
    function frame(now) {
        requestAnimationFrame(frame);
        let dt = (now - last) / 1000;
        last = now;
        dt = (0, math3_1.clamp)(dt, 0.0001, 0.05);
        simStep(dt);
        updateAudio(dt);
        render(dt);
    }
    requestAnimationFrame(frame);
}
};

// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};

// ── module: src/math3.ts ──
__mods["src/math3.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.wrapPi = exports.smooth01 = exports.expDamp = exports.deg = exports.rad = exports.clamp01 = exports.clamp = exports.lerp = exports.dist = exports.dotN = exports.norm = exports.len = exports.cross = exports.dot = exports.scale = exports.sub = exports.add = exports.v = void 0;
exports.yawPitchRollToMatrix = yawPitchRollToMatrix;
exports.eulerFromYawPitchRoll = eulerFromYawPitchRoll;
const v = (x = 0, y = 0, z = 0) => ({ x, y, z });
exports.v = v;
const add = (a, b) => (0, exports.v)(a.x + b.x, a.y + b.y, a.z + b.z);
exports.add = add;
const sub = (a, b) => (0, exports.v)(a.x - b.x, a.y - b.y, a.z - b.z);
exports.sub = sub;
const scale = (a, s) => (0, exports.v)(a.x * s, a.y * s, a.z * s);
exports.scale = scale;
const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z;
exports.dot = dot;
const cross = (a, b) => (0, exports.v)(a.y * b.z - a.z * b.y, a.z * b.x - a.x * b.z, a.x * b.y - a.y * b.x);
exports.cross = cross;
const len = (a) => Math.sqrt(a.x * a.x + a.y * a.y + a.z * a.z);
exports.len = len;
const norm = (a) => { const l = (0, exports.len)(a) || 1; return (0, exports.v)(a.x / l, a.y / l, a.z / l); };
exports.norm = norm;
const dotN = (a, b) => (0, exports.dot)(a, b) / ((0, exports.len)(a) * (0, exports.len)(b) || 1);
exports.dotN = dotN;
const dist = (a, b) => (0, exports.len)((0, exports.sub)(a, b));
exports.dist = dist;
const lerp = (a, b, t) => a + (b - a) * t;
exports.lerp = lerp;
const clamp = (x, lo, hi) => Math.min(hi, Math.max(lo, x));
exports.clamp = clamp;
const clamp01 = (x) => (0, exports.clamp)(x, 0, 1);
exports.clamp01 = clamp01;
const rad = (d) => (d * Math.PI) / 180;
exports.rad = rad;
const deg = (r) => (r * 180) / Math.PI;
exports.deg = deg;
const expDamp = (a, b, lambda, dt) => (0, exports.lerp)(a, b, 1 - Math.exp(-lambda * dt));
exports.expDamp = expDamp;
const smooth01 = (t) => { t = (0, exports.clamp01)(t); return t * t * (3 - 2 * t); };
exports.smooth01 = smooth01;
// Wrap angle to [-PI, PI]
const wrapPi = (a) => {
    while (a > Math.PI)
        a -= 2 * Math.PI;
    while (a < -Math.PI)
        a += 2 * Math.PI;
    return a;
};
exports.wrapPi = wrapPi;
// Rotation matrix (ZYX applied like Euler YXZ... standard aerospace: yaw->pitch->roll)
function yawPitchRollToMatrix(yaw, pitch, roll) {
    const cy = Math.cos(yaw), sy = Math.sin(yaw);
    const cp = Math.cos(pitch), sp = Math.sin(pitch);
    const cr = Math.cos(roll), sr = Math.sin(roll);
    const a = cp * cy, b = sp * sr + cp * sy * cr, c = -sp * cr + cp * sy * sr;
    const d = -sy, e = cp * cy * cr - sp * sr, f = sp * cr + cp * cy * sr;
    const g = sy * sr, h = cp * cy * sr + sp * cr, i = -sp * sr + cp * cy * cr;
    // Rows are world components of body axes: X (forward), Y (right), Z (up)
    return {
        fx: a, fy: b, fz: c, // forward
        rx: -d, ry: e, rz: f, // right
        ux: g, uy: h, uz: i, // up
        // inverse (transpose) for world->body
        wx: a, wy: -d, wz: g,
        wy2: b, wy3: e, wy4: h,
        wz2: c, wz3: f, wz4: i,
    };
}
function eulerFromYawPitchRoll(yaw, pitch, roll) {
    return { yaw: (0, exports.wrapPi)(yaw), pitch, roll: (0, exports.wrapPi)(roll) };
}
};

// ── module: src/specs.ts ──
__mods["src/specs.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CLOUDS = exports.FAR_PLANE = exports.SEA_Y = exports.WATER_Y = exports.TERRAIN_SEG = exports.WORLD_RADIUS = exports.G0 = exports.RHO0 = exports.AIRCRAFT = void 0;
exports.AIRCRAFT = [
    {
        id: 'falcon', name: 'FA-20 "FALCON"', tagline: 'High-bypass twin. Heavy, stable, fast.',
        mass: 9200, S: 54, Cd0: 0.022, k: 0.046, clA: 5.6, maxCl: 2.05, cl0: 0.3, clMin: -1.2,
        alphaMax: 0.42, alphaMin: -0.28, maxThrust: 148000, maxAlt: 15200, minAlt: 200,
        maxV: 265, cmt: -0.008, tcm: 0.12, phiB: 0.42, psiA: 0.30, tRoll: 0.9, tPitch: 0.8,
        vRef: 41, groundRoll: true, accent: 0x4fc3f7,
    },
    {
        id: 'shrike', name: 'VT-9 "SHRIKE"', tagline: 'Turboprop trainer. Gentle, predictable, slow.',
        mass: 1450, S: 15.5, Cd0: 0.031, k: 0.062, clA: 5.9, maxCl: 1.85, cl0: 0.22, clMin: -1.0,
        alphaMax: 0.34, alphaMin: -0.22, maxThrust: 13500, maxAlt: 7900, minAlt: 100,
        maxV: 150, cmt: -0.006, tcm: 0.09, phiB: 0.55, psiA: 0.34, tRoll: 0.7, tPitch: 0.6,
        vRef: 29, groundRoll: true, accent: 0xffca28,
    },
    {
        id: 'viper', name: 'FX-12 "VIPER"', tagline: 'Supersonic interceptor. Fierce, fast, unforgiving.',
        mass: 4300, S: 29, Cd0: 0.017, k: 0.039, clA: 5.35, maxCl: 1.95, cl0: 0.25, clMin: -1.1,
        alphaMax: 0.50, alphaMin: -0.32, maxThrust: 88000, maxAlt: 16800, minAlt: 200,
        maxV: 460, cmt: -0.010, tcm: 0.14, phiB: 0.60, psiA: 0.40, tRoll: 0.5, tPitch: 0.45,
        vRef: 35, groundRoll: false, accent: 0xef5350,
    },
];
// ---- World constants ----
exports.RHO0 = 1.225; // sea-level air density kg/m^3
exports.G0 = 9.80665;
exports.WORLD_RADIUS = 42000; // m, terrain disc radius
exports.TERRAIN_SEG = 128;
exports.WATER_Y = -12;
exports.SEA_Y = 2; // nominal sea level for pressure altitude
exports.FAR_PLANE = 400000;
exports.CLOUDS = 260;
};

// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createState = createState;
exports.airDensity = airDensity;
exports.updatePhysics = updatePhysics;
exports.speedKts = speedKts;
exports.speedKmh = speedKmh;
exports.ft = ft;
const specs_1 = require("./specs");
const math3_1 = require("./math3");
const MAX_STALL_G = 4; // |g| beyond which structural failure
const GEAR_DAMP = 0.85;
const FLAP_CL = 0.45;
const FLAP_CD = 0.030;
function createState(spec, p, yaw, pitch, roll, speed) {
    const m = (0, math3_1.yawPitchRollToMatrix)(yaw, pitch, roll);
    const vel = (0, math3_1.scale)({ x: m.fx, y: m.fy, z: m.fz }, speed);
    const b = {
        p, vel, yaw, pitch, roll, yawRate: 0, pitchRate: 0, rollRate: 0,
        alpha: 0, alphaRate: 0, beta: 0, throttle: 0.5, gear: spec.groundRoll, gearExt: spec.groundRoll ? 1 : 0,
    };
    return {
        b, spec, q: 0, V: speed, rho: specs_1.RHO0, gamma: 0, cl: 0, cd: 0, thrust: 0, lift: 0, drag: 0,
        agl: 0, vsi: 0, gLoad: 1, aoaDeg: 0, gear: b.gear, flaps: false, flapsExt: 0, heat: 0,
        stall: false, inverted: false, overspeed: false, compress: false, onGround: false,
        crashed: false, crashTimer: 0,
    };
}
// ISA atmosphere
function airDensity(alt) {
    const h = Math.max(alt, 0);
    if (h < 11000)
        return specs_1.RHO0 * Math.pow(1 - 2.25577e-5 * h, 4.2561);
    return 0.36391;
}
function updatePhysics(st, inp, dt) {
    const b = st.b;
    const s = st.spec;
    if (st.crashed) {
        // dead-aircraft tumble / slide
        b.pitchRate = (0, math3_1.expDamp)(b.pitchRate, b.pitch > 0 ? 0.5 : -0.5, 1.5, dt);
        b.yawRate = (0, math3_1.expDamp)(b.yawRate, 0.3, 2, dt);
        b.rollRate = (0, math3_1.expDamp)(b.rollRate, 0, 2, dt);
        b.pitch += b.pitchRate * dt;
        b.roll += b.rollRate * dt;
        b.yaw += b.yawRate * dt;
        const m = (0, math3_1.yawPitchRollToMatrix)(b.yaw, b.pitch, b.roll);
        b.vel = (0, math3_1.scale)(b.vel, Math.exp(-2.2 * dt));
        const gh = inp.terrainHeight(b.p.x, b.p.z);
        b.p = (0, math3_1.add)(b.p, (0, math3_1.scale)(b.vel, dt));
        b.p.y = Math.max(b.p.y, gh + 1.6);
        st.crashTimer += dt;
        st.V = (0, math3_1.len)(b.vel);
        st.agl = b.p.y - gh;
        st.heat = Math.max(0, st.heat - dt * 0.5);
        return;
    }
    // --- controls -> actuator response (rate-limited control surfaces) ---
    const c = inp.controls;
    const inv = c.invert ? -1 : 1;
    const pitchCmd = (0, math3_1.clamp)(inv * c.pitchIn + (0, math3_1.rad)(c.trimPitch) * 0.35, -1, 1);
    b.pitchRate = (0, math3_1.expDamp)(b.pitchRate, pitchCmd * 1.5, 3.0 / s.tPitch, dt); // up to ~1.5 rad/s
    b.rollRate = (0, math3_1.expDamp)(b.rollRate, -c.rollIn * 2.6, 3.0 / s.tRoll, dt);
    b.yawRate = (0, math3_1.expDamp)(b.yawRate, -c.yawIn * 0.5, 3.0, dt);
    // throttle trim
    b.throttle = (0, math3_1.clamp)(b.throttle + c.throttleIn * 0.6 * dt, 0, 1);
    // gear & flaps (animated, add drag while moving)
    b.gear = c.gear;
    st.gear = c.gear;
    const gearTarget = b.gear ? 1 : 0;
    const gDir = Math.sign(gearTarget - b.gearExt);
    b.gearExt = (0, math3_1.clamp)(b.gearExt + gDir * 1.4 * dt, 0, 1);
    const flapTarget = c.flaps ? 1 : 0;
    st.flapsExt = (0, math3_1.clamp)(st.flapsExt + Math.sign(flapTarget - st.flapsExt) * 1.2 * dt, 0, 1);
    st.flaps = c.flaps;
    // --- kinematics: body rates -> yaw/pitch/roll (aerospace convention, small-angle coupling) ---
    b.yaw += (b.yawRate - b.rollRate * Math.tan(b.pitch)) * dt;
    b.pitch += b.pitchRate * dt;
    b.roll += b.rollRate * dt;
    b.pitch = (0, math3_1.clamp)(b.pitch, -1.45, 1.45);
    b.roll = (0, math3_1.wrapPi)(b.roll);
    b.yaw = (0, math3_1.wrapPi)(b.yaw);
    // --- velocity in body frame ---
    const m = (0, math3_1.yawPitchRollToMatrix)(b.yaw, b.pitch, b.roll);
    // world -> body rotation = transpose
    const vw = (0, math3_1.sub)(b.vel, inp.wind(b.p));
    const vb = {
        x: m.fx * vw.x + m.fy * vw.y + m.fz * vw.z, // u: forward
        y: -m.rx * vw.x - m.ry * vw.y - m.rz * vw.z, // v: right (body Y)
        z: -m.ux * vw.x - m.uy * vw.y - m.uz * vw.z, // w: up (body Z)
    };
    // --- AoA from body w/u and geometric pitch ---
    const aoa = Math.atan2(vb.z, Math.max(Math.abs(vb.x), 1e-4)) - b.pitch;
    const alpha = (0, math3_1.clamp)(aoa, -0.9, 0.9);
    b.alpha = (0, math3_1.expDamp)(b.alpha, alpha, 8, dt);
    b.beta = Math.asin((0, math3_1.clamp)(vb.y / ((0, math3_1.len)(b.vel) || 1), -0.5, 0.5));
    // --- engine: RPM-like response + thermal stress + altitude derate ---
    const alt = b.p.y - 2; // pressure altitude approx
    const altF = (0, math3_1.clamp01)(1 - alt / (s.maxAlt - s.minAlt));
    const derate = altF < 0.02 ? 0 : Math.pow(Math.max(altF, 0), 0.42);
    const targetThrust = b.throttle * s.maxThrust * derate;
    st.thrust = (0, math3_1.expDamp)(st.thrust, targetThrust, 1.6, dt);
    const heatTarget = b.throttle * (0.45 + 0.55 * (0, math3_1.clamp01)(alt / 12000));
    st.heat = (0, math3_1.expDamp)(st.heat, heatTarget, 0.5, dt);
    const compress = st.thrust > 6000 && b.throttle > 0.6 && alt > 400 && Math.abs(b.pitchRate) > 0.22;
    st.compress = compress;
    if (compress)
        st.heat = Math.min(1, st.heat + 0.8 * dt);
    // --- aerodynamics: Cl from alpha with stall, Cd = profile + induced + gear/flap ---
    let cl;
    if (b.alpha > s.alphaMax) {
        cl = s.maxCl - s.clA * (b.alpha - s.alphaMax) * 1.35; // post-stall roll-off
    }
    else if (b.alpha < s.alphaMin) {
        cl = s.clMin;
    }
    else {
        cl = s.cl0 + s.clA * b.alpha;
    }
    cl += st.flapsExt * FLAP_CL;
    const clEff = Math.max(cl, -1.4);
    const rho = airDensity(b.p.y);
    st.rho = rho;
    const V = (0, math3_1.len)(b.vel);
    st.V = V;
    const q = 0.5 * rho * V * V;
    st.q = q;
    const cl2 = clEff * clEff;
    let cd = s.Cd0 + s.k * (cl2 / Math.max(s.S, 1e-3)) + b.gearExt * 0.032 + st.flapsExt * FLAP_CD;
    if (st.compress)
        cd += 0.05; // compressibility buffet
    const L = q * s.S * clEff;
    const D = q * s.S * cd;
    st.lift = L;
    st.drag = D;
    st.cl = clEff;
    st.cd = cd;
    st.stall = b.alpha > s.alphaMax * 0.82 || clEff < -0.8;
    st.inverted = b.roll > (0, math3_1.rad)(90) || b.roll < -(0, math3_1.rad)(90);
    st.overspeed = V > s.maxV;
    // --- forces in world frame ---
    const fwd = { x: m.fx, y: m.fy, z: m.fz };
    const upB = { x: m.ux, y: m.uy, z: m.uz };
    const rightB = { x: m.rx, y: m.ry, z: m.rz };
    const thrustF = (0, math3_1.scale)(fwd, st.thrust);
    const dragF = (0, math3_1.scale)(vw, -D / (V || 1));
    // lift along body-up (flipped if inverted -> "negative lift" pulls toward earth)
    const liftF = (0, math3_1.scale)(upB, L);
    const gF = (0, math3_1.scale)((0, math3_1.v)(0, -1, 0), s.mass * specs_1.G0);
    const a = (0, math3_1.scale)((0, math3_1.add)((0, math3_1.add)((0, math3_1.add)(gF, thrustF), dragF), liftF), 1 / s.mass);
    b.vel = (0, math3_1.add)(b.vel, (0, math3_1.scale)(a, dt));
    // --- roll-induced turn coupling (banking dynamics: horizontal component of lift) ---
    // Already included via liftF using body up; keep for g-load computation:
    const gLoad = (V > 1 ? (0, math3_1.dot)(liftF, (0, math3_1.v)(0, 1, 0)) / (s.mass * specs_1.G0) : 1);
    st.gLoad = (0, math3_1.expDamp)(st.gLoad, gLoad, 10, dt);
    // --- spin/roll runaway from high alpha + slip (dogfight danger) ---
    const slip = Math.abs(b.beta) > 0.25 && b.alpha > s.alphaMax * 0.6;
    if (slip)
        b.roll += Math.sign(b.rollRate || b.beta) * 0.35 * dt;
    // --- integrate position ---
    b.p = (0, math3_1.add)(b.p, (0, math3_1.scale)(b.vel, dt));
    // --- ground interaction ---
    const gh = inp.terrainHeight(b.p.x, b.p.z);
    st.agl = b.p.y - gh;
    st.vsi = b.vel.y;
    const onGround = st.agl <= 0.9 && b.p.y <= gh + 0.95 && b.vel.y < 0.5;
    st.onGround = onGround;
    if (onGround || st.agl <= 0.5) {
        const ground = Math.max(gh, -14);
        b.p.y = Math.max(b.p.y, ground + 0.9);
        // hard impact check
        const impactV = b.vel.y < -12 ? -b.vel.y : 0;
        const rollG = Math.abs(b.roll) > (0, math3_1.rad)(28) && V > 3;
        if (impactV > 12 || rollG && (st.agl < 0.2 || impactV > 4)) {
            st.crashed = true;
            st.crashTimer = 0;
        }
        else if (b.p.y <= ground + 0.91) {
            b.p.y = ground + 0.9;
            if (b.vel.y < 0)
                b.vel.y = 0;
            // ground friction: rolling resistance + braking + gear drag
            const fr = 0.0025 + (c.brakes ? 0.045 : 0) + b.gearExt * 0.004;
            const dec = fr * specs_1.G0 * dt;
            const hvel = { x: b.vel.x, y: 0, z: b.vel.z };
            const hl = (0, math3_1.len)(hvel);
            if (hl > 1e-3) {
                const nl = Math.max(0, hl - dec);
                b.vel.x = (hvel.x / hl) * nl;
                b.vel.z = (hvel.z / hl) * nl;
            }
            // align to ground: damp pitch to 0 on ground, prevent flipping
            b.pitch = (0, math3_1.expDamp)(b.pitch, 0, 4, dt);
            b.roll = (0, math3_1.expDamp)(b.roll, 0, 3, dt);
        }
    }
    // structural failure from sustained overload
    if (Math.abs(st.gLoad) > MAX_STALL_G) {
        st.crashed = true;
        st.crashTimer = 0;
    }
    // water impact is just ground (terrain height handles it)
    // --- weapons ---
    if (c.fire && inp.fireCallback) {
        inp.fireCallback({ x: b.p.x, y: b.p.y, z: b.p.z }, fwd, Math.abs(b.roll) > (0, math3_1.rad)(70) ? 0.35 : 0.12, 1);
    }
    st.gamma = Math.asin((0, math3_1.clamp)(b.vel.y / (V || 1), -1, 1));
    st.aoaDeg = (0, math3_1.deg)(b.alpha);
}
function speedKts(st) { return st.V * 1.94384; }
function speedKmh(st) { return st.V * 3.6; }
function ft(st) { return st.b.p.y * 3.28084; }
};

// ── module: src/world.ts ──
__mods["src/world.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RUNWAY = void 0;
exports.noise2 = noise2;
exports.fbm = fbm;
exports.ridged = ridged;
exports.terrainHeight = terrainHeight;
exports.groundHeight = groundHeight;
exports.buildWorld = buildWorld;
const specs_1 = require("./specs");
// ---------- seeded noise ----------
function mulberry32(seed) {
    let a = seed >>> 0;
    return () => {
        a |= 0;
        a = (a + 0x6D2B79F5) | 0;
        let t = Math.imul(a ^ (a >>> 15), 1 | a);
        t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
        return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
    };
}
const P = new Uint8Array(512);
{
    const rnd = mulberry32(1337);
    const base = new Uint8Array(256);
    for (let i = 0; i < 256; i++)
        base[i] = i;
    for (let i = 255; i > 0; i--) {
        const j = Math.floor(rnd() * (i + 1));
        const t = base[i];
        base[i] = base[j];
        base[j] = t;
    }
    for (let i = 0; i < 512; i++)
        P[i] = base[i & 255];
}
const fade = (t) => t * t * t * (t * (t * 6 - 15) + 10);
function grad2(h, x, y) {
    const g = h & 7;
    const u = g < 4 ? x : y;
    const w = g < 4 ? y : x;
    return ((g & 1) ? -u : u) + ((g & 2) ? -2 * w : 2 * w) * 0.5;
}
function noise2(x, y) {
    const X = Math.floor(x) & 255, Y = Math.floor(y) & 255;
    x -= Math.floor(x);
    y -= Math.floor(y);
    const u = fade(x), w = fade(y);
    const a = P[X + P[Y]], b = P[X + 1 + P[Y]], c = P[X + P[Y + 1]], d = P[X + 1 + P[Y + 1]];
    return ((grad2(a, x, y) * (1 - u) + grad2(b, x - 1, y) * u) * (1 - w) +
        (grad2(c, x, y - 1) * (1 - u) + grad2(d, x - 1, y - 1) * u) * w) * 0.5;
}
function fbm(x, y, oct, gain = 0.5) {
    let amp = 0.5, f = 1, sum = 0, norm = 0;
    for (let i = 0; i < oct; i++) {
        sum += amp * noise2(x * f, y * f);
        norm += amp;
        amp *= gain;
        f *= 2.02;
    }
    return sum / norm;
}
function ridged(x, y, oct) {
    let amp = 0.55, f = 1, sum = 0, norm = 0;
    for (let i = 0; i < oct; i++) {
        let n = 1 - Math.abs(noise2(x * f, y * f));
        n *= n;
        sum += amp * n;
        norm += amp;
        amp *= 0.52;
        f *= 2.05;
    }
    return sum / norm;
}
// ---------- terrain height ----------
// Runway: center (0, -1500), along Z from -2600..-400, width 42. Base elevation RUNWAY_Y.
exports.RUNWAY = { cx: 0, cz: -1500, half: 1150, width: 42, y: 24 };
function terrainHeight(x, z) {
    const nx = x / 9000, nz = z / 9000;
    const d = Math.sqrt(x * x + z * z) / specs_1.WORLD_RADIUS;
    const fall = 1 - smooth(clamp01(d / 0.92)); // island falloff -> coastline
    const low = fbm(nx + 3.7, nz - 1.2, 5) * 900;
    const ridge = ridged(nx * 0.55 + 8.1, nz * 0.55 + 4.4, 5) * 2300;
    const macro = fbm(nx * 0.18 + 12.3, nz * 0.18 + 6.6, 3) * 1500;
    let h = (low * 0.55 + ridge * 0.5) * (0.25 + 0.75 * fall) + macro * fall - 260 * (1 - fall);
    // keep the runway valley a guaranteed low corridor
    const flat = runwayFlatness(x, z);
    if (flat > 0)
        h = h * (1 - flat) + exports.RUNWAY.y * flat;
    return h;
}
const clamp01 = (x) => Math.min(1, Math.max(0, x));
const smooth = (t) => t * t * (3 - 2 * t);
function runwayFlatness(x, z) {
    const along = Math.abs(z - exports.RUNWAY.cz);
    const across = Math.abs(x - exports.RUNWAY.cx);
    const aA = 1 - smooth(clamp01((along - (exports.RUNWAY.half - 450)) / 700));
    const cA = 1 - smooth(clamp01((across - (exports.RUNWAY.width + 250)) / 500));
    return clamp01(Math.min(aA, cA));
}
function groundHeight(x, z) {
    return Math.max(terrainHeight(x, z), specs_1.WATER_Y);
}
function buildWorld(scene) {
    const root = new THREE.Group();
    scene.add(root);
    // lights (no shadow maps — perf over shadow realism at this scale)
    const hemi = new THREE.HemisphereLight(0xcfe4ff, 0x574f42, 0.85);
    const sun = new THREE.DirectionalLight(0xfff0d8, 1.55);
    sun.position.set(28000, 36000, -18000);
    const amb = new THREE.AmbientLight(0x8aa3c2, 0.35);
    root.add(hemi, sun, amb);
    // sky gradient background
    const skyC = document.createElement('canvas');
    skyC.width = 16;
    skyC.height = 256;
    const sctx = skyC.getContext('2d');
    const grad = sctx.createLinearGradient(0, 0, 0, 256);
    grad.addColorStop(0, '#2c5c9e');
    grad.addColorStop(0.42, '#7fa8d4');
    grad.addColorStop(0.62, '#c6dcec');
    grad.addColorStop(1, '#e8f1f7');
    sctx.fillStyle = grad;
    sctx.fillRect(0, 0, 16, 256);
    const skyTex = new THREE.CanvasTexture(skyC);
    scene.background = skyTex;
    scene.fog = new THREE.Fog(0xc3d6e6, 4000, 110000);
    // terrain grid
    const size = specs_1.WORLD_RADIUS * 2;
    const geo = new THREE.PlaneGeometry(size, size, specs_1.TERRAIN_SEG, specs_1.TERRAIN_SEG);
    geo.rotateX(-Math.PI / 2);
    const pos = geo.getAttribute('position');
    for (let i = 0; i < pos.count; i++) {
        pos.setY(i, terrainHeight(pos.getX(i), pos.getZ(i)));
    }
    geo.computeVertexNormals();
    const nrm = geo.getAttribute('normal');
    const colors = new Float32Array(pos.count * 3);
    const cLow = { r: 0.42, g: 0.56, b: 0.30 }; // grass
    const cLow2 = { r: 0.55, g: 0.62, b: 0.34 };
    const cRock = { r: 0.48, g: 0.44, b: 0.40 };
    const cSnow = { r: 0.92, g: 0.94, b: 0.96 };
    const cSand = { r: 0.78, g: 0.72, b: 0.52 };
    for (let i = 0; i < pos.count; i++) {
        const h = pos.getY(i);
        const ny = nrm.getY(i);
        const j = (noise2(pos.getX(i) / 600 + 40, pos.getZ(i) / 600 - 17) + 1) / 2;
        let r, g, b;
        if (h < specs_1.WATER_Y + 3) {
            r = cSand.r;
            g = cSand.g;
            b = cSand.b;
        }
        else {
            const lowMix = cLow.r + (cLow2.r - cLow.r) * j;
            const lowG = cLow.g + (cLow2.g - cLow.g) * j;
            const lowB = cLow.b + (cLow2.b - cLow.b) * j;
            const rockMix = smooth(clamp01((h - 420) / 480));
            const snowMix = smooth(clamp01((h - 1350) / 380)) * smooth(clamp01((ny - 0.55) / 0.3));
            const slopeRock = smooth(clamp01((0.82 - ny) / 0.22));
            r = lowMix + (cRock.r - lowMix) * Math.max(rockMix, slopeRock);
            g = lowG + (cRock.g - lowG) * Math.max(rockMix, slopeRock);
            b = lowB + (cRock.b - lowB) * Math.max(rockMix, slopeRock);
            r += (cSnow.r - r) * snowMix;
            g += (cSnow.g - g) * snowMix;
            b += (cSnow.b - b) * snowMix;
        }
        colors[i * 3] = r;
        colors[i * 3 + 1] = g;
        colors[i * 3 + 2] = b;
    }
    geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
    const tMat = new THREE.MeshStandardMaterial({ vertexColors: true, roughness: 0.94, metalness: 0.02 });
    const terrain = new THREE.Mesh(geo, tMat);
    root.add(terrain);
    // water
    const wGeo = new THREE.PlaneGeometry(size * 1.4, size * 1.4, 1, 1);
    wGeo.rotateX(-Math.PI / 2);
    const water = new THREE.Mesh(wGeo, new THREE.MeshStandardMaterial({
        color: 0x1c4f6e, roughness: 0.28, metalness: 0.55,
    }));
    water.position.y = specs_1.WATER_Y;
    root.add(water);
    // runway + markings + simple structures
    const rY = exports.RUNWAY.y;
    const asphalt = new THREE.MeshStandardMaterial({ color: 0x2a2d31, roughness: 0.9, metalness: 0.05 });
    const white = new THREE.MeshStandardMaterial({ color: 0xe8e8e8, roughness: 0.8 });
    const runway = new THREE.Mesh(new THREE.PlaneGeometry(exports.RUNWAY.width, exports.RUNWAY.half * 2 + 200), asphalt);
    runway.geometry.rotateX(-Math.PI / 2);
    runway.position.set(exports.RUNWAY.cx, rY + 0.25, exports.RUNWAY.cz);
    root.add(runway);
    const apron = new THREE.Mesh(new THREE.PlaneGeometry(340, 420), asphalt);
    apron.geometry.rotateX(-Math.PI / 2);
    apron.position.set(exports.RUNWAY.cx, rY + 0.2, exports.RUNWAY.cz - exports.RUNWAY.half - 160);
    root.add(apron);
    // threshold stripes + centerline dashes
    const stripeGeo = new THREE.PlaneGeometry(3, 40);
    for (const side of [-1, 1]) {
        for (let i = 0; i < 6; i++) {
            const s = new THREE.Mesh(stripeGeo, white);
            s.geometry.rotateX(-Math.PI / 2);
            s.position.set(exports.RUNWAY.cx - 15 + i * 6, rY + 0.4, exports.RUNWAY.cz + side * (exports.RUNWAY.half - 60));
            root.add(s);
        }
    }
    const dashGeo = new THREE.PlaneGeometry(1.6, 28);
    for (let z = -exports.RUNWAY.half + 120; z < exports.RUNWAY.half - 120; z += 120) {
        const d = new THREE.Mesh(dashGeo, white);
        d.geometry.rotateX(-Math.PI / 2);
        d.position.set(exports.RUNWAY.cx, rY + 0.4, exports.RUNWAY.cz + z);
        root.add(d);
    }
    // hangar + tower (boxes only)
    const hangarMat = new THREE.MeshStandardMaterial({ color: 0x8f969c, roughness: 0.7, metalness: 0.2 });
    const hangar = new THREE.Mesh(new THREE.BoxGeometry(60, 22, 50), hangarMat);
    hangar.position.set(-120, rY + 11, exports.RUNWAY.cz - exports.RUNWAY.half - 180);
    const tower = new THREE.Mesh(new THREE.BoxGeometry(14, 42, 14), hangarMat);
    tower.position.set(120, rY + 21, exports.RUNWAY.cz - exports.RUNWAY.half - 180);
    const cab = new THREE.Mesh(new THREE.BoxGeometry(20, 10, 20), new THREE.MeshStandardMaterial({ color: 0x2f3d4a, roughness: 0.3, metalness: 0.4 }));
    cab.position.set(120, rY + 46, exports.RUNWAY.cz - exports.RUNWAY.half - 180);
    root.add(hangar, tower, cab);
    // clouds — point sprites with soft radial texture
    const cc = document.createElement('canvas');
    cc.width = cc.height = 64;
    const cctx = cc.getContext('2d');
    const rg = cctx.createRadialGradient(32, 32, 2, 32, 32, 30);
    rg.addColorStop(0, 'rgba(255,255,255,0.85)');
    rg.addColorStop(0.55, 'rgba(255,255,255,0.35)');
    rg.addColorStop(1, 'rgba(255,255,255,0)');
    cctx.fillStyle = rg;
    cctx.fillRect(0, 0, 64, 64);
    const cloudTex = new THREE.CanvasTexture(cc);
    const rnd = mulberry32(99);
    const N = 240;
    const cpos = new Float32Array(N * 3);
    for (let i = 0; i < N; i++) {
        const ang = rnd() * Math.PI * 2;
        const rr = Math.pow(rnd(), 0.6) * specs_1.WORLD_RADIUS * 1.3;
        cpos[i * 3] = Math.cos(ang) * rr;
        cpos[i * 3 + 1] = 4500 + rnd() * 9500;
        cpos[i * 3 + 2] = Math.sin(ang) * rr;
    }
    const cgeo = new THREE.BufferGeometry();
    cgeo.setAttribute('position', new THREE.BufferAttribute(cpos, 3));
    const clouds = new THREE.Points(cgeo, new THREE.PointsMaterial({
        size: 2600, map: cloudTex, transparent: true, opacity: 0.5,
        depthWrite: false, sizeAttenuation: true, fog: false,
    }));
    root.add(clouds);
    return { root, water, clouds };
}
};

// ── module: src/planes.ts ──
__mods["src/planes.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAircraft = buildAircraft;
function stdMat(color, rough = 0.45, metal = 0.55) {
    return new THREE.MeshStandardMaterial({ color, roughness: rough, metalness: metal });
}
// ---------- FA-20 FALCON: modern twin ----------
function buildFalcon(s) {
    const g = new THREE.Group();
    const fus = stdMat(0xb9c2cc, 0.38, 0.65);
    const dark = stdMat(0x5c6670, 0.5, 0.6);
    const red = stdMat(0xb03a3a, 0.5, 0.3);
    // fuselage: nose + body + tail cone
    const nose = new THREE.Mesh(new THREE.BoxGeometry(2.6, 2.6, 7), fus);
    nose.position.set(0, 0, 9.5);
    const noseTip = new THREE.Mesh(new THREE.BoxGeometry(1.6, 1.6, 3), fus);
    noseTip.position.set(0, 0, 13.6);
    const body = new THREE.Mesh(new THREE.BoxGeometry(4.2, 4.0, 14), fus);
    body.position.set(0, 0, 2.5);
    const belly = new THREE.Mesh(new THREE.BoxGeometry(3.6, 0.8, 12), dark);
    belly.position.set(0, -2.0, 2.5);
    const tailCone = new THREE.Mesh(new THREE.BoxGeometry(2.2, 2.4, 8), fus);
    tailCone.position.set(0, 0.4, -10);
    const tailTip = new THREE.Mesh(new THREE.BoxGeometry(1.2, 1.4, 3), dark);
    tailTip.position.set(0, 0.6, -14.6);
    g.add(nose, noseTip, body, belly, tailCone, tailTip);
    // canopy
    const canopy = new THREE.Mesh(new THREE.BoxGeometry(3.0, 1.8, 6), stdMat(0x18242e, 0.15, 0.9));
    canopy.position.set(0, 2.4, 5.5);
    g.add(canopy);
    // delta main wings
    const wingGeo = new THREE.BoxGeometry(15, 0.5, 8.5);
    wingGeo.translate(7.6, 0, -1.2);
    const wingL = new THREE.Mesh(wingGeo, fus);
    const wingR = new THREE.Mesh(wingGeo, fus);
    wingR.rotation.y = Math.PI;
    wingL.position.set(0, -0.4, 2.2);
    wingR.position.set(0, -0.4, 2.2);
    // wingtip stripes
    const tipL = new THREE.Mesh(new THREE.BoxGeometry(2.6, 0.55, 8.5), red);
    tipL.position.set(15.2, 0, -1.2);
    wingL.add(tipL);
    const tipR = new THREE.Mesh(new THREE.BoxGeometry(2.6, 0.55, 8.5), red);
    tipR.position.set(-15.2, 0, -1.2);
    wingR.add(tipR);
    // leading edge slats
    const slatL = new THREE.Mesh(new THREE.BoxGeometry(12, 0.3, 1.6), dark);
    slatL.position.set(6.5, 0.25, 3.2);
    const slatR = slatL.clone();
    slatR.position.x = -6.5;
    wingL.add(slatL);
    wingR.add(slatR);
    g.add(wingL, wingR);
    // twin engines
    for (const sx of [-1, 1]) {
        const eng = new THREE.Mesh(new THREE.BoxGeometry(3.0, 3.0, 6.5), dark);
        eng.position.set(sx * 3.2, -1.4, -2.5);
        g.add(eng);
        const ab = new THREE.Mesh(new THREE.BoxGeometry(2.0, 2.0, 0.4), new THREE.MeshBasicMaterial({ color: 0xffa640, transparent: true, opacity: 0.9 }));
        ab.position.set(sx * 3.2, -1.4, -6.0);
        ab.name = 'ab' + sx;
        g.add(ab);
    }
    // twin tails
    for (const sx of [-1, 1]) {
        const vfin = new THREE.Mesh(new THREE.BoxGeometry(0.5, 5.4, 6), fus);
        vfin.position.set(sx * 3.1, 3.4, -9.5);
        const vfinTop = new THREE.Mesh(new THREE.BoxGeometry(0.55, 1.6, 2), red);
        vfinTop.position.set(sx * 3.1, 6.1, -11.2);
        g.add(vfin, vfinTop);
    }
    const hstab = new THREE.Mesh(new THREE.BoxGeometry(9, 0.4, 4), fus);
    hstab.position.set(0, 0.6, -12.5);
    g.add(hstab);
    // gear
    const wheel = new THREE.BoxGeometry(0.7, 0.7, 0.7);
    const wheelMat = stdMat(0x1a1a1a, 0.8, 0.1);
    const mkGear = (x, z) => {
        const gr = new THREE.Group();
        const strut = new THREE.Mesh(new THREE.BoxGeometry(0.35, 1.4, 0.35), dark);
        const w = new THREE.Mesh(wheel, wheelMat);
        gr.add(strut, w);
        gr.position.set(x, -2.6, z);
        return gr;
    };
    const gearL = mkGear(2.2, 2.5);
    const gearR = mkGear(-2.2, 2.5);
    const gearC = mkGear(0, 9.5);
    g.add(gearL, gearR, gearC);
    const sc = 0.52;
    g.scale.setScalar(sc);
    return { root: g, gearL, gearR, gearC, afterburner: g.children.find(c => c.name === 'ab1'), scale: sc };
}
// ---------- VT-9 SHRIKE: turboprop trainer ----------
function buildShrike(s) {
    const g = new THREE.Group();
    const fus = stdMat(0xcfd6da, 0.4, 0.5);
    const blue = stdMat(0x2a5f8f, 0.45, 0.4);
    const red = stdMat(0xc0392b, 0.5, 0.3);
    const dark = stdMat(0x3a4148, 0.6, 0.5);
    const nose = new THREE.Mesh(new THREE.BoxGeometry(2.4, 2.4, 5), fus);
    nose.position.set(0, 0, 5.5);
    const spinner = new THREE.Mesh(new THREE.BoxGeometry(1.2, 1.2, 1.6), red);
    spinner.position.set(0, 0, 8.6);
    const body = new THREE.Mesh(new THREE.BoxGeometry(3.0, 3.0, 11), fus);
    body.position.set(0, 0, -0.5);
    const tail = new THREE.Mesh(new THREE.BoxGeometry(1.4, 1.8, 6), fus);
    tail.position.set(0, 0.2, -8);
    g.add(nose, spinner, body, tail);
    // high-wing
    const wing = new THREE.Mesh(new THREE.BoxGeometry(17.5, 0.6, 5.2), fus);
    wing.position.set(0, 1.9, 1.4);
    const tipL = new THREE.Mesh(new THREE.BoxGeometry(3, 0.65, 5.2), blue);
    tipL.position.set(7.3, 0, 0);
    const tipR = new THREE.Mesh(new THREE.BoxGeometry(3, 0.65, 5.2), blue);
    tipR.position.set(-7.3, 0, 0);
    wing.add(tipL, tipR);
    g.add(wing);
    // single engine nacelle + prop
    const nac = new THREE.Mesh(new THREE.BoxGeometry(2.2, 2.2, 4.6), blue);
    nac.position.set(0, 0.4, 6.8);
    g.add(nac);
    const prop = new THREE.Group();
    const blade = new THREE.Mesh(new THREE.BoxGeometry(9.5, 0.35, 0.5), dark);
    const blade2 = blade.clone();
    blade2.rotation.z = Math.PI / 2;
    prop.add(blade, blade2);
    prop.position.set(0, 0.4, 9.2);
    g.add(prop);
    // canopy
    const canopy = new THREE.Mesh(new THREE.BoxGeometry(2.4, 1.6, 6.5), stdMat(0x141e26, 0.12, 0.9));
    canopy.position.set(0, 1.7, 3.4);
    g.add(canopy);
    // V-tail
    for (const sx of [-1, 1]) {
        const vt = new THREE.Mesh(new THREE.BoxGeometry(0.4, 4.6, 4.4), fus);
        vt.position.set(sx * 1.9, 2.2, -9.2);
        vt.rotation.y = -sx * 0.5;
        g.add(vt);
    }
    const tipT = new THREE.Mesh(new THREE.BoxGeometry(0.45, 1.4, 1.7), red);
    tipT.position.set(1.9, 4.2, -9.6);
    g.add(tipT);
    const tipT2 = tipT.clone();
    tipT2.position.x = -1.9;
    g.add(tipT2);
    // gear (fixed)
    const wheel = new THREE.BoxGeometry(0.8, 0.8, 0.8);
    const wheelMat = stdMat(0x161616, 0.85, 0.1);
    const mk = (x, z) => {
        const gr = new THREE.Group();
        const strut = new THREE.Mesh(new THREE.BoxGeometry(0.3, 1.2, 0.3), dark);
        gr.add(strut, new THREE.Mesh(wheel, wheelMat));
        gr.position.set(x, -2.1, z);
        return gr;
    };
    const gearL = mk(2.0, 1.6);
    const gearR = mk(-2.0, 1.6);
    const gearC = mk(0, 6.6);
    g.add(gearL, gearR, gearC);
    const sc2 = 1.0;
    g.scale.setScalar(sc2);
    return { root: g, prop, gearL, gearR, gearC, scale: sc2 };
}
// ---------- FX-12 VIPER: supersonic interceptor ----------
function buildViper(s) {
    const g = new THREE.Group();
    const fus = stdMat(0x4d5a66, 0.32, 0.72);
    const panel = stdMat(0x2e3842, 0.4, 0.6);
    const red = stdMat(0xd8452e, 0.5, 0.35);
    const glass = stdMat(0x0e1620, 0.1, 0.95);
    // long slender fuselage
    const nose = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.8, 9), fus);
    nose.position.set(0, 0, 12);
    const pitot = new THREE.Mesh(new THREE.BoxGeometry(0.18, 0.18, 2.6), red);
    pitot.position.set(0, 0, 17.4);
    const body = new THREE.Mesh(new THREE.BoxGeometry(3.0, 3.0, 16), fus);
    body.position.set(0, 0, 1);
    const bellyFin = new THREE.Mesh(new THREE.BoxGeometry(0.4, 2.2, 4.4), panel);
    bellyFin.position.set(0, -2.0, -3.5);
    const tail = new THREE.Mesh(new THREE.BoxGeometry(1.6, 1.6, 8), panel);
    tail.position.set(0, 0, -9.5);
    g.add(nose, pitot, body, bellyFin, tail);
    // single bubble canopy
    const canopy = new THREE.Mesh(new THREE.BoxGeometry(2.2, 1.7, 5.6), glass);
    canopy.position.set(0, 1.8, 4.6);
    g.add(canopy);
    // long thin sweep-ish wings (angled boxes)
    const wingGeo = new THREE.BoxGeometry(10.5, 0.42, 5.6);
    wingGeo.translate(5.4, 0, -0.8);
    wingGeo.rotateZ(-0.10);
    const wingL = new THREE.Mesh(wingGeo, fus);
    wingL.position.set(0, -0.6, 1.6);
    const wingR = new THREE.Mesh(wingGeo.clone(), fus);
    wingR.rotation.y = Math.PI;
    wingR.position.set(0, -0.6, 1.6);
    const tipL = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.46, 5.6), red);
    tipL.position.set(10.2, 0, -0.9);
    wingL.add(tipL);
    const tipR = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.46, 5.6), red);
    tipR.position.set(-10.2, 0, -0.9);
    wingR.add(tipR);
    g.add(wingL, wingR);
    // single big engine + afterburner
    const eng = new THREE.Mesh(new THREE.BoxGeometry(2.6, 2.8, 5), panel);
    eng.position.set(0, -0.5, -8.5);
    g.add(eng);
    const ab = new THREE.Mesh(new THREE.BoxGeometry(1.8, 1.8, 0.4), new THREE.MeshBasicMaterial({ color: 0xff9040, transparent: true, opacity: 0.9 }));
    ab.position.set(0, -0.5, -11.2);
    ab.name = 'ab';
    g.add(ab);
    // tall single v-fin
    const vfin = new THREE.Mesh(new THREE.BoxGeometry(0.5, 7.2, 7.4), fus);
    vfin.position.set(0, 4.0, -8.6);
    const vfinRed = new THREE.Mesh(new THREE.BoxGeometry(0.55, 2.0, 2.2), red);
    vfinRed.position.set(0, 7.2, -10.6);
    g.add(vfin, vfinRed);
    // retractable gear
    const wheel = new THREE.BoxGeometry(0.6, 0.6, 0.6);
    const wheelMat = stdMat(0x141414, 0.85, 0.1);
    const mk = (x, z) => {
        const gr = new THREE.Group();
        const strut = new THREE.Mesh(new THREE.BoxGeometry(0.3, 1.5, 0.3), panel);
        gr.add(strut, new THREE.Mesh(wheel, wheelMat));
        gr.position.set(x, -2.6, z);
        return gr;
    };
    const gearL = mk(1.7, 3.5);
    const gearR = mk(-1.7, 3.5);
    const gearC = mk(0, 12.5);
    g.add(gearL, gearR, gearC);
    const sc3 = 0.62;
    g.scale.setScalar(sc3);
    return { root: g, gearL, gearR, gearC, afterburner: ab, scale: sc3 };
}
function buildAircraft(spec) {
    switch (spec.id) {
        case 'shrike': return buildShrike(spec);
        case 'viper': return buildViper(spec);
        default: return buildFalcon(spec);
    }
}
};

// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.audio = void 0;
class AudioEngine {
    constructor() {
        this.ctx = null;
        this.noiseBuf = null;
        this.lastHit = 0;
        this.enabled = true;
        this.muted = false;
    }
    init() {
        if (this.ctx)
            return;
        try {
            this.ctx = new AudioContext();
        }
        catch {
            return;
        }
        const c = this.ctx;
        this.master = c.createGain();
        this.master.gain.value = this.muted ? 0 : 0.9;
        this.master.connect(c.destination);
        // white noise buffer
        const len = c.sampleRate * 2;
        this.noiseBuf = c.createBuffer(1, len, c.sampleRate);
        const d = this.noiseBuf.getChannelData(0);
        for (let i = 0; i < len; i++)
            d[i] = Math.random() * 2 - 1;
        // --- engine: two detuned oscillators (fundamental + octave) through lowpass ---
        this.engGain = c.createGain();
        this.engGain.gain.value = 0;
        const engLP = c.createBiquadFilter();
        engLP.type = 'lowpass';
        engLP.frequency.value = 500;
        engLP.Q.value = 1.2;
        this.engOsc1 = c.createOscillator();
        this.engOsc1.type = 'sawtooth';
        this.engOsc1.frequency.value = 55;
        this.engOsc2 = c.createOscillator();
        this.engOsc2.type = 'square';
        this.engOsc2.frequency.value = 112;
        const o2g = c.createGain();
        o2g.gain.value = 0.4;
        this.engOsc1.connect(engLP);
        this.engOsc2.connect(o2g);
        o2g.connect(engLP);
        engLP.connect(this.engGain);
        this.engGain.connect(this.master);
        this.engOsc1.start();
        this.engOsc2.start();
        // --- aero noise: bandpass noise (whoosh), gain by q and alpha ---
        this.noiseSrc = c.createBufferSource();
        this.noiseSrc.buffer = this.noiseBuf;
        this.noiseSrc.loop = true;
        this.noiseFilter = c.createBiquadFilter();
        this.noiseFilter.type = 'bandpass';
        this.noiseFilter.frequency.value = 500;
        this.noiseFilter.Q.value = 0.8;
        this.noiseGain = c.createGain();
        this.noiseGain.gain.value = 0;
        this.noiseSrc.connect(this.noiseFilter);
        this.noiseFilter.connect(this.noiseGain);
        this.noiseGain.connect(this.master);
        this.noiseSrc.start();
        // --- gust/wind ambient: lowpass noise, slow LFO ---
        const gustSrc = c.createBufferSource();
        gustSrc.buffer = this.noiseBuf;
        gustSrc.loop = true;
        const gustLP = c.createBiquadFilter();
        gustLP.type = 'lowpass';
        gustLP.frequency.value = 220;
        this.gustGain = c.createGain();
        this.gustGain.gain.value = 0.05;
        const lfo = c.createOscillator();
        lfo.frequency.value = 0.11;
        const lfoG = c.createGain();
        lfoG.gain.value = 0.03;
        lfo.connect(lfoG);
        lfoG.connect(this.gustGain.gain);
        gustSrc.connect(gustLP);
        gustLP.connect(this.gustGain);
        this.gustGain.connect(this.master);
        gustSrc.start();
        lfo.start();
        // --- distant mountain rumble: very low thump loop ---
        const rumSrc = c.createBufferSource();
        rumSrc.buffer = this.noiseBuf;
        rumSrc.loop = true;
        const rumLP = c.createBiquadFilter();
        rumLP.type = 'lowpass';
        rumLP.frequency.value = 70;
        this.rumbleGain = c.createGain();
        this.rumbleGain.gain.value = 0.0;
        const rlfo = c.createOscillator();
        rlfo.frequency.value = 0.05;
        const rlfoG = c.createGain();
        rlfoG.gain.value = 0.02;
        rlfo.connect(rlfoG);
        rlfoG.connect(this.rumbleGain.gain);
        rumSrc.connect(rumLP);
        rumLP.connect(this.rumbleGain);
        this.rumbleGain.connect(this.master);
        rumSrc.start();
        rlfo.start();
    }
    setMuted(m) {
        this.muted = m;
        if (this.ctx)
            this.master.gain.value = m ? 0 : 0.9;
    }
    update(f, dt) {
        if (!this.ctx || this.ctx.state !== 'running')
            return;
        const c = this.ctx;
        const t = c.currentTime;
        // engine: frequency from throttle (RPM feel), gain from throttle + airspeed
        const freq = 42 + f.throttle * 190 + f.V * 0.22;
        this.engOsc1.frequency.setTargetAtTime(freq, t, 0.12);
        this.engOsc2.frequency.setTargetAtTime(freq * 2.03, t, 0.12);
        const engVol = f.crashed
            ? 0.05
            : (0.05 + f.throttle * 0.4) * (0.55 + 0.45 * Math.min(f.V / 160, 1));
        this.engGain.gain.setTargetAtTime(engVol, t, 0.15);
        // aero noise: scales with dynamic pressure, whistle rises with AoA via freq
        const q = 0.5 * 1.225 * f.V * f.V * Math.exp(-f.alt / 8500);
        const nv = Math.min(0.5, (q / 90000) * 0.55 + (f.onGround ? 0.05 : 0));
        this.noiseGain.gain.setTargetAtTime(nv, t, 0.2);
        this.noiseFilter.frequency.setTargetAtTime(280 + f.V * 7 + f.gear * 120, t, 0.2);
        // gust + rumble: scale with altitude (thinner air = sharper gusts) & speed
        this.gustGain.gain.setTargetAtTime(0.02 + f.V / 600 + f.alt / 40000, t, 0.8);
        this.rumbleGain.gain.setTargetAtTime(0.015 * Math.max(0, 1 - f.alt / 20000), t, 1.2);
        // impact thump when touching ground
        if (f.onGround && f.V > 4 && c.currentTime - this.lastHit > 0.25) {
            this.lastHit = c.currentTime;
            const o = c.createOscillator();
            o.type = 'sine';
            o.frequency.value = 60;
            const g = c.createGain();
            g.gain.setValueAtTime(0.4, t);
            g.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
            o.connect(g);
            g.connect(this.master);
            o.start(t);
            o.stop(t + 0.15);
        }
    }
    gunshot() {
        if (!this.ctx || this.ctx.state !== 'running')
            return;
        const c = this.ctx, t = c.currentTime;
        const o = c.createOscillator();
        o.type = 'triangle';
        o.frequency.setValueAtTime(180, t);
        o.frequency.exponentialRampToValueAtTime(48, t + 0.09);
        const g = c.createGain();
        g.gain.setValueAtTime(0.5, t);
        g.gain.exponentialRampToValueAtTime(0.001, t + 0.12);
        o.connect(g);
        g.connect(this.master);
        o.start(t);
        o.stop(t + 0.14);
        // crack
        const n = c.createBufferSource();
        if (this.noiseBuf) {
            n.buffer = this.noiseBuf;
        }
        const hp = c.createBiquadFilter();
        hp.type = 'highpass';
        hp.frequency.value = 2000;
        const ng = c.createGain();
        ng.gain.setValueAtTime(0.35, t);
        ng.gain.exponentialRampToValueAtTime(0.001, t + 0.08);
        n.connect(hp);
        hp.connect(ng);
        ng.connect(this.master);
        n.start(t);
        n.stop(t + 0.1);
    }
    explosion(big) {
        if (!this.ctx || this.ctx.state !== 'running')
            return;
        const c = this.ctx, t = c.currentTime;
        const o = c.createOscillator();
        o.type = 'sine';
        o.frequency.setValueAtTime(big ? 70 : 110, t);
        o.frequency.exponentialRampToValueAtTime(30, t + (big ? 0.9 : 0.4));
        const g = c.createGain();
        g.gain.setValueAtTime(big ? 1.0 : 0.55, t);
        g.gain.exponentialRampToValueAtTime(0.001, t + (big ? 1.1 : 0.5));
        o.connect(g);
        g.connect(this.master);
        o.start(t);
        o.stop(t + (big ? 1.2 : 0.6));
        const n = c.createBufferSource();
        if (this.noiseBuf)
            n.buffer = this.noiseBuf;
        const lp = c.createBiquadFilter();
        lp.type = 'lowpass';
        lp.frequency.setValueAtTime(900, t);
        lp.frequency.exponentialRampToValueAtTime(120, t + 0.5);
        const ng = c.createGain();
        ng.gain.setValueAtTime(0.8, t);
        ng.gain.exponentialRampToValueAtTime(0.001, t + 0.6);
        n.connect(lp);
        lp.connect(ng);
        ng.connect(this.master);
        n.start(t);
        n.stop(t + 0.7);
    }
    resume() {
        if (this.ctx && this.ctx.state === 'suspended')
            this.ctx.resume().catch(() => undefined);
    }
}
exports.audio = new AudioEngine();
};

// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Hud = void 0;
exports.makeHudRefs = makeHudRefs;
const physics_1 = require("./physics");
const math3_1 = require("./math3");
class Hud {
    constructor(refs) {
        this.acc = 0;
        this.msgTO = 0;
        this.refs = refs;
    }
    msg(text, ms = 2600) {
        const el = this.refs.msg;
        el.textContent = text;
        el.style.opacity = '1';
        clearTimeout(this.msgTO);
        this.msgTO = window.setTimeout(() => { el.style.opacity = '0'; }, ms);
    }
    banner(title, sub) {
        this.refs.bannerT.textContent = title;
        this.refs.bannerSub.textContent = sub;
    }
    setMode(name) { this.refs.mode.textContent = name; }
    setCallsign(name) { this.refs.callsign.textContent = name; }
    tick(data, dt) {
        const r = this.refs;
        const st = data.st;
        const accent = '#' + st.spec.accent.toString(16).padStart(6, '0');
        drawAttitude(r.attC, st.b.pitch, st.b.roll, accent);
        drawHeading(r.hdgC, st.b.yaw, accent);
        if (data.aimInfo)
            drawAim(r.aimC, data.aimInfo);
        this.acc += dt;
        if (this.acc < 0.1)
            return;
        this.acc = 0;
        r.spd.textContent = (0, physics_1.speedKts)(st).toFixed(0);
        const vs = Math.sqrt(1.32 / (0.5 * st.rho * st.spec.S * st.spec.maxCl)); // stall speed m/s (cl0 bonus ignored)
        r.spd.style.color = st.V < vs * 1.05 ? '#ff6b5e' : '#f2f8ff';
        r.alt.textContent = Math.round(st.b.p.y * 3.28084).toLocaleString();
        const vsi = Math.round(st.vsi * 196.85);
        r.vsi.textContent = (vsi > 0 ? '+' : '') + vsi.toLocaleString();
        r.aoa.textContent = st.aoaDeg.toFixed(1);
        r.g.textContent = st.gLoad.toFixed(1);
        r.thr.style.width = (st.b.throttle * 100).toFixed(0) + '%';
        r.thr.style.background = accent;
        r.scoreN.textContent = data.score.toLocaleString();
        r.ammoN.textContent = String(data.ammo);
        const w = [];
        if (st.stall)
            w.push('STALL');
        if (st.inverted)
            w.push('INVERTED');
        if (st.overspeed)
            w.push('OVERSPEED');
        if (st.compress)
            w.push('COMPRESSOR');
        if (st.heat > 0.85)
            w.push('ENG HOT');
        if (!st.onGround && st.vsi < -14 && st.V > 25 && st.agl < 200)
            w.push('SINK RATE');
        r.warn.innerHTML = w.map(x => `<span class="warnchip${x === 'STALL' || x === 'OVERSPEED' || x === 'COMPRESSOR' ? ' hot' : ''}">${x}</span>`).join('');
    }
}
exports.Hud = Hud;
function makeHudRefs(root) {
    const el = (id) => root.querySelector(id);
    const canvas = (id) => root.querySelector(id);
    return {
        root,
        spd: el('#hud-spd'),
        alt: el('#hud-alt'),
        vsi: el('#hud-vsi'),
        thr: el('#hud-thr'),
        aoa: el('#hud-aoa'),
        g: el('#hud-g'),
        callsign: el('#hud-callsign'),
        mode: el('#hud-mode'),
        msg: el('#hud-msg'),
        warn: el('#hud-warn'),
        scoreBox: el('#score-box'),
        scoreN: el('#hud-score-n'),
        ammoBox: el('#hud-ammo'),
        ammoN: el('#hud-ammo-n'),
        attC: canvas('#att-canvas'),
        hdgC: canvas('#hdg-canvas'),
        aimC: canvas('#aim-canvas'),
        bannerT: el('#hud-banner-t'),
        bannerSub: el('#hud-banner-sub'),
    };
}
// ---------- canvas indicators ----------
function drawAttitude(cv, pitch, roll, accent) {
    const c = cv.getContext('2d');
    const W = cv.width, H = cv.height;
    c.clearRect(0, 0, W, H);
    c.save();
    c.beginPath();
    c.arc(W / 2, H / 2, H / 2 - 2, 0, 7);
    c.clip();
    c.translate(W / 2, H / 2);
    c.rotate(-roll);
    const ppx = pitch * 2.4;
    const hor = H / 2 - ppx;
    c.fillStyle = 'rgba(40,86,150,0.4)';
    c.fillRect(-W, -H * 2, W * 2, Math.max(0, hor + H * 2));
    c.fillStyle = 'rgba(92,70,48,0.42)';
    c.fillRect(-W, hor, W * 2, H * 2 + 10);
    c.strokeStyle = 'rgba(255,255,255,0.85)';
    c.lineWidth = 2;
    c.beginPath();
    c.moveTo(-W, hor);
    c.lineTo(W, hor);
    c.stroke();
    c.lineWidth = 1;
    c.font = '10px monospace';
    c.textAlign = 'center';
    for (let p = -60; p <= 60; p += 10) {
        if (p === 0)
            continue;
        const y = hor - p * 2.4;
        if (y < -H * 2 || y > H * 2)
            continue;
        c.strokeStyle = 'rgba(255,255,255,0.55)';
        c.beginPath();
        c.moveTo(-26, y);
        c.lineTo(-9, y);
        c.moveTo(9, y);
        c.lineTo(26, y);
        c.stroke();
        c.fillStyle = 'rgba(255,255,255,0.65)';
        c.fillText(String(Math.abs(p)), -36, y + 3);
        c.fillText(String(Math.abs(p)), 36, y + 3);
    }
    c.restore();
    c.strokeStyle = accent;
    c.lineWidth = 3;
    c.beginPath();
    c.moveTo(W / 2 - 34, H / 2);
    c.lineTo(W / 2 - 12, H / 2);
    c.moveTo(W / 2 + 12, H / 2);
    c.lineTo(W / 2 + 34, H / 2);
    c.moveTo(W / 2, H / 2 - 6);
    c.lineTo(W / 2, H / 2 + 6);
    c.stroke();
    c.fillStyle = accent;
    c.beginPath();
    c.arc(W / 2, H / 2, 2.5, 0, 7);
    c.fill();
}
function drawHeading(cv, yaw, accent) {
    const c = cv.getContext('2d');
    const W = cv.width, H = cv.height;
    c.clearRect(0, 0, W, H);
    const hdg = (((0, math3_1.deg)(yaw) % 360) + 360) % 360;
    const per = W / 180;
    c.save();
    c.beginPath();
    c.rect(0, 0, W, H);
    c.clip();
    const cx = W / 2;
    for (let d = -120; d <= 120; d += 5) {
        const degd = ((hdg + d) % 360 + 360) % 360;
        if (Math.abs(((hdg + d) % 360 + 540) % 360 - 180) > 180)
            continue;
        const x = cx + d * per;
        const major = degd % 30 === 0;
        c.strokeStyle = 'rgba(255,255,255,0.8)';
        c.lineWidth = 1;
        c.beginPath();
        c.moveTo(x, H - (major ? 11 : 6));
        c.lineTo(x, H);
        c.stroke();
        if (major) {
            c.fillStyle = 'rgba(255,255,255,0.85)';
            c.font = '9px monospace';
            c.textAlign = 'center';
            const label = degd === 0 ? 'N' : degd === 90 ? 'E' : degd === 180 ? 'S' : degd === 270 ? 'W' : String(Math.round(degd / 10));
            c.fillText(label, x, H - 13);
        }
    }
    c.strokeStyle = accent;
    c.lineWidth = 2;
    c.beginPath();
    c.moveTo(cx, 2);
    c.lineTo(cx, H - 8);
    c.stroke();
    c.fillStyle = 'rgba(255,255,255,0.9)';
    c.font = '11px monospace';
    c.textAlign = 'center';
    c.fillText(String(Math.round(hdg)).padStart(3, '0'), cx, 12);
    c.restore();
}
function drawAim(cv, a) {
    const c = cv.getContext('2d');
    const W = cv.width, H = cv.height;
    c.clearRect(0, 0, W, H);
    const cx = W / 2, cy = H / 2, R = 46;
    const off = Math.min(a.offDeg, 90);
    const col = a.locked ? '#ff3b30' : '#ffcf5c';
    c.strokeStyle = 'rgba(255,255,255,0.45)';
    c.lineWidth = 1;
    c.beginPath();
    c.arc(cx, cy, R, 0, 7);
    c.stroke();
    c.strokeStyle = col;
    c.lineWidth = 2;
    c.beginPath();
    for (const s of [-1, 1]) {
        c.moveTo(cx + s * 9, cy - 9);
        c.lineTo(cx + s * 3, cy);
        c.lineTo(cx + s * 9, cy + 9);
    }
    c.stroke();
    c.fillStyle = col;
    c.beginPath();
    c.arc(cx, cy, 2.5, 0, 7);
    c.fill();
    c.font = '10px monospace';
    c.fillStyle = col;
    c.textAlign = 'center';
    c.fillText(a.locked ? 'LOCK' : off.toFixed(0) + '\u00B0', cx, cy + R + 12);
    if (a.dist > 0) {
        c.fillStyle = 'rgba(255,255,255,0.6)';
        c.fillText((a.dist / 1000).toFixed(1) + ' km', cx, cy + R + 24);
    }
}
};

// ── module: src/scenarios.ts ──
__mods["src/scenarios.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DOGFIGHT_TIME = exports.precisionInitial = void 0;
exports.spawnPrecision = spawnPrecision;
exports.updatePrecision = updatePrecision;
exports.spawnDogfight = spawnDogfight;
exports.dogfightWind = dogfightWind;
exports.updateDogfight = updateDogfight;
const physics_1 = require("./physics");
const math3_1 = require("./math3");
const world_1 = require("./world");
const math3_2 = require("./math3");
const precisionInitial = () => ({
    phase: 'ready', airborneTime: 0, score: 0, msgCd: 0,
});
exports.precisionInitial = precisionInitial;
function spawnPrecision() {
    // runway runs z = -2600..-400 (center -1500); start south end, nose toward -z
    return { p: (0, math3_1.v)(world_1.RUNWAY.cx, world_1.RUNWAY.y + 1, -400 - 260), yaw: Math.PI, speed: 0 };
}
function updatePrecision(st, ps, dt, msg) {
    ps.msgCd = Math.max(0, ps.msgCd - dt);
    const say = (t) => { if (ps.msgCd <= 0) {
        msg(t);
        ps.msgCd = 1.8;
    } };
    if (ps.phase === 'ready') {
        if (st.onGround && st.b.throttle > 0.15) {
            ps.phase = 'flying';
            say('ROLLING — rotate above ' + Math.round(st.spec.vRef * 1.15 * 3.6) + ' km/h');
        }
    }
    else if (ps.phase === 'flying') {
        ps.airborneTime = st.onGround ? 0 : ps.airborneTime + dt;
        if (ps.airborneTime > 1 && st.onGround) {
            const sink = -st.vsi;
            let s = 0;
            let note = '';
            if (sink > 16) {
                note = ' — HARD LANDING. Gear damaged.';
            }
            else {
                s = Math.round((0, math3_1.clamp)(500 - (sink - 3) * 55, 80, 500));
                if (sink < 4) {
                    s = 500;
                    note = ' — butter smooth.';
                }
                else if (sink < 7)
                    note = ' — nice.';
                else
                    note = ' — a bit firm.';
            }
            ps.phase = 'landed';
            ps.score = Math.round(200 + s);
            msg('LANDED  +' + ps.score + ' pts' + note);
        }
    }
}
exports.DOGFIGHT_TIME = 180;
function spawnDogfight(spec, n) {
    const center = (0, math3_1.v)(9000, 1800, 9000);
    const enemies = [];
    for (let i = 0; i < n; i++) {
        const ang = (i / n) * Math.PI * 2 + 0.8;
        const p = (0, math3_1.v)(center.x + Math.cos(ang) * 2600, 1600 + i * 260, center.z + Math.sin(ang) * 2600);
        const st = (0, physics_1.createState)(spec, p, Math.atan2(-(center.x - p.x), -(center.z - p.z)), 0.1, 0, 220);
        enemies.push({
            spec, st, alive: true, hp: 3, state: 'fight', evadeT: 0,
            idx: i, lastShot: 0, scoreVal: spec.id === 'falcon' ? 300 : spec.id === 'viper' ? 400 : 150,
        });
    }
    return {
        enemies, bullets: [], booms: [], score: 0, alive: n, kills: 0,
        playerAmmo: 42, fireCd: 0, time: 0, msgCd: 0, result: 'none', resultT: 0,
        wind: { dir: 0.9, speed: 8 },
    };
}
function dogfightWind(p, d) {
    return (0, math3_1.v)(Math.cos(d.wind.dir) * d.wind.speed, 0, Math.sin(d.wind.dir) * d.wind.speed);
}
function updateDogfight(player, d, dt, fireFlag, onFire, onBoom, msg) {
    d.time += dt;
    d.msgCd = Math.max(0, d.msgCd - dt);
    const say = (t) => { if (d.msgCd <= 0) {
        msg(t);
        d.msgCd = 1.4;
    } };
    const pb = player.b;
    const pFwd = fwdOf(player);
    // player weapons
    d.fireCd = Math.max(0, d.fireCd - dt);
    if (fireFlag && d.fireCd <= 0 && d.playerAmmo > 0 && d.result === 'none' && !player.crashed) {
        d.fireCd = 0.085;
        d.playerAmmo--;
        const spread = Math.abs(pb.roll) > (0, math3_1.rad)(70) ? 0.3 : 0.06;
        onFire({ fromPlayer: true, origin: { x: pb.p.x, y: pb.p.y, z: pb.p.z }, dir: { x: pFwd.x, y: pFwd.y, z: pFwd.z }, spread });
        if (d.playerAmmo === 0)
            say('AMMO DEPLETED');
    }
    // enemies: AI + fire
    for (const e of d.enemies) {
        if (!e.alive)
            continue;
        updateEnemyAI(e, player, d, dt);
        if (d.result === 'none' && !player.crashed) {
            const dp = (0, math3_1.dist)(e.st.b.p, pb.p);
            const toP = norm3((0, math3_1.sub)(pb.p, e.st.b.p));
            const eff = (0, math3_1.dot)(eForward(e), toP);
            if (dp < 3000 && eff > 0.93 && d.time > e.lastShot + 1.6 + (e.idx % 3) * 0.5 && e.state === 'fight') {
                e.lastShot = d.time;
                onFire({ fromPlayer: false, origin: { x: e.st.b.p.x, y: e.st.b.p.y, z: e.st.b.p.z }, dir: { x: eForward(e).x, y: eForward(e).y, z: eForward(e).z }, spread: 0.05 });
            }
        }
        stepEnemy(e, dt, d);
    }
    // bullets
    for (const b of d.bullets) {
        b.p = (0, math3_1.add)(b.p, (0, math3_1.scale)(b.dir, b.speed * dt));
        b.life -= dt;
        if (b.life <= 0)
            continue;
        if (b.p.y < (0, world_1.terrainHeight)(b.p.x, b.p.z)) {
            b.life = 0;
            continue;
        }
        if (b.fromPlayer) {
            for (const e of d.enemies) {
                if (!e.alive)
                    continue;
                if ((0, math3_1.dist)(b.p, e.st.b.p) < 22) {
                    b.life = 0;
                    e.hp--;
                    if (e.hp <= 0) {
                        e.alive = false;
                        e.state = 'dead';
                        d.kills++;
                        d.score += e.scoreVal;
                        d.alive--;
                        onBoom(e.st.b.p, true);
                        say('TARGET DESTROYED  +' + e.scoreVal);
                    }
                    else {
                        e.state = 'evade';
                        e.evadeT = 2.4;
                        onBoom(b.p, false);
                    }
                    break;
                }
            }
        }
        else if ((0, math3_1.dist)(b.p, pb.p) < 20 && !player.crashed) {
            b.life = 0;
            player.crashed = true;
            player.crashTimer = 0;
            say('DIRECT HIT — AIRCRAFT LOST');
        }
    }
    d.bullets = d.bullets.filter(b => b.life > 0);
    // explosions age out
    for (const bm of d.booms)
        bm.t += dt;
    d.booms = d.booms.filter(bm => bm.t < 1.6);
    // outcome
    if (d.result === 'none') {
        if (player.crashed) {
            d.result = 'lose';
            d.resultT = 0;
            onBoom(pb.p, true);
        }
        else if (d.alive === 0) {
            d.result = 'win';
            d.resultT = 0;
            d.score += Math.round((exports.DOGFIGHT_TIME - d.time) * 4);
            say('AIRSPACE CLEARED  +' + Math.round((exports.DOGFIGHT_TIME - d.time) * 4) + ' TIME BONUS');
        }
        else if (d.time > exports.DOGFIGHT_TIME) {
            d.result = 'timeout';
            d.resultT = 0;
            say('SORTIE TIME EXPIRED');
        }
    }
    else {
        d.resultT += dt;
    }
}
// ---------- helpers ----------
function fwdOf(st) {
    const m = (0, math3_2.yawPitchRollToMatrix)(st.b.yaw, st.b.pitch, st.b.roll);
    return (0, math3_1.v)(m.fx, m.fy, m.fz);
}
function eForward(e) {
    return fwdOf(e.st);
}
function norm3(a) { const l = (0, math3_1.len)(a) || 1; return (0, math3_1.v)(a.x / l, a.y / l, a.z / l); }
function wrapPi(a) {
    while (a > Math.PI)
        a -= 2 * Math.PI;
    while (a < -Math.PI)
        a += 2 * Math.PI;
    return a;
}
// ---------- simple but physics-driven AI ----------
function updateEnemyAI(e, player, d, dt) {
    const b = e.st.b;
    const dp = (0, math3_1.dist)(b.p, player.b.p);
    const toP = (0, math3_1.sub)(player.b.p, b.p);
    const toPn = norm3(toP);
    const fwd = eForward(e);
    const eff = (0, math3_1.dot)(fwd, toPn);
    let targetYaw = Math.atan2(toPn.x, toPn.z);
    let targetPitch = (0, math3_1.clamp)(Math.atan2(toPn.y, Math.max(1, Math.hypot(toPn.x, toPn.z))) * 1.1, -0.6, 0.6);
    let throttle = 0.6;
    let rollIn = 0;
    let yawIn = 0;
    if (e.state === 'evade') {
        e.evadeT -= dt;
        const side = e.idx % 2 === 0 ? 1 : -1;
        rollIn = side;
        yawIn = -side * 0.7;
        throttle = 1.0;
        if (e.evadeT <= 0)
            e.state = 'fight';
    }
    else {
        // pursue with turn lead; keep energy
        const lead = (0, math3_1.clamp)(dp / 380, -1.2, 1.2) * 0.3 * (e.idx % 2 === 0 ? 1 : -1);
        targetYaw += lead;
        throttle = dp > 1800 ? 1.0 : 0.65;
        const yErr = wrapPi(targetYaw - b.yaw);
        yawIn = (0, math3_1.clamp)(yErr * 2.2, -1, 1);
        rollIn = (0, math3_1.clamp)(yErr * 1.5, -1, 1);
        // hold a comfortable band altitude
        const targetAlt = 1500 + 800 * Math.sin(d.time * 0.12 + e.idx * 2.1);
        const altErr = targetAlt - b.p.y;
        if (Math.abs(altErr) > 130)
            targetPitch = (0, math3_1.clamp)(targetPitch + Math.sign(altErr) * 0.45, -0.7, 0.7);
    }
    const gh = (0, world_1.terrainHeight)(b.p.x, b.p.z);
    if (b.p.y - gh < 320 && b.vel.y < -5)
        targetPitch = 0.55;
    const pitchErr = (0, math3_1.clamp)(targetPitch - b.pitch, -1, 1);
    const ctl = {
        pitchIn: pitchErr * 1.25,
        rollIn,
        yawIn,
        throttleIn: throttle > b.throttle + 0.06 ? 0.8 : throttle < b.throttle - 0.06 ? -0.8 : 0,
        trimPitch: 0, gear: false, flaps: false, brakes: false, fire: false, reset: false,
    };
    e.st._ctl = ctl;
}
function stepEnemy(e, dt, d) {
    const ctl = e.st._ctl;
    (0, physics_1.updatePhysics)(e.st, {
        controls: ctl, dt,
        terrainHeight: (x, z) => (0, world_1.terrainHeight)(x, z),
        wind: (p) => dogfightWind(p, d),
    }, dt);
    if (e.st.crashed && e.alive) {
        e.alive = false;
        e.state = 'dead';
        d.alive--;
    }
}
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"unsloth/Qwen3.8-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"2de8ad23-4b86-4b27-91a7-789e15676eb4","tokensIn":4204982,"tokensOut":161307,"tokensTotal":4366289,"cost":0,"turns":57,"toolCalls":83,"failedToolCalls":6,"timestamp":"2026-08-15T22:24:10.458Z"} -->