Flight Simulator
Qwen 3.6 27b MTP · 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 91,833 bytes · SHA-256 2cfc72c83671
<!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: 'Courier New', monospace; }
canvas { display: block; }
/* Loading Screen */
#loading-screen {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(135deg, #0a0a2e 0%, #1a1a4e 50%, #0d0d3d 100%);
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 1000; transition: opacity 0.8s ease;
}
#loading-screen.hidden { opacity: 0; pointer-events: none; }
#loading-screen h1 {
font-size: 4em; color: #00e5ff; text-shadow: 0 0 30px #00e5ff, 0 0 60px #0077ff;
letter-spacing: 8px; margin-bottom: 10px;
}
#loading-screen .subtitle { color: #88aacc; font-size: 1.2em; letter-spacing: 4px; margin-bottom: 40px; }
#load-bar-container { width: 400px; height: 6px; background: rgba(255,255,255,0.1); border-radius: 3px; overflow: hidden; }
#load-bar { width: 0%; height: 100%; background: linear-gradient(90deg, #00e5ff, #0077ff); transition: width 0.3s; border-radius: 3px; }
#load-text { color: #6688aa; margin-top: 15px; font-size: 0.9em; letter-spacing: 2px; }
/* Menu Screen */
#menu-screen {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: linear-gradient(135deg, #0a0a2e 0%, #1a1a4e 50%, #0d0d3d 100%);
display: none; flex-direction: column; align-items: center; justify-content: center; z-index: 900;
}
#menu-screen.active { display: flex; }
.menu-title { font-size: 3em; color: #00e5ff; text-shadow: 0 0 20px #00e5ff; letter-spacing: 6px; margin-bottom: 40px; }
/* Aircraft Selection */
.aircraft-select { display: flex; gap: 30px; margin-bottom: 40px; }
.aircraft-card {
width: 280px; padding: 25px; background: rgba(0,229,255,0.05); border: 1px solid rgba(0,229,255,0.2);
border-radius: 12px; cursor: pointer; transition: all 0.3s ease; text-align: center;
}
.aircraft-card:hover { background: rgba(0,229,255,0.1); border-color: #00e5ff; transform: translateY(-5px); box-shadow: 0 10px 40px rgba(0,229,255,0.2); }
.aircraft-card.selected { background: rgba(0,229,255,0.15); border-color: #00e5ff; box-shadow: 0 0 30px rgba(0,229,255,0.3); }
.aircraft-card h3 { color: #00e5ff; font-size: 1.4em; margin-bottom: 8px; letter-spacing: 2px; }
.aircraft-card .type { color: #6688aa; font-size: 0.85em; margin-bottom: 15px; letter-spacing: 1px; }
.stat-row { display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid rgba(255,255,255,0.05); }
.stat-label { color: #88aacc; font-size: 0.8em; }
.stat-value { color: #ffffff; font-size: 0.8em; }
.stat-bar { width: 100%; height: 4px; background: rgba(255,255,255,0.1); border-radius: 2px; margin-top: 3px; }
.stat-fill { height: 100%; border-radius: 2px; transition: width 0.5s ease; }
/* Scenario Selection */
.scenario-select { display: flex; gap: 30px; margin-bottom: 40px; }
.scenario-card {
width: 300px; padding: 25px; background: rgba(0,229,255,0.05); border: 1px solid rgba(0,229,255,0.2);
border-radius: 12px; cursor: pointer; transition: all 0.3s ease; text-align: center;
}
.scenario-card:hover { background: rgba(0,229,255,0.1); border-color: #00e5ff; transform: translateY(-5px); }
.scenario-card.selected { background: rgba(0,229,255,0.15); border-color: #00e5ff; box-shadow: 0 0 30px rgba(0,229,255,0.3); }
.scenario-card h3 { color: #00e5ff; font-size: 1.3em; margin-bottom: 8px; letter-spacing: 2px; }
.scenario-card p { color: #88aacc; font-size: 0.85em; line-height: 1.6; }
/* Launch Button */
#launch-btn {
padding: 15px 60px; background: linear-gradient(90deg, #00e5ff, #0077ff); border: none;
color: #fff; font-size: 1.2em; letter-spacing: 4px; cursor: pointer; border-radius: 8px;
transition: all 0.3s ease; text-transform: uppercase;
}
#launch-btn:hover { transform: scale(1.05); box-shadow: 0 0 40px rgba(0,229,255,0.5); }
/* HUD */
#hud {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 100; display: none;
}
#hud.active { display: block; }
/* HUD Canvas */
#hud-canvas { position: absolute; top: 0; left: 0; width: 100%; height: 100%; }
/* Crosshair */
.crosshair {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 40px; height: 40px;
}
.crosshair::before, .crosshair::after {
content: ''; position: absolute; background: rgba(0,229,255,0.7);
}
.crosshair::before { top: 50%; left: 0; width: 100%; height: 1px; transform: translateY(-50%); }
.crosshair::after { top: 0; left: 50%; width: 1px; height: 100%; transform: translateX(-50%); }
/* HUD Panels */
.hud-panel {
position: absolute; background: rgba(0,20,40,0.6); border: 1px solid rgba(0,229,255,0.3);
border-radius: 8px; padding: 12px 16px; color: #00e5ff; font-size: 0.75em; letter-spacing: 1px;
}
.hud-panel .label { color: #4488aa; font-size: 0.85em; }
.hud-panel .value { color: #ffffff; font-size: 1.3em; margin-top: 2px; }
#hud-speed { top: 20px; left: 20px; }
#hud-altitude { top: 20px; right: 20px; text-align: right; }
#hud-throttle { bottom: 80px; left: 20px; }
#hud-gforce { bottom: 80px; right: 20px; text-align: right; }
#hud-heading { top: 50%; left: 20px; transform: translateY(-50%); }
#hud-aoa { top: 50%; right: 20px; transform: translateY(-50%); text-align: right; }
/* Stall Warning */
.stall-warning {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -80px);
color: #ff4444; font-size: 1.5em; letter-spacing: 3px; text-shadow: 0 0 20px #ff0000;
animation: blink 0.5s infinite; display: none;
}
.stall-warning.active { display: block; }
@keyframes blink { 0%,100%{opacity:1;} 50%{opacity:0.3;} }
/* G-Force Warning */
.g-warning {
position: absolute; top: 20%; left: 50%; transform: translateX(-50%);
color: #ffaa00; font-size: 1.2em; letter-spacing: 2px; text-shadow: 0 0 15px #ff8800;
display: none;
}
.g-warning.active { display: block; }
/* Minimap */
#minimap-container {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
width: 200px; height: 200px; border-radius: 50%; overflow: hidden;
border: 2px solid rgba(0,229,255,0.4); background: rgba(0,20,40,0.7);
}
#minimap-canvas { width: 100%; height: 100%; }
/* Controls Help */
.controls-help {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
color: #4488aa; font-size: 0.7em; letter-spacing: 1px; text-align: center;
}
/* Damage Overlay */
#damage-overlay {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 99;
border: 0px solid rgba(255,0,0,0); transition: border 0.3s; display: none;
}
#damage-overlay.active { display: block; }
/* Pause Menu */
#pause-menu {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,20,0.85); display: none; flex-direction: column;
align-items: center; justify-content: center; z-index: 950;
}
#pause-menu.active { display: flex; }
#pause-menu h2 { color: #00e5ff; font-size: 2.5em; letter-spacing: 6px; margin-bottom: 30px; }
.pause-btn {
padding: 12px 40px; background: rgba(0,229,255,0.1); border: 1px solid #00e5ff;
color: #00e5ff; font-size: 1em; letter-spacing: 3px; cursor: pointer; margin: 8px;
transition: all 0.3s ease; text-transform: uppercase; border-radius: 6px;
}
.pause-btn:hover { background: rgba(0,229,255,0.2); }
/* Mission Briefing */
#briefing {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,20,0.9); display: none; flex-direction: column;
align-items: center; justify-content: center; z-index: 960;
}
#briefing.active { display: flex; }
#briefing h2 { color: #00e5ff; font-size: 2em; letter-spacing: 4px; margin-bottom: 15px; }
#briefing p { color: #88aacc; max-width: 600px; text-align: center; line-height: 1.8; margin-bottom: 30px; font-size: 0.9em; }
/* Dogfight HUD extras */
.enemy-indicator {
position: absolute; width: 20px; height: 20px; border: 2px solid #ff4444;
transform: translate(-50%, -50%); pointer-events: none; display: none;
}
.enemy-indicator.active { display: block; }
/* Score */
#score-panel {
position: absolute; top: 80px; left: 20px; background: rgba(0,20,40,0.6);
border: 1px solid rgba(0,229,255,0.3); border-radius: 8px; padding: 10px 14px;
color: #00e5ff; font-size: 0.75em; letter-spacing: 1px; display: none;
}
#score-panel.active { display: block; }
/* Landing Score */
#landing-score {
position: absolute; top: 30%; left: 50%; transform: translate(-50%, -50%);
color: #00ff88; font-size: 2em; letter-spacing: 4px; text-shadow: 0 0 20px #00ff88;
display: none; text-align: center;
}
#landing-score.active { display: block; }
/* Vignette */
.vignette {
position: fixed; top: 0; left: 0; width: 100%; height: 100%; pointer-events: none; z-index: 98;
background: radial-gradient(ellipse at center, transparent 60%, rgba(0,0,0,0.4) 100%);
}
/* Horizon Line */
.horizon-line {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 300px; height: 2px; background: rgba(0,229,255,0.3); display: none;
}
/* Pitch Ladder */
.pitch-ladder {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 400px; height: 200px; pointer-events: none; display: none;
}
/* Message popup */
.message-popup {
position: absolute; top: 15%; left: 50%; transform: translateX(-50%);
color: #ffffff; font-size: 1.2em; letter-spacing: 3px; text-shadow: 0 0 10px rgba(0,229,255,0.5);
opacity: 0; transition: opacity 0.5s ease; pointer-events: none;
}
.message-popup.show { opacity: 1; }
/* Target reticle for dogfight */
.target-reticle {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 60px; height: 60px; pointer-events: none; display: none;
}
.target-reticle.active { display: block; }
</style>
</head>
<body>
<!-- Loading Screen -->
<div id="loading-screen">
<h1>APEX AERO</h1>
<div class="subtitle">FLIGHT SIMULATOR</div>
<div id="load-bar-container"><div id="load-bar"></div></div>
<div id="load-text">INITIALIZING SYSTEMS...</div>
</div>
<!-- Menu Screen -->
<div id="menu-screen">
<div class="menu-title">APEX AERO</div>
<h3 style="color:#6688aa;letter-spacing:3px;margin-bottom:20px;font-size:0.9em;">SELECT AIRCRAFT</h3>
<div class="aircraft-select" id="aircraft-select"></div>
<h3 style="color:#6688aa;letter-spacing:3px;margin-bottom:20px;font-size:0.9em;">SELECT MISSION</h3>
<div class="scenario-select" id="scenario-select"></div>
<button id="launch-btn">LAUNCH</button>
<div style="margin-top:30px;color:#446688;font-size:0.7em;letter-spacing:2px;">
MOUSE: PITCH/ROLL | W/S: THROTTLE UP/DOWN | Q/E: YAW | SHIFT: BRAKE | SPACE: FLARES
</div>
</div>
<!-- HUD -->
<div id="hud">
<canvas id="hud-canvas"></canvas>
<div class="crosshair" id="crosshair"></div>
<div class="hud-panel" id="hud-speed">
<div class="label">AIRSPEED</div>
<div class="value"><span id="speed-val">0</span> KTS</div>
</div>
<div class="hud-panel" id="hud-altitude">
<div class="label">ALTITUDE</div>
<div class="value"><span id="alt-val">0</span> FT</div>
</div>
<div class="hud-panel" id="hud-throttle">
<div class="label">THROTTLE</div>
<div class="value"><span id="throttle-val">0</span>%</div>
</div>
<div class="hud-panel" id="hud-gforce">
<div class="label">G-FORCE</div>
<div class="value"><span id="g-val">1.0</span>G</div>
</div>
<div class="hud-panel" id="hud-heading">
<div class="label">HEADING</div>
<div class="value"><span id="heading-val">360</span>°</div>
</div>
<div class="hud-panel" id="hud-aoa">
<div class="label">ANGLE OF ATTACK</div>
<div class="value"><span id="aoa-val">0.0</span>°</div>
</div>
<div class="stall-warning" id="stall-warning">⚠ STALL WARNING ⚠</div>
<div class="g-warning" id="g-warning">HIGH G-FORCE</div>
<canvas id="minimap-canvas" width="200" height="200"></canvas>
<div id="minimap-container"><canvas id="minimap-inner" width="200" height="200"></canvas></div>
<div class="message-popup" id="message-popup"></div>
<div id="score-panel">
<div class="label">SCORE</div>
<div class="value"><span id="score-val">0</span></div>
<div style="margin-top:5px;"><span class="label">ENEMIES:</span> <span id="enemy-count" style="color:#ff4444;">0</span></div>
</div>
<div id="landing-score"></div>
</div>
<div class="vignette"></div>
<div id="damage-overlay"></div>
<!-- Pause Menu -->
<div id="pause-menu">
<h2>PAUSED</h2>
<button class="pause-btn" onclick="resumeGame()">RESUME</button>
<button class="pause-btn" onclick="restartGame()">RESTART</button>
<button class="pause-btn" onclick="backToMenu()">MAIN MENU</button>
</div>
<!-- Briefing -->
<div id="briefing">
<h2 id="briefing-title"></h2>
<p id="briefing-text"></p>
<button class="pause-btn" onclick="startMission()">COMMENCE MISSION</button>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ============================================================
// APEX AERO - FLIGHT SIMULATOR
// Complete Newtonian Physics Flight Model
// ============================================================
// --- Simplex Noise Implementation (inline) ---
class SimplexNoise {
constructor(seed = Math.random()) {
this.grad3 = [
[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]
];
this.p = [];
for (let i = 0; i < 256; i++) this.p[i] = Math.floor(seed * 256 + i * 131.7) & 255;
this.perm = new Array(512);
for (let i = 0; i < 512; i++) this.perm[i] = this.p[i & 255];
}
dot(g, x, y) { return g[0]*x + g[1]*y; }
noise2D(xin, yin) {
const F2 = 0.5*(Math.sqrt(3)-1);
const G2 = (3-Math.sqrt(3))/6;
let s = (xin+yin)*F2;
let i = Math.floor(xin+s), j = Math.floor(yin+s);
let t = (i+j)*G2;
let X0 = i-t, Y0 = j-t;
let x0 = xin-X0, y0 = yin-Y0;
let i1, j1;
if(x0>y0) { i1=1; j1=0; } else { i1=0; j1=1; }
let x1=x0-i1+G2, y1=y0-j1+G2;
let x2=x0-1+2*G2, y2=y0-1+2*G2;
let ii=i&255, jj=j&255;
let gi0=this.perm[ii+this.perm[jj]]%12;
let gi1=this.perm[ii+i1+this.perm[jj+j1]]%12;
let gi2=this.perm[ii+1+this.perm[jj+1]]%12;
let n0=0, n1=0, n2=0;
let t0=0.5-x0*x0-y0*y0;
if(t0>=0){t0*=t0;n0=t0*t0*this.dot(this.grad3[gi0],x0,y0);}
let t1=0.5-x1*x1-y1*y1;
if(t1>=0){t1*=t1;n1=t1*t1*this.dot(this.grad3[gi1],x1,y1);}
let t2=0.5-x2*x2-y2*y2;
if(t2>=0){t2*=t2;n2=t2*t2*this.dot(this.grad3[gi2],x2,y2);}
return 70*(n0+n1+n2);
}
fbm(x, y, octaves=6, lacunarity=2, gain=0.5) {
let val = 0, amp = 1, freq = 1, maxVal = 0;
for(let i=0;i<octaves;i++){
val += this.noise2D(x*freq,y*freq)*amp;
maxVal += amp;
amp *= gain;
freq *= lacunarity;
}
return val/maxVal;
}
}
// --- Aircraft Definitions ---
const AIRCRAFT = {
interceptor: {
name: "VIPER X-1",
type: "Interceptor",
color: 0x4488ff,
accentColor: 0x00e5ff,
mass: 8500,
wingArea: 32,
maxThrust: 160000,
thrustToWeight: 1.9,
liftCoeffMax: 1.4,
dragProfile: { cd0: 0.015, k: 0.04 },
turnRate: 2.8,
rollRate: 3.5,
yawRate: 1.2,
maxSpeed: 700, // knots
stallSpeed: 90,
gLimit: 9,
description: "High-speed interceptor with exceptional thrust-to-weight ratio and tight turning radius."
},
fighterBomber: {
name: "RAVEN MK-III",
type: "Fighter Bomber",
color: 0x88aa44,
accentColor: 0xaacc66,
mass: 15000,
wingArea: 48,
maxThrust: 220000,
thrustToWeight: 1.5,
liftCoeffMax: 1.6,
dragProfile: { cd0: 0.022, k: 0.06 },
turnRate: 2.0,
rollRate: 2.5,
yawRate: 0.9,
maxSpeed: 550,
stallSpeed: 110,
gLimit: 7.5,
description: "Heavy fighter bomber with superior low-altitude performance and payload capacity."
},
stealthFighter: {
name: "PHANTOM S-9",
type: "Stealth Fighter",
color: 0x334466,
accentColor: 0x5577aa,
mass: 12000,
wingArea: 40,
maxThrust: 180000,
thrustToWeight: 1.6,
liftCoeffMax: 1.5,
dragProfile: { cd0: 0.012, k: 0.035 },
turnRate: 2.4,
rollRate: 3.0,
yawRate: 1.0,
maxSpeed: 620,
stallSpeed: 95,
gLimit: 8.5,
description: "Balanced stealth fighter with low radar cross-section and excellent maneuverability."
}
};
const SCENARIOS = {
takeoffLanding: {
name: "PRECISION FLIGHT",
subtitle: "Takeoff & Landing",
description: "Navigate the runway, achieve liftoff using proper aerodynamic principles, maintain stable flight patterns, and execute a precision landing. Manage airspeed, angle of attack, and descent rate carefully.",
briefing: "Your mission is to demonstrate mastery of low-speed flight dynamics. Take off from the runway, climb to 3000 feet, perform a circuit pattern, and land back on the runway. Watch for stall warnings at low speeds and manage your approach speed carefully."
},
dogfight: {
name: "COMBAT ENGAGEMENT",
subtitle: "Dogfight Scenario",
description: "Engage enemy aircraft in high-G aerial combat. Use superior maneuvering, energy management, and tactical positioning to outmaneuver opponents.",
briefing: "Hostile aircraft detected in your sector. Engage and destroy all enemy fighters while maintaining positive G-forces within structural limits. Manage your energy state - speed and altitude are your weapons."
}
};
// --- Global State ---
let gameState = 'loading'; // loading, menu, briefing, playing, paused, ended
let selectedAircraft = 'interceptor';
let selectedScenario = 'takeoffLanding';
let score = 0;
let enemiesRemaining = 0;
let landingScore = null;
// --- Three.js Setup ---
let scene, camera, renderer, clock;
let terrainMesh, waterMesh, skyDome;
let aircraftModel, aircraftGroup;
let enemyAircraft = [];
let runwayMesh;
let particles = [];
let smokeParticles = [];
// --- Physics State ---
const physics = {
position: new THREE.Vector3(0, 50, 0),
velocity: new THREE.Vector3(0, 0, 0),
angularVelocity: new THREE.Vector3(0, 0, 0),
orientation: new THREE.Quaternion(),
euler: new THREE.Euler(0, 0, 0, 'YXZ'),
throttle: 0, // 0-1
gForce: 1.0,
angleOfAttack: 0,
airspeed: 0, // m/s
altitude: 0,
heading: 0,
isStalled: false,
onGround: true,
damage: 0,
engineRPM: 0,
engineHeat: 0
};
// --- Input State ---
const input = {
pitch: 0, roll: 0, yaw: 0, throttleUp: false, throttleDown: false,
brake: false, flares: false, mouseLocked: false
};
// --- Audio System ---
let audioCtx, engineOsc, engineGain, windNoise, windGain;
let masterGain;
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 = audioCtx.createOscillator();
engineOsc.type = 'sawtooth';
engineOsc.frequency.value = 80;
const engineFilter = audioCtx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 400;
engineFilter.Q.value = 2;
engineGain = audioCtx.createGain();
engineGain.gain.value = 0;
// Second harmonic for richness
const osc2 = audioCtx.createOscillator();
osc2.type = 'square';
osc2.frequency.value = 40;
const gain2 = audioCtx.createGain();
gain2.gain.value = 0.3;
engineOsc.connect(engineFilter);
osc2.connect(gain2);
gain2.connect(engineFilter);
engineFilter.connect(engineGain);
engineGain.connect(masterGain);
engineOsc.start();
osc2.start();
// Wind noise using buffer source
const bufferSize = audioCtx.sampleRate * 2;
const windBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = windBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = Math.random() * 2 - 1;
}
windNoise = audioCtx.createBufferSource();
windNoise.buffer = windBuffer;
windNoise.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;
windNoise.connect(windFilter);
windFilter.connect(windGain);
windGain.connect(masterGain);
windNoise.start();
// Store references for updates
physics._audioRefs = { engineOsc, osc2, gain2, engineFilter, engineGain, windFilter, windGain };
} catch(e) {
console.warn('Audio not available:', e);
}
}
function updateAudio() {
if (!physics._audioRefs) return;
const refs = physics._audioRefs;
const ac = audioCtx;
// Engine frequency based on throttle and RPM
const baseFreq = 60 + physics.throttle * 200;
refs.engineOsc.frequency.setTargetAtTime(baseFreq, ac.currentTime, 0.1);
refs.osc2.frequency.setTargetAtTime(baseFreq * 0.5, ac.currentTime, 0.1);
// Engine volume based on throttle and airspeed
const engineVol = Math.min(0.4, physics.throttle * 0.3 + (physics.airspeed / 300) * 0.1);
refs.engineGain.gain.setTargetAtTime(engineVol, ac.currentTime, 0.05);
// Filter opens with RPM
refs.engineFilter.frequency.setTargetAtTime(200 + physics.throttle * 800, ac.currentTime, 0.1);
// Wind noise based on airspeed
const windVol = Math.min(0.3, (physics.airspeed / 350) * 0.25);
refs.windGain.gain.setTargetAtTime(windVol, ac.currentTime, 0.1);
refs.windFilter.frequency.setTargetAtTime(400 + physics.airspeed * 3, ac.currentTime, 0.1);
}
// --- Terrain Generation ---
const noise = new SimplexNoise(42);
let terrainData;
const TERRAIN_SIZE = 8000;
const TERRAIN_SEGMENTS = 256;
function generateTerrain() {
const geometry = new THREE.PlaneGeometry(TERRAIN_SIZE, TERRAIN_SIZE, TERRAIN_SEGMENTS, TERRAIN_SEGMENTS);
geometry.rotateX(-Math.PI / 2);
const vertices = geometry.attributes.position.array;
terrainData = [];
for (let i = 0; i < vertices.length; i += 3) {
const x = vertices[i];
const z = vertices[i + 2];
// Multi-octave noise for realistic terrain
let height = 0;
// Large scale mountains
height += noise.fbm(x * 0.0003, z * 0.0003, 6, 2.1, 0.5) * 800;
// Medium hills
height += noise.fbm(x * 0.001, z * 0.001, 4, 2.0, 0.45) * 200;
// Small details
height += noise.fbm(x * 0.005, z * 0.005, 3, 2.0, 0.4) * 30;
// Flatten near center area
const distFromCenter = Math.sqrt(x*x + z*z);
if (distFromCenter < 600) {
const flattenFactor = Math.max(0, 1 - distFromCenter / 600);
height *= (1 - flattenFactor * 0.95);
}
// Ensure runway area is completely flat at y=0
if (Math.abs(x) < 250 && z > -400 && z < 1000) {
const edgeX = Math.max(0, Math.abs(x) - 200);
const edgeZ_front = Math.max(0, -(z + 350));
const edgeZ_back = Math.max(0, z - 950);
const edgeY = Math.max(0, Math.min(edgeX, Math.min(edgeZ_front, edgeZ_back)) - 40);
if (edgeY < 1) {
height *= Math.max(0, edgeY / 1);
} else {
height = 0; // Completely flat runway zone
}
}
// Water level
if (height < -20) height = -20;
vertices[i + 1] = height;
terrainData.push({ x, y: height, z });
}
geometry.computeVertexNormals();
// Vertex colors for terrain
const colors = new Float32Array(vertices.length);
for (let i = 0; i < vertices.length; i += 3) {
const h = vertices[i + 1];
let r, g, b;
if (h < -15) { // Water edge / beach
r = 0.76; g = 0.7; b = 0.5;
} else if (h < 20) { // Grass/lowland
const t = Math.min(1, (h + 15) / 35);
r = 0.15 + t * 0.1; g = 0.35 + t * 0.15; b = 0.08;
} else if (h < 200) { // Forest/hills
const t = Math.min(1, (h - 20) / 180);
r = 0.1 + t * 0.2; g = 0.25 + t * 0.1; b = 0.05 + t * 0.05;
} else if (h < 400) { // Rock
const t = Math.min(1, (h - 200) / 200);
r = 0.35 + t * 0.15; g = 0.32 + t * 0.12; b = 0.28 + t * 0.1;
} else { // Snow caps
const t = Math.min(1, (h - 400) / 200);
r = 0.5 + t * 0.45; g = 0.47 + t * 0.48; b = 0.38 + t * 0.52;
}
colors[i] = r;
colors[i+1] = g;
colors[i+2] = b;
}
geometry.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const material = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.85,
metalness: 0.1,
flatShading: false
});
terrainMesh = new THREE.Mesh(geometry, material);
scene.add(terrainMesh);
}
function getTerrainHeight(x, z) {
// Sample from generated data using bilinear interpolation
const halfSize = TERRAIN_SIZE / 2;
const cellSize = TERRAIN_SIZE / TERRAIN_SEGMENTS;
let nx = (x + halfSize) / cellSize;
let nz = (z + halfSize) / cellSize;
const ix = Math.floor(nx);
const iz = Math.floor(nz);
const fx = nx - ix;
const fz = nz - iz;
if (ix < 0 || ix >= TERRAIN_SEGMENTS || iz < 0 || iz >= TERRAIN_SEGMENTS) return 0;
const getH = (i, j) => {
if (i < 0 || i > TERRAIN_SEGMENTS || j < 0 || j > TERRAIN_SEGMENTS) return 0;
const idx = (j * (TERRAIN_SEGMENTS + 1) + i);
const geo = terrainMesh.geometry;
return geo.attributes.position.array[idx * 3 + 1];
};
const h00 = getH(ix, iz);
const h10 = getH(ix+1, iz);
const h01 = getH(ix, iz+1);
const h11 = getH(ix+1, iz+1);
return h00*(1-fx)*(1-fz) + h10*fx*(1-fz) + h01*(1-fx)*fz + h11*fx*fz;
}
// --- Water Plane ---
function createWater() {
const waterGeo = new THREE.PlaneGeometry(TERRAIN_SIZE * 2, TERRAIN_SIZE * 2);
waterGeo.rotateX(-Math.PI / 2);
const waterMat = new THREE.MeshStandardMaterial({
color: 0x1a5276,
transparent: true,
opacity: 0.7,
roughness: 0.1,
metalness: 0.8
});
waterMesh = new THREE.Mesh(waterGeo, waterMat);
waterMesh.position.y = -18;
scene.add(waterMesh);
}
// --- Sky Dome ---
function createSky() {
const skyGeo = new THREE.SphereGeometry(15000, 32, 32);
// Gradient texture for sky
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 512;
const ctx = canvas.getContext('2d');
const gradient = ctx.createLinearGradient(0, 0, 0, 512);
// Time-based sky colors (default: golden hour)
gradient.addColorStop(0, '#0a1628'); // Top - deep blue
gradient.addColorStop(0.3, '#1a3a5c'); // Mid-upper
gradient.addColorStop(0.5, '#4a7aaa'); // Mid
gradient.addColorStop(0.7, '#ff9966'); // Horizon - warm
gradient.addColorStop(0.85, '#ffcc88'); // Near horizon
gradient.addColorStop(1, '#ffe8bb'); // Bottom
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 512, 512);
const skyTex = new THREE.CanvasTexture(canvas);
const skyMat = new THREE.MeshBasicMaterial({
map: skyTex,
side: THREE.BackSide
});
skyDome = new THREE.Mesh(skyGeo, skyMat);
scene.add(skyDome);
}
// --- Runway ---
function createRunway() {
const runwayGroup = new THREE.Group();
// Main runway surface
const rwGeo = new THREE.PlaneGeometry(40, 1200);
rwGeo.rotateX(-Math.PI / 2);
const rwMat = new THREE.MeshStandardMaterial({
color: 0x333333,
roughness: 0.9,
metalness: 0.05
});
const rwMesh = new THREE.Mesh(rwGeo, rwMat);
rwMesh.position.set(0, 1.5, 250);
runwayGroup.add(rwMesh);
// Runway markings - center line dashes
for (let z = -400; z < 900; z += 30) {
const dashGeo = new THREE.PlaneGeometry(1, 12);
dashGeo.rotateX(-Math.PI / 2);
const dashMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const dash = new THREE.Mesh(dashGeo, dashMat);
dash.position.set(0, 1.6, z + 250);
runwayGroup.add(dash);
}
// Edge lines
for (let side of [-18, 18]) {
const edgeGeo = new THREE.PlaneGeometry(1, 1200);
edgeGeo.rotateX(-Math.PI / 2);
const edgeMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const edge = new THREE.Mesh(edgeGeo, edgeMat);
edge.position.set(side, 1.6, 250);
runwayGroup.add(edge);
}
// Threshold markings
for (let i = -3; i <= 3; i++) {
if (i === 0) continue;
const tGeo = new THREE.PlaneGeometry(4, 15);
tGeo.rotateX(-Math.PI / 2);
const tMat = new THREE.MeshBasicMaterial({ color: 0xffffff });
const t1 = new THREE.Mesh(tGeo, tMat);
t1.position.set(i * 6, 1.6, -350 + 250);
runwayGroup.add(t1);
const t2 = new THREE.Mesh(tGeo, tMat);
t2.position.set(i * 6, 1.6, 850 + 250);
runwayGroup.add(t2);
}
// Runway numbers
const numCanvas = document.createElement('canvas');
numCanvas.width = 256;
numCanvas.height = 128;
const nctx = numCanvas.getContext('2d');
nctx.fillStyle = '#333';
nctx.fillRect(0, 0, 256, 128);
nctx.font = 'bold 72px Arial';
nctx.fillStyle = '#fff';
nctx.textAlign = 'center';
nctx.fillText('09', 128, 85);
const numTex = new THREE.CanvasTexture(numCanvas);
const numMat = new THREE.MeshBasicMaterial({ map: numTex });
for (let z of [-370 + 250, 870 + 250]) {
const numGeo = new THREE.PlaneGeometry(20, 10);
numGeo.rotateX(-Math.PI / 2);
const numMesh = new THREE.Mesh(numGeo, numMat);
numMesh.position.set(0, 1.6, z);
runwayGroup.add(numMesh);
}
// Taxiway lights (emissive)
for (let side of [-22, 22]) {
for (let z = -450; z < 950; z += 30) {
const lightGeo = new THREE.SphereGeometry(0.3, 8, 8);
const lightMat = new THREE.MeshBasicMaterial({ color: 0x00ff44 });
const light = new THREE.Mesh(lightGeo, lightMat);
light.position.set(side, 1.7, z + 250);
runwayGroup.add(light);
}
}
scene.add(runwayGroup);
}
// --- Aircraft Model Builder ---
function createAircraftModel(type) {
const group = new THREE.Group();
const ac = AIRCRAFT[type];
const bodyColor = ac.color;
const accentColor = ac.accentColor;
// Materials
const bodyMat = new THREE.MeshStandardMaterial({
color: bodyColor, roughness: 0.3, metalness: 0.7
});
const darkMat = new THREE.MeshStandardMaterial({
color: 0x222222, roughness: 0.5, metalness: 0.8
});
const glassMat = new THREE.MeshStandardMaterial({
color: 0x88ccff, roughness: 0.1, metalness: 0.3, transparent: true, opacity: 0.6
});
const accentMat = new THREE.MeshStandardMaterial({
color: accentColor, roughness: 0.2, metalness: 0.5, emissive: accentColor, emissiveIntensity: 0.1
});
// Fuselage - main body
const fuselageGeo = new THREE.CylinderGeometry(0.8, 0.6, 14, 12);
fuselageGeo.rotateZ(Math.PI / 2);
const fuselage = new THREE.Mesh(fuselageGeo, bodyMat);
group.add(fuselage);
// Nose cone
const noseGeo = new THREE.ConeGeometry(0.8, 4, 12);
noseGeo.rotateZ(-Math.PI / 2);
const nose = new THREE.Mesh(noseGeo, bodyMat);
nose.position.x = 9;
group.add(nose);
// Cockpit canopy
const cockpitGeo = new THREE.SphereGeometry(1.0, 12, 8, 0, Math.PI * 2, 0, Math.PI / 2);
const cockpit = new THREE.Mesh(cockpitGeo, glassMat);
cockpit.position.set(3, 0.6, 0);
group.add(cockpit);
// Wings - different shapes per type
if (type === 'interceptor') {
// Delta wing
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.lineTo(-8, 2);
wingShape.lineTo(-6, 5);
wingShape.lineTo(0, 4);
const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.15, bevelEnabled: true, bevelThickness: 0.05, bevelSize: 0.05 });
const leftWing = new THREE.Mesh(wingGeo, bodyMat);
leftWing.position.set(2, -0.3, 0);
leftWing.rotation.x = Math.PI / 2;
group.add(leftWing);
const rightWing = leftWing.clone();
rightWing.scale.z = -1;
group.add(rightWing);
} else if (type === 'fighterBomber') {
// Swept wing with larger area
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.lineTo(-12, 3);
wingShape.lineTo(-10, 7);
wingShape.lineTo(0, 5);
const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.2, bevelEnabled: true, bevelThickness: 0.05, bevelSize: 0.05 });
const leftWing = new THREE.Mesh(wingGeo, bodyMat);
leftWing.position.set(1, -0.3, 0);
leftWing.rotation.x = Math.PI / 2;
group.add(leftWing);
const rightWing = leftWing.clone();
rightWing.scale.z = -1;
group.add(rightWing);
// Hardpoints under wings
for (let side of [-8, 8]) {
const hardpointGeo = new THREE.CylinderGeometry(0.15, 0.15, 2, 6);
const hardpoint = new THREE.Mesh(hardpointGeo, darkMat);
hardpoint.position.set(-1, -1.5, side);
group.add(hardpoint);
}
} else { // stealthFighter
// Diamond/diamond wing shape
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.lineTo(-9, 2);
wingShape.lineTo(-7, 6);
wingShape.lineTo(0, 4.5);
const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.18, bevelEnabled: true, bevelThickness: 0.05, bevelSize: 0.05 });
const leftWing = new THREE.Mesh(wingGeo, bodyMat);
leftWing.position.set(2, -0.3, 0);
leftWing.rotation.x = Math.PI / 2;
group.add(leftWing);
const rightWing = leftWing.clone();
rightWing.scale.z = -1;
group.add(rightWing);
}
// Tail fins (vertical stabilizers)
if (type === 'stealthFighter') {
// Canted vertical tails
for (let side of [-1, 1]) {
const tailShape = new THREE.Shape();
tailShape.moveTo(0, 0);
tailShape.lineTo(-3, 4);
tailShape.lineTo(-2, 5);
tailShape.lineTo(0, 3.5);
const tailGeo = new THREE.ExtrudeGeometry(tailShape, { depth: 0.1, bevelEnabled: true });
const tail = new THREE.Mesh(tailGeo, bodyMat);
tail.position.set(-6, -0.2, side * 2);
tail.rotation.x = Math.PI / 2;
tail.rotation.z = side * 0.3;
group.add(tail);
}
} else {
// Standard vertical stabilizer
const vTailShape = new THREE.Shape();
vTailShape.moveTo(0, 0);
vTailShape.lineTo(-4, 5);
vTailShape.lineTo(-3, 5.5);
vTailShape.lineTo(0, 4);
const vTailGeo = new THREE.ExtrudeGeometry(vTailShape, { depth: 0.12, bevelEnabled: true });
const vTail = new THREE.Mesh(vTailGeo, bodyMat);
vTail.position.set(-6, -0.3, 0);
vTail.rotation.x = Math.PI / 2;
group.add(vTail);
// Horizontal stabilizer
for (let side of [-1, 1]) {
const hTailShape = new THREE.Shape();
hTailShape.moveTo(0, 0);
hTailShape.lineTo(-4, 1.5);
hTailShape.lineTo(-3, 2);
hTailShape.lineTo(0, 1.5);
const hTailGeo = new THREE.ExtrudeGeometry(hTailShape, { depth: 0.1, bevelEnabled: true });
const hTail = new THREE.Mesh(hTailGeo, bodyMat);
hTail.position.set(-6, -0.3, side * 2);
hTail.rotation.x = Math.PI / 2;
group.add(hTail);
}
}
// Engine exhaust/nozzles
for (let z of [-1.5, 1.5]) {
const nozzleGeo = new THREE.CylinderGeometry(0.4, 0.6, 2, 8);
nozzleGeo.rotateZ(Math.PI / 2);
const nozzle = new THREE.Mesh(nozzleGeo, darkMat);
nozzle.position.set(-7.5, 0, z);
group.add(nozzle);
// Exhaust glow
const exhaustGeo = new THREE.CircleGeometry(0.35, 8);
const exhaustMat = new THREE.MeshBasicMaterial({
color: accentColor, transparent: true, opacity: 0.6, side: THREE.DoubleSide
});
const exhaust = new THREE.Mesh(exhaustGeo, exhaustMat);
exhaust.position.set(-8.5, 0, z);
exhaust.rotation.y = Math.PI / 2;
exhaust.name = 'exhaust';
group.add(exhaust);
}
// Landing gear (simplified)
const gearMat = new THREE.MeshStandardMaterial({ color: 0x444444, metalness: 0.9 });
for (let x of [5, -3]) {
const strutGeo = new THREE.CylinderGeometry(0.08, 0.08, 2, 6);
const strut = new THREE.Mesh(strutGeo, gearMat);
strut.position.set(x, -1.5, 0);
group.add(strut);
const wheelGeo = new THREE.CylinderGeometry(0.3, 0.3, 0.2, 8);
wheelGeo.rotateZ(Math.PI / 2);
const wheel = new THREE.Mesh(wheelGeo, darkMat);
wheel.position.set(x, -2.5, 0);
group.add(wheel);
}
// Nose gear
const noseStrut = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 1.5, 6), gearMat);
noseStrut.position.set(7, -1.2, 0);
group.add(noseStrut);
// Accent stripes
const stripeGeo = new THREE.BoxGeometry(8, 0.05, 0.3);
for (let z of [-0.9, 0.9]) {
const stripe = new THREE.Mesh(stripeGeo, accentMat);
stripe.position.set(2, 0.75, z);
group.add(stripe);
}
// Scale the model appropriately
group.scale.set(1.5, 1.5, 1.5);
return group;
}
// --- Enemy Aircraft (simpler models) ---
function createEnemyAircraft() {
const group = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0xcc3333, roughness: 0.4, metalness: 0.6 });
const darkMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.5, metalness: 0.8 });
// Simple fighter shape
const fuselageGeo = new THREE.CylinderGeometry(0.7, 0.5, 12, 10);
fuselageGeo.rotateZ(Math.PI / 2);
group.add(new THREE.Mesh(fuselageGeo, bodyMat));
const noseGeo = new THREE.ConeGeometry(0.7, 3, 10);
noseGeo.rotateZ(-Math.PI / 2);
const nose = new THREE.Mesh(noseGeo, bodyMat);
nose.position.x = 7.5;
group.add(nose);
// Wings
for (let side of [-1, 1]) {
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.lineTo(-8, 2);
wingShape.lineTo(-6, 5);
wingShape.lineTo(0, 4);
const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.15 });
const wing = new THREE.Mesh(wingGeo, bodyMat);
wing.position.set(2, -0.3, 0);
wing.rotation.x = Math.PI / 2;
if (side === -1) wing.scale.z = -1;
group.add(wing);
}
// Tail
const tailShape = new THREE.Shape();
tailShape.moveTo(0, 0);
tailShape.lineTo(-3, 4);
tailShape.lineTo(-2, 4.5);
tailShape.lineTo(0, 3.5);
const tailGeo = new THREE.ExtrudeGeometry(tailShape, { depth: 0.1 });
const tail = new THREE.Mesh(tailGeo, bodyMat);
tail.position.set(-5, -0.2, 0);
tail.rotation.x = Math.PI / 2;
group.add(tail);
// Red accent
const stripeGeo = new THREE.BoxGeometry(6, 0.05, 0.3);
const stripeMat = new THREE.MeshBasicMaterial({ color: 0xff0000 });
for (let z of [-0.8, 0.8]) {
const s = new THREE.Mesh(stripeGeo, stripeMat);
s.position.set(1, 0.75, z);
group.add(s);
}
group.scale.set(1.3, 1.3, 1.3);
return group;
}
// --- Particle System ---
function createSmokeParticle(pos) {
const geo = new THREE.SphereGeometry(0.5 + Math.random() * 0.5, 6, 6);
const mat = new THREE.MeshBasicMaterial({
color: 0x888888, transparent: true, opacity: 0.4
});
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(pos);
scene.add(mesh);
smokeParticles.push({
mesh, life: 1.0, decay: 0.01 + Math.random() * 0.02,
velocity: new THREE.Vector3((Math.random()-0.5)*2, (Math.random()-0.5)*2, (Math.random()-0.5)*2)
});
}
function updateParticles(dt) {
for (let i = smokeParticles.length - 1; i >= 0; i--) {
const p = smokeParticles[i];
p.life -= p.decay;
p.mesh.position.add(p.velocity.clone().multiplyScalar(dt));
p.mesh.material.opacity = Math.max(0, p.life * 0.4);
p.mesh.scale.setScalar(1 + (1 - p.life) * 3);
if (p.life <= 0) {
scene.remove(p.mesh);
smokeParticles.splice(i, 1);
}
}
}
// --- Physics Engine ---
const GRAVITY = 9.81; // m/s²
const SEA_LEVEL_DENSITY = 1.225; // kg/m³
const SCALE_FACTOR = 0.3; // Scale factor for game feel (not full real-world scale)
function getAirDensity(altitudeMeters) {
// ISA atmosphere model approximation
const h = Math.max(0, altitudeMeters);
return SEA_LEVEL_DENSITY * Math.exp(-h / 8500);
}
function computeLiftCoeff(angleOfAttackRad) {
const ac = AIRCRAFT[selectedAircraft];
// Linear region: Cl = Cl_alpha * alpha (up to stall angle ~15 degrees)
const stallAngle = Math.PI / 6; // ~15 degrees
const clAlpha = ac.liftCoeffMax / stallAngle;
if (Math.abs(angleOfAttackRad) < stallAngle) {
return clAlpha * angleOfAttackRad + 0.2; // +0.2 for zero-alpha lift
} else {
// Post-stall: rapid decrease in lift
const excess = Math.abs(angleOfAttackRad) - stallAngle;
return ac.liftCoeffMax * (1 - excess * 3);
}
}
function computeDragCoeff(angleOfAttackRad, liftCoeff) {
const ac = AIRCRAFT[selectedAircraft];
// Parasite drag + induced drag
const cdParasite = ac.dragProfile.cd0;
const cdInduced = (liftCoeff * liftCoeff) / (Math.PI * 0.9 * 8); // AR ~ 8 for typical fighter
return cdParasite + cdInduced;
}
function updatePhysics(dt) {
if (gameState !== 'playing') return;
const ac = AIRCRAFT[selectedAircraft];
dt = Math.min(dt, 0.05); // Cap delta time to prevent physics explosions
// --- Input Processing ---
const pitchInput = input.pitch * ac.turnRate;
const rollInput = input.roll * ac.rollRate;
const yawInput = input.yaw * ac.yawRate;
// Throttle management
if (input.throttleUp) physics.throttle = Math.min(1, physics.throttle + dt * 0.8);
if (input.throttleDown) physics.throttle = Math.max(0, physics.throttle - dt * 0.8);
// Engine RPM response (with lag)
const targetRPM = physics.throttle;
physics.engineRPM += (targetRPM - physics.engineRPM) * dt * 3;
// --- Atmospheric Properties ---
const altitudeMeters = Math.max(0, physics.position.y);
const airDensity = getAirDensity(altitudeMeters);
// --- Velocity in aircraft frame ---
const worldVelocity = physics.velocity.clone();
const localVelocity = worldVelocity.clone().applyQuaternion(physics.orientation.clone().invert());
// Airspeed (magnitude of velocity)
physics.airspeed = worldVelocity.length();
// Angle of attack: angle between velocity vector and aircraft's x-axis in the pitch plane
const forwardDir = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.orientation);
const velDir = worldVelocity.clone().normalize();
if (physics.airspeed > 1) {
// Project velocity onto aircraft's vertical plane
const rightDir = new THREE.Vector3(0, 0, 1).applyQuaternion(physics.orientation);
const upDir = new THREE.Vector3(0, 1, 0).applyQuaternion(physics.orientation);
const velForward = worldVelocity.dot(forwardDir);
const velUp = worldVelocity.dot(upDir);
physics.angleOfAttack = Math.atan2(velUp, Math.max(1, velForward));
} else {
physics.angleOfAttack = 0;
}
// --- Aerodynamic Forces (in aircraft local frame) ---
const vSq = physics.airspeed * physics.airspeed;
const S = ac.wingArea;
const rho = airDensity;
// Lift coefficient based on AoA
const cl = computeLiftCoeff(physics.angleOfAttack);
// Drag coefficient
const cd = computeDragCoeff(physics.angleOfAttack, cl);
// Lift force (upward in aircraft frame)
const liftForce = 0.5 * rho * vSq * S * cl;
// Drag force (backward along velocity direction)
const dragForce = 0.5 * rho * vSq * S * cd;
// Thrust force
const thrustForce = ac.maxThrust * physics.throttle * SCALE_FACTOR;
// Weight
const weight = ac.mass * GRAVITY;
// --- Apply Forces ---
// Transform forces to world frame
// Lift is in aircraft's up direction
const liftDir = new THREE.Vector3(0, 1, 0).applyQuaternion(physics.orientation);
const liftVec = liftDir.multiplyScalar(liftForce * SCALE_FACTOR);
// Drag opposes velocity
let dragVec;
if (physics.airspeed > 0.5) {
dragVec = velDir.clone().multiplyScalar(-dragForce * SCALE_FACTOR);
} else {
dragVec = new THREE.Vector3(0, 0, 0);
}
// Thrust is in aircraft's forward direction
const thrustVec = forwardDir.multiplyScalar(thrustForce);
// Weight (downward)
const weightVec = new THREE.Vector3(0, -weight, 0);
// Total force
const totalForce = new THREE.Vector3();
totalForce.add(liftVec).add(dragVec).add(thrustVec).add(weightVec);
// Ground friction when on ground
if (physics.onGround) {
const frictionCoeff = 0.8;
const normalForce = Math.max(0, weight - liftForce * SCALE_FACTOR);
const frictionForce = normalForce * frictionCoeff;
if (worldVelocity.length() > 1) {
totalForce.add(worldVelocity.clone().normalize().multiplyScalar(-frictionForce));
}
// Brake force
if (input.brake && physics.airspeed > 5) {
const brakeForce = normalForce * 0.6;
totalForce.add(worldVelocity.clone().normalize().multiplyScalar(-brakeForce));
}
}
// --- Newton's Second Law: F = ma ---
const acceleration = totalForce.divideScalar(ac.mass);
// Update velocity
physics.velocity.add(acceleration.multiplyScalar(dt));
// --- Angular Dynamics (Inertia) ---
// Apply control surface deflections as angular accelerations
const effectiveControlAuthority = Math.min(1, physics.airspeed / 50); // Controls less effective at low speed
// Target angular velocities based on input
const targetPitchRate = pitchInput * effectiveControlAuthority;
const targetRollRate = rollInput * effectiveControlAuthority;
const targetYawRate = yawInput * effectiveControlAuthority +
(input.roll * 0.3 * effectiveControlAuthority); // Adverse yaw from rolling
// Angular damping (aerodynamic stability)
const angularDamping = 2.5;
physics.angularVelocity.x += (targetPitchRate - physics.angularVelocity.x) * dt * angularDamping;
physics.angularVelocity.y += (targetYawRate - physics.angularVelocity.y) * dt * angularDamping;
physics.angularVelocity.z += (targetRollRate - physics.angularVelocity.z) * dt * angularDamping;
// Additional damping at high angles of attack
if (Math.abs(physics.angleOfAttack) > Math.PI / 8) {
const aoaFactor = (Math.abs(physics.angleOfAttack) - Math.PI / 8) * 5;
physics.angularVelocity.multiplyScalar(1 - aoaFactor * dt);
}
// --- Update Orientation ---
// Convert angular velocity to quaternion rotation
const angVelVec = new THREE.Vector3(
physics.angularVelocity.x,
physics.angularVelocity.y,
physics.angularVelocity.z
);
const halfAngle = angVelVec.length() * dt / 2;
if (halfAngle > 0.001) {
const quatDelta = new THREE.Quaternion();
quatDelta.setFromAxisAngle(angVelVec.normalize(), angVelVec.length() * dt);
physics.orientation.multiply(quatDelta);
physics.orientation.normalize();
}
// --- Update Position ---
physics.position.add(physics.velocity.clone().multiplyScalar(dt));
// --- Ground Collision ---
const terrainHeight = getTerrainHeight(physics.position.x, physics.position.z);
const aircraftAltitude = physics.position.y;
if (aircraftAltitude <= terrainHeight + 2) {
physics.position.y = terrainHeight + 2;
// Ground impact
if (physics.velocity.y < -5) {
// Hard landing/crash
const impactForce = Math.abs(physics.velocity.y);
physics.damage += impactForce * 0.1;
// Bounce slightly
physics.velocity.y = Math.abs(physics.velocity.y) * 0.2;
if (physics.damage > 5) {
showMessage("CRITICAL DAMAGE - MISSION FAILED");
setTimeout(() => endGame(), 3000);
}
} else {
physics.velocity.y = Math.max(0, physics.velocity.y);
}
// Check if on ground (low speed + low altitude)
const horizontalSpeed = Math.sqrt(physics.velocity.x ** 2 + physics.velocity.z ** 2);
physics.onGround = horizontalSpeed < 30 && aircraftAltitude <= terrainHeight + 5;
// Dampen lateral movement on ground
if (physics.onGround) {
physics.velocity.x *= 0.98;
physics.velocity.z *= 0.98;
// Level the aircraft gradually when on ground
const levelPitch = -physics.angularVelocity.x * dt * 2;
const levelRoll = -physics.angularVelocity.z * dt * 2;
physics.angularVelocity.x += levelPitch;
physics.angularVelocity.z += levelRoll;
}
} else {
physics.onGround = false;
}
// --- Stall Detection ---
const stallSpeedMs = ac.stallSpeed * 0.514444; // Convert knots to m/s
physics.isStalled = (physics.airspeed < stallSpeedMs && !physics.onGround) ||
Math.abs(physics.angleOfAttack) > Math.PI / 5;
// --- G-Force Calculation ---
const gForceVec = totalForce.clone().divideScalar(ac.mass);
physics.gForce = Math.sqrt(gForceVec.x ** 2 + (gForceVec.y + GRAVITY) ** 2 + gForceVec.z ** 2) / GRAVITY;
// --- Altitude and Heading ---
physics.altitude = aircraftAltitude * 3.28084; // meters to feet
const headingRad = Math.atan2(physics.velocity.x, physics.velocity.z);
physics.heading = ((headingRad * 180 / Math.PI) + 360) % 360;
// --- Smoke trail when at high AoA or low speed ---
if (physics.isStalled && !physics.onGround && Math.random() < 0.3) {
createSmokeParticle(physics.position.clone().add(
new THREE.Vector3(-12, 0, 0).applyQuaternion(physics.orientation)
));
}
// --- Engine heat ---
physics.engineHeat = physics.throttle * (1 + altitudeMeters / 5000);
}
// --- Enemy AI ---
function spawnEnemies(count) {
for (let i = 0; i < count; i++) {
const model = createEnemyAircraft();
// Random position around the player
const angle = Math.random() * Math.PI * 2;
const dist = 1500 + Math.random() * 2000;
const alt = 300 + Math.random() * 800;
model.position.set(
physics.position.x + Math.cos(angle) * dist,
alt,
physics.position.z + Math.sin(angle) * dist
);
scene.add(model);
enemyAircraft.push({
mesh: model,
velocity: new THREE.Vector3((Math.random()-0.5)*100, 20, (Math.random()-0.5)*100),
orientation: new THREE.Quaternion(),
health: 100,
state: 'patrol', // patrol, engage, evade
targetHeading: Math.random() * Math.PI * 2,
turnTimer: 0,
alive: true
});
}
enemiesRemaining = count;
}
function updateEnemies(dt) {
const playerPos = physics.position.clone();
for (const enemy of enemyAircraft) {
if (!enemy.alive) continue;
const toPlayer = playerPos.clone().sub(enemy.mesh.position);
const distToPlayer = toPlayer.length();
// State machine
if (distToPlayer < 800) {
enemy.state = 'engage';
} else if (enemy.health < 30) {
enemy.state = 'evade';
} else {
enemy.state = 'patrol';
}
// AI behavior
let targetDir;
switch(enemy.state) {
case 'patrol':
enemy.turnTimer -= dt;
if (enemy.turnTimer <= 0) {
enemy.targetHeading += (Math.random() - 0.5) * Math.PI;
enemy.turnTimer = 3 + Math.random() * 5;
}
targetDir = new THREE.Vector3(
Math.sin(enemy.targetHeading),
0,
Math.cos(enemy.targetHeading)
);
break;
case 'engage':
// Try to get behind the player
const playerVelDir = physics.velocity.clone().normalize();
const idealPos = playerPos.clone().sub(playerVelDir.multiplyScalar(200));
targetDir = idealPos.clone().sub(enemy.mesh.position).normalize();
// Fire at player if close enough and roughly aligned
if (distToPlayer < 400 && Math.random() < dt * 0.5) {
// Visual laser effect
createLaserEffect(enemy.mesh.position, playerPos);
// Hit chance based on distance
if (Math.random() < 0.15) {
physics.damage += 0.5;
showDamageFlash();
}
}
break;
case 'evade':
targetDir = toPlayer.clone().normalize().multiplyScalar(-1);
break;
}
// Apply AI movement with inertia
const currentHeading = new THREE.Vector3(1, 0, 0).applyQuaternion(enemy.orientation);
const turnRate = 1.5 * dt;
// Smoothly rotate towards target direction
const dotProduct = Math.max(-1, Math.min(1, currentHeading.dot(targetDir)));
const angleToTurn = Math.acos(dotProduct);
if (angleToTurn > 0.01) {
const axis = new THREE.Vector3().crossVectors(currentHeading, targetDir).normalize();
const rotationQuat = new THREE.Quaternion().setFromAxisAngle(axis, Math.min(angleToTurn, turnRate));
enemy.orientation.multiply(rotationQuat);
}
// Maintain altitude
const upDir = new THREE.Vector3(0, 1, 0).applyQuaternion(enemy.orientation);
if (enemy.mesh.position.y < 200) {
const climbQuat = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 0, 1), dt * 0.5
);
enemy.orientation.multiply(climbQuat);
} else if (enemy.mesh.position.y > 600) {
const descendQuat = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(0, 0, 1), -dt * 0.5
);
enemy.orientation.multiply(descendQuat);
}
// Move forward
const speed = 80 + Math.random() * 20;
const moveDir = new THREE.Vector3(1, 0, 0).applyQuaternion(enemy.orientation);
enemy.mesh.position.add(moveDir.multiplyScalar(speed * dt));
// Update mesh transform
enemy.mesh.quaternion.copy(enemy.orientation);
// Keep terrain clearance
const th = getTerrainHeight(enemy.mesh.position.x, enemy.mesh.position.z);
if (enemy.mesh.position.y < th + 50) {
enemy.mesh.position.y = th + 50;
}
}
}
function createLaserEffect(from, to) {
const dir = to.clone().sub(from).normalize();
const midPoint = from.clone().add(dir.multiplyScalar(to.distanceTo(from) * 0.5));
const geo = new THREE.SphereGeometry(2, 6, 6);
const mat = new THREE.MeshBasicMaterial({ color: 0xff4444 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.copy(midPoint);
scene.add(mesh);
setTimeout(() => {
scene.remove(mesh);
geo.dispose();
mat.dispose();
}, 200);
}
// --- Player Shooting ---
function playerShoot() {
if (gameState !== 'playing' || selectedScenario !== 'dogfight') return;
const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.orientation);
const fromPos = physics.position.clone().add(forward.multiplyScalar(15));
// Check if any enemy is in crosshair
for (const enemy of enemyAircraft) {
if (!enemy.alive) continue;
const toEnemy = enemy.mesh.position.clone().sub(physics.position);
const dist = toEnemy.length();
const dirToEnemy = toEnemy.normalize();
// Check alignment with forward direction
const alignment = forward.dot(dirToEnemy);
if (alignment > 0.95 && dist < 600) {
enemy.health -= 25 + Math.random() * 15;
// Hit effect
createLaserEffect(fromPos, enemy.mesh.position.clone());
if (enemy.health <= 0) {
enemy.alive = false;
scene.remove(enemy.mesh);
enemiesRemaining--;
score += 1000;
showMessage("ENEMY DESTROYED! +1000");
// Explosion particles
for (let i = 0; i < 20; i++) {
createSmokeParticle(enemy.mesh.position.clone());
}
} else {
score += 100;
}
break; // Only hit one enemy per shot
}
}
// Visual muzzle flash
const flashGeo = new THREE.SphereGeometry(3, 8, 8);
const flashMat = new THREE.MeshBasicMaterial({ color: 0xffaa44 });
const flash = new THREE.Mesh(flashGeo, flashMat);
flash.position.copy(fromPos);
scene.add(flash);
setTimeout(() => {
scene.remove(flash);
flashGeo.dispose();
flashMat.dispose();
}, 100);
}
// --- Camera System ---
const cameraOffset = new THREE.Vector3(-25, 8, 0); // Behind and above aircraft
let cameraSmoothPos = new THREE.Vector3();
let cameraSmoothTarget = new THREE.Vector3();
function updateCamera() {
if (!aircraftGroup) return;
// Calculate desired camera position based on aircraft orientation
const offsetTransformed = cameraOffset.clone().applyQuaternion(physics.orientation);
const desiredPos = physics.position.clone().add(offsetTransformed);
// Smooth camera follow (with slight lag for realism)
const lerpFactor = 1 - Math.pow(0.02, clock.getDelta() || 0.016);
cameraSmoothPos.lerp(desiredPos, lerpFactor * 3);
// Camera target is slightly ahead of aircraft
const lookAhead = new THREE.Vector3(30, 2, 0).applyQuaternion(physics.orientation);
cameraSmoothTarget.copy(physics.position.clone().add(lookAhead));
camera.position.lerp(cameraSmoothPos, 0.15);
// Smoothly rotate camera to look at target
const currentLook = new THREE.Vector3();
camera.getWorldDirection(currentLook);
const desiredLook = cameraSmoothTarget.clone().sub(camera.position).normalize();
currentLook.lerp(desiredLook, 0.1);
camera.lookAt(camera.position.clone().add(currentLook));
}
// --- HUD Rendering ---
const hudCanvas = document.getElementById('hud-canvas');
const hudCtx = hudCanvas.getContext('2d');
function resizeHUD() {
hudCanvas.width = window.innerWidth;
hudCanvas.height = window.innerHeight;
}
function renderHUD() {
const w = hudCanvas.width;
const h = hudCanvas.height;
hudCtx.clearRect(0, 0, w, h);
// --- Attitude Indicator (Artificial Horizon) ---
const cx = w / 2;
const cy = h / 2;
const horizonRadius = Math.min(w, h) * 0.35;
hudCtx.save();
hudCtx.beginPath();
hudCtx.arc(cx, cy, horizonRadius + 10, 0, Math.PI * 2);
hudCtx.clip();
// Get aircraft pitch and roll from orientation
const euler = new THREE.Euler().setFromQuaternion(physics.orientation, 'YXZ');
const pitchDeg = euler.x * 180 / Math.PI;
const rollDeg = euler.z * 180 / Math.PI;
hudCtx.translate(cx, cy);
hudCtx.rotate(-rollDeg * Math.PI / 180);
// Sky (above horizon)
const pitchOffset = pitchDeg * (horizonRadius / 45);
hudCtx.fillStyle = '#1a3a6c';
hudCtx.fillRect(-horizonRadius*2, -horizonRadius*2 + pitchOffset, horizonRadius*4, horizonRadius*2);
// Ground (below horizon)
hudCtx.fillStyle = '#5a3a1a';
hudCtx.fillRect(-horizonRadius*2, pitchOffset, horizonRadius*4, horizonRadius*2);
// Horizon line
hudCtx.strokeStyle = '#ffffff';
hudCtx.lineWidth = 2;
hudCtx.beginPath();
hudCtx.moveTo(-horizonRadius, pitchOffset);
hudCtx.lineTo(horizonRadius, pitchOffset);
hudCtx.stroke();
// Pitch ladder marks
hudCtx.strokeStyle = 'rgba(255,255,255,0.6)';
hudCtx.lineWidth = 1;
hudCtx.font = '10px Courier New';
hudCtx.fillStyle = '#ffffff';
hudCtx.textAlign = 'center';
for (let deg = -30; deg <= 30; deg += 5) {
if (deg === 0) continue;
const y = pitchOffset - deg * (horizonRadius / 45);
const lineWidth = Math.abs(deg) % 10 === 0 ? 60 : 30;
hudCtx.beginPath();
hudCtx.moveTo(-lineWidth, y);
hudCtx.lineTo(lineWidth, y);
hudCtx.stroke();
if (Math.abs(deg) % 10 === 0) {
hudCtx.fillText(Math.abs(deg).toString(), lineWidth + 15, y + 3);
hudCtx.fillText(Math.abs(deg).toString(), -lineWidth - 15, y + 3);
}
}
hudCtx.restore();
// --- Aircraft Symbol (fixed wings) ---
hudCtx.strokeStyle = '#ffcc00';
hudCtx.lineWidth = 2;
// Left wing
hudCtx.beginPath();
hudCtx.moveTo(cx - 50, cy);
hudCtx.lineTo(cx - 30, cy);
hudCtx.stroke();
// Right wing
hudCtx.beginPath();
hudCtx.moveTo(cx + 30, cy);
hudCtx.lineTo(cx + 50, cy);
hudCtx.stroke();
// Center dot
hudCtx.fillStyle = '#ffcc00';
hudCtx.beginPath();
hudCtx.arc(cx, cy, 3, 0, Math.PI * 2);
hudCtx.fill();
// --- Flight Path Vector ---
const fpvX = cx + (physics.velocity.x / 150) * horizonRadius;
const fpvY = cy - (physics.velocity.y / 150) * horizonRadius;
hudCtx.strokeStyle = 'rgba(0,229,255,0.7)';
hudCtx.lineWidth = 1.5;
hudCtx.beginPath();
hudCtx.arc(fpvX, fpvY, 8, 0, Math.PI * 2);
hudCtx.stroke();
// --- Heading Tape (top of screen) ---
const headingTapeH = 40;
const headingTapeW = w * 0.6;
const headingTapeX = cx - headingTapeW / 2;
const headingTapeY = h * 0.15;
hudCtx.fillStyle = 'rgba(0,20,40,0.5)';
hudCtx.fillRect(headingTapeX, headingTapeY, headingTapeW, headingTapeH);
hudCtx.strokeStyle = 'rgba(0,229,255,0.3)';
hudCtx.lineWidth = 1;
hudCtx.strokeRect(headingTapeX, headingTapeY, headingTapeW, headingTapeH);
hudCtx.font = '11px Courier New';
hudCtx.fillStyle = '#88aacc';
hudCtx.textAlign = 'center';
const startHeading = Math.floor(physics.heading / 10) * 10 - 30;
for (let hVal = startHeading; hVal <= startHeading + 60; hVal += 5) {
const headingNorm = ((hVal % 360) + 360) % 360;
const xOff = ((headingNorm - physics.heading + 180) % 360 - 180) / 60 * headingTapeW;
if (xOff > -20 && xOff < headingTapeW + 20) {
const markX = cx + xOff;
hudCtx.beginPath();
hudCtx.moveTo(markX, headingTapeY + headingTapeH - 5);
hudCtx.lineTo(markX, headingTapeY + headingTapeH);
hudCtx.stroke();
if (hVal % 10 === 0) {
const label = headingNorm < 10 ? '0' + Math.round(headingNorm) : Math.round(headingNorm).toString();
hudCtx.fillText(label, markX, headingTapeY + 25);
}
}
}
// Heading pointer triangle
hudCtx.fillStyle = '#ffcc00';
hudCtx.beginPath();
hudCtx.moveTo(cx, headingTapeY - 5);
hudCtx.lineTo(cx - 6, headingTapeY - 12);
hudCtx.lineTo(cx + 6, headingTapeY - 12);
hudCtx.closePath();
hudCtx.fill();
// --- Speed Tape (left side) ---
const speedTapeW = 50;
const speedTapeH = h * 0.4;
const speedTapeX = w * 0.12;
const speedTapeY = cy - speedTapeH / 2;
hudCtx.fillStyle = 'rgba(0,20,40,0.5)';
hudCtx.fillRect(speedTapeX, speedTapeY, speedTapeW, speedTapeH);
hudCtx.strokeStyle = 'rgba(0,229,255,0.3)';
hudCtx.strokeRect(speedTapeX, speedTapeY, speedTapeW, speedTapeH);
const speedKts = Math.round(physics.airspeed * 1.94384); // m/s to knots
const startSpeed = Math.floor(speedKts / 20) * 20 - 50;
hudCtx.font = '10px Courier New';
hudCtx.fillStyle = '#88aacc';
hudCtx.textAlign = 'right';
for (let s = startSpeed; s <= startSpeed + 100; s += 10) {
const yOff = ((s - speedKts) / 100) * speedTapeH;
const markY = cy - yOff;
if (markY > speedTapeY && markY < speedTapeY + speedTapeH) {
hudCtx.beginPath();
hudCtx.moveTo(speedTapeX + speedTapeW, markY);
hudCtx.lineTo(speedTapeX + speedTapeW - 8, markY);
hudCtx.stroke();
if (s % 20 === 0) {
hudCtx.fillText(s.toString(), speedTapeX + speedTapeW - 12, markY + 3);
}
}
}
// Speed value box
hudCtx.fillStyle = 'rgba(0,40,80,0.8)';
hudCtx.fillRect(speedTapeX + speedTapeW, cy - 12, 50, 24);
hudCtx.strokeStyle = '#00e5ff';
hudCtx.strokeRect(speedTapeX + speedTapeW, cy - 12, 50, 24);
hudCtx.font = 'bold 14px Courier New';
hudCtx.fillStyle = '#ffffff';
hudCtx.textAlign = 'center';
hudCtx.fillText(speedKts.toString(), speedTapeX + speedTapeW + 25, cy + 5);
// --- Altitude Tape (right side) ---
const altTapeX = w - w * 0.12 - speedTapeW;
hudCtx.fillStyle = 'rgba(0,20,40,0.5)';
hudCtx.fillRect(altTapeX, speedTapeY, speedTapeW, speedTapeH);
hudCtx.strokeStyle = 'rgba(0,229,255,0.3)';
hudCtx.strokeRect(altTapeX, speedTapeY, speedTapeW, speedTapeH);
const altFt = Math.round(physics.altitude);
const startAlt = Math.floor(altFt / 200) * 200 - 500;
hudCtx.textAlign = 'left';
for (let a = startAlt; a <= startAlt + 1000; a += 100) {
const yOff = ((a - altFt) / 1000) * speedTapeH;
const markY = cy - yOff;
if (markY > speedTapeY && markY < speedTapeY + speedTapeH) {
hudCtx.beginPath();
hudCtx.moveTo(altTapeX, markY);
hudCtx.lineTo(altTapeX + 8, markY);
hudCtx.stroke();
if (a % 200 === 0) {
hudCtx.fillText(Math.round(a / 100).toString(), altTapeX + 12, markY + 3);
}
}
}
// Altitude value box
hudCtx.fillStyle = 'rgba(0,40,80,0.8)';
hudCtx.fillRect(altTapeX - 50, cy - 12, 50, 24);
hudCtx.strokeStyle = '#00e5ff';
hudCtx.strokeRect(altTapeX - 50, cy - 12, 50, 24);
hudCtx.font = 'bold 14px Courier New';
hudCtx.fillStyle = '#ffffff';
hudCtx.textAlign = 'center';
hudCtx.fillText(Math.round(altFt / 100).toString(), altTapeX - 25, cy + 5);
// --- Throttle Bar (bottom left) ---
const throttleBarW = 30;
const throttleBarH = h * 0.25;
const throttleBarX = w * 0.12;
const throttleBarY = cy + speedTapeH / 2 + 40;
hudCtx.fillStyle = 'rgba(0,20,40,0.5)';
hudCtx.fillRect(throttleBarX, throttleBarY, throttleBarW, throttleBarH);
hudCtx.strokeStyle = 'rgba(0,229,255,0.3)';
hudCtx.strokeRect(throttleBarX, throttleBarY, throttleBarW, throttleBarH);
const throttleFill = physics.throttle * (throttleBarH - 4);
const throttleGrad = hudCtx.createLinearGradient(0, throttleBarY + throttleBarH, 0, throttleBarY);
throttleGrad.addColorStop(0, '#00aa44');
throttleGrad.addColorStop(0.7, '#ffaa00');
throttleGrad.addColorStop(1, '#ff4444');
hudCtx.fillStyle = throttleGrad;
hudCtx.fillRect(throttleBarX + 2, throttleBarY + throttleBarH - throttleFill, throttleBarW - 4, throttleFill);
// Throttle label
hudCtx.font = '10px Courier New';
hudCtx.fillStyle = '#88aacc';
hudCtx.textAlign = 'center';
hudCtx.fillText('THR', throttleBarX + throttleBarW / 2, throttleBarY - 5);
// --- G-Meter (bottom right) ---
const gMeterX = w - w * 0.12 - speedTapeW;
const gMeterY = cy + speedTapeH / 2 + 40;
hudCtx.fillStyle = 'rgba(0,20,40,0.5)';
hudCtx.fillRect(gMeterX, gMeterY, throttleBarW, throttleBarH);
hudCtx.strokeStyle = 'rgba(0,229,255,0.3)';
hudCtx.strokeRect(gMeterX, gMeterY, throttleBarW, throttleBarH);
const gCenter = gMeterY + throttleBarH / 2;
const gNorm = Math.max(-1, Math.min(1, (physics.gForce - 1) / 5));
const gMarkY = gCenter - gNorm * (throttleBarH / 2 - 4);
const acRef = AIRCRAFT[selectedAircraft];
hudCtx.fillStyle = physics.gForce > acRef.gLimit ? '#ff4444' : '#00e5ff';
hudCtx.beginPath();
hudCtx.arc(gMeterX + throttleBarW / 2, gMarkY, 6, 0, Math.PI * 2);
hudCtx.fill();
// G reference marks
for (let g = -3; g <= 5; g++) {
const gy = gCenter - ((g) / 5) * (throttleBarH / 2 - 4);
if (gy > gMeterY && gy < gMeterY + throttleBarH) {
hudCtx.strokeStyle = 'rgba(255,255,255,0.3)';
hudCtx.beginPath();
hudCtx.moveTo(gMeterX, gy);
hudCtx.lineTo(gMeterX + 6, gy);
hudCtx.stroke();
}
}
hudCtx.font = '10px Courier New';
hudCtx.fillStyle = '#88aacc';
hudCtx.textAlign = 'center';
hudCtx.fillText('G', gMeterX + throttleBarW / 2, gMeterY - 5);
// --- Stall Warning Visual ---
if (physics.isStalled) {
document.getElementById('stall-warning').classList.add('active');
// Red flash on horizon
hudCtx.fillStyle = 'rgba(255,0,0,' + (0.1 + Math.sin(Date.now() * 0.01) * 0.05) + ')';
hudCtx.fillRect(0, 0, w, h);
} else {
document.getElementById('stall-warning').classList.remove('active');
}
// --- G-Force Warning ---
const acData = AIRCRAFT[selectedAircraft];
if (physics.gForce > acData.gLimit * 0.85) {
document.getElementById('g-warning').classList.add('active');
// Yellow vignette at high G
hudCtx.fillStyle = 'rgba(255,170,0,' + ((physics.gForce - acData.gLimit * 0.85) / (acData.gLimit * 0.3)) * 0.15 + ')';
hudCtx.fillRect(0, 0, w, h);
} else {
document.getElementById('g-warning').classList.remove('active');
}
// --- Damage Overlay ---
if (physics.damage > 0) {
const dmgOverlay = document.getElementById('damage-overlay');
dmgOverlay.style.border = `${Math.min(30, physics.damage * 5)}px solid rgba(255,0,0,${Math.min(0.4, physics.damage * 0.08)})`;
} else {
document.getElementById('damage-overlay').style.border = '0px solid rgba(255,0,0,0)';
}
// --- Update DOM HUD elements ---
const speedKtsVal = Math.round(physics.airspeed * 1.94384);
document.getElementById('speed-val').textContent = speedKtsVal;
document.getElementById('alt-val').textContent = Math.round(physics.altitude);
document.getElementById('throttle-val').textContent = Math.round(physics.throttle * 100);
document.getElementById('g-val').textContent = physics.gForce.toFixed(1);
document.getElementById('heading-val').textContent = Math.round(physics.heading);
document.getElementById('aoa-val').textContent = (physics.angleOfAttack * 180 / Math.PI).toFixed(1);
// --- Dogfight HUD extras ---
if (selectedScenario === 'dogfight') {
document.getElementById('score-panel').classList.add('active');
document.getElementById('score-val').textContent = score;
document.getElementById('enemy-count').textContent = enemiesRemaining;
// Enemy indicators on screen edges
for (const enemy of enemyAircraft) {
if (!enemy.alive) continue;
const toEnemy = enemy.mesh.position.clone().sub(camera.position);
toEnemy.applyQuaternion(camera.quaternion.clone().invert());
if (toEnemy.x > 0) { // In front of camera
const screenX = cx + (toEnemy.y / Math.max(1, toEnemy.x)) * horizonRadius;
const screenY = cy - (toEnemy.z / Math.max(1, toEnemy.x)) * horizonRadius;
if (screenX > 30 && screenX < w - 30 && screenY > 30 && screenY < h - 30) {
hudCtx.strokeStyle = '#ff4444';
hudCtx.lineWidth = 2;
// Diamond indicator
const size = Math.max(8, 15 - toEnemy.x / 200);
hudCtx.beginPath();
hudCtx.moveTo(screenX, screenY - size);
hudCtx.lineTo(screenX + size, screenY);
hudCtx.lineTo(screenX, screenY + size);
hudCtx.lineTo(screenX - size, screenY);
hudCtx.closePath();
hudCtx.stroke();
// Distance label
const distKm = (toEnemy.length() / 1000).toFixed(1);
hudCtx.font = '9px Courier New';
hudCtx.fillStyle = '#ff4444';
hudCtx.textAlign = 'center';
hudCtx.fillText(distKm + 'km', screenX, screenY - size - 5);
}
}
}
// Target reticle when enemy is near crosshair
let targetLocked = false;
for (const enemy of enemyAircraft) {
if (!enemy.alive) continue;
const toEnemy = enemy.mesh.position.clone().sub(physics.position);
const dist = toEnemy.length();
const dirToEnemy = toEnemy.normalize();
const forward = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.orientation);
const alignment = forward.dot(dirToEnemy);
if (alignment > 0.95 && dist < 600) {
targetLocked = true;
break;
}
}
// Draw targeting reticle
hudCtx.strokeStyle = targetLocked ? '#ff4444' : 'rgba(255,68,68,0.3)';
hudCtx.lineWidth = 1;
const retSize = 25;
hudCtx.beginPath();
hudCtx.moveTo(cx - retSize, cy);
hudCtx.lineTo(cx - retSize + 8, cy);
hudCtx.stroke();
hudCtx.beginPath();
hudCtx.moveTo(cx + retSize - 8, cy);
hudCtx.lineTo(cx + retSize, cy);
hudCtx.stroke();
hudCtx.beginPath();
hudCtx.moveTo(cx, cy - retSize);
hudCtx.lineTo(cx, cy - retSize + 8);
hudCtx.stroke();
hudCtx.beginPath();
hudCtx.moveTo(cx, cy + retSize - 8);
hudCtx.lineTo(cx, cy + retSize);
hudCtx.stroke();
if (targetLocked) {
hudCtx.fillStyle = '#ff4444';
hudCtx.font = '10px Courier New';
hudCtx.textAlign = 'center';
hudCtx.fillText('TARGET LOCKED', cx, cy + retSize + 15);
}
} else {
document.getElementById('score-panel').classList.remove('active');
// Landing scenario: show runway alignment indicator
if (physics.altitude < 2000 && !physics.onGround) {
const runwayDir = new THREE.Vector3(0, 0, -1); // Runway direction
const aircraftHeading = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.orientation);
const alignmentAngle = Math.acos(Math.max(-1, Math.min(1,
(aircraftHeading.x * runwayDir.x + aircraftHeading.z * runwayDir.z) /
(Math.sqrt(aircraftHeading.x**2+aircraftHeading.z**2) || 1)))) * 180 / Math.PI;
hudCtx.font = '12px Courier New';
hudCtx.fillStyle = alignmentAngle < 5 ? '#00ff88' : alignmentAngle < 15 ? '#ffaa00' : '#ff4444';
hudCtx.textAlign = 'center';
hudCtx.fillText(`RUNWAY ALIGN: ${alignmentAngle.toFixed(1)}°`, cx, h * 0.7);
// Approach speed recommendation
const acData2 = AIRCRAFT[selectedAircraft];
const approachSpeed = Math.round(acData2.stallSpeed * 1.3);
hudCtx.fillStyle = '#88aacc';
hudCtx.fillText(`APPROACH SPD: ${approachSpeed} KTS`, cx, h * 0.7 + 20);
}
// Landing score display
if (landingScore !== null) {
const lsEl = document.getElementById('landing-score');
lsEl.classList.add('active');
let grade, color;
if (landingScore >= 90) { grade = 'EXCELLENT'; color = '#00ff88'; }
else if (landingScore >= 70) { grade = 'GOOD'; color = '#aaff00'; }
else if (landingScore >= 50) { grade = 'FAIR'; color = '#ffaa00'; }
else { grade = 'POOR'; color = '#ff4444'; }
lsEl.innerHTML = `<div style="color:${color}">${grade}</div><div>LANDING SCORE: ${landingScore}/100</div>`;
}
}
// --- Minimap ---
renderMinimap();
}
function renderMinimap() {
const canvas = document.getElementById('minimap-inner');
const ctx = canvas.getContext('2d');
const w = 200, h = 200;
ctx.clearRect(0, 0, w, h);
// Background
ctx.fillStyle = 'rgba(0,20,40,0.8)';
ctx.beginPath();
ctx.arc(w/2, h/2, w/2, 0, Math.PI * 2);
ctx.fill();
const scale = 0.05; // pixels per meter
const cx = w / 2;
const cy = h / 2;
// Draw terrain contour (simplified)
for (let x = -100; x <= 100; x += 8) {
for (let z = -100; z <= 100; z += 8) {
const wx = physics.position.x + x / scale;
const wz = physics.position.z + z / scale;
const th = getTerrainHeight(wx, wz);
if (th > 50) {
ctx.fillStyle = `rgba(100,80,60,${Math.min(0.3, th/500)})`;
ctx.fillRect(cx + x - 2, cy + z - 2, 4, 4);
}
}
}
// Draw runway
const rwRelX = (0 - physics.position.x) * scale;
const rwRelZ = (250 - physics.position.z) * scale;
ctx.strokeStyle = '#ffffff';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(cx + rwRelX, cy + rwRelZ - 60);
ctx.lineTo(cx + rwRelX, cy + rwRelZ + 60);
ctx.stroke();
// Draw enemies (dogfight)
if (selectedScenario === 'dogfight') {
for (const enemy of enemyAircraft) {
if (!enemy.alive) continue;
const ex = cx + (enemy.mesh.position.x - physics.position.x) * scale;
const ez = cy + (enemy.mesh.position.z - physics.position.z) * scale;
if (ex > 0 && ex < w && ez > 0 && ez < h) {
ctx.fillStyle = '#ff4444';
ctx.beginPath();
ctx.arc(ex, ez, 3, 0, Math.PI * 2);
ctx.fill();
}
}
}
// Draw player (center)
const euler = new THREE.Euler().setFromQuaternion(physics.orientation, 'YXZ');
ctx.save();
ctx.translate(cx, cy);
ctx.rotate(-euler.y);
ctx.fillStyle = '#00e5ff';
ctx.beginPath();
ctx.moveTo(0, -8);
ctx.lineTo(-5, 6);
ctx.lineTo(5, 6);
ctx.closePath();
ctx.fill();
ctx.restore();
// Compass ring
ctx.strokeStyle = 'rgba(0,229,255,0.3)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.arc(cx, cy, w/2 - 2, 0, Math.PI * 2);
ctx.stroke();
// Cardinal directions
ctx.font = '8px Courier New';
ctx.fillStyle = '#6688aa';
ctx.textAlign = 'center';
ctx.fillText('N', cx, 12);
ctx.fillText('S', cx, h - 5);
ctx.fillText('E', w - 8, cy + 3);
ctx.fillText('W', 8, cy + 3);
}
// --- Message System ---
let messageTimeout;
function showMessage(text) {
const el = document.getElementById('message-popup');
el.textContent = text;
el.classList.add('show');
clearTimeout(messageTimeout);
messageTimeout = setTimeout(() => {
el.classList.remove('show');
}, 3000);
}
function showDamageFlash() {
const overlay = document.getElementById('damage-overlay');
overlay.style.border = '15px solid rgba(255,0,0,0.6)';
setTimeout(() => {
if (physics.damage <= 0) {
overlay.style.border = '0px solid rgba(255,0,0,0)';
}
}, 300);
}
// --- Landing Evaluation ---
function evaluateLanding() {
const acData = AIRCRAFT[selectedAircraft];
// Factors: touchdown speed, vertical speed, alignment with runway
const touchSpeed = physics.airspeed * 1.94384; // knots
const approachSpeed = acData.stallSpeed * 1.3;
const vertSpeed = Math.abs(physics.velocity.y);
// Speed score (0-40)
let speedScore = 40;
if (touchSpeed > approachSpeed + 20) {
speedScore -= Math.min(40, (touchSpeed - approachSpeed - 20) * 2);
} else if (touchSpeed < approachSpeed - 10) {
speedScore -= Math.min(40, (approachSpeed - 10 - touchSpeed) * 3);
}
// Vertical speed score (0-30)
let vertScore = 30;
if (vertSpeed > 3) {
vertScore -= Math.min(30, (vertSpeed - 3) * 10);
}
// Alignment score (0-30)
const runwayDir = new THREE.Vector3(0, 0, -1);
const aircraftHeading = new THREE.Vector3(1, 0, 0).applyQuaternion(physics.orientation);
const headingAngle = Math.acos(Math.max(-1, Math.min(1,
(aircraftHeading.x * runwayDir.x + aircraftHeading.z * runwayDir.z) /
(Math.sqrt(aircraftHeading.x**2+aircraftHeading.z**2) || 1)))) * 180 / Math.PI;
let alignScore = 30;
if (headingAngle > 5) {
alignScore -= Math.min(30, headingAngle * 2);
}
landingScore = Math.max(0, Math.round(speedScore + vertScore + alignScore));
score += landingScore * 10;
}
// --- Game Flow ---
function startMission() {
document.getElementById('briefing').classList.remove('active');
gameState = 'playing';
// Reset physics state based on scenario
if (selectedScenario === 'takeoffLanding') {
// Start on runway (runway surface is at y=1.5, aircraft sits above it)
physics.position.set(0, 4, -250);
physics.velocity.set(0, 0, 0);
physics.orientation.set(0, 0, 0, 1);
physics.throttle = 0;
physics.onGround = true;
showMessage("TAKEOFF MISSION - BUILD SPEED AND LIFTOFF");
} else {
// Dogfight: start airborne
physics.position.set(0, 500, 0);
physics.velocity.set(80, 10, 0);
physics.orientation.set(0, 0, 0, 1);
physics.throttle = 0.7;
physics.onGround = false;
spawnEnemies(4);
showMessage("COMBAT ENGAGEMENT - DESTROY ALL HOSTILES");
}
// Reset score and damage
score = 0;
physics.damage = 0;
landingScore = null;
document.getElementById('landing-score').classList.remove('active');
}
function endGame() {
gameState = 'ended';
showMessage("MISSION COMPLETE - SCORE: " + score);
setTimeout(() => {
backToMenu();
}, 5000);
}
function resumeGame() {
document.getElementById('pause-menu').classList.remove('active');
gameState = 'playing';
}
function restartGame() {
// Reset everything
physics.position.set(0, 50, 0);
physics.velocity.set(0, 0, 0);
physics.orientation.set(0, 0, 0, 1);
physics.throttle = 0;
physics.damage = 0;
score = 0;
// Remove enemies
for (const enemy of enemyAircraft) {
scene.remove(enemy.mesh);
}
enemyAircraft = [];
document.getElementById('pause-menu').classList.remove('active');
showBriefing();
}
function backToMenu() {
gameState = 'menu';
// Remove enemies
for (const enemy of enemyAircraft) {
scene.remove(enemy.mesh);
}
enemyAircraft = [];
document.getElementById('hud').classList.remove('active');
document.getElementById('pause-menu').classList.remove('active');
document.getElementById('briefing').classList.remove('active');
document.getElementById('menu-screen').classList.add('active');
}
function showBriefing() {
gameState = 'briefing';
const scenario = SCENARIOS[selectedScenario];
document.getElementById('briefing-title').textContent = scenario.name;
document.getElementById('briefing-text').textContent = scenario.briefing;
document.getElementById('briefing').classList.add('active');
}
// --- Menu UI Generation ---
function generateMenuUI() {
const aircraftContainer = document.getElementById('aircraft-select');
for (const [key, ac] of Object.entries(AIRCRAFT)) {
const card = document.createElement('div');
card.className = 'aircraft-card' + (key === selectedAircraft ? ' selected' : '');
card.onclick = () => selectAircraft(key);
const stats = [
['MAX SPEED', ac.maxSpeed, 700],
['THRUST/WT', ac.thrustToWeight.toFixed(1), 2.5],
['TURN RATE', ac.turnRate.toFixed(1), 3.5],
['ROLL RATE', ac.rollRate.toFixed(1), 4.0],
['G LIMIT', ac.gLimit, 10]
];
let statsHTML = '';
for (const [label, value, max] of stats) {
const pct = Math.min(100, (value / max) * 100);
const color = pct > 70 ? '#00e5ff' : pct > 40 ? '#ffaa00' : '#ff4444';
statsHTML += `
<div class="stat-row">
<span class="stat-label">${label}</span>
<span class="stat-value">${value}</span>
</div>
<div class="stat-bar"><div class="stat-fill" style="width:${pct}%;background:${color};"></div></div>
`;
}
card.innerHTML = `
<h3>${ac.name}</h3>
<div class="type">${ac.type}</div>
${statsHTML}
`;
aircraftContainer.appendChild(card);
}
const scenarioContainer = document.getElementById('scenario-select');
for (const [key, sc] of Object.entries(SCENARIOS)) {
const card = document.createElement('div');
card.className = 'scenario-card' + (key === selectedScenario ? ' selected' : '');
card.onclick = () => selectScenario(key);
card.innerHTML = `
<h3>${sc.name}</h3>
<p style="color:#00e5ff;margin-bottom:8px;">${sc.subtitle}</p>
<p>${sc.description}</p>
`;
scenarioContainer.appendChild(card);
}
}
function selectAircraft(key) {
selectedAircraft = key;
document.querySelectorAll('.aircraft-card').forEach((card, i) => {
card.classList.toggle('selected', Object.keys(AIRCRAFT)[i] === key);
});
}
function selectScenario(key) {
selectedScenario = key;
document.querySelectorAll('.scenario-card').forEach((card, i) => {
card.classList.toggle('selected', Object.keys(SCENARIOS)[i] === key);
});
}
// --- Input Handling ---
function setupInput() {
// Mouse movement for pitch/roll
document.addEventListener('mousemove', (e) => {
if (!input.mouseLocked || gameState !== 'playing') return;
const sensitivity = 0.003;
input.roll = Math.max(-1, Math.min(1, e.movementX * sensitivity));
input.pitch = Math.max(-1, Math.min(1, -e.movementY * sensitivity));
});
// Keyboard controls
document.addEventListener('keydown', (e) => {
if (gameState === 'playing') {
switch(e.code) {
case 'KeyW': input.throttleUp = true; break;
case 'KeyS': input.throttleDown = true; break;
case 'KeyQ': input.yaw = -1; break;
case 'KeyE': input.yaw = 1; break;
case 'ShiftLeft': case 'ShiftRight': input.brake = true; break;
case 'Space':
e.preventDefault();
if (selectedScenario === 'dogfight') playerShoot();
else {
// Flare for landing - reduce descent rate
physics.angularVelocity.x -= 0.5;
}
break;
case 'Escape':
gameState = 'paused';
document.getElementById('pause-menu').classList.add('active');
break;
}
} else if (gameState === 'paused') {
if (e.code === 'Escape') resumeGame();
}
});
document.addEventListener('keyup', (e) => {
switch(e.code) {
case 'KeyW': input.throttleUp = false; break;
case 'KeyS': input.throttleDown = false; break;
case 'KeyQ': input.yaw = 0; break;
case 'KeyE': input.yaw = 0; break;
case 'ShiftLeft': case 'ShiftRight': input.brake = false; break;
}
});
// Click to lock mouse for flight controls
document.addEventListener('click', () => {
if (gameState === 'playing' && !input.mouseLocked) {
document.body.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
input.mouseLocked = !!document.pointerLockElement;
});
}
// --- Lighting Setup ---
function setupLighting() {
// Ambient light
const ambient = new THREE.AmbientLight(0x4466aa, 0.4);
scene.add(ambient);
// Hemisphere light for sky/ground color variation
const hemi = new THREE.HemisphereLight(0x87ceeb, 0x3a2a1a, 0.5);
scene.add(hemi);
// Main directional light (sun) - golden hour angle
const sun = new THREE.DirectionalLight(0xffeedd, 1.2);
sun.position.set(-2000, 800, 1000);
sun.castShadow = true;
scene.add(sun);
// Secondary fill light
const fill = new THREE.DirectionalLight(0x6688cc, 0.3);
fill.position.set(2000, 400, -1000);
scene.add(fill);
// Sun visual (glow sphere)
const sunGeo = new THREE.SphereGeometry(50, 16, 16);
const sunMat = new THREE.MeshBasicMaterial({ color: 0xffeeaa });
const sunMesh = new THREE.Mesh(sunGeo, sunMat);
sunMesh.position.copy(sun.position).normalize().multiplyScalar(14000);
scene.add(sunMesh);
}
// --- Clouds ---
function createClouds() {
for (let i = 0; i < 60; i++) {
const cloudGroup = new THREE.Group();
// Each cloud is made of several spheres
const numPuffs = 3 + Math.floor(Math.random() * 5);
for (let j = 0; j < numPuffs; j++) {
const puffGeo = new THREE.SphereGeometry(20 + Math.random() * 40, 8, 6);
const puffMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.7 + Math.random() * 0.2,
roughness: 1,
metalness: 0
});
const puff = new THREE.Mesh(puffGeo, puffMat);
puff.position.set(
(Math.random() - 0.5) * 60,
(Math.random() - 0.5) * 15,
(Math.random() - 0.5) * 40
);
puff.scale.y = 0.3 + Math.random() * 0.2;
cloudGroup.add(puff);
}
// Position clouds at various altitudes and locations
const angle = Math.random() * Math.PI * 2;
const dist = 500 + Math.random() * 4000;
cloudGroup.position.set(
Math.cos(angle) * dist,
300 + Math.random() * 1500,
Math.sin(angle) * dist
);
scene.add(cloudGroup);
}
}
// --- Main Game Loop ---
function gameLoop() {
requestAnimationFrame(gameLoop);
const dt = clock.getDelta();
if (gameState === 'playing') {
// Update physics
updatePhysics(dt);
// Update enemies
if (selectedScenario === 'dogfight') {
updateEnemies(dt);
// Check win condition
if (enemiesRemaining <= 0) {
showMessage("ALL HOSTILES NEUTRALIZED - MISSION COMPLETE!");
score += 5000;
setTimeout(() => endGame(), 3000);
}
} else {
// Landing scenario: check if landed on runway
if (physics.onGround && Math.abs(physics.position.x) < 25 &&
physics.position.z > -100 && physics.position.z < 700) {
if (!landingScore) {
evaluateLanding();
showMessage(`LANDING COMPLETE! SCORE: ${landingScore}/100`);
setTimeout(() => endGame(), 4000);
}
}
}
// Update aircraft model transform
if (aircraftGroup) {
aircraftGroup.position.copy(physics.position);
aircraftGroup.quaternion.copy(physics.orientation);
// Engine exhaust glow based on throttle
aircraftGroup.traverse((child) => {
if (child.name === 'exhaust') {
child.material.opacity = 0.2 + physics.throttle * 0.6;
const heatColor = new THREE.Color().setHSL(
0.08 - physics.engineHeat * 0.05,
1,
0.3 + physics.throttle * 0.4
);
child.material.color.copy(heatColor);
}
});
}
// Update camera
updateCamera();
// Update particles
updateParticles(dt);
// Update audio
updateAudio();
// Render HUD
renderHUD();
}
renderer.render(scene, camera);
}
// --- Initialization ---
async function init() {
const loadBar = document.getElementById('load-bar');
const loadText = document.getElementById('load-text');
// Step 1: Setup Three.js scene
loadText.textContent = 'INITIALIZING RENDERER...';
loadBar.style.width = '10%';
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x87ceeb, 0.00008);
camera = new THREE.PerspectiveCamera(65, window.innerWidth / window.innerHeight, 1, 20000);
camera.position.set(0, 100, -50);
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();
await sleep(200);
// Step 2: Generate terrain
loadText.textContent = 'GENERATING TERRAIN...';
loadBar.style.width = '30%';
generateTerrain();
await sleep(200);
// Step 3: Create water
loadText.textContent = 'CREATING WATER BODIES...';
loadBar.style.width = '45%';
createWater();
await sleep(150);
// Step 4: Sky and lighting
loadText.textContent = 'SETTING UP ATMOSPHERE...';
loadBar.style.width = '60%';
createSky();
setupLighting();
createClouds();
await sleep(150);
// Step 5: Runway
loadText.textContent = 'BUILDING AIRFIELD...';
loadBar.style.width = '75%';
createRunway();
await sleep(150);
// Step 6: Aircraft model
loadText.textContent = 'LOADING AIRCRAFT...';
loadBar.style.width = '85%';
aircraftGroup = createAircraftModel(selectedAircraft);
scene.add(aircraftGroup);
await sleep(150);
// Step 7: Audio and input
loadText.textContent = 'INITIALIZING AUDIO SYSTEMS...';
loadBar.style.width = '95%';
initAudio();
setupInput();
await sleep(200);
// Done loading
loadText.textContent = 'SYSTEMS READY';
loadBar.style.width = '100%';
await sleep(800);
document.getElementById('loading-screen').classList.add('hidden');
setTimeout(() => {
document.getElementById('loading-screen').style.display = 'none';
document.getElementById('menu-screen').classList.add('active');
gameState = 'menu';
generateMenuUI();
resizeHUD();
// Launch button handler
document.getElementById('launch-btn').addEventListener('click', () => {
if (audioCtx && audioCtx.state === 'suspended') {
audioCtx.resume();
}
document.getElementById('menu-screen').classList.remove('active');
showBriefing();
});
}, 500);
// Start game loop
gameLoop();
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
// --- Window Resize Handler ---
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
resizeHUD();
});
// --- Start ---
init().catch(console.error);
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen3.6-27b-mtp","provider":"lmstudio","persona":"full","sessionId":"c5e10443-ad9f-495b-8fa6-c7c5967fab94","tokensIn":2205081,"tokensOut":39124,"tokensTotal":2244205,"turns":35,"toolCalls":34,"failedToolCalls":1,"timestamp":"2026-07-29T14:54:29.199Z"} -->