Flight Simulator
Qwen 3.8 · typescript

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 105,026 bytes · SHA-256 c5ec3efb9418
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Apex Aero — High-Fidelity Flight Simulator</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #0a0e13; }
body { font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; color: #e8eef4; -webkit-font-smoothing: antialiased; }
#stage { position: fixed; inset: 0; }
#c3d { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#vignette { position: absolute; inset: 0; pointer-events: none;
background: radial-gradient(ellipse at center, transparent 55%, rgba(5,8,12,0.42) 100%); }
.hidden { display: none !important; }
/* ============ HUD ============ */
#hud { position: fixed; inset: 0; pointer-events: none; z-index: 10; }
.hud { position: absolute; inset: 0; }
.hud-box { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
background: rgba(8,14,20,0.55); border: 1px solid rgba(120,180,220,0.22); border-radius: 6px; padding: 6px 14px;
backdrop-filter: blur(3px); }
.hud-label { font-size: 10px; letter-spacing: 2px; color: #7fb4d8; font-weight: 600; }
.hud-value { font-size: 20px; font-weight: 700; font-variant-numeric: tabular-nums; color: #eaf6ff; line-height: 1; }
.hud-value.sub { font-size: 13px; color: #9fc4de; }
.hud-value.msg { font-size: 15px; color: #ffd479; letter-spacing: 1px; min-height: 18px; }
.hud-warn { font-size: 11px; font-weight: 800; letter-spacing: 2px; color: #ff5544; visibility: hidden;
text-shadow: 0 0 8px rgba(255,60,40,0.8); animation: blink 0.5s steps(2) infinite; }
@keyframes blink { 50% { opacity: 0.15; } }
.hud-top { position: absolute; top: 14px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; align-items: stretch; }
#boxMsg { min-width: 380px; }
.hud-left { position: absolute; left: 18px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 10px; width: 150px; }
.hud-right { position: absolute; right: 18px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 10px; width: 150px; }
.gauge-v { display: flex; flex-direction: column; align-items: center; gap: 4px; padding: 10px; position: relative;
background: rgba(8,14,20,0.55); border: 1px solid rgba(120,180,220,0.22); border-radius: 8px; }
.gauge-h { display: flex; align-items: center; gap: 8px; }
.bar { width: 70px; height: 8px; background: rgba(255,255,255,0.12); border-radius: 4px; overflow: hidden; }
.bar-fill { height: 100%; width: 0%; background: linear-gradient(90deg, #35c46f, #ffd23f 70%, #ff5544); border-radius: 4px; transition: width 0.1s linear; }
.bar-fill.heat { background: linear-gradient(90deg, #4fa8e8, #ffd23f, #ff6a3d); }
.bar-fill.dur { background: linear-gradient(90deg, #ff5544, #ffd23f 55%, #35c46f); }
.speed-arc { position: absolute; left: 50%; top: 50%; width: 96px; height: 96px; margin: -48px 0 0 -48px;
border: 2px solid transparent; border-top-color: #59d4ff; border-radius: 50%; opacity: 0; }
#hudRoot.locked .crosshair { border-color: #ff4433; transform: translate(-50%,-50%) scale(0.82);
box-shadow: 0 0 14px rgba(255,50,30,0.7); }
.crosshair { position: absolute; left: 50%; top: 50%; width: 44px; height: 44px; margin: -22px 0 0 -22px;
border: 1.5px solid rgba(220,245,255,0.75); border-radius: 50%;
transition: transform 0.12s ease, border-color 0.12s ease; }
.crosshair::before, .crosshair::after { content: ''; position: absolute; background: rgba(220,245,255,0.75); }
.crosshair::before { left: 50%; top: -8px; width: 1.5px; height: 60px; margin-left: -0.75px; }
.crosshair::after { top: 50%; left: -8px; height: 1.5px; width: 60px; margin-top: -0.75px; }
.hud-bottom { position: absolute; bottom: 16px; left: 50%; transform: translateX(-50%); display: flex; gap: 10px; }
/* ============ MENU ============ */
#menu { position: fixed; inset: 0; z-index: 30; overflow: auto; }
.menu-bg { position: absolute; inset: 0;
background:
radial-gradient(1200px 600px at 70% -10%, rgba(64,120,190,0.35), transparent 60%),
radial-gradient(900px 500px at 15% 110%, rgba(30,70,120,0.4), transparent 60%),
linear-gradient(180deg, #0c1420 0%, #101a28 45%, #0a1018 100%); }
.menu-bg::after { content: ''; position: absolute; inset: 0; opacity: 0.16;
background-image: repeating-linear-gradient(115deg, transparent 0 46px, rgba(140,190,235,0.16) 46px 48px); }
.menu { position: relative; z-index: 1; max-width: 1080px; margin: 0 auto; padding: 40px 28px 48px; }
.menu-title h1 { font-size: 58px; font-weight: 900; letter-spacing: 10px; color: #eef6ff;
text-shadow: 0 0 30px rgba(90,170,255,0.35); }
.menu-title h1 span { color: #59b7ff; }
.menu-sub { color: #8fb6d4; letter-spacing: 3px; font-size: 13px; margin-top: 6px; text-transform: uppercase; }
.menu-craft { display: grid; grid-template-columns: repeat(3, 1fr); gap: 14px; margin: 30px 0 20px; }
.craft { background: rgba(14,24,36,0.82); border: 1px solid rgba(110,170,220,0.25); border-radius: 10px;
padding: 16px; cursor: pointer; transition: transform 0.12s ease, border-color 0.12s, box-shadow 0.12s; }
.craft:hover { transform: translateY(-3px); border-color: rgba(130,190,240,0.6); }
.craft.sel { border-color: #59b7ff; box-shadow: 0 0 0 1px #59b7ff, 0 8px 30px rgba(60,140,230,0.25); }
.craft-name { font-size: 19px; font-weight: 800; letter-spacing: 1px; }
.craft-role { font-size: 11px; color: #6fc1ff; letter-spacing: 2px; margin: 2px 0 8px; text-transform: uppercase; }
.craft-desc { font-size: 12px; line-height: 1.5; color: #a9c2d6; min-height: 54px; }
.craft-stats { margin-top: 10px; display: flex; flex-direction: column; gap: 5px; }
.stat { display: flex; align-items: center; gap: 8px; }
.stat-l { width: 34px; font-size: 10px; letter-spacing: 1px; color: #7fa8c8; }
.stat-bar { flex: 1; height: 5px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; }
.stat-bar span { display: block; height: 100%; background: linear-gradient(90deg, #3f9fe0, #6fd6ff); border-radius: 3px; }
.menu-modes { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; margin-bottom: 22px; }
.mode-btn { display: flex; flex-direction: column; align-items: flex-start; gap: 3px;
background: rgba(14,24,36,0.82); border: 1px solid rgba(110,170,220,0.25); border-radius: 10px;
padding: 14px 18px; color: #dcecf8; cursor: pointer; transition: all 0.12s; text-align: left; }
.mode-btn:hover { border-color: rgba(130,190,240,0.6); }
.mode-btn.sel { border-color: #ffd23f; box-shadow: 0 0 0 1px #ffd23f; }
.mode-ico { font-size: 18px; color: #6fc1ff; }
.mode-t { font-weight: 800; letter-spacing: 2px; font-size: 15px; }
.mode-d { font-size: 12px; color: #8fb0c8; }
.play-btn { width: 100%; padding: 16px; font-size: 18px; font-weight: 900; letter-spacing: 4px;
background: linear-gradient(180deg, #2e77c2, #1d4f8c); color: #fff; border: 1px solid #6fb6ff;
border-radius: 10px; cursor: pointer; transition: all 0.15s; box-shadow: 0 6px 24px rgba(40,110,200,0.4); }
.play-btn:hover { filter: brightness(1.15); transform: translateY(-2px); }
.menu-keys { display: flex; flex-wrap: wrap; gap: 8px 22px; margin-top: 22px; font-size: 12px; color: #87a9c4; }
.menu-keys b { color: #cfe6f7; background: rgba(255,255,255,0.08); padding: 2px 7px; border-radius: 4px; margin-right: 6px; }
/* ============ PAUSE ============ */
#pause { position: fixed; inset: 0; z-index: 40; background: rgba(4,8,14,0.72); backdrop-filter: blur(4px);
display: flex; align-items: center; justify-content: center; }
.pause-box { background: rgba(12,20,30,0.95); border: 1px solid rgba(120,180,220,0.3); border-radius: 12px;
padding: 30px 44px; display: flex; flex-direction: column; gap: 12px; text-align: center; }
.pause-box h2 { letter-spacing: 6px; font-size: 26px; margin-bottom: 8px; }
.pause-box button { padding: 11px 30px; font-size: 14px; font-weight: 700; letter-spacing: 2px; cursor: pointer;
background: rgba(40,70,110,0.6); color: #e8f4ff; border: 1px solid rgba(140,190,240,0.4); border-radius: 8px; transition: all 0.12s; }
.pause-box button:hover { background: rgba(60,110,170,0.75); }
@media (max-width: 860px) {
.menu-craft { grid-template-columns: 1fr; }
.menu-modes { grid-template-columns: 1fr; }
.hud-left, .hud-right { width: 110px; }
}
</style>
</head>
<body>
<div id="app"><div id="stage"><canvas id="c3d"></canvas><div id="hud" class="hidden"></div><div id="vignette"></div></div><div id="menu"></div><div id="pause" class="hidden"></div></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts","./fleet":"src/fleet.ts","./menu":"src/menu.ts","./sound":"src/sound.ts"},"src/vec.ts":{"./types":"src/types.ts"},"src/fleet.ts":{"./types":"src/types.ts"},"src/terrain.ts":{"./noise":"src/noise.ts"},"src/aerodynamics.ts":{"./types":"src/types.ts","./vec":"src/vec.ts"},"src/sound.ts":{"./types":"src/types.ts"},"src/input.ts":{"./types":"src/types.ts","./vec":"src/vec.ts"},"src/models.ts":{"./types":"src/types.ts"},"src/scene.ts":{"./terrain":"src/terrain.ts"},"src/hud.ts":{"./types":"src/types.ts"},"src/enemies.ts":{"./types":"src/types.ts","./vec":"src/vec.ts","./models":"src/models.ts"},"src/effects.ts":{"./types":"src/types.ts"},"src/game.ts":{"./types":"src/types.ts","./vec":"src/vec.ts","./aerodynamics":"src/aerodynamics.ts","./terrain":"src/terrain.ts","./scene":"src/scene.ts","./models":"src/models.ts","./sound":"src/sound.ts","./input":"src/input.ts","./hud":"src/hud.ts","./enemies":"src/enemies.ts","./effects":"src/effects.ts"},"src/menu.ts":{"./fleet":"src/fleet.ts","./game":"src/game.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 game_1 = require("./game");
const fleet_1 = require("./fleet");
const menu_1 = require("./menu");
let game = null;
let menu = null;
let pauseUI = null;
let sharedSound = null;
let raf = 0;
let lastT = performance.now();
function disposeGame() {
if (game) {
game.dispose();
game = null;
}
document.getElementById('hud')?.classList.add('hidden');
}
function start(mode, specId) {
disposeGame();
const spec = (0, fleet_1.fleetById)(specId);
const canvas = document.getElementById('c3d');
game = new game_1.Game(mode, spec, canvas);
sharedSound = game.sound;
sharedSound.init();
game.hud.show();
document.getElementById('hud').classList.remove('hidden');
if (menu)
menu.hide();
if (pauseUI)
pauseUI.hide();
}
function toMenu() {
disposeGame();
if (menu)
menu.show();
}
function ensureMenu() {
if (!menu) {
menu = new menu_1.Menu({ onPlay: start });
pauseUI = new menu_1.PauseUI({
onResume: () => { if (game)
game.paused = false; pauseUI?.hide(); },
onRestart: () => {
if (game) {
const m = game.mode, id = game.spec.id;
start(m, id);
}
},
onMenu: () => {
if (pauseUI)
pauseUI.hide();
toMenu();
},
});
}
}
function loop(t) {
raf = requestAnimationFrame(loop);
const dt = Math.min(0.05, (t - lastT) / 1000);
lastT = t;
if (game && !game.over) {
game.update(dt);
game.renderer.render(game.world.scene, game.camera);
}
}
// global keys: pause / mute / reset / quit
window.addEventListener('keydown', (e) => {
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
if (k === 'p') {
if (game && !game.over) {
game.paused = !game.paused;
if (game.paused)
pauseUI?.show();
else
pauseUI?.hide();
}
}
else if (k === 'm') {
sharedSound?.setMuted(!(sharedSound && sharedSound.muted));
}
else if (k === 'r') {
if (game && !game.paused) {
const m = game.mode, id = game.spec.id;
start(m, id);
}
}
else if (k === 'Escape') {
if (game && !game.paused) {
game.paused = true;
pauseUI?.show();
}
else if (game && game.paused) {
game.paused = false;
pauseUI?.hide();
}
}
});
ensureMenu();
menu.show();
raf = requestAnimationFrame(loop);
// debug/test handle (also handy for in-page tooling)
window.__apex = {
start: (mode, specId) => start(mode, specId),
game: () => game,
menu: () => menu,
};
};
// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeEvents = makeEvents;
exports.makeState = makeState;
function makeEvents() {
return { takeoff: false, landed: null, crash: null, kill: false, wave: false };
}
function makeState() {
return {
pos: { x: 0, y: 1, z: 0 },
vel: { x: 0, y: 0, z: 0 },
q: { x: 0, y: 0, z: 0, w: 1 },
p: 0, qRate: 0, r: 0,
bank: 0,
alpha: 0, beta: 0,
throttle: 0, rpm: 1800, heat: 0.15,
gLoad: 1, lift: 0, drag: 0,
stalled: false, lowSpeed: false,
onGround: true, wasOnGround: true,
dead: false, time: 0,
};
}
};
// ── module: src/vec.ts ──
__mods["src/vec.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.qRotInv = exports.qRot = exports.qNormalize = exports.qFromAxisAngle = exports.qMul = exports.qCopy = exports.qId = exports.clamp = exports.dist = exports.normalize = exports.lenSq = exports.len = exports.cross = exports.dot = exports.addScaled = exports.scale = exports.sub = exports.add = exports.copy = exports.set = exports.clone = exports.v3 = void 0;
const v3 = (x = 0, y = 0, z = 0) => ({ x, y, z });
exports.v3 = v3;
const clone = (a) => ({ x: a.x, y: a.y, z: a.z });
exports.clone = clone;
const set = (a, x, y, z) => { a.x = x; a.y = y; a.z = z; return a; };
exports.set = set;
const copy = (a, b) => { a.x = b.x; a.y = b.y; a.z = b.z; return a; };
exports.copy = copy;
const add = (a, b) => { a.x += b.x; a.y += b.y; a.z += b.z; return a; };
exports.add = add;
const sub = (a, b) => { a.x -= b.x; a.y -= b.y; a.z -= b.z; return a; };
exports.sub = sub;
const scale = (a, s) => { a.x *= s; a.y *= s; a.z *= s; return a; };
exports.scale = scale;
const addScaled = (a, b, s) => { a.x += b.x * s; a.y += b.y * s; a.z += b.z * s; return a; };
exports.addScaled = addScaled;
const dot = (a, b) => a.x * b.x + a.y * b.y + a.z * b.z;
exports.dot = dot;
const cross = (a, b) => {
const x = a.y * b.z - a.z * b.y, y = a.z * b.x - a.x * b.z, z = a.x * b.y - a.y * b.x;
a.x = x;
a.y = y;
a.z = z;
return a;
};
exports.cross = cross;
const len = (a) => Math.sqrt(a.x * a.x + a.y * a.y + a.z * a.z);
exports.len = len;
const lenSq = (a) => a.x * a.x + a.y * a.y + a.z * a.z;
exports.lenSq = lenSq;
const normalize = (a) => { const l = (0, exports.len)(a); if (l > 1e-9) {
a.x /= l;
a.y /= l;
a.z /= l;
} return a; };
exports.normalize = normalize;
const dist = (a, b) => Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2);
exports.dist = dist;
const clamp = (v, lo, hi) => (v < lo ? lo : v > hi ? hi : v);
exports.clamp = clamp;
const qId = () => ({ x: 0, y: 0, z: 0, w: 1 });
exports.qId = qId;
const qCopy = (a, b) => { a.x = b.x; a.y = b.y; a.z = b.z; a.w = b.w; return a; };
exports.qCopy = qCopy;
// q = q * r (rotate by r applied after q)
const qMul = (a, b) => {
const ax = a.x, ay = a.y, az = a.z, aw = a.w;
a.x = aw * b.x + ax * b.w + ay * b.z - az * b.y;
a.y = aw * b.y + ay * b.w + az * b.x - ax * b.z;
a.z = aw * b.z + az * b.w + ax * b.y - ay * b.x;
a.w = aw * b.w - ax * b.x - ay * b.y - az * b.z;
return a;
};
exports.qMul = qMul;
const qFromAxisAngle = (out, axis, ang) => {
const s = Math.sin(ang / 2);
out.x = axis.x * s;
out.y = axis.y * s;
out.z = axis.z * s;
out.w = Math.cos(ang / 2);
return out;
};
exports.qFromAxisAngle = qFromAxisAngle;
const qNormalize = (q) => {
const l = Math.sqrt(q.x * q.x + q.y * q.y + q.z * q.z + q.w * q.w) || 1;
q.x /= l;
q.y /= l;
q.z /= l;
q.w /= l;
return q;
};
exports.qNormalize = qNormalize;
// rotate vector v by unit quaternion q -> out
const qRot = (out, q, v) => {
const tx = q.y * v.z - q.z * v.y, ty = q.z * v.x - q.x * v.z, tz = q.x * v.y - q.y * v.x;
out.x = v.x + 2 * (q.w * tx + (q.y * tz - q.z * ty));
out.y = v.y + 2 * (q.w * ty + (q.z * tx - q.x * tz));
out.z = v.z + 2 * (q.w * tz + (q.x * ty - q.y * tx));
return out;
};
exports.qRot = qRot;
// rotate vector by the INVERSE of q (world -> body when q maps body -> world)
const qRotInv = (out, q, v) => (0, exports.qRot)(out, { x: -q.x, y: -q.y, z: -q.z, w: q.w }, v);
exports.qRotInv = qRotInv;
};
// ── module: src/noise.ts ──
__mods["src/noise.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.smoothstep = void 0;
exports.makeNoise2D = makeNoise2D;
exports.fbm2 = fbm2;
function makeNoise2D(seed) {
const h = (x, y) => {
let n = (x * 374761393 + y * 668265263 + seed * 974634) | 0;
n = ((n ^ (n >> 13)) * 1274126177) | 0;
return ((n ^ (n >> 16)) >>> 0) / 4294967295;
};
const smooth = (t) => t * t * (3 - 2 * t);
return (x, y) => {
const xi = Math.floor(x), yi = Math.floor(y);
const xf = x - xi, yf = y - yi;
const u = smooth(xf), v = smooth(yf);
const a = h(xi, yi), b = h(xi + 1, yi), c = h(xi, yi + 1), d = h(xi + 1, yi + 1);
return a + (b - a) * u + (c - a) * v + (a - b - c + d) * u * v;
};
}
function fbm2(n, x, y, oct, gain = 0.5) {
let amp = 1, f = 1, sum = 0, tot = 0;
for (let i = 0; i < oct; i++) {
sum += n(x * f + i * 17.31, y * f + i * 9.7) * amp;
tot += amp;
amp *= gain;
f *= 2.03;
}
return sum / tot;
}
const smoothstep = (a, b, x) => {
const t = Math.min(1, Math.max(0, (x - a) / (b - a)));
return t * t * (3 - 2 * t);
};
exports.smoothstep = smoothstep;
};
// ── module: src/fleet.ts ──
__mods["src/fleet.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.fleetById = exports.FLEET = void 0;
exports.FLEET = [
{
id: 'gull',
name: 'GA-2 Gull',
role: 'Light Trainer',
desc: 'Single-engine trainer. Slow, forgiving, tight turning circle. Demanding but manageable takeoff and roll. Low ceiling, gentle handling.',
mass: 1100,
wingArea: 16.2,
wingSpan: 10.8,
cd0: 0.045,
clMax: 1.55,
clAlpha: 5.4,
clL0: 0.10,
thrust: 5200,
vstall: 24,
vmax: 98,
gLimit: 4.5,
ctrl: { pitch: 0.75, roll: 2.6, yaw: 0.8, resp: 2.2, maxBank: 0.55 },
colors: { a: 0xdde3ea, b: 0x8fa3b8, accent: 0xc23b2e, glass: 0x1a2b3d },
},
{
id: 'kestrel',
name: 'KS-1 Kestrel',
role: 'Interceptor / Multirole',
desc: 'Balanced fighter. Strong thrust-to-weight, wide speed envelope, honest energy handling. The all-round choice for dogfights.',
mass: 6600,
wingArea: 32.0,
wingSpan: 9.6,
cd0: 0.026,
clMax: 1.42,
clAlpha: 5.0,
clL0: 0.06,
thrust: 16000,
vstall: 48,
vmax: 185,
gLimit: 7.5,
ctrl: { pitch: 1.15, roll: 4.4, yaw: 1.0, resp: 2.6, maxBank: 0.7 },
colors: { a: 0x9aa7b4, b: 0x55606d, accent: 0xd8a03c, glass: 0x101d2c },
},
{
id: 'viper',
name: 'VP-9 Viper',
role: 'Heavy Strike Fighter',
desc: 'Twin-engine beast. Enormous thrust and heavy structural limits, but a fat airframe: slow to respond, needs the full runway and big energy to turn.',
mass: 12000,
wingArea: 48.0,
wingSpan: 11.8,
cd0: 0.024,
clMax: 1.28,
clAlpha: 4.6,
clL0: 0.04,
thrust: 34000,
vstall: 54,
vmax: 225,
gLimit: 9.5,
ctrl: { pitch: 0.85, roll: 3.0, yaw: 0.85, resp: 2.0, maxBank: 0.5 },
colors: { a: 0x3d4a56, b: 0x232c35, accent: 0x27c2a8, glass: 0x0c141f },
},
];
const fleetById = (id) => exports.FLEET.find(f => f.id === id) || exports.FLEET[0];
exports.fleetById = fleetById;
};
// ── module: src/terrain.ts ──
__mods["src/terrain.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createTerrain = createTerrain;
exports.mulberry = mulberry;
const noise_1 = require("./noise");
const SEA = 0;
function createTerrain(size = 5200, seed = 1337) {
const n1 = (0, noise_1.makeNoise2D)(seed);
const n2 = (0, noise_1.makeNoise2D)(seed + 71);
const n3 = (0, noise_1.makeNoise2D)(seed + 991);
const edge = size / 2 - 250;
const edgeFalloff = (x, z) => {
const d = Math.max(Math.abs(x), Math.abs(z));
return (0, noise_1.smoothstep)(size / 2, size / 2 - 1400, d);
};
const h = (x, z) => {
const falloff = edgeFalloff(x, z);
const base = (0, noise_1.fbm2)(n1, x / 1500, z / 1500, 5) - 0.5;
const ridge = 1 - Math.abs((0, noise_1.fbm2)(n2, x / 900 + 31.7, z / 900 - 12.2, 4) * 2 - 1);
const mtn = Math.pow(ridge, 2.6) * 850;
const detail = ((0, noise_1.fbm2)(n3, x / 140, z / 140, 3) - 0.5) * 26;
let alt = (base * 260 + mtn + detail) * falloff;
// coastal shelf: pull east half toward water
const coast = (0, noise_1.smoothstep)(-1600, 1900, x) * 0.75;
alt *= 1 - coast;
alt -= coast * 40;
if (alt < SEA && alt > SEA - 60)
alt = SEA - (alt - SEA) * 0.25; // flatten seabed
// runway plateau (north end): finite, ramps to ground at the ends
const px = 1 - (0, noise_1.smoothstep)(18, 92, Math.abs(x)); // 1 over the strip, 0 far off
const rampN = 1 - (0, noise_1.smoothstep)(960, 1180, -z); // 1 in-runway, 0 past north end
const rampS = 1 - (0, noise_1.smoothstep)(390, 470, z); // 1 in-runway, 0 past south end
const rp = Math.min(px, Math.max(rampN, rampS));
if (rp > 0)
alt = alt * (1 - rp) + 8 * rp;
return Math.max(alt, SEA - 45);
};
const isRunway = (x, z) => Math.abs(x) < 100 && (-700 < z && z < 340) || (Math.abs(x) < 100 && z < -240);
const build = () => {
const res = 128;
const step = size / res;
const half = size / 2;
const verts = [];
const cols = [];
const idx = [];
const cLow = [44, 74, 40], cHi = [120, 112, 88], cRock = [112, 102, 92], cSnow = [228, 232, 238], cSand = [178, 160, 118];
for (let j = 0; j <= res; j++) {
for (let i = 0; i <= res; i++) {
const x = -half + i * step;
const z = -half + j * step;
const a = h(x, z);
verts.push(x, a, z);
// slope approximation
const e = 12;
const sx = (h(x + e, z) - h(x - e, z)) / (2 * e);
const sz = (h(x, z + e) - h(x, z - e)) / (2 * e);
const slope = Math.min(1, Math.sqrt(sx * sx + sz * sz));
let r, g, b;
if (a < 1.5) {
r = cSand[0];
g = cSand[1];
b = cSand[2];
}
else if (a < 130) {
const t = a / 130;
r = cLow[0] + (cHi[0] - cLow[0]) * t;
g = cLow[1] + (cHi[1] - cLow[1]) * t;
b = cLow[2] + (cHi[2] - cLow[2]) * t;
}
else if (a < 320 || slope > 0.55) {
const t = Math.min(1, (a - 130) / 190 + slope * 0.4);
r = cRock[0];
g = cRock[1];
b = cRock[2];
r += (a > 320 ? t * 20 : 0);
g += a > 320 ? t * 20 : 0;
b += a > 320 ? t * 22 : 0;
}
else {
const t = Math.min(1, (a - 320) / 200);
r = cRock[0] + (cSnow[0] - cRock[0]) * t;
g = cRock[1] + (cSnow[1] - cRock[1]) * t;
b = cRock[2] + (cSnow[2] - cRock[2]) * t;
}
const v = 0.92 + 0.08 * n3(x * 0.05, z * 0.05);
cols.push(r * v, g * v, b * v);
if (i < res && j < res) {
const A = j * (res + 1) + i, B = A + 1, C = A + res + 1, D = C + 1;
idx.push(A, C, B, B, C, D);
}
}
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(new Float32Array(verts), 3));
geo.setAttribute('color', new THREE.BufferAttribute(new Float32Array(cols), 3));
geo.setIndex(idx);
geo.computeVertexNormals();
const mat = new THREE.MeshPhongMaterial({ vertexColors: true, flatShading: false, shininess: 4 });
const mesh = new THREE.Mesh(geo, mat);
mesh.receiveShadow = true;
mesh.castShadow = false;
mesh.name = 'terrain';
return mesh;
};
const waterMesh = () => {
const geo = new THREE.PlaneGeometry(size, size, 1, 1);
geo.rotateX(-Math.PI / 2);
const mat = new THREE.MeshPhongMaterial({
color: 0x16405c, shininess: 90, specular: new THREE.Color(0x88bbee),
transparent: true, opacity: 0.82,
});
const m = new THREE.Mesh(geo, mat);
m.position.y = 0.4;
m.name = 'water';
return m;
};
return { size, seed, h, isRunway, build, waterMesh };
}
// deterministic random from two seeds (shared between scene builder and missions)
function mulberry(seed) {
let a = seed | 0;
return () => {
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;
};
}
};
// ── module: src/aerodynamics.ts ──
__mods["src/aerodynamics.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Aero = exports.densityFromAlt = void 0;
const vec_1 = require("./vec");
const G0 = 9.80665;
const RHO0 = 1.225;
/**
* Flight frame: aircraft body axes are +X = forward (nose), +Y = up, +Z = right wing.
* State quaternion s.q maps body -> world (mesh.quaternion = s.q).
*/
const densityFromAlt = (alt) => RHO0 * Math.pow((0, vec_1.clamp)(1013.25 - 0.1327 * alt, 120, 1013.25) / 1013.25, 5.255);
exports.densityFromAlt = densityFromAlt;
const FX = (0, vec_1.v3)(1, 0, 0); // fuselage forward
const FY = (0, vec_1.v3)(0, 1, 0); // body up
const FZ = (0, vec_1.v3)(0, 0, 1); // right wing
const axX = (0, vec_1.v3)(0, 0, 0), axY = (0, vec_1.v3)(0, 0, 0), axZ = (0, vec_1.v3)(0, 0, 0); // body axes in world
const velL = (0, vec_1.v3)(0, 0, 0); // velocity in body frame
const accL = (0, vec_1.v3)(0, 0, 0); // acceleration in body frame
const qTmp = (0, vec_1.qId)();
const wq = (0, vec_1.qId)(); // scratch for world-axis composition
const WUP = (0, vec_1.v3)(0, 1, 0);
class Aero {
constructor(spec, s) {
this.onRunway = false; // scene: plane over the runway polygon
this.onWater = false; // scene: over open water
this.groundY = 0; // scene: surface height at plane position
this.gOverT = 0;
this.events = { takeoff: false, landed: null, crash: null, kill: false, wave: false };
this.spec = spec;
this.s = s;
}
frames() {
const s = this.s;
(0, vec_1.qRot)(axX, s.q, FX);
(0, vec_1.qRot)(axY, s.q, FY);
(0, vec_1.qRot)(axZ, s.q, FZ);
(0, vec_1.qRotInv)(velL, s.q, s.vel);
}
worldYaw(dt, angle) {
// rotate attitude about WORLD up
(0, vec_1.qFromAxisAngle)(wq, WUP, angle);
(0, vec_1.qMul)(wq, this.s.q); // W * q
(0, vec_1.qCopy)(this.s.q, wq);
(0, vec_1.qNormalize)(this.s.q);
}
/** One fixed substep of flight dynamics. */
step(dt, ctl, ev) {
const s = this.s;
if (s.dead)
return;
s.time += dt;
// NOTE: events accumulate; the caller clears the struct before its substep loop.
const M = this.spec.mass;
const sp = this.spec;
const rho = (0, exports.densityFromAlt)(Math.max(0, s.pos.y));
this.frames();
const Vt = Math.max(1e-4, (0, vec_1.len)(s.vel));
// AoA: velocity below the nose => positive. (body-up dot vel is negative of that)
const aA = -Math.asin((0, vec_1.clamp)(velL.y / Vt, -1, 1));
const bB = Math.asin((0, vec_1.clamp)(velL.z / Vt, -1, 1)); // sideslip
s.alpha = aA;
s.beta = bB;
// ---------- engine (spool-up lag) ----------
const tRate = ctl.throttle >= s.throttle ? 1.1 : 1.9;
s.throttle = (0, vec_1.clamp)(s.throttle + (ctl.throttle - s.throttle) * Math.min(1, tRate * dt), 0, 1);
s.rpm += (2400 + 9800 * s.throttle - s.rpm) * Math.min(1, 1.7 * dt);
s.heat = (0, vec_1.clamp)(s.heat + (0.12 + 0.88 * s.throttle - s.heat) * 0.3 * dt, 0, 1);
const T = sp.thrust * s.throttle * (0.78 + 0.22 * (rho / RHO0)); // thrust falls with density
// ---------- aerodynamics ----------
const S = sp.wingArea;
const cl = sp.clL0 + sp.clAlpha * (0, vec_1.clamp)(aA, -0.55, 0.55) - 1.25 * bB * bB;
const liftMag = 0.5 * rho * Vt * Vt * S * (0, vec_1.clamp)(cl, -0.3, sp.clMax * 1.05);
const cdP = sp.cd0 + 0.032 * s.throttle + 0.045 * aA * aA;
const ar = (sp.wingSpan / Math.sqrt(S)) ** 2;
const cdI = ((0, vec_1.clamp)(cl, 0, 9) ** 2) / (Math.PI * ar * 0.8);
const dragMag = 0.5 * rho * Vt * Vt * S * (cdP + cdI);
s.lift = liftMag;
s.drag = dragMag;
s.stalled = aA > sp.clMax / sp.clAlpha - 0.02 && Vt > 8;
s.lowSpeed = Vt < sp.vstall;
(0, vec_1.set)(accL, 0, 0, 0);
(0, vec_1.addScaled)(accL, FX, T / M); // thrust along nose
(0, vec_1.addScaled)(accL, FY, liftMag / M); // lift along body up
(0, vec_1.addScaled)(accL, velL, -dragMag / (M * Vt)); // drag opposite velocity
accL.x -= G0 * axX.y; // gravity world->body
accL.y -= G0 * axY.y;
accL.z -= G0 * axZ.y;
// ---------- surfaces / ground ----------
const wasGround = s.onGround;
s.wasOnGround = wasGround;
const overRW = this.onRunway;
const overWtr = this.onWater && s.pos.y <= this.groundY + 0.5;
const onSurface = overRW && s.pos.y <= this.groundY + 2;
if (onSurface) {
const Vg = Vt;
const brakeF = ctl.brake * 26000;
const rollF = 300 * (1 - 0.3 * s.throttle) + 130;
const decel = (brakeF + rollF) / M;
if (Vg > 1e-4) {
// friction opposes motion, never reverses it (no negative speed)
const fr = Math.min(decel * dt, Vg);
(0, vec_1.addScaled)(accL, velL, -fr / Vg);
}
// steering about world up, scaled with speed
const steer = -(0, vec_1.clamp)(ctl.roll, -1, 1) * (0, vec_1.clamp)(Vg / 26, 0, 1) * 1.5;
this.worldYaw(dt, steer * dt);
// takeoff when lift overcomes weight
if (liftMag > M * G0 * 0.985) {
if (wasGround)
ev.takeoff = true;
s.onGround = false;
s.vel.y += 1.5;
s.pos.y = this.groundY + 0.6;
}
else {
accL.y += s.vel.y < 0 ? -s.vel.y * 25 : 0;
}
}
else if (overWtr) {
(0, vec_1.set)(accL, accL.x * 0.4, accL.y * 0.4, accL.z * 0.4);
s.pos.y = this.groundY;
s.onGround = true;
ev.landed = { ok: false, speed: Vt };
ev.crash = ev.crash || 'Crashed into open water';
}
else {
s.onGround = false;
}
// ---------- touchdown (airborne -> runway, only near the surface & moving forward) ----------
const fwdSpd = axX.x * s.vel.x + axX.y * s.vel.y + axX.z * s.vel.z;
if (overRW && !wasGround && s.vel.y < 0 && s.pos.y - this.groundY < 3 && fwdSpd > 6) {
const ok = s.vel.y > -7.2 && Vt < 98 && Math.abs(aA) < 0.5;
ev.landed = { ok, speed: Vt };
if (ok)
s.vel.y = 0;
else
ev.crash = 'Hard landing — airframe destroyed';
s.onGround = true;
}
// ---------- integrate (world) ----------
const ax = accL.x * axX.x + accL.y * axY.x + accL.z * axZ.x;
const ay = accL.x * axX.y + accL.y * axY.y + accL.z * axZ.y;
const az = accL.x * axX.z + accL.y * axY.z + accL.z * axZ.z;
s.vel.x += ax * dt;
s.vel.y += ay * dt;
s.vel.z += az * dt;
if (s.onGround)
s.vel.y = Math.max(0, s.vel.y);
s.pos.x += s.vel.x * dt;
s.pos.y += s.vel.y * dt;
s.pos.z += s.vel.z * dt;
if (s.onGround)
s.pos.y = this.groundY;
// ---------- G load & limits (load factor n = L/(m g)) ----------
s.gLoad = (0, vec_1.clamp)(1 + ay / G0, -12, 20);
if (!s.onGround) {
// structural failure requires sustained over-G (energy, not a momentary spike)
if (Math.abs(s.gLoad) > sp.gLimit)
this.gOverT += dt;
if (this.gOverT > 0.35)
ev.crash = `Structural failure — ${sp.gLimit} G sustained`;
else if (s.gLoad < 0.25 && Vt < sp.vstall * 0.95 && aA > 0.12 && s.pos.y - this.groundY > 4)
ev.crash = 'Stall-spin — lost the wings';
}
else {
this.gOverT = 0;
}
if (ev.crash || (ev.landed && !ev.landed.ok))
s.dead = true;
}
/** Control-surface angular response (first-order lag = inertia). */
updateRates(dt, ctl) {
const s = this.s;
if (s.dead)
return;
const Vt = (0, vec_1.len)(s.vel);
const sp = this.spec;
const eff = (0, vec_1.clamp)(Vt / 42, 0, 1.25) * (s.stalled ? 0.3 : 1);
const spd = 0.55 + 0.45 * (0, vec_1.clamp)(Vt / sp.vmax, 0, 1);
const k = Math.min(1, sp.ctrl.resp * dt);
const tP = (0, vec_1.clamp)(ctl.pitch, -1, 1) * sp.ctrl.pitch * eff * spd;
const tY = (0, vec_1.clamp)(ctl.yaw, -1, 1) * sp.ctrl.yaw * eff;
// Roll: self-coordinating bank-angle law (digital stick) — hold stick to hold bank,
// release to roll out. Rate limited by control authority (inertia via lag below).
const tgtBank = (0, vec_1.clamp)(ctl.roll, -1, 1) * sp.ctrl.maxBank * (0, vec_1.clamp)(Vt / 42, 0, 1) * (s.stalled ? 0.35 : 1);
const db = (0, vec_1.clamp)(tgtBank - s.bank, -sp.ctrl.roll * dt, sp.ctrl.roll * dt);
s.bank += db;
s.p = db / Math.max(dt, 1e-4); // display rate
s.qRate += (tP - s.qRate) * k; // pitch about body Z (nose up +)
s.r += (tY - s.r) * k; // yaw about world up
if (db !== 0) {
(0, vec_1.qFromAxisAngle)(qTmp, FX, db);
(0, vec_1.qMul)(s.q, qTmp);
(0, vec_1.qNormalize)(s.q);
}
(0, vec_1.qFromAxisAngle)(qTmp, FZ, s.qRate * dt);
(0, vec_1.qMul)(s.q, qTmp);
(0, vec_1.qNormalize)(s.q);
if (!s.onGround)
this.worldYaw(dt, s.r * dt);
}
}
exports.Aero = Aero;
};
// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Sound = void 0;
/**
* Fully procedural WebAudio flight soundscape:
* - engine: sub + mid oscillators w/ vibrato, RPM-pitched, throttle-driven level
* - airflow: filtered bandpassed noise, level with dynamic pressure
* - ambient: lowpass noise bed + occasional distant rumble
* - SFX: radio beep, alarm, explosion, splash, stall warning, gear/throttle clacks
*/
class Sound {
constructor() {
this.ctx = null;
this.master = null;
this.engGain = null;
this.engSub = null;
this.engMid = null;
this.engVib = null;
this.engVibGain = null;
this.flowGain = null;
this.flowFilt = null;
this.ambGain = null;
this.noiseBuf = null;
this.windTimer = 0;
this.alarmTimer = 0;
this.muted = false;
}
init() {
if (this.ctx) {
this.ctx.resume().catch(() => { });
return;
}
try {
const AC = window.AudioContext || window.webkitAudioContext;
const ctx = new AC();
this.ctx = ctx;
const master = ctx.createGain();
master.gain.value = 0.8;
master.connect(ctx.destination);
this.master = master;
// shared noise buffer (2 s)
const nb = ctx.createBuffer(1, ctx.sampleRate * 2, ctx.sampleRate);
const d = nb.getChannelData(0);
for (let i = 0; i < d.length; i++)
d[i] = Math.random() * 2 - 1;
this.noiseBuf = nb;
// ---- engine chain: sub + mid -> vibrato-mod gain -> lowpass -> engGain
const engGain = ctx.createGain();
engGain.gain.value = 0;
const lp = ctx.createBiquadFilter();
lp.type = 'lowpass';
lp.frequency.value = 1200;
engGain.connect(lp);
lp.connect(master);
this.engGain = engGain;
const mkOsc = (type, f, g) => {
const o = ctx.createOscillator();
o.type = type;
o.frequency.value = f;
const gg = ctx.createGain();
gg.gain.value = g;
o.connect(gg);
gg.connect(engGain);
o.start();
return o;
};
this.engSub = mkOsc('sawtooth', 55, 0.9);
this.engMid = mkOsc('square', 110, 0.35);
const vib = ctx.createOscillator();
vib.type = 'sine';
vib.frequency.value = 28;
const vibG = ctx.createGain();
vibG.gain.value = 5;
vib.connect(vibG);
vibG.connect(this.engSub.frequency);
vibG.connect(this.engMid.frequency);
vib.start();
this.engVib = vib;
this.engVibGain = vibG;
// ---- airflow: bandpass noise
const flowSrc = ctx.createBufferSource();
flowSrc.buffer = nb;
flowSrc.loop = true;
const flowFilt = ctx.createBiquadFilter();
flowFilt.type = 'bandpass';
flowFilt.frequency.value = 400;
flowFilt.Q.value = 0.7;
const flowGain = ctx.createGain();
flowGain.gain.value = 0;
flowSrc.connect(flowFilt);
flowFilt.connect(flowGain);
flowGain.connect(master);
flowSrc.start();
this.flowGain = flowGain;
this.flowFilt = flowFilt;
// ---- ambient bed: lowpass noise
const ambSrc = ctx.createBufferSource();
ambSrc.buffer = nb;
ambSrc.loop = true;
const ambFilt = ctx.createBiquadFilter();
ambFilt.type = 'lowpass';
ambFilt.frequency.value = 320;
const ambGain = ctx.createGain();
ambGain.gain.value = 0.015;
ambSrc.connect(ambFilt);
ambFilt.connect(ambGain);
ambGain.connect(master);
ambSrc.start();
this.ambGain = ambGain;
}
catch {
this.ctx = null;
}
}
setMuted(m) {
this.muted = m;
if (this.master && this.ctx)
this.master.gain.setTargetAtTime(m ? 0 : 0.8, this.ctx.currentTime, 0.05);
}
/** Per-frame update driven by physics. */
update(s, dt, windAmp, speed, altitude) {
const ctx = this.ctx;
if (!ctx || !this.master)
return;
const t = ctx.currentTime;
const safe = (x) => (Number.isFinite(x) ? x : 0);
const rpm = safe(s.rpm);
const thr = safe(s.throttle);
const spd = safe(speed);
const fBase = safe(38 + rpm * 0.008);
if (this.engSub)
this.engSub.frequency.setTargetAtTime(fBase, t, 0.06);
if (this.engMid)
this.engMid.frequency.setTargetAtTime(safe(fBase * 2.01), t, 0.06);
if (this.engVib)
this.engVib.frequency.setTargetAtTime(safe(14 + rpm * 0.004), t, 0.1);
if (this.engVibGain)
this.engVibGain.gain.setTargetAtTime(safe(2 + thr * 7), t, 0.1);
// engine level: throttle dominant, density slightly
const eLvl = safe((0.05 + 0.42 * thr) * (s.onGround ? 1 : 0.96));
if (this.engGain)
this.engGain.gain.setTargetAtTime(eLvl, t, 0.08);
// airflow: q = .5 rho v^2 normalized
const q = safe(0.5 * (spd * spd)) / 14000;
if (this.flowGain)
this.flowGain.gain.setTargetAtTime(safe(Math.min(0.5, q * 0.85 + windAmp * 0.08)), t, 0.09);
if (this.flowFilt)
this.flowFilt.frequency.setTargetAtTime(safe(260 + spd * 7 + Math.abs(safe(s.alpha)) * 900), t, 0.12);
// ambient wind bed: grows with speed
if (this.ambGain)
this.ambGain.gain.setTargetAtTime(safe(0.008 + windAmp * 0.03 + Math.min(0.04, spd * 0.0002)), t, 0.3);
// stall / overspeed alarm (call from game loop instead? keep here for cohesion)
this.alarmTimer -= dt;
const alarm = (s.lowSpeed || s.stalled) && !s.onGround && !s.dead;
if (alarm && this.alarmTimer <= 0) {
this.alarmTimer = 0.55;
this.siren(0.09);
}
// random distant thunder at altitude
this.windTimer -= dt;
if (this.windTimer <= 0 && altitude > 400 && Math.random() < 0.004) {
this.windTimer = 20 + Math.random() * 40;
this.rumble(0.05 + Math.random() * 0.06);
}
}
env(dur, peak) {
const ctx = this.ctx;
if (!ctx || !this.master)
return null;
if (!Number.isFinite(peak) || peak <= 0)
return null;
const g = ctx.createGain();
const t = ctx.currentTime;
g.gain.setValueAtTime(0.0001, t);
g.gain.exponentialRampToValueAtTime(peak, t + 0.015);
g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
g.connect(this.master);
return g;
}
noiseHit(dur, peak, freq, type = 'lowpass') {
const ctx = this.ctx;
if (!ctx || !this.noiseBuf)
return;
const src = ctx.createBufferSource();
src.buffer = this.noiseBuf;
const f = ctx.createBiquadFilter();
f.type = type;
f.frequency.value = freq;
const g = this.env(dur, peak);
if (!g)
return;
src.connect(f);
f.connect(g);
src.start();
src.stop(ctx.currentTime + dur + 0.05);
}
siren(peak) {
const ctx = this.ctx;
if (!ctx)
return;
const o = ctx.createOscillator();
o.type = 'square';
o.frequency.setValueAtTime(950, ctx.currentTime);
o.frequency.linearRampToValueAtTime(620, ctx.currentTime + 0.18);
const g = this.env(0.22, peak);
if (!g)
return;
o.connect(g);
o.start();
o.stop(ctx.currentTime + 0.25);
}
beep(f = 880, peak = 0.05, dur = 0.07) {
const ctx = this.ctx;
if (!ctx)
return;
const o = ctx.createOscillator();
o.type = 'sine';
o.frequency.value = f;
const g = this.env(dur, peak);
if (!g)
return;
o.connect(g);
o.start();
o.stop(ctx.currentTime + dur + 0.03);
}
clack(peak = 0.06) { this.noiseHit(0.05, peak, 2400, 'bandpass'); }
explosion(peak = 0.8) {
this.noiseHit(1.6, peak, 180);
const ctx = this.ctx;
if (!ctx)
return;
const o = ctx.createOscillator();
o.type = 'sine';
o.frequency.setValueAtTime(90, ctx.currentTime);
o.frequency.exponentialRampToValueAtTime(28, ctx.currentTime + 1.2);
const g = this.env(1.3, peak * 0.7);
if (!g)
return;
o.connect(g);
o.start();
o.stop(ctx.currentTime + 1.35);
}
splash(peak = 0.5) { this.noiseHit(1.4, peak, 900); }
fireGun() { this.noiseHit(0.09, 0.32, 1600, 'bandpass'); this.noiseHit(0.05, 0.18, 400); }
boom(peak = 0.2) { this.noiseHit(0.5, peak, 220); }
rumble(peak) { this.noiseHit(4, peak, 70); }
wave() {
this.beep(740, 0.06);
setTimeout(() => this.beep(740, 0.06), 140);
setTimeout(() => this.beep(740, 0.06), 280);
}
}
exports.Sound = Sound;
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Input = void 0;
const vec_1 = require("./vec");
class Input {
constructor() {
this.ctl = { pitch: 0, roll: 0, yaw: 0, throttle: 0, brake: 0 };
this.keys = new Set();
this.target = { pitch: 0, roll: 0, yaw: 0 };
this.external = false; // setStick is in command
this.enabled = true;
this.onKeyDown = (e) => {
if (!this.enabled)
return;
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight', ' '].includes(e.key))
e.preventDefault();
this.keys.add(k);
if (['w', 'a', 's', 'd', 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(k))
this.external = false;
};
this.onKeyUp = (e) => {
const k = e.key.length === 1 ? e.key.toLowerCase() : e.key;
this.keys.delete(k);
};
}
attach(el) {
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
void el;
}
detach() {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
}
/** Set throttle target directly (used by auto-throttle / AI). */
setThrottle(t) { this.ctl.throttle = (0, vec_1.clamp)(t, 0, 1); }
/** Drive the stick directly (external autopilot / test hook). */
setStick(pitch = 0, roll = 0, yaw = 0) {
const n = (x) => (Number.isFinite(x) ? x : 0);
this.external = true;
this.target.pitch = (0, vec_1.clamp)(n(pitch), -1, 1);
this.target.roll = (0, vec_1.clamp)(n(roll), -1, 1);
this.target.yaw = (0, vec_1.clamp)(n(yaw), -1, 1);
this.ctl.pitch = this.target.pitch;
this.ctl.roll = this.target.roll;
this.ctl.yaw = this.target.yaw;
}
/** Smoothly ease the stick toward keyboard targets and return effective controls. */
update(dt) {
const k = this.keys;
const up = k.has('w') || k.has('ArrowUp');
const down = k.has('s') || k.has('ArrowDown');
const left = k.has('a') || k.has('ArrowLeft');
const right = k.has('d') || k.has('ArrowRight');
const rate = 3.4; // stick ease rate
const lerp = (cur, tgt) => cur + (tgt - cur) * Math.min(1, rate * dt);
if (!this.external) {
this.target.pitch = (up ? 1 : 0) - (down ? 1 : 0);
this.target.roll = (left ? -1 : 0) - (right ? 1 : 0);
this.target.yaw = 0;
}
this.ctl.pitch = lerp(this.ctl.pitch, this.target.pitch);
this.ctl.roll = lerp(this.ctl.roll, this.target.roll);
this.ctl.yaw = lerp(this.ctl.yaw, this.target.yaw);
// throttle keys
if (k.has('e') || k.has('Shift'))
this.setThrottle(this.ctl.throttle + 1.4 * dt);
if (k.has('q') || k.has('Control'))
this.setThrottle(this.ctl.throttle - 1.8 * dt);
this.ctl.brake = k.has(' ') || k.has('x') ? 1 : 0;
return this.ctl;
}
reset() {
this.keys.clear();
this.ctl.pitch = 0;
this.ctl.roll = 0;
this.ctl.yaw = 0;
this.ctl.throttle = 0;
this.ctl.brake = 0;
this.target.pitch = 0;
this.target.roll = 0;
this.target.yaw = 0;
}
key(name) {
return this.keys.has(name);
}
}
exports.Input = Input;
};
// ── module: src/models.ts ──
__mods["src/models.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAircraft = buildAircraft;
/**
* Builds low-poly-but-detailed fighter models from primitives.
* All groups follow the flight frame: +X = forward, +Y = up, +Z = right.
* Returns a group plus a small propeller object (spun by the game loop).
*/
function buildAircraft(spec) {
const g = new THREE.Group();
const c = spec.colors;
const primary = new THREE.MeshStandardMaterial({ color: c.a, roughness: 0.42, metalness: 0.55, flatShading: true });
const secondary = new THREE.MeshStandardMaterial({ color: c.b, roughness: 0.55, metalness: 0.35, flatShading: true });
const accent = new THREE.MeshStandardMaterial({ color: c.accent, roughness: 0.4, metalness: 0.4, flatShading: true });
const dark = new THREE.MeshStandardMaterial({ color: 0x14181d, roughness: 0.7, metalness: 0.2 });
const glass = new THREE.MeshStandardMaterial({ color: c.glass, roughness: 0.15, metalness: 0.9, flatShading: true });
const isTrainer = spec.id === 'gull';
const isHeavy = spec.id === 'viper';
const L = isTrainer ? 9 : isHeavy ? 21 : 15; // fuselage length
const H = isTrainer ? 1.5 : 1.9; // height
const W = isTrainer ? 1.6 : 2.0; // width
const wingSpan = isTrainer ? 10 : isHeavy ? 13 : 10.5;
const wingChord = isTrainer ? 2.4 : isHeavy ? 3.4 : 2.6;
const wingY = isTrainer ? 0.75 : 0.1; // high wing vs low wing
const add = (mesh, x, y, z, rx = 0, ry = 0, rz = 0) => {
mesh.position.set(x, y, z);
mesh.rotation.set(rx, ry, rz);
mesh.castShadow = true;
g.add(mesh);
return mesh;
};
// ---------- fuselage ----------
const fus = add(new THREE.Mesh(new THREE.BoxGeometry(L, H, W), primary), 0, 0, 0);
const nose = new THREE.Group();
const noseCone = new THREE.Mesh(new THREE.ConeGeometry(W * 0.62, L * 0.38, 4), secondary);
noseCone.rotation.x = -Math.PI / 2; // point forward (+X)
noseCone.rotation.y = Math.PI / 4;
noseCone.position.x = L / 2 + L * 0.19;
noseCone.castShadow = true;
nose.add(noseCone);
g.add(nose);
// propeller
const prop = new THREE.Group();
prop.position.x = L / 2 + L * 0.36;
const hub = new THREE.Mesh(new THREE.CylinderGeometry(0.28, 0.34, 0.4, 8), dark);
hub.rotation.z = Math.PI / 2;
prop.add(hub);
const blades = 3;
for (let i = 0; i < blades; i++) {
const b = new THREE.Mesh(new THREE.BoxGeometry(0.08, H * 2.6, 0.42), dark);
b.position.y = (H * 2.6) / 2 + 0.2;
const holder = new THREE.Group();
holder.add(b);
holder.rotation.x = (i / blades) * Math.PI * 2;
prop.add(holder);
}
g.add(prop);
// intakes / exhausts (twin for heavy)
if (isHeavy) {
for (const s of [-1, 1]) {
const intake = new THREE.Mesh(new THREE.BoxGeometry(2.4, 1.2, 1.4), secondary);
add(intake, -L * 0.1, -0.5, s * 0.95);
const exhaust = new THREE.Mesh(new THREE.CylinderGeometry(0.38, 0.5, 1.6, 8), dark);
exhaust.rotation.z = Math.PI / 2;
add(exhaust, -L * 0.48, 0.2, s * 0.9);
}
}
else {
const intake = new THREE.Mesh(new THREE.BoxGeometry(1.8, 0.9, W * 0.8), secondary);
add(intake, -L * 0.1, 0.35, 0);
const exhaust = new THREE.Mesh(new THREE.CylinderGeometry(0.3, 0.42, 1.4, 8), dark);
exhaust.rotation.z = Math.PI / 2;
add(exhaust, -L * 0.45, 0, 0);
}
// ---------- wings ----------
for (const s of [-1, 1]) {
const wing = new THREE.Mesh(new THREE.BoxGeometry(wingChord, 0.22, wingSpan / 2 - W / 2), primary);
add(wing, -L * 0.04, wingY, s * (W / 2 + (wingSpan / 2 - W / 2) / 2));
// wingtip accent
const tip = new THREE.Mesh(new THREE.BoxGeometry(wingChord * 0.9, 0.26, 0.35), accent);
add(tip, -L * 0.04, wingY, s * (wingSpan / 2 - 0.15));
// fuel tanks / pylons (heavy)
if (isHeavy) {
const tank = new THREE.Mesh(new THREE.CylinderGeometry(0.55, 0.55, 4.2, 8), secondary);
tank.rotation.z = Math.PI / 2;
add(tank, -L * 0.02, wingY - 0.85, s * 6.2);
}
}
// ---------- tail ----------
const tailX = -L * 0.46;
for (const s of [-1, 1]) {
const hw = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.18, 3.2), primary);
add(hw, tailX, -0.1, s * 1.8);
}
const eh = isTrainer ? 1.9 : 2.4;
const emp = new THREE.Mesh(new THREE.BoxGeometry(2.6, eh, 0.24), isHeavy ? primary : secondary);
add(emp, -L * 0.42, eh / 2 - 0.2, 0);
const empFin = new THREE.Mesh(new THREE.BoxGeometry(2.2, 0.5, 0.9), accent);
add(empFin, -L * 0.42, eh + 0.1, 0);
// twin vertical stabilizers for heavy
if (isHeavy) {
emp.visible = false;
for (const s of [-1, 1]) {
const vs = new THREE.Mesh(new THREE.BoxGeometry(2.4, 3.4, 0.26), primary);
add(vs, -L * 0.42, 1.5, s * 1.7);
const finA = new THREE.Mesh(new THREE.BoxGeometry(2.0, 0.5, 0.7), accent);
add(finA, -L * 0.42, 3.2, s * 1.7);
}
}
// rudder accent strip
const ru = new THREE.Mesh(new THREE.BoxGeometry(0.5, eh * 0.7, 0.12), accent);
add(ru, -L * 0.5, eh / 2 - 0.2, 0);
// ---------- cockpit ----------
const canopy = new THREE.Mesh(new THREE.BoxGeometry(L * 0.34, 0.7, W * 0.72), glass);
add(canopy, L * 0.14, H / 2 + 0.12, 0);
if (isTrainer) {
// side windows for two-seater
for (const s of [-1, 1]) {
const win = new THREE.Mesh(new THREE.BoxGeometry(1.6, 0.5, 0.1), glass);
add(win, L * 0.05, H / 2 + 0.15, s * W * 0.42);
}
// little antenna
const ant = new THREE.Mesh(new THREE.BoxGeometry(1.2, 0.06, 0.06), dark);
add(ant, 0, H / 2 + 0.75, 0);
}
// ---------- landing gear ----------
const strutMat = dark;
const gear = (x, y, z, len) => {
const strut = new THREE.Mesh(new THREE.BoxGeometry(len, 0.18, 0.18), strutMat);
add(strut, x, y, z, 0, 0, Math.PI / 2 * 0.4);
const wheel = new THREE.Mesh(new THREE.CylinderGeometry(0.42, 0.42, 0.3, 10), dark);
wheel.rotation.x = Math.PI / 2;
add(wheel, x, y - len * 0.5, z);
};
gear(L * 0.28, -H / 2 - 0.35, 0, 0.7);
gear(-L * 0.1, -H / 2 - 0.35, W * 0.7, 0.6);
gear(-L * 0.1, -H / 2 - 0.35, -W * 0.7, 0.6);
g.traverse((o) => { const m = o; if (m.isMesh)
m.castShadow = true; });
const noseWorld = new THREE.Vector3(L / 2 + L * 0.42, 0, 0);
return { group: g, prop, nose: noseWorld };
}
};
// ── module: src/scene.ts ──
__mods["src/scene.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createWorld = createWorld;
const terrain_1 = require("./terrain");
// module-level sky uniforms (shared with the injected shader)
const skyU = {
uElev: { value: 30 },
uSun: { value: new THREE.Vector3(0, 1, 0) },
};
function makeSky() {
const geo = new THREE.SphereGeometry(24000, 32, 16);
const mat = new THREE.MeshBasicMaterial({ side: THREE.BackSide, fog: false });
mat.onBeforeCompile = (sh) => {
sh.vertexShader = 'varying vec3 vSkyDir;\n' + sh.vertexShader.replace('#include <begin_vertex>', '#include <begin_vertex>\nvSkyDir = position;');
const fn = 'vec3 skyGrad(vec3 d, float el, vec3 sd){' +
' float h = clamp(d.y * 0.5 + 0.5, 0.0, 1.0);' +
' vec3 zen = mix(vec3(0.10,0.22,0.42), vec3(0.16,0.42,0.82), h);' +
' vec3 hor = mix(vec3(0.80,0.74,0.66), vec3(0.56,0.71,0.87), h);' +
' vec3 col = mix(hor, zen, pow(h, 0.5));' +
' float sa = max(dot(normalize(d), sd), 0.0);' +
' col += vec3(1.0,0.88,0.62) * pow(sa, 550.0) * 2.4;' +
' col += vec3(1.0,0.78,0.5) * pow(sa, 16.0) * 0.22;' +
' float dusk = smoothstep(0.22, 0.0, el);' +
' col = mix(col, vec3(0.5,0.3,0.36) * (0.35 + 0.65 * (1.0 - h)), dusk * 0.55);' +
' return col;}';
sh.fragmentShader =
'varying vec3 vSkyDir;\nuniform float uElev;\nuniform vec3 uSun;\n' + fn +
sh.fragmentShader.replace('vec4 diffuseColor = vec4( diffuse, opacity );', 'vec4 diffuseColor = vec4( skyGrad( vSkyDir, uElev, uSun ), 1.0 );');
sh.uniforms.uElev = skyU.uElev;
sh.uniforms.uSun = skyU.uSun;
};
const mesh = new THREE.Mesh(geo, mat);
mesh.frustumCulled = false;
return mesh;
}
function createWorld(terrain) {
const scene = new THREE.Scene();
const fog = new THREE.Fog(0xbfd3e0, 1200, 13000);
scene.fog = fog;
const sky = makeSky();
scene.add(sky);
const hemi = new THREE.HemisphereLight(0xcfe4f5, 0x5e6b52, 0.75);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff0dc, 1.1);
sun.castShadow = true;
sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;
sun.shadow.camera.near = 50;
sun.shadow.camera.far = 16000;
sun.shadow.camera.left = -700;
sun.shadow.camera.right = 700;
sun.shadow.camera.top = 700;
sun.shadow.camera.bottom = -700;
sun.shadow.bias = -0.0004;
scene.add(sun);
scene.add(sun.target);
const world = terrain.build();
scene.add(world);
const water = terrain.waterMesh();
scene.add(water);
// ---------- runway (north plateau) ----------
const rw = new THREE.Group();
const asphalt = new THREE.MeshBasicMaterial({ color: 0x23262b });
const strip = new THREE.Mesh(new THREE.PlaneGeometry(36, 1480), asphalt);
strip.rotation.x = -Math.PI / 2;
strip.position.set(0, 8.15, -325);
rw.add(strip);
const white = new THREE.MeshBasicMaterial({ color: 0xd8d8d0 });
for (let z = -960; z < 400; z += 62) {
const d = new THREE.Mesh(new THREE.PlaneGeometry(1.6, 26), white);
d.rotation.x = -Math.PI / 2;
d.position.set(0, 8.26, z);
rw.add(d);
}
for (const z of [-1000, 415]) {
for (let i = -3; i <= 3; i++) {
if (i === 0)
continue;
const b = new THREE.Mesh(new THREE.PlaneGeometry(2.4, 14), white);
b.rotation.x = -Math.PI / 2;
b.position.set(i * 4.4, 8.26, z);
rw.add(b);
}
}
const apron = new THREE.Mesh(new THREE.BoxGeometry(120, 0.4, 70), asphalt);
apron.position.set(95, 8.3, -140);
rw.add(apron);
const hangar = new THREE.Mesh(new THREE.BoxGeometry(46, 14, 34), new THREE.MeshPhongMaterial({ color: 0x77808a, shininess: 20 }));
hangar.position.set(95, 15.3, -150);
rw.add(hangar);
const tower = new THREE.Mesh(new THREE.CylinderGeometry(3.4, 4.4, 26, 8), new THREE.MeshPhongMaterial({ color: 0x9aa2ab }));
tower.position.set(-52, 21, -620);
rw.add(tower);
const cab = new THREE.Mesh(new THREE.BoxGeometry(9, 6, 9), new THREE.MeshPhongMaterial({ color: 0x2f4358 }));
cab.position.set(-52, 38, -620);
rw.add(cab);
scene.add(rw);
// ---------- clouds ----------
const rnd = (0, terrain_1.mulberry)(4242);
const cloudMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.85, fog: false });
const clouds = new THREE.Group();
const puffs = 9;
const puffGeo = new THREE.SphereGeometry(1, 7, 5);
for (let i = 0; i < puffs; i++) {
const c = new THREE.Mesh(puffGeo, cloudMat);
const s = 180 + rnd() * 340;
c.scale.set(s, s * 0.3, s * 0.8);
c.position.set((rnd() - 0.5) * 9000, 1350 + rnd() * 1300, (rnd() - 0.5) * 9000);
clouds.add(c);
}
scene.add(clouds);
// ---------- birds ----------
const birds = new THREE.Group();
const birdMat = new THREE.MeshBasicMaterial({ color: 0x1c2126 });
const flock = 14;
for (let i = 0; i < flock; i++) {
const b = new THREE.Mesh(new THREE.ConeGeometry(0.9, 3.4, 4), birdMat);
b.rotation.z = Math.PI / 2;
b.position.set((rnd() - 0.5) * 1600, 250 + rnd() * 220, -3000 + (rnd() - 0.5) * 1400);
birds.add(b);
}
scene.add(birds);
const setSun = (elDeg) => {
const el = (elDeg * Math.PI) / 180;
const az = 2.1;
const dir = skyU.uSun.value.set(Math.cos(el) * Math.cos(az), Math.sin(el), Math.cos(el) * Math.sin(az)).normalize();
sun.position.copy(dir).multiplyScalar(6000);
sun.target.position.set(0, 0, 0);
const warm = Math.max(0, 1 - Math.sin(el) * 2.2);
sun.intensity = 0.3 + Math.max(0, Math.sin(el)) * 0.9;
sun.color.setRGB(1, 0.82 + 0.18 * (1 - warm), 0.62 + 0.38 * (1 - warm));
hemi.intensity = 0.3 + Math.max(0, Math.sin(el)) * 0.45;
skyU.uElev.value = elDeg;
};
const update = (dt, t) => {
clouds.children.forEach((c, i) => {
c.position.x += dt * (3 + (i % 3));
if (c.position.x > 4700)
c.position.x = -4700;
});
birds.children.forEach((b, i) => {
const a = t * 0.05 + (i / flock) * Math.PI * 2;
b.position.x += Math.cos(a) * dt * 26;
b.position.z += Math.sin(a) * dt * 26;
b.rotation.y = -a;
if (b.position.z < -4400)
b.position.z = -1000;
if (b.position.z > -800)
b.position.z = -4200;
});
};
return {
scene, sun, hemi, sky, fog, terrain, setSun, update,
dispose() {
world.geometry.dispose();
world.material.dispose();
},
};
}
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Hud = void 0;
const el = (id) => document.getElementById(id);
class Hud {
constructor(mode, spec) {
this.root = document.getElementById('hud');
this.t = id => el(id);
const limit = mode === 'dogfight' ? spec.vmax * 1.02 : null;
this.root.innerHTML = `
<div class="hud" id="hudRoot">
<div class="crosshair" id="crosshair"></div>
<div class="hud-top">
<div class="hud-box" id="boxMode"><span class="hud-label">${mode === 'dogfight' ? 'DOG FIGHT' : 'PRECISION FLIGHT'}</span><span class="hud-value" id="hTimer">0:00</span></div>
<div class="hud-box" id="boxScore"><span class="hud-label">SCORE</span><span class="hud-value" id="hScore">0</span></div>
<div class="hud-box" id="boxMsg"><span class="hud-value msg" id="hMsg"></span></div>
</div>
<div class="hud-left">
<div class="gauge-v" id="gSpd"><span class="hud-label">KTS</span><span class="hud-value" id="hIas">0</span><span class="speed-arc" id="hArc"></span><span class="hud-warn" id="hStall">STALL</span></div>
<div class="gauge-h" id="gAlt"><span class="hud-label">ALT FT</span><span class="hud-value" id="hAlt">0</span><span class="hud-value sub" id="hVs">0</span></div>
</div>
<div class="hud-right">
<div class="gauge-h" id="gThr"><span class="hud-label">THR</span><span class="hud-value" id="hThr">0</span><div class="bar"><div class="bar-fill" id="hThrBar"></div></div><span class="hud-warn" id="hOver">OVERSPEED</span></div>
<div class="gauge-h" id="gHeat"><span class="hud-label">ENG</span><div class="bar"><div class="bar-fill heat" id="hHeat"></div></div><span class="hud-value sub" id="hRpm">0</span></div>
${mode === 'dogfight' ? `<div class="gauge-h"><span class="hud-label">TRK</span><span class="hud-value" id="hAmmo">30</span><span class="hud-value sub" id="hKills">0 KILLS</span></div>
<div class="gauge-h"><span class="hud-label">DUR</span><div class="bar"><div class="bar-fill dur" id="hDur"></div></div><span class="hud-value sub" id="hDurT">100</span></div>` : ''}
</div>
<div class="hud-bottom">
<div class="hud-box"><span class="hud-label">HDG</span><span class="hud-value" id="hHdg">000</span></div>
<div class="hud-box"><span class="hud-label">G</span><span class="hud-value" id="hG">1.0</span></div>
${mode === 'dogfight' ? '<div class="hud-box" id="boxTgt"><span class="hud-label">TARGET</span><span class="hud-value" id="hTgt">—</span></div>' : ''}
<div class="hud-box" id="boxWave">${mode === 'dogfight' ? '<span class="hud-label">WAVE</span><span class="hud-value" id="hWave">1</span>' : '<span class="hud-label">TODAY</span><span class="hud-value" id="hTod">07:00</span>'}</div>
</div>
</div>`;
void limit;
}
show() { this.root.classList.remove('hidden'); }
hide() { this.root.classList.add('hidden'); }
set(d) {
const t = this.t;
const txt = (id, v) => { const e = document.getElementById(id); if (e)
e.textContent = v; };
txt('hIas', String(Math.max(0, Math.round(d.ias))));
txt('hAlt', String(Math.max(0, Math.round(d.alt * 3.28084))));
txt('hVs', (d.vs >= 0 ? '+' : '') + Math.round(d.vs * 3.28084) + ' fpm');
txt('hHdg', String(Math.round(d.hdg)).padStart(3, '0'));
txt('hG', d.gLoad.toFixed(1));
txt('hThr', String(Math.round(d.throttle * 100)));
const thrBar = document.getElementById('hThrBar');
if (thrBar)
thrBar.style.width = Math.round(d.throttle * 100) + '%';
const heat = document.getElementById('hHeat');
if (heat)
heat.style.width = Math.round(d.heat * 100) + '%';
txt('hRpm', String(Math.round(d.rpm)));
txt('hScore', String(d.score));
txt('hMsg', d.message);
const m = Math.floor(d.timer / 60), s = Math.floor(d.timer % 60);
txt('hTimer', m + ':' + String(s).padStart(2, '0'));
const stallEl = document.getElementById('hStall');
if (stallEl)
stallEl.style.visibility = d.stalled || d.lowSpeed ? 'visible' : 'hidden';
const overEl = document.getElementById('hOver');
if (overEl)
overEl.style.visibility = d.speedLimit !== null && d.speed > d.speedLimit ? 'visible' : 'hidden';
const arc = document.getElementById('hArc');
if (arc) {
if (d.speedLimit !== null) {
const frac = Math.min(1.25, d.speed / (d.speedLimit * 1.15));
arc.style.transform = `rotate(${frac * 270 - 135}deg)`;
arc.style.borderColor = d.speed > d.speedLimit ? '#ff5544' : '#59d4ff';
arc.style.opacity = '1';
}
else {
arc.style.opacity = '0';
}
}
const durBar = document.getElementById('hDur');
if (durBar)
durBar.style.width = Math.max(0, Math.min(100, d.durability)) + '%';
const durT = document.getElementById('hDurT');
if (durT)
durT.textContent = String(Math.max(0, Math.round(d.durability)));
txt('hAmmo', String(d.ammo));
txt('hKills', d.kills + ' KILLS');
txt('hWave', String(d.wave));
txt('hTgt', d.targetDist === null ? 'CLEAR' :
Math.round(d.targetDist / 10) / 100 + ' km ' + (d.targetBearing !== null ? (d.targetBearing > 0 ? 'R' : 'L') : ''));
const tod = d.timeOfDay;
txt('hTod', String(Math.floor(tod)).padStart(2, '0') + ':' + String(Math.floor((tod % 1) * 60)).padStart(2, '0'));
}
}
exports.Hud = Hud;
};
// ── module: src/enemies.ts ──
__mods["src/enemies.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Enemy = void 0;
const vec_1 = require("./vec");
const models_1 = require("./models");
const AI_SPEC = {
id: 'bandit',
name: 'Bandit',
role: 'AI',
desc: '',
mass: 7600, wingArea: 31, wingSpan: 9.8, cd0: 0.021, clMax: 1.45, clAlpha: 5.0, clL0: 0.06,
thrust: 15600, vstall: 47, vmax: 200, gLimit: 7,
ctrl: { pitch: 1.9, roll: 4.4, yaw: 1.0, resp: 3.6, maxBank: 0.6 },
colors: { a: 0x4a3f3d, b: 0x2c2624, accent: 0xff4433, glass: 0x1a100e },
};
class Enemy {
constructor(spawn, heading, speed, terrainH) {
this.terrainH = terrainH;
this.hp = 100;
this.alive = true;
this.deadTimer = 0;
this.fireCd = 0;
this.lastHit = -99;
this.pose = { pos: { ...spawn }, yaw: heading, pitch: 0, bank: 0, speed, vy: 0 };
const built = (0, models_1.buildAircraft)(AI_SPEC);
this.mesh = built.group;
this.prop = built.prop;
this.mesh.userData.isEnemy = true;
}
get pos() { return this.pose.pos; }
get alive_() { return this.alive; }
update(dt, targetPos, t) {
const P = this.pose;
if (!this.alive) {
this.deadTimer += dt;
P.pos.y -= dt * (20 + 80 * this.deadTimer);
P.bank += dt * 2.2;
this.applyPose();
return { firing: false, muzzle: null };
}
const dx = targetPos.x - P.pos.x, dy = targetPos.y - P.pos.y, dz = targetPos.z - P.pos.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
// desired heading toward target with slight altitude bias
const aim = (0, vec_1.v3)(dx, (0, vec_1.clamp)(dy, -150, 150) * 0.85, dz);
const desiredYaw = Math.atan2(aim.x, aim.z);
let dYaw = desiredYaw - P.yaw;
while (dYaw > Math.PI)
dYaw -= Math.PI * 2;
while (dYaw < -Math.PI)
dYaw += Math.PI * 2;
// turn rate limit (rad/s)
const maxTurn = 1.35;
P.yaw += (0, vec_1.clamp)(dYaw, -maxTurn * dt, maxTurn * dt);
// bank into turn (visual + energy flavor)
P.bank += ((0, vec_1.clamp)(dYaw * 1.6, -0.75, 0.75) - P.bank) * Math.min(1, 2.4 * dt);
// pitch toward target altitude
const desiredPitch = Math.atan2((0, vec_1.clamp)(dy, -260, 260), Math.max(120, dist));
P.pitch += (0, vec_1.clamp)(desiredPitch - P.pitch, -0.8 * dt, 0.8 * dt);
// speed management: slow when close, fast when far, never below stall-ish
const desiredSpeed = (0, vec_1.clamp)(172 - dist * 0.1, 116, 182);
P.speed += (desiredSpeed - P.speed) * Math.min(1, 0.8 * dt);
P.vy = Math.sin(P.pitch) * P.speed;
// move
P.pos.x += Math.sin(P.yaw) * Math.cos(P.pitch) * P.speed * dt;
P.pos.y += P.vy * dt;
P.pos.z += Math.cos(P.yaw) * Math.cos(P.pitch) * P.speed * dt;
// terrain follow
const terr = this.terrainH(P.pos.x, P.pos.z);
if (P.pos.y < terr + 85)
P.pos.y = terr + 85;
if (P.pos.y > 2400)
P.pos.y = 2400;
// firing
this.fireCd -= dt;
const fwd = this.forward();
const toT = (0, vec_1.v3)(dx, dy, dz);
const align = dist > 1 ? (0, vec_1.dot)(fwd, normalizeIn(toT)) : 0;
let firing = false, muzzle = null;
if (dist < 680 && align > 0.96 && this.fireCd <= 0) {
this.fireCd = 0.085;
firing = true;
muzzle = (0, vec_1.v3)(P.pos.x + fwd.x * 9, P.pos.y + fwd.y * 9, P.pos.z + fwd.z * 9);
}
this.applyPose();
this.prop.rotation.x += dt * 30;
return { firing, muzzle };
}
applyPose() {
const P = this.pose;
this.mesh.position.set(P.pos.x, P.pos.y, P.pos.z);
// flight frame +X forward: yaw about world Y, pitch about local X (nose up), bank about local X
this.mesh.rotation.set(0, 0, 0);
this.mesh.rotateY(-P.yaw);
this.mesh.rotateZ(P.pitch); // nose about lateral axis
this.mesh.rotateX(P.bank); // roll about forward axis
}
forward() {
const P = this.pose;
const cx = Math.cos(P.pitch);
return (0, vec_1.v3)(Math.sin(P.yaw) * cx, Math.sin(P.pitch), Math.cos(P.yaw) * cx);
}
hit(dmg, t) {
this.hp -= dmg;
this.lastHit = t;
if (this.hp <= 0 && this.alive) {
this.alive = false;
this.deadTimer = 0;
}
}
}
exports.Enemy = Enemy;
function normalizeIn(v) {
const l = (0, vec_1.len)(v) || 1;
return (0, vec_1.v3)(v.x / l, v.y / l, v.z / l);
}
};
// ── module: src/effects.ts ──
__mods["src/effects.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Effects = void 0;
const MAX = 260;
class Effects {
constructor() {
this.group = new THREE.Group();
this.pool = [];
this.idx = 0;
this.tracers = [];
this.tracerLife = [];
const mk = (color, opacity) => new THREE.SpriteMaterial({ color, transparent: true, opacity, depthWrite: false, fog: false });
this.fireMat = mk(0xffa640, 0.9);
this.smokeMat = mk(0x555a60, 0.5);
this.sparkMat = mk(0xffe9a0, 0.95);
this.splashMat = mk(0xbfd9e8, 0.8);
for (let i = 0; i < MAX; i++) {
const sp = new THREE.Sprite(this.fireMat);
sp.visible = false;
this.group.add(sp);
this.pool.push({ alive: false, pos: { x: 0, y: 0, z: 0 }, vel: { x: 0, y: 0, z: 0 }, life: 0, maxLife: 1, size: 1, sprite: sp, kind: 'fire' });
}
const tg = new THREE.BoxGeometry(1, 0.14, 0.14);
const tm = new THREE.SpriteMaterial({ color: 0xffe9a0, transparent: true, opacity: 0.9, depthWrite: false, fog: false });
for (let i = 0; i < 80; i++) {
const m = new THREE.Mesh(tg, tm);
m.visible = false;
this.group.add(m);
this.tracers.push(m);
this.tracerLife.push(0);
}
}
spawn(pos, vel, kind, life, size) {
const p = this.pool[this.idx];
this.idx = (this.idx + 1) % MAX;
p.alive = true;
p.pos.x = pos.x;
p.pos.y = pos.y;
p.pos.z = pos.z;
p.vel.x = vel.x;
p.vel.y = vel.y;
p.vel.z = vel.z;
p.life = life;
p.maxLife = life;
p.size = size;
p.kind = kind;
p.sprite.material = kind === 'fire' ? this.fireMat : kind === 'smoke' ? this.smokeMat : kind === 'spark' ? this.sparkMat : this.splashMat;
p.sprite.visible = true;
p.sprite.position.set(p.pos.x, p.pos.y, p.pos.z);
}
explosion(pos, big = 1) {
const n = 14 + Math.floor(big * 10);
for (let i = 0; i < n; i++) {
const a = Math.random() * Math.PI * 2, e = Math.random() * Math.PI;
const sp = (6 + Math.random() * 26) * big;
this.spawn(pos, { x: Math.cos(a) * Math.sin(e) * sp, y: Math.cos(e) * sp * 0.8 + 8, z: Math.sin(a) * Math.sin(e) * sp }, 'fire', 0.5 + Math.random() * 0.5, 1.6 * big + Math.random());
}
for (let i = 0; i < n * 2; i++) {
const a = Math.random() * Math.PI * 2, e = Math.random() * Math.PI;
const sp = (3 + Math.random() * 14) * big;
this.spawn(pos, { x: Math.cos(a) * Math.sin(e) * sp, y: Math.cos(e) * sp * 0.5 + 14, z: Math.sin(a) * Math.sin(e) * sp }, 'smoke', 1.2 + Math.random() * 1.6, 2.2 * big + Math.random() * 2);
}
for (let i = 0; i < 10; i++) {
const a = Math.random() * Math.PI * 2, e = Math.random() * Math.PI;
const sp = (20 + Math.random() * 40) * big;
this.spawn(pos, { x: Math.cos(a) * Math.sin(e) * sp, y: Math.cos(e) * sp, z: Math.sin(a) * Math.sin(e) * sp }, 'spark', 0.3 + Math.random() * 0.3, 0.4);
}
}
splash(pos) {
for (let i = 0; i < 24; i++) {
const a = Math.random() * Math.PI * 2;
const sp = 8 + Math.random() * 22;
this.spawn(pos, { x: Math.cos(a) * sp * 0.5, y: 16 + Math.random() * 24, z: Math.sin(a) * sp * 0.5 }, 'splash', 0.9 + Math.random() * 0.6, 0.9 + Math.random());
}
}
hitFlash(pos) {
for (let i = 0; i < 6; i++) {
const a = Math.random() * Math.PI * 2;
this.spawn(pos, { x: Math.cos(a) * 12, y: 6 + Math.random() * 8, z: Math.sin(a) * 12 }, 'spark', 0.25, 0.5);
}
}
tracer(from, to, hit) {
// find free tracer
for (let i = 0; i < this.tracers.length; i++) {
if (this.tracerLife[i] <= 0) {
const m = this.tracers[i];
const ax = from.x, ay = from.y, az = from.z;
const tx = hit ? hit.x : to.x, ty = hit ? hit.y : to.y, tz = hit ? hit.z : to.z;
const dx = tx - ax, dy = ty - ay, dz = tz - az;
const l = Math.sqrt(dx * dx + dy * dy + dz * dz) || 1;
m.visible = true;
m.scale.set(l, 1, 1);
m.position.set((ax + tx) / 2, (ay + ty) / 2, (az + tz) / 2);
m.lookAt(tx, ty, tz);
this.tracerLife[i] = 0.09;
return;
}
}
}
smokeTrails(pos, q, speed, dt) {
if (this.pool[this.idx].alive && Math.random() < 0.5)
return;
// emit from wingtips roughly: offset in local frame
const off = 1;
const { x: qx, y: qy, z: qz, w: qw } = q;
const rot = (v) => {
const tx = qy * v.z - qz * v.y, ty = qz * v.x - qx * v.z, tz = qx * v.y - qy * v.x;
return {
x: v.x + 2 * (qw * tx + (qy * tz - qz * ty)),
y: v.y + 2 * (qw * ty + (qz * tx - qx * tz)),
z: v.z + 2 * (qw * tz + (qx * ty - qy * tx)),
};
};
const kind = speed > 130 ? 'smoke' : 'spark';
for (const s of [-1, 1]) {
const r = rot({ x: -1.2, y: 0.2, z: s * off * 5 });
this.spawn({ x: pos.x + r.x, y: pos.y + r.y, z: pos.z + r.z }, { x: 0, y: 2, z: 0 }, kind, 0.9, 0.7);
}
void dt;
}
update(dt) {
for (const p of this.pool) {
if (!p.alive)
continue;
p.life -= dt;
if (p.life <= 0) {
p.alive = false;
p.sprite.visible = false;
continue;
}
p.pos.x += p.vel.x * dt;
p.pos.y += p.vel.y * dt;
p.pos.z += p.vel.z * dt;
p.vel.y += (p.kind === 'smoke' ? 4 : -22) * dt;
const f = p.life / p.maxLife;
p.sprite.position.set(p.pos.x, p.pos.y, p.pos.z);
const s = p.size * (p.kind === 'smoke' ? (1.4 - f) * 3 : f + 0.35);
p.sprite.scale.set(s, s, 1);
p.sprite.material.opacity = f * (p.kind === 'smoke' ? 0.4 : 0.85);
}
for (let i = 0; i < this.tracers.length; i++) {
if (this.tracerLife[i] > 0) {
this.tracerLife[i] -= dt;
if (this.tracerLife[i] <= 0)
this.tracers[i].visible = false;
}
}
}
}
exports.Effects = Effects;
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Game = void 0;
const types_1 = require("./types");
const vec_1 = require("./vec");
const aerodynamics_1 = require("./aerodynamics");
const terrain_1 = require("./terrain");
const scene_1 = require("./scene");
const models_1 = require("./models");
const sound_1 = require("./sound");
const input_1 = require("./input");
const hud_1 = require("./hud");
const enemies_1 = require("./enemies");
const effects_1 = require("./effects");
class Game {
constructor(mode, spec, canvas) {
this.sound = new sound_1.Sound();
this.input = new input_1.Input();
this.effects = new effects_1.Effects();
this.enemies = [];
this.time = 0;
this.paused = false;
this.over = false; // mission over (player dead)
this.score = 0;
this.kills = 0;
this.durability = 100;
this.wave = 1;
this.ammo = 30;
this.tod = 7; // time of day (hours, precision)
this.waveClear = true;
this.waveMsgT = 0;
this.msg = '';
this.msgT = 0;
this.shake = 0;
this.fireCd = 0;
this.waveTimer = 0;
this.lastWaves = 0;
this.ev = (0, types_1.makeEvents)();
this.camQ = (0, vec_1.qId)();
this.tmp = (0, vec_1.v3)(0, 0, 0);
// precision landing bookkeeping
this.landCount = 0;
this.takeoffs = 0;
this.lastLandingOk = true;
this.mode = mode;
this.spec = spec;
this.renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
this.renderer.setPixelRatio(Math.min(2, window.devicePixelRatio || 1));
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.renderer.shadowMap.enabled = true;
this.renderer.shadowMap.type = THREE.PCFSoftShadowMap;
this.camera = new THREE.PerspectiveCamera(62, window.innerWidth / window.innerHeight, 0.5, 60000);
this.terrain = (0, terrain_1.createTerrain)(5200, mode === 'dogfight' ? 1337 : 9001);
this.world = (0, scene_1.createWorld)(this.terrain);
const scene = this.world.scene;
scene.add(this.effects.group);
// place the plane
const built = (0, models_1.buildAircraft)(spec);
this.plane = built.group;
this.prop = built.prop;
scene.add(this.plane);
const s = (0, types_1.makeState)();
if (mode === 'precision') {
s.pos = (0, vec_1.v3)(0, 8.6, 250);
// nose along the runway: body +X -> world -Z (heading north)
s.q = { x: 0, y: Math.SQRT1_2, z: 0, w: Math.SQRT1_2 };
s.throttle = 0;
this.world.setSun(38);
}
else {
s.pos = (0, vec_1.v3)(500, 1300, -2400);
s.vel = (0, vec_1.v3)(0, 0, 135); // heading south (+Z)
s.q = { x: 0, y: -Math.SQRT1_2, z: 0, w: Math.SQRT1_2 }; // body +X -> world +Z
this.world.setSun(46);
}
this.aero = new aerodynamics_1.Aero(spec, s);
this.hud = new hud_1.Hud(mode, spec);
this.input.attach(window);
this.input.enabled = true;
this.say(mode === 'dogfight' ? 'WAVE 1 INBOUND — ENGAGE' : 'TAKE OFF, CLIMB, AND RETURN TO LAND', 4);
if (mode === 'dogfight')
this.spawnWave(1);
this.resize = this.resize.bind(this);
window.addEventListener('resize', this.resize);
}
s() {
return this.aero.s;
}
resize() {
this.renderer.setSize(window.innerWidth, window.innerHeight);
this.camera.aspect = window.innerWidth / window.innerHeight;
this.camera.updateProjectionMatrix();
}
say(m, t = 2.5) {
this.msg = m;
this.msgT = t;
}
spawnWave(n) {
const rnd = (0, terrain_1.mulberry)(1000 + n * 97);
const count = Math.min(2 + Math.floor(n * 0.9), 5);
const base = this.aero.s.pos;
for (let i = 0; i < count; i++) {
const ang = (i / count) * Math.PI * 2 + rnd() * 0.6;
const d = 1500 + rnd() * 500;
const spawn = (0, vec_1.v3)(base.x + Math.sin(ang) * d, base.y + (rnd() - 0.4) * 420, base.z + Math.cos(ang) * d);
spawn.y = (0, vec_1.clamp)(spawn.y, 300, 2200);
const e = new enemies_1.Enemy(spawn, ang, 150, (x, z) => this.terrain.h(x, z));
e.alive = true;
e.hp = 80 + n * 8;
this.enemies.push(e);
this.world.scene.add(e.mesh);
}
this.ammo = 40;
this.waveClear = false;
}
cleanupDead() {
for (let i = this.enemies.length - 1; i >= 0; i--) {
const e = this.enemies[i];
if (!e.alive && (e.deadTimer > 4 || e.pos.y < this.terrain.h(e.pos.x, e.pos.z))) {
this.world.scene.remove(e.mesh);
this.enemies.splice(i, 1);
}
}
}
/** main update; returns false if the player died */
update(dt) {
if (this.paused || this.over)
return !this.over;
this.time += dt;
this.msgT -= dt;
this.waveMsgT -= dt;
this.fireCd -= dt;
this.shake = Math.max(0, this.shake - dt * 2.2);
const ctl = this.input.update(dt);
const s = this.aero.s;
// ---- surface info ----
const terr = this.terrain.h(s.pos.x, s.pos.z);
const rwZ = s.pos.z < 430 && s.pos.z > -1100 && Math.abs(s.pos.x) < 20;
const rwPlateau = rwZ && terr < 12;
const water = !rwZ && terr < 0.6 && s.pos.y < 12;
// positional flag: plane over the runway polygon (any height)
this.aero.onRunway = rwPlateau;
this.aero.onWater = water;
this.aero.groundY = rwPlateau ? 8.6 : terr;
// ---- physics substeps ----
const ev = this.ev;
ev.takeoff = false;
ev.landed = null;
ev.crash = null;
ev.kill = false;
ev.wave = false;
const sub = 4;
const h = dt / sub;
for (let i = 0; i < sub && !s.dead; i++) {
this.aero.updateRates(h, ctl);
this.aero.step(h, ctl, ev);
}
if (ev.takeoff) {
this.sound.clack();
this.say('Nose up — you are airborne', 2);
if (this.mode === 'precision')
this.takeoffs++;
}
const landed = this.ev.landed;
if (landed) {
if (landed.ok) {
this.landCount++;
this.lastLandingOk = true;
const spd = landed.speed;
const pts = Math.max(100, Math.round(1400 - spd * 14));
this.score += pts;
this.tod += 0.32;
this.world.setSun(8 + 52 * Math.sin(Math.min(1, Math.max(0, (this.tod - 6) / 12))));
this.say(`GOOD TOUCHDOWN — +${pts} (speed ${Math.round(spd * 1.944)} kts)`, 4);
this.sound.wave();
if (spd < 55)
this.say('SMOOTH AS BUTTER — +250', 3), (this.score += 250);
}
else {
this.lastLandingOk = false;
}
}
if (ev.crash) {
const s2 = s;
this.effects.explosion((0, vec_1.v3)(s2.pos.x, s2.pos.y + 1, s2.pos.z), 1.4);
if (this.aero.onWater)
this.effects.splash((0, vec_1.v3)(s2.pos.x, this.aero.groundY, s2.pos.z));
this.sound.explosion();
this.shake = 1;
this.endPlayer('crash', ev.crash);
}
// ---- terrain / water / boundary collision (post-integration) ----
if (!s.dead && !this.aero.onRunway) {
const hgt = this.terrain.h(s.pos.x, s.pos.z);
if (s.pos.y < hgt + 0.9) {
if (hgt < 0.6) {
this.effects.splash((0, vec_1.v3)(s.pos.x, 0, s.pos.z));
this.sound.splash(0.7);
}
else {
this.effects.explosion((0, vec_1.v3)(s.pos.x, hgt + 1, s.pos.z), 1);
this.sound.explosion(0.6);
}
this.shake = 1;
s.pos.y = hgt + 0.9;
this.endPlayer('crash', hgt < 0.6 ? 'Flew into the sea' : 'Terrain impact');
}
}
const bound = 2560;
if (!s.dead && (Math.abs(s.pos.x) > bound || Math.abs(s.pos.z) > bound)) {
this.endPlayer('crash', 'Lost over the mountains — mission boundary');
}
// ---- mode logic ----
if (this.mode === 'dogfight')
this.updateDogfight(dt);
// ---- enemies ----
const tPos = (0, vec_1.v3)(s.pos.x, s.pos.y, s.pos.z);
for (const e of this.enemies) {
if (!e.alive) {
e.update(dt, tPos, this.time);
continue;
}
const res = e.update(dt, tPos, this.time);
if (res.firing) {
// enemy tracer + hit check
const aim = (0, vec_1.v3)(s.pos.x + (Math.random() - 0.5) * 34 * (0.4 + (0, vec_1.len)(s.vel) / 300), s.pos.y + (Math.random() - 0.5) * 34 * (0.4 + (0, vec_1.len)(s.vel) / 300), s.pos.z + (Math.random() - 0.5) * 34 * (0.4 + (0, vec_1.len)(s.vel) / 300));
this.effects.tracer(res.muzzle, aim, null);
this.sound.boom(0.12);
const dd = (0, vec_1.dist)(res.muzzle, tPos);
const hitChance = (0, vec_1.clamp)(1 - dd / 900, 0, 0.3);
if (Math.random() < hitChance)
this.playerHit(4, tPos);
}
}
this.cleanupDead();
// ---- visual sync ----
this.plane.position.set(s.pos.x, s.pos.y, s.pos.z);
this.plane.quaternion.set(s.q.x, s.q.y, s.q.z, s.q.w);
this.prop.rotation.x += dt * (10 + 60 * s.throttle);
if (s.stalled || (s.gLoad > 5.5 && (0, vec_1.len)(s.vel) > 120))
this.effects.smokeTrails(s.pos, s.q, (0, vec_1.len)(s.vel), dt);
this.updateCamera(dt);
this.world.update(dt, this.time);
this.effects.update(dt);
// fog follows plane altitude
const alt = s.pos.y;
this.world.fog.near = 1500 + alt * 0.8;
this.world.fog.far = 14000 + alt * 1.6;
// ---- audio ----
const windAmp = (0, vec_1.clamp)((0, vec_1.len)(s.vel) / 160, 0, 1) * (0, aerodynamics_1.densityFromAlt)(alt) / 1.225;
this.sound.update(s, dt, windAmp, (0, vec_1.len)(s.vel), Math.max(0, alt));
// ---- HUD ----
const hdg = (Math.atan2(s.q.x, s.q.z) * 180) / Math.PI;
let targetDist = null, targetBearing = null;
if (this.mode === 'dogfight') {
let best = null, bd = 1e9;
for (const e of this.enemies)
if (e.alive) {
const d = (0, vec_1.dist)(e.pos, tPos);
if (d < bd) {
bd = d;
best = e;
}
}
if (best) {
targetDist = bd;
const rel = this.relativeBearing(best.pos);
targetBearing = rel;
if (Math.abs(rel) < 5)
this.hud.root.classList.add('locked');
else
this.hud.root.classList.remove('locked');
}
else
this.hud.root.classList.remove('locked');
}
else
this.hud.root.classList.remove('locked');
this.hud.set({
speed: (0, vec_1.len)(s.vel),
ias: (0, vec_1.len)(s.vel) * 1.94384,
alt: Math.max(0, s.pos.y),
vs: s.vel.y,
hdg: (hdg + 360) % 360,
gLoad: s.gLoad,
throttle: s.throttle,
rpm: s.rpm,
heat: s.heat,
stalled: s.stalled,
lowSpeed: s.lowSpeed,
onGround: s.onGround,
score: this.score,
wave: this.wave,
kills: this.kills,
enemies: this.enemies.filter(e => e.alive).length,
ammo: this.ammo,
durability: this.durability,
mode: this.mode,
message: this.msgT > 0 ? this.msg : '',
timer: this.time,
targetDist,
targetBearing,
speedLimit: this.spec.vmax,
timeOfDay: this.tod,
});
return true;
}
playerHit(dmg, at) {
this.durability = Math.max(0, this.durability - 12);
this.score = Math.max(0, this.score - 15);
this.shake = Math.max(this.shake, 0.4);
this.effects.hitFlash((0, vec_1.v3)(at.x + (Math.random() - 0.5) * 6, at.y, at.z + (Math.random() - 0.5) * 6));
this.sound.boom(0.3);
if (this.durability <= 0) {
this.effects.explosion((0, vec_1.v3)(this.aero.s.pos.x, this.aero.s.pos.y, this.aero.s.pos.z), 1.2);
this.sound.explosion(0.8);
this.endPlayer('shotdown', 'Shot down — airframe destroyed');
}
else {
this.say('TAKING FIRE', 1.2);
}
}
relativeBearing(tp) {
const s = this.aero.s;
// heading from attitude: world direction of body X
const fx = s.q.w * 1 + 2 * (s.q.y * s.q.z - s.q.z * s.q.y); // simplify: compute below properly
// proper: world direction of (1,0,0)
const wx = s.q.w * 1 - 2 * 0; // placeholder
const fwd = {
x: s.q.w * s.q.w + s.q.x * s.q.x - s.q.y * s.q.y - s.q.z * s.q.z,
y: 2 * (s.q.x * s.q.y + s.q.z * s.q.w),
z: 2 * (s.q.x * s.q.z - s.q.y * s.q.w),
};
const up = {
x: 2 * (s.q.x * s.q.y - s.q.z * s.q.w),
y: s.q.w * s.q.w - s.q.x * s.q.x + s.q.y * s.q.y - s.q.z * s.q.z,
z: 2 * (s.q.y * s.q.z + s.q.x * s.q.w),
};
void fx;
void wx;
const to = { x: tp.x - s.pos.x, y: tp.y - s.pos.y, z: tp.z - s.pos.z };
const rl = Math.sqrt(to.x * to.x + to.y * to.y + to.z * to.z) || 1;
const nx = to.x / rl, ny = to.y / rl, nz = to.z / rl;
const rx = up.y * nz - up.z * ny, ry = up.z * nx - up.x * nz, rz = up.x * ny - up.y * nx; // fwd x to
const doty = fwd.x * nx + fwd.y * ny + fwd.z * nz;
return (Math.atan2(rx, doty) * 180) / Math.PI;
}
updateDogfight(dt) {
// shooting
const s = this.aero.s;
if (this.input.key('Space') && this.ammo > 0 && this.fireCd <= 0) {
this.ammo--;
this.fireCd = 0.1;
this.sound.fireGun();
this.shake = Math.max(this.shake, 0.12);
const fwd = this.forward();
const muzzle = (0, vec_1.v3)(s.pos.x + fwd.x * 9, s.pos.y + fwd.y * 9, s.pos.z + fwd.z * 9);
// hit-scan
let hitE = null, bestD = 1e9, bestProj = -2;
for (const e of this.enemies) {
if (!e.alive)
continue;
const rel = { x: e.pos.x - muzzle.x, y: e.pos.y - muzzle.y, z: e.pos.z - muzzle.z };
const l = Math.sqrt(rel.x * rel.x + rel.y * rel.y + rel.z * rel.z);
if (l > 900)
continue;
const nl = { x: rel.x / l, y: rel.y / l, z: rel.z / l };
const proj = fwd.x * nl.x + fwd.y * nl.y + fwd.z * nl.z;
const perp = Math.sqrt(Math.max(0, 1 - proj * proj));
const dist = proj - perp * 26; // 26 m cone half-length to hit
if (proj > 0.99 && perp < 0.03 && dist < bestD) {
bestD = dist;
hitE = e;
bestProj = proj;
}
}
void bestProj;
if (hitE) {
const end = (0, vec_1.v3)(s.pos.x + fwd.x * (bestD + 20), s.pos.y + fwd.y * (bestD + 20), s.pos.z + fwd.z * (bestD + 20));
this.effects.tracer(muzzle, end, (0, vec_1.v3)(hitE.pos.x, hitE.pos.y, hitE.pos.z));
hitE.hit(100, this.time);
this.score += 500;
this.say('TARGET DESTROYED +500', 2);
this.sound.explosion(0.5);
this.effects.explosion((0, vec_1.v3)(hitE.pos.x, hitE.pos.y, hitE.pos.z), 1.1);
this.shake = Math.max(this.shake, 0.4);
this.kills++;
}
else {
const end = (0, vec_1.v3)(s.pos.x + fwd.x * 900, s.pos.y + fwd.y * 900, s.pos.z + fwd.z * 900);
this.effects.tracer(muzzle, end, null);
}
}
// wave management
const alive = this.enemies.filter(e => e.alive).length;
if (alive === 0 && !this.waveClear) {
this.waveClear = true;
this.waveTimer = 0;
this.score += 300;
this.say(`WAVE ${this.wave} CLEARED — +300 REFUEL & REARM`, 3);
this.sound.wave();
}
if (this.waveClear) {
this.waveTimer += dt;
if (this.waveTimer > 4.5) {
this.wave++;
this.waveClear = false;
this.say(`WAVE ${this.wave} INBOUND`, 3);
this.sound.beep(660, 0.06);
this.spawnWave(this.wave);
}
}
}
forward() {
const q = this.aero.s.q;
return {
x: q.w * q.w + q.x * q.x - q.y * q.y - q.z * q.z,
y: 2 * (q.x * q.y + q.z * q.w),
z: 2 * (q.x * q.z - q.y * q.w),
};
}
updateCamera(dt) {
const s = this.aero.s;
const V = (0, vec_1.len)(s.vel);
// chase target point behind & above
const back = 26 + (0, vec_1.clamp)(V * 0.055, 0, 22);
const up = 9 + (0, vec_1.clamp)(V * 0.012, 0, 5);
const q = s.q;
const rX = { x: 2 * (q.y * q.z + q.w * q.x), y: q.w * q.w - q.x * q.x + q.y * q.y - q.z * q.z, z: 2 * (q.z * q.y - q.w * q.x) }; // body Y in world
const rY = { x: 2 * (q.x * q.z + q.w * q.y), y: 2 * (q.y * q.z - q.w * q.x), z: q.w * q.w - q.x * q.x - q.y * q.y + q.z * q.z }; // body Z? use world up instead
// keep chase point up in WORLD space (feels stable): behind = -fwd * back, +world up * up
const fwd = this.forward();
const tX = s.pos.x - fwd.x * back + (Math.random() - 0.5) * this.shake * 1.6;
const tY = Math.max(s.pos.y + up, this.terrain.h(s.pos.x, s.pos.z) + 4);
const tZ = s.pos.z - fwd.z * back + (Math.random() - 0.5) * this.shake * 1.6;
void rX;
void rY;
const k = 1 - Math.pow(0.0018, dt);
if (this.camera.position.x === 0 && this.camera.position.y === 0 && this.camera.position.z === 0) {
this.camera.position.set(tX, tY, tZ);
}
else {
this.camera.position.x += (tX - this.camera.position.x) * k;
this.camera.position.y += (tY - this.camera.position.y) * k;
this.camera.position.z += (tZ - this.camera.position.z) * k;
}
this.camera.lookAt(s.pos.x + fwd.x * 14, s.pos.y + 2, s.pos.z + fwd.z * 14);
}
endPlayer(_kind, reason) {
const s = this.aero.s;
s.dead = true;
this.over = true;
this.sound.clack();
if (this.mode === 'dogfight') {
this.say(`AIRFRAME LOST — ${reason.toUpperCase()}`, 6);
}
else {
this.say(`MISSION FAILED — ${reason.toUpperCase()}`, 6);
}
}
dispose() {
window.removeEventListener('resize', this.resize);
this.input.detach();
this.world.dispose();
this.renderer.dispose();
}
}
exports.Game = Game;
};
// ── module: src/menu.ts ──
__mods["src/menu.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PauseUI = exports.Menu = void 0;
const fleet_1 = require("./fleet");
class Menu {
constructor(cb) {
this.selId = 'kestrel';
this.selMode = 'precision';
this.cb = cb;
this.root = document.getElementById('menu');
this.render();
}
show() { this.root.classList.remove('hidden'); }
hide() { this.root.classList.add('hidden'); }
render() {
const fleet = fleet_1.FLEET.map(f => `
<div class="craft ${f.id === this.selId ? 'sel' : ''}" data-id="${f.id}">
<div class="craft-name">${f.name}</div>
<div class="craft-role">${f.role}</div>
<div class="craft-desc">${f.desc}</div>
<div class="craft-stats">
${this.stat('SPD', f.vmax, 260)}
${this.stat('THR', f.thrust, 46000)}
${this.stat('LFT', f.clMax, 1.7)}
${this.stat('AGL', 30 / f.vstall, 1.4)}
${this.stat('G', f.gLimit, 10)}
</div>
</div>`).join('');
this.root.innerHTML = `
<div class="menu-bg"></div>
<div class="menu">
<div class="menu-title">
<h1>APEX<span>AERO</span></h1>
<p class="menu-sub">High-Fidelity Flight Dynamics · Procedural World</p>
</div>
<div class="menu-craft">${fleet}</div>
<div class="menu-modes">
<button class="mode-btn ${this.selMode === 'precision' ? 'sel' : ''}" data-mode="precision">
<span class="mode-ico">◈</span><span class="mode-t">PRECISION FLIGHT</span>
<span class="mode-d">Takeoff & landing</span>
</button>
<button class="mode-btn ${this.selMode === 'dogfight' ? 'sel' : ''}" data-mode="dogfight">
<span class="mode-ico">✦</span><span class="mode-t">DOG FIGHT</span>
<span class="mode-d">Survive the waves</span>
</button>
</div>
<button id="btnPlay" class="play-btn">▶ LAUNCH MISSION</button>
<div class="menu-keys">
<div><b>W A S D</b> Stick (pitch / roll)</div>
<div><b>E / Q</b> Throttle ↑ / ↓</div>
<div><b>SPACE</b> Brake · Fire</div>
<div><b>X</b> Brake</div>
<div><b>P</b> Pause</div>
<div><b>M</b> Mute</div>
<div><b>R</b> Reset</div>
</div>
</div>`;
this.root.querySelectorAll('.craft').forEach(el => {
el.addEventListener('click', () => {
this.selId = el.dataset.id || 'kestrel';
this.render();
});
});
this.root.querySelectorAll('.mode-btn').forEach(el => {
el.addEventListener('click', () => {
this.selMode = el.dataset.mode;
this.render();
});
});
this.root.querySelector('#btnPlay').addEventListener('click', () => {
this.cb.onPlay(this.selMode, (0, fleet_1.fleetById)(this.selId).id);
});
}
stat(label, val, max) {
const w = Math.max(4, Math.min(100, (val / max) * 100));
return `<div class="stat"><span class="stat-l">${label}</span><span class="stat-bar"><span style="width:${w}%"></span></span></div>`;
}
}
exports.Menu = Menu;
class PauseUI {
constructor(cb) {
this.cb = cb;
this.root = document.getElementById('pause');
this.root.innerHTML = `
<div class="pause-box">
<h2>PAUSED</h2>
<button id="btnResume">RESUME</button>
<button id="btnRestart">RESTART MISSION</button>
<button id="btnMenu">MAIN MENU</button>
</div>`;
this.root.querySelector('#btnResume').addEventListener('click', this.cb.onResume);
this.root.querySelector('#btnRestart').addEventListener('click', this.cb.onRestart);
this.root.querySelector('#btnMenu').addEventListener('click', this.cb.onMenu);
}
show() { this.root.classList.remove('hidden'); }
hide() { this.root.classList.add('hidden'); }
}
exports.PauseUI = PauseUI;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"typescript","sessionId":"eeb19dc9-751b-49cc-928e-6f0fe21ce9fd","tokensIn":17894892,"tokensOut":269005,"tokensTotal":18163897,"cost":0,"turns":169,"toolCalls":227,"failedToolCalls":0,"timestamp":"2026-09-09T14:36:13.989Z"} -->