Flight Simulator
Qwen 3.6 35b a3b · full
◈Preview unavailable — try playing the result
Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 66,279 bytes · SHA-256 13182c894cf1
<!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; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
#canvas-container { width: 100%; height: 100%; position: relative; }
canvas { display: block; width: 100%; height: 100%; }
/* HUD Overlay */
#hud {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
pointer-events: none; z-index: 10; display: none;
}
.hud-panel {
position: absolute; background: rgba(0, 20, 40, 0.6);
border: 1px solid rgba(0, 200, 255, 0.3); border-radius: 4px;
padding: 8px 14px; color: #0cf; font-size: 13px; line-height: 1.6;
}
#hud-left { top: 20px; left: 20px; }
#hud-right { top: 20px; right: 20px; text-align: right; }
#hud-center { bottom: 20px; left: 50%; transform: translateX(-50%); text-align: center; }
#hud-bottom { bottom: 20px; left: 20px; }
.hud-label { color: rgba(0, 200, 255, 0.6); font-size: 10px; text-transform: uppercase; letter-spacing: 2px; }
.hud-value { color: #0cf; font-size: 20px; font-weight: bold; }
.hud-bar { width: 120px; height: 6px; background: rgba(0, 200, 255, 0.15); border-radius: 3px; margin-top: 2px; }
.hud-bar-fill { height: 100%; border-radius: 3px; transition: width 0.1s; }
#hud-center .hud-value { font-size: 28px; text-shadow: 0 0 20px rgba(0, 200, 255, 0.5); }
/* Crosshair */
#crosshair {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 40px; height: 40px; pointer-events: none;
}
#crosshair::before, #crosshair::after {
content: ''; position: absolute; background: rgba(0, 255, 150, 0.6);
}
#crosshair::before { width: 20px; height: 2px; top: 19px; left: 10px; }
#crosshair::after { width: 2px; height: 20px; top: 10px; left: 19px; }
/* Pitch ladder */
#pitch-ladder {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 200px; height: 300px; pointer-events: none; opacity: 0.4;
}
/* Menu Screen */
#menu {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; flex-direction: column; align-items: center; justify-content: center;
z-index: 100; background: linear-gradient(180deg, #0a0a2e 0%, #1a1a3e 40%, #0d2137 100%);
}
#menu.hidden { display: none; }
#menu h1 {
font-size: 64px; color: #0cf; text-shadow: 0 0 40px rgba(0, 200, 255, 0.5), 0 0 80px rgba(0, 200, 255, 0.2);
margin-bottom: 8px; letter-spacing: 8px; font-weight: 300;
}
#menu .subtitle { color: rgba(0, 200, 255, 0.5); font-size: 14px; letter-spacing: 6px; margin-bottom: 50px; }
.menu-section { margin-bottom: 30px; width: 500px; max-width: 90vw; }
.menu-section h2 { color: rgba(0, 200, 255, 0.7); font-size: 12px; letter-spacing: 4px; text-transform: uppercase; margin-bottom: 12px; }
.aircraft-grid { display: flex; gap: 12px; }
.aircraft-card {
flex: 1; padding: 16px; background: rgba(0, 200, 255, 0.05);
border: 1px solid rgba(0, 200, 255, 0.15); border-radius: 6px;
cursor: pointer; transition: all 0.3s; text-align: center;
}
.aircraft-card:hover { border-color: rgba(0, 200, 255, 0.5); background: rgba(0, 200, 255, 0.1); }
.aircraft-card.selected { border-color: #0cf; background: rgba(0, 200, 255, 0.15); box-shadow: 0 0 20px rgba(0, 200, 255, 0.2); }
.aircraft-card .name { color: #0cf; font-size: 16px; font-weight: bold; margin-bottom: 6px; }
.aircraft-card .desc { color: rgba(0, 200, 255, 0.5); font-size: 11px; line-height: 1.4; }
.aircraft-card .stats { margin-top: 10px; }
.stat-row { display: flex; justify-content: space-between; font-size: 10px; color: rgba(0, 200, 255, 0.6); margin: 3px 0; }
.stat-row .bar { width: 60px; height: 4px; background: rgba(0, 200, 255, 0.1); border-radius: 2px; overflow: hidden; }
.stat-row .bar-fill { height: 100%; background: #0cf; border-radius: 2px; }
.mode-grid { display: flex; gap: 12px; }
.mode-card {
flex: 1; padding: 20px; background: rgba(0, 200, 255, 0.05);
border: 1px solid rgba(0, 200, 255, 0.15); border-radius: 6px;
cursor: pointer; transition: all 0.3s; text-align: center;
}
.mode-card:hover { border-color: rgba(0, 200, 255, 0.5); background: rgba(0, 200, 255, 0.1); }
.mode-card.selected { border-color: #0cf; background: rgba(0, 200, 255, 0.15); box-shadow: 0 0 20px rgba(0, 200, 255, 0.2); }
.mode-card .name { color: #0cf; font-size: 18px; font-weight: bold; margin-bottom: 6px; }
.mode-card .desc { color: rgba(0, 200, 255, 0.5); font-size: 11px; }
#start-btn {
margin-top: 30px; padding: 14px 60px; background: rgba(0, 200, 255, 0.15);
border: 1px solid #0cf; border-radius: 4px; color: #0cf; font-size: 16px;
font-family: 'Courier New', monospace; letter-spacing: 4px; cursor: pointer;
transition: all 0.3s; text-transform: uppercase;
}
#start-btn:hover { background: rgba(0, 200, 255, 0.3); box-shadow: 0 0 30px rgba(0, 200, 255, 0.3); }
#start-btn:disabled { opacity: 0.3; cursor: default; }
/* Controls help */
#controls-help {
position: absolute; bottom: 60px; left: 50%; transform: translateX(-50%);
color: rgba(0, 200, 255, 0.4); font-size: 11px; text-align: center;
letter-spacing: 1px; pointer-events: none;
}
.key { display: inline-block; padding: 2px 6px; border: 1px solid rgba(0, 200, 255, 0.3); border-radius: 3px; margin: 0 2px; font-size: 10px; }
/* Mission overlay */
#mission-overlay {
position: absolute; top: 100px; left: 50%; transform: translateX(-50%);
color: #0cf; font-size: 18px; letter-spacing: 3px; text-align: center;
pointer-events: none; opacity: 0; transition: opacity 0.5s;
text-shadow: 0 0 20px rgba(0, 200, 255, 0.5);
}
#mission-overlay.visible { opacity: 1; }
/* Stall warning */
#stall-warning {
position: absolute; top: 40%; left: 50%; transform: translate(-50%, -50%);
color: #f44; font-size: 24px; letter-spacing: 4px; pointer-events: none;
opacity: 0; text-shadow: 0 0 30px rgba(255, 0, 0, 0.5);
animation: none;
}
#stall-warning.active { opacity: 1; animation: blink 0.3s infinite; }
@keyframes blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
/* G-force indicator */
#g-indicator {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 60px; height: 100px; pointer-events: none; opacity: 0.3;
}
#g-indicator .tick { position: absolute; left: 50%; width: 8px; height: 1px; background: #0cf; transform: translateX(-50%); }
#g-indicator .needle {
position: absolute; bottom: 50%; left: 50%; width: 2px; height: 40px;
background: #f44; transform-origin: bottom center; transform: translateX(-50%) rotate(0deg);
transition: transform 0.1s;
}
/* Dogfight markers */
#enemy-marker {
position: absolute; width: 20px; height: 20px; pointer-events: none; opacity: 0;
}
#enemy-marker::before {
content: ''; position: absolute; width: 100%; height: 100%;
border: 2px solid #f44; border-radius: 50%;
animation: pulse 1s infinite;
}
#enemy-marker::after {
content: ''; position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 4px; height: 4px; background: #f44; border-radius: 50%;
}
@keyframes pulse { 0%, 100% { transform: scale(1); opacity: 1; } 50% { transform: scale(1.5); opacity: 0.5; } }
/* Minimap */
#minimap {
position: absolute; bottom: 20px; right: 20px; width: 150px; height: 150px;
border: 1px solid rgba(0, 200, 255, 0.3); border-radius: 4px;
background: rgba(0, 20, 40, 0.6); pointer-events: none;
}
#minimap canvas { width: 100%; height: 100%; border-radius: 4px; }
/* Loading */
#loading {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
display: flex; align-items: center; justify-content: center;
background: #000; z-index: 200; color: #0cf; font-size: 14px; letter-spacing: 3px;
}
#loading.hidden { display: none; }
</style>
</head>
<body>
<div id="loading">INITIALIZING AERO SYSTEMS...</div>
<div id="canvas-container"></div>
<div id="menu">
<h1>APEX AERO</h1>
<div class="subtitle">FLIGHT SIMULATOR</div>
<div class="menu-section">
<h2>Select Aircraft</h2>
<div class="aircraft-grid">
<div class="aircraft-card selected" data-plane="fighter">
<div class="name">F-22 RAPTOR</div>
<div class="desc">Air superiority fighter. High thrust, excellent maneuverability, moderate lift.</div>
<div class="stats">
<div class="stat-row"><span>Speed</span><div class="bar"><div class="bar-fill" style="width:95%"></div></div></div>
<div class="stat-row"><span>Thrust</span><div class="bar"><div class="bar-fill" style="width:90%"></div></div></div>
<div class="stat-row"><span>Lift</span><div class="bar"><div class="bar-fill" style="width:60%"></div></div></div>
<div class="stat-row"><span>Maneuver</span><div class="bar"><div class="bar-fill" style="width:95%"></div></div></div>
</div>
</div>
<div class="aircraft-card" data-plane="glider">
<div class="name">DG-1 GLIDER</div>
<div class="desc">High-lift sailplane. Slow but efficient. Excellent climb in thermal updrafts.</div>
<div class="stats">
<div class="stat-row"><span>Speed</span><div class="bar"><div class="bar-fill" style="width:20%"></div></div></div>
<div class="stat-row"><span>Thrust</span><div class="bar"><div class="bar-fill" style="width:0%"></div></div></div>
<div class="stat-row"><span>Lift</span><div class="bar"><div class="bar-fill" style="width:100%"></div></div></div>
<div class="stat-row"><span>Maneuver</span><div class="bar"><div class="bar-fill" style="width:15%"></div></div></div>
</div>
</div>
<div class="aircraft-card" data-plane="bomber">
<div class="name">B-17 FORTRESS</div>
<div class="desc">Heavy bomber. High lift, slow acceleration, massive turning radius. Needs long runway.</div>
<div class="stats">
<div class="stat-row"><span>Speed</span><div class="bar"><div class="bar-fill" style="width:40%"></div></div></div>
<div class="stat-row"><span>Thrust</span><div class="bar"><div class="bar-fill" style="width:50%"></div></div></div>
<div class="stat-row"><span>Lift</span><div class="bar"><div class="bar-fill" style="width:95%"></div></div></div>
<div class="stat-row"><span>Maneuver</span><div class="bar"><div class="bar-fill" style="width:25%"></div></div></div>
</div>
</div>
</div>
</div>
<div class="menu-section">
<h2>Select Scenario</h2>
<div class="mode-grid">
<div class="mode-card selected" data-mode="landing">
<div class="name">✈ TAKEOFF & LANDING</div>
<div class="desc">Master precision flight. Take off from runway and return for a smooth landing.</div>
</div>
<div class="mode-card" data-mode="dogfight">
<div class="name">⚔ DOGFIGHT</div>
<div class="desc">High-G combat. Pursue and down enemy aircraft using advanced maneuvering.</div>
</div>
</div>
</div>
<button id="start-btn">LAUNCH</button>
</div>
<div id="hud">
<div class="hud-panel" id="hud-left">
<div class="hud-label">Airspeed</div>
<div class="hud-value" id="hud-speed">0 <small>KT</small></div>
<div class="hud-label" style="margin-top:8px">Altitude (AGL)</div>
<div class="hud-value" id="hud-alt">0 <small>FT</small></div>
<div class="hud-label" style="margin-top:8px">Vertical Speed</div>
<div class="hud-value" id="hud-vspeed">0 <small>FT/MIN</small></div>
</div>
<div class="hud-panel" id="hud-right">
<div class="hud-label">Throttle</div>
<div class="hud-value" id="hud-throttle">0 <small>%</small></div>
<div class="hud-bar" style="margin-top:4px"><div class="hud-bar-fill" id="hud-throttle-bar" style="width:0%;background:#0cf"></div></div>
<div class="hud-label" style="margin-top:8px">RPM</div>
<div class="hud-value" id="hud-rpm">0 <small>×</small></div>
<div class="hud-label" style="margin-top:8px">Angle of Attack</div>
<div class="hud-value" id="hud-aoa">0 <small>°</small></div>
<div class="hud-label" style="margin-top:8px">G-Force</div>
<div class="hud-value" id="hud-g">1.0 <small>G</small></div>
</div>
<div class="hud-panel" id="hud-center">
<div class="hud-label">Attitude</div>
<div class="hud-value" id="hud-pitch-roll">0° / 0°</div>
<div class="hud-label" style="margin-top:8px">Heading</div>
<div class="hud-value" id="hud-heading">360 <small>°</small></div>
</div>
<div class="hud-panel" id="hud-bottom">
<div class="hud-label">Engine Temp</div>
<div class="hud-bar"><div class="hud-bar-fill" id="hud-temp-bar" style="width:30%;background:#0f0"></div></div>
<div id="hud-temp-val" style="font-size:11px;margin-top:2px">120°C</div>
</div>
<div id="crosshair"></div>
<div id="stall-warning">STALL WARNING</div>
</div>
<div id="controls-help">
<span class="key">W</span><span class="key">S</span> Pitch
<span class="key">A</span><span class="key">D</span> Roll
<span class="key">Q</span><span class="key">E</span> Yaw
<span class="key">↑</span><span class="key">↓</span> Throttle
<span class="key">C</span> Camera
<span class="key">R</span> Reset
<span class="key">ESC</span> Menu
</div>
<div id="mission-overlay"></div>
<div id="minimap"><canvas id="minimap-canvas" width="150" height="150"></canvas></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ============================================================
// APEX AERO — Flight Simulator
// ============================================================
(function() {
'use strict';
// ==================== CONFIGURATION ====================
const CONFIG = {
// Physics
gravity: 9.81,
seaLevelDensity: 1.225,
atmosphereScaleHeight: 8500,
maxGForce: 9.0,
minGForce: -3.0,
stallAOA: 16, // degrees
maxAOA: 30,
// Terrain
terrainSize: 80000,
terrainResolution: 256,
terrainLOD: 2,
// Camera
camFOV: 65,
camNear: 0.5,
camFar: 100000,
// Audio
audioEnabled: true,
// Rendering
shadowMapSize: 2048,
fogNear: 5000,
fogFar: 60000,
};
// ==================== AIRCRAFT DEFINITIONS ====================
const AIRCRAFT = {
fighter: {
name: 'F-22 RAPTOR',
mass: 19700, // kg
wingArea: 78.04, // m²
wingSpan: 13.56, // m
aspectRatio: 3.1,
liftCoeff: 0.08, // per degree
liftMax: 2.2,
dragCoeff: 0.025,
inducedDragFactor: 0.05,
maxThrust: 240000, // N
idleThrust: 5000,
thrustResponse: 0.8,
pitchRate: 2.5,
rollRate: 3.0,
yawRate: 1.8,
pitchResponse: 0.4,
rollResponse: 0.35,
yawResponse: 0.5,
stallSpeed: 85, // m/s
maxSpeed: 340, // m/s
color: 0x4a6a8a,
accentColor: 0x0cf,
length: 18,
type: 'fighter',
},
glider: {
name: 'DG-1 GLIDER',
mass: 500,
wingArea: 12.0,
wingSpan: 15.0,
aspectRatio: 25,
liftCoeff: 0.12,
liftMax: 1.8,
dragCoeff: 0.012,
inducedDragFactor: 0.004,
maxThrust: 0,
idleThrust: 0,
thrustResponse: 0,
pitchRate: 0.8,
rollRate: 0.6,
yawRate: 0.4,
pitchResponse: 0.6,
rollResponse: 0.5,
yawResponse: 0.7,
stallSpeed: 30,
maxSpeed: 70,
color: 0xe8e8e8,
accentColor: 0xff8,
length: 7,
type: 'glider',
},
bomber: {
name: 'B-17 FORTRESS',
mass: 28000,
wingArea: 134.3,
wingSpan: 31.62,
aspectRatio: 7.4,
liftCoeff: 0.10,
liftMax: 2.0,
dragCoeff: 0.035,
inducedDragFactor: 0.012,
maxThrust: 116000,
idleThrust: 3000,
thrustResponse: 0.3,
pitchRate: 1.2,
rollRate: 1.0,
yawRate: 0.6,
pitchResponse: 0.5,
rollResponse: 0.45,
yawResponse: 0.6,
stallSpeed: 110,
maxSpeed: 200,
color: 0x5a5a4a,
accentColor: 0x8a8,
length: 22,
type: 'bomber',
},
};
// ==================== GAME STATE ====================
let gameState = {
mode: null, // 'landing' or 'dogfight'
selectedPlane: 'fighter',
running: false,
paused: false,
cameraMode: 0, // 0=chase, 1=cockpit, 2=free
time: 0,
dt: 0,
lastTime: 0,
// Aircraft physics state
pos: new THREE.Vector3(0, 10, 0),
vel: new THREE.Vector3(0, 0, 0),
quat: new THREE.Quaternion(),
euler: new THREE.Euler(0, 0, 0, 'YXZ'),
// Control surface states (smooth interpolation)
throttle: 0,
throttleTarget: 0,
pitchInput: 0,
pitchTarget: 0,
rollInput: 0,
rollTarget: 0,
yawInput: 0,
yawTarget: 0,
// Control surface deflections
elevator: 0,
aileron: 0,
rudder: 0,
elevatorTarget: 0,
aileronTarget: 0,
rudderTarget: 0,
// Derived physics
airspeed: 0,
aoa: 0,
sideslip: 0,
lift: 0,
drag: 0,
thrust: 0,
gForce: 1.0,
engineTemp: 120,
// Landing mode state
onGround: false,
landingGears: true,
flapAngle: 0,
runwayHit: false,
landed: false,
takeoffSpeedReached: false,
score: 0,
// Dogfight mode state
enemyPos: new THREE.Vector3(),
enemyVel: new THREE.Vector3(),
enemyQuat: new THREE.Quaternion(),
enemyHP: 100,
enemyTarget: new THREE.Vector3(),
enemyAggression: 0.5,
kills: 0,
shotsFired: 0,
shotsHit: 0,
ammo: 500,
heat: 0,
gunCooldown: 0,
};
// ==================== INPUT ====================
const keys = {};
const mouse = { x: 0, y: 0, dx: 0, dy: 0, locked: false };
document.addEventListener('keydown', (e) => {
keys[e.code] = true;
if (e.code === 'Escape' && gameState.running) {
showMenu();
}
});
document.addEventListener('keyup', (e) => { keys[e.code] = false; });
document.addEventListener('mousemove', (e) => {
if (mouse.locked) {
mouse.dx += e.movementX;
mouse.dy += e.movementY;
}
});
document.addEventListener('mousedown', (e) => {
if (gameState.running && !mouse.locked) {
document.body.requestPointerLock();
}
});
document.addEventListener('pointerlockchange', () => {
mouse.locked = document.pointerLockElement === document.body;
});
// ==================== THREE.JS SETUP ====================
let scene, camera, renderer, clock;
let terrainMesh, terrainLOD;
let sunLight, ambientLight, skyMesh;
let aircraftMesh, aircraftGroup;
let runwayMesh, runwayLights = [];
let clouds = [];
let particles = [];
let minimapCtx;
let enemyMesh, enemyGroup;
let muzzleFlash;
let wakeParticles;
function initThreeJS() {
scene = new THREE.Scene();
scene.background = new THREE.Color(0x87CEEB);
scene.fog = new THREE.FogExp2(0x87CEEB, 0.00004);
camera = new THREE.PerspectiveCamera(CONFIG.camFOV, window.innerWidth / window.innerHeight, CONFIG.camNear, CONFIG.camFar);
camera.position.set(0, 20, 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.2;
document.getElementById('canvas-container').appendChild(renderer.domElement);
clock = new THREE.Clock();
// Lighting
ambientLight = new THREE.AmbientLight(0x405070, 0.6);
scene.add(ambientLight);
sunLight = new THREE.DirectionalLight(0xfff5e0, 1.5);
sunLight.position.set(5000, 8000, 3000);
sunLight.castShadow = true;
sunLight.shadow.mapSize.set(CONFIG.shadowMapSize, CONFIG.shadowMapSize);
sunLight.shadow.camera.near = 100;
sunLight.shadow.camera.far = 50000;
sunLight.shadow.camera.left = -5000;
sunLight.shadow.camera.right = 5000;
sunLight.shadow.camera.top = 5000;
sunLight.shadow.camera.bottom = -5000;
scene.add(sunLight);
const hemiLight = new THREE.HemisphereLight(0x87CEEB, 0x3a5a2a, 0.4);
scene.add(hemiLight);
// Sky dome
const skyGeo = new THREE.SphereGeometry(80000, 32, 32);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color(0x0044aa) },
bottomColor: { value: new THREE.Color(0xaaccff) },
offset: { value: 20 },
exponent: { value: 0.4 },
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPos = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPos.xyz;
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
}
`,
fragmentShader: `
uniform vec3 topColor;
uniform vec3 bottomColor;
uniform float offset;
uniform float exponent;
varying vec3 vWorldPosition;
void main() {
float h = normalize(vWorldPosition + offset).y;
gl_FragColor = vec4(mix(bottomColor, topColor, max(pow(max(h, 0.0), exponent), 0.0)), 1.0);
}
`,
side: THREE.BackSide,
});
skyMesh = new THREE.Mesh(skyGeo, skyMat);
scene.add(skyMesh);
// Minimap
minimapCtx = document.getElementById('minimap-canvas').getContext('2d');
// Resize
window.addEventListener('resize', () => {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
});
}
// ==================== TERRAIN GENERATION ====================
let terrainHeights = [];
let terrainGeo;
function generateTerrain() {
const size = CONFIG.terrainSize;
const res = CONFIG.terrainResolution;
const step = size / res;
// Generate heightmap using layered noise
const heightData = new Float32Array((res + 1) * (res + 1));
// Simple multi-octave noise
function noise2D(x, y) {
const n = Math.sin(x * 12.9898 + y * 78.233) * 43758.5453;
return n - Math.floor(n);
}
function smoothNoise(x, y, scale) {
const sx = x / scale;
const sy = y / scale;
const ix = Math.floor(sx);
const iy = Math.floor(sy);
const fx = sx - ix;
const fy = sy - iy;
const a = noise2D(ix, iy);
const b = noise2D(ix + 1, iy);
const c = noise2D(ix, iy + 1);
const d = noise2D(ix + 1, iy + 1);
const ux = fx * fx * (3 - 2 * fx);
const uy = fy * fy * (3 - 2 * fy);
return a * (1 - ux) * (1 - uy) + b * ux * (1 - uy) + c * (1 - ux) * uy + d * ux * uy;
}
function fbm(x, y, octaves) {
let val = 0, amp = 1, freq = 1, max = 0;
for (let i = 0; i < octaves; i++) {
val += smoothNoise(x * freq, y * freq, 1.0) * amp;
max += amp;
amp *= 0.5;
freq *= 2.0;
}
return val / max;
}
for (let iz = 0; iz <= res; iz++) {
for (let ix = 0; ix <= res; ix++) {
const x = ix * step - size / 2;
const z = iz * step - size / 2;
// Base terrain
let h = fbm(x, z, 6) * 2000;
// Mountains
const mountainNoise = fbm(x * 0.001 + 100, z * 0.001 + 100, 4);
if (mountainNoise > 0.55) {
h += (mountainNoise - 0.55) * 6000;
}
// Valley carving
const valleyNoise = Math.sin(x * 0.0003) * Math.cos(z * 0.0004);
if (valleyNoise < -0.3) {
h -= (valleyNoise + 0.3) * 800;
}
// Coastline / sea level
if (h < 50) {
h = 50 + (h - 50) * 0.3;
}
// Flatten runway area
const distToRunway = Math.sqrt(
Math.pow(x - 0, 2) + Math.pow(z - 2000, 2)
);
if (distToRunway < 500) {
const blend = Math.max(0, 1 - distToRunway / 500);
h = h * (1 - blend) + 100 * blend;
}
heightData[iz * (res + 1) + ix] = h;
}
}
// Create Three.js terrain
const geo = new THREE.PlaneGeometry(size, size, res, res);
geo.rotateX(-Math.PI / 2);
const pos = geo.attributes.position;
const colors = new Float32Array(pos.count * 3);
for (let i = 0; i < pos.count; i++) {
const ix = i % (res + 1);
const iz = Math.floor(i / (res + 1));
const h = heightData[iz * (res + 1) + ix];
pos.setY(i, h);
// Vertex colors based on height
const t = Math.max(0, Math.min(1, (h - 50) / 1500));
let r, g, b;
if (h < 80) {
// Water
r = 0.1; g = 0.3; b = 0.6;
} else if (t < 0.1) {
// Beach/sand
r = 0.76; g = 0.7; b = 0.5;
} else if (t < 0.5) {
// Grass
r = 0.2 + t * 0.5; g = 0.5 + t * 0.3; b = 0.1;
} else if (t < 0.7) {
// Forest
r = 0.15; g = 0.35; b = 0.1;
} else if (t < 0.85) {
// Rock
r = 0.4; g = 0.38; b = 0.35;
} else {
// Snow
r = 0.9; g = 0.92; b = 0.95;
}
colors[i * 3] = r;
colors[i * 3 + 1] = g;
colors[i * 3 + 2] = b;
}
geo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
geo.computeVertexNormals();
const mat = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.8,
metalness: 0.1,
flatShading: false,
});
terrainMesh = new THREE.Mesh(geo, mat);
terrainMesh.receiveShadow = true;
scene.add(terrainMesh);
// Water plane
const waterGeo = new THREE.PlaneGeometry(size, size);
waterGeo.rotateX(-Math.PI / 2);
const waterMat = new THREE.MeshStandardMaterial({
color: 0x1a5a8a,
transparent: true,
opacity: 0.7,
roughness: 0.3,
metalness: 0.8,
});
const water = new THREE.Mesh(waterGeo, waterMat);
water.position.y = 45;
scene.add(water);
// Store heightmap for collision
terrainHeights = heightData;
terrainGeo = geo;
}
function getTerrainHeight(x, z) {
const halfSize = CONFIG.terrainSize / 2;
const res = CONFIG.terrainResolution;
const step = CONFIG.terrainSize / res;
const ix = Math.floor((x + halfSize) / step);
const iz = Math.floor((z + halfSize) / step);
if (ix < 0 || ix > res || iz < 0 || iz > res) return 0;
const fx = (x + halfSize) / step - ix;
const fz = (z + halfSize) / step - iz;
const h00 = terrainHeights[iz * (res + 1) + ix];
const h10 = terrainHeights[iz * (res + 1) + ix + 1];
const h01 = terrainHeights[(iz + 1) * (res + 1) + ix];
const h11 = terrainHeights[(iz + 1) * (res + 1) + ix + 1];
return h00 * (1 - fx) * (1 - fz) + h10 * fx * (1 - fz) + h01 * (1 - fx) * fz + h11 * fx * fz;
}
// ==================== RUNWAY ====================
function createRunway() {
const rwLength = 3000;
const rwWidth = 60;
const geo = new THREE.PlaneGeometry(rwWidth, rwLength, 1, 1);
geo.rotateX(-Math.PI / 2);
// Create runway texture via canvas
const canvas = document.createElement('canvas');
canvas.width = 512;
canvas.height = 4096;
const ctx = canvas.getContext('2d');
// Base gray
ctx.fillStyle = '#3a3a3a';
ctx.fillRect(0, 0, 512, 4096);
// Edge lines
ctx.fillStyle = '#ffffff';
ctx.fillRect(10, 0, 8, 4096);
ctx.fillRect(494, 0, 8, 4096);
// Center dashes
ctx.fillStyle = '#ffcc00';
for (let y = 100; y < 4096; y += 200) {
ctx.fillRect(250, y, 12, 100);
}
// Threshold markings
ctx.fillStyle = '#ffffff';
for (let i = 0; i < 6; i++) {
ctx.fillRect(80 + i * 60, 50, 30, 150);
ctx.fillRect(402 - i * 60, 3896, 30, 150);
}
const texture = new THREE.CanvasTexture(canvas);
texture.wrapS = THREE.ClampToEdgeWrapping;
texture.wrapT = THREE.ClampToEdgeWrapping;
const mat = new THREE.MeshStandardMaterial({ map: texture, roughness: 0.9 });
runwayMesh = new THREE.Mesh(geo, mat);
runwayMesh.position.set(0, getTerrainHeight(0, 2000) + 1, 0);
scene.add(runwayMesh);
// Runway lights
const lightGeo = new THREE.SphereGeometry(1.5, 8, 8);
for (let i = 0; i < 40; i++) {
const z = -rwLength / 2 + (rwLength / 39) * i;
for (let side = -1; side <= 1; side += 2) {
const lightMat = new THREE.MeshStandardMaterial({
color: 0x00ff44,
emissive: 0x00ff44,
emissiveIntensity: 2,
});
const light = new THREE.Mesh(lightGeo, lightMat);
light.position.set(side * rwWidth / 2, getTerrainHeight(side * rwWidth / 2, z) + 2, z + 2000);
scene.add(light);
runwayLights.push(light);
}
}
}
// ==================== AIRCRAFT MODEL ====================
function createAircraftModel(type) {
if (aircraftGroup) {
scene.remove(aircraftGroup);
aircraftGroup.traverse(child => {
if (child.geometry) child.geometry.dispose();
if (child.material) {
if (Array.isArray(child.material)) child.material.forEach(m => m.dispose());
else child.material.dispose();
}
});
}
aircraftGroup = new THREE.Group();
const bodyColor = AIRCRAFT[type].color;
const accentCol = AIRCRAFT[type].accentColor;
// Fuselage
const fuselageGeo = new THREE.CylinderGeometry(0.8, 0.6, 12, 12);
fuselageGeo.rotateZ(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({ color: bodyColor, roughness: 0.4, metalness: 0.6 });
const fuselage = new THREE.Mesh(fuselageGeo, bodyMat);
aircraftGroup.add(fuselage);
// Nose cone
const noseGeo = new THREE.ConeGeometry(0.8, 3, 12);
noseGeo.rotateZ(-Math.PI / 2);
const nose = new THREE.Mesh(noseGeo, bodyMat);
nose.position.x = 7.5;
aircraftGroup.add(nose);
// Cockpit
const cockpitGeo = new THREE.SphereGeometry(0.7, 12, 8, 0, Math.PI * 2, 0, Math.PI / 2);
const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x88ccff, roughness: 0.1, metalness: 0.9, transparent: true, opacity: 0.7 });
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(2, 0.8, 0);
cockpit.rotation.x = -0.3;
aircraftGroup.add(cockpit);
// Main wings
const wingSpan = AIRCRAFT[type].wingSpan;
const wingChord = 3.5;
const wingGeo = new THREE.BoxGeometry(wingSpan, 0.15, wingChord);
const wingMat = new THREE.MeshStandardMaterial({ color: bodyColor, roughness: 0.5, metalness: 0.5 });
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.set(-1, 0.1, 0);
aircraftGroup.add(wings);
// Wing tips
const tipGeo = new THREE.BoxGeometry(1, 0.15, wingChord * 0.6);
for (let side = -1; side <= 1; side += 2) {
const tip = new THREE.Mesh(tipGeo, wingMat);
tip.position.set(-1, 0.1, side * wingSpan / 2);
aircraftGroup.add(tip);
}
// Vertical stabilizer
const vstabGeo = new THREE.BoxGeometry(1.5, 3, 0.15);
const vstab = new THREE.Mesh(vstabGeo, wingMat);
vstab.position.set(-6, 1.5, 0);
vstab.rotation.z = 0.3;
aircraftGroup.add(vstab);
// Horizontal stabilizers
const hstabGeo = new THREE.BoxGeometry(4, 0.1, 1.5);
for (let side = -1; side <= 1; side += 2) {
const hstab = new THREE.Mesh(hstabGeo, wingMat);
hstab.position.set(-7, 0.3, side * 2);
aircraftGroup.add(hstab);
}
// Tail fin accent
const tailAccentGeo = new THREE.BoxGeometry(1.2, 2.5, 0.2);
const tailAccentMat = new THREE.MeshStandardMaterial({ color: accentCol, roughness: 0.3, metalness: 0.7 });
const tailAccent = new THREE.Mesh(tailAccentGeo, tailAccentMat);
tailAccent.position.set(-6, 2.8, 0);
tailAccent.rotation.z = 0.3;
aircraftGroup.add(tailAccent);
// Engines (for powered aircraft)
if (AIRCRAFT[type].maxThrust > 0) {
const engineGeo = new THREE.CylinderGeometry(0.5, 0.4, 3, 12);
engineGeo.rotateZ(Math.PI / 2);
const engineMat = new THREE.MeshStandardMaterial({ color: 0x222222, roughness: 0.6, metalness: 0.8 });
if (type === 'bomber') {
// 4 engines
for (let i = -1; i <= 1; i += 2) {
for (let j = -1; j <= 1; j += 2) {
const eng = new THREE.Mesh(engineGeo, engineMat);
eng.position.set(-3, -0.5 + j * 0.3, i * 4);
aircraftGroup.add(eng);
}
}
} else {
// 1-2 engines
const eng1 = new THREE.Mesh(engineGeo, engineMat);
eng1.position.set(-2, -0.3, 2);
aircraftGroup.add(eng1);
if (type === 'fighter') {
const eng2 = new THREE.Mesh(engineGeo, engineMat);
eng2.position.set(-2, -0.3, -2);
aircraftGroup.add(eng2);
}
}
}
// Landing gear
const gearMat = new THREE.MeshStandardMaterial({ color: 0x333333, roughness: 0.7, metalness: 0.8 });
// Nose gear
const noseGearGeo = new THREE.CylinderGeometry(0.08, 0.08, 2, 6);
const noseGear = new THREE.Mesh(noseGearGeo, gearMat);
noseGear.position.set(5, -1.2, 0);
aircraftGroup.add(noseGear);
const noseWheelGeo = new THREE.SphereGeometry(0.3, 8, 8);
const noseWheel = new THREE.Mesh(noseWheelGeo, gearMat);
noseWheel.position.set(5, -2.2, 0);
aircraftGroup.add(noseWheel);
// Main gear
for (let side = -1; side <= 1; side += 2) {
const gearGeo = new THREE.CylinderGeometry(0.1, 0.1, 2.5, 6);
const mainGear = new THREE.Mesh(gearGeo, gearMat);
mainGear.position.set(-2, -1.5, side * 2.5);
aircraftGroup.add(mainGear);
const mainWheel = new THREE.Mesh(noseWheelGeo, gearMat);
mainWheel.position.set(-2, -2.8, side * 2.5);
aircraftGroup.add(mainWheel);
}
// Muzzle flash (for dogfight)
const flashGeo = new THREE.SphereGeometry(0.3, 8, 8);
const flashMat = new THREE.MeshBasicMaterial({ color: 0xffaa00, transparent: true, opacity: 0 });
muzzleFlash = new THREE.Mesh(flashGeo, flashMat);
muzzleFlash.position.set(10, 0, 0);
aircraftGroup.add(muzzleFlash);
scene.add(aircraftGroup);
}
// ==================== ENEMY AIRCRAFT ====================
function createEnemyModel() {
if (enemyGroup) scene.remove(enemyGroup);
enemyGroup = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x6a2a2a, roughness: 0.5, metalness: 0.5 });
const wingMat = new THREE.MeshStandardMaterial({ color: 0x5a2020, roughness: 0.5, metalness: 0.5 });
// Simple enemy plane
const bodyGeo = new THREE.CylinderGeometry(0.6, 0.5, 10, 10);
bodyGeo.rotateZ(Math.PI / 2);
const body = new THREE.Mesh(bodyGeo, bodyMat);
enemyGroup.add(body);
const noseGeo = new THREE.ConeGeometry(0.6, 2.5, 10);
noseGeo.rotateZ(-Math.PI / 2);
const nose = new THREE.Mesh(noseGeo, bodyMat);
nose.position.x = 6.25;
enemyGroup.add(nose);
const wingGeo = new THREE.BoxGeometry(10, 0.1, 2.5);
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.set(-1, 0, 0);
enemyGroup.add(wings);
const vstabGeo = new THREE.BoxGeometry(1.2, 2.5, 0.12);
const vstab = new THREE.Mesh(vstabGeo, bodyMat);
vstab.position.set(-5, 1.3, 0);
vstab.rotation.z = 0.3;
enemyGroup.add(vstab);
// Red accent
const accentGeo = new THREE.BoxGeometry(1, 2, 0.15);
const accentMat = new THREE.MeshStandardMaterial({ color: 0xff0000, emissive: 0x440000 });
const accent = new THREE.Mesh(accentGeo, accentMat);
accent.position.set(-5, 2.5, 0);
accent.rotation.z = 0.3;
enemyGroup.add(accent);
enemyGroup.visible = false;
scene.add(enemyGroup);
}
// ==================== CLOUDS ====================
function createClouds() {
const cloudGeo = new THREE.SphereGeometry(1, 8, 6);
const cloudMat = new THREE.MeshStandardMaterial({
color: 0xffffff,
transparent: true,
opacity: 0.6,
roughness: 1,
});
for (let i = 0; i < 200; i++) {
const group = new THREE.Group();
const numPuffs = 3 + Math.floor(Math.random() * 5);
for (let j = 0; j < numPuffs; j++) {
const puff = new THREE.Mesh(cloudGeo, cloudMat);
puff.scale.set(
15 + Math.random() * 30,
8 + Math.random() * 12,
15 + Math.random() * 30
);
puff.position.set(
(Math.random() - 0.5) * 40,
(Math.random() - 0.5) * 10,
(Math.random() - 0.5) * 40
);
group.add(puff);
}
group.position.set(
(Math.random() - 0.5) * 60000,
2000 + Math.random() * 8000,
(Math.random() - 0.5) * 60000
);
scene.add(group);
clouds.push(group);
}
}
// ==================== WAKE / PARTICLES ====================
function createWakeSystem() {
const count = 500;
const geo = new THREE.BufferGeometry();
const positions = new Float32Array(count * 3);
const lifetimes = new Float32Array(count);
geo.setAttribute('position', new THREE.BufferAttribute(positions, 3));
geo.userData = { lifetimes: lifetimes, velocities: [] };
const mat = new THREE.PointsMaterial({
color: 0xffffff,
size: 2,
transparent: true,
opacity: 0.3,
depthWrite: false,
});
wakeParticles = new THREE.Points(geo, mat);
wakeParticles.userData.maxCount = count;
scene.add(wakeParticles);
}
function updateWake(dt) {
if (!wakeParticles) return;
const pos = wakeParticles.geometry.attributes.position;
const life = wakeParticles.geometry.userData.lifetimes;
const count = wakeParticles.userData.maxCount;
// Update existing particles
for (let i = 0; i < count; i++) {
if (life[i] > 0) {
life[i] -= dt * 0.5;
pos.array[i * 3] += (wakeParticles.userData.velocities[i]?.x || 0) * dt;
pos.array[i * 3 + 1] += (wakeParticles.userData.velocities[i]?.y || 0) * dt;
pos.array[i * 3 + 2] += (wakeParticles.userData.velocities[i]?.z || 0) * dt;
} else {
life[i] = 0;
pos.array[i * 3 + 1] = -9999; // Hide
}
}
pos.needsUpdate = true;
}
function emitWake(x, y, z, vx, vy, vz, amount) {
if (!wakeParticles) return;
const count = wakeParticles.userData.maxCount;
const life = wakeParticles.geometry.userData.lifetimes;
const pos = wakeParticles.geometry.attributes.position;
const vel = wakeParticles.userData.velocities;
for (let i = 0; i < count && amount > 0; i++) {
if (life[i] <= 0) {
pos.array[i * 3] = x + (Math.random() - 0.5) * 2;
pos.array[i * 3 + 1] = y + (Math.random() - 0.5) * 2;
pos.array[i * 3 + 2] = z + (Math.random() - 0.5) * 2;
life[i] = 1 + Math.random() * 2;
vel[i] = { x: vx * 0.1 + (Math.random() - 0.5) * 2, y: vy * 0.1 + (Math.random() - 0.5), z: vz * 0.1 + (Math.random() - 0.5) * 2 };
amount--;
}
}
}
// ==================== ATMOSPHERE ====================
function getAirDensity(altitude) {
return CONFIG.seaLevelDensity * Math.exp(-altitude / CONFIG.atmosphereScaleHeight);
}
// ==================== PHYSICS ENGINE ====================
const _v1 = new THREE.Vector3();
const _v2 = new THREE.Vector3();
const _v3 = new THREE.Vector3();
const _q1 = new THREE.Quaternion();
const _e1 = new THREE.Euler();
function updatePhysics(dt) {
const plane = AIRCRAFT[gameState.selectedPlane];
const gs = gameState;
dt = Math.min(dt, 0.05); // Clamp delta time
// === Throttle response ===
gs.throttle += (gs.throttleTarget - gs.throttle) * plane.thrustResponse * dt * 3;
gs.throttle = Math.max(0, Math.min(1, gs.throttle));
// === Control surface response (smooth) ===
gs.elevator += (gs.elevatorTarget - gs.elevator) * 4.0 * dt;
gs.aileron += (gs.aileronTarget - gs.aileron) * 4.0 * dt;
gs.rudder += (gs.rudderTarget - gs.rudder) * 3.0 * dt;
gs.elevator = Math.max(-1, Math.min(1, gs.elevator));
gs.aileron = Math.max(-1, Math.min(1, gs.aileron));
gs.rudder = Math.max(-1, Math.min(1, gs.rudder));
// === Compute thrust ===
if (plane.maxThrust > 0) {
gs.thrust = plane.idleThrust + (plane.maxThrust - plane.idleThrust) * gs.throttle;
} else {
gs.thrust = 0;
}
// === Engine temp ===
if (plane.maxThrust > 0) {
const targetTemp = 120 + gs.throttle * 400;
gs.engineTemp += (targetTemp - gs.engineTemp) * dt * 0.5;
}
// === Airspeed ===
gs.airspeed = gs.vel.length();
// === Air density at altitude ===
const altitude = gs.pos.y;
const airDensity = getAirDensity(altitude);
const densityRatio = airDensity / CONFIG.seaLevelDensity;
// === Angle of Attack ===
// Aircraft forward direction
_v1.set(1, 0, 0).applyQuaternion(gs.quat);
// Relative wind = -velocity
_v2.copy(gs.vel).negate().normalize();
// Project wind onto aircraft's vertical plane
_v3.set(0, 1, 0).applyQuaternion(gs.quat);
const forwardDotWind = _v1.dot(_v2);
const aoaRad = Math.asin(Math.max(-1, Math.min(1, _v3.dot(_v2))));
gs.aoa = aoaRad * 180 / Math.PI;
if (forwardDotWind < 0) gs.aoa = -gs.aoa; // inverted flight
// === Lift ===
// C_L = liftCoeff * aoa (in degrees), clamped
let cl = plane.liftCoeff * gs.aoa;
cl = Math.max(-plane.liftMax, Math.min(plane.liftMax, cl));
// Dynamic pressure
const q_dyn = 0.5 * airDensity * gs.airspeed * gs.airspeed;
gs.lift = q_dyn * plane.wingArea * cl;
// === Drag ===
// Profile drag
const cd_profile = plane.dragCoeff;
// Induced drag
const cd_induced = plane.inducedDragFactor * cl * cl;
const cd = cd_profile + cd_induced + (gs.aileron * gs.aileron + gs.elevator * gs.elevator) * 0.005;
gs.drag = q_dyn * plane.wingArea * cd;
// === Forces in body frame ===
// Thrust along body X
const thrustX = gs.thrust;
const thrustY = 0;
const thrustZ = 0;
// Lift along aircraft Y (up)
const liftX = 0;
const liftY = gs.lift;
const liftZ = 0;
// Drag opposes velocity - transform to body frame
const dragForce = gs.drag;
const windDir = _v2.clone(); // -velocity direction
const dragBody = windDir.clone().negate().multiplyScalar(dragForce);
// === Total force in body frame ===
const fx = thrustX + dragBody.x;
const fy = liftY + dragBody.y;
const fz = dragBody.z;
// === Convert forces to world frame ===
const forceWorld = new THREE.Vector3(fx, fy, fz).applyQuaternion(gs.quat);
// === Weight ===
const weight = _v1.set(0, -gs.mass * CONFIG.gravity, 0);
// === Total acceleration ===
const accel = forceWorld.add(weight).divideScalar(gs.mass);
// === Apply angular rates from control surfaces ===
// Pitch rate
const pitchRate = plane.pitchRate * gs.elevator;
// Roll rate
const rollRate = plane.rollRate * gs.aileron;
// Yaw rate
const yawRate = plane.yawRate * gs.rudder;
// Angular velocity in body frame
const omegaBody = new THREE.Vector3(
rollRate * gs.throttle + plane.rollRate * 0.1 * Math.sin(gs.time * 0.5),
yawRate * 0.5,
-pitchRate
);
// Convert omega to world frame
const omegaWorld = omegaBody.clone().applyQuaternion(gs.quat);
// === Update position ===
gs.vel.add(accel.multiplyScalar(dt));
// Speed clamp (prevent physics explosion)
const maxSpeed = plane.maxSpeed * 1.5;
if (gs.airspeed > maxSpeed) {
gs.vel.normalize().multiplyScalar(maxSpeed);
}
gs.pos.add(gs.vel.clone().multiplyScalar(dt));
// === Ground collision ===
const terrainH = getTerrainHeight(gs.pos.x, gs.pos.z);
if (gs.pos.y < terrainH + 2) {
gs.pos.y = terrainH + 2;
if (gs.vel.y < 0) gs.vel.y *= -0.3;
if (gs.airspeed < 5 && gs.pos.y < terrainH + 5) {
gs.onGround = true;
gs.vel.y = 0;
}
} else {
gs.onGround = false;
}
// === Update rotation ===
const qOmega = new THREE.Quaternion().setFromAxisAngle(
new THREE.Vector3(omegaWorld.x, omegaWorld.y, omegaWorld.z).normalize(),
omegaWorld.length() * dt
);
gs.quat.multiply(qOmega).normalize();
// === G-Force calculation ===
// G = (acceleration - gravity) / g, along aircraft Y
const gForceAccel = _v1.copy(accel).add(_v1.set(0, CONFIG.gravity, 0));
const gAlongBodyY = gForceAccel.dot(_v1.set(0, 1, 0).applyQuaternion(gs.quat));
gs.gForce = gAlongBodyY / CONFIG.gravity;
// Clamp G-force
gs.gForce = Math.max(gs.minGForce, Math.min(gs.maxGForce, gs.gForce));
// === Vertical speed ===
gs.verticalSpeed = gs.vel.y * 196.85; // m/s to ft/min
// === Stall check ===
const stallWarning = document.getElementById('stall-warning');
if (gs.airspeed < plane.stallSpeed * 1.1 && gs.pos.y > terrainH + 10) {
stallWarning.classList.add('active');
} else {
stallWarning.classList.remove('active');
}
// === Emit wake particles ===
if (gs.airspeed > 30 && gs.pos.y > terrainH + 5) {
const wingTipL = gs.quat.clone().multiply(new THREE.Vector3(0, 0, plane.wingSpan / 2));
const wingTipR = gs.quat.clone().multiply(new THREE.Vector3(0, 0, -plane.wingSpan / 2));
emitWake(
gs.pos.x + wingTipL.x, gs.pos.y + wingTipL.y, gs.pos.z + wingTipL.z,
-gs.vel.x * 0.05, -gs.vel.y * 0.02, -gs.vel.z * 0.05, 1
);
emitWake(
gs.pos.x + wingTipR.x, gs.pos.y + wingTipR.y, gs.pos.z + wingTipR.z,
-gs.vel.x * 0.05, -gs.vel.y * 0.02, -gs.vel.z * 0.05, 1
);
}
}
// ==================== AUDIO SYSTEM ====================
let audioCtx, masterGain;
let engineOsc1, engineOsc2, engineFilter, engineGain;
let windGain, windNoise;
let stallTone;
function initAudio() {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.4;
masterGain.connect(audioCtx.destination);
// Engine sound - dual oscillator
engineOsc1 = audioCtx.createOscillator();
engineOsc1.type = 'sawtooth';
engineOsc1.frequency.value = 80;
engineOsc2 = audioCtx.createOscillator();
engineOsc2.type = 'square';
engineOsc2.frequency.value = 160;
engineFilter = audioCtx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 800;
engineFilter.Q.value = 2;
engineGain = audioCtx.createGain();
engineGain.gain.value = 0;
engineOsc1.connect(engineFilter);
engineOsc2.connect(engineFilter);
engineFilter.connect(engineGain);
engineGain.connect(masterGain);
engineOsc1.start();
engineOsc2.start();
// Wind noise
const bufferSize = audioCtx.sampleRate * 2;
const noiseBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * 0.5;
}
windNoise = audioCtx.createBufferSource();
windNoise.buffer = noiseBuffer;
windNoise.loop = true;
const windFilter = audioCtx.createBiquadFilter();
windFilter.type = 'bandpass';
windFilter.frequency.value = 1000;
windFilter.Q.value = 0.5;
windGain = audioCtx.createGain();
windGain.gain.value = 0;
windNoise.connect(windFilter);
windFilter.connect(windGain);
windGain.connect(masterGain);
windNoise.start();
// Stall warning tone
stallTone = audioCtx.createOscillator();
stallTone.type = 'square';
stallTone.frequency.value = 800;
const stallGain = audioCtx.createGain();
stallGain.gain.value = 0;
stallTone.connect(stallGain);
stallGain.connect(masterGain);
stallTone.start();
stallTone._gain = stallGain;
gameState.audioInitialized = true;
} catch (e) {
console.log('Audio not available:', e);
}
}
function updateAudio() {
if (!gameState.audioInitialized || !audioCtx) return;
if (audioCtx.state !== 'running') return;
const throttle = gameState.throttle;
const airspeed = gameState.airspeed || 0;
const altitude = gameState.pos.y || 0;
try {
const ct = audioCtx.currentTime;
if (!isFinite(ct)) return;
if (engineOsc1 && engineOsc2 && engineFilter && engineGain) {
// Engine sound
const baseFreq = 60 + throttle * 300;
const jitter = Math.sin(gameState.time * 30) * 5 * throttle;
engineOsc1.frequency.setTargetAtTime(baseFreq + jitter, ct, 0.05);
engineOsc2.frequency.setTargetAtTime(baseFreq * 2 + jitter, ct, 0.05);
engineFilter.frequency.setTargetAtTime(400 + throttle * 2000, ct, 0.05);
engineGain.gain.setTargetAtTime(0.05 + throttle * 0.25, ct, 0.05);
}
// Wind noise
if (windGain) {
const windVol = Math.min(0.15, airspeed * 0.0005) * (1 - altitude / 15000);
windGain.gain.setTargetAtTime(Math.max(0, Math.min(0.15, windVol)), ct, 0.1);
}
// Stall warning
if (stallTone && stallTone._gain) {
const plane = AIRCRAFT[gameState.selectedPlane];
const isStalling = airspeed < plane.stallSpeed * 1.1 && gameState.pos.y > getTerrainHeight(gameState.pos.x, gameState.pos.z) + 10;
stallTone._gain.gain.setTargetAtTime(isStalling ? 0.15 : 0, ct, 0.05);
if (isStalling) {
stallTone.frequency.setTargetAtTime(800 + Math.sin(gameState.time * 8) * 200, ct, 0.05);
}
}
} catch (e) {
// Audio context may have been destroyed
}
}
// ==================== INPUT HANDLING ====================
function updateInput(dt) {
const gs = gameState;
// Keyboard controls
gs.pitchTarget = 0;
gs.rollTarget = 0;
gs.yawTarget = 0;
if (keys['KeyW']) gs.pitchTarget = 1;
if (keys['KeyS']) gs.pitchTarget = -1;
if (keys['KeyA']) gs.rollTarget = -1;
if (keys['KeyD']) gs.rollTarget = 1;
if (keys['KeyQ']) gs.yawTarget = -1;
if (keys['KeyE']) gs.yawTarget = 1;
// Mouse pitch/roll
if (mouse.locked) {
const sensitivity = 0.002;
gs.pitchTarget += mouse.dy * sensitivity;
gs.rollTarget += mouse.dx * sensitivity;
mouse.dx = 0;
mouse.dy = 0;
}
// Clamp
gs.pitchTarget = Math.max(-1, Math.min(1, gs.pitchTarget));
gs.rollTarget = Math.max(-1, Math.min(1, gs.rollTarget));
// Throttle
if (keys['ArrowUp'] || keys['KeyZ']) {
gs.throttleTarget = Math.min(1, gs.throttleTarget + dt * 0.5);
}
if (keys['ArrowDown'] || keys['KeyX']) {
gs.throttleTarget = Math.max(0, gs.throttleTarget - dt * 0.5);
}
// Camera
if (keys['KeyC']) {
gs.cameraMode = (gs.cameraMode + 1) % 3;
keys['KeyC'] = false;
}
// Reset
if (keys['KeyR']) {
gs.pos.set(0, 10, 0);
gs.vel.set(0, 0, 0);
gs.quat.identity();
gs.euler.set(0, 0, 0);
gs.throttle = 0;
gs.throttleTarget = 0;
gs.pitchTarget = 0;
gs.rollTarget = 0;
gs.yawTarget = 0;
gs.landed = false;
gs.runwayHit = false;
keys['KeyR'] = false;
}
// Gun fire (dogfight)
if (keys['Mouse0'] && gs.mode === 'dogfight' && gs.gunCooldown <= 0) {
fireGun();
}
// Update control surface targets
gs.elevatorTarget = gs.pitchTarget;
gs.aileronTarget = gs.rollTarget;
gs.rudderTarget = gs.yawTarget;
}
// ==================== DOGFIGHT ====================
function fireGun() {
const gs = gameState;
if (gs.ammo <= 0) return;
gs.ammo--;
gs.shotsFired++;
gs.heat += 5;
gs.gunCooldown = 0.08;
// Muzzle flash
muzzleFlash.material.opacity = 1;
muzzleFlash.scale.setScalar(2);
// Check hit on enemy
if (enemyGroup && enemyGroup.visible) {
const dir = new THREE.Vector3(1, 0, 0).applyQuaternion(gs.quat);
const toEnemy = _v1.copy(gs.enemyPos).sub(gs.pos);
const dist = toEnemy.length();
toEnemy.normalize();
const dot = dir.dot(toEnemy);
if (dot > 0.95 && dist < 2000) {
// Hit!
const accuracy = Math.max(0, dot - 0.95) * 20;
if (Math.random() < accuracy * 0.3) {
gs.enemyHP -= 5 + Math.random() * 5;
gs.shotsHit++;
if (gs.enemyHP <= 0) {
gs.kills++;
gs.enemyHP = 100;
gs.enemyPos.set(
(Math.random() - 0.5) * 20000,
1000 + Math.random() * 5000,
(Math.random() - 0.5) * 20000
);
showMission('TARGET DOWN! +100 PTS');
}
}
}
}
}
function updateEnemy(dt) {
const gs = gameState;
if (gs.mode !== 'dogfight') return;
if (!enemyGroup.visible) return;
// Enemy AI
gs.enemyAggression += (Math.sin(gs.time * 0.1) * 0.5) * dt;
gs.enemyAggression = Math.max(0.2, Math.min(0.9, gs.enemyAggression));
// Move toward player with some maneuvering
const toPlayer = _v1.copy(gs.pos).sub(gs.enemyPos);
const dist = toPlayer.length();
// Desired position
const desiredDist = 800 + Math.sin(gs.time * 0.3) * 300;
const desiredHeight = gs.pos.y + Math.sin(gs.time * 0.2) * 500;
_v2.copy(toPlayer).normalize().multiplyScalar(desiredDist);
_v2.y = desiredHeight;
// Maneuvering offset
_v3.set(
Math.sin(gs.time * 0.5) * 500,
Math.cos(gs.time * 0.7) * 300,
Math.cos(gs.time * 0.5) * 500
);
const targetPos = _v2.add(_v3);
const moveDir = targetPos.sub(gs.enemyPos).normalize();
// Enemy velocity
const enemySpeed = 80 + gs.enemyAggression * 60;
gs.enemyVel.lerp(moveDir.multiplyScalar(enemySpeed), dt * 2);
gs.enemyPos.add(gs.enemyVel.clone().multiplyScalar(dt));
// Enemy rotation - face direction of travel
if (gs.enemyVel.length() > 1) {
const targetQuat = new THREE.Quaternion().setFromUnitVectors(
new THREE.Vector3(1, 0, 0),
gs.enemyVel.clone().normalize()
);
gs.enemyQuat.slerp(targetQuat, dt * 3);
}
// Keep above terrain
const enemyTerrain = getTerrainHeight(gs.enemyPos.x, gs.enemyPos.z);
if (gs.enemyPos.y < enemyTerrain + 100) {
gs.enemyPos.y = enemyTerrain + 100;
}
// Update enemy mesh
enemyGroup.position.copy(gs.enemyPos);
enemyGroup.quaternion.copy(gs.enemyQuat);
// Gun cooldown
gs.gunCooldown -= dt;
gs.heat = Math.max(0, gs.heat - dt * 10);
// Muzzle flash decay
if (muzzleFlash) {
muzzleFlash.material.opacity *= 0.85;
muzzleFlash.scale.multiplyScalar(0.9);
}
}
// ==================== LANDING MODE ====================
function updateLanding(dt) {
const gs = gameState;
const plane = AIRCRAFT[gameState.selectedPlane];
if (gs.mode !== 'landing') return;
// Runway position
const runwayZ = 2000;
const runwayX = 0;
const runwayY = getTerrainHeight(runwayX, runwayZ);
// Check if near runway
const distToRunway = Math.sqrt(
Math.pow(gs.pos.x - runwayX, 2) + Math.pow(gs.pos.z - runwayZ, 2)
);
// Takeoff check
if (!gs.takeoffSpeedReached && gs.airspeed > plane.stallSpeed * 1.3 && gs.onGround) {
gs.takeoffSpeedReached = true;
showMission('AIRBORNE! NAVIGATE AND RETURN FOR LANDING');
}
// Landing scoring
if (gs.landed && !gs.runwayHit) {
if (distToRunway < 300 && gs.pos.y < runwayY + 50) {
gs.runwayHit = true;
let score = 0;
let msg = '';
// Speed penalty
if (gs.airspeed < 80) { score += 100; msg += 'PERFECT! +100\n'; }
else if (gs.airspeed < 120) { score += 70; msg += 'GOOD LANDING +70\n'; }
else if (gs.airspeed < 200) { score += 30; msg += 'ROUGH +30\n'; }
else { msg += 'TOO FAST!\n'; }
// Altitude penalty
if (Math.abs(gs.pos.y - runwayY) < 5) { score += 50; msg += 'FLAT +50\n'; }
else if (gs.pos.y < runwayY + 20) { score += 20; msg += 'OK +20\n'; }
// Position penalty
if (distToRunway < 100) { score += 50; msg += 'ON RUNWAY +50\n'; }
else if (distToRunway < 300) { score += 20; msg += 'CLOSE +20\n'; }
// G-force penalty
if (gs.gForce < 3) { score += 30; msg += 'SMOOTH +30\n'; }
else { msg += 'HARD IMPACT\n'; }
msg += `SCORE: ${score}`;
showMission(msg);
gs.score = score;
}
}
// Ground friction
if (gs.onGround) {
gs.vel.multiplyScalar(1 - dt * 0.3);
gs.landed = true;
}
}
// ==================== HUD UPDATE ====================
function updateHUD() {
const gs = gameState;
const plane = AIRCRAFT[gameState.selectedPlane];
const altAGL = Math.max(0, gs.pos.y - getTerrainHeight(gs.pos.x, gs.pos.z));
// Speed (knots)
const speedKnots = gs.airspeed * 1.944;
document.getElementById('hud-speed').innerHTML = `${Math.round(speedKnots)} <small>KT</small>`;
// Altitude (feet)
document.getElementById('hud-alt').innerHTML = `${Math.round(altAGL)} <small>FT</small>`;
// Vertical speed
const vs = gs.verticalSpeed;
document.getElementById('hud-vspeed').innerHTML = `${Math.round(vs)} <small>FT/MIN</small>`;
// Throttle
document.getElementById('hud-throttle').innerHTML = `${Math.round(gs.throttle * 100)} <small>%</small>`;
document.getElementById('hud-throttle-bar').style.width = `${gs.throttle * 100}%`;
// RPM
const rpm = Math.round(50 + gs.throttle * 950);
document.getElementById('hud-rpm').innerHTML = `${rpm} <small>×</div>`;
// AoA
document.getElementById('hud-aoa').innerHTML = `${gs.aoa.toFixed(1)} <small>°</small>`;
// G-force
const gColor = gs.gForce > 5 ? '#f44' : gs.gForce < -1 ? '#f44' : '#0cf';
document.getElementById('hud-g').innerHTML = `<span style="color:${gColor}">${gs.gForce.toFixed(1)}</span> <small>G</small>`;
// Pitch/Roll
const pitchDeg = gs.euler.x * 180 / Math.PI;
const rollDeg = gs.euler.z * 180 / Math.PI;
document.getElementById('hud-pitch-roll').innerHTML = `${pitchDeg.toFixed(1)}° / ${rollDeg.toFixed(1)}°`;
// Heading
let heading = gs.euler.y * 180 / Math.PI;
if (heading < 0) heading += 360;
document.getElementById('hud-heading').innerHTML = `${Math.round(heading)} <small>°</small>`;
// Engine temp
const temp = Math.round(gs.engineTemp);
document.getElementById('hud-temp-val').textContent = `${temp}°C`;
const tempBar = document.getElementById('hud-temp-bar');
tempBar.style.width = `${Math.min(100, temp / 520 * 100)}%`;
tempBar.style.background = temp > 400 ? '#f44' : temp > 300 ? '#ff4' : '#0f0';
// Dogfight HUD
if (gs.mode === 'dogfight') {
document.getElementById('hud-bottom').innerHTML = `
<div class="hud-label">Ammo</div>
<div class="hud-bar"><div class="hud-bar-fill" style="width:${gs.ammo/5}%;background:#ff4"></div></div>
<div style="font-size:11px;margin-top:2px">${gs.ammo} rounds</div>
<div class="hud-label" style="margin-top:8px">Heat</div>
<div class="hud-bar"><div class="hud-bar-fill" style="width:${Math.min(100,gs.heat)}%;background:${gs.heat>70?'#f44':'#ff4'}"></div></div>
<div style="font-size:11px;margin-top:2px">${Math.round(gs.heat)}°</div>
<div class="hud-label" style="margin-top:8px">Kills</div>
<div class="hud-value" style="font-size:16px">${gs.kills}</div>
`;
} else {
document.getElementById('hud-bottom').innerHTML = `
<div class="hud-label">Engine Temp</div>
<div class="hud-bar"><div class="hud-bar-fill" id="hud-temp-bar" style="width:${Math.min(100,gs.engineTemp/520*100)}%;background:${gs.engineTemp>400?'#f44':gs.engineTemp>300?'#ff4':'#0f0'}"></div></div>
<div id="hud-temp-val" style="font-size:11px;margin-top:2px">${Math.round(gs.engineTemp)}°C</div>
`;
}
// Minimap
updateMinimap();
}
function updateMinimap() {
const ctx = minimapCtx;
const w = 150, h = 150;
ctx.fillStyle = 'rgba(0, 20, 40, 0.8)';
ctx.fillRect(0, 0, w, h);
const scale = w / CONFIG.terrainSize * 3;
const cx = w / 2;
const cy = h / 2;
// Player
ctx.fillStyle = '#0cf';
ctx.beginPath();
ctx.arc(cx, cy, 3, 0, Math.PI * 2);
ctx.fill();
// Player heading indicator
const heading = gameState.euler.y;
ctx.strokeStyle = '#0cf';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.sin(heading) * 12, cy - Math.cos(heading) * 12);
ctx.stroke();
// Runway
const rwX = cx + (0 - gameState.pos.x) * scale;
const rwZ = cy + (2000 - gameState.pos.z) * scale;
ctx.fillStyle = '#444';
ctx.fillRect(rwX - 2, rwZ - 15, 4, 30);
// Enemy
if (gameState.mode === 'dogfight' && enemyGroup.visible) {
const ex = cx + (gameState.enemyPos.x - gameState.pos.x) * scale;
const ez = cy + (gameState.enemyPos.z - gameState.pos.z) * scale;
if (ex > 0 && ex < w && ez > 0 && ez < h) {
ctx.fillStyle = '#f44';
ctx.beginPath();
ctx.arc(ex, ez, 3, 0, Math.PI * 2);
ctx.fill();
}
}
// Border
ctx.strokeStyle = 'rgba(0, 200, 255, 0.3)';
ctx.lineWidth = 1;
ctx.strokeRect(0, 0, w, h);
}
// ==================== CAMERA ====================
function updateCamera() {
const gs = gameState;
const cam = camera;
if (gs.cameraMode === 0) {
// Chase camera
const dist = 30;
const height = 8;
const targetPos = _v1.copy(gs.pos);
// Behind aircraft
const behind = _v2.set(0, 0, 1).applyQuaternion(gs.quat).multiplyScalar(dist);
const up = _v3.set(0, 1, 0).applyQuaternion(gs.quat).multiplyScalar(height);
const camTarget = targetPos.clone().sub(behind).add(up);
cam.position.lerp(camTarget, 0.08);
cam.lookAt(gs.pos);
} else if (gs.cameraMode === 1) {
// Cockpit camera
const cockpitOffset = _v1.set(2.5, 0.5, 0).applyQuaternion(gs.quat);
cam.position.copy(gs.pos).add(cockpitOffset);
cam.quaternion.copy(gs.quat);
} else {
// Free camera
cam.position.copy(gs.pos);
cam.lookAt(gs.pos);
}
// Update sun shadow to follow player
sunLight.position.set(gs.pos.x + 5000, gs.pos.y + 8000, gs.pos.z + 3000);
sunLight.target.position.copy(gs.pos);
sunLight.target.updateMatrixWorld();
}
// ==================== MISSION OVERLAY ====================
let missionTimeout;
function showMission(text) {
const overlay = document.getElementById('mission-overlay');
overlay.innerHTML = text.replace(/\n/g, '<br>');
overlay.classList.add('visible');
clearTimeout(missionTimeout);
missionTimeout = setTimeout(() => overlay.classList.remove('visible'), 4000);
}
// ==================== SCENE MANAGEMENT ====================
function showMenu() {
gameState.running = false;
document.getElementById('menu').classList.remove('hidden');
document.getElementById('hud').style.display = 'none';
document.getElementById('controls-help').style.display = 'none';
document.getElementById('minimap').style.display = 'none';
document.getElementById('mission-overlay').classList.remove('visible');
if (document.pointerLockElement) document.exitPointerLock();
}
function startGame() {
const gs = gameState;
gs.running = true;
gs.mode = document.querySelector('.mode-card.selected').dataset.mode;
gs.selectedPlane = document.querySelector('.aircraft-card.selected').dataset.plane;
// Reset state
gs.pos.set(0, 10, -1500);
gs.vel.set(0, 0, 0);
gs.quat.identity();
gs.euler.set(0, 0, 0);
gs.throttle = 0;
gs.throttleTarget = 0;
gs.pitchTarget = 0;
gs.rollTarget = 0;
gs.yawTarget = 0;
gs.elevator = 0;
gs.aileron = 0;
gs.rudder = 0;
gs.onGround = true;
gs.landed = false;
gs.takeoffSpeedReached = false;
gs.score = 0;
gs.kills = 0;
gs.ammo = 500;
gs.heat = 0;
gs.enemyHP = 100;
gs.time = 0;
createAircraftModel(gs.selectedPlane);
if (gs.mode === 'dogfight') {
createEnemyModel();
gs.enemyPos.set(
(Math.random() - 0.5) * 15000,
2000 + Math.random() * 4000,
2000 + (Math.random() - 0.5) * 15000
);
gs.enemyVel.set(50, 0, 30);
gs.enemyQuat.setFromUnitVectors(new THREE.Vector3(1, 0, 0), gs.enemyVel.clone().normalize());
enemyGroup.position.copy(gs.enemyPos);
enemyGroup.quaternion.copy(gs.enemyQuat);
enemyGroup.visible = true;
showMission('DOGFIGHT ENGAGED — DOWN THE ENEMY');
} else {
showMission('TAKEOFF — PULL UP AT 80 KT');
}
document.getElementById('menu').classList.add('hidden');
document.getElementById('hud').style.display = 'block';
document.getElementById('controls-help').style.display = 'block';
document.getElementById('minimap').style.display = 'block';
if (audioCtx && audioCtx.state === 'suspended') audioCtx.resume();
// Request pointer lock
document.body.requestPointerLock();
}
// ==================== MENU HANDLERS ====================
function initMenu() {
const cards = document.querySelectorAll('.aircraft-card');
cards.forEach(card => {
card.addEventListener('click', () => {
cards.forEach(c => c.classList.remove('selected'));
card.classList.add('selected');
checkReady();
});
});
const modes = document.querySelectorAll('.mode-card');
modes.forEach(mode => {
mode.addEventListener('click', () => {
modes.forEach(m => m.classList.remove('selected'));
mode.classList.add('selected');
checkReady();
});
});
document.getElementById('start-btn').addEventListener('click', startGame);
}
function checkReady() {
const hasPlane = document.querySelector('.aircraft-card.selected');
const hasMode = document.querySelector('.mode-card.selected');
document.getElementById('start-btn').disabled = !(hasPlane && hasMode);
}
// ==================== MAIN LOOP ====================
function mainLoop() {
requestAnimationFrame(mainLoop);
if (!gameState.running) {
renderer.render(scene, camera);
return;
}
const now = performance.now();
const dt = Math.min((now - gameState.lastTime) / 1000, 0.05);
gameState.lastTime = now;
gameState.dt = dt;
gameState.time += dt;
// Update
updateInput(dt);
updatePhysics(dt);
updateEnemy(dt);
updateLanding(dt);
updateCamera();
updateAudio();
updateHUD();
updateWake(dt);
// Update aircraft mesh
if (aircraftGroup) {
aircraftGroup.position.copy(gameState.pos);
aircraftGroup.quaternion.copy(gameState.quat);
}
// Update clouds to follow player
clouds.forEach(cloud => {
const dx = cloud.position.x - gameState.pos.x;
const dz = cloud.position.z - gameState.pos.z;
if (Math.abs(dx) > 30000 || Math.abs(dz) > 30000) {
cloud.position.x = gameState.pos.x + (Math.random() - 0.5) * 60000;
cloud.position.z = gameState.pos.z + (Math.random() - 0.5) * 60000;
}
});
// Update runway lights
const runwayZ = 2000;
runwayLights.forEach((light, i) => {
light.position.y = getTerrainHeight(light.position.x, light.position.z) + 2;
// Blink effect
light.material.emissiveIntensity = 1.5 + Math.sin(gameState.time * 3 + i) * 0.5;
});
// Update sun position based on time
const sunAngle = gameState.time * 0.01;
sunLight.position.set(
Math.cos(sunAngle) * 10000,
8000 + Math.sin(sunAngle) * 2000,
Math.sin(sunAngle) * 5000
);
// Fog color based on altitude
const altRatio = Math.max(0, Math.min(1, gameState.pos.y / 10000));
const fogR = 0.53 + altRatio * 0.1;
const fogG = 0.81 - altRatio * 0.3;
const fogB = 0.92 - altRatio * 0.4;
scene.fog.color.setRGB(fogR, fogG, fogB);
scene.background.setRGB(fogR, fogG, fogB);
// Render
renderer.render(scene, camera);
}
// ==================== INITIALIZATION ====================
function init() {
initMenu();
initThreeJS();
generateTerrain();
createRunway();
createClouds();
createWakeSystem();
initAudio();
// Hide loading
setTimeout(() => {
document.getElementById('loading').classList.add('hidden');
}, 500);
gameState.lastTime = performance.now();
mainLoop();
}
// Start when Three.js is loaded
if (typeof THREE !== 'undefined') {
init();
} else {
window.addEventListener('load', init);
}
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"full","sessionId":"0ee6807e-45b2-4117-802b-b78fe816319a","tokensIn":1265539,"tokensOut":32588,"tokensTotal":1298127,"turns":23,"toolCalls":22,"failedToolCalls":0,"timestamp":"2026-07-21T01:33:21.769Z"} -->