Flight Simulator
Qwen 3.6 27B MTP 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 81,620 bytes · SHA-256 a34f01763ba6
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Apex Aero — 3D Flight Simulator</title>
<script src="https://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif; }
#app { position: relative; width: 100%; height: 100%; }
#game-canvas { display: block; width: 100%; height: 100%; }
/* ─── Menu Overlay ─── */
#menu-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
background: radial-gradient(ellipse at center, rgba(10,20,40,0.95) 0%, rgba(0,5,15,0.98) 100%);
display: flex; align-items: center; justify-content: center;
z-index: 100;
transition: opacity 0.5s;
}
#menu-overlay.hidden { opacity: 0; pointer-events: none; }
#menu-panel {
max-width: 900px; width: 90%; padding: 40px;
background: rgba(10,25,50,0.85);
border: 1px solid rgba(60,120,200,0.3);
border-radius: 12px;
backdrop-filter: blur(10px);
text-align: center;
}
#menu-panel h1 { font-size: 3em; color: #4fc3f7; text-shadow: 0 0 30px rgba(79,195,247,0.5); margin-bottom: 8px; letter-spacing: 4px; }
.subtitle { color: #78909c; font-size: 1.1em; margin-bottom: 30px; letter-spacing: 2px; }
#menu-panel h2 { color: #b0bec5; font-size: 1.2em; margin: 20px 0 15px; text-transform: uppercase; letter-spacing: 2px; }
.aircraft-cards, .scenario-cards {
display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-bottom: 10px;
}
.ac-card, .sc-card {
background: rgba(20,40,70,0.7);
border: 2px solid rgba(60,120,200,0.2);
border-radius: 8px; padding: 16px; width: 260px;
cursor: pointer; transition: all 0.3s;
}
.ac-card:hover, .sc-card:hover { border-color: rgba(79,195,247,0.6); background: rgba(30,60,100,0.7); }
.ac-card.selected, .sc-card.selected { border-color: #4fc3f7; background: rgba(40,80,130,0.8); box-shadow: 0 0 20px rgba(79,195,247,0.3); }
.ac-card h3, .sc-card h3 { color: #4fc3f7; font-size: 1.1em; margin-bottom: 6px; }
.ac-card p, .sc-card p { color: #90a4ae; font-size: 0.85em; line-height: 1.4; }
.ac-stats { display: flex; justify-content: space-between; margin-top: 10px; font-size: 0.75em; color: #607d8b; }
.ac-stats span { background: rgba(0,0,0,0.3); padding: 2px 8px; border-radius: 4px; }
.btn-primary {
background: linear-gradient(135deg, #1565c0, #0d47a1);
color: white; border: none; padding: 14px 40px; font-size: 1.1em;
border-radius: 6px; cursor: pointer; margin-top: 25px;
letter-spacing: 2px; text-transform: uppercase; font-weight: bold;
transition: all 0.3s;
}
.btn-primary:hover { background: linear-gradient(135deg, #1976d2, #1565c0); transform: scale(1.05); }
/* ─── HUD ─── */
#hud {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 10; display: none;
}
#hud.active { display: block; }
#hud-top {
position: absolute; top: 15px; left: 50%; transform: translateX(-50%);
text-align: center; color: #4fc3f7; font-size: 0.8em; letter-spacing: 1px;
}
#hud-aircraft-name { font-size: 1.2em; font-weight: bold; }
#hud-scenario-name { color: #78909c; font-size: 0.85em; }
.hud-left, #hud-right {
position: absolute; top: 50%; transform: translateY(-50%);
display: flex; flex-direction: column; gap: 4px;
}
#hud-left { left: 20px; }
#hud-right { right: 20px; text-align: right; }
.hud-item {
background: rgba(0,10,30,0.5); border: 1px solid rgba(79,195,247,0.15);
border-radius: 4px; padding: 6px 12px; min-width: 120px;
display: flex; align-items: center; gap: 8px;
}
.hud-label { color: #607d8b; font-size: 0.7em; letter-spacing: 1px; min-width: 30px; }
.hud-value { color: #4fc3f7; font-size: 1em; font-weight: bold; font-family: 'Courier New', monospace; min-width: 50px; }
.hud-unit { color: #546e7a; font-size: 0.7em; }
#hud-bottom {
position: absolute; bottom: 10px; left: 50%; transform: translateX(-50%);
text-align: center;
}
#hud-controls-hint {
color: rgba(120,144,156,0.6); font-size: 0.7em; margin-top: 5px; letter-spacing: 1px;
}
#hud-status { color: #4fc3f7; font-size: 0.85em; min-height: 1.2em; }
#crosshair {
position: absolute; top: 50%; left: 50%; transform: translate(-50%,-50%);
color: rgba(79,195,247,0.5); font-size: 24px; font-weight: bold;
pointer-events: none; z-index: 11;
}
#message-overlay {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
pointer-events: none; z-index: 20;
}
.msg-box {
background: rgba(0,10,30,0.9); border: 2px solid #4fc3f7; border-radius: 12px;
padding: 30px 50px; text-align: center; color: #4fc3f7; font-size: 1.5em;
animation: fadeIn 0.3s;
}
@keyframes fadeIn { from { opacity: 0; transform: scale(0.9); } to { opacity: 1; transform: scale(1); } }
/* Pitch Ladder */
#hud-pitch-ladder {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 200px; height: 150px; pointer-events: none;
}
</style>
</head>
<body>
<div id="app">
<canvas id="game-canvas"></canvas>
<div id="menu-overlay">
<div id="menu-panel">
<h1>🚀 APEX AERO</h1>
<p class="subtitle">Flight Simulator</p>
<div id="aircraft-select">
<h2>Select Aircraft</h2>
<div class="aircraft-cards" id="aircraft-cards"></div>
</div>
<div id="scenario-select">
<h2>Select Scenario</h2>
<div class="scenario-cards" id="scenario-cards"></div>
</div>
<button id="start-btn" class="btn-primary">LAUNCH SIMULATION</button>
</div>
</div>
<div id="hud">
<div id="hud-top">
<div id="hud-aircraft-name"></div>
<div id="hud-scenario-name"></div>
</div>
<div id="hud-left">
<div class="hud-item"><span class="hud-label">ALT</span><span class="hud-value" id="hud-alt"></span><span class="hud-unit">ft</span></div>
<div class="hud-item"><span class="hud-label">SPD</span><span class="hud-value" id="hud-spd"></span><span class="hud-unit">kts</span></div>
<div class="hud-item"><span class="hud-label">VS</span><span class="hud-value" id="hud-vs"></span><span class="hud-unit">ft/s</span></div>
<div class="hud-item"><span class="hud-label">G</span><span class="hud-value" id="hud-g"></span><span class="hud-unit"></span></div>
</div>
<div id="hud-right">
<div class="hud-item"><span class="hud-label">THR</span><span class="hud-value" id="hud-throttle"></span><span class="hud-unit">%</span></div>
<div class="hud-item"><span class="hud-label">HDG</span><span class="hud-value" id="hud-hdg"></span><span class="hud-unit">°</span></div>
<div class="hud-item"><span class="hud-label">PITCH</span><span class="hud-value" id="hud-pitch"></span><span class="hud-unit">°</span></div>
<div class="hud-item"><span class="hud-label">ROLL</span><span class="hud-value" id="hud-roll"></span><span class="hud-unit">°</span></div>
</div>
<div id="hud-bottom">
<div id="hud-pitch-ladder"></div>
<div id="hud-status"></div>
<div id="hud-controls-hint">W/S: Pitch | A/D: Roll | Q/E: Yaw | Shift/Ctrl: Throttle | R: Gear</div>
</div>
</div>
<div id="message-overlay"></div>
<div id="crosshair">+</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./types":"src/types.ts","./aircraft":"src/aircraft.ts","./physics":"src/physics.ts","./modeler":"src/modeler.ts","./terrain":"src/terrain.ts","./input":"src/input.ts","./audio":"src/audio.ts","./hud":"src/hud.ts","./scenarios":"src/scenarios.ts","./pitchladder":"src/pitchladder.ts"},"src/aircraft.ts":{"./types":"src/types.ts"},"src/physics.ts":{"./types":"src/types.ts"},"src/modeler.ts":{"./types":"src/types.ts"},"src/terrain.ts":{"./types":"src/types.ts"},"src/hud.ts":{"./types":"src/types.ts"},"src/scenarios.ts":{"./types":"src/types.ts"},"src/pitchladder.ts":{"./types":"src/types.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 types_1 = require("./types");
const aircraft_1 = require("./aircraft");
const physics_1 = require("./physics");
const modeler_1 = require("./modeler");
const terrain_1 = require("./terrain");
const input_1 = require("./input");
const audio_1 = require("./audio");
const hud_1 = require("./hud");
const scenarios_1 = require("./scenarios");
const pitchladder_1 = require("./pitchladder");
// ─── Game State ─────────────────────────────────────────────
const gameState = {
mode: types_1.InputMode.MENU,
selectedAircraft: 'falcon',
selectedScenario: 'takeoff',
flight: null,
enemies: [],
destroyedCount: 0,
totalStartYaw: 0,
lastTime: 0,
objectives: [],
message: '',
messageTimer: 0,
};
// ─── Three.js Globals ──────────────────────────────────────
let renderer;
let scene;
let camera;
let playerModel = null;
let terrain = null;
let waterPlane = null;
let skyDome = null;
let runway = null;
let clouds = null;
let sunLight;
const input = new input_1.InputManager();
const audio = new audio_1.AudioSystem();
const hud = new hud_1.HUD();
const pitchLadder = new pitchladder_1.PitchLadder();
// Camera chase params
const cameraTarget = { dist: 35, height: 8, pitch: -0.15 };
const cameraSmoothPos = { x: 0, y: 0, z: 0 };
// ─── Menu System ────────────────────────────────────────────
function buildMenu() {
const cardsContainer = document.getElementById('aircraft-cards');
cardsContainer.innerHTML = '';
aircraft_1.AIRCRAFT_KEYS.forEach(key => {
const spec = aircraft_1.AIRCRAFT[key];
const card = document.createElement('div');
card.className = 'ac-card' + (key === gameState.selectedAircraft ? ' selected' : '');
card.innerHTML = `
<h3>${spec.name}</h3>
<p>${spec.description}</p>
<div class="ac-stats">
<span>${Math.round(spec.maxSpeed * 1.94)} kts</span>
<span>Wt: ${spec.weight} kg</span>
<span>Cl: ${spec.liftCoeff}</span>
</div>
`;
card.onclick = () => {
gameState.selectedAircraft = key;
buildMenu();
};
cardsContainer.appendChild(card);
});
const scContainer = document.getElementById('scenario-cards');
scContainer.innerHTML = '';
Object.keys(scenarios_1.SCENARIOS).forEach(key => {
const sc = scenarios_1.SCENARIOS[key];
const card = document.createElement('div');
card.className = 'sc-card' + (key === gameState.selectedScenario ? ' selected' : '');
card.innerHTML = `
<h3>${sc.icon} ${sc.name}</h3>
<p>${sc.description}</p>
`;
card.onclick = () => {
gameState.selectedScenario = key;
buildMenu();
};
scContainer.appendChild(card);
});
}
function startSimulation() {
const spec = aircraft_1.AIRCRAFT[gameState.selectedAircraft];
const scenario = scenarios_1.SCENARIOS[gameState.selectedScenario];
// Initialize flight state
gameState.flight = {
pos: { ...scenario.spawnPos },
vel: { ...scenario.spawnVel },
pitch: scenario.spawnOrientation.pitch,
roll: scenario.spawnOrientation.roll,
yaw: scenario.spawnOrientation.yaw,
pitchRate: 0,
rollRate: 0,
yawRate: 0,
throttle: 0,
rpm: 800,
gearDown: scenario.id === 'takeoff',
airspeed: (0, physics_1.vec3Len)(scenario.spawnVel),
angleOfAttack: 0,
altitude: scenario.spawnPos.y,
gForce: 1,
stall: false,
groundContact: scenario.id === 'takeoff',
};
gameState.totalStartYaw = scenario.spawnOrientation.yaw;
gameState.destroyedCount = 0;
gameState.objectives = scenario.objectives.map(o => ({ ...o }));
gameState.message = 'Simulation started';
gameState.messageTimer = 3;
// Spawn enemies for dogfight
if (scenario.id === 'dogfight') {
gameState.enemies = (0, scenarios_1.spawnEnemies)(3, gameState.flight.pos);
gameState.enemies.forEach(e => scene.add(e.mesh));
}
else {
gameState.enemies = [];
}
// Update UI
document.getElementById('menu-overlay').classList.add('hidden');
document.getElementById('hud').classList.add('active');
gameState.mode = types_1.InputMode.PLAYING;
audio.init();
hud.show();
pitchLadder.mount();
pitchLadder.show();
showStatus('Simulation started. Use W/S/A/D to fly, Shift/Ctrl for throttle, R for gear.');
}
function showStatus(msg) {
gameState.message = msg;
gameState.messageTimer = 4;
}
// ─── Scene Setup ────────────────────────────────────────────
function initScene() {
// Renderer
renderer = new THREE.WebGLRenderer({
canvas: document.getElementById('game-canvas'),
antialias: true,
});
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setClearColor(0x87ceeb);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
// Scene
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x88bbee, 0.0008);
// Camera
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 1, 50000);
camera.position.set(0, 100, 50);
// ── Lighting ──
const ambientLight = new THREE.AmbientLight(0x446688, 0.5);
scene.add(ambientLight);
const hemiLight = new THREE.HemisphereLight(0x88bbff, 0x445522, 0.6);
scene.add(hemiLight);
sunLight = new THREE.DirectionalLight(0xffeedd, 1.2);
sunLight.position.set(2000, 3000, 1000);
sunLight.castShadow = true;
// Shadow map config
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 10;
sunLight.shadow.camera.far = 10000;
sunLight.shadow.camera.left = -2000;
sunLight.shadow.camera.right = 2000;
sunLight.shadow.camera.top = 2000;
sunLight.shadow.camera.bottom = -2000;
scene.add(sunLight);
// ── Terrain ──
terrain = (0, terrain_1.generateTerrain)(8000, 256, 600, 42);
scene.add(terrain.mesh);
// ── Water ──
waterPlane = (0, terrain_1.createWaterPlane)(8000);
scene.add(waterPlane);
// ── Sky dome ──
skyDome = (0, terrain_1.createSkyDome)(25000);
scene.add(skyDome);
// ── Clouds ──
clouds = (0, terrain_1.createClouds)(8000, 200);
scene.add(clouds);
// ── Runway ──
runway = (0, terrain_1.createRunway)(200, 40, { x: 0, y: 0, z: 0 }, 0);
scene.add(runway);
// ── Handle resize ──
window.addEventListener('resize', onResize);
// ── Start button ──
document.getElementById('start-btn').addEventListener('click', startSimulation);
buildMenu();
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// ─── Update Player Model ────────────────────────────────────
function updatePlayerModel(state, spec) {
if (!playerModel) {
playerModel = (0, modeler_1.buildAircraftModel)(spec);
scene.add(playerModel);
}
// Position
playerModel.position.set(state.pos.x, state.pos.y, state.pos.z);
// Orientation (Three.js uses different convention)
// Our pitch: positive = nose up, roll: positive = right wing down, yaw: compass heading
// Three.js: rotation.x = pitch (but inverted), rotation.y = yaw (inverted), rotation.z = roll (inverted)
playerModel.rotation.x = -state.pitch;
playerModel.rotation.y = -state.yaw;
playerModel.rotation.z = -state.roll;
// Gear visibility
const gearGroup = playerModel.getObjectByName('gear');
if (gearGroup) {
gearGroup.visible = state.gearDown;
}
// Exhaust glow based on throttle
const exhaust = playerModel.getObjectByName('exhaust');
if (exhaust && exhaust.material) {
const mat = exhaust.material;
mat.opacity = state.throttle * 0.7;
const hue = 0xff6f00 + Math.floor(state.throttle * 0x880000);
mat.color.set(hue);
}
}
// ─── Camera Chase ───────────────────────────────────────────
function updateCamera(state, dt) {
const { fw, rt, up } = (0, physics_1.orientationVectors)(state.pitch, state.yaw, state.roll);
// Camera position: behind and slightly above aircraft
const camDesiredPos = {
x: state.pos.x - fw.x * cameraTarget.dist + up.x * cameraTarget.height,
y: state.pos.y - fw.y * cameraTarget.dist + up.y * cameraTarget.height,
z: state.pos.z - fw.z * cameraTarget.dist + up.z * cameraTarget.height,
};
// Smooth camera follow
const smoothFactor = 1 - Math.exp(-dt * 3);
cameraSmoothPos.x += (camDesiredPos.x - cameraSmoothPos.x) * smoothFactor;
cameraSmoothPos.y += (camDesiredPos.y - cameraSmoothPos.y) * smoothFactor;
cameraSmoothPos.z += (camDesiredPos.z - cameraSmoothPos.z) * smoothFactor;
camera.position.set(cameraSmoothPos.x, cameraSmoothPos.y, cameraSmoothPos.z);
// Look at a point slightly ahead of the aircraft
const lookTarget = {
x: state.pos.x + fw.x * 20,
y: state.pos.y + fw.y * 20 + up.y * 2,
z: state.pos.z + fw.z * 20 + up.z * 2,
};
camera.lookAt(new THREE.Vector3(lookTarget.x, lookTarget.y, lookTarget.z));
// Update sky dome to follow camera
if (skyDome) {
skyDome.position.set(state.pos.x, state.pos.y, state.pos.z);
}
// Update sun shadow target to follow aircraft
sunLight.position.set(state.pos.x + 2000, state.pos.y + 3000, state.pos.z + 1000);
sunLight.target.position.set(state.pos.x, state.pos.y, state.pos.z);
}
// ─── Update Enemies ─────────────────────────────────────────
function updateEnemies(dt, time) {
gameState.enemies.forEach(enemy => {
if (!enemy.alive)
return;
(0, scenarios_1.updateEnemyAI)(enemy, dt, gameState.flight.pos, time);
// Update mesh transform
enemy.mesh.position.set(enemy.pos.x, enemy.pos.y, enemy.pos.z);
enemy.mesh.rotation.x = -enemy.pitch;
enemy.mesh.rotation.y = -enemy.yaw;
enemy.mesh.rotation.z = -enemy.roll;
// Update health bar
const healthBar = enemy.mesh.getObjectByName('healthBar');
if (healthBar) {
const healthRatio = enemy.health / enemy.maxHealth;
healthBar.scale.x = Math.max(0.01, healthRatio);
healthBar.material.color.set(healthRatio > 0.5 ? 0x00ff00 : healthRatio > 0.25 ? 0xffaa00 : 0xff0000);
}
// Check if player is close enough for "shooting"
const dx = gameState.flight.pos.x - enemy.pos.x;
const dy = gameState.flight.pos.y - enemy.pos.y;
const dz = gameState.flight.pos.z - enemy.pos.z;
const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
if (dist < 80) {
// Player is "in range" - auto-fire lasers
enemy.health -= dt * 25;
if (enemy.health <= 0) {
enemy.alive = false;
enemy.mesh.visible = false;
gameState.destroyedCount++;
showStatus(`Enemy aircraft destroyed! (${gameState.destroyedCount})`);
}
}
});
}
// ─── Objectives Check ───────────────────────────────────────
function checkObjectives(state) {
gameState.objectives = (0, scenarios_1.updateObjectives)(state, gameState.objectives, gameState.totalStartYaw);
// Check if all complete
const allDone = gameState.objectives.every(o => o.completed);
if (allDone) {
const msgEl = document.getElementById('message-overlay');
if (!msgEl.querySelector('.msg-box')) {
msgEl.innerHTML = `<div class="msg-box">
<div>🎉 ALL OBJECTIVES COMPLETE</div>
<div style="font-size:0.6em;margin-top:10px;color:#90a4ae">Press ESC to return to menu</div>
</div>`;
}
}
}
// ─── Render HUD Objectives ──────────────────────────────────
function updateObjectiveDisplay() {
const statusEl = document.getElementById('hud-status');
if (!statusEl)
return;
const completed = gameState.objectives.filter(o => o.completed).length;
const total = gameState.objectives.length;
const activeObj = gameState.objectives.find(o => !o.completed);
if (activeObj) {
statusEl.textContent = `[${completed}/${total}] ${activeObj.text}`;
}
else {
statusEl.textContent = gameState.message;
}
}
// ─── Return to Menu ─────────────────────────────────────────
function returnToMenu() {
gameState.mode = types_1.InputMode.MENU;
// Remove player model
if (playerModel) {
scene.remove(playerModel);
playerModel = null;
}
// Remove enemies
gameState.enemies.forEach(e => {
if (e.mesh)
scene.remove(e.mesh);
});
gameState.enemies = [];
// Hide HUD
document.getElementById('hud').classList.remove('active');
pitchLadder.hide();
document.getElementById('menu-overlay').classList.remove('hidden');
document.getElementById('message-overlay').innerHTML = '';
buildMenu();
}
// ─── Main Game Loop ─────────────────────────────────────────
function gameLoop(timestamp) {
requestAnimationFrame(gameLoop);
const dt = Math.min((timestamp - gameState.lastTime) / 1000, 0.05);
gameState.lastTime = timestamp;
if (gameState.mode === types_1.InputMode.PLAYING && gameState.flight) {
const spec = aircraft_1.AIRCRAFT[gameState.selectedAircraft];
const flight = gameState.flight;
// Get input
const inputState = input.getState();
// Get terrain height at aircraft position
const terrainAlt = terrain ? terrain.getHeightAt(flight.pos.x, flight.pos.z) : 0;
// Physics update
const newFlight = (0, physics_1.physicsStep)(flight, spec, dt, terrainAlt, inputState);
Object.assign(flight, newFlight);
// Check for ESC
if (inputState.escPressed) {
returnToMenu();
return;
}
// Update player model
updatePlayerModel(flight, spec);
// Update camera
updateCamera(flight, dt);
// Update enemies
updateEnemies(dt, timestamp / 1000);
// Update audio
audio.update(flight.throttle, flight.airspeed, flight.stall, flight.altitude);
// Update HUD
const scenarioName = scenarios_1.SCENARIOS[gameState.selectedScenario].name;
let hudMsg = gameState.message;
if (gameState.messageTimer > 0) {
gameState.messageTimer -= dt;
}
else {
const activeObj = gameState.objectives.find(o => !o.completed);
hudMsg = activeObj ? activeObj.text : '';
}
if (flight.stall)
hudMsg = '⚠️ STALL WARNING';
hud.update(flight, spec.name, scenarioName, hudMsg);
pitchLadder.render(flight);
updateObjectiveDisplay();
// Check objectives
checkObjectives(flight);
// Animate water
if (waterPlane) {
waterPlane.position.y = 1.5 + Math.sin(timestamp * 0.001) * 0.3;
}
// Animate clouds
if (clouds) {
const pos = clouds.geometry.attributes.position.array;
for (let i = 0; i < pos.length; i += 3) {
pos[i] += dt * 3; // drift east
if (pos[i] > 4000)
pos[i] = -4000;
}
clouds.geometry.attributes.position.needsUpdate = true;
}
}
// Render
renderer.render(scene, camera);
}
// ─── Initialize ─────────────────────────────────────────────
function main() {
try {
hud.init();
initScene();
gameState.lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
catch (e) {
window.__INIT_ERROR__ = String(e);
throw e;
}
}
// Auto-run
main();
exports.default = main;
};
// ── types: globals.d.ts ──
__mods["globals.d.ts"] = function (exports) {};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ─── Core Types ─────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputMode = void 0;
var InputMode;
(function (InputMode) {
InputMode[InputMode["MENU"] = 0] = "MENU";
InputMode[InputMode["PLAYING"] = 1] = "PLAYING";
InputMode[InputMode["PAUSED"] = 2] = "PAUSED";
})(InputMode || (exports.InputMode = InputMode = {}));
};
// ── module: src/aircraft.ts ──
__mods["src/aircraft.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.AIRCRAFT_KEYS = exports.AIRCRAFT = void 0;
exports.AIRCRAFT = {
falcon: {
name: 'Falcon X-1',
description: 'Light interceptor. Fast, agile, but fragile. Excellent turn rate.',
maxSpeed: 180,
thrust: 28000,
liftCoeff: 1.4,
dragCoeff: 0.02,
wingArea: 15,
weight: 6500,
momentOfInertia: 1.0,
turnRate: 120,
pitchRate: 60,
yawRate: 30,
stallAngle: 0.25,
fuselageColor: 0x90a4ae,
wingColor: 0x78909c,
accentColor: 0xf44336,
bodyLength: 12,
wingSpan: 8,
tailSpan: 5,
bodyDiameter: 1.2,
},
titan: {
name: 'Titan Heavy',
description: 'Heavy fighter-bomber. Slow but powerful. Can sustain high-G turns.',
maxSpeed: 140,
thrust: 65000,
liftCoeff: 1.2,
dragCoeff: 0.035,
wingArea: 40,
weight: 22000,
momentOfInertia: 2.5,
turnRate: 70,
pitchRate: 35,
yawRate: 20,
stallAngle: 0.3,
fuselageColor: 0x5d4037,
wingColor: 0x4e342e,
accentColor: 0xffa000,
bodyLength: 20,
wingSpan: 16,
tailSpan: 8,
bodyDiameter: 2.5,
},
phantom: {
name: 'Phantom Stealth',
description: 'Balanced multi-role. Good all-around performance with stealth profile.',
maxSpeed: 160,
thrust: 40000,
liftCoeff: 1.3,
dragCoeff: 0.025,
wingArea: 25,
weight: 12000,
momentOfInertia: 1.5,
turnRate: 90,
pitchRate: 50,
yawRate: 25,
stallAngle: 0.28,
fuselageColor: 0x37474f,
wingColor: 0x263238,
accentColor: 0x76ff03,
bodyLength: 16,
wingSpan: 12,
tailSpan: 6,
bodyDiameter: 1.8,
},
};
exports.AIRCRAFT_KEYS = Object.keys(exports.AIRCRAFT);
};
// ── module: src/physics.ts ──
__mods["src/physics.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.DEG2RAD = void 0;
exports.physicsStep = physicsStep;
exports.vec3 = vec3;
exports.vec3Add = vec3Add;
exports.vec3Scale = vec3Scale;
exports.vec3Dot = vec3Dot;
exports.vec3Len = vec3Len;
exports.vec3Norm = vec3Norm;
exports.vec3Cross = vec3Cross;
exports.airDensityAtAlt = airDensityAtAlt;
exports.orientationVectors = orientationVectors;
// ─── Constants ───────────────────────────────────────────────
const SEA_LEVEL_PRESSURE = 101325; // Pa
const SEA_LEVEL_TEMP = 288.15; // K
const GRAVITY = 9.81; // m/s²
const SEA_LEVEL_DENSITY = 1.225; // kg/m³
const LAPS_RATE = 0.0065; // K/m (troposphere)
const GAS_CONST = 287.05; // J/(kg·K)
const DEG2RAD = Math.PI / 180;
exports.DEG2RAD = DEG2RAD;
// ─── Helper Functions ────────────────────────────────────────
function vec3(x = 0, y = 0, z = 0) { return { x, y, z }; }
function vec3Add(a, b) { return { x: a.x + b.x, y: a.y + b.y, z: a.z + b.z }; }
function vec3Scale(a, s) { return { x: a.x * s, y: a.y * s, z: a.z * s }; }
function vec3Dot(a, b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
function vec3Len(a) { return Math.sqrt(a.x * a.x + a.y * a.y + a.z * a.z); }
function vec3Norm(a) {
const l = vec3Len(a);
return l > 1e-8 ? { x: a.x / l, y: a.y / l, z: a.z / l } : vec3(0, 0, 0);
}
function vec3Cross(a, b) {
return {
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,
};
}
// Air density at altitude (ISA model, troposphere)
function airDensityAtAlt(altM) {
if (altM < 0)
altM = 0;
const temp = Math.max(SEA_LEVEL_TEMP - LAPS_RATE * altM, 200);
const ratio = temp / SEA_LEVEL_TEMP;
return SEA_LEVEL_DENSITY * Math.pow(ratio, 4.255877);
}
// Convert euler angles to forward/right/up unit vectors
function orientationVectors(pitch, yaw, roll) {
const cosP = Math.cos(pitch), sinP = Math.sin(pitch);
const cosY = Math.cos(yaw), sinY = Math.sin(yaw);
const cosR = Math.cos(roll), sinR = Math.sin(roll);
// Forward direction in aircraft frame, transformed to world
// Aircraft forward: (0, 0, -1) in local frame
// Apply rotations: yaw (Y), pitch (X), roll (Z)
const forward = vec3(-sinY * cosP + cosY * sinP * sinR, cosY * cosP * sinR + sinY * sinP, cosY * cosP * cosR - sinY * sinP * sinR);
// But simpler approach: standard aerospace sequence
// yaw around Y, pitch around local X, roll around local Z
// Let's use proper rotation order: Yaw → Pitch → Roll
const fw = vec3(cosP * cosY, -sinP, cosP * sinY);
// Right vector (positive roll = right wing down)
const rt = vec3(-cosR * sinY + sinR * sinP * cosY, sinR * cosP, -cosR * cosY - sinR * sinP * sinY);
// Up vector
const up = vec3Cross(fw, rt);
return { fw: vec3Norm(fw), rt: vec3Norm(rt), up: vec3Norm(up) };
}
// ─── Physics Step ────────────────────────────────────────────
function physicsStep(state, spec, dt, terrainAlt, input) {
const { pos, vel, pitch, roll, yaw, throttle, gearDown } = state;
// ── Air density ──
const altAboveSea = pos.y;
const rho = airDensityAtAlt(altAboveSea);
const dynamicPressure = 0.5 * rho * vel.z * vel.z;
// ── True airspeed ──
const speed = vec3Len(vel);
const speedClamped = Math.max(speed, 0.1);
// ── Angle of Attack ──
// AoA is angle between velocity vector and aircraft longitudinal plane
const { fw, rt, up } = orientationVectors(pitch, yaw, roll);
const velNorm = vec3Norm(vel);
const velInFwdPlane = vec3Dot(velNorm, fw);
const velInUpDir = vec3Dot(velNorm, up);
let aoa = Math.atan2(-velInUpDir, velInFwdPlane);
aoa = Math.max(-0.6, Math.min(0.6, aoa)); // clamp
// ── Sideslip angle ──
const velInRight = vec3Dot(velNorm, rt);
const beta = Math.asin(Math.max(-1, Math.min(1, velInRight)));
// ── Lift ──
// L = 0.5 * rho * V² * S * Cl
// Cl depends on AoA: linear up to stall, then drops
let cl;
const aoaRad = aoa;
if (Math.abs(aoaRad) >= spec.stallAngle) {
// Stall: lift drops dramatically
const stallExcess = Math.abs(aoaRad) - spec.stallAngle;
cl = spec.liftCoeff * (1.0 - stallExcess * 3.0);
cl = Math.max(-0.5, Math.min(spec.liftCoeff, cl));
}
else {
// Linear region with slight nonlinearity
const aoaRatio = aoaRad / spec.stallAngle;
cl = spec.liftCoeff * aoaRatio * (1.0 + 0.5 * Math.abs(aoaRatio));
}
// Also add lift from roll (banked flight: vertical lift component reduces)
const liftMagnitude = 0.5 * rho * speedClamped * speedClamped * spec.wingArea * cl;
const liftDir = vec3Cross(fw, rt); // up in aircraft frame
// ── Drag ──
// Total drag = profile drag + induced drag
// Cd = Cd0 + Cl² / (π * e * AR)
const aspectRatio = spec.wingSpan * spec.wingSpan / spec.wingArea;
const efficiency = 0.8; // Oswald efficiency
const cdInduced = (cl * cl) / (Math.PI * efficiency * aspectRatio);
const cdTotal = spec.dragCoeff + cdInduced;
// Speed-dependent profile drag increases
const speedRatio = speed / spec.maxSpeed;
const cdAdjusted = cdTotal * (1.0 + speedRatio * speedRatio * 0.5);
const dragMagnitude = 0.5 * rho * speedClamped * speedClamped * spec.wingArea * cdAdjusted;
// Ground effect: reduces induced drag near ground
const heightAboveTerrain = pos.y - terrainAlt;
const groundEffectFactor = heightAboveTerrain < spec.wingSpan
? 1.0 - (1.0 - heightAboveTerrain / spec.wingSpan) * 0.3
: 1.0;
// ── Thrust ──
// Throttle ramps up/down gradually
const throttleRate = 0.5; // per second
let targetThrottle = throttle + input.throttleCmd * dt * throttleRate;
targetThrottle = Math.max(0, Math.min(1, targetThrottle));
// Throttle decay when no input
if (Math.abs(input.throttleCmd) < 0.01) {
// Keep current throttle
}
const currentThrottle = targetThrottle;
const thrustForce = spec.thrust * currentThrottle;
// ── Build Force Vectors ──
const gravityForce = vec3(0, -spec.weight * GRAVITY, 0);
// Lift acts perpendicular to velocity in the aircraft's lift plane
const liftVector = vec3Scale(liftDir, liftMagnitude);
// Drag acts opposite to velocity
const dragDir = speed > 0.01 ? vec3Scale(vec3Norm(vel), -1) : vec3(0, 0, 0);
const dragVector = vec3Scale(dragDir, dragMagnitude * groundEffectFactor);
// Thrust acts along aircraft forward axis
const thrustVector = vec3Scale(fw, thrustForce);
// Total force
const totalForce = vec3Add(vec3Add(vec3Add(thrustVector, liftVector), dragVector), gravityForce);
// ── Acceleration (F = ma) ──
const accel = vec3Scale(totalForce, 1.0 / spec.weight);
// ── Integrate velocity & position ──
const newVel = vec3Add(vel, vec3Scale(accel, dt));
const newPos = vec3Add(pos, vec3Scale(newVel, dt));
// ── Angular Motion ──
// Angular rates based on input, scaled by aircraft response
const pitchTarget = -input.pitch * spec.pitchRate * DEG2RAD; // positive pitch input → nose up (negative pitch angle change)
const rollTarget = input.roll * spec.turnRate * DEG2RAD;
const yawTarget = input.yaw * spec.yawRate * DEG2RAD;
// Smooth angular rate transitions (inertia)
const rateResponseTime = spec.momentOfInertia * 0.5;
const rateDt = dt / rateResponseTime;
const rateSmooth = 1.0 - Math.exp(-rateDt);
const newPitchRate = state.pitchRate + (pitchTarget - state.pitchRate) * rateSmooth;
const newRollRate = state.rollRate + (rollTarget - state.rollRate) * rateSmooth;
const newYawRate = state.yawRate + (yawTarget - state.yawRate) * rateSmooth;
// Update orientation
let newPitch = pitch + newPitchRate * dt;
let newRoll = roll + newRollRate * dt;
let newYaw = yaw + newYawRate * dt;
// Clamp pitch to avoid flip
newPitch = Math.max(-Math.PI * 0.45, Math.min(Math.PI * 0.45, newPitch));
// Auto-dampen extreme roll
if (Math.abs(newRoll) > Math.PI * 0.45) {
newRoll *= 0.98;
}
// ── Stall effects ──
const isStalling = Math.abs(aoa) >= spec.stallAngle && speed > 1;
if (isStalling) {
// Add random pitch perturbation on stall
newPitch += (Math.random() - 0.5) * dt * 2.0;
}
// ── G-force ──
const gForce = vec3Len(vec3Add(accel, vec3(0, GRAVITY, 0))) / GRAVITY;
// ── Ground collision ──
const terrainHeight = terrainAlt;
let groundContact = false;
const aircraftRadius = spec.bodyDiameter * 0.5;
if (newPos.y < terrainHeight + aircraftRadius) {
// Hard ground collision
const impactSpeed = Math.abs(newVel.y);
if (gearDown && impactSpeed < 8) {
// Safe landing / ground roll
newPos.y = terrainHeight + aircraftRadius;
newVel.y = Math.min(newVel.y, 0);
// Ground friction
const friction = 0.9;
newVel.x *= friction;
newVel.z *= friction;
groundContact = true;
}
else if (impactSpeed > 15) {
// Crash - bounce violently
newVel.y = -newVel.y * 0.3;
newPos.y = terrainHeight + aircraftRadius + impactSpeed * 0.5;
newPitch += (Math.random() - 0.5) * 0.5;
newRoll += (Math.random() - 0.5) * 0.5;
}
else {
// Soft collision
newPos.y = terrainHeight + aircraftRadius;
newVel.y = Math.min(newVel.y * 0.5, 0);
groundContact = true;
}
}
// ── Gear ──
let newGearDown = gearDown;
if (input.gearToggle) {
newGearDown = !gearDown;
}
// Auto-retract gear at high speed
if (speed > 50 && !groundContact) {
newGearDown = false;
}
// ── RPM ──
const rpm = currentThrottle * 12000 + 800;
// ── Build new state ──
return {
pos: newPos,
vel: newVel,
pitch: newPitch,
roll: newRoll,
yaw: newYaw,
pitchRate: newPitchRate,
rollRate: newRollRate,
yawRate: newYawRate,
throttle: currentThrottle,
rpm,
gearDown: newGearDown,
airspeed: speed,
angleOfAttack: aoa,
altitude: pos.y - terrainHeight,
gForce,
stall: isStalling,
groundContact,
};
}
};
// ── module: src/modeler.ts ──
__mods["src/modeler.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.buildAircraftModel = buildAircraftModel;
// Build a 3D aircraft model from spec parameters
function buildAircraftModel(spec) {
const group = new THREE.Group();
const fuselageMat = new THREE.MeshStandardMaterial({
color: spec.fuselageColor,
metalness: 0.6,
roughness: 0.3,
});
const wingMat = new THREE.MeshStandardMaterial({
color: spec.wingColor,
metalness: 0.5,
roughness: 0.4,
});
const accentMat = new THREE.MeshStandardMaterial({
color: spec.accentColor,
metalness: 0.3,
roughness: 0.5,
});
const glassMat = new THREE.MeshStandardMaterial({
color: 0x80deea,
metalness: 0.1,
roughness: 0.1,
transparent: true,
opacity: 0.6,
});
const darkMat = new THREE.MeshStandardMaterial({
color: 0x263238,
metalness: 0.8,
roughness: 0.2,
});
const { bodyLength, wingSpan, tailSpan, bodyDiameter } = spec;
const bl = bodyLength;
const ws = wingSpan;
const ts = tailSpan;
const bd = bodyDiameter;
// ── Fuselage (cylinder along local X axis) ──
const fuselageGeo = new THREE.PlaneGeometry(bd * 1.1, bl, 8, 32);
// We'll use a lathe approach: build fuselage from profile
// Actually, simpler: just use cylinder scaled
const fuselage = new THREE.Mesh(new THREE.CylinderGeometry(bd * 0.45, bd * 0.25, bl, 12, 1), fuselageMat);
fuselage.rotation.z = Math.PI / 2;
fuselage.position.set(0, 0, 0);
group.add(fuselage);
// Nose cone (pointed)
const nose = new THREE.Mesh(new THREE.ConeGeometry(bd * 0.45, bl * 0.35, 12), fuselageMat);
nose.rotation.z = -Math.PI / 2;
nose.position.set(bl * 0.5 + bl * 0.15, 0, 0);
group.add(nose);
// Cockpit canopy
const canopy = new THREE.Mesh(new THREE.SphereGeometry(bd * 0.5, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2), glassMat);
canopy.position.set(bl * 0.15, bd * 0.4, 0);
canopy.scale.set(1.2, 0.7, 1.0);
group.add(canopy);
// ── Wings ──
// Main wing: swept back
const wingHalfSpan = ws / 2;
const wingChord = ws * 0.18;
const wingThickness = bd * 0.12;
// Left wing
const leftWing = new THREE.Mesh(new THREE.BoxGeometry(wingChord, wingThickness, wingHalfSpan), wingMat);
leftWing.position.set(-bl * 0.05, 0, -wingHalfSpan / 2 - 0.3);
// Sweep
leftWing.rotation.y = -0.2;
group.add(leftWing);
// Right wing
const rightWing = new THREE.Mesh(new THREE.BoxGeometry(wingChord, wingThickness, wingHalfSpan), wingMat);
rightWing.position.set(-bl * 0.05, 0, wingHalfSpan / 2 + 0.3);
rightWing.rotation.y = -0.2;
group.add(rightWing);
// Wing tips (colored accent)
const tipGeo = new THREE.BoxGeometry(wingChord * 0.3, wingThickness * 1.5, 0.3);
const leftTip = new THREE.Mesh(tipGeo, accentMat);
leftTip.position.set(-bl * 0.05 - wingChord * 0.2, 0, -wingHalfSpan - 0.3);
group.add(leftTip);
const rightTip = new THREE.Mesh(tipGeo, accentMat);
rightTip.position.set(-bl * 0.05 - wingChord * 0.2, 0, wingHalfSpan + 0.3);
group.add(rightTip);
// ── Tail ──
// Horizontal stabilizer
const hTail = new THREE.Mesh(new THREE.BoxGeometry(wingChord * 0.6, wingThickness * 0.7, ts), wingMat);
hTail.position.set(-bl * 0.4, bd * 0.2, 0);
group.add(hTail);
// Vertical stabilizer
const vTailHeight = bl * 0.25;
const vTail = new THREE.Mesh(new THREE.BoxGeometry(wingChord * 0.7, vTailHeight, wingThickness * 1.5), wingMat);
vTail.position.set(-bl * 0.45, vTailHeight / 2 + bd * 0.1, 0);
group.add(vTail);
// V-tail accent stripe
const vStripe = new THREE.Mesh(new THREE.BoxGeometry(0.1, vTailHeight * 0.8, wingThickness * 2), accentMat);
vStripe.position.set(-bl * 0.45, vTailHeight / 2 + bd * 0.1, 0);
group.add(vStripe);
// ── Engine nacelles ──
const nacelleLen = bl * 0.3;
const nacelleRad = bd * 0.3;
for (const side of [-1, 1]) {
const nacelle = new THREE.Mesh(new THREE.CylinderGeometry(nacelleRad, nacelleRad * 0.7, nacelleLen, 8), darkMat);
nacelle.rotation.z = Math.PI / 2;
nacelle.position.set(-bl * 0.15, -bd * 0.15, side * (wingHalfSpan * 0.5 + 0.5));
group.add(nacelle);
// Engine intake
const intake = new THREE.Mesh(new THREE.TorusGeometry(nacelleRad * 0.8, 0.08, 6, 8), darkMat);
intake.position.set(-bl * 0.15 + nacelleLen / 2, -bd * 0.15, side * (wingHalfSpan * 0.5 + 0.5));
intake.rotation.y = Math.PI / 2;
group.add(intake);
}
// ── Landing gear (visible when deployed) ──
const gearGroup = new THREE.Group();
gearGroup.name = 'gear';
const gearMat = new THREE.MeshStandardMaterial({ color: 0x424242, metalness: 0.8, roughness: 0.3 });
// Nose gear
const noseStrut = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, bd * 0.8, 6), gearMat);
noseStrut.position.set(bl * 0.2, -bd * 0.8, 0);
gearGroup.add(noseStrut);
const noseWheel = new THREE.Mesh(new THREE.SphereGeometry(0.15, 8, 8), darkMat);
noseWheel.position.set(bl * 0.2, -bd * 1.2, 0);
gearGroup.add(noseWheel);
// Main gear
for (const side of [-1, 1]) {
const mainStrut = new THREE.Mesh(new THREE.CylinderGeometry(0.08, 0.08, bd * 1.0, 6), gearMat);
mainStrut.position.set(-bl * 0.05, -bd * 1.0, side * wingHalfSpan * 0.3);
gearGroup.add(mainStrut);
const mainWheel = new THREE.Mesh(new THREE.SphereGeometry(0.2, 8, 8), darkMat);
mainWheel.position.set(-bl * 0.05, -bd * 1.5, side * wingHalfSpan * 0.3);
gearGroup.add(mainWheel);
}
gearGroup.visible = false;
group.add(gearGroup);
// ── Exhaust glow ──
const exhaustMat = new THREE.MeshBasicMaterial({
color: 0xff6f00,
transparent: true,
opacity: 0.4,
});
const exhaust = new THREE.Mesh(new THREE.ConeGeometry(nacelleRad * 0.6, 1.5, 8), exhaustMat);
exhaust.rotation.z = Math.PI / 2;
exhaust.name = 'exhaust';
// We'll place it on one nacelle (visual only)
exhaust.position.set(-bl * 0.15 - nacelleLen / 2 - 0.3, -bd * 0.15, 0);
group.add(exhaust);
// ── Navigation lights ──
const redLight = new THREE.Mesh(new THREE.SphereGeometry(0.1, 6, 6), new THREE.MeshBasicMaterial({ color: 0xff0000 }));
redLight.position.set(-bl * 0.1, 0.2, -wingHalfSpan - 0.3);
group.add(redLight);
const greenLight = new THREE.Mesh(new THREE.SphereGeometry(0.1, 6, 6), new THREE.MeshBasicMaterial({ color: 0x00ff00 }));
greenLight.position.set(-bl * 0.1, 0.2, wingHalfSpan + 0.3);
group.add(greenLight);
// ── Scale model to scene-appropriate size ──
// Models are built at ~1:1 scale of the aircraft dimensions
// We keep them as-is; the scene will position them accordingly
return group;
}
};
// ── module: src/terrain.ts ──
__mods["src/terrain.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateTerrain = generateTerrain;
exports.createWaterPlane = createWaterPlane;
exports.createSkyDome = createSkyDome;
exports.createRunway = createRunway;
exports.createClouds = createClouds;
// ─── Simplex-like noise for terrain ─────────────────────────
// Fast hash-based pseudo-random
function hash(x, y) {
let h = x * 374761393 + y * 668265263;
h = (h ^ (h >> 13)) * 1274126177;
return (h ^ (h >> 16)) / 4294967296.0;
}
function smoothNoise(x, y) {
const ix = Math.floor(x);
const iy = Math.floor(y);
const fx = x - ix;
const fy = y - iy;
const sx = fx * fx * (3 - 2 * fx);
const sy = fy * fy * (3 - 2 * fy);
const n00 = hash(ix, iy);
const n10 = hash(ix + 1, iy);
const n01 = hash(ix, iy + 1);
const n11 = hash(ix + 1, iy + 1);
const nx0 = n00 + (n10 - n00) * sx;
const nx1 = n01 + (n11 - n01) * sx;
return nx0 + (nx1 - nx0) * sy;
}
function fbm(x, y, octaves) {
let value = 0;
let amplitude = 1;
let frequency = 1;
let maxValue = 0;
for (let i = 0; i < octaves; i++) {
value += amplitude * smoothNoise(x * frequency, y * frequency);
maxValue += amplitude;
amplitude *= 0.5;
frequency *= 2.0;
}
return value / maxValue;
}
function generateTerrain(size, resolution, maxHeight, seed) {
const halfSize = size / 2;
const segments = resolution;
const vertsPerRow = segments + 1;
// Build geometry
const geo = new THREE.PlaneGeometry(size, size, segments, segments);
// Rotate to lie flat on XZ plane
geo.rotateX(-Math.PI / 2);
const positions = geo.attributes.position.array;
const colors = new Float32Array(positions.length);
for (let i = 0; i < positions.length; i += 3) {
const x = positions[i];
const z = positions[i + 2];
// Multi-octave noise for realistic terrain
const nx = (x + seed * 100) * 0.002;
const nz = (z + seed * 100) * 0.002;
let height = fbm(nx, nz, 6) * maxHeight;
// Add ridges
const ridge = 1.0 - Math.abs(fbm(nx * 2.5 + 5, nz * 2.5 + 5, 4) * 2 - 1);
height += ridge * ridge * maxHeight * 0.4;
// Flatten near center for runway area
const distFromCenter = Math.sqrt(x * x + z * z);
const runwayRadius = halfSize * 0.15;
if (distFromCenter < runwayRadius) {
const flattenFactor = distFromCenter / runwayRadius;
height *= flattenFactor * flattenFactor;
height = Math.min(height, 2);
}
// Water areas (low height)
if (height < 5) {
height = Math.min(height, 2);
}
positions[i + 1] = height;
// Vertex colors based on height
const h = Math.max(0, height) / maxHeight;
if (height < 3) {
// Water/sand
colors[i] = 0.4;
colors[i + 1] = 0.55;
colors[i + 2] = 0.3;
}
else if (height < maxHeight * 0.3) {
// Grass
colors[i] = 0.25;
colors[i + 1] = 0.45;
colors[i + 2] = 0.15;
}
else if (height < maxHeight * 0.6) {
// Rock
colors[i] = 0.4;
colors[i + 1] = 0.4;
colors[i + 2] = 0.35;
}
else if (height < maxHeight * 0.8) {
// Dark rock
colors[i] = 0.3;
colors[i + 1] = 0.3;
colors[i + 2] = 0.28;
}
else {
// Snow
colors[i] = 0.9;
colors[i + 1] = 0.9;
colors[i + 2] = 0.92;
}
}
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geo.computeVertexNormals();
const mat = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.85,
metalness: 0.05,
flatShading: true,
side: THREE.DoubleSide,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.receiveShadow = true;
// Store height data for collision
const heightData = new Float32Array(vertsPerRow * vertsPerRow);
for (let iz = 0; iz <= segments; iz++) {
for (let ix = 0; ix <= segments; ix++) {
const idx = iz * vertsPerRow + ix;
const worldX = -halfSize + (ix / segments) * size;
const worldZ = -halfSize + (iz / segments) * size;
const localIdx = iz * vertsPerRow + ix;
heightData[localIdx] = positions[localIdx * 3 + 1];
}
}
// Height lookup function
const getHeightAt = (x, z) => {
// Map world coord to grid index
const gridX = ((x + halfSize) / size) * segments;
const gridZ = ((z + halfSize) / size) * segments;
const ix0 = Math.max(0, Math.min(segments - 1, Math.floor(gridX)));
const iz0 = Math.max(0, Math.min(segments - 1, Math.floor(gridZ)));
const ix1 = Math.min(segments, ix0 + 1);
const iz1 = Math.min(segments, iz0 + 1);
const fx = gridX - ix0;
const fz = gridZ - iz0;
const h00 = heightData[iz0 * vertsPerRow + ix0];
const h10 = heightData[iz0 * vertsPerRow + ix1];
const h01 = heightData[iz1 * vertsPerRow + ix0];
const h11 = heightData[iz1 * vertsPerRow + ix1];
const h0 = h00 * (1 - fx) + h10 * fx;
const h1 = h01 * (1 - fx) + h11 * fx;
return h0 * (1 - fz) + h1 * fz;
};
return {
mesh,
getWidth: () => size,
getDepth: () => size,
getHeightAt,
};
}
// ─── Water plane ────────────────────────────────────────────
function createWaterPlane(size) {
const geo = new THREE.PlaneGeometry(size, size);
geo.rotateX(-Math.PI / 2);
const mat = new THREE.MeshPhongMaterial({
color: 0x1a5276,
transparent: true,
opacity: 0.6,
shininess: 100,
specular: 0x87ceeb,
});
const water = new THREE.Mesh(geo, mat);
water.position.y = 1.5; // Slightly above absolute zero
return water;
}
// ─── Sky dome ───────────────────────────────────────────────
function createSkyDome(radius) {
const geo = new THREE.SphereGeometry(radius, 32, 16);
// Gradient material
const mat = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color(0x0044aa) },
bottomColor: { value: new THREE.Color(0x88ccff) },
horizonColor: { value: new THREE.Color(0xddeeff) },
offset: { value: 20 },
exponent: { value: 0.4 },
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPos.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform vec3 horizonColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + offset).y;
float t = max(pow(max(h, 0.0), exponent), 0.0);
vec3 col = mix(horizonColor, topColor, smoothstep(0.2, 1.0, t));
col = mix(bottomColor, col, smoothstep(0.0, 0.3, t));
gl_FragColor = vec4(col, 1.0);
}
`,
side: THREE.BackSide,
depthWrite: false,
});
return new THREE.Mesh(geo, mat);
}
// ─── Runway ─────────────────────────────────────────────────
function createRunway(length, width, pos, heading) {
const group = new THREE.Group();
// Runway surface
const runwayGeo = new THREE.PlaneGeometry(length, width);
runwayGeo.rotateX(-Math.PI / 2);
const runwayMat = new THREE.MeshStandardMaterial({
color: 0x333333,
roughness: 0.9,
metalness: 0.0,
});
const runway = new THREE.Mesh(runwayGeo, runwayMat);
group.add(runway);
// Runway markings (center line dashes)
const dashMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const numDashes = Math.floor(length / 8);
for (let i = 0; i < numDashes; i++) {
const dashGeo = new THREE.PlaneGeometry(3, 0.4);
dashGeo.rotateX(-Math.PI / 2);
const dash = new THREE.Mesh(dashGeo, dashMat);
dash.position.set(-length / 2 + 4 + i * 8, 0.02, 0);
group.add(dash);
}
// Edge lines
for (const side of [-1, 1]) {
const edgeGeo = new THREE.PlaneGeometry(length, 0.3);
edgeGeo.rotateX(-Math.PI / 2);
const edge = new THREE.Mesh(edgeGeo, dashMat);
edge.position.set(0, 0.02, side * (width / 2 - 0.5));
group.add(edge);
}
// Threshold markings
const thresholdCount = 6;
const thresholdSpacing = width / (thresholdCount + 1);
for (const end of [-1, 1]) {
for (let i = 0; i < thresholdCount; i++) {
const stripGeo = new THREE.PlaneGeometry(4, 0.8);
stripGeo.rotateX(-Math.PI / 2);
const strip = new THREE.Mesh(stripGeo, dashMat);
strip.position.set(end * (length / 2 - 5), 0.02, -thresholdSpacing + i * thresholdSpacing);
group.add(strip);
}
}
group.position.set(pos.x, 3, pos.z);
group.rotation.y = heading;
return group;
}
// ─── Cloud particles ────────────────────────────────────────
function createClouds(size, count) {
const positions = new Float32Array(count * 3);
for (let i = 0; i < count; i++) {
positions[i * 3] = (Math.random() - 0.5) * size;
positions[i * 3 + 1] = 80 + Math.random() * 120;
positions[i * 3 + 2] = (Math.random() - 0.5) * size;
}
const geo = new THREE.BufferGeometry();
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
const mat = new THREE.PointsMaterial({
color: 0xffffff,
size: 30,
transparent: true,
opacity: 0.4,
sizeAttenuation: true,
});
return new THREE.Points(geo, mat);
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
// ─── Input Manager ──────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.InputManager = void 0;
class InputManager {
constructor() {
this.keys = new Set();
this._state = {
pitch: 0, roll: 0, yaw: 0, throttleCmd: 0, gearToggle: false, escPressed: false,
};
this._prevGearToggle = false;
window.addEventListener('keydown', (e) => {
this.keys.add(e.code);
if (e.code === 'KeyR')
this._prevGearToggle = false;
if (e.code === 'Escape')
this._state.escPressed = true;
});
window.addEventListener('keyup', (e) => {
this.keys.delete(e.code);
this._state.escPressed = false;
});
}
getState() {
const state = this._state;
// Pitch
state.pitch = 0;
if (this.keys.has('KeyW'))
state.pitch -= 1;
if (this.keys.has('KeyS'))
state.pitch += 1;
// Roll
state.roll = 0;
if (this.keys.has('KeyA'))
state.roll -= 1;
if (this.keys.has('KeyD'))
state.roll += 1;
// Yaw
state.yaw = 0;
if (this.keys.has('KeyQ'))
state.yaw -= 1;
if (this.keys.has('KeyE'))
state.yaw += 1;
// Throttle
state.throttleCmd = 0;
if (this.keys.has('ShiftLeft') || this.keys.has('ShiftRight'))
state.throttleCmd += 1;
if (this.keys.has('ControlLeft') || this.keys.has('ControlRight'))
state.throttleCmd -= 1;
// Gear toggle
if (this.keys.has('KeyR') && !this._prevGearToggle) {
state.gearToggle = true;
}
else {
state.gearToggle = false;
}
if (this.keys.has('KeyR'))
this._prevGearToggle = true;
return state;
}
}
exports.InputManager = InputManager;
};
// ── module: src/audio.ts ──
__mods["src/audio.ts"] = function (exports, require, module) {
"use strict";
// ─── Procedural Audio System ────────────────────────────────
// Generates engine noise, wind, stall warnings, etc. from Web Audio API
Object.defineProperty(exports, "__esModule", { value: true });
exports.AudioSystem = void 0;
class AudioSystem {
constructor() {
this.ctx = null;
this.engineOsc = null;
this.engineGain = null;
this.windNoise = null;
this.windGain = null;
this.stallOsc = null;
this.stallGain = null;
this.masterGain = null;
this.initialized = false;
}
init() {
if (this.initialized)
return;
try {
this.ctx = new AudioContext();
this.masterGain = this.ctx.createGain();
this.masterGain.gain.value = 0.4;
this.masterGain.connect(this.ctx.destination);
// ── Engine sound: layered oscillators for realism ──
// Low-frequency rumble
const engineOsc1 = this.ctx.createOscillator();
engineOsc1.type = 'sawtooth';
engineOsc1.frequency.value = 55;
// Mid harmonic
const engineOsc2 = this.ctx.createOscillator();
engineOsc2.type = 'square';
engineOsc2.frequency.value = 110;
// High whine (turbine)
const engineOsc3 = this.ctx.createOscillator();
engineOsc3.type = 'sine';
engineOsc3.frequency.value = 440;
// Engine gain (volume controlled by throttle)
this.engineGain = this.ctx.createGain();
this.engineGain.gain.value = 0;
// Filter for engine tone
const engineFilter = this.ctx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 800;
engineFilter.Q.value = 2;
engineOsc1.connect(this.engineGain);
engineOsc2.connect(this.engineGain);
engineOsc3.connect(this.engineGain);
this.engineGain.connect(engineFilter);
engineFilter.connect(this.masterGain);
engineOsc1.start();
engineOsc2.start();
engineOsc3.start();
this.engineOsc = engineOsc1; // reference for frequency updates
// ── Wind noise ──
const bufferSize = this.ctx.sampleRate * 2;
const windBuffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate);
const windData = windBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
windData[i] = (Math.random() * 2 - 1) * 0.5;
}
this.windNoise = this.ctx.createBufferSource();
this.windNoise.buffer = windBuffer;
this.windNoise.loop = true;
this.windGain = this.ctx.createGain();
this.windGain.gain.value = 0;
const windFilter = this.ctx.createBiquadFilter();
windFilter.type = 'bandpass';
windFilter.frequency.value = 2000;
windFilter.Q.value = 0.5;
this.windNoise.connect(windFilter);
windFilter.connect(this.windGain);
this.windGain.connect(this.masterGain);
this.windNoise.start();
// ── Stall warning oscillator ──
this.stallOsc = this.ctx.createOscillator();
this.stallOsc.type = 'square';
this.stallOsc.frequency.value = 880;
this.stallGain = this.ctx.createGain();
this.stallGain.gain.value = 0;
this.stallOsc.connect(this.stallGain);
this.stallGain.connect(this.masterGain);
this.stallOsc.start();
this.initialized = true;
}
catch (e) {
console.warn('Audio init failed:', e);
}
}
update(throttle, airspeed, stall, altitude) {
if (!this.initialized || !this.ctx)
return;
// Resume context if suspended
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
// ── Engine sound update ──
if (this.engineOsc && this.engineGain) {
// Frequency scales with throttle + RPM
const baseFreq = 40 + throttle * 120;
this.engineOsc.frequency.setTargetAtTime(baseFreq, this.ctx.currentTime, 0.05);
// Volume scales with throttle
const engineVol = 0.02 + throttle * 0.25;
this.engineGain.gain.setTargetAtTime(engineVol, this.ctx.currentTime, 0.1);
}
// ── Wind noise ──
if (this.windGain) {
// Wind volume based on airspeed
const windVol = Math.min(airspeed / 200, 1) * 0.15;
// Altitude effect: less dense air at high altitude = quieter wind
const altFactor = Math.max(0.2, 1 - altitude / 10000);
this.windGain.gain.setTargetAtTime(windVol * altFactor, this.ctx.currentTime, 0.1);
}
// ── Stall warning ──
if (this.stallGain) {
this.stallGain.gain.setTargetAtTime(stall ? 0.08 : 0, this.ctx.currentTime, 0.05);
}
}
setVolume(v) {
if (this.masterGain) {
this.masterGain.gain.value = v;
}
}
}
exports.AudioSystem = AudioSystem;
};
// ── module: src/hud.ts ──
__mods["src/hud.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.HUD = void 0;
// ─── HUD Updater ────────────────────────────────────────────
class HUD {
constructor() {
this.els = {};
this.active = false;
}
init() {
const ids = ['hud-alt', 'hud-spd', 'hud-vs', 'hud-g', 'hud-throttle',
'hud-hdg', 'hud-pitch', 'hud-roll', 'hud-aircraft-name',
'hud-scenario-name', 'hud-status', 'hud-bottom'];
for (const id of ids) {
this.els[id] = document.getElementById(id);
}
}
show() { this.active = true; }
hide() { this.active = false; }
update(state, aircraftName, scenarioName, message) {
if (!this.active)
return;
const altFt = Math.round(state.altitude * 3.28084);
const spdKts = Math.round(state.airspeed * 1.94384);
const vs = state.vel ? Math.round(state.vel.y * 3.28084) : 0;
const g = state.gForce.toFixed(1);
const thr = Math.round(state.throttle * 100);
const hdg = Math.round(((state.yaw % (2 * Math.PI)) + 2 * Math.PI) % (2 * Math.PI) * 180 / Math.PI) % 360;
const pitchDeg = Math.round(-state.pitch * 180 / Math.PI);
const rollDeg = Math.round(-state.roll * 180 / Math.PI);
const set = (id, val) => {
const el = this.els[id];
if (el)
el.textContent = val;
};
set('hud-alt', altFt.toString());
set('hud-spd', spdKts.toString());
set('hud-vs', vs.toString());
set('hud-g', g);
set('hud-throttle', thr.toString());
set('hud-hdg', hdg.toString());
set('hud-pitch', pitchDeg.toString());
set('hud-roll', rollDeg.toString());
set('hud-aircraft-name', aircraftName);
set('hud-scenario-name', scenarioName);
set('hud-status', message);
// Color warnings
const spdEl = this.els['hud-spd'];
if (spdEl) {
spdEl.style.color = state.stall ? '#f44336' : state.airspeed > 150 ? '#ff9800' : '#4fc3f7';
}
const gEl = this.els['hud-g'];
if (gEl) {
gEl.style.color = state.gForce > 6 ? '#f44336' : state.gForce > 4 ? '#ff9800' : '#4fc3f7';
}
}
}
exports.HUD = HUD;
};
// ── module: src/scenarios.ts ──
__mods["src/scenarios.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SCENARIOS = void 0;
exports.spawnEnemies = spawnEnemies;
exports.updateEnemyAI = updateEnemyAI;
exports.updateObjectives = updateObjectives;
exports.SCENARIOS = {
takeoff: {
id: 'takeoff',
name: 'Precision Takeoff & Landing',
description: 'Master the art of takeoff and landing. Manage airspeed, altitude, and throttle carefully. Watch for stalls on approach.',
icon: '🛫',
spawnPos: { x: 0, y: 5, z: 0 },
spawnOrientation: { pitch: 0, roll: 0, yaw: 0 },
spawnVel: { x: 0, y: 0, z: 0 },
objectives: [
{ text: '🛫 Apply full throttle and take off from runway', type: 'speed', target: 60, completed: false },
{ text: '📈 Climb to 1000 ft altitude', type: 'altitude', target: 305, completed: false },
{ text: '🔄 Complete a 360° turn at altitude', type: 'reach', target: 0, completed: false },
{ text: '🛬 Land back on runway safely (gear down, < 60 kts)', type: 'land', target: 30, completed: false },
],
},
dogfight: {
id: 'dogfight',
name: 'High-Speed Dogfight',
description: 'Engage enemy aircraft in aerial combat. Use speed, altitude, and energy management to gain advantage.',
icon: '⚔️',
spawnPos: { x: 0, y: 300, z: 0 },
spawnOrientation: { pitch: -0.1, roll: 0, yaw: 0 },
spawnVel: { x: 120, y: 0, z: 0 },
objectives: [
{ text: '🎯 Chase and engage enemy aircraft', type: 'destroy', target: 3, completed: false },
{ text: '💨 Maintain speed above 80 kts during maneuvers', type: 'speed', target: 41, completed: false },
{ text: '🔄 Execute a high-G turn (4G+)', type: 'reach', target: 0, completed: false },
{ text: '🏆 Outmaneuver and defeat all hostiles', type: 'destroy', target: 0, completed: false },
],
},
};
function spawnEnemies(count, playerPos) {
const enemies = [];
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const dist = 800 + Math.random() * 400;
const pos = {
x: playerPos.x + Math.cos(angle) * dist,
y: playerPos.y + 100 + Math.random() * 200,
z: playerPos.z + Math.sin(angle) * dist,
};
// Simple enemy model (red hostile)
const group = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xc62828, metalness: 0.5, roughness: 0.4 });
const wingMat = new THREE.MeshStandardMaterial({ color: 0xb71c1c, metalness: 0.5, roughness: 0.4 });
const accentMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
// Body
const body = new THREE.Mesh(new THREE.CylinderGeometry(0.8, 0.5, 10, 8), bodyMat);
body.rotation.z = Math.PI / 2;
group.add(body);
// Wings
for (const side of [-1, 1]) {
const wing = new THREE.Mesh(new THREE.BoxGeometry(3, 0.15, 5), wingMat);
wing.position.set(-0.5, 0, side * 3.5);
group.add(wing);
// Nav light
const light = new THREE.Mesh(new THREE.SphereGeometry(0.15, 6, 6), new THREE.MeshBasicMaterial({ color: side === -1 ? 0xff0000 : 0x00ff00 }));
light.position.set(-0.5, 0.3, side * 6);
group.add(light);
}
// Tail
const vTail = new THREE.Mesh(new THREE.BoxGeometry(2, 3, 0.15), bodyMat);
vTail.position.set(-5, 1.5, 0);
group.add(vTail);
// Health bar (a small bar above the aircraft)
const healthBar = new THREE.Mesh(new THREE.BoxGeometry(6, 0.3, 0.3), new THREE.MeshBasicMaterial({ color: 0x00ff00 }));
healthBar.position.set(0, 3, 0);
healthBar.name = 'healthBar';
group.add(healthBar);
const hHealthBar = new THREE.Mesh(new THREE.BoxGeometry(6, 0.3, 0.3), new THREE.MeshBasicMaterial({ color: 0x333333 }));
hHealthBar.position.set(0, 3, -0.1);
group.add(hHealthBar);
// Laser beam (hidden by default)
const laserMat = new THREE.MeshBasicMaterial({ color: 0xff0000, transparent: true, opacity: 0.8 });
const laser = new THREE.Mesh(new THREE.CylinderGeometry(0.1, 0.1, 20, 4), laserMat);
laser.rotation.z = Math.PI / 2;
laser.name = 'laser';
laser.visible = false;
group.add(laser);
enemies.push({
mesh: group,
pos,
vel: { x: 60, y: 0, z: 0 },
pitch: -0.1,
roll: 0,
yaw: Math.PI + angle,
health: 100,
maxHealth: 100,
alive: true,
speed: 50 + Math.random() * 30,
turnRadius: 200,
lastShot: 0,
state: 'patrol',
patrolCenter: { ...pos },
targetPos: { x: pos.x, y: pos.y, z: pos.z },
shootCooldown: 2 + Math.random() * 2,
});
}
return enemies;
}
function updateEnemyAI(enemy, dt, playerPos, time) {
if (!enemy.alive)
return;
const dx = playerPos.x - enemy.pos.x;
const dz = playerPos.z - enemy.pos.z;
const distToPlayer = Math.sqrt(dx * dx + dz * dz);
const dy = playerPos.y - enemy.pos.y;
// State machine
if (distToPlayer < 600) {
enemy.state = distToPlayer < 200 ? 'attack' : 'chase';
}
else if (distToPlayer > 1200) {
enemy.state = 'patrol';
}
switch (enemy.state) {
case 'patrol': {
// Circle around patrol center
const t = time * 0.3;
const r = enemy.turnRadius;
enemy.targetPos = {
x: enemy.patrolCenter.x + Math.cos(t) * r,
y: enemy.patrolCenter.y + Math.sin(t * 0.5) * 30,
z: enemy.patrolCenter.z + Math.sin(t) * r,
};
break;
}
case 'chase': {
// Steer toward player with lag
enemy.targetPos = {
x: playerPos.x + dx * 0.3,
y: playerPos.y + dy * 0.3,
z: playerPos.z + dz * 0.3,
};
break;
}
case 'attack': {
// Try to get behind player, shoot
enemy.targetPos = {
x: playerPos.x + dx * 0.5,
y: playerPos.y + dy * 0.3 + 20,
z: playerPos.z + dz * 0.5,
};
// Shoot at player
if (time - enemy.lastShot > enemy.shootCooldown) {
enemy.lastShot = time;
// Visual laser
const laser = enemy.mesh.getObjectByName('laser');
if (laser) {
laser.visible = true;
laser.position.set(5, 0, 0);
// Point toward player
const angle = Math.atan2(dz, dx);
laser.parent.rotation.y = angle;
setTimeout(() => { if (laser)
laser.visible = false; }, 150);
}
}
break;
}
}
// Move toward target
const tdx = enemy.targetPos.x - enemy.pos.x;
const tdy = enemy.targetPos.y - enemy.pos.y;
const tdz = enemy.targetPos.z - enemy.pos.z;
const tDist = Math.sqrt(tdx * tdx + tdy * tdy + tdz * tdz);
if (tDist > 1) {
const targetVelX = (tdx / tDist) * enemy.speed;
const targetVelY = (tdy / tDist) * enemy.speed;
const targetVelZ = (tdz / tDist) * enemy.speed;
// Smooth velocity transition
const lerp = 1 - Math.exp(-dt * 0.8);
enemy.vel.x += (targetVelX - enemy.vel.x) * lerp;
enemy.vel.y += (targetVelY - enemy.vel.y) * lerp;
enemy.vel.z += (targetVelZ - enemy.vel.z) * lerp;
// Update orientation to face velocity direction
enemy.yaw = Math.atan2(enemy.vel.z, enemy.vel.x);
enemy.pitch = Math.atan2(-enemy.vel.y, Math.sqrt(enemy.vel.x * enemy.vel.x + enemy.vel.z * enemy.vel.z));
enemy.roll = Math.sin(time * 2) * 0.1; // slight banking
}
// Apply velocity
enemy.pos.x += enemy.vel.x * dt;
enemy.pos.y += enemy.vel.y * dt;
enemy.pos.z += enemy.vel.z * dt;
// Keep above terrain
enemy.pos.y = Math.max(enemy.pos.y, 50);
}
function updateObjectives(state, objectives, prevYaw) {
const completed = objectives.filter(o => o.completed).length;
objectives.forEach((obj, idx) => {
if (obj.completed)
return;
switch (obj.type) {
case 'speed':
if (state.airspeed >= (obj.target || 0))
obj.completed = true;
break;
case 'altitude':
if (state.altitude >= (obj.target || 0))
obj.completed = true;
break;
case 'land':
if (state.groundContact && state.airspeed < (obj.target || 30) && state.gearDown)
obj.completed = true;
break;
case 'reach':
// 360 turn or G check
if (idx === 2 && Math.abs(state.gForce) >= 4)
obj.completed = true;
break;
}
});
// Check all completed
if (completed === objectives.length) {
// All objectives complete
}
return objectives;
}
};
// ── module: src/pitchladder.ts ──
__mods["src/pitchladder.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PitchLadder = void 0;
class PitchLadder {
constructor() {
this.active = false;
this.canvas = document.createElement('canvas');
this.canvas.width = 400;
this.canvas.height = 300;
this.canvas.style.position = 'absolute';
this.canvas.style.top = '50%';
this.canvas.style.left = '50%';
this.canvas.style.transform = 'translate(-50%, -50%)';
this.canvas.style.pointerEvents = 'none';
this.canvas.style.zIndex = '11';
this.ctx = this.canvas.getContext('2d');
}
mount() {
const container = document.getElementById('hud-pitch-ladder');
if (container) {
container.appendChild(this.canvas);
container.style.position = 'absolute';
container.style.top = '50%';
container.style.left = '50%';
container.style.transform = 'translate(-50%, -50%)';
container.style.width = '400px';
container.style.height = '300px';
container.style.pointerEvents = 'none';
}
}
show() { this.active = true; }
hide() { this.active = false; }
render(state) {
if (!this.active)
return;
const ctx = this.ctx;
const w = this.canvas.width;
const h = this.canvas.height;
ctx.clearRect(0, 0, w, h);
// Convert pitch offset to pixels
const pitchOffsetPx = state.pitch * (180 / Math.PI) * (h / 40);
const rollAngle = -state.roll;
ctx.save();
ctx.translate(w / 2, h / 2);
ctx.rotate(rollAngle);
ctx.translate(0, pitchOffsetPx);
// Draw pitch ladder
ctx.strokeStyle = 'rgba(79, 195, 247, 0.5)';
ctx.fillStyle = 'rgba(79, 195, 247, 0.8)';
ctx.lineWidth = 1;
ctx.font = '10px monospace';
ctx.textAlign = 'center';
const pitchMarks = [-20, -15, -10, -5, 0, 5, 10, 15, 20];
const pxPerDegree = h / 40;
for (const deg of pitchMarks) {
const y = -deg * pxPerDegree;
if (y < -h / 2 || y > h / 2)
continue;
if (deg === 0) {
// Horizon line (full width)
ctx.strokeStyle = 'rgba(79, 195, 247, 0.7)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(-w / 2, y);
ctx.lineTo(w / 2, y);
ctx.stroke();
}
else if (deg % 10 === 0) {
// Major marks
ctx.strokeStyle = 'rgba(79, 195, 247, 0.4)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-60, y);
ctx.lineTo(60, y);
ctx.stroke();
ctx.fillText(deg.toString(), -70, y + 4);
ctx.fillText(deg.toString(), 70, y + 4);
}
else {
// Minor marks
ctx.strokeStyle = 'rgba(79, 195, 247, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(-30, y);
ctx.lineTo(30, y);
ctx.stroke();
}
}
// Roll indicator arc
ctx.strokeStyle = 'rgba(79, 195, 247, 0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(0, -pitchOffsetPx, 80, Math.PI, Math.PI * 2);
ctx.stroke();
// Roll pointer at top
ctx.fillStyle = 'rgba(79, 195, 247, 0.8)';
ctx.beginPath();
ctx.moveTo(0, -h / 2 + 10);
ctx.lineTo(-5, -h / 2 + 5);
ctx.lineTo(5, -h / 2 + 5);
ctx.fill();
// Aircraft wings/bolts
ctx.strokeStyle = 'rgba(255, 200, 50, 0.9)';
ctx.lineWidth = 2;
// Left bolt
ctx.beginPath();
ctx.moveTo(-80, 0);
ctx.lineTo(-30, 0);
ctx.lineTo(-20, 8);
ctx.stroke();
// Right bolt
ctx.beginPath();
ctx.moveTo(80, 0);
ctx.lineTo(30, 0);
ctx.lineTo(20, 8);
ctx.stroke();
// Center dot
ctx.fillStyle = 'rgba(255, 200, 50, 0.9)';
ctx.beginPath();
ctx.arc(0, 0, 3, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
exports.PitchLadder = PitchLadder;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.6-27B-MTP-GGUF","provider":"brigid","persona":"typescript","sessionId":"6fafba1b-65c0-4807-bf65-11b0890fc6c5","tokensIn":3733012,"tokensOut":43279,"tokensTotal":3776291,"cost":0,"turns":82,"toolCalls":82,"failedToolCalls":3,"timestamp":"2026-08-02T07:55:54.209Z"} -->