Flight Simulator
thinkingcap qwen3.6 27b · full

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 57,481 bytes · SHA-256 dc94b8f4c2a3
<!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; }
/* Loading Screen */
#loading-screen {
position: fixed; inset: 0; z-index: 1000;
background: linear-gradient(135deg, #0a0a2e 0%, #1a1a4e 50%, #0d0d3b 100%);
display: flex; flex-direction: column; align-items: center; justify-content: center;
transition: opacity 0.8s ease;
}
#loading-screen.hidden { opacity: 0; pointer-events: none; }
#loading-screen h1 {
font-size: 4em; color: #fff; text-shadow: 0 0 30px rgba(0,150,255,0.8);
letter-spacing: 8px; margin-bottom: 10px;
}
#loading-screen .subtitle { color: #6af; font-size: 1.2em; letter-spacing: 4px; margin-bottom: 40px; }
#load-bar-outer { width: 300px; height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden; }
#load-bar-inner { width: 0%; height: 100%; background: linear-gradient(90deg, #0af, #f0a); transition: width 0.3s; }
/* Menu Screen */
#menu-screen {
position: fixed; inset: 0; z-index: 900;
background: linear-gradient(135deg, rgba(5,5,25,0.97), rgba(15,15,45,0.97));
display: none; flex-direction: column; align-items: center; justify-content: center;
}
#menu-screen.active { display: flex; }
.menu-title { font-size: 3em; color: #fff; text-shadow: 0 0 20px rgba(0,150,255,0.6); letter-spacing: 6px; margin-bottom: 8px; }
.menu-subtitle { color: #4af; font-size: 1em; letter-spacing: 3px; margin-bottom: 40px; }
.aircraft-select { display: flex; gap: 20px; margin-bottom: 30px; }
.ac-card {
width: 200px; padding: 20px; 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;
}
.ac-card:hover { border-color: #4af; background: rgba(70,170,255,0.1); transform: translateY(-4px); }
.ac-card.selected { border-color: #0f0; background: rgba(0,255,100,0.1); box-shadow: 0 0 20px rgba(0,255,100,0.3); }
.ac-card .ac-icon { font-size: 3em; margin-bottom: 8px; }
.ac-card .ac-name { color: #fff; font-size: 1.1em; font-weight: bold; margin-bottom: 4px; }
.ac-card .ac-type { color: #aaa; font-size: 0.8em; margin-bottom: 12px; }
.ac-stats { text-align: left; }
.ac-stat { display: flex; justify-content: space-between; margin: 3px 0; font-size: 0.75em; color: #ccc; }
.ac-stat-bar { width: 60px; height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; overflow: hidden; }
.ac-stat-fill { height: 100%; border-radius: 2px; }
.scenario-select { display: flex; gap: 20px; margin-bottom: 30px; }
.sc-card {
width: 250px; padding: 20px; 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;
}
.sc-card:hover { border-color: #fa4; background: rgba(255,170,70,0.1); transform: translateY(-4px); }
.sc-card.selected { border-color: #fa4; background: rgba(255,170,70,0.15); box-shadow: 0 0 20px rgba(255,170,70,0.3); }
.sc-card .sc-icon { font-size: 2.5em; margin-bottom: 8px; }
.sc-card .sc-name { color: #fff; font-size: 1.1em; font-weight: bold; margin-bottom: 4px; }
.sc-card .sc-desc { color: #aaa; font-size: 0.8em; line-height: 1.4; }
#start-btn {
padding: 15px 60px; border: none; border-radius: 8px; cursor: pointer;
background: linear-gradient(90deg, #0af, #f0a); color: #fff; font-size: 1.2em;
letter-spacing: 3px; font-weight: bold; transition: all 0.3s;
}
#start-btn:hover { transform: scale(1.05); box-shadow: 0 0 30px rgba(0,170,255,0.5); }
.controls-info { color: #666; font-size: 0.75em; margin-top: 20px; text-align: center; line-height: 1.8; }
.controls-info span { color: #aaa; background: rgba(255,255,255,0.1); padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
/* HUD */
#hud {
position: fixed; inset: 0; z-index: 100; pointer-events: none; display: none;
}
#hud.active { display: block; }
.hud-speed {
position: absolute; left: 20px; top: 50%; transform: translateY(-50%);
writing-mode: vertical-rl; text-orientation: mixed; color: #0f0; font-size: 1.4em;
font-family: 'Courier New', monospace; letter-spacing: 2px;
}
.hud-alt {
position: absolute; right: 20px; top: 50%; transform: translateY(-50%);
writing-mode: vertical-rl; text-orientation: mixed; color: #0f0; font-size: 1.4em;
font-family: 'Courier New', monospace; letter-spacing: 2px;
}
.hud-top {
position: absolute; top: 15px; left: 50%; transform: translateX(-50%);
display: flex; gap: 30px; color: #0f0; font-family: 'Courier New', monospace; font-size: 0.85em;
}
.hud-top-item { text-align: center; }
.hud-top-label { color: #0a0; font-size: 0.7em; letter-spacing: 1px; }
.hud-top-val { font-size: 1.3em; margin-top: 2px; }
.hud-bottom {
position: absolute; bottom: 15px; left: 50%; transform: translateX(-50%);
display: flex; gap: 40px; color: #0f0; font-family: 'Courier New', monospace; font-size: 0.8em;
}
.hud-pitch-ladder {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
width: 300px; height: 200px; pointer-events: none;
}
.pitch-line {
position: absolute; left: 50%; transform: translateX(-50%);
height: 1px; background: rgba(0,255,0,0.4);
}
.pitch-label {
position: absolute; color: rgba(0,255,0,0.6); font-size: 0.7em;
font-family: 'Courier New', monospace; transform: translateX(-50%);
}
.hud-horizon-line {
position: absolute; left: 10%; right: 10%; top: 50%; height: 2px;
background: rgba(0,255,0,0.6); transform-origin: center;
}
.hud-center-dot {
position: absolute; left: 50%; top: 50%; width: 8px; height: 8px;
border: 2px solid #0f0; border-radius: 50%; transform: translate(-50%, -50%);
}
.hud-compass {
position: absolute; top: 15px; left: 50%; transform: translateX(-50%);
width: 200px; height: 30px; overflow: hidden; text-align: center;
color: #0f0; font-family: 'Courier New', monospace; font-size: 0.8em;
}
.hud-g-meter {
position: absolute; right: 60px; top: 50%; transform: translateY(-50%);
width: 30px; height: 120px; border: 1px solid rgba(0,255,0,0.4);
border-radius: 4px; overflow: hidden; position: relative;
}
.g-indicator {
position: absolute; left: 0; right: 0; height: 3px; background: #f00;
transition: bottom 0.1s;
}
.hud-throttle-bar {
position: absolute; left: 60px; top: 50%; transform: translateY(-50%);
width: 12px; height: 120px; border: 1px solid rgba(0,255,0,0.4);
border-radius: 4px; overflow: hidden; position: relative; background: rgba(0,0,0,0.3);
}
.throttle-fill {
position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(to top, #0f0, #ff0, #f00);
transition: height 0.1s;
}
.hud-warning {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -80px);
color: #f00; font-size: 1.5em; font-weight: bold; letter-spacing: 4px;
text-shadow: 0 0 10px rgba(255,0,0,0.8); display: none;
animation: blink-warn 0.5s infinite alternate;
}
@keyframes blink-warn { from { opacity: 1; } to { opacity: 0.3; } }
.hud-crosshair {
position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%);
width: 40px; height: 40px; display: none;
}
.hud-crosshair.active { display: block; }
.ch-line { position: absolute; background: rgba(255,0,0,0.7); }
.ch-top { top: 0; left: 50%; width: 2px; height: 12px; transform: translateX(-50%); }
.ch-bottom { bottom: 0; left: 50%; width: 2px; height: 12px; transform: translateX(-50%); }
.ch-left { left: 0; top: 50%; width: 12px; height: 2px; transform: translateY(-50%); }
.ch-right { right: 0; top: 50%; width: 12px; height: 2px; transform: translateY(-50%); }
.hud-score {
position: absolute; top: 60px; right: 20px; color: #fa4; font-family: 'Courier New', monospace;
font-size: 1em; text-align: right; display: none;
}
.hud-score.active { display: block; }
/* Damage overlay */
.damage-overlay {
position: fixed; inset: 0; z-index: 200; pointer-events: none;
border: 0px solid rgba(255,0,0,0); transition: border-width 0.3s, border-color 0.3s;
}
.damage-overlay.hit { border-width: 8px; 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;
}
#pause-overlay.active { display: flex; }
#pause-overlay h2 { color: #fff; font-size: 2em; margin-bottom: 20px; letter-spacing: 4px; }
#pause-overlay p { color: #aaa; font-size: 1em; margin: 5px 0; }
/* Minimap */
.hud-minimap {
position: absolute; bottom: 60px; right: 20px; width: 120px; height: 120px;
border: 1px solid rgba(0,255,0,0.4); border-radius: 50%; overflow: hidden;
background: rgba(0,20,0,0.6);
}
.minimap-dot { position: absolute; width: 4px; height: 4px; border-radius: 50%; }
</style>
</head>
<body>
<!-- Loading Screen -->
<div id="loading-screen">
<h1>APEX AERO</h1>
<div class="subtitle">FLIGHT SIMULATOR</div>
<div id="load-bar-outer"><div id="load-bar-inner"></div></div>
</div>
<!-- Menu Screen -->
<div id="menu-screen">
<div class="menu-title">APEX AERO</div>
<div class="menu-subtitle">SELECT YOUR AIRCRAFT & MISSION</div>
<div class="aircraft-select" id="ac-select"></div>
<div class="scenario-select" id="sc-select"></div>
<button id="start-btn">LAUNCH</button>
<div class="controls-info">
<span>W/S</span> Pitch <span>A/D</span> Roll <span>Q/E</span> Yaw<br>
<span>SHIFT</span> Throttle Up <span>CTRL</span> Throttle Down <span>TAB</span> Gear<br>
<span>F</span> Flaps <span>P</span> Pause <span>R</span> Reset Position
</div>
</div>
<!-- HUD -->
<div id="hud">
<div class="hud-speed" id="hud-speed">0 KTS</div>
<div class="hud-alt" id="hud-alt">0 FT</div>
<div class="hud-top">
<div class="hud-top-item"><div class="hud-top-label">HEADING</div><div class="hud-top-val" id="hud-hdg">000°</div></div>
<div class="hud-top-item"><div class="hud-top-label">VERT SPEED</div><div class="hud-top-val" id="hud-vs">0 FPM</div></div>
<div class="hud-top-item"><div class="hud-top-label">G-FORCE</div><div class="hud-top-val" id="hud-g">1.0 G</div></div>
<div class="hud-top-item"><div class="hud-top-label">AOA</div><div class="hud-top-val" id="hud-aoa">0°</div></div>
</div>
<div class="hud-bottom">
<div class="hud-top-item"><div class="hud-top-label">THRUST</div><div class="hud-top-val" id="hud-thrust">0%</div></div>
<div class="hud-top-item"><div class="hud-top-label">MACH</div><div class="hud-top-val" id="hud-mach">0.00</div></div>
</div>
<div class="hud-pitch-ladder" id="pitch-ladder"></div>
<div class="hud-horizon-line" id="horizon-line"></div>
<div class="hud-center-dot"></div>
<div class="hud-g-meter"><div class="g-indicator" id="g-ind"></div></div>
<div class="hud-throttle-bar"><div class="throttle-fill" id="throttle-fill"></div></div>
<div class="hud-warning" id="hud-stall">STALL</div>
<div class="hud-crosshair" id="hud-crosshair">
<div class="ch-line ch-top"></div><div class="ch-line ch-bottom"></div>
<div class="ch-line ch-left"></div><div class="ch-line ch-right"></div>
</div>
<div class="hud-score" id="hud-score">SCORE: 0</div>
<canvas class="hud-minimap" id="minimap-canvas" width="120" height="120"></canvas>
</div>
<div class="damage-overlay" id="damage-overlay"></div>
<div id="pause-overlay">
<h2>PAUSED</h2>
<p>Press P to resume</p>
<p>Press ESC for menu</p>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ============================================================
// APEX AERO - Flight Simulator
// ============================================================
(function() {
'use strict';
// ---- Aircraft Database ----
const AIRCRAFT = [
{
id: 'phoenix', name: 'Phoenix MK-I', type: 'Fighter Jet', icon: '✈️',
color: 0x4488ff, accentColor: 0xff4444,
stats: { maxSpeed: 650, thrust: 120000, weight: 12000, wingArea: 35,
liftCoeff: 1.8, dragCoeff: 0.035, turnRate: 2.5, agility: 9 },
statBars: { speed: 90, thrust: 95, maneuverability: 85, weight: 60 }
},
{
id: 'eagle', name: 'Eagle X-7', type: 'Interceptor', icon: '🦅',
color: 0x44ff88, accentColor: 0xffaa00,
stats: { maxSpeed: 520, thrust: 90000, weight: 9000, wingArea: 30,
liftCoeff: 1.6, dragCoeff: 0.028, turnRate: 3.2, agility: 10 },
statBars: { speed: 70, thrust: 75, maneuverability: 95, weight: 40 }
},
{
id: 'viper', name: 'Viper Heavy', type: 'Bomber', icon: '🔥',
color: 0xff6644, accentColor: 0x44aaff,
stats: { maxSpeed: 380, thrust: 150000, weight: 25000, wingArea: 55,
liftCoeff: 2.2, dragCoeff: 0.05, turnRate: 1.4, agility: 4 },
statBars: { speed: 45, thrust: 100, maneuverability: 30, weight: 95 }
}
];
const SCENARIOS = [
{ id: 'takeoff', name: 'Takeoff & Landing', icon: '🛫', desc: 'Master precision flight. Take off from the runway and land safely.' },
{ id: 'dogfight', name: 'Dogfight', icon: '⚔️', desc: 'Engage enemy aircraft in aerial combat. Destroy targets to score points.' }
];
// ---- Global State ----
let selectedAircraft = 0;
let selectedScenario = 0;
let gameRunning = false;
let paused = false;
let scene, camera, renderer, clock;
let terrainMesh, waterMesh;
let aircraftModel, aircraftGroup;
let physicsState = {};
let audioCtx, engineOsc, windGain, masterGain;
let keys = {};
let enemies = [];
let particles = [];
let score = 0;
let terrainSize = 8000;
let terrainRes = 256;
// ---- Loading ----
function updateLoadBar(pct) {
document.getElementById('load-bar-inner').style.width = pct + '%';
}
async function init() {
updateLoadBar(10);
await sleep(200);
// Build menu UI
buildMenu();
updateLoadBar(30);
await sleep(200);
// Init Three.js
initThreeJS();
updateLoadBar(50);
await sleep(200);
// Generate terrain
generateTerrain();
updateLoadBar(75);
await sleep(200);
// Setup lighting & sky
setupLighting();
updateLoadBar(90);
await sleep(200);
// Init audio system
initAudio();
updateLoadBar(100);
await sleep(400);
document.getElementById('loading-screen').classList.add('hidden');
document.getElementById('menu-screen').classList.add('active');
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
// ---- Menu Building ----
function buildMenu() {
const acDiv = document.getElementById('ac-select');
AIRCRAFT.forEach((ac, i) => {
const card = document.createElement('div');
card.className = 'ac-card' + (i === selectedAircraft ? ' selected' : '');
card.innerHTML = `
<div class="ac-icon">${ac.icon}</div>
<div class="ac-name">${ac.name}</div>
<div class="ac-type">${ac.type}</div>
<div class="ac-stats">
${buildStatBar('Speed', ac.statBars.speed, '#0af')}
${buildStatBar('Thrust', ac.statBars.thrust, '#f80')}
${buildStatBar('Agility', ac.statBars.maneuverability, '#0f0')}
${buildStatBar('Weight', ac.statBars.weight, '#a4f')}
</div>`;
card.onclick = () => { selectedAircraft = i; updateAcSelection(); };
acDiv.appendChild(card);
});
const scDiv = document.getElementById('sc-select');
SCENARIOS.forEach((sc, i) => {
const card = document.createElement('div');
card.className = 'sc-card' + (i === selectedScenario ? ' selected' : '');
card.innerHTML = `
<div class="sc-icon">${sc.icon}</div>
<div class="sc-name">${sc.name}</div>
<div class="sc-desc">${sc.desc}</div>`;
card.onclick = () => { selectedScenario = i; updateScSelection(); };
scDiv.appendChild(card);
});
document.getElementById('start-btn').onclick = startGame;
}
function buildStatBar(label, val, color) {
return `<div class="ac-stat"><span>${label}</span><div class="ac-stat-bar"><div class="ac-stat-fill" style="width:${val}%;background:${color}"></div></div></div>`;
}
function updateAcSelection() {
document.querySelectorAll('.ac-card').forEach((c, i) => c.classList.toggle('selected', i === selectedAircraft));
}
function updateScSelection() {
document.querySelectorAll('.sc-card').forEach((c, i) => c.classList.toggle('selected', i === selectedScenario));
}
// ---- Three.js Init ----
function initThreeJS() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x87CEEB, 0.00015);
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 1, 30000);
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);
clock = new THREE.Clock();
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
}
// ---- Lighting & Sky ----
function setupLighting() {
const ambient = new THREE.AmbientLight(0x446688, 0.5);
scene.add(ambient);
const hemi = new THREE.HemisphereLight(0x87CEEB, 0x3a5f0b, 0.6);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff4e0, 1.2);
sun.position.set(5000, 8000, 3000);
sun.castShadow = true;
sun.shadow.mapSize.set(2048, 2048);
sun.shadow.camera.left = -2000;
sun.shadow.camera.right = 2000;
sun.shadow.camera.top = 2000;
sun.shadow.camera.bottom = -2000;
sun.shadow.camera.far = 20000;
scene.add(sun);
// Sky dome with sun glow
const skyGeo = new THREE.SphereGeometry(15000, 32, 32);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color(0x001845) },
midColor: { value: new THREE.Color(0x4a90d9) },
bottomColor: { value: new THREE.Color(0xc8e6ff) },
sunDirection: { value: new THREE.Vector3(0.4, 0.55, 0.3).normalize() },
sunGlowColor: { value: new THREE.Color(0xffeedd) },
offset: { value: 20 },
exponent: { value: 0.6 }
},
vertexShader: `
varying vec3 vWorldPos;
void main() {
vec4 wp = modelMatrix * vec4(position, 1.0);
vWorldPos = wp.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 midColor;
uniform vec3 bottomColor;
uniform vec3 sunDirection;
uniform vec3 sunGlowColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPos;
void main() {
float h = normalize(vWorldPos + offset).y;
// Three-band gradient
vec3 sky;
if (h > 0.5) {
sky = mix(midColor, topColor, pow((h - 0.5) * 2.0, exponent));
} else {
sky = mix(bottomColor, midColor, pow(h * 2.0, exponent));
}
// Sun glow
vec3 dir = normalize(vWorldPos);
float sunDot = max(dot(dir, sunDirection), 0.0);
float sunGlow = pow(sunDot, 64.0) * 1.5;
float sunHalo = pow(sunDot, 8.0) * 0.3;
sky += sunGlowColor * (sunGlow + sunHalo);
gl_FragColor = vec4(clamp(sky, 0.0, 1.0), 1.0);
}`,
side: THREE.BackSide,
depthWrite: false
});
scene.add(new THREE.Mesh(skyGeo, skyMat));
// Clouds
createClouds();
}
function createClouds() {
const cloudGroup = new THREE.Group();
for (let i = 0; i < 60; i++) {
const size = 200 + Math.random() * 400;
const geo = new THREE.SphereGeometry(size, 8, 6);
const mat = new THREE.MeshStandardMaterial({
color: 0xffffff, transparent: true, opacity: 0.35 + Math.random() * 0.2,
roughness: 1, metalness: 0
});
const cloud = new THREE.Mesh(geo, mat);
cloud.position.set(
(Math.random() - 0.5) * terrainSize,
800 + Math.random() * 2000,
(Math.random() - 0.5) * terrainSize
);
cloud.scale.y = 0.3;
cloudGroup.add(cloud);
}
scene.add(cloudGroup);
}
// ---- Procedural Terrain ----
function generateTerrain() {
const geo = new THREE.PlaneGeometry(terrainSize, terrainSize, terrainRes, terrainRes);
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];
let h = getTerrainHeight(x, z);
positions[i + 1] = h;
// Vertex colors based on height
const t = Math.max(0, Math.min(1, (h + 50) / 3000));
if (t < 0.15) {
colors[i] = 0.76; colors[i+1] = 0.7; colors[i+2] = 0.5; // sand
} else if (t < 0.35) {
colors[i] = 0.2 + t; colors[i+1] = 0.45 + t * 0.5; colors[i+2] = 0.15; // grass
} else if (t < 0.6) {
colors[i] = 0.35; colors[i+1] = 0.35; colors[i+2] = 0.3; // rock
} else if (t < 0.8) {
colors[i] = 0.5; colors[i+1] = 0.48; colors[i+2] = 0.46; // snow-rock
} else {
colors[i] = 0.9; colors[i+1] = 0.92; colors[i+2] = 0.95; // snow
}
}
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geo.computeVertexNormals();
const mat = new THREE.MeshStandardMaterial({
vertexColors: true, roughness: 0.85, metalness: 0.1, flatShading: false
});
terrainMesh = new THREE.Mesh(geo, mat);
terrainMesh.receiveShadow = true;
scene.add(terrainMesh);
// Water plane
const waterGeo = new THREE.PlaneGeometry(terrainSize * 2, terrainSize * 2);
waterGeo.rotateX(-Math.PI / 2);
const waterMat = new THREE.MeshStandardMaterial({
color: 0x1a6baa, transparent: true, opacity: 0.7, roughness: 0.1, metalness: 0.3
});
waterMesh = new THREE.Mesh(waterGeo, waterMat);
waterMesh.position.y = -5;
scene.add(waterMesh);
// Runway for takeoff scenario
createRunway();
}
function getTerrainHeight(x, z) {
let h = 0;
// Multiple octaves of noise-like terrain for realistic landscape
h += Math.sin(x * 0.0008 + 0.5) * Math.cos(z * 0.0007 + 1.2) * 600;
h += Math.sin(x * 0.002 + 1.5) * Math.cos(z * 0.0018 + 2.3) * 350;
h += Math.sin(x * 0.004 + 0.3) * Math.cos(z * 0.0035 + 0.7) * 150;
h += Math.sin(x * 0.008 + 2.1) * Math.cos(z * 0.006 + 1.9) * 60;
h += (Math.sin(x * 0.016) * Math.cos(z * 0.014)) * 25;
h += (Math.sin(x * 0.03 + z * 0.025)) * 8;
// Mountain ridges with varying intensity
const ridge = Math.abs(Math.sin(x * 0.0015 + z * 0.001));
h += ridge * ridge * 1000;
// Secondary ridge system
const ridge2 = Math.abs(Math.cos(x * 0.001 - z * 0.0018 + 3.7));
h += ridge2 * ridge2 * 500;
return Math.max(-10, h);
}
function createRunway() {
const rwGeo = new THREE.PlaneGeometry(60, 2000);
rwGeo.rotateX(-Math.PI / 2);
const rwMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.9 });
const runway = new THREE.Mesh(rwGeo, rwMat);
runway.position.set(0, getTerrainHeight(0, 0) + 1, 0);
scene.add(runway);
// Runway markings
for (let i = -900; i < 950; i += 80) {
const markGeo = new THREE.PlaneGeometry(4, 30);
markGeo.rotateX(-Math.PI / 2);
const markMat = new THREE.MeshStandardMaterial({ color: 0xffffff });
const mark = new THREE.Mesh(markGeo, markMat);
mark.position.set(0, getTerrainHeight(0, 0) + 1.5, i);
scene.add(mark);
}
// Runway edge lights
for (let i = -950; i < 960; i += 40) {
[-28, 28].forEach(xOff => {
const lightGeo = new THREE.SphereGeometry(1.5);
const lightMat = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
const lightMesh = new THREE.Mesh(lightGeo, lightMat);
lightMesh.position.set(xOff, getTerrainHeight(xOff, i) + 2, i);
scene.add(lightMesh);
});
}
}
// ---- Aircraft Model Builder ----
function buildAircraftModel(acData) {
const group = new THREE.Group();
// Per-aircraft visual parameters
let fuselageLen, fuselageRad, wingSpan, tailHeight, noseLen;
if (acData.id === 'phoenix') {
fuselageLen = 16; fuselageRad = 1.2; wingSpan = 8; tailHeight = 5; noseLen = 5;
} else if (acData.id === 'eagle') {
fuselageLen = 14; fuselageRad = 1.0; wingSpan = 7; tailHeight = 4.5; noseLen = 6;
} else { // viper - heavy bomber
fuselageLen = 20; fuselageRad = 1.8; wingSpan = 12; tailHeight = 6; noseLen = 4;
}
// Fuselage with tapered shape
const fuselageGeo = new THREE.CylinderGeometry(fuselageRad * 0.7, fuselageRad, fuselageLen, 8);
fuselageGeo.rotateZ(Math.PI / 2);
const fuselageMat = new THREE.MeshStandardMaterial({
color: acData.color, roughness: 0.3, metalness: 0.7
});
group.add(new THREE.Mesh(fuselageGeo, fuselageMat));
// Nose cone
const noseGeo = new THREE.ConeGeometry(fuselageRad, noseLen, 8);
noseGeo.rotateZ(-Math.PI / 2);
const noseMesh = new THREE.Mesh(noseGeo, fuselageMat);
noseMesh.position.x = (fuselageLen + noseLen) / 2;
group.add(noseMesh);
// Wings - swept design
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.lineTo(-4, -wingSpan);
wingShape.lineTo(-1.5, -(wingSpan + 1));
wingShape.lineTo(3, 0);
wingShape.closePath();
const wingExtrudeSettings = { depth: 0.3, bevelEnabled: true, bevelThickness: 0.1, bevelSize: 0.1 };
const wingGeo = new THREE.ExtrudeGeometry(wingShape, wingExtrudeSettings);
const wingMat = new THREE.MeshStandardMaterial({ color: acData.color, roughness: 0.4, metalness: 0.6 });
const leftWing = new THREE.Mesh(wingGeo, wingMat);
leftWing.position.set(-2, -fuselageRad * 1.2, 0);
group.add(leftWing);
const rightWing = new THREE.Mesh(wingGeo.clone(), wingMat);
rightWing.position.set(-2, fuselageRad * 1.2, 0);
rightWing.rotation.x = Math.PI;
group.add(rightWing);
// Wing tip details (navigation lights)
[-wingSpan - 1, wingSpan + 1].forEach(zOff => {
const tipGeo = new THREE.SphereGeometry(0.3);
const tipMat = new THREE.MeshBasicMaterial({ color: zOff > 0 ? 0x00ff00 : 0xff0000 });
const tip = new THREE.Mesh(tipGeo, tipMat);
tip.position.set(-1.5, fuselageRad * 1.2, zOff);
group.add(tip);
});
// Tail wings (horizontal stabilizer)
const tailWingShape = new THREE.Shape();
tailWingShape.moveTo(0, 0);
tailWingShape.lineTo(-3, -wingSpan * 0.5);
tailWingShape.lineTo(-1.5, -(wingSpan * 0.5 + 0.5));
tailWingShape.lineTo(2, 0);
tailWingShape.closePath();
const twGeo = new THREE.ExtrudeGeometry(tailWingShape, { depth: 0.2, bevelEnabled: false });
const lTail = new THREE.Mesh(twGeo, wingMat);
lTail.position.set(-fuselageLen * 0.5, -fuselageRad * 0.8, 0);
group.add(lTail);
const rTail = new THREE.Mesh(twGeo.clone(), wingMat);
rTail.position.set(-fuselageLen * 0.5, fuselageRad * 0.8, 0);
rTail.rotation.x = Math.PI;
group.add(rTail);
// Vertical stabilizer
const vStabShape = new THREE.Shape();
vStabShape.moveTo(0, 0);
vStabShape.lineTo(-3, tailHeight);
vStabShape.lineTo(-1.5, tailHeight + 0.5);
vStabShape.lineTo(2, 0);
vStabShape.closePath();
const vsGeo = new THREE.ExtrudeGeometry(vStabShape, { depth: 0.2, bevelEnabled: false });
const vStab = new THREE.Mesh(vsGeo, wingMat);
vStab.position.set(-fuselageLen * 0.5, 0, -fuselageRad * 0.8);
vStab.rotation.x = Math.PI / 2;
group.add(vStab);
// Engine exhaust glow (dual for bomber)
const numEngines = acData.id === 'viper' ? 2 : 1;
for (let e = 0; e < numEngines; e++) {
const zOff = numEngines > 1 ? (e === 0 ? -3 : 3) : 0;
const exhaustGeo = new THREE.CylinderGeometry(fuselageRad * 0.4, fuselageRad * 0.6, 3, 8);
exhaustGeo.rotateZ(Math.PI / 2);
const exhaustMat = new THREE.MeshBasicMaterial({ color: acData.accentColor, transparent: true, opacity: 0.5 });
const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);
exhaust.position.set(-(fuselageLen + 1) / 2 - 1, zOff * 0.3, zOff);
exhaust.name = 'exhaust';
group.add(exhaust);
const exLight = new THREE.PointLight(acData.accentColor, 2, 30);
exLight.position.set(-(fuselageLen + 1) / 2 - 3, zOff * 0.3, zOff);
exLight.name = 'exhaustLight';
group.add(exLight);
}
// Cockpit canopy
const cockpitGeo = new THREE.SphereGeometry(fuselageRad * 1.1, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
const cockpitMat = new THREE.MeshStandardMaterial({
color: 0x88ccff, transparent: true, opacity: 0.5, roughness: 0.1, metalness: 0.9
});
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(fuselageLen * 0.3, fuselageRad * 0.8, 0);
group.add(cockpit);
// Landing gear (retractable)
if (acData.id === 'viper') {
// Triple gear for heavy bomber
[[-5, -fuselageRad * 1.5, -3], [-5, -fuselageRad * 1.5, 3], [6, -fuselageRad * 1.2, 0]].forEach(pos => {
const strutGeo = new THREE.CylinderGeometry(0.15, 0.15, 2);
const strutMat = new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.8 });
const strut = new THREE.Mesh(strutGeo, strutMat);
strut.position.set(pos[0], pos[1] - 1, pos[2]);
group.add(strut);
const wheelGeo = new THREE.CylinderGeometry(0.4, 0.4, 0.3, 8);
const wheelMat = new THREE.MeshStandardMaterial({ color: 0x222222 });
const wheel = new THREE.Mesh(wheelGeo, wheelMat);
wheel.position.set(pos[0], pos[1] - 2.1, pos[2]);
group.add(wheel);
});
}
return group;
}
// ---- Physics Engine ----
function initPhysics() {
const ac = AIRCRAFT[selectedAircraft];
const s = ac.stats;
const startY = selectedScenario === 0 ? getTerrainHeight(0, 0) + 5 : 2000;
physicsState = {
position: new THREE.Vector3(0, startY, 0),
velocity: new THREE.Vector3(selectedScenario === 0 ? 0 : 150, 0, 0),
quaternion: new THREE.Quaternion(),
angularVelocity: new THREE.Vector3(0, 0, 0),
// Control inputs (normalized -1 to 1)
pitchInput: 0, rollInput: 0, yawInput: 0, throttleInput: 0.5,
// State flags
gearDown: true, flapsExtended: false, stalled: false, crashed: false,
altitude: startY, airspeed: 0, machNumber: 0, gForce: 1, aoa: 0,
verticalSpeed: 0, heading: 0,
// Engine state
throttlePercent: 50, engineRPM: 3000, maxRPM: 12000,
// Aircraft properties (from selection)
mass: s.weight / 9.81,
wingArea: s.wingArea,
liftCoeffBase: s.liftCoeff,
dragCoeffBase: s.dragCoeff,
maxThrust: s.thrust,
turnRateMax: s.turnRate,
maxSpeedKnots: s.maxSpeed,
// Inertia tensor (approximate)
inertiaX: s.weight * 150,
inertiaY: s.weight * 300,
inertiaZ: s.weight * 400,
dt: 0.016
};
}
function getAirDensity(altitude) {
// ISA atmosphere model (simplified)
const h = Math.max(0, altitude);
if (h < 11000) return 1.225 * Math.pow(1 - 0.0000225577 * h, 4.256);
return 0.3639; // above troposphere cap
}
function updatePhysics(dt) {
if (physicsState.crashed || paused) return;
const ps = physicsState;
ps.dt = dt;
// --- Throttle & Engine Response ---
const throttleTarget = ps.throttleInput;
ps.throttlePercent += (throttleTarget * 100 - ps.throttlePercent) * Math.min(1, dt * 3);
ps.engineRPM = 2000 + (ps.throttlePercent / 100) * (ps.maxRPM - 2000);
// --- Air properties ---
const airDensity = getAirDensity(ps.altitude);
const speedKnots = ps.airspeed;
const speedMs = speedKnots * 0.514444;
const vSq = speedMs * speedMs;
// --- Angle of Attack (approximate) ---
const forwardDir = new THREE.Vector3(1, 0, 0).applyQuaternion(ps.quaternion);
const velDir = ps.velocity.clone().normalize();
if (ps.velocity.length() > 1) {
ps.aoa = Math.acos(Math.max(-1, Math.min(1, forwardDir.dot(velDir)))) * 180 / Math.PI;
}
// --- Lift Calculation ---
let liftCoeff = ps.liftCoeffBase + (ps.flapsExtended ? 0.5 : 0);
const aoaRad = ps.aoa * Math.PI / 180;
liftCoeff *= (1 + 2 * Math.sin(aoaRad));
// Stall detection
ps.stalled = false;
if (ps.aoa > 15 && speedKnots < 80) {
ps.stalled = true;
document.getElementById('hud-stall').style.display = 'block';
} else {
document.getElementById('hud-stall').style.display = 'none';
}
const liftForce = 0.5 * airDensity * vSq * ps.wingArea * liftCoeff;
// --- Drag Calculation ---
let dragCoeff = ps.dragCoeffBase + (ps.gearDown ? 0.02 : 0) + (ps.flapsExtended ? 0.015 : 0);
const inducedDrag = (liftCoeff * liftCoeff) / (Math.PI * 6 * 0.4); // AR=6, e=0.8
dragCoeff += inducedDrag;
const dragForce = 0.5 * airDensity * vSq * ps.wingArea * dragCoeff;
// --- Thrust ---
const thrustForce = (ps.throttlePercent / 100) * ps.maxThrust;
// --- Force vectors in aircraft local space ---
const liftDir = new THREE.Vector3(0, 1, 0).applyQuaternion(ps.quaternion);
const dragDir = ps.velocity.clone().normalize();
if (ps.velocity.length() < 0.1) dragDir.set(-forwardDir.x, -forwardDir.y, -forwardDir.z);
// Gravity
const gravityForce = new THREE.Vector3(0, -9.81 * ps.mass, 0);
// Total force
let totalForce = gravityForce.clone();
totalForce.addScaledVector(liftDir, liftForce);
if (ps.velocity.length() > 0.5) {
totalForce.addScaledVector(dragDir, -dragForce);
}
totalForce.addScaledVector(forwardDir, thrustForce);
// --- Acceleration & velocity update ---
const accel = totalForce.clone().divideScalar(ps.mass);
ps.velocity.addScaledVector(accel, dt);
// Speed limiting (compressibility)
if (speedKnots > ps.maxSpeedKnots * 1.2) {
ps.velocity.multiplyScalar(0.995);
}
// --- Position update ---
ps.position.addScaledVector(ps.velocity, dt);
// --- Angular dynamics (attitude control) ---
const pitchRate = ps.pitchInput * ps.turnRateMax;
const rollRate = ps.rollInput * ps.turnRateMax * 1.2;
const yawRate = ps.yawInput * ps.turnRateMax * 0.5;
// Apply angular damping
ps.angularVelocity.x += (pitchRate - ps.angularVelocity.x) * Math.min(1, dt * 4);
ps.angularVelocity.y += (yawRate - ps.angularVelocity.y) * Math.min(1, dt * 3);
ps.angularVelocity.z += (rollRate - ps.angularVelocity.z) * Math.min(1, dt * 5);
// Damping
ps.angularVelocity.multiplyScalar(Math.pow(0.98, dt * 60));
// Apply rotation
const rotQuat = new THREE.Quaternion();
const pitchQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), -ps.angularVelocity.x * dt);
const rollQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(1, 0, 0), ps.angularVelocity.z * dt);
const yawQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 1, 0), -ps.angularVelocity.y * dt);
rotQuat.multiply(pitchQ).multiply(rollQ).multiply(yawQ);
ps.quaternion.multiply(rotQuat);
ps.quaternion.normalize();
// Auto-leveling (subtle)
const upDir = new THREE.Vector3(0, 1, 0).applyQuaternion(ps.quaternion);
if (!ps.rollInput && !ps.pitchInput) {
const levelQ = new THREE.Quaternion().setFromUnitVectors(upDir, new THREE.Vector3(0, 1, 0));
ps.quaternion.slerp(levelQ, dt * 0.5);
ps.quaternion.normalize();
}
// --- Update derived values ---
ps.altitude = ps.position.y;
ps.airspeed = ps.velocity.length() / 0.514444;
ps.machNumber = speedMs / 340.3;
ps.verticalSpeed = ps.velocity.y * 196.85; // ft/min
// Heading (degrees)
const fwd2D = new THREE.Vector3(forwardDir.x, 0, forwardDir.z).normalize();
ps.heading = ((Math.atan2(fwd2D.x, fwd2D.z) * 180 / Math.PI) + 360) % 360;
// G-force (approximate from vertical acceleration)
const vertAccel = accel.y + 9.81;
ps.gForce = Math.max(0, 1 + vertAccel / 9.81);
// --- Terrain collision ---
const terrainH = getTerrainHeight(ps.position.x, ps.position.z);
if (ps.altitude < terrainH + 3) {
if (ps.stalled || ps.verticalSpeed < -20 || speedKnots > 150) {
crashAircraft();
} else if (speedKnots < 140 && Math.abs(ps.verticalSpeed) < 600) {
// Safe landing on ground
ps.altitude = terrainH + 3;
ps.position.y = terrainH + 3;
ps.velocity.y = Math.max(0, ps.velocity.y);
} else {
crashAircraft();
}
}
// Water collision
if (ps.altitude < -5) {
crashAircraft();
}
// --- Update model transform ---
if (aircraftModel) {
aircraftModel.position.copy(ps.position);
aircraftModel.quaternion.copy(ps.quaternion);
// Exhaust glow based on throttle
const exhaust = aircraftModel.getObjectByName('exhaust');
const exLight = aircraftModel.getObjectByName('exhaustLight');
if (exhaust) {
exhaust.material.opacity = 0.2 + (ps.throttlePercent / 100) * 0.6;
exhaust.scale.x = 1 + (ps.throttlePercent / 100) * 2;
}
if (exLight) exLight.intensity = ps.throttlePercent / 50;
// Smoke trail particles at high throttle or during turns
const turnGForce = Math.abs(ps.gForce - 1);
if ((ps.throttlePercent > 70 || turnGForce > 1.5) && selectedScenario === 1) {
const backDir = new THREE.Vector3(-1, 0, 0).applyQuaternion(ps.quaternion);
const smokePos = ps.position.clone().addScaledVector(backDir, 8);
const smokeGeo = new THREE.SphereGeometry(1.5 + Math.random());
const smokeMat = new THREE.MeshBasicMaterial({
color: turnGForce > 2 ? 0xff6644 : 0xaaaaaa,
transparent: true, opacity: 0.4
});
const smokeMesh = new THREE.Mesh(smokeGeo, smokeMat);
smokeMesh.position.copy(smokePos);
scene.add(smokeMesh);
particles.push({
mesh: smokeMesh, velocity: backDir.clone().multiplyScalar(-20), life: 3 + Math.random(), isBullet: false
});
}
}
// --- Dogfight enemy AI ---
updateEnemies(dt);
// --- Update audio ---
updateAudio();
}
function crashAircraft() {
physicsState.crashed = true;
const overlay = document.getElementById('damage-overlay');
overlay.classList.add('hit');
// Explosion particles
createExplosion(physicsState.position.clone());
setTimeout(() => {
if (gameRunning) resetPosition();
}, 2000);
}
function resetPosition() {
const ac = AIRCRAFT[selectedAircraft];
const startY = selectedScenario === 0 ? getTerrainHeight(0, 0) + 5 : 2000;
physicsState.position.set(0, startY, 0);
physicsState.velocity.set(selectedScenario === 0 ? 0 : 150, 0, 0);
physicsState.quaternion.identity();
physicsState.angularVelocity.set(0, 0, 0);
physicsState.crashed = false;
physicsState.stalled = false;
document.getElementById('hud-stall').style.display = 'none';
document.getElementById('damage-overlay').classList.remove('hit');
}
// ---- Enemy Aircraft (Dogfight) ----
function spawnEnemies() {
enemies.forEach(e => scene.remove(e.mesh));
enemies = [];
for (let i = 0; i < 5; i++) {
const angle = (i / 5) * Math.PI * 2;
const dist = 1500 + Math.random() * 1000;
const enemyData = { ...AIRCRAFT[(selectedAircraft + 1 + i) % 3] };
const mesh = buildAircraftModel(enemyData);
mesh.position.set(
physicsState.position.x + Math.cos(angle) * dist,
1500 + Math.random() * 1000,
physicsState.position.z + Math.sin(angle) * dist
);
scene.add(mesh);
enemies.push({
mesh: mesh,
velocity: new THREE.Vector3(80 + Math.random() * 40, 0, (Math.random() - 0.5) * 60),
health: 100,
alive: true,
turnTimer: 0,
targetHeading: Math.random() * Math.PI * 2,
shootCooldown: 0
});
}
}
function updateEnemies(dt) {
if (selectedScenario !== 1) return;
enemies.forEach(enemy => {
if (!enemy.alive) return;
// Simple AI: circle around player area with altitude changes
enemy.turnTimer -= dt;
if (enemy.turnTimer <= 0) {
const toPlayer = physicsState.position.clone().sub(enemy.mesh.position);
enemy.targetHeading = Math.atan2(toPlayer.x, toPlayer.z);
enemy.turnTimer = 3 + Math.random() * 5;
}
// Steer toward target heading
const fwd = new THREE.Vector3(1, 0, 0).applyQuaternion(enemy.mesh.quaternion);
const desiredDir = new THREE.Vector3(Math.sin(enemy.targetHeading), 0, Math.cos(enemy.targetHeading));
const cross = new THREE.Vector3().crossVectors(fwd, desiredDir);
enemy.mesh.quaternion.multiply(
new THREE.Quaternion().setFromAxisAngle(cross.normalize(), dt * 1.5)
);
// Altitude control
if (enemy.mesh.position.y < 500) {
const upQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), dt * 2);
enemy.mesh.quaternion.multiply(upQ);
} else if (enemy.mesh.position.y > 3000) {
const downQ = new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0, 0, 1), -dt * 2);
enemy.mesh.quaternion.multiply(downQ);
}
// Move forward
const speed = 120;
const moveDir = new THREE.Vector3(1, 0, 0).applyQuaternion(enemy.mesh.quaternion);
enemy.mesh.position.addScaledVector(moveDir, speed * dt);
// Shooting at player (occasional)
enemy.shootCooldown -= dt;
if (enemy.shootCooldown <= 0) {
const distToPlayer = enemy.mesh.position.distanceTo(physicsState.position);
if (distToPlayer < 800) {
createBullet(enemy.mesh.position.clone(), moveDir, true);
enemy.shootCooldown = 1.5 + Math.random();
}
}
// Proximity damage to player
const dist = enemy.mesh.position.distanceTo(physicsState.position);
if (dist < 20) {
score = Math.max(0, score - 50);
document.getElementById('damage-overlay').classList.add('hit');
setTimeout(() => document.getElementById('damage-overlay').classList.remove('hit'), 300);
}
});
// Respawn dead enemies
const aliveCount = enemies.filter(e => e.alive).length;
if (aliveCount < 2) {
const angle = Math.random() * Math.PI * 2;
const dist = 2000 + Math.random() * 1000;
const enemyData = { ...AIRCRAFT[Math.floor(Math.random() * 3)] };
const mesh = buildAircraftModel(enemyData);
mesh.position.set(
physicsState.position.x + Math.cos(angle) * dist,
2000 + Math.random() * 1000,
physicsState.position.z + Math.sin(angle) * dist
);
scene.add(mesh);
enemies.push({
mesh, velocity: new THREE.Vector3(100, 0, 50), health: 100, alive: true,
turnTimer: 0, targetHeading: Math.random() * Math.PI * 2, shootCooldown: 2
});
}
}
// ---- Bullets & Particles ----
function createBullet(pos, dir, isEnemy) {
const geo = new THREE.SphereGeometry(1);
const mat = new THREE.MeshBasicMaterial({ color: isEnemy ? 0xff4444 : 0x44ff44 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos);
scene.add(mesh);
particles.push({
mesh, velocity: dir.clone().multiplyScalar(500), life: 3, isBullet: true, isEnemy
});
}
function createExplosion(pos) {
for (let i = 0; i < 30; i++) {
const geo = new THREE.SphereGeometry(2 + Math.random() * 4);
const mat = new THREE.MeshBasicMaterial({
color: new THREE.Color().setHSL(0.05 + Math.random() * 0.1, 1, 0.5),
transparent: true, opacity: 1
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos);
scene.add(mesh);
particles.push({
mesh, velocity: new THREE.Vector3(
(Math.random() - 0.5) * 100,
Math.random() * 80,
(Math.random() - 0.5) * 100
), life: 2 + Math.random(), isBullet: false
});
}
}
function updateParticles(dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.isBullet) {
p.mesh.position.addScaledVector(p.velocity, dt);
// Check bullet hits
if (!p.isEnemy) {
enemies.forEach(enemy => {
if (!enemy.alive) return;
const dist = p.mesh.position.distanceTo(enemy.mesh.position);
if (dist < 20) {
enemy.health -= 34;
if (enemy.health <= 0) {
enemy.alive = false;
createExplosion(enemy.mesh.position.clone());
scene.remove(enemy.mesh);
score += 100;
} else {
// Hit flash
enemy.mesh.children.forEach(c => {
if (c.material && !c.name) {
const origColor = c.material.color.getHex();
c.material.color.set(0xff0000);
setTimeout(() => c.material.color.set(origColor), 100);
}
});
}
p.life = 0;
}
});
// Check if enemy bullet hits player
} else {
const dist = p.mesh.position.distanceTo(physicsState.position);
if (dist < 15) {
score = Math.max(0, score - 25);
document.getElementById('damage-overlay').classList.add('hit');
setTimeout(() => document.getElementById('damage-overlay').classList.remove('hit'), 200);
p.life = 0;
}
}
} else {
p.mesh.position.addScaledVector(p.velocity, dt);
p.velocity.y -= 10 * dt; // gravity on debris
p.mesh.material.opacity = Math.max(0, p.life / 2);
p.mesh.scale.multiplyScalar(0.97);
}
if (p.life <= 0) {
scene.remove(p.mesh);
particles.splice(i, 1);
}
}
}
// ---- Audio System ----
function initAudio() {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
// Master gain
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.3;
masterGain.connect(audioCtx.destination);
// Engine sound - layered oscillators for realism
engineOsc = [];
const oscTypes = ['sawtooth', 'square'];
oscTypes.forEach(type => {
const osc = audioCtx.createOscillator();
osc.type = type;
osc.frequency.value = 80;
const gain = audioCtx.createGain();
gain.gain.value = 0.15;
// Low-pass filter for muffled engine sound
const filter = audioCtx.createBiquadFilter();
filter.type = 'lowpass';
filter.frequency.value = 400;
filter.Q.value = 2;
osc.connect(filter);
filter.connect(gain);
gain.connect(masterGain);
osc.start();
engineOsc.push({ osc, gain, filter });
});
// Wind noise (filtered white noise)
const bufferSize = audioCtx.sampleRate * 2;
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 = 800;
windFilter.Q.value = 0.5;
windGain = audioCtx.createGain();
windGain.gain.value = 0;
windSource.connect(windFilter);
windFilter.connect(windGain);
windGain.connect(masterGain);
windSource.start();
// Stall warning oscillator
const stallOsc = audioCtx.createOscillator();
stallOsc.type = 'square';
stallOsc.frequency.value = 800;
const stallGain = audioCtx.createGain();
stallGain.gain.value = 0;
stallOsc.connect(stallGain);
stallGain.connect(masterGain);
stallOsc.start();
physicsState.stallOsc = stallOsc;
physicsState.stallGain = stallGain;
} catch(e) {
console.warn('Audio init failed:', e);
}
}
function updateAudio() {
if (!audioCtx || !engineOsc.length) return;
const ps = physicsState;
const rpmFactor = ps.engineRPM / ps.maxRPM;
// Engine pitch based on RPM
engineOsc.forEach((eo, i) => {
eo.osc.frequency.value = 60 + rpmFactor * 200 + (i === 1 ? 30 : 0);
eo.gain.gain.value = 0.05 + rpmFactor * 0.2;
eo.filter.frequency.value = 200 + rpmFactor * 800;
});
// Wind noise based on airspeed
if (windGain) {
const windVol = Math.min(1, ps.airspeed / 400);
windGain.gain.value += (windVol * 0.3 - windGain.gain.value) * 0.1;
}
// Stall warning
if (ps.stallGain) {
ps.stallGain.gain.value = ps.stalled ? 0.2 : 0;
}
}
// ---- Camera System ----
function updateCamera(dt) {
const ps = physicsState;
// Third-person chase camera with smooth follow
const forwardDir = new THREE.Vector3(1, 0, 0).applyQuaternion(ps.quaternion);
const upDir = new THREE.Vector3(0, 1, 0).applyQuaternion(ps.quaternion);
const camDist = 40;
const camHeight = 8;
const desiredPos = ps.position.clone()
.addScaledVector(forwardDir, -camDist)
.addScaledVector(new THREE.Vector3(0, 1, 0), camHeight);
// Smooth camera follow
camera.position.lerp(desiredPos, Math.min(1, dt * 4));
const lookTarget = ps.position.clone().addScaledVector(forwardDir, 20);
camera.lookAt(lookTarget);
}
// ---- HUD Update ----
function updateHUD() {
const ps = physicsState;
document.getElementById('hud-speed').textContent = Math.round(ps.airspeed) + ' KTS';
document.getElementById('hud-alt').textContent = Math.round(ps.altitude * 3.281) + ' FT';
document.getElementById('hud-hdg').textContent = String(Math.round(ps.heading)).padStart(3, '0') + '°';
document.getElementById('hud-vs').textContent = (ps.verticalSpeed > 0 ? '+' : '') + Math.round(ps.verticalSpeed) + ' FPM';
document.getElementById('hud-g').textContent = ps.gForce.toFixed(1) + ' G';
document.getElementById('hud-aoa').textContent = ps.aoa.toFixed(1) + '°';
document.getElementById('hud-thrust').textContent = Math.round(ps.throttlePercent) + '%';
document.getElementById('hud-mach').textContent = ps.machNumber.toFixed(2);
// G-meter indicator
const gInd = document.getElementById('g-ind');
const gPct = Math.max(0, Math.min(1, (ps.gForce - 1) / 4));
gInd.style.bottom = (50 + gPct * 50) + '%';
// Throttle bar
document.getElementById('throttle-fill').style.height = ps.throttlePercent + '%';
// Horizon line rotation
const horizonLine = document.getElementById('horizon-line');
const rightDir = new THREE.Vector3(0, 0, 1).applyQuaternion(ps.quaternion);
const rollAngle = Math.atan2(rightDir.x, rightDir.z) * 180 / Math.PI;
horizonLine.style.transform = `rotate(${-rollAngle}deg)`;
// Score display (dogfight only)
if (selectedScenario === 1) {
document.getElementById('hud-score').textContent = 'SCORE: ' + score;
}
// Minimap
drawMinimap();
}
function drawMinimap() {
const canvas = document.getElementById('minimap-canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, 120, 120);
// Background
ctx.fillStyle = 'rgba(0,30,0,0.8)';
ctx.fillRect(0, 0, 120, 120);
const cx = 60, cy = 60;
const scale = 0.04;
// Draw terrain contour hints
ctx.strokeStyle = 'rgba(0,255,0,0.15)';
for (let r = 15; r < 60; r += 15) {
ctx.beginPath();
ctx.arc(cx, cy, r, 0, Math.PI * 2);
ctx.stroke();
}
// Player dot
ctx.fillStyle = '#0f0';
ctx.beginPath();
ctx.arc(cx, cy, 3, 0, Math.PI * 2);
ctx.fill();
// Direction indicator
const fwdAngle = (physicsState.heading - 90) * Math.PI / 180;
ctx.strokeStyle = '#0f0';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.sin(fwdAngle) * 10, cy - Math.cos(fwdAngle) * 10);
ctx.stroke();
// Enemy dots (dogfight)
if (selectedScenario === 1) {
enemies.forEach(e => {
if (!e.alive) return;
const dx = (e.mesh.position.x - physicsState.position.x) * scale;
const dz = (e.mesh.position.z - physicsState.position.z) * scale;
if (Math.abs(dx) < 55 && Math.abs(dz) < 55) {
ctx.fillStyle = '#f00';
ctx.beginPath();
ctx.arc(cx + dx, cy + dz, 2.5, 0, Math.PI * 2);
ctx.fill();
}
});
}
// Border circle
ctx.strokeStyle = 'rgba(0,255,0,0.4)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(cx, cy, 58, 0, Math.PI * 2);
ctx.stroke();
}
// ---- Input Handling ----
function setupInput() {
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (e.code === 'KeyP' && gameRunning) {
paused = !paused;
document.getElementById('pause-overlay').classList.toggle('active', paused);
}
if (e.code === 'Escape' && gameRunning && paused) {
endGame();
}
if (e.code === 'KeyR' && gameRunning) resetPosition();
if (e.code === 'Tab') { e.preventDefault(); physicsState.gearDown = !physicsState.gearDown; }
if (e.code === 'KeyF') physicsState.flapsExtended = !physicsState.flapsExtended;
});
document.addEventListener('keyup', e => { keys[e.code] = false; });
// Mouse for camera look offset (optional)
let mouseX = window.innerWidth / 2, mouseY = window.innerHeight / 2;
document.addEventListener('mousemove', e => {
if (!gameRunning || paused) return;
mouseX = e.clientX;
mouseY = e.clientY;
});
// Click to shoot (dogfight)
document.addEventListener('mousedown', e => {
if (!gameRunning || paused || selectedScenario !== 1) return;
const fwd = new THREE.Vector3(1, 0, 0).applyQuaternion(physicsState.quaternion);
createBullet(physicsState.position.clone().addScaledVector(fwd, 5), fwd, false);
});
// Resume audio context on interaction
document.addEventListener('click', () => {
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
}, { once: true });
}
function processInput() {
const ps = physicsState;
// Pitch
let pitchTarget = 0;
if (keys['KeyW']) pitchTarget = -1;
if (keys['KeyS']) pitchTarget = 1;
ps.pitchInput += (pitchTarget - ps.pitchInput) * Math.min(1, 0.2);
// Roll
let rollTarget = 0;
if (keys['KeyA']) rollTarget = 1;
if (keys['KeyD']) rollTarget = -1;
ps.rollInput += (rollTarget - ps.rollInput) * Math.min(1, 0.2);
// Yaw
let yawTarget = 0;
if (keys['KeyQ']) yawTarget = 1;
if (keys['KeyE']) yawTarget = -1;
ps.yawInput += (yawTarget - ps.yawInput) * Math.min(1, 0.2);
// Throttle
let throttleDelta = 0;
if (keys['ShiftLeft'] || keys['ShiftRight']) throttleDelta = 0.8;
if (keys['ControlLeft'] || keys['ControlRight']) throttleDelta = -0.8;
ps.throttleInput = Math.max(0, Math.min(1, ps.throttleInput + throttleDelta * 0.02));
// Decay inputs when released
if (!keys['KeyW'] && !keys['KeyS']) ps.pitchInput *= 0.95;
if (!keys['KeyA'] && !keys['KeyD']) ps.rollInput *= 0.95;
if (!keys['KeyQ'] && !keys['KeyE']) ps.yawInput *= 0.95;
}
// ---- Game Loop ----
function gameLoop() {
requestAnimationFrame(gameLoop);
if (!gameRunning) return;
const dt = Math.min(clock.getDelta(), 0.033); // Cap at ~30fps worth of physics
if (!paused) {
processInput();
updatePhysics(dt);
updateCamera(dt);
updateParticles(dt);
updateHUD();
}
renderer.render(scene, camera);
}
// ---- Start / End Game ----
function startGame() {
document.getElementById('menu-screen').classList.remove('active');
document.getElementById('hud').classList.add('active');
gameRunning = true;
paused = false;
score = 0;
initPhysics();
// Build aircraft model
if (aircraftModel) scene.remove(aircraftModel);
const acData = AIRCRAFT[selectedAircraft];
aircraftModel = buildAircraftModel(acData);
scene.add(aircraftModel);
// Spawn enemies for dogfight
if (selectedScenario === 1) {
spawnEnemies();
document.getElementById('hud-crosshair').classList.add('active');
document.getElementById('hud-score').classList.add('active');
} else {
document.getElementById('hud-crosshair').classList.remove('active');
document.getElementById('hud-score').classList.remove('active');
}
// Build pitch ladder
buildPitchLadder();
clock.start();
}
function endGame() {
gameRunning = false;
document.getElementById('hud').classList.remove('active');
document.getElementById('menu-screen').classList.add('active');
if (aircraftModel) scene.remove(aircraftModel);
enemies.forEach(e => scene.remove(e.mesh));
enemies = [];
particles.forEach(p => scene.remove(p.mesh));
particles = [];
// Reset audio
if (engineOsc.length) {
engineOsc.forEach(eo => { eo.osc.frequency.value = 80; eo.gain.gain.value = 0.15; });
}
}
function buildPitchLadder() {
const ladder = document.getElementById('pitch-ladder');
ladder.innerHTML = '';
for (let deg = -20; deg <= 20; deg += 5) {
if (deg === 0) continue;
const line = document.createElement('div');
line.className = 'pitch-line';
const yPos = 100 + deg * 3;
line.style.top = yPos + '%';
line.style.width = (Math.abs(deg) % 10 === 0 ? '80px' : '40px');
ladder.appendChild(line);
if (deg % 10 === 0) {
const label = document.createElement('div');
label.className = 'pitch-label';
label.style.top = (yPos - 2) + '%';
label.textContent = deg > 0 ? '+' + deg : deg;
// Left label
const l1 = label.cloneNode(true);
l1.style.left = '35%';
ladder.appendChild(l1);
// Right label
const l2 = label.cloneNode(true);
l2.style.right = '35%';
l2.style.left = 'auto';
ladder.appendChild(l2);
}
}
}
// ---- Boot ----
window.addEventListener('DOMContentLoaded', () => {
setupInput();
init().then(() => {
gameLoop();
});
});
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"full","sessionId":"73d18c4c-8c65-4073-bd9c-eb7f861df27c","tokensIn":824647,"tokensOut":25965,"tokensTotal":850612,"turns":21,"toolCalls":20,"failedToolCalls":0,"timestamp":"2026-07-18T20:25:29.247Z"} -->