Flight Simulator
inkling · basic

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 29,988 bytes · SHA-256 c1bc935982c7
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apex Aero — Flight Simulator</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
@import url('https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Share+Tech+Mono&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
body { background: #030507; overflow: hidden; font-family: 'Orbitron', sans-serif; color: #e0f7ff; user-select: none; }
/* HUD Layout */
#hud { position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 10; }
.hud-panel { position: absolute; background: rgba(0, 10, 20, 0.55); border: 1px solid rgba(0, 255, 255, 0.15); backdrop-filter: blur(4px); border-radius: 6px; }
.hud-value { font-family: 'Share Tech Mono', monospace; font-size: 1.6rem; color: #00ffaa; text-shadow: 0 0 8px rgba(0, 255, 170, 0.5); letter-spacing: 2px; }
/* Top-Left: Aircraft Info */
#info-panel { top: 20px; left: 20px; padding: 16px 22px; width: 260px; }
#info-panel h2 { font-size: 0.85rem; text-transform: uppercase; letter-spacing: 2px; color: #00ffaa; margin-bottom: 6px; opacity: 0.9; }
#info-panel .spec { font-family: 'Share Tech Mono', monospace; font-size: 0.75rem; color: #88ccff; margin: 2px 0; opacity: 0.8; }
/* Top-Right: Telemetry */
#telemetry { top: 20px; right: 20px; padding: 16px 22px; width: 240px; text-align: right; }
#telemetry div { margin-bottom: 6px; }
#telemetry .label { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 1px; color: #88ccff; opacity: 0.7; display: block; }
#telemetry .value { font-family: 'Share Tech Mono', monospace; font-size: 1.2rem; color: #fff; text-shadow: 0 0 6px rgba(255,255,255,0.3); }
/* Center Crosshair / Pitch Ladder */
#center-hud { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); pointer-events: none; }
.crosshair { width: 40px; height: 40px; border: 2px solid rgba(0,255,170,0.3); border-radius: 50%; position: relative; }
.crosshair::before, .crosshair::after { content: ''; position: absolute; background: rgba(0,255,170,0.5); }
.crosshair::before { width: 2px; height: 24px; left: 50%; top: 50%; transform: translate(-50%, -50%); }
.crosshair::after { width: 24px; height: 2px; top: 50%; left: 50%; transform: translate(-50%, -50%); }
/* Bottom Center: Mode & Status */
#status-bar { bottom: 30px; left: 50%; transform: translateX(-50%); padding: 14px 28px; text-align: center; min-width: 320px; }
#status-bar h3 { font-size: 0.9rem; letter-spacing: 3px; color: #00ffaa; text-transform: uppercase; margin-bottom: 4px; }
#status-bar p { font-family: 'Share Tech Mono', monospace; font-size: 0.85rem; color: #88ccff; }
/* Menu Overlay */
#main-menu { position: fixed; inset: 0; z-index: 100; background: linear-gradient(180deg, rgba(3,8,15,0.95) 0%, rgba(3,15,30,0.98) 100%); display: flex; flex-direction: column; align-items: center; justify-content: center; transition: opacity 0.8s ease; }
#main-menu.hidden { opacity: 0; pointer-events: none; }
.menu-title { font-size: 5rem; font-weight: 900; letter-spacing: 12px; color: transparent; background: linear-gradient(90deg, #00ffaa 0%, #0088ff 50%, #aa00ff 100%); -webkit-background-clip: text; background-clip: text; text-shadow: 0 0 60px rgba(0,255,170,0.2); margin-bottom: 10px; animation: pulseTitle 3s infinite alternate; }
@keyframes pulseTitle { from { filter: drop-shadow(0 0 20px rgba(0,255,170,0.2)); } to { filter: drop-shadow(0 0 40px rgba(0,136,255,0.4)); } }
.menu-sub { font-family: 'Share Tech Mono', monospace; font-size: 1.2rem; color: #88ccff; letter-spacing: 6px; margin-bottom: 60px; opacity: 0.7; }
.fleet-grid { display: flex; gap: 30px; margin-bottom: 50px; }
.plane-card { background: rgba(255,255,255,0.03); border: 1px solid rgba(0,255,170,0.15); border-radius: 12px; padding: 24px; width: 220px; text-align: center; cursor: pointer; transition: all 0.3s ease; position: relative; overflow: hidden; }
.plane-card:hover { border-color: rgba(0,255,170,0.6); background: rgba(0,255,170,0.05); transform: translateY(-8px); box-shadow: 0 20px 40px rgba(0,255,170,0.1); }
.plane-card.active { border-color: #00ffaa; box-shadow: 0 0 30px rgba(0,255,170,0.3); }
.plane-card h4 { font-size: 1.1rem; letter-spacing: 2px; margin-bottom: 4px; color: #fff; }
.plane-card .role { font-family: 'Share Tech Mono', monospace; font-size: 0.7rem; color: #00ffaa; margin-bottom: 14px; text-transform: uppercase; }
.plane-card .stat-row { display: flex; justify-content: space-between; font-family: 'Share Tech Mono', monospace; font-size: 0.75rem; color: #ccc; margin: 3px 0; padding-bottom: 2px; border-bottom: 1px solid rgba(255,255,255,0.05); }
.scenario-grid { display: flex; gap: 20px; }
.scenario-btn { padding: 16px 32px; border: 1px solid rgba(0,136,255,0.4); background: rgba(0,136,255,0.05); color: #88ccff; font-family: 'Orbitron', sans-serif; font-size: 0.9rem; letter-spacing: 3px; text-transform: uppercase; cursor: pointer; border-radius: 6px; transition: all 0.3s; }
.scenario-btn:hover { background: rgba(0,136,255,0.2); color: #00ffaa; border-color: #00ffaa; }
/* Canvas */
#sim-canvas { position: fixed; top: 0; left: 0; width: 100%; height: 100%; z-index: 1; }
</style>
</head>
<body>
<canvas id="sim-canvas"></canvas>
<!-- HUD -->
<div id="hud">
<div id="info-panel" class="hud-panel">
<h2 id="plane-name">Apex Striker</h2>
<div class="spec" id="spec-thrust">Thrust: 45,000 N</div>
<div class="spec" id="spec-weight">Weight: 8,200 kg</div>
<div class="spec" id="spec-lift">Lift Coeff: 0.42</div>
<div class="spec" id="spec-drag">Drag Coeff: 0.08</div>
</div>
<div id="telemetry" class="hud-panel">
<div><span class="label">Airspeed</span><span class="hud-value" id="val-speed">0</span><span style="font-size:0.8rem; color:#888;"> KTS</span></div>
<div><span class="label">Altitude</span><span class="hud-value" id="val-alt">120</span><span style="font-size:0.8rem; color:#888;"> M</span></div>
<div><span class="label">Pitch</span><span class="hud-value" id="val-pitch">0</span><span style="font-size:0.8rem; color:#888;"> °</span></div>
<div><span class="label">Roll</span><span class="hud-value" id="val-roll">0</span><span style="font-size:0.8rem; color:#888;"> °</span></div>
<div><span class="label">AOA</span><span class="hud-value" id="val-aoa">0</span><span style="font-size:0.8rem; color:#888;"> °</span></div>
<div><span class="label">Throttle</span><span class="hud-value" id="val-throttle">30</span><span style="font-size:0.8rem; color:#888;">%</span></div>
</div>
<div id="center-hud">
<div class="crosshair"></div>
</div>
<div id="status-bar" class="hud-panel">
<h3 id="scenario-title">Mission: Takeoff & Landing</h3>
<p id="scenario-desc">Manage throttle and pitch for a clean lift-off from the valley runway.</p>
</div>
</div>
<!-- Main Menu -->
<div id="main-menu">
<h1 class="menu-title">APEX AERO</h1>
<p class="menu-sub">HIGH-FIDELITY FLIGHT SIMULATION</p>
<div class="fleet-grid">
<div class="plane-card active" onclick="selectPlane(0)">
<h4>F-77 Striker</h4>
<div class="role">Multirole Fighter</div>
<div class="stat-row"><span>Max Speed</span><span>920 kts</span></div>
<div class="stat-row"><span>Thrust</span><span>52k N</span></div>
<div class="stat-row"><span>Weight</span><span>9,200 kg</span></div>
<div class="stat-row"><span>Lift</span><span>High</span></div>
</div>
<div class="plane-card" onclick="selectPlane(1)">
<h4>CS-200 Heavy</h4>
<div class="role">Heavy Interceptor</div>
<div class="stat-row"><span>Max Speed</span><span>780 kts</span></div>
<div class="stat-row"><span>Thrust</span><span>68k N</span></div>
<div class="stat-row"><span>Weight</span><span>14,500 kg</span></div>
<div class="stat-row"><span>Lift</span><span>Medium</span></div>
</div>
<div class="plane-card" onclick="selectPlane(2)">
<h4>VX-9 Glider</h4>
<div class="role">Light Recon</div>
<div class="stat-row"><span>Max Speed</span><span>650 kts</span></div>
<div class="stat-row"><span>Thrust</span><span>28k N</span></div>
<div class="stat-row"><span>Weight</span><span>4,800 kg</span></div>
<div class="stat-row"><span>Lift</span><span>Very High</span></div>
</div>
</div>
<div class="scenario-grid">
<button class="scenario-btn" onclick="startScenario('landing')">Takeoff / Landing</button>
<button class="scenario-btn" onclick="startScenario('dogfight')">Dogfight</button>
</div>
</div>
<script>
/* =========================================================
THREE.JS FLIGHT SIMULATOR ENGINE — APEX AERO
========================================================= */
// Global references
let renderer, scene, camera, clock;
let planeMesh, planePhysics;
let terrain;
let audioCtx, engineOsc, engineGain, windOsc, windGain, audioStarted = false;
let selectedPlaneIndex = 0;
let currentScenario = 'landing';
let controls = { thrust: 0.3, pitch: 0, roll: 0, yaw: 0, throttleDelta: 0 };
let keys = {};
// Aircraft database with physics profiles
const AIRCRAFT_DB = [
{
name: "F-77 Striker", role: "Multirole Fighter",
mass: 9200, thrustMax: 52000, thrustAcc: 24000, dragCoeff: 0.08, liftCoeff: 0.72,
wingArea: 42, maxSpeed: 450, stallAoa: 18,
specsText: ["Thrust: 52,000 N", "Weight: 9,200 kg", "Lift Coeff: 0.72", "Drag Coeff: 0.08"]
},
{
name: "CS-200 Heavy", role: "Heavy Interceptor",
mass: 14500, thrustMax: 68000, thrustAcc: 18000, dragCoeff: 0.10, liftCoeff: 0.55,
wingArea: 55, maxSpeed: 380, stallAoa: 14,
specsText: ["Thrust: 68,000 N", "Weight: 14,500 kg", "Lift Coeff: 0.55", "Drag Coeff: 0.10"]
},
{
name: "VX-9 Glider", role: "Light Recon",
mass: 4800, thrustMax: 28000, thrustAcc: 32000, dragCoeff: 0.05, liftCoeff: 0.95,
wingArea: 32, maxSpeed: 320, stallAoa: 22,
specsText: ["Thrust: 28,000 N", "Weight: 4,800 kg", "Lift Coeff: 0.95", "Drag Coeff: 0.05"]
}
];
// Scene init
function init() {
const canvas = document.getElementById('sim-canvas');
renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.outputEncoding = THREE.sRGBEncoding;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.2;
scene = new THREE.Scene();
scene.background = new THREE.Color(0x050a14);
scene.fog = new THREE.FogExp2(0x0a1225, 0.012);
// Camera
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 0.1, 5000);
camera.position.set(0, 30, 60);
clock = new THREE.Clock();
// Lights
const hemi = new THREE.HemisphereLight(0x8899cc, 0x223344, 1.2);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xffffff, 1.6);
sun.position.set(-200, 300, -100);
sun.castShadow = true;
sun.shadow.mapSize.width = 2048;
sun.shadow.mapSize.height = 2048;
sun.shadow.bias = -0.001;
scene.add(sun);
const fill = new THREE.DirectionalLight(0x5588cc, 0.5);
fill.position.set(100, -50, 50);
scene.add(fill);
// Procedural Terrain
generateTerrain();
// Skybox (procedural gradient sphere)
const skyGeo = new THREE.SphereGeometry(800, 32, 32);
const skyMat = new THREE.MeshBasicMaterial({
color: 0x020a14,
side: THREE.BackSide,
fog: false
});
const skyMesh = new THREE.Mesh(skyGeo, skyMat);
scene.add(skyMesh);
// Stars
addStars();
// Create aircraft geometry
buildAircraft(selectedPlaneIndex);
// Initialize physics state
planePhysics = {
velocity: new THREE.Vector3(0, 0, -20), // Start moving slightly forward
velocityLocal: new THREE.Vector3(0, 0, -20),
position: new THREE.Vector3(0, 30, 0),
rotation: new THREE.Euler(0, 0, 0),
pitchRate: 0, rollRate: 0, yawRate: 0,
altitude: 30,
airspeed: 20,
aoa: 0, // Angle of attack
stall: false
};
// Place mesh
planeMesh.position.copy(planePhysics.position);
planeMesh.rotation.copy(planePhysics.rotation);
// Input listeners
window.addEventListener('resize', onResize);
window.addEventListener('keydown', e => { keys[e.code] = true; if(e.code === 'Space') startAudio(); });
window.addEventListener('keyup', e => keys[e.code] = false);
// Start loop
animate();
}
// Procedural Terrain using Simplex-like noise approximation
function generateTerrain() {
const segments = 120;
const size = 600;
const geo = new THREE.PlaneGeometry(size, size, segments, segments);
geo.rotateX(-Math.PI / 2);
const pos = geo.attributes.position;
const colors = [];
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
const z = pos.getZ(i);
// Procedural height using multiple octaves of sine/noise approximation
const dist = Math.sqrt(x*x + z*z);
let h = 0;
h += Math.sin(x * 0.025) * 15 + Math.sin(z * 0.03) * 15;
h += Math.sin(x * 0.05 + z * 0.03) * 8;
h += Math.sin(x * 0.012) * Math.sin(z * 0.012) * 25;
h += Math.sin(dist * 0.02) * 10;
// Central valley / runway area
if (Math.abs(x) < 40 && z > -100 && z < 100) {
h = 2 + Math.sin(x * 0.05) * 1;
} else {
h = Math.max(h, 2); // Keep base above water-ish
}
// Add some mountains further out
if (dist > 150) {
h += Math.sin(dist * 0.04) * 30 + 20;
}
pos.setY(i, h);
// Vertex colors for terrain shading
const color = new THREE.Color();
if (h < 10) {
color.set(0x2a5a3a); // Grass/low
} else if (h < 40) {
color.set(0x6b5e4a); // Dirt/rock mid
} else {
color.set(0x8899aa); // Snow/peak
}
colors.push(color.r, color.g, color.b);
}
geo.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
geo.computeVertexNormals();
const terrainMat = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.9,
metalness: 0.05,
flatShading: false
});
terrain = new THREE.Mesh(geo, terrainMat);
terrain.receiveShadow = true;
scene.add(terrain);
// Add runway marker in valley
const runwayMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 });
const runway = new THREE.Mesh(new THREE.BoxGeometry(60, 0.2, 300), runwayMat);
runway.position.set(0, 0.1, 0);
runway.receiveShadow = true;
scene.add(runway);
}
// Add starfield
function addStars() {
const starGeo = new THREE.BufferGeometry();
const starCount = 3000;
const posArray = new Float32Array(starCount * 3);
for (let i = 0; i < starCount * 3; i++) {
posArray[i] = (Math.random() - 0.5) * 800;
}
starGeo.setAttribute('position', new THREE.BufferAttribute(posArray, 3));
const starMat = new THREE.PointsMaterial({ color: 0xffffff, size: 1.5, sizeAttenuation: true });
const stars = new THREE.Points(starGeo, starMat);
scene.add(stars);
}
// Aircraft builder
function buildAircraft(index) {
const ac = AIRCRAFT_DB[index];
const group = new THREE.Group();
// Fuselage
const fuselageGeo = new THREE.BoxGeometry(1, 1.5, 8);
const fuselageMat = new THREE.MeshStandardMaterial({ color: 0x334455, roughness: 0.3, metalness: 0.7 });
const fuselage = new THREE.Mesh(fuselageGeo, fuselageMat);
group.add(fuselage);
// Wings (angled for lift effect)
const wingGeo = new THREE.BoxGeometry(16, 0.2, 4);
const wingMat = new THREE.MeshStandardMaterial({ color: 0x556677, roughness: 0.4, metalness: 0.6 });
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.set(0, 0.2, 0);
group.add(wings);
// Tail
const tailGeo = new THREE.BoxGeometry(0.1, 3, 2);
const tail = new THREE.Mesh(tailGeo, wingMat);
tail.position.set(0, 1.5, -3.5);
group.add(tail);
// Nose cone (pointy)
const noseGeo = new THREE.ConeGeometry(0.8, 3, 8);
const nose = new THREE.Mesh(noseGeo, fuselageMat);
nose.position.set(0, 0, 3.5);
nose.rotation.x = -Math.PI / 2;
group.add(nose);
// Engines (cylinders)
const engGeo = new THREE.CylinderGeometry(0.4, 0.5, 1.5, 8);
const engMat = new THREE.MeshStandardMaterial({ color: 0x222, roughness: 0.5, metalness: 0.8 });
const leftEng = new THREE.Mesh(engGeo, engMat);
leftEng.position.set(-3, -0.8, -1);
leftEng.rotation.x = Math.PI / 2;
group.add(leftEng);
const rightEng = new THREE.Mesh(engGeo, engMat);
rightEng.position.set(3, -0.8, -1);
rightEng.rotation.x = Math.PI / 2;
group.add(rightEng);
group.scale.set(0.8, 0.8, 0.8);
group.rotation.y = Math.PI;
if (planeMesh) scene.remove(planeMesh);
planeMesh = group;
scene.add(planeMesh);
}
// Physics engine
function updatePhysics(delta) {
const ac = AIRCRAFT_DB[selectedPlaneIndex];
const p = planePhysics;
// Inputs
if (keys['ArrowUp']) controls.pitch += 0.04;
if (keys['ArrowDown']) controls.pitch -= 0.04;
if (keys['ArrowLeft']) controls.roll += 0.03;
if (keys['ArrowRight']) controls.roll -= 0.03;
if (keys['KeyW']) controls.thrust += 0.02;
if (keys['KeyS']) controls.thrust -= 0.02;
// Clamp
controls.pitch = Math.max(-0.8, Math.min(0.8, controls.pitch));
controls.roll = Math.max(-1.2, Math.min(1.2, controls.roll));
controls.thrust = Math.max(0, Math.min(1.2, controls.thrust));
// Atmospheric density based on altitude
const altitudeFactor = Math.exp(-p.altitude / 8500); // Simple exponential decay
const airDensity = 1.225 * altitudeFactor;
// Thrust
const thrustForce = ac.thrustMax * controls.thrust;
// Velocity magnitude
const speed = p.velocity.length();
p.airspeed = Math.round(speed * 1.94384); // Knots approx
// Angle of Attack (pitch relative to velocity vector)
const pitchAngle = p.rotation.x;
const aoa = pitchAngle - Math.atan2(p.velocity.y, Math.abs(p.velocity.z));
p.aoa = THREE.MathUtils.clamp(aoa, -Math.PI, Math.PI);
const aoaDeg = THREE.MathUtils.radToDeg(p.aoa);
p.stall = Math.abs(aoaDeg) > ac.stallAoa || speed < ac.maxSpeed * 0.15;
// Lift
// L = 0.5 * rho * v^2 * S * Cl
// Cl depends roughly linearly on AoA up to stall
let clFactor = 0;
if (!p.stall) {
clFactor = 0.1 + (Math.abs(aoaDeg) / ac.stallAoa) * ac.liftCoeff;
clFactor = THREE.MathUtils.clamp(clFactor, 0, ac.liftCoeff * 1.5);
} else {
clFactor = ac.liftCoeff * 0.3; // Post-stall reduced lift
}
// Induced drag increases at high lift
const inducedDragCoeff = (clFactor * clFactor) / (Math.PI * 6); // Approx
const profileDragCoeff = ac.dragCoeff;
// Drag
// D = 0.5 * rho * v^2 * S * Cd
const dragCoeffTotal = profileDragCoeff + inducedDragCoeff;
const dragForceMag = 0.5 * airDensity * speed * speed * ac.wingArea * dragCoeffTotal;
// Direction vectors
const forward = new THREE.Vector3(0, 0, -1);
forward.applyQuaternion(p.rotation); // Incorrect for world direction, let's use simpler model.
// Simplified world-space physics approximation:
// Apply thrust along plane's forward axis
const thrustVec = new THREE.Vector3(0, 0, -1); // Local forward
thrustVec.applyEuler(p.rotation);
thrustVec.multiplyScalar(thrustForce);
// Drag opposes velocity
const dragVec = p.velocity.clone().normalize().multiplyScalar(-dragForceMag);
// Lift is perpendicular to velocity, in pitch plane (simplified)
const velocityDir = p.velocity.clone().normalize();
// Perpendicular to velocity in vertical plane
const liftDir = new THREE.Vector3(-velocityDir.y, velocityDir.x, 0).normalize();
// Adjust lift direction relative to pitch/roll
const liftVec = new THREE.Vector3(0, 1, 0);
liftVec.applyQuaternion(p.rotation); // Rough approximation
// Better lift approximation: lift acts "up" relative to aircraft, scaled by AoA
const liftForceMag = 0.5 * airDensity * speed * speed * ac.wingArea * clFactor;
const liftVecSimple = new THREE.Vector3(0, liftForceMag, 0);
liftVecSimple.applyQuaternion(p.rotation);
// Weight / Gravity
const weightVec = new THREE.Vector3(0, -ac.mass * 9.81, 0);
// Combine forces
const totalForce = new THREE.Vector3();
totalForce.add(thrustVec);
totalForce.add(dragVec);
totalForce.add(liftVecSimple);
totalForce.add(weightVec);
// Accelerate (F=ma)
const accel = totalForce.divideScalar(ac.mass);
p.velocity.add(accel.multiplyScalar(delta));
// Update position
p.position.add(p.velocity.clone().multiplyScalar(delta));
// Inertia / rotational dynamics
// Roll and pitch rates based on inputs, dampened
const rollDamp = 0.85, pitchDamp = 0.85;
p.rollRate = THREE.MathUtils.lerp(p.rollRate, controls.roll * 1.5, 0.08);
p.pitchRate = THREE.MathUtils.lerp(p.pitchRate, controls.pitch * 1.5, 0.08);
p.yawRate = THREE.MathUtils.lerp(p.yawRate, controls.roll * 0.3, 0.05);
const rotationStep = new THREE.Euler(p.pitchRate * delta, 0, p.rollRate * delta, 'YXZ');
// Actually apply continuous rotation based on rates
p.rotation.y += p.yawRate * delta * 2;
p.rotation.x += p.pitchRate * delta * 2;
p.rotation.z += p.rollRate * delta * 2;
// Auto-level tendency (stability)
if (Math.abs(controls.roll) < 0.01) {
p.rotation.z *= 0.98;
p.rollRate *= 0.95;
}
if (Math.abs(controls.pitch) < 0.01) {
p.rotation.x *= 0.98;
p.pitchRate *= 0.95;
}
// Update plane mesh
p.altitude = p.position.y;
planeMesh.position.copy(p.position);
planeMesh.rotation.copy(p.rotation);
// Check terrain collision (simple)
// Look up terrain height at plane position
// Note: we don't have exact terrain lookup here but we approximate with distance-based height or raycast for simplicity.
// For fidelity, we'll do a simple raycast down.
const raycaster = new THREE.Raycaster();
const down = new THREE.Vector3(0, -1, 0);
raycaster.set(p.position, down);
const hits = raycaster.intersectObject(terrain);
if (hits.length > 0) {
const groundHeight = hits[0].point.y;
if (p.position.y < groundHeight + 3) {
p.position.y = groundHeight + 3;
p.velocity.y = 0;
p.velocity.multiplyScalar(0.95); // Ground friction
}
}
// Update camera (follow plane with lag)
const offset = new THREE.Vector3(0, 10, 30);
offset.applyQuaternion(p.rotation);
const targetCamPos = p.position.clone().add(offset);
camera.position.lerp(targetCamPos, 0.1);
camera.lookAt(p.position);
}
// Update HUD text
function updateHUD() {
const ac = AIRCRAFT_DB[selectedPlaneIndex];
document.getElementById('plane-name').textContent = ac.name;
document.getElementById('spec-thrust').textContent = "Thrust: " + (ac.thrustMax/1000).toFixed(0) + " kN";
document.getElementById('spec-weight').textContent = "Weight: " + (ac.mass/1000).toFixed(1) + " t";
document.getElementById('spec-lift').textContent = "Lift Coeff: " + ac.liftCoeff.toFixed(2);
document.getElementById('spec-drag').textContent = "Drag Coeff: " + ac.dragCoeff.toFixed(2);
document.getElementById('val-speed').textContent = planePhysics ? Math.round(planePhysics.airspeed) : 0;
document.getElementById('val-alt').textContent = planePhysics ? Math.round(planePhysics.altitude) : 0;
document.getElementById('val-pitch').textContent = planePhysics ? THREE.MathUtils.radToDeg(planePhysics.rotation.x).toFixed(0) : 0;
document.getElementById('val-roll').textContent = planePhysics ? THREE.MathUtils.radToDeg(planePhysics.rotation.z).toFixed(0) : 0;
document.getElementById('val-aoa').textContent = planePhysics ? THREE.MathUtils.radToDeg(planePhysics.aoa).toFixed(0) : 0;
document.getElementById('val-throttle').textContent = Math.round(controls.thrust * 100);
}
// Audio generation using Web Audio API
function startAudio() {
if (audioStarted) return;
audioStarted = true;
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
} catch(e) { return; }
// Engine sound: multiple detuned oscillators for richness
engineOsc = [];
engineGain = [];
for(let i=0; i<3; i++) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(60 + i*10, audioCtx.currentTime);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
engineOsc.push(osc);
engineGain.push(gain);
gain.gain.setValueAtTime(0, audioCtx.currentTime);
}
// Wind noise
windOsc = audioCtx.createOscillator();
windGain = audioCtx.createGain();
windOsc.type = 'triangle';
windOsc.frequency.setValueAtTime(120, audioCtx.currentTime);
windOsc.connect(windGain);
windGain.connect(audioCtx.destination);
windOsc.start();
windGain.gain.setValueAtTime(0.05, audioCtx.currentTime);
}
function updateAudio() {
if (!audioStarted || !audioCtx) return;
const ac = AIRCRAFT_DB[selectedPlaneIndex];
const p = planePhysics;
// Engine pitch and volume based on throttle and airspeed
const throttleFactor = controls.thrust;
const rpmFactor = throttleFactor * 0.8 + (p.airspeed / ac.maxSpeed) * 0.4;
const baseFreq = 60 + rpmFactor * 220;
const volume = Math.min(0.4, throttleFactor * 0.6 + (p.airspeed / 500) * 0.1);
engineOsc.forEach((osc, i) => {
osc.frequency.linearRampToValueAtTime(baseFreq + i*15, audioCtx.currentTime + 0.05);
engineGain[i].gain.linearRampToValueAtTime(volume * (0.7 + i*0.15), audioCtx.currentTime + 0.05);
});
// Wind noise increases with speed and altitude
const windVol = Math.min(0.3, (p.airspeed / 600) * 0.2 + (p.altitude / 200) * 0.05);
windGain.gain.linearRampToValueAtTime(windVol, audioCtx.currentTime + 0.05);
}
// Menu logic
function selectPlane(index) {
selectedPlaneIndex = index;
document.querySelectorAll('.plane-card').forEach((el, i) => {
el.classList.toggle('active', i === index);
});
// Update aircraft if already running
if (planeMesh) {
buildAircraft(index);
planePhysics.velocity.set(0,0,-20);
}
}
function startScenario(type) {
currentScenario = type;
document.getElementById('main-menu').classList.add('hidden');
if (type === 'landing') {
document.getElementById('scenario-title').textContent = "Mission: Precision Landing";
document.getElementById('scenario-desc').textContent = "Maintain 15° pitch on final approach. Throttle below 30% for touchdown.";
planePhysics.position.set(0, 120, -300);
planePhysics.velocity.set(0, -5, 60);
planePhysics.rotation.set(0, 0, 0);
controls.thrust = 0.25;
controls.pitch = 0.15;
} else {
document.getElementById('scenario-title').textContent = "Mission: Dogfight Maneuvers";
document.getElementById('scenario-desc').textContent = "Execute high-G turns. Watch angle of attack to avoid stall.";
planePhysics.position.set(-100, 200, 0);
planePhysics.velocity.set(60, 0, 0);
planePhysics.rotation.set(0, Math.PI/2, 0);
controls.thrust = 0.8;
}
buildAircraft(selectedPlaneIndex);
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
// Main loop
function animate() {
requestAnimationFrame(animate);
const dt = Math.min(clock.getDelta(), 0.05); // Cap delta to avoid physics explosions
updatePhysics(dt);
updateHUD();
updateAudio();
renderer.render(scene, camera);
}
// Initialize after DOM load
window.addEventListener('DOMContentLoaded', () => {
init();
});
// Expose functions for HTML onclick
window.selectPlane = selectPlane;
window.startScenario = startScenario;
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingmachines/inkling","provider":"openrouter","persona":"basic","sessionId":"8e32c323-fcfe-4c95-97c3-da99e738f6a5","tokensIn":1205,"tokensOut":9083,"tokensTotal":10288,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-18T20:39:12.011Z"} -->