Flight Simulator
thinkingcap qwen3.6 27b · middle

Browser check: External resource unavailable · Artifact runtime error. 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 56,832 bytes · SHA-256 1e455305e476
<!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>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body { overflow: hidden; background: #000; font-family: 'Segoe UI', Arial, sans-serif; cursor: none; }
canvas { display: block; }
/* ── Menu Screen ── */
#menu-screen {
position: fixed; inset: 0; z-index: 1000;
background: linear-gradient(135deg, #0a0e27 0%, #1a1a3e 40%, #0d1b2a 100%);
display: flex; flex-direction: column; align-items: center; justify-content: center;
transition: opacity 0.6s;
}
#menu-screen.hidden { opacity: 0; pointer-events: none; }
#menu-screen h1 {
font-size: 4rem; color: #fff; letter-spacing: 0.3em; text-transform: uppercase;
text-shadow: 0 0 40px rgba(0,200,255,0.6), 0 0 80px rgba(0,100,255,0.3);
margin-bottom: 0.2em;
}
#menu-screen .subtitle { color: #7ec8e3; font-size: 1rem; letter-spacing: 0.5em; margin-bottom: 3rem; }
.aircraft-select { display: flex; gap: 2rem; margin-bottom: 3rem; }
.ac-card {
width: 220px; padding: 1.5rem; border-radius: 12px; cursor: pointer;
background: rgba(255,255,255,0.05); border: 2px solid rgba(255,255,255,0.1);
transition: all 0.3s; text-align: center; color: #ccc;
}
.ac-card:hover { border-color: rgba(0,200,255,0.5); background: rgba(0,200,255,0.1); transform: translateY(-4px); }
.ac-card.selected { border-color: #00c8ff; background: rgba(0,200,255,0.15); box-shadow: 0 0 30px rgba(0,200,255,0.3); }
.ac-card .icon { font-size: 3rem; margin-bottom: 0.5rem; }
.ac-card h3 { color: #fff; margin-bottom: 0.5rem; font-size: 1.1rem; }
.ac-card p { font-size: 0.75rem; line-height: 1.4; color: #999; }
.scenario-select { display: flex; gap: 2rem; margin-bottom: 3rem; }
.sc-card {
width: 260px; padding: 1.5rem; border-radius: 12px; cursor: pointer;
background: rgba(255,255,255,0.05); border: 2px solid rgba(255,255,255,0.1);
transition: all 0.3s; text-align: center; color: #ccc;
}
.sc-card:hover { border-color: rgba(255,165,0,0.5); background: rgba(255,165,0,0.1); transform: translateY(-4px); }
.sc-card.selected { border-color: #ffa500; background: rgba(255,165,0,0.15); box-shadow: 0 0 30px rgba(255,165,0,0.3); }
.sc-card .icon { font-size: 2.5rem; margin-bottom: 0.5rem; }
.sc-card h3 { color: #fff; margin-bottom: 0.5rem; }
.sc-card p { font-size: 0.75rem; line-height: 1.4; color: #999; }
#start-btn {
padding: 1rem 4rem; font-size: 1.2rem; letter-spacing: 0.3em; text-transform: uppercase;
background: linear-gradient(135deg, #00c8ff, #0066ff); color: #fff; border: none;
border-radius: 8px; cursor: pointer; transition: all 0.3s; font-weight: bold;
}
#start-btn:hover { transform: scale(1.05); box-shadow: 0 0 40px rgba(0,200,255,0.5); }
/* ── HUD Overlay ── */
#hud {
position: fixed; inset: 0; z-index: 100; pointer-events: none; display: none;
}
#hud.active { display: block; }
.hud-top-left {
position: absolute; top: 20px; left: 20px; color: #0f0; font-family: 'Courier New', monospace;
font-size: 13px; line-height: 1.6; text-shadow: 0 0 8px rgba(0,255,0,0.5);
}
.hud-top-right {
position: absolute; top: 20px; right: 20px; color: #0f0; font-family: 'Courier New', monospace;
font-size: 13px; line-height: 1.6; text-align: right; text-shadow: 0 0 8px rgba(0,255,0,0.5);
}
.hud-bottom {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
color: #ff0; font-family: 'Courier New', monospace; font-size: 14px; text-align: center;
text-shadow: 0 0 8px rgba(255,255,0,0.5);
}
.hud-center {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 300px; height: 300px; pointer-events: none;
}
/* Crosshair */
.crosshair-h, .crosshair-v {
position: absolute; background: rgba(0,255,0,0.7);
}
.crosshair-h { width: 40px; height: 1px; top: 50%; left: 50%; transform: translate(-50%, -50%); }
.crosshair-v { width: 1px; height: 40px; top: 50%; left: 50%; transform: translate(-50%, -50%); }
/* Pitch ladder */
.pitch-ladder { position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%); width: 200px; height: 200px; }
.pitch-line {
position: absolute; left: 50%; transform: translateX(-50%); height: 1px;
background: rgba(0,255,0,0.4); width: 60px;
}
/* Horizon line */
.horizon-line {
position: absolute; top: 50%; left: 0; right: 0; height: 1px;
background: rgba(0,255,0,0.3); transform-origin: center;
}
/* Stall warning */
.stall-warning {
position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%);
color: #f00; font-size: 2rem; font-weight: bold; letter-spacing: 0.3em;
text-shadow: 0 0 20px rgba(255,0,0,0.8); display: none; animation: blink 0.3s infinite;
}
@keyframes blink { 0%,100%{opacity:1;} 50%{opacity:0.3;} }
/* G-meter */
.g-meter {
position: absolute; right: 20px; top: 50%; transform: translateY(-50%);
width: 30px; height: 200px; border: 1px solid rgba(0,255,0,0.4); border-radius: 4px;
}
.g-meter-fill {
position: absolute; bottom: 50%; left: 2px; right: 2px; background: linear-gradient(to top, #0f0, #ff0, #f00);
transition: height 0.1s; border-radius: 2px;
}
/* Minimap */
.minimap {
position: absolute; bottom: 20px; right: 20px; width: 150px; height: 150px;
border: 1px solid rgba(0,255,0,0.4); border-radius: 50%; overflow: hidden;
background: rgba(0,20,0,0.7);
}
/* Damage overlay */
.damage-overlay {
position: fixed; inset: 0; z-index: 99; pointer-events: none;
border: 8px solid transparent; transition: border-color 0.3s;
}
.damage-overlay.hit { border-color: rgba(255,0,0,0.6); }
/* Pause overlay */
#pause-overlay {
position: fixed; inset: 0; z-index: 500; background: rgba(0,0,0,0.7);
display: none; align-items: center; justify-content: center; flex-direction: column; color: #fff;
}
#pause-overlay.active { display: flex; }
#pause-overlay h2 { font-size: 3rem; margin-bottom: 1rem; letter-spacing: 0.2em; }
#pause-overlay p { font-size: 1rem; color: #aaa; margin-bottom: 0.5rem; }
/* Controls help */
.controls-help {
position: absolute; bottom: 20px; left: 20px; color: rgba(255,255,255,0.4);
font-size: 11px; line-height: 1.6; font-family: 'Courier New', monospace;
}
/* Target reticle for dogfight */
.target-reticle {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 80px; height: 80px; border: 2px solid rgba(255,0,0,0.6); border-radius: 50%;
display: none;
}
.target-reticle.locked { border-color: #f00; animation: pulse 0.5s infinite; }
@keyframes pulse { 0%,100%{transform:translate(-50%,-50%) scale(1);} 50%{transform:translate(-50%,-50%) scale(1.1);} }
/* Score display */
.score-display {
position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
color: #ffa500; font-size: 1.5rem; font-weight: bold; text-shadow: 0 0 10px rgba(255,165,0,0.5);
}
/* Landing score */
.landing-score {
position: fixed; inset: 0; z-index: 600; display: none; align-items: center; justify-content: center;
background: rgba(0,0,0,0.8); color: #fff; flex-direction: column;
}
.landing-score.active { display: flex; }
.landing-score h2 { font-size: 3rem; margin-bottom: 1rem; }
.landing-score .grade { font-size: 6rem; color: #0f0; text-shadow: 0 0 40px rgba(0,255,0,0.8); }
/* Loading */
#loading-screen {
position: fixed; inset: 0; z-index: 2000; background: #0a0e27;
display: flex; align-items: center; justify-content: center; color: #fff; font-size: 1.5rem;
}
</style>
</head>
<body>
<!-- Loading -->
<div id="loading-screen">Initializing Apex Aero...</div>
<!-- Menu Screen -->
<div id="menu-screen" class="hidden">
<h1>Apex Aero</h1>
<div class="subtitle">Flight Simulator</div>
<div style="color:#aaa;margin-bottom:1rem;font-size:0.9rem;">SELECT AIRCRAFT</div>
<div class="aircraft-select" id="aircraft-select"></div>
<div style="color:#aaa;margin-bottom:1rem;font-size:0.9rem;">SELECT SCENARIO</div>
<div class="scenario-select" id="scenario-select"></div>
<button id="start-btn">LAUNCH</button>
</div>
<!-- HUD -->
<div id="hud">
<div class="hud-top-left" id="hud-left"></div>
<div class="hud-top-right" id="hud-right"></div>
<div class="hud-bottom" id="hud-bottom"></div>
<div class="hud-center">
<div class="crosshair-h"></div>
<div class="crosshair-v"></div>
<div class="horizon-line" id="horizon-line"></div>
<div class="pitch-ladder" id="pitch-ladder"></div>
</div>
<div class="stall-warning" id="stall-warning">⚠ STALL ⚠</div>
<div class="g-meter"><div class="g-meter-fill" id="g-meter-fill"></div></div>
<div class="minimap" id="minimap"><canvas id="minimap-canvas" width="150" height="150"></canvas></div>
<div class="target-reticle" id="target-reticle"></div>
<div class="score-display" id="score-display"></div>
<div class="controls-help">W/S: Pitch | A/D: Roll | Q/E: Yaw | SHIFT/CTRL: Throttle ↑↓ | SPACE: Brake | P: Pause</div>
</div>
<div class="damage-overlay" id="damage-overlay"></div>
<!-- Pause -->
<div id="pause-overlay">
<h2>PAUSED</h2>
<p>Press P to resume</p>
<p style="margin-top:1rem;color:#666;">ESC for main menu</p>
</div>
<!-- Landing Score -->
<div class="landing-score" id="landing-score">
<h2>Landing Complete</h2>
<div class="grade" id="landing-grade"></div>
<p style="margin-top:1rem;color:#aaa;" id="landing-details"></p>
<button onclick="location.reload()" style="margin-top:2rem;padding:0.8rem 2rem;background:#0f0;border:none;border-radius:6px;cursor:pointer;font-size:1rem;">Play Again</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r152/three.min.js"></script>
<script>
// ═══════════════════════════════════════════════════════
// APEX AERO — FLIGHT SIMULATOR
// ═══════════════════════════════════════════════════════
// ── Aircraft Definitions ──
const AIRCRAFT = {
falcon: {
name: "Falcon X-1", icon: "🦅", desc: "High-performance fighter. Fast, agile, excellent for dogfighting.",
color: 0x4488ff, accentColor: 0x2266cc,
mass: 8500, wingArea: 35, maxThrust: 180000, thrustIdle: 30000,
clMax: 2.2, cd0: 0.02, kInduced: 0.04,
rollRate: 2.5, pitchRate: 2.0, yawRate: 1.2,
maxSpeed: 350, stallSpeed: 65, engineResponseTime: 0.8,
turnRadius: 300, gLimit: 9,
length: 16, wingspan: 10, height: 4.5
},
hawk: {
name: "Hawk R-7", icon: "🦾", desc: "Balanced multi-role aircraft. Good all-around performance.",
color: 0x88cc44, accentColor: 0x669933,
mass: 12000, wingArea: 50, maxThrust: 140000, thrustIdle: 25000,
clMax: 1.8, cd0: 0.025, kInduced: 0.035,
rollRate: 1.8, pitchRate: 1.6, yawRate: 1.0,
maxSpeed: 280, stallSpeed: 70, engineResponseTime: 1.0,
turnRadius: 400, gLimit: 7.5,
length: 18, wingspan: 13, height: 5
},
titan: {
name: "Titan B-9", icon: "🛩️", desc: "Heavy bomber. Slow but stable. Requires skill for precision landings.",
color: 0xcc6644, accentColor: 0x994422,
mass: 35000, wingArea: 180, maxThrust: 400000, thrustIdle: 60000,
clMax: 2.5, cd0: 0.035, kInduced: 0.025,
rollRate: 0.8, pitchRate: 0.7, yawRate: 0.5,
maxSpeed: 220, stallSpeed: 80, engineResponseTime: 1.5,
turnRadius: 600, gLimit: 4,
length: 30, wingspan: 35, height: 9
}
};
// ── Scenario Definitions ──
const SCENARIOS = {
landing: { name: "Takeoff & Landing", icon: "🛬", desc: "Master precision flight. Take off and land safely on the runway." },
dogfight: { name: "Dogfight Arena", icon: "⚔️", desc: "Engage enemy aircraft in high-speed aerial combat." }
};
// ── Global State ──
let selectedAircraft = 'falcon';
let selectedScenario = 'landing';
let gameRunning = false;
let paused = false;
let score = 0;
// ── Three.js Setup ──
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x87CEEB, 0.00015);
const camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 1, 50000);
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: false });
renderer.setSize(window.innerWidth, window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.0;
document.body.appendChild(renderer.domElement);
// ── Lighting ──
const ambientLight = new THREE.AmbientLight(0x6688aa, 0.5);
scene.add(ambientLight);
const sunLight = new THREE.DirectionalLight(0xffeedd, 2.0);
sunLight.position.set(5000, 3000, 2000);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 4096;
sunLight.shadow.mapSize.height = 4096;
sunLight.shadow.camera.near = 100;
sunLight.shadow.camera.far = 20000;
sunLight.shadow.camera.left = -5000;
sunLight.shadow.camera.right = 5000;
sunLight.shadow.camera.top = 5000;
sunLight.shadow.camera.bottom = -5000;
scene.add(sunLight);
const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x445522, 0.6);
scene.add(hemiLight);
// ── Sky Dome ──
function createSky() {
const skyGeo = new THREE.SphereGeometry(30000, 32, 32);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color(0x0044aa) },
bottomColor: { value: new THREE.Color(0xaaddff) },
offset: { value: 20 },
exponent: { value: 0.5 }
},
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 float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + offset).y;
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
}
`,
side: THREE.BackSide
});
scene.add(new THREE.Mesh(skyGeo, skyMat));
}
createSky();
// ── Clouds ──
function createClouds() {
const cloudGroup = new THREE.Group();
for (let i = 0; i < 200; i++) {
const size = 100 + Math.random() * 300;
const geo = new THREE.SphereGeometry(size, 8, 6);
const mat = new THREE.MeshStandardMaterial({
color: 0xffffff, transparent: true, opacity: 0.4 + Math.random() * 0.3,
roughness: 1, metalness: 0
});
const cloud = new THREE.Mesh(geo, mat);
cloud.position.set(
(Math.random() - 0.5) * 20000,
800 + Math.random() * 3000,
(Math.random() - 0.5) * 20000
);
cloud.scale.y = 0.3;
cloudGroup.add(cloud);
}
scene.add(cloudGroup);
}
createClouds();
// ── Procedural Terrain ──
const TERRAIN_SIZE = 16000;
const TERRAIN_SEGMENTS = 512;
function noise2D(x, z) {
const n = Math.sin(x * 12.9898 + z * 78.233) * 43758.5453;
return n - Math.floor(n);
}
function fbmNoise(x, z, octaves) {
let val = 0, amp = 1, freq = 1, maxAmp = 0;
for (let i = 0; i < octaves; i++) {
val += (noise2D(x * freq, z * freq) - 0.5) * amp;
maxAmp += amp;
amp *= 0.5;
freq *= 2;
}
return val / maxAmp;
}
function getTerrainHeight(x, z) {
const scale = 0.0003;
let h = fbmNoise(x * scale, z * scale, 6);
h = Math.abs(h) * 2;
// Mountains in certain regions
const distFromCenter = Math.sqrt(x*x + z*z);
const mountainFactor = Math.max(0, 1 - distFromCenter / 8000);
h *= (1 + mountainFactor * 3);
// Flat areas for runways near center
if (distFromCenter < 500) {
h *= distFromCenter / 500;
}
return Math.max(0, h * 2000);
}
function createTerrain() {
const geo = new THREE.PlaneGeometry(TERRAIN_SIZE, TERRAIN_SIZE, TERRAIN_SEGMENTS, TERRAIN_SEGMENTS);
geo.rotateX(-Math.PI / 2);
const positions = geo.attributes.position;
const colors = new Float32Array(positions.count * 3);
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i);
const z = positions.getZ(i);
const h = getTerrainHeight(x, z);
positions.setY(i, h);
// Vertex colors based on height
let r, g, b;
if (h < 20) {
// Water/shore
r = 0.15; g = 0.35; b = 0.45;
} else if (h < 100) {
// Grass
const t = (h - 20) / 80;
r = 0.2 + t * 0.1; g = 0.5 + t * 0.1; b = 0.15;
} else if (h < 400) {
// Forest/rock
const t = (h - 100) / 300;
r = 0.25 + t * 0.2; g = 0.35 + t * 0.1; b = 0.15 + t * 0.1;
} else if (h < 700) {
// Rock
const t = (h - 400) / 300;
r = 0.4 + t * 0.2; g = 0.35 + t * 0.15; b = 0.3 + t * 0.1;
} else {
// Snow
const t = Math.min(1, (h - 700) / 400);
r = 0.6 + t * 0.35; g = 0.6 + t * 0.35; b = 0.65 + t * 0.3;
}
colors[i * 3] = r;
colors[i * 3 + 1] = g;
colors[i * 3 + 2] = b;
}
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geo.computeVertexNormals();
const mat = new THREE.MeshStandardMaterial({
vertexColors: true, roughness: 0.9, metalness: 0.1, flatShading: false
});
const terrain = new THREE.Mesh(geo, mat);
terrain.receiveShadow = true;
scene.add(terrain);
return terrain;
}
const terrain = createTerrain();
// ── Runway ──
function createRunway() {
const runwayGroup = new THREE.Group();
// Main runway surface
const rwGeo = new THREE.PlaneGeometry(60, 2500);
rwGeo.rotateX(-Math.PI / 2);
const rwMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.8 });
const runway = new THREE.Mesh(rwGeo, rwMat);
runway.position.y = 1;
runway.receiveShadow = true;
runwayGroup.add(runway);
// Center line markings
for (let i = -1200; i < 1250; i += 40) {
const markGeo = new THREE.PlaneGeometry(2, 20);
markGeo.rotateX(-Math.PI / 2);
const markMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
const mark = new THREE.Mesh(markGeo, markMat);
mark.position.set(0, 1.5, i);
runwayGroup.add(mark);
}
// Threshold markings
for (let side of [-1, 1]) {
for (let j = 0; j < 4; j++) {
const tGeo = new THREE.PlaneGeometry(8, 30);
tGeo.rotateX(-Math.PI / 2);
const tMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
const tMark = new THREE.Mesh(tGeo, tMat);
tMark.position.set(side * (15 + j * 8), 1.5, -1200);
runwayGroup.add(tMark);
const tMark2 = tMark.clone();
tMark2.position.z = 1200;
runwayGroup.add(tMark2);
}
}
// Edge lights
for (let side of [-1, 1]) {
for (let i = -1250; i <= 1250; i += 30) {
const lightGeo = new THREE.SphereGeometry(0.5);
const lightMat = new THREE.MeshStandardMaterial({ color: 0x44aaff, emissive: 0x44aaff, emissiveIntensity: 2 });
const light = new THREE.Mesh(lightGeo, lightMat);
light.position.set(side * 31, 1.5, i);
runwayGroup.add(light);
}
}
// PAPI lights (approach slope indicators)
for (let i = 0; i < 4; i++) {
const pGeo = new THREE.BoxGeometry(2, 2, 1);
const pMat = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 1 });
const pLight = new THREE.Mesh(pGeo, pMat);
pLight.position.set(35, 2, -1250 + i * 4);
runwayGroup.add(pLight);
}
scene.add(runwayGroup);
}
createRunway();
// ── Aircraft Model Builder ──
function buildAircraftModel(type) {
const ac = AIRCRAFT[type];
const group = new THREE.Group();
// Fuselage
const fuselageGeo = new THREE.CylinderGeometry(1.2, 0.8, ac.length * 0.7, 12);
fuselageGeo.rotateZ(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({ color: ac.color, roughness: 0.3, metalness: 0.6 });
const fuselage = new THREE.Mesh(fuselageGeo, bodyMat);
group.add(fuselage);
// Nose cone
const noseGeo = new THREE.ConeGeometry(1.2, ac.length * 0.35, 12);
noseGeo.rotateZ(-Math.PI / 2);
const nose = new THREE.Mesh(noseGeo, bodyMat);
nose.position.x = ac.length * 0.4;
group.add(nose);
// Cockpit canopy
const cockpitGeo = new THREE.SphereGeometry(1.0, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x88ccff, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.7 });
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(ac.length * 0.15, 1.2, 0);
group.add(cockpit);
// Wings
const wingHalfSpan = ac.wingspan / 2;
const wingGeo = new THREE.BoxGeometry(ac.length * 0.3, 0.25, ac.wingspan);
const wings = new THREE.Mesh(wingGeo, bodyMat);
wings.position.set(-ac.length * 0.1, -0.2, 0);
group.add(wings);
// Wing tips (ailerons)
for (let side of [-1, 1]) {
const tipGeo = new THREE.BoxGeometry(1.5, 0.3, 2);
const tipMat = new THREE.MeshStandardMaterial({ color: ac.accentColor });
const tip = new THREE.Mesh(tipGeo, tipMat);
tip.position.set(-ac.length * 0.1, -0.2, side * wingHalfSpan);
group.add(tip);
// Navigation lights
const navLightGeo = new THREE.SphereGeometry(0.3);
const navColor = side === -1 ? 0xff0000 : 0x00ff00;
const navMat = new THREE.MeshStandardMaterial({ color: navColor, emissive: navColor, emissiveIntensity: 3 });
const navLight = new THREE.Mesh(navLightGeo, navMat);
navLight.position.set(-ac.length * 0.1, -0.2, side * (wingHalfSpan + 0.5));
group.add(navLight);
}
// Tail section
const tailFuselageGeo = new THREE.CylinderGeometry(0.6, 0.4, ac.length * 0.3, 8);
tailFuselageGeo.rotateZ(Math.PI / 2);
const tailFuselage = new THREE.Mesh(tailFuselageGeo, bodyMat);
tailFuselage.position.x = -ac.length * 0.45;
group.add(tailFuselage);
// Vertical stabilizer
const vStabGeo = new THREE.BoxGeometry(2, ac.height * 0.6, 0.15);
const vStab = new THREE.Mesh(vStabGeo, bodyMat);
vStab.position.set(-ac.length * 0.55, ac.height * 0.3, 0);
group.add(vStab);
// Rudder accent
const rudderGeo = new THREE.BoxGeometry(1, ac.height * 0.4, 0.2);
const rudderMat = new THREE.MeshStandardMaterial({ color: ac.accentColor });
const rudder = new THREE.Mesh(rudderGeo, rudderMat);
rudder.position.set(-ac.length * 0.65, ac.height * 0.25, 0);
group.add(rudder);
// Horizontal stabilizers
for (let side of [-1, 1]) {
const hStabGeo = new THREE.BoxGeometry(3, 0.15, ac.wingspan * 0.4);
const hStab = new THREE.Mesh(hStabGeo, bodyMat);
hStab.position.set(-ac.length * 0.5, side * ac.height * 0.25, 0);
group.add(hStab);
}
// Engine exhausts
for (let side of [-1, 1]) {
const engineGeo = new THREE.CylinderGeometry(0.6, 0.8, 3, 8);
engineGeo.rotateZ(Math.PI / 2);
const engineMat = new THREE.MeshStandardMaterial({ color: 0x555555, roughness: 0.7, metalness: 0.8 });
const engine = new THREE.Mesh(engineGeo, engineMat);
engine.position.set(-ac.length * 0.35, -0.5, side * 2);
group.add(engine);
// Exhaust glow
const exhaustGeo = new THREE.SphereGeometry(0.7);
const exhaustMat = new THREE.MeshStandardMaterial({ color: 0xff4400, emissive: 0xff4400, emissiveIntensity: 0 });
const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);
exhaust.position.set(-ac.length * 0.5, -0.5, side * 2);
exhaust.name = 'exhaust';
group.add(exhaust);
}
// Landing gear (simplified)
for (let x of [ac.length * 0.3, -ac.length * 0.3]) {
const gearGeo = new THREE.CylinderGeometry(0.15, 0.15, 2, 6);
const gearMat = new THREE.MeshStandardMaterial({ color: 0x444444 });
for (let side of [-1, 1]) {
const gear = new THREE.Mesh(gearGeo, gearMat);
gear.position.set(x, -1.5, side * 2);
group.add(gear);
// Wheel
const wheelGeo = new THREE.CylinderGeometry(0.4, 0.4, 0.3, 8);
wheelGeo.rotateZ(Math.PI / 2);
const wheelMat = new THREE.MeshStandardMaterial({ color: 0x222222 });
const wheel = new THREE.Mesh(wheelGeo, wheelMat);
wheel.position.set(x, -2.5, side * 2);
group.add(wheel);
}
}
// Nose gear
const noseGearGeo = new THREE.CylinderGeometry(0.12, 0.12, 1.5, 6);
const noseGearMat = new THREE.MeshStandardMaterial({ color: 0x444444 });
const noseGear = new THREE.Mesh(noseGearGeo, noseGearMat);
noseGear.position.set(ac.length * 0.35, -1.2, 0);
group.add(noseGear);
// Scale the model appropriately
const scaleFactor = type === 'titan' ? 1.5 : (type === 'hawk' ? 1.2 : 1.0);
group.scale.setScalar(scaleFactor);
return group;
}
// ── Enemy Aircraft for Dogfight ──
function buildEnemyModel() {
const group = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xcc2222, roughness: 0.3, metalness: 0.6 });
const fuselageGeo = new THREE.CylinderGeometry(1, 0.7, 14, 10);
fuselageGeo.rotateZ(Math.PI / 2);
group.add(new THREE.Mesh(fuselageGeo, bodyMat));
const noseGeo = new THREE.ConeGeometry(1, 5, 10);
noseGeo.rotateZ(-Math.PI / 2);
const nose = new THREE.Mesh(noseGeo, bodyMat);
nose.position.x = 9;
group.add(nose);
const wingGeo = new THREE.BoxGeometry(4, 0.2, 10);
group.add(new THREE.Mesh(wingGeo, bodyMat));
for (let side of [-1, 1]) {
const tipGeo = new THREE.SphereGeometry(0.3);
const tipMat = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0xff0000, emissiveIntensity: 2 });
const tip = new THREE.Mesh(tipGeo, tipMat);
tip.position.set(0, 0, side * 5.3);
group.add(tip);
}
return group;
}
// ── Physics State ──
const physics = {
position: new THREE.Vector3(0, 10, -1200),
velocity: new THREE.Vector3(0, 0, 0),
quaternion: new THREE.Quaternion(),
angularVelocity: new THREE.Vector3(0, 0, 0),
// Engine state
throttleTarget: 0.0,
throttleCurrent: 0.0,
rpm: 0,
// Flight data
airspeed: 0,
altitude: 10,
verticalSpeed: 0,
gForce: 1,
angleOfAttack: 0,
isStalled: false,
onGround: true,
// Inertia tensor (simplified)
inertiaX: 1.0,
inertiaY: 2.0,
inertiaZ: 2.5,
// Damage
health: 100,
damaged: false
};
// ── Input State ──
const input = {
pitch: 0, roll: 0, yaw: 0, throttleUp: false, throttleDown: false, brake: false
};
document.addEventListener('keydown', (e) => {
switch(e.code) {
case 'KeyW': input.pitch = -1; break;
case 'KeyS': input.pitch = 1; break;
case 'KeyA': input.roll = -1; break;
case 'KeyD': input.roll = 1; break;
case 'KeyQ': input.yaw = -1; break;
case 'KeyE': input.yaw = 1; break;
case 'ShiftLeft': case 'ShiftRight': input.throttleUp = true; break;
case 'ControlLeft': case 'ControlRight': input.throttleDown = true; break;
case 'Space': input.brake = true; e.preventDefault(); break;
case 'KeyP': togglePause(); break;
case 'Escape': if(paused) { /* handled in pause */ } break;
}
});
document.addEventListener('keyup', (e) => {
switch(e.code) {
case 'KeyW': case 'KeyS': input.pitch = 0; break;
case 'KeyA': case 'KeyD': input.roll = 0; break;
case 'KeyQ': case 'KeyE': input.yaw = 0; break;
case 'ShiftLeft': case 'ShiftRight': input.throttleUp = false; break;
case 'ControlLeft': case 'ControlRight': input.throttleDown = false; break;
case 'Space': input.brake = false; break;
}
});
// ── Audio System ──
let audioCtx, engineGain, windGain, masterGain;
let engineOsc1, engineOsc2, engineNoise;
function initAudio() {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.3;
masterGain.connect(audioCtx.destination);
// Engine sound - layered oscillators + noise
engineGain = audioCtx.createGain();
engineGain.gain.value = 0;
engineGain.connect(masterGain);
engineOsc1 = audioCtx.createOscillator();
engineOsc1.type = 'sawtooth';
engineOsc1.frequency.value = 80;
const osc1Gain = audioCtx.createGain();
osc1Gain.gain.value = 0.3;
engineOsc1.connect(osc1Gain);
osc1Gain.connect(engineGain);
engineOsc1.start();
engineOsc2 = audioCtx.createOscillator();
engineOsc2.type = 'square';
engineOsc2.frequency.value = 40;
const osc2Gain = audioCtx.createGain();
osc2Gain.gain.value = 0.15;
engineOsc2.connect(osc2Gain);
osc2Gain.connect(engineGain);
engineOsc2.start();
// Engine noise (filtered white noise)
const bufferSize = audioCtx.sampleRate * 2;
const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) data[i] = Math.random() * 2 - 1;
engineNoise = audioCtx.createBufferSource();
engineNoise.buffer = noiseBuffer;
engineNoise.loop = true;
const noiseFilter = audioCtx.createBiquadFilter();
noiseFilter.type = 'lowpass';
noiseFilter.frequency.value = 200;
const noiseGain = audioCtx.createGain();
noiseGain.gain.value = 0.4;
engineNoise.connect(noiseFilter);
noiseFilter.connect(noiseGain);
noiseGain.connect(engineGain);
engineNoise.start();
// Wind sound
windGain = audioCtx.createGain();
windGain.gain.value = 0;
const windBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const windData = windBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) windData[i] = Math.random() * 2 - 1;
const windSource = audioCtx.createBufferSource();
windSource.buffer = windBuffer;
windSource.loop = true;
const windFilter = audioCtx.createBiquadFilter();
windFilter.type = 'bandpass';
windFilter.frequency.value = 500;
windFilter.Q.value = 0.5;
windSource.connect(windFilter);
windFilter.connect(windGain);
windSource.start();
// Wind connects to master directly (not engine gain)
windGain.connect(masterGain);
} catch(e) { console.warn('Audio init failed:', e); }
}
function updateAudio() {
if (!audioCtx || audioCtx.state === 'suspended') return;
const ac = AIRCRAFT[selectedAircraft];
const throttleRatio = physics.throttleCurrent / (ac.maxThrust - ac.thrustIdle);
const speedFactor = Math.min(physics.airspeed / ac.maxSpeed, 1);
// Engine pitch and volume based on throttle + RPM
const baseFreq = 40 + throttleRatio * 120;
engineOsc1.frequency.setTargetAtTime(baseFreq, audioCtx.currentTime, 0.1);
engineOsc2.frequency.setTargetAtTime(baseFreq * 0.5, audioCtx.currentTime, 0.1);
const vol = Math.max(0.05, throttleRatio * 0.8 + speedFactor * 0.2);
engineGain.gain.setTargetAtTime(vol, audioCtx.currentTime, 0.1);
// Wind noise based on airspeed
const windVol = Math.min(speedFactor * 0.6, 0.5);
windGain.gain.setTargetAtTime(windVol, audioCtx.currentTime, 0.2);
}
// ── Dogfight Enemies ──
let enemies = [];
const ENEMY_COUNT = 4;
function spawnEnemies() {
for (let i = 0; i < ENEMY_COUNT; i++) {
const model = buildEnemyModel();
const angle = (i / ENEMY_COUNT) * Math.PI * 2;
const dist = 1500 + Math.random() * 1000;
model.position.set(
physics.position.x + Math.cos(angle) * dist,
300 + Math.random() * 800,
physics.position.z + Math.sin(angle) * dist
);
scene.add(model);
enemies.push({
mesh: model,
velocity: new THREE.Vector3(
(Math.random() - 0.5) * 100,
(Math.random() - 0.5) * 20,
(Math.random() - 0.5) * 100
),
health: 100,
alive: true,
targetAngle: Math.random() * Math.PI * 2,
turnTimer: 0,
altitude: model.position.y
});
}
}
function updateEnemies(dt) {
const playerPos = physics.position;
enemies.forEach(enemy => {
if (!enemy.alive) return;
enemy.turnTimer -= dt;
if (enemy.turnTimer <= 0) {
// Pick new target direction - sometimes chase player, sometimes patrol
if (Math.random() < 0.4) {
const toPlayer = playerPos.clone().sub(enemy.mesh.position).normalize();
enemy.targetAngle = Math.atan2(toPlayer.x, toPlayer.z);
} else {
enemy.targetAngle += (Math.random() - 0.5) * 1.5;
}
enemy.turnTimer = 2 + Math.random() * 4;
// Vary altitude
if (Math.random() < 0.3) {
enemy.altitude = 200 + Math.random() * 1000;
}
}
const speed = 80 + Math.random() * 40;
const dirX = Math.sin(enemy.targetAngle);
const dirZ = Math.cos(enemy.targetAngle);
// Smooth velocity change
enemy.velocity.x += (dirX * speed - enemy.velocity.x) * dt * 2;
enemy.velocity.z += (dirZ * speed - enemy.velocity.z) * dt * 2;
enemy.velocity.y += ((enemy.altitude - enemy.mesh.position.y) * 0.5 - enemy.velocity.y) * dt;
enemy.mesh.position.add(enemy.velocity.clone().multiplyScalar(dt));
// Orient mesh to velocity direction
const velDir = enemy.velocity.clone().normalize();
if (velDir.length() > 0.1) {
const targetQuat = new THREE.Quaternion();
const lookMatrix = new THREE.Matrix4();
lookMatrix.lookAt(enemy.mesh.position, enemy.mesh.position.clone().add(velDir), new THREE.Vector3(0, 1, 0));
targetQuat.setFromRotationMatrix(lookMatrix);
enemy.mesh.quaternion.slerp(targetQuat, dt * 3);
}
// Keep above terrain
const th = getTerrainHeight(enemy.mesh.position.x, enemy.mesh.position.z);
if (enemy.mesh.position.y < th + 50) {
enemy.velocity.y += 100;
enemy.altitude = Math.max(enemy.altitude, th + 200);
}
// Collision with player
const distToPlayer = enemy.mesh.position.distanceTo(playerPos);
if (distToPlayer < 30) {
physics.health -= 5 * dt;
document.getElementById('damage-overlay').classList.add('hit');
setTimeout(() => document.getElementById('damage-overlay').classList.remove('hit'), 200);
// Push player away
const pushDir = playerPos.clone().sub(enemy.mesh.position).normalize();
physics.velocity.add(pushDir.multiplyScalar(50));
}
if (physics.health <= 0) {
enemy.alive = false;
scene.remove(enemy.mesh);
}
});
}
// ── Weapon System ──
let missiles = [];
let canFire = true;
let fireCooldown = 0;
function fireWeapon() {
if (!canFire || fireCooldown > 0) return;
fireCooldown = 1.5;
canFire = false;
// Create missile trail
const missileGeo = new THREE.SphereGeometry(2);
const missileMat = new THREE.MeshStandardMaterial({ color: 0xffaa00, emissive: 0xff6600, emissiveIntensity: 3 });
const missile = new THREE.Mesh(missileGeo, missileMat);
// Start from aircraft position + forward offset
const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.quaternion);
missile.position.copy(physics.position).add(forward.multiplyScalar(20));
scene.add(missile);
missiles.push({
mesh: missile,
velocity: forward.clone().multiplyScalar(400),
life: 5.0
});
}
document.addEventListener('mousedown', () => {
if (selectedScenario === 'dogfight' && gameRunning && !paused) fireWeapon();
});
function updateMissiles(dt) {
for (let i = missiles.length - 1; i >= 0; i--) {
const m = missiles[i];
m.mesh.position.add(m.velocity.clone().multiplyScalar(dt));
m.life -= dt;
// Check hit enemies
let hit = false;
enemies.forEach(enemy => {
if (!enemy.alive) return;
if (m.mesh.position.distanceTo(enemy.mesh.position) < 40) {
enemy.health -= 50;
if (enemy.health <= 0) {
enemy.alive = false;
scene.remove(enemy.mesh);
score += 100;
// Explosion effect
createExplosion(enemy.mesh.position.clone());
} else {
createExplosion(m.mesh.position.clone(), true);
}
hit = true;
}
});
if (hit || m.life <= 0) {
scene.remove(m.mesh);
missiles.splice(i, 1);
}
}
}
function createExplosion(pos, small) {
const count = small ? 8 : 20;
for (let i = 0; i < count; i++) {
const geo = new THREE.SphereGeometry(small ? 1 : 3);
const mat = new THREE.MeshStandardMaterial({
color: Math.random() > 0.5 ? 0xff4400 : 0xffaa00,
emissive: 0xff6600, emissiveIntensity: 2
});
const particle = new THREE.Mesh(geo, mat);
particle.position.copy(pos);
scene.add(particle);
const vel = new THREE.Vector3(
(Math.random() - 0.5) * 100,
Math.random() * 80,
(Math.random() - 0.5) * 100
);
setTimeout(() => scene.remove(particle), small ? 300 : 800);
// Animate particle
let t = 0;
const anim = () => {
t += 0.016;
particle.position.add(vel.clone().multiplyScalar(0.016));
vel.y -= 20 * 0.016;
particle.scale.multiplyScalar(0.97);
if (t < (small ? 0.3 : 0.8)) requestAnimationFrame(anim);
};
anim();
}
}
// ── Landing Score System ──
let landingScore = null;
let hasLanded = false;
function evaluateLanding() {
if (hasLanded) return;
const ac = AIRCRAFT[selectedAircraft];
// Check if on runway and slow enough
const distFromCenter = Math.sqrt(physics.position.x * physics.position.x + physics.position.z * physics.position.z);
const isOnRunway = Math.abs(physics.position.x) < 30 && Math.abs(physics.position.z) < 1250;
if (isOnRunway && physics.onGround && physics.airspeed < ac.stallSpeed + 10) {
hasLanded = true;
let grade, points;
const speedFactor = Math.max(0, 1 - physics.airspeed / (ac.stallSpeed * 2));
const vertSpeedFactor = Math.max(0, 1 - Math.abs(physics.verticalSpeed) / 5);
const alignmentFactor = Math.max(0, 1 - Math.abs(physics.position.x) / 30);
points = (speedFactor * 40 + vertSpeedFactor * 30 + alignmentFactor * 30).toFixed(0);
if (points >= 90) grade = 'A+';
else if (points >= 80) grade = 'A';
else if (points >= 70) grade = 'B';
else if (points >= 60) grade = 'C';
else if (points >= 40) grade = 'D';
else grade = 'F';
landingScore = { grade, points };
setTimeout(() => {
document.getElementById('landing-grade').textContent = grade;
document.getElementById('landing-details').textContent = `Score: ${points}/100 | Speed: ${physics.airspeed.toFixed(1)} m/s | Vertical: ${Math.abs(physics.verticalSpeed).toFixed(1)} m/s`;
document.getElementById('landing-score').classList.add('active');
}, 2000);
}
// Crash detection
if (physics.onGround && physics.airspeed > ac.stallSpeed * 3) {
hasLanded = true;
createExplosion(physics.position.clone());
setTimeout(() => {
document.getElementById('landing-grade').textContent = 'CRASH';
document.getElementById('landing-grade').style.color = '#f00';
document.getElementById('landing-details').textContent = `Impact speed: ${physics.airspeed.toFixed(1)} m/s`;
document.getElementById('landing-score').classList.add('active');
}, 1500);
}
}
// ── Physics Engine ──
function updatePhysics(dt) {
const ac = AIRCRAFT[selectedAircraft];
dt = Math.min(dt, 0.033); // Cap delta time
// Air density based on altitude (simplified barometric formula)
const seaLevelDensity = 1.225;
const airDensity = seaLevelDensity * Math.exp(-physics.altitude / 8500);
// ── Throttle Response ──
if (input.throttleUp && physics.throttleTarget < 1) {
physics.throttleTarget += dt / ac.engineResponseTime;
}
if (input.throttleDown && physics.throttleTarget > 0) {
physics.throttleTarget -= dt / ac.engineResponseTime;
}
physics.throttleTarget = Math.max(0, Math.min(1, physics.throttleTarget));
// Smooth throttle transition
const thrustDiff = physics.throttleTarget - physics.throttleCurrent;
physics.throttleCurrent += thrustDiff * dt * (1 / ac.engineResponseTime);
// Current thrust force
const currentThrust = ac.thrustIdle + (ac.maxThrust - ac.thrustIdle) * physics.throttleCurrent;
// ── Transform basis vectors ──
const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.quaternion);
const up = new THREE.Vector3(0, 1, 0).applyQuaternion(physics.quaternion);
const right = new THREE.Vector3(0, 0, 1).applyQuaternion(physics.quaternion);
// ── Airspeed ──
physics.airspeed = physics.velocity.length();
// ── Angle of Attack ──
if (physics.airspeed > 1) {
const velDir = physics.velocity.clone().normalize();
physics.angleOfAttack = Math.asin(Math.max(-1, Math.min(1, -forward.dot(velDir))));
}
// ── Lift Calculation ──
let cl = 0;
if (physics.airspeed > 5) {
const aoaRad = physics.angleOfAttack;
// Linear lift region
if (Math.abs(aoaRad) < 0.3) {
cl = 2 * Math.PI * aoaRad + 0.2; // Cl_alpha ~ 2π for thin airfoil
} else if (Math.abs(aoaRad) < 0.5) {
// Approaching stall - lift peaks then drops
const stallFactor = 1 - ((Math.abs(aoaRad) - 0.3) / 0.2);
cl = ac.clMax * stallFactor;
} else {
// Post-stall
cl = ac.clMax * 0.3;
}
physics.isStalled = Math.abs(aoaRad) > 0.4 && !physics.onGround;
}
const liftMagnitude = 0.5 * airDensity * physics.airspeed * physics.airspeed * ac.wingArea * cl;
const liftVector = up.clone().multiplyScalar(liftMagnitude);
// ── Drag Calculation ──
const cdProfile = ac.cd0;
const cdInduced = ac.kInduced * cl * cl;
const totalCd = cdProfile + cdInduced;
const dragMagnitude = 0.5 * airDensity * physics.airspeed * physics.airspeed * ac.wingArea * totalCd;
let dragVector = new THREE.Vector3();
if (physics.airspeed > 1) {
dragVector = physics.velocity.clone().normalize().multiplyScalar(-dragMagnitude);
}
// ── Weight ──
const weightVector = new THREE.Vector3(0, -ac.mass * 9.81, 0);
// ── Thrust Vector ──
const thrustVector = forward.clone().multiplyScalar(currentThrust);
// ── Ground friction (when on ground) ──
let groundFriction = new THREE.Vector3();
if (physics.onGround) {
const horizontalVel = new THREE.Vector3(physics.velocity.x, 0, physics.velocity.z);
if (horizontalVel.length() > 1) {
groundFriction = horizontalVel.clone().normalize().multiplyScalar(-ac.mass * 9.81 * 0.4);
// Braking
if (input.brake) {
groundFriction.multiplyScalar(2.5);
}
}
}
// ── Net Force ──
const netForce = new THREE.Vector3();
netForce.add(liftVector);
netForce.add(dragVector);
netForce.add(weightVector);
netForce.add(thrustVector);
netForce.add(groundFriction);
// ── Acceleration & Velocity Update ──
const acceleration = netForce.clone().divideScalar(ac.mass);
physics.velocity.add(acceleration.multiplyScalar(dt));
// ── Position Update ──
physics.position.add(physics.velocity.clone().multiplyScalar(dt));
// ── Terrain Collision ──
const terrainHeight = getTerrainHeight(physics.position.x, physics.position.z);
if (physics.position.y < terrainHeight + 3) {
physics.position.y = terrainHeight + 3;
physics.velocity.y = Math.max(0, physics.velocity.y);
if (!physics.onGround && physics.airspeed > ac.stallSpeed * 2) {
// Hard crash
physics.health -= (physics.airspeed - ac.stallSpeed * 2) * 5;
}
physics.onGround = true;
physics.verticalSpeed = 0;
} else {
if (physics.onGround && currentThrust > ac.mass * 9.81 * 0.7 && physics.airspeed > ac.stallSpeed * 0.5) {
physics.onGround = false;
}
// Also take off with enough lift
if (physics.onGround && liftMagnitude > ac.mass * 9.81 * 0.9) {
physics.onGround = false;
}
}
physics.altitude = physics.position.y - terrainHeight;
physics.verticalSpeed = physics.velocity.dot(new THREE.Vector3(0, 1, 0));
// ── Rotational Dynamics (Inertia-based) ──
const controlAuthority = Math.min(physics.airspeed / 50, 1); // Controls less effective at low speed
// Target angular rates from input
const targetRollRate = -input.roll * ac.rollRate * controlAuthority;
const targetPitchRate = -input.pitch * ac.pitchRate * controlAuthority;
const targetYawRate = input.yaw * ac.yawRate * controlAuthority;
// Smooth angular velocity changes (inertia)
physics.angularVelocity.x += (targetRollRate - physics.angularVelocity.x) * dt * 3;
physics.angularVelocity.y += (targetYawRate - physics.angularVelocity.y) * dt * 2;
physics.angularVelocity.z += (targetPitchRate - physics.angularVelocity.z) * dt * 3;
// Apply angular damping
physics.angularVelocity.multiplyScalar(0.95);
// Convert angular velocity to quaternion rotation
const rotQuat = new THREE.Quaternion();
const angVelScaled = physics.angularVelocity.clone().multiplyScalar(dt);
rotQuat.setFromAxisAngle(
physics.angularVelocity.clone().normalize(),
physics.angularVelocity.length() * dt
);
// Apply rotation: roll around forward, pitch around right, yaw around up
const rollQuat = new THREE.Quaternion().setFromAxisAngle(forward, -input.roll * ac.rollRate * controlAuthority * dt);
const pitchQuat = new THREE.Quaternion().setFromAxisAngle(right, -input.pitch * ac.pitchRate * controlAuthority * dt);
const yawQuat = new THREE.Quaternion().setFromAxisAngle(up, input.yaw * ac.yawRate * controlAuthority * dt);
physics.quaternion.premultiply(rollQuat);
physics.quaternion.premultiply(pitchQuat);
physics.quaternion.premultiply(yawQuat);
physics.quaternion.normalize();
// ── G-Force Calculation ──
const totalAccel = acceleration.length();
physics.gForce = (totalAccel + 9.81) / 9.81;
// ── Auto-level when no input and not stalled ──
if (!input.pitch && !input.roll && !physics.isStalled && !physics.onGround) {
const currentUp = new THREE.Vector3(0, 1, 0).applyQuaternion(physics.quaternion);
const worldUp = new THREE.Vector3(0, 1, 0);
const levelQuat = new THREE.Quaternion().setFromUnitVectors(currentUp, worldUp);
physics.quaternion.slerp(levelQuat, dt * 0.5);
}
// ── Landing evaluation ──
if (selectedScenario === 'landing') {
evaluateLanding();
}
}
// ── Camera System ──
const cameraOffset = new THREE.Vector3(-40, 12, 0);
let cameraSmoothPos = new THREE.Vector3();
let cameraSmoothTarget = new THREE.Vector3();
function updateCamera() {
// Calculate desired camera position relative to aircraft
const camLocalOffset = cameraOffset.clone().applyQuaternion(physics.quaternion);
const desiredCamPos = physics.position.clone().add(camLocalOffset);
// Smooth follow
cameraSmoothPos.lerp(desiredCamPos, 0.08);
camera.position.copy(cameraSmoothPos);
// Look slightly ahead of aircraft
const lookAhead = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.quaternion).multiplyScalar(50);
cameraSmoothTarget.lerp(physics.position.clone().add(lookAhead), 0.1);
camera.lookAt(cameraSmoothTarget);
// FOV based on speed (warp effect at high speed)
const ac = AIRCRAFT[selectedAircraft];
const speedRatio = physics.airspeed / ac.maxSpeed;
camera.fov = 65 + speedRatio * 15;
camera.updateProjectionMatrix();
}
// ── HUD Update ──
function updateHUD() {
const ac = AIRCRAFT[selectedAircraft];
const kts = (physics.airspeed * 1.943).toFixed(0);
const altFt = (physics.altitude * 3.281).toFixed(0);
const vsFpm = (physics.verticalSpeed * 196.85).toFixed(0);
const heading = ((Math.atan2(physics.velocity.x, physics.velocity.z) * 180 / Math.PI + 360) % 360).toFixed(0);
document.getElementById('hud-left').innerHTML = `
<div style="font-size:1.4em;font-weight:bold;">${kts} KTS</div>
<div>AIRSPEED</div>
<br>
<div style="font-size:1.3em;">${altFt} FT</div>
<div>ALTITUDE</div>
<br>
<div>${vsFpm > 0 ? '+' : ''}${vsFpm} FPM</div>
<div>V/SPEED</div>
`;
document.getElementById('hud-right').innerHTML = `
<div style="font-size:1.3em;">HDG ${heading}°</div>
<br>
<div>G: ${physics.gForce.toFixed(1)}</div>
<br>
<div>THR: ${(physics.throttleCurrent * 100).toFixed(0)}%</div>
<br>
<div>${ac.name}</div>
<div style="color:${physics.health > 50 ? '#0f0' : '#f00'};">HP: ${Math.max(0, physics.health).toFixed(0)}%</div>
`;
// Bottom status bar
let bottomText = '';
if (physics.isStalled) {
document.getElementById('stall-warning').style.display = 'block';
} else {
document.getElementById('stall-warning').style.display = 'none';
}
if (selectedScenario === 'dogfight') {
const aliveCount = enemies.filter(e => e.alive).length;
bottomText = `ENEMIES: ${aliveCount}/${ENEMY_COUNT} | SCORE: ${score}`;
} else {
bottomText = physics.onGround ? 'ON GROUND' : 'AIRBORNE';
}
document.getElementById('hud-bottom').textContent = bottomText;
// G-meter
const gPercent = Math.min(100, ((physics.gForce - 1) / (ac.gLimit - 1)) * 100);
document.getElementById('g-meter-fill').style.height = `${Math.max(0, gPercent)}%`;
// Horizon line rotation
const horizonLine = document.getElementById('horizon-line');
const pitchDeg = physics.angleOfAttack * 180 / Math.PI;
horizonLine.style.transform = `rotate(${-pitchDeg}deg)`;
// Score display for dogfight
if (selectedScenario === 'dogfight') {
document.getElementById('score-display').textContent = `SCORE: ${score}`;
} else {
document.getElementById('score-display').textContent = '';
}
// Target reticle for dogfight
const targetReticle = document.getElementById('target-reticle');
if (selectedScenario === 'dogfight') {
let closestDist = Infinity;
enemies.forEach(e => {
if (!e.alive) return;
const d = e.mesh.position.distanceTo(physics.position);
if (d < closestDist) closestDist = d;
});
if (closestDist < 800) {
targetReticle.style.display = 'block';
targetReticle.classList.toggle('locked', closestDist < 300);
} else {
targetReticle.style.display = 'none';
}
} else {
targetReticle.style.display = 'none';
}
// Minimap
updateMinimap();
}
function updateMinimap() {
const canvas = document.getElementById('minimap-canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 150, 150);
// Background
ctx.fillStyle = 'rgba(0,30,0,0.8)';
ctx.fillRect(0, 0, 150, 150);
const scale = 0.02;
const cx = 75, cy = 75;
// Draw terrain contour (simplified)
for (let x = -60; x < 60; x += 8) {
for (let z = -60; z < 60; z += 8) {
const worldX = physics.position.x + x / scale;
const worldZ = physics.position.z + z / scale;
const h = getTerrainHeight(worldX, worldZ);
const brightness = Math.min(255, (h / 1000) * 255);
ctx.fillStyle = `rgb(${brightness*0.3},${brightness*0.6},${brightness*0.2})`;
ctx.fillRect(cx + x, cy + z, 7, 7);
}
}
// Draw runway
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1;
const rwDistZ = (-1250 - physics.position.z) * scale;
if (Math.abs(rwDistZ) < 60) {
ctx.beginPath();
ctx.moveTo(cx, cy + Math.max(-60, rwDistZ));
ctx.lineTo(cx, cy + Math.min(60, rwDistZ + 2500 * scale));
ctx.stroke();
}
// Draw enemies (dogfight)
if (selectedScenario === 'dogfight') {
enemies.forEach(e => {
if (!e.alive) return;
const dx = (e.mesh.position.x - physics.position.x) * scale;
const dz = (e.mesh.position.z - physics.position.z) * scale;
if (Math.abs(dx) < 60 && Math.abs(dz) < 60) {
ctx.fillStyle = '#f00';
ctx.beginPath();
ctx.arc(cx + dx, cy + dz, 3, 0, Math.PI * 2);
ctx.fill();
}
});
}
// Player position (center)
ctx.fillStyle = '#0f0';
ctx.beginPath();
ctx.arc(cx, cy, 4, 0, Math.PI * 2);
ctx.fill();
// Direction indicator
const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.quaternion);
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + forward.x * 15, cy + forward.z * 15);
ctx.stroke();
// Border circle
ctx.strokeStyle = 'rgba(0,255,0,0.4)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(cx, cy, 73, 0, Math.PI * 2);
ctx.stroke();
}
// ── Aircraft Mesh Update ──
let aircraftMesh;
function updateAircraftVisuals() {
if (!aircraftMesh) return;
// Position and rotation
aircraftMesh.position.copy(physics.position);
aircraftMesh.quaternion.copy(physics.quaternion);
// Exhaust glow based on throttle
const ac = AIRCRAFT[selectedAircraft];
aircraftMesh.traverse(child => {
if (child.name === 'exhaust') {
child.material.emissiveIntensity = physics.throttleCurrent * 3;
}
});
}
// ── Pause Toggle ──
function togglePause() {
paused = !paused;
document.getElementById('pause-overlay').classList.toggle('active', paused);
}
// ── Menu Setup ──
function setupMenu() {
const acSelect = document.getElementById('aircraft-select');
Object.entries(AIRCRAFT).forEach(([key, ac]) => {
const card = document.createElement('div');
card.className = 'ac-card' + (key === selectedAircraft ? ' selected' : '');
card.innerHTML = `
<div class="icon">${ac.icon}</div>
<h3>${ac.name}</h3>
<p>${ac.desc}</p>
<p style="margin-top:0.5rem;color:#666;">Max Speed: ${ac.maxSpeed} m/s | Weight: ${(ac.mass/1000).toFixed(0)}t</p>
`;
card.addEventListener('click', () => {
document.querySelectorAll('.ac-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedAircraft = key;
});
acSelect.appendChild(card);
});
const scSelect = document.getElementById('scenario-select');
Object.entries(SCENARIOS).forEach(([key, sc]) => {
const card = document.createElement('div');
card.className = 'sc-card' + (key === selectedScenario ? ' selected' : '');
card.innerHTML = `
<div class="icon">${sc.icon}</div>
<h3>${sc.name}</h3>
<p>${sc.desc}</p>
`;
card.addEventListener('click', () => {
document.querySelectorAll('.sc-card').forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
selectedScenario = key;
});
scSelect.appendChild(card);
});
document.getElementById('start-btn').addEventListener('click', startGame);
}
// ── Start Game ──
function startGame() {
document.getElementById('menu-screen').classList.add('hidden');
document.getElementById('hud').classList.add('active');
// Reset physics state
physics.position.set(0, 15, -1200);
physics.velocity.set(0, 0, 0);
physics.quaternion.identity();
physics.angularVelocity.set(0, 0, 0);
physics.throttleTarget = 0;
physics.throttleCurrent = 0;
physics.health = 100;
physics.onGround = true;
hasLanded = false;
score = 0;
// Build aircraft model
if (aircraftMesh) scene.remove(aircraftMesh);
aircraftMesh = buildAircraftModel(selectedAircraft);
scene.add(aircraftMesh);
// Spawn enemies for dogfight
enemies.forEach(e => { if (e.mesh && e.mesh.parent) scene.remove(e.mesh); });
enemies = [];
missiles.forEach(m => scene.remove(m.mesh));
missiles = [];
if (selectedScenario === 'dogfight') {
spawnEnemies();
}
// Init audio
initAudio();
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
gameRunning = true;
}
// ── Main Game Loop ──
let lastTime = performance.now();
function gameLoop(time) {
requestAnimationFrame(gameLoop);
const dt = Math.min((time - lastTime) / 1000, 0.05);
lastTime = time;
if (!gameRunning || paused) {
renderer.render(scene, camera);
return;
}
// Update physics
updatePhysics(dt);
// Update visuals
updateAircraftVisuals();
updateCamera();
// Dogfight updates
if (selectedScenario === 'dogfight') {
fireCooldown -= dt;
if (fireCooldown <= 0) canFire = true;
updateEnemies(dt);
updateMissiles(dt);
// Respawn enemies if all dead
const aliveCount = enemies.filter(e => e.alive).length;
if (aliveCount === 0 && score > 0) {
setTimeout(() => spawnEnemies(), 3000);
}
}
// Update HUD
updateHUD();
// Update audio
updateAudio();
// Render
renderer.render(scene, camera);
}
// ── Window Resize ──
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
// ── Initialize ──
function init() {
setupMenu();
document.getElementById('menu-screen').classList.remove('hidden');
document.getElementById('loading-screen').style.display = 'none';
requestAnimationFrame(gameLoop);
}
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"middle","sessionId":"e5527513-7e77-44c1-a691-4e379174b60d","tokensIn":23853,"tokensOut":19974,"tokensTotal":43827,"turns":2,"toolCalls":1,"failedToolCalls":0,"timestamp":"2026-07-18T22:48:06.980Z"} -->