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 & 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"} -->