Flight Simulator
Qwen 3.6 35b a3b · composer
◈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 67,020 bytes · SHA-256 09b5b8c3ea4e
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Apex Aero — Flight Simulator</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #000; font-family: 'Courier New', monospace; }
#gameCanvas { display: block; width: 100%; height: 100%; }
/* Menu Overlay */
.menu-overlay {
position: fixed; inset: 0; z-index: 100;
display: flex; align-items: center; justify-content: center;
background: linear-gradient(135deg, rgba(10,15,30,0.95), rgba(20,30,50,0.92));
backdrop-filter: blur(8px);
transition: opacity 0.5s;
}
.menu-overlay.hidden { opacity: 0; pointer-events: none; }
.menu-panel {
text-align: center; max-width: 900px; width: 90%;
padding: 40px;
}
.menu-title {
font-size: 64px; font-weight: 900; letter-spacing: 12px;
background: linear-gradient(180deg, #fff, #4a9eff);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
text-shadow: 0 0 40px rgba(74,158,255,0.3);
margin-bottom: 4px;
}
.menu-subtitle {
font-size: 18px; color: #4a9eff; letter-spacing: 8px;
margin-bottom: 36px; opacity: 0.8;
}
.menu-section { margin-bottom: 28px; }
.menu-section h3 {
font-size: 14px; color: #4a9eff; letter-spacing: 4px;
margin-bottom: 12px; opacity: 0.7;
}
.aircraft-options, .scenario-options {
display: flex; gap: 16px; justify-content: center; flex-wrap: wrap;
}
.aircraft-card, .scenario-card {
background: rgba(255,255,255,0.04); border: 1px solid rgba(74,158,255,0.15);
border-radius: 12px; padding: 20px 24px; cursor: pointer;
transition: all 0.3s; min-width: 200px; text-align: center;
}
.aircraft-card:hover, .scenario-card:hover {
background: rgba(74,158,255,0.12); border-color: rgba(74,158,255,0.4);
transform: translateY(-2px);
}
.aircraft-card.selected {
background: rgba(74,158,255,0.18); border-color: #4a9eff;
box-shadow: 0 0 20px rgba(74,158,255,0.2);
}
.scenario-card.selected {
background: rgba(74,158,255,0.18); border-color: #4a9eff;
box-shadow: 0 0 20px rgba(74,158,255,0.2);
}
.plane-icon { font-size: 36px; margin-bottom: 8px; }
.scenario-icon { font-size: 32px; margin-bottom: 8px; }
.aircraft-card h4, .scenario-card h4 { color: #fff; font-size: 16px; margin-bottom: 6px; }
.plane-stats { color: #8ab4f8; font-size: 12px; }
.scenario-card p { color: #8ab4f8; font-size: 12px; }
.menu-btn {
background: linear-gradient(135deg, #4a9eff, #1a6fd4);
border: none; color: #fff; font-family: inherit;
font-size: 20px; font-weight: 700; letter-spacing: 6px;
padding: 16px 64px; border-radius: 8px; cursor: pointer;
transition: all 0.3s; margin-top: 12px;
}
.menu-btn:hover { transform: scale(1.05); box-shadow: 0 0 30px rgba(74,158,255,0.4); }
.controls-hint {
color: #5a7a9a; font-size: 11px; margin-top: 20px; letter-spacing: 1px;
}
/* HUD */
.hud { position: fixed; inset: 0; z-index: 50; pointer-events: none; }
.hud.hidden { display: none; }
.hud-left, .hud-right {
position: absolute; top: 20px; display: flex; flex-direction: column; gap: 4px;
}
.hud-left { left: 24px; }
.hud-right { right: 24px; }
.hud-item {
display: flex; align-items: baseline; gap: 4px;
background: rgba(0,0,0,0.4); padding: 4px 12px; border-radius: 4px;
border-left: 2px solid #4a9eff;
}
.hud-label { color: #4a9eff; font-size: 10px; letter-spacing: 2px; }
.hud-value { color: #fff; font-size: 20px; font-weight: 700; }
.hud-unit { color: #5a7a9a; font-size: 10px; }
.hud-center {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
display: flex; flex-direction: column; align-items: center;
}
.hud-attitude {
width: 200px; height: 200px; border-radius: 50%;
border: 2px solid rgba(74,158,255,0.4);
overflow: hidden; position: relative;
background: linear-gradient(180deg, #1a3a5c 50%, #3a2a1a 50%);
}
.horizon-line {
position: absolute; left: -50%; right: -50%; height: 2px;
background: #4a9eff; top: 50%;
transition: none;
}
.pitch-ladder {
position: absolute; inset: 0;
}
.pitch-ladder-line {
position: absolute; left: 50%; transform: translateX(-50%);
width: 40px; height: 1px; background: rgba(255,255,255,0.5);
}
.pitch-ladder-label {
position: absolute; left: 50%; transform: translateX(-50%);
color: rgba(255,255,255,0.5); font-size: 8px; margin-left: 24px;
}
.center-dot {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 20px; height: 2px; background: #4a9eff;
}
.center-dot::before {
content: ''; position: absolute; left: 50%; top: 50%;
transform: translate(-50%, -50%); width: 6px; height: 6px;
border: 1px solid #4a9eff; border-radius: 50%;
}
.hud-instruments {
display: flex; gap: 24px; margin-top: 12px;
}
.mini-gauge { text-align: center; }
.gauge-label { color: #4a9eff; font-size: 9px; letter-spacing: 2px; }
.gauge-bar {
width: 80px; height: 6px; background: rgba(255,255,255,0.1);
border-radius: 3px; overflow: hidden; margin-top: 4px;
}
.gauge-fill {
height: 100%; border-radius: 3px;
background: linear-gradient(90deg, #4a9eff, #ff6b35);
transition: width 0.1s;
}
.gauge-value { color: #fff; font-size: 18px; font-weight: 700; }
.hud-bottom {
position: absolute; bottom: 20px; left: 50%; transform: translateX(-50%);
color: #4a9eff; font-size: 14px; letter-spacing: 2px;
text-align: center; text-shadow: 0 0 10px rgba(74,158,255,0.5);
}
/* Dogfight UI */
.dogfight-ui { position: fixed; inset: 0; z-index: 55; pointer-events: none; }
.dogfight-ui.hidden { display: none; }
.dogfight-score {
position: absolute; top: 20px; left: 50%; transform: translateX(-50%);
display: flex; gap: 32px;
}
.score-item { text-align: center; background: rgba(0,0,0,0.4); padding: 6px 16px; border-radius: 4px; }
.score-label { color: #ff6b35; font-size: 10px; letter-spacing: 2px; }
.score-value { color: #fff; font-size: 24px; font-weight: 700; }
.dogfight-crosshair {
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);
width: 40px; height: 40px;
}
.dogfight-crosshair::before, .dogfight-crosshair::after {
content: ''; position: absolute; background: rgba(255,107,53,0.7);
}
.dogfight-crosshair::before { left: 50%; top: 0; bottom: 0; width: 1px; transform: translateX(-50%); }
.dogfight-crosshair::after { top: 50%; left: 0; right: 0; height: 1px; transform: translateY(-50%); }
/* Mission UI */
.mission-ui { position: fixed; top: 80px; left: 50%; transform: translateX(-50%); z-index: 55; pointer-events: none; }
.mission-ui.hidden { display: none; }
.mission-objective {
background: rgba(0,0,0,0.5); border: 1px solid rgba(74,158,255,0.3);
padding: 12px 24px; border-radius: 8px; color: #fff; font-size: 14px;
text-align: center; letter-spacing: 1px;
}
</style>
</head>
<body>
<canvas id="gameCanvas"></canvas>
<div id="menuOverlay" class="menu-overlay">
<div class="menu-panel">
<h1 class="menu-title">APEX AERO</h1>
<p class="menu-subtitle">Flight Simulator</p>
<div class="menu-section">
<h3>Select Aircraft</h3>
<div class="aircraft-options">
<div class="aircraft-card" data-plane="fighter" onclick="selectPlane('fighter')">
<div class="plane-icon">✈</div>
<h4>F-16 Fighter</h4>
<p class="plane-stats">High Speed · Agile · Medium Range</p>
</div>
<div class="aircraft-card" data-plane="bomber" onclick="selectPlane('bomber')">
<div class="plane-icon">✈</div>
<h4>B-2 Bomber</h4>
<p class="plane-stats">Heavy · Stable · Low Speed</p>
</div>
<div class="aircraft-card" data-plane="glider" onclick="selectPlane('glider')">
<div class="plane-icon">✈</div>
<h4>Sailplane</h4>
<p class="plane-stats">Ultra Light · High Lift · No Engine</p>
</div>
</div>
</div>
<div class="menu-section">
<h3>Select Scenario</h3>
<div class="scenario-options">
<div class="scenario-card" data-scenario="takeoff" onclick="selectScenario('takeoff')">
<div class="scenario-icon">🛫</div>
<h4>Takeoff & Landing</h4>
<p>Master precision flight on a runway</p>
</div>
<div class="scenario-card" data-scenario="dogfight" onclick="selectScenario('dogfight')">
<div class="scenario-icon">⚔️</div>
<h4>Dogfight</h4>
<p>High-G aerial combat</p>
</div>
</div>
</div>
<button class="menu-btn" id="startBtn" onclick="startGame()">LAUNCH</button>
<p class="controls-hint">W/S: Pitch · A/D: Roll · Q/E: Yaw · Shift/Ctrl: Throttle · F: Brake · R: Restart</p>
</div>
</div>
<div id="hud" class="hud hidden">
<div class="hud-left">
<div class="hud-item"><span class="hud-label">SPD</span><span class="hud-value" id="hudSpeed">0</span><span class="hud-unit">kt</span></div>
<div class="hud-item"><span class="hud-label">ALT</span><span class="hud-value" id="hudAlt">0</span><span class="hud-unit">ft</span></div>
<div class="hud-item"><span class="hud-label">HDG</span><span class="hud-value" id="hudHdg">000</span><span class="hud-unit">°</span></div>
</div>
<div class="hud-center">
<div class="hud-attitude" id="hudAttitude">
<div class="horizon-line" id="horizonLine"></div>
<div class="pitch-ladder" id="pitchLadder"></div>
<div class="flight-path-marker" id="fpm"></div>
<div class="center-dot"></div>
</div>
<div class="hud-instruments">
<div class="mini-gauge"><div class="gauge-label">THR</div><div class="gauge-bar"><div class="gauge-fill" id="throttleBar" style="width:0%"></div></div></div>
<div class="mini-gauge"><div class="gauge-label">AoA</div><div class="gauge-bar"><div class="gauge-fill" id="aoaBar" style="width:50%"></div></div></div>
<div class="mini-gauge"><div class="gauge-label">G</div><div class="gauge-value" id="gForce">1.0</div></div>
</div>
</div>
<div class="hud-right">
<div class="hud-item"><span class="hud-label">RPM</span><span class="hud-value" id="hudRpm">0</span><span class="hud-unit">%</span></div>
<div class="hud-item"><span class="hud-label">VS</span><span class="hud-value" id="hudVs">0</span><span class="hud-unit">ft/s</span></div>
<div class="hud-item"><span class="hud-label">STALL</span><span class="hud-value" id="hudStall">—</span></div>
</div>
<div class="hud-bottom" id="hudMessage"></div>
</div>
<div id="dogfightUI" class="dogfight-ui hidden">
<div class="dogfight-score"><div class="score-item"><span class="score-label">KILLS</span><span class="score-value" id="killCount">0</span></div><div class="score-item"><span class="score-label">TURNS</span><span class="score-value" id="turnCount">0</span></div></div>
<div class="dogfight-crosshair" id="dogfightCrosshair"></div>
</div>
<div id="missionUI" class="mission-ui hidden"><div class="mission-objective" id="missionObj"></div></div>
<script>
'use strict';
// ── const: DEG2RAD ──
// ============================================================
// CONSTANTS
// ============================================================
const DEG2RAD = Math.PI / 180;
// ── const: RAD2DEG ──
const RAD2DEG = 180 / Math.PI;
// ── const: FT2M ──
const FT2M = 0.3048;
// ── const: M2FT ──
const M2FT = 1 / FT2M;
// ── const: KT2MS ──
const KT2MS = 0.514444;
// ── const: MS2KT ──
const MS2KT = 1 / KT2MS;
// ── const: SEA_LEVEL_DENSITY ──
const SEA_LEVEL_DENSITY = 1.225;
// ── const: GRAVITY ──
// kg/m³
const GRAVITY = 9.81;
// ── const: AIRCRAFT_DEFS ──
// m/s²
// ============================================================
// AIRCRAFT DEFINITIONS
// ============================================================
const AIRCRAFT_DEFS = {
fighter: {
name: "F-16 Fighter",
mass: 9500, // kg
wingArea: 27.87, // m²
wingspan: 9.96, // m
maxThrust: 129000, // N (afterburner)
cruiseThrust: 45000, // N
dragCoeff: 0.028, // base profile drag
inducedDragFactor: 0.045,
liftCurveSlope: 5.7, // per radian
stallAngle: 16 * DEG2RAD,
maxAoA: 22 * DEG2RAD,
rollRate: 2.5, // rad/s per unit input
pitchRate: 1.8,
yawRate: 1.2,
rollDamping: 3.0,
pitchDamping: 2.5,
yawDamping: 2.0,
throttleResponse: 0.8, // fraction of max thrust at idle
maxG: 9.0,
color: 0x4a9eff,
accentColor: 0xff6b35,
},
bomber: {
name: "B-2 Bomber",
mass: 170000,
wingArea: 470,
wingspan: 52.4,
maxThrust: 200000,
cruiseThrust: 80000,
dragCoeff: 0.022,
inducedDragFactor: 0.035,
liftCurveSlope: 4.8,
stallAngle: 14 * DEG2RAD,
maxAoA: 18 * DEG2RAD,
rollRate: 0.8,
pitchRate: 0.6,
yawRate: 0.5,
rollDamping: 5.0,
pitchDamping: 4.0,
yawDamping: 3.5,
throttleResponse: 0.75,
maxG: 2.5,
color: 0x3a3a3a,
accentColor: 0xff4444,
},
glider: {
name: "Sailplane",
mass: 500,
wingArea: 12.0,
wingspan: 15.0,
maxThrust: 0,
cruiseThrust: 0,
dragCoeff: 0.018,
inducedDragFactor: 0.030,
liftCurveSlope: 6.2,
stallAngle: 12 * DEG2RAD,
maxAoA: 16 * DEG2RAD,
rollRate: 1.5,
pitchRate: 1.0,
yawRate: 0.8,
rollDamping: 4.0,
pitchDamping: 3.0,
yawDamping: 2.5,
throttleResponse: 0,
maxG: 5.0,
color: 0xffffff,
accentColor: 0x4a9eff,
},
};
// ── const: TERRAIN_SIZE ──
// ============================================================
// TERRAIN CONFIG
// ============================================================
const TERRAIN_SIZE = 80000;
// ── const: TERRAIN_RES ──
// meters
const TERRAIN_RES = 512;
// ── const: TERRAIN_CHUNK_SIZE ──
// vertices
const TERRAIN_CHUNK_SIZE = 4000;
// ── const: RUNWAY_LENGTH ──
// meters per LOD chunk
const RUNWAY_LENGTH = 3000;
// ── const: RUNWAY_WIDTH ──
// meters
const RUNWAY_WIDTH = 60;
// ── const: RUNWAY_START ──
// meters
const RUNWAY_START = -RUNWAY_LENGTH / 2;
// ── const: RUNWAY_END ──
const RUNWAY_END = RUNWAY_LENGTH / 2;
// ── function: createAircraft ──
// ============================================================
// AIRCRAFT PHYSICS ENGINE
// ============================================================
function createAircraft(type) {
const def = AIRCRAFT_DEFS[type];
return {
type: type,
def: def,
pos: new THREE.Vector3(0, 50, 0),
vel: new THREE.Vector3(0, 0, 0),
rot: new THREE.Euler(0, 0, 0, 'YXZ'),
angularVel: new THREE.Vector3(0, 0, 0),
throttle: 0,
targetThrottle: 0,
pitchInput: 0,
rollInput: 0,
yawInput: 0,
aoa: 0,
sideslip: 0,
dynamicPressure: 0,
lift: 0,
drag: 0,
thrust: 0,
gForce: 1,
isStalled: false,
stallProgress: 0,
gearDown: false,
gearExtension: 0,
brake: false,
rpm: 0,
engineTemp: 20,
speedBrake: 0,
lastTime: 0,
trail: [],
crashed: false,
};
}
// ── function: resetAircraft ──
function resetAircraft(aircraft) {
aircraft.pos.set(0, 5, 0);
aircraft.vel.set(0, 0, 0);
aircraft.rot.set(0, 0, 0);
aircraft.angularVel.set(0, 0, 0);
aircraft.throttle = 0;
aircraft.targetThrottle = 0;
aircraft.pitchInput = 0;
aircraft.rollInput = 0;
aircraft.yawInput = 0;
aircraft.gearDown = false;
aircraft.gearExtension = 0;
aircraft.brake = false;
aircraft.speedBrake = 0;
aircraft.crashed = false;
aircraft.trail = [];
}
// ── function: createAircraftMesh ──
// ============================================================
// AIRCRAFT 3D MODEL CREATION
// ============================================================
function createAircraftMesh(type) {
const group = new THREE.Group();
const bodyColor = type === 'fighter' ? 0x4a9eff : type === 'bomber' ? 0x5a5a5a : 0xeeeeee;
const accentColor = type === 'fighter' ? 0xff6b35 : type === 'bomber' ? 0xff4444 : 0x4a9eff;
if (type === 'fighter') {
const bodyGeo = new THREE.ConeGeometry(0.8, 12, 8);
bodyGeo.rotateX(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.6, roughness: 0.3 });
group.add(new THREE.Mesh(bodyGeo, bodyMat));
const cockpitGeo = new THREE.SphereGeometry(0.7, 8, 6, 0, Math.PI * 2, 0, Math.PI / 2);
cockpitGeo.scale(0.8, 0.5, 1.2);
const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x88ccff, metalness: 0.9, roughness: 0.1, transparent: true, opacity: 0.7 });
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(0, 0.5, 2);
group.add(cockpit);
const wingGeo = new THREE.BoxGeometry(10, 0.15, 2.5);
const wingMat = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.5, roughness: 0.4 });
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.set(0, 0, -1);
group.add(wings);
for (const side of [-1, 1]) {
const tipGeo = new THREE.CylinderGeometry(0.05, 0.05, 1.5, 6);
tipGeo.rotateZ(side * Math.PI / 2);
const tipMat = new THREE.MeshStandardMaterial({ color: accentColor });
const tip = new THREE.Mesh(tipGeo, tipMat);
tip.position.set(side * 5.2, 0, -1);
group.add(tip);
}
const tailGeo = new THREE.BoxGeometry(4, 0.1, 1.5);
const tailMat = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.5, roughness: 0.4 });
group.add(new THREE.Mesh(tailGeo, tailMat).translateZ(-5.5));
const vStabGeo = new THREE.BoxGeometry(0.1, 2.5, 1.8);
const vStabMat = new THREE.MeshStandardMaterial({ color: accentColor, metalness: 0.5, roughness: 0.4 });
const vStab = new THREE.Mesh(vStabGeo, vStabMat);
vStab.position.set(0, 1.2, -5.5);
group.add(vStab);
const nozzleGeo = new THREE.CylinderGeometry(0.5, 0.7, 1, 8);
nozzleGeo.rotateX(Math.PI / 2);
const nozzleMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.8, roughness: 0.2 });
const nozzle = new THREE.Mesh(nozzleGeo, nozzleMat);
nozzle.position.set(0, 0, -6);
group.add(nozzle);
const glowGeo = new THREE.SphereGeometry(0.4, 8, 8);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.6 });
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.position.set(0, 0, -6.5);
glow.name = 'engineGlow';
group.add(glow);
} else if (type === 'bomber') {
const bodyGeo = new THREE.BoxGeometry(3, 1.5, 14);
const bodyMat = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.4, roughness: 0.6 });
group.add(new THREE.Mesh(bodyGeo, bodyMat));
const noseGeo = new THREE.SphereGeometry(1.5, 8, 8, 0, Math.PI, 0, Math.PI / 2);
noseGeo.rotateX(-Math.PI / 2);
noseGeo.scale(1, 0.5, 1.5);
const nose = new THREE.Mesh(noseGeo, bodyMat);
nose.position.set(0, 0, 7);
group.add(nose);
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.lineTo(12, -8);
wingShape.lineTo(12, -10);
wingShape.lineTo(0, -6);
wingShape.lineTo(-12, -10);
wingShape.lineTo(-12, -8);
wingShape.lineTo(0, 0);
const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.2, bevelEnabled: false });
const wingMat = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.4, roughness: 0.6 });
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.rotation.x = -Math.PI / 2;
wings.position.set(0, -0.5, 0);
group.add(wings);
for (const side of [-1, 1]) {
const podGeo = new THREE.CylinderGeometry(0.8, 0.8, 4, 8);
podGeo.rotateX(Math.PI / 2);
const podMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.6, roughness: 0.3 });
const pod = new THREE.Mesh(podGeo, podMat);
pod.position.set(side * 4, -0.3, 1);
group.add(pod);
const glowGeo = new THREE.SphereGeometry(0.5, 8, 8);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xff6600, transparent: true, opacity: 0.6 });
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.position.set(side * 4, -0.3, -1);
glow.name = 'engineGlow';
group.add(glow);
}
for (const side of [-1, 1]) {
const finGeo = new THREE.BoxGeometry(0.15, 2, 3);
const finMat = new THREE.MeshStandardMaterial({ color: accentColor, metalness: 0.4, roughness: 0.6 });
const fin = new THREE.Mesh(finGeo, finMat);
fin.position.set(side * 1.2, 1, -5);
group.add(fin);
}
} else if (type === 'glider') {
const bodyGeo = new THREE.ConeGeometry(0.4, 8, 6);
bodyGeo.rotateX(Math.PI / 2);
const bodyMat = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.3, roughness: 0.7 });
group.add(new THREE.Mesh(bodyGeo, bodyMat));
const cockpitGeo = new THREE.SphereGeometry(0.35, 6, 6, 0, Math.PI * 2, 0, Math.PI / 2);
cockpitGeo.scale(0.6, 0.5, 1);
const cockpitMat = new THREE.MeshStandardMaterial({ color: 0x88ccff, metalness: 0.9, roughness: 0.1, transparent: true, opacity: 0.7 });
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(0, 0.3, 1);
group.add(cockpit);
const wingGeo = new THREE.BoxGeometry(15, 0.08, 1.2);
const wingMat = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.3, roughness: 0.7 });
group.add(new THREE.Mesh(wingGeo, wingMat).translateY(0).translateZ(-0.5));
const tailGeo = new THREE.BoxGeometry(2.5, 0.08, 0.8);
const tailMat = new THREE.MeshStandardMaterial({ color: bodyColor, metalness: 0.3, roughness: 0.7 });
group.add(new THREE.Mesh(tailGeo, tailMat).translateZ(-3.5));
const vStabGeo = new THREE.BoxGeometry(0.08, 1.2, 0.8);
const vStabMat = new THREE.MeshStandardMaterial({ color: accentColor, metalness: 0.3, roughness: 0.7 });
const vStab = new THREE.Mesh(vStabGeo, vStabMat);
vStab.position.set(0, 0.6, -3.5);
group.add(vStab);
for (const side of [-1, 1]) {
const weightGeo = new THREE.SphereGeometry(0.15, 6, 6);
const weightMat = new THREE.MeshStandardMaterial({ color: accentColor });
const weight = new THREE.Mesh(weightGeo, weightMat);
weight.position.set(side * 7.5, 0, -0.5);
group.add(weight);
}
}
group.traverse(function(child) {
if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; }
});
return group;
}
// ── let: audioCtx ──
// ============================================================
// AUDIO ENGINE (Web Audio API)
// ============================================================
let audioCtx = null;
// ── let: masterGain ──
let masterGain = null;
// ── let: engineGain ──
let engineGain = null;
// ── let: windGain ──
let windGain = null;
// ── let: engineOsc ──
let engineOsc = null;
// ── let: engineNoise ──
let engineNoise = null;
// ── let: windNoise ──
let windNoise = null;
// ── function: initAudio ──
function initAudio() {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
masterGain = audioCtx.createGain();
masterGain.gain.value = 0.4;
masterGain.connect(audioCtx.destination);
// Engine sound: oscillator + noise
engineGain = audioCtx.createGain();
engineGain.gain.value = 0;
engineGain.connect(masterGain);
engineOsc = audioCtx.createOscillator();
engineOsc.type = 'sawtooth';
engineOsc.frequency.value = 80;
engineOsc.connect(engineGain);
engineOsc.start();
// Low-pass filter for engine
const engineFilter = audioCtx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 800;
engineFilter.Q.value = 2;
engineOsc.connect(engineFilter);
engineFilter.connect(engineGain);
// Engine noise
engineNoise = audioCtx.createBufferSource();
const bufferSize = audioCtx.sampleRate * 2;
const buffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const data = buffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
data[i] = (Math.random() * 2 - 1) * 0.3;
}
engineNoise.buffer = buffer;
engineNoise.loop = true;
const noiseFilter = audioCtx.createBiquadFilter();
noiseFilter.type = 'bandpass';
noiseFilter.frequency.value = 300;
noiseFilter.Q.value = 1;
const noiseGain = audioCtx.createGain();
noiseGain.gain.value = 0.15;
engineNoise.connect(noiseFilter);
noiseFilter.connect(noiseGain);
noiseGain.connect(engineGain);
engineNoise.start();
// Wind noise
windGain = audioCtx.createGain();
windGain.gain.value = 0;
windGain.connect(masterGain);
windNoise = audioCtx.createBufferSource();
const windBuffer = audioCtx.createBuffer(1, bufferSize, audioCtx.sampleRate);
const windData = windBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
windData[i] = (Math.random() * 2 - 1);
}
windNoise.buffer = windBuffer;
windNoise.loop = true;
const windFilter = audioCtx.createBiquadFilter();
windFilter.type = 'lowpass';
windFilter.frequency.value = 500;
windFilter.Q.value = 0.5;
windNoise.connect(windFilter);
windFilter.connect(windGain);
windNoise.start();
}
// ── function: updateAudio ──
function updateAudio(aircraft, speed) {
if (!audioCtx) return;
const throttle = aircraft.throttle;
const rpm = aircraft.rpm / 100;
const alt = Math.max(0, aircraft.pos.y);
const airDensity = Math.exp(-alt / 8500);
// Engine pitch based on RPM
engineOsc.frequency.setTargetAtTime(60 + rpm * 200, audioCtx.currentTime, 0.05);
// Engine volume based on throttle and air density
const engineVol = throttle * (0.3 + airDensity * 0.7);
engineGain.gain.setTargetAtTime(engineVol, audioCtx.currentTime, 0.05);
// Wind noise based on speed
const windVol = Math.min(0.3, speed * 0.003) * airDensity;
windGain.gain.setTargetAtTime(windVol, audioCtx.currentTime, 0.1);
// Filter cutoff based on speed
const engineFilter = engineOsc.context.createBiquadFilter();
}
// ── function: createEnemyAI ──
// ============================================================
// DOGFIGHT ENEMY AI
// ============================================================
function createEnemyAI() {
return {
pos: new THREE.Vector3(500, 300, 500),
vel: new THREE.Vector3(-10, 0, -10),
rot: new THREE.Euler(0, 0, 0, 'YXZ'),
targetPos: new THREE.Vector3(),
alive: true,
shootCooldown: 0,
maneuvers: [],
currentManeuver: 0,
maneuverTimer: 0,
};
}
// ── function: updateEnemyAI ──
function updateEnemyAI(enemy, player, dt) {
if (!enemy.alive) return;
enemy.maneuverTimer += dt;
if (enemy.maneuverTimer > 3 + Math.random() * 4) {
enemy.maneuverTimer = 0;
// Pick new target
const angle = Math.random() * Math.PI * 2;
const dist = 400 + Math.random() * 600;
const alt = 100 + Math.random() * 500;
enemy.targetPos.set(
player.pos.x + Math.cos(angle) * dist,
alt,
player.pos.z + Math.sin(angle) * dist
);
}
// Move toward target
const toTarget = enemy.targetPos.clone().sub(enemy.pos);
const distToTarget = toTarget.length();
if (distToTarget > 10) {
toTarget.normalize();
const speed = 30 + Math.random() * 10;
enemy.vel.x += toTarget.x * speed * dt;
enemy.vel.y += toTarget.y * speed * dt * 0.5;
enemy.vel.z += toTarget.z * speed * dt;
}
// Avoid terrain
const groundH = getTerrainHeight(enemy.pos.x, enemy.pos.z);
if (enemy.pos.y < groundH + 50) {
enemy.vel.y += 5 * dt;
}
// Clamp speed
const maxSpeed = 60;
const currentSpeed = enemy.vel.length();
if (currentSpeed > maxSpeed) {
enemy.vel.multiplyScalar(maxSpeed / currentSpeed);
}
// Update position
enemy.pos.x += enemy.vel.x * dt;
enemy.pos.y += enemy.vel.y * dt;
enemy.pos.z += enemy.vel.z * dt;
// Align with velocity
const lookTarget = enemy.pos.clone().add(enemy.vel);
enemy.rot.setFromVectorMatrix(new THREE.Matrix4().lookAt(enemy.pos, lookTarget, new THREE.Vector3(0, 1, 0)));
// Shoot at player if close enough
const distToPlayer = enemy.pos.distanceTo(player.pos);
enemy.shootCooldown -= dt;
if (distToPlayer < 800 && enemy.shootCooldown <= 0) {
enemy.shootCooldown = 2 + Math.random() * 3;
return 'shoot';
}
return null;
}
// ── function: createEnemyMesh ──
function createEnemyMesh() {
const group = new THREE.Group();
const bodyMat = new THREE.MeshStandardMaterial({ color: 0x884444, metalness: 0.4, roughness: 0.5 });
const bodyGeo = new THREE.ConeGeometry(0.6, 8, 6);
bodyGeo.rotateX(Math.PI / 2);
group.add(new THREE.Mesh(bodyGeo, bodyMat));
const wingGeo = new THREE.BoxGeometry(7, 0.1, 2);
const wingMat = new THREE.MeshStandardMaterial({ color: 0x663333, metalness: 0.4, roughness: 0.5 });
group.add(new THREE.Mesh(wingGeo, wingMat).translateZ(-0.5));
const vStabGeo = new THREE.BoxGeometry(0.1, 2, 1.5);
const vStab = new THREE.Mesh(vStabGeo, bodyMat);
vStab.position.set(0, 1, -3.5);
group.add(vStab);
const glowGeo = new THREE.SphereGeometry(0.3, 6, 6);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xff4400, transparent: true, opacity: 0.6 });
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.position.set(0, 0, -4.5);
glow.name = 'engineGlow';
group.add(glow);
group.traverse(function(child) {
if (child.isMesh) { child.castShadow = true; child.receiveShadow = true; }
});
return group;
}
// ── function: updateHUD ──
// ============================================================
// HUD UPDATE
// ============================================================
function updateHUD(aircraft, missionState) {
const speed = aircraft.vel.length();
const speedKts = Math.round(speed * MS2KT);
const altFt = Math.round(aircraft.pos.y * M2FT);
const heading = ((aircraft.rot.y * RAD2DEG) % 360 + 360) % 360;
const vsFt = aircraft.vel.y * M2FT;
document.getElementById('hudSpeed').textContent = speedKts;
document.getElementById('hudAlt').textContent = altFt;
document.getElementById('hudHdg').textContent = String(Math.round(heading)).padStart(3, '0');
document.getElementById('hudRpm').textContent = Math.round(aircraft.rpm);
document.getElementById('hudVs').textContent = Math.round(vsFt);
document.getElementById('hudStall').textContent = aircraft.isStalled ? '!!' : '—';
document.getElementById('hudStall').style.color = aircraft.isStalled ? '#ff4444' : '#fff';
// Throttle bar
document.getElementById('throttleBar').style.width = (aircraft.throttle * 100) + '%';
// AoA bar
const aoaPct = Math.min(100, (Math.abs(aircraft.aoa) / (22 * DEG2RAD)) * 100);
document.getElementById('aoaBar').style.width = aoaPct + '%';
document.getElementById('aoaBar').style.background = aircraft.isStalled
? 'linear-gradient(90deg, #ff4444, #ff0000)'
: 'linear-gradient(90deg, #4a9eff, #ff6b35)';
// G-force
document.getElementById('gForce').textContent = aircraft.gForce.toFixed(1);
document.getElementById('gForce').style.color = Math.abs(aircraft.gForce) > 7 ? '#ff4444' : '#fff';
// Attitude indicator
const horizonLine = document.getElementById('horizonLine');
const attitudeEl = document.getElementById('hudAttitude');
const pitchDeg = aircraft.rot.x * RAD2DEG;
const rollDeg = aircraft.rot.z * RAD2DEG;
horizonLine.style.transform = `rotate(${-rollDeg}deg) translateY(${pitchDeg * 1.5}px)`;
attitudeEl.style.transform = `rotate(${-rollDeg}deg)`;
// Flight path marker
const fpm = document.getElementById('fpm');
const flightPathAngle = Math.atan2(aircraft.vel.y, aircraft.vel.length()) * RAD2DEG;
const speedVector = new THREE.Vector3();
aircraft.vel.clone().applyAxisAngle(new THREE.Vector3(0, 1, 0), -aircraft.rot.y);
const lateralSpeed = speedVector.x;
fpm.style.left = (50 + lateralSpeed * 0.5) + '%';
fpm.style.top = (50 - flightPathAngle * 0.8) + '%';
// Mission messages
if (missionState) {
const msgEl = document.getElementById('hudMessage');
if (missionState.message) {
msgEl.textContent = missionState.message;
msgEl.style.opacity = '1';
} else {
msgEl.style.opacity = '0';
}
}
}
// ── let: selectedPlane ──
// ============================================================
// MENU INTERACTION
// ============================================================
let selectedPlane = null;
// ── let: selectedScenario ──
let selectedScenario = null;
// ── function: selectPlane ──
function selectPlane(type) {
selectedPlane = type;
document.querySelectorAll('.aircraft-card').forEach(c => c.classList.remove('selected'));
document.querySelector(`[data-plane="${type}"]`).classList.add('selected');
}
// ── function: selectScenario ──
function selectScenario(type) {
selectedScenario = type;
document.querySelectorAll('.scenario-card').forEach(c => c.classList.remove('selected'));
document.querySelector(`[data-scenario="${type}"]`).classList.add('selected');
}
// ── function: noise2D ──
// ============================================================
// NOISE / TERRAIN GENERATION
// ============================================================
function noise2D(x, z) {
const n = Math.sin(x * 12.9898 + z * 78.233) * 43758.5453;
return n - Math.floor(n);
}
// ── function: smoothNoise ──
function smoothNoise(x, z, scale) {
const sx = x / scale, sz = z / scale;
const ix = Math.floor(sx), iz = Math.floor(sz);
const fx = sx - ix, fz = sz - iz;
const a = noise2D(ix, iz);
const b = noise2D(ix + 1, iz);
const c = noise2D(ix, iz + 1);
const d = noise2D(ix + 1, iz + 1);
const ux = fx * fx * (3 - 2 * fx);
const uz = fz * fz * (3 - 2 * fz);
return a + (b - a) * ux + (c - a) * uz + (a - b - c + d) * ux * uz;
}
// ── function: fbmNoise ──
function fbmNoise(x, z, octaves) {
let val = 0, amp = 1, freq = 1, max = 0;
for (let i = 0; i < octaves; i++) {
val += smoothNoise(x * freq, z * freq, 1) * amp;
max += amp;
amp *= 0.5;
freq *= 2;
}
return val / max;
}
// ── function: getBaseTerrainHeight ──
function getBaseTerrainHeight(px, pz) {
let h = fbmNoise(px, pz, 6) * 2000 - 500;
h += Math.sin(px * 0.0003) * Math.cos(pz * 0.0004) * 1200;
h += Math.sin(px * 0.0008 + pz * 0.0006) * 600;
h += Math.sin(px * 0.002 + 1.3) * Math.cos(pz * 0.0015 + 0.7) * 200;
return h;
}
// ── function: isRunway ──
function isRunway(px, pz) {
const zCenter = 0;
const distToCenter = Math.abs(pz - zCenter);
return px >= RUNWAY_START && px <= RUNWAY_END && distToCenter <= RUNWAY_WIDTH / 2;
}
// ── function: getTerrainHeight ──
function getTerrainHeight(px, pz) {
let h = getBaseTerrainHeight(px, pz);
// Flatten near runway
const distToRunway = Math.abs(px);
if (distToRunway < 800) {
const blend = Math.max(0, 1 - distToRunway / 800);
const runwayH = getRunwayHeight(px, pz);
h = h * (1 - blend) + runwayH * blend;
}
// Also flatten along Z axis near runway
const distToZCenter = Math.abs(pz);
if (distToZCenter < 200 && px >= RUNWAY_START - 400 && px <= RUNWAY_END + 400) {
const zBlend = Math.max(0, 1 - distToZCenter / 200);
const runwayH = getRunwayHeight(px, pz);
h = h * (1 - zBlend) + runwayH * zBlend;
}
return h;
}
// ── function: getRunwayHeight ──
function getRunwayHeight(px, pz) {
const distToCenter = Math.abs(pz);
if (distToCenter > RUNWAY_WIDTH / 2) return getBaseTerrainHeight(px, pz);
// Crown shape
const crown = 1 - Math.pow(distToCenter / (RUNWAY_WIDTH / 2), 2) * 0.02;
// Slight slope at ends
let slope = 0;
if (px < RUNWAY_START + 200) {
slope = (px - (RUNWAY_START + 200)) / 200 * 0.02;
} else if (px > RUNWAY_END - 200) {
slope = (px - (RUNWAY_END - 200)) / 200 * 0.02;
}
return getBaseTerrainHeight(px, pz) * crown + slope * 50;
}
// ── function: getTerrainNormal ──
function getTerrainNormal(px, pz) {
const delta = 1;
const hL = getBaseTerrainHeight(px - delta, pz);
const hR = getBaseTerrainHeight(px + delta, pz);
const hD = getBaseTerrainHeight(px, pz - delta);
const hU = getBaseTerrainHeight(px, pz + delta);
return new THREE.Vector3(hL - hR, 2 * delta, hD - hU).normalize();
}
// ── function: startGame ──
function startGame() {
if (!selectedPlane || !selectedScenario) return;
document.getElementById('menuOverlay').classList.add('hidden');
document.getElementById('hud').classList.remove('hidden');
if (selectedScenario === 'dogfight') {
document.getElementById('dogfightUI').classList.remove('hidden');
} else {
document.getElementById('dogfightUI').classList.add('hidden');
}
if (selectedScenario === 'takeoff') {
document.getElementById('missionUI').classList.remove('hidden');
document.getElementById('missionObj').textContent = 'Takeoff: Apply throttle, accelerate, rotate at 80kt. Land: Approach at 120kt, flare at 50ft.';
} else {
document.getElementById('missionUI').classList.add('hidden');
}
initAudio();
startScenario();
}
// ── function: setupShooting ──
// ============================================================
// SHOOTING HANDLER
// ============================================================
function setupShooting() {
const canvas = document.getElementById('gameCanvas');
canvas.addEventListener('mousedown', (e) => {
if (gameState !== 'playing' || selectedScenario !== 'dogfight') return;
if (e.button === 0) {
// Left click - shoot
const playerForward = new THREE.Vector3(1, 0, 0).applyEuler(playerAircraft.rot);
const playerPos = playerAircraft.pos.clone();
for (let i = dogfightEnemies.length - 1; i >= 0; i--) {
const enemy = dogfightEnemies[i];
if (!enemy.alive) continue;
const toEnemy = enemy.pos.clone().sub(playerPos);
const dist = toEnemy.length();
toEnemy.normalize();
const dot = playerForward.dot(toEnemy);
const angle = Math.acos(Math.min(1, Math.max(-1, dot)));
if (angle < 0.087 && dist < 2000) {
enemy.alive = false;
scene.remove(enemyMeshes[i]);
enemyMeshes.splice(i, 1);
dogfightEnemies.splice(i, 1);
killCount++;
document.getElementById('killCount').textContent = killCount;
missionState.message = 'Enemy destroyed! ' + killCount + ' kills';
if (dogfightEnemies.length === 0) {
missionState.message = 'All enemies destroyed! Wave complete!';
setTimeout(() => {
for (let j = 0; j < 5; j++) {
const enemy = createEnemyAI();
const angle2 = (j / 5) * Math.PI * 2;
const dist2 = 1500 + Math.random() * 1500;
enemy.pos.set(
playerAircraft.pos.x + Math.cos(angle2) * dist2,
200 + Math.random() * 400,
playerAircraft.pos.z + Math.sin(angle2) * dist2
);
enemy.vel.set(-Math.cos(angle2) * 25, 0, -Math.sin(angle2) * 25);
enemy.targetPos.set(
playerAircraft.pos.x + Math.cos(angle2 + Math.PI) * 600,
300 + Math.random() * 300,
playerAircraft.pos.z + Math.sin(angle2 + Math.PI) * 600
);
const mesh = createEnemyMesh();
mesh.position.copy(enemy.pos);
scene.add(mesh);
enemyMeshes.push(mesh);
dogfightEnemies.push(enemy);
}
missionState.message = 'New wave incoming!';
}, 3000);
}
break;
}
}
}
});
}
// ── function: updatePlayerControls ──
// ============================================================
// PLAYER CONTROLS
// ============================================================
function updatePlayerControls() {
const aircraft = playerAircraft;
const def = aircraft.def;
// Mouse controls (pitch/roll)
const mouseSensitivity = 0.002;
aircraft.pitchInput = -mouse.dy * mouseSensitivity;
aircraft.rollInput = -mouse.dx * mouseSensitivity;
// Keyboard overrides
if (keys['w']) aircraft.pitchInput = Math.max(aircraft.pitchInput, -1);
if (keys['s']) aircraft.pitchInput = Math.min(aircraft.pitchInput, 1);
if (keys['a']) aircraft.rollInput = Math.max(aircraft.rollInput, -1);
if (keys['d']) aircraft.rollInput = Math.min(aircraft.rollInput, 1);
if (keys['q']) aircraft.yawInput = Math.max(aircraft.yawInput, -1);
if (keys['e']) aircraft.yawInput = Math.min(aircraft.yawInput, 1);
// Throttle
if (def.maxThrust > 0) {
if (keys['shift']) aircraft.targetThrottle = Math.min(1, aircraft.targetThrottle + 0.02);
if (keys['control']) aircraft.targetThrottle = Math.max(0, aircraft.targetThrottle - 0.02);
}
// Brake
aircraft.brake = keys['f'] || false;
// Speed brake (dogfight)
if (keys['g']) {
aircraft.speedBrake = Math.min(1, aircraft.speedBrake + 0.05);
} else {
aircraft.speedBrake = Math.max(0, aircraft.speedBrake - 0.02);
}
// Gear toggle (V key)
if (keys['v']) {
keys['v'] = false; // prevent repeat
aircraft.gearDown = !aircraft.gearDown;
}
if (aircraft.gearDown) {
aircraft.gearExtension = Math.min(1, aircraft.gearExtension + 0.05);
} else {
aircraft.gearExtension = Math.max(0, aircraft.gearExtension - 0.05);
}
// Reset mouse deltas
mouse.dx = 0;
mouse.dy = 0;
// Auto throttle for glider
if (def.maxThrust === 0) {
aircraft.targetThrottle = 0;
}
}
// ── function: updateEngineGlow ──
// ============================================================
// ENGINE GLOW UPDATE
// ============================================================
function updateEngineGlow() {
if (!playerMesh) return;
playerMesh.traverse((child) => {
if (child.name === 'engineGlow' && child.isMesh) {
const intensity = playerAircraft.throttle;
child.material.opacity = 0.2 + intensity * 0.6;
child.scale.setScalar(0.5 + intensity * 1.5);
child.material.color.setHSL(0.08 - intensity * 0.05, 1, 0.4 + intensity * 0.3);
}
});
}
// ── function: updateAircraftPhysics ──
function updateAircraftPhysics(aircraft, dt) {
const def = aircraft.def;
aircraft.lastTime = performance.now();
// Throttle response (smooth)
aircraft.throttle += (aircraft.targetThrottle - aircraft.throttle) * def.throttleResponse * dt * 60;
aircraft.throttle = Math.max(0, Math.min(1, aircraft.throttle));
aircraft.rpm = aircraft.throttle * 100;
// Engine temperature
if (aircraft.throttle > 0.1) {
aircraft.engineTemp += aircraft.throttle * 2 * dt - 0.5 * dt;
aircraft.engineTemp = Math.min(100, Math.max(20, aircraft.engineTemp));
} else {
aircraft.engineTemp -= 3 * dt;
aircraft.engineTemp = Math.max(20, aircraft.engineTemp);
}
// Air density based on altitude
const altM = Math.max(0, aircraft.pos.y);
const airDensity = SEA_LEVEL_DENSITY * Math.exp(-altM / 8500);
// Speed
const speed = aircraft.vel.length();
if (speed < 0.001) {
aircraft.dynamicPressure = 0;
aircraft.aoa = 0;
aircraft.sideslip = 0;
aircraft.lift = 0;
aircraft.drag = 0;
aircraft.gForce = 1;
aircraft.vel.y -= GRAVITY * dt;
aircraft.pos.y += aircraft.vel.y * dt;
return;
}
const velDir = aircraft.vel.clone().normalize();
aircraft.dynamicPressure = 0.5 * airDensity * speed * speed;
// Angle of attack
const forward = new THREE.Vector3(1, 0, 0);
forward.applyEuler(aircraft.rot);
aircraft.aoa = forward.angleTo(velDir);
const cross = new THREE.Vector3().crossVectors(forward, velDir);
if (cross.y < 0) aircraft.aoa = -aircraft.aoa;
// Sideslip
const lateralVel = new THREE.Vector3().copy(aircraft.vel).applyEuler(new THREE.Euler(0, -aircraft.rot.y, 0));
aircraft.sideslip = Math.atan2(lateralVel.x, aircraft.vel.length());
// Stall detection
const stallThreshold = def.stallAngle;
aircraft.stallProgress = Math.max(0, (Math.abs(aircraft.aoa) - stallThreshold) / (def.maxAoA - stallThreshold));
aircraft.isStalled = aircraft.stallProgress > 0;
// Lift coefficient
let cl = def.liftCurveSlope * aircraft.aoa;
if (aircraft.isStalled) {
cl *= (1 - aircraft.stallProgress * 0.8);
cl += Math.sin(aircraft.aoa * 3) * aircraft.stallProgress * 0.3;
}
aircraft.lift = aircraft.dynamicPressure * def.wingArea * cl;
// Drag
let cd = def.dragCoeff;
if (def.maxThrust > 0) cd += def.inducedDragFactor * cl * cl;
if (aircraft.isStalled) cd += aircraft.stallProgress * 2;
cd += aircraft.speedBrake * 0.5;
aircraft.drag = aircraft.dynamicPressure * def.wingArea * cd;
// Thrust
aircraft.thrust = def.maxThrust > 0 ? def.maxThrust * aircraft.throttle : 0;
// Ground friction
if (aircraft.gearExtension > 0.5 && speed > 5) {
const groundH = getTerrainHeight(aircraft.pos.x, aircraft.pos.z);
if (aircraft.pos.y <= groundH + 2) {
const frictionForce = aircraft.def.mass * GRAVITY * 0.4 * aircraft.brake * 2;
aircraft.vel.multiplyScalar(1 - Math.min(1, frictionForce / (aircraft.def.mass * speed) * dt));
}
}
// Forces
const thrustForce = new THREE.Vector3(aircraft.thrust, 0, 0);
const dragForce = velDir.clone().negate().multiplyScalar(aircraft.drag);
const weightForce = new THREE.Vector3(0, -aircraft.def.mass * GRAVITY, 0);
// Lift direction
const liftDir = new THREE.Vector3();
const up = new THREE.Vector3(0, 1, 0);
const liftPlane = new THREE.Vector3().crossVectors(velDir, up);
if (liftPlane.length() > 0.001) {
liftDir.crossVectors(velDir, liftPlane).normalize();
liftDir.y = Math.abs(liftDir.y) > 0.01 ? Math.sign(liftDir.y) : 1;
} else {
liftDir.copy(up);
}
const liftForce = liftDir.clone().multiplyScalar(aircraft.lift);
const totalForce = new THREE.Vector3();
totalForce.add(thrustForce);
totalForce.add(liftForce);
totalForce.add(dragForce);
totalForce.add(weightForce);
// Angular acceleration
const controlEffectiveness = Math.min(1, aircraft.dynamicPressure / 5000);
const pitchAcc = aircraft.pitchInput * controlEffectiveness * def.pitchRate * 60 * dt;
const rollAcc = aircraft.rollInput * controlEffectiveness * def.rollRate * 60 * dt;
const yawAcc = aircraft.yawInput * controlEffectiveness * def.yawRate * 60 * dt;
aircraft.angularVel.x += pitchAcc;
aircraft.angularVel.y += yawAcc;
aircraft.angularVel.z += rollAcc;
const dampingFactor = Math.exp(-dt * 2);
aircraft.angularVel.x *= dampingFactor;
aircraft.angularVel.y *= dampingFactor;
aircraft.angularVel.z *= dampingFactor;
// G-force
const gMag = totalForce.length() / aircraft.def.mass;
aircraft.gForce = gMag / GRAVITY;
if (Math.abs(aircraft.gForce) > def.maxG) {
aircraft.gForce = Math.sign(aircraft.gForce) * def.maxG;
}
// Stall effects
if (aircraft.isStalled) {
const sr = 1 - aircraft.stallProgress * 0.7;
aircraft.angularVel.x *= sr;
aircraft.angularVel.z *= sr;
aircraft.angularVel.y *= (1 - aircraft.stallProgress * 0.3);
}
// Update rotation
aircraft.rot.x += aircraft.angularVel.x * dt;
aircraft.rot.y += aircraft.angularVel.y * dt;
aircraft.rot.z += aircraft.angularVel.z * dt;
aircraft.rot.x = Math.max(-Math.PI / 2, Math.min(Math.PI / 2, aircraft.rot.x));
// Translation
const accel = totalForce.clone().divideScalar(aircraft.def.mass);
aircraft.vel.x += accel.x * dt;
aircraft.vel.y += accel.y * dt;
aircraft.vel.z += accel.z * dt;
aircraft.pos.x += aircraft.vel.x * dt;
aircraft.pos.y += aircraft.vel.y * dt;
aircraft.pos.z += aircraft.vel.z * dt;
// Ground collision
const groundH = getTerrainHeight(aircraft.pos.x, aircraft.pos.z);
if (aircraft.pos.y < groundH + 1) {
aircraft.pos.y = groundH + 1;
if (aircraft.vel.y < -5) aircraft.crashed = true;
aircraft.vel.y = Math.max(0, aircraft.vel.y);
const normal = getTerrainNormal(aircraft.pos.x, aircraft.pos.z);
const proj = new THREE.Vector3().copy(forward).projectOnPlane(normal);
if (proj.length() > 0.01) {
const m = new THREE.Matrix4().lookAt(new THREE.Vector3(), proj.normalize(), normal);
aircraft.rot.setFromRotationMatrix(m);
}
}
// Ground effect
if (aircraft.pos.y < 100 && aircraft.pos.y > 0) {
aircraft.lift *= (1 + Math.max(0, 1 - aircraft.pos.y / 100) * 0.15);
}
}
// ── function: createTerrainMesh ──
// ============================================================
// TERRAIN MESH CREATION (Improved)
// ============================================================
function createTerrainMesh() {
const size = TERRAIN_SIZE;
const segments = TERRAIN_RES;
const geo = new THREE.PlaneGeometry(size, size, segments, segments);
geo.rotateX(-Math.PI / 2);
const positions = geo.attributes.position;
const colors = new Float32Array(positions.count * 3);
for (let i = 0; i < positions.count; i++) {
const x = positions.getX(i);
const z = positions.getZ(i);
const h = getTerrainHeight(x, z);
positions.setY(i, h);
// Color based on height with smooth transitions
const normH = (h + 500) / 2500;
let r, g, b;
if (isRunway(x, z)) {
r = 0.22; g = 0.22; b = 0.22;
} else if (h < -150) {
r = 0.05 + normH * 0.1; g = 0.15 + normH * 0.2; b = 0.4 + normH * 0.2;
} else if (normH < 0.1) {
r = 0.15 + normH; g = 0.45 + normH * 0.5; b = 0.1;
} else if (normH < 0.25) {
r = 0.2 + normH * 0.5; g = 0.45 + normH * 0.3; b = 0.15;
} else if (normH < 0.45) {
r = 0.35 + normH * 0.3; g = 0.35 + normH * 0.2; b = 0.2;
} else if (normH < 0.65) {
r = 0.5 + normH * 0.2; g = 0.48 + normH * 0.1; b = 0.45;
} else {
r = 0.85 + normH * 0.1; g = 0.85 + normH * 0.1; b = 0.88 + normH * 0.05;
}
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,
metalness: 0.05,
roughness: 0.85,
flatShading: false,
});
const mesh = new THREE.Mesh(geo, mat);
mesh.receiveShadow = true;
return mesh;
}
// ── function: createRunwayMesh ──
// ============================================================
// RUNWAY MESH (Improved with lights)
// ============================================================
function createRunwayMesh() {
const group = new THREE.Group();
// Main runway surface
const rwGeo = new THREE.PlaneGeometry(RUNWAY_LENGTH, RUNWAY_WIDTH);
rwGeo.rotateX(-Math.PI / 2);
const rwMat = new THREE.MeshStandardMaterial({ color: 0x2a2a2a, roughness: 0.9 });
const rw = new THREE.Mesh(rwGeo, rwMat);
rw.position.set(0, 0.15, 0);
group.add(rw);
// Runway edge lights
const lightMat = new THREE.MeshBasicMaterial({ color: 0x44ff44 });
const lightMatWhite = new THREE.MeshBasicMaterial({ color: 0xffffff });
const lightMatAmber = new THREE.MeshBasicMaterial({ color: 0xffaa00 });
for (let x = RUNWAY_START; x <= RUNWAY_END; x += 50) {
for (const side of [-1, 1]) {
const z = side * RUNWAY_WIDTH / 2;
const mat = (x < RUNWAY_START + 200 || x > RUNWAY_END - 200) ? lightMatAmber : lightMat;
const geo = new THREE.SphereGeometry(0.3, 6, 6);
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(x, 0.3, z);
group.add(mesh);
}
}
// Threshold lights (bright white)
for (const endX of [RUNWAY_START + 10, RUNWAY_END - 10]) {
for (let z = -RUNWAY_WIDTH / 2 + 5; z <= RUNWAY_WIDTH / 2 - 5; z += 10) {
const geo = new THREE.SphereGeometry(0.5, 6, 6);
const mesh = new THREE.Mesh(geo, lightMatWhite);
mesh.position.set(endX, 0.4, z);
group.add(mesh);
}
}
// Center line markings
for (let x = RUNWAY_START + 50; x < RUNWAY_END - 50; x += 40) {
const dashGeo = new THREE.PlaneGeometry(20, 1.5);
dashGeo.rotateX(-Math.PI / 2);
const dashMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.8 });
const dash = new THREE.Mesh(dashGeo, dashMat);
dash.position.set(x, 0.18, 0);
group.add(dash);
}
// Touchdown zone markings
for (const endX of [RUNWAY_START + 150, RUNWAY_END - 150]) {
for (let z = -15; z <= 15; z += 10) {
const markGeo = new THREE.PlaneGeometry(4, 3);
markGeo.rotateX(-Math.PI / 2);
const markMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.7 });
const mark = new THREE.Mesh(markGeo, markMat);
mark.position.set(endX, 0.19, z);
group.add(mark);
}
}
// Aimpoint marking
for (const endX of [RUNWAY_START + 300, RUNWAY_END - 300]) {
for (const z of [-5, 5]) {
const markGeo = new THREE.PlaneGeometry(6, 2);
markGeo.rotateX(-Math.PI / 2);
const markMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.7 });
const mark = new THREE.Mesh(markGeo, markMat);
mark.position.set(endX, 0.19, z);
group.add(mark);
}
}
// Edge lines
for (const side of [-1, 1]) {
const edgeGeo = new THREE.PlaneGeometry(RUNWAY_LENGTH, 0.8);
edgeGeo.rotateX(-Math.PI / 2);
const edgeMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.6 });
const edge = new THREE.Mesh(edgeGeo, edgeMat);
edge.position.set(0, 0.17, side * RUNWAY_WIDTH / 2);
group.add(edge);
}
// Runway approach lights (bright)
for (const endX of [RUNWAY_START - 100, RUNWAY_END + 100]) {
for (let z = -30; z <= 30; z += 15) {
const geo = new THREE.SphereGeometry(0.6, 6, 6);
const mesh = new THREE.Mesh(geo, new THREE.MeshBasicMaterial({ color: 0xffffff }));
mesh.position.set(endX, 0.5, z);
group.add(mesh);
}
}
return group;
}
// ── main ──
// ============================================================
// MAIN: INITIALIZATION AND GAME LOOP
// ============================================================
let scene, camera, renderer;
let terrainMesh, runwayMesh;
let playerAircraft, playerMesh;
let enemyMeshes = [];
let missionState = null;
let gameState = 'menu';
let lastFrameTime = 0;
let keys = {};
let mouse = { x: 0, y: 0, dx: 0, dy: 0 };
let isPointerLocked = false;
let killCount = 0;
let turnCount = 0;
let dogfightEnemies = [];
let cloudMeshes = [];
let stars = null;
let sunLight, ambientLight;
let skyMesh;
function init() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x87CEEB, 0.00008);
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100000);
const canvas = document.getElementById('gameCanvas');
renderer = new THREE.WebGLRenderer({ canvas, antialias: true });
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.1;
ambientLight = new THREE.AmbientLight(0x404060, 0.6);
scene.add(ambientLight);
sunLight = new THREE.DirectionalLight(0xfff4e0, 1.5);
sunLight.position.set(5000, 8000, 3000);
sunLight.castShadow = true;
sunLight.shadow.mapSize.width = 2048;
sunLight.shadow.mapSize.height = 2048;
sunLight.shadow.camera.near = 100;
sunLight.shadow.camera.far = 30000;
sunLight.shadow.camera.left = -5000;
sunLight.shadow.camera.right = 5000;
sunLight.shadow.camera.top = 5000;
sunLight.shadow.camera.bottom = -5000;
scene.add(sunLight);
scene.add(new THREE.HemisphereLight(0x87CEEB, 0x3a5a2a, 0.4));
createSky();
createClouds();
createStars();
terrainMesh = createTerrainMesh();
scene.add(terrainMesh);
runwayMesh = createRunwayMesh();
scene.add(runwayMesh);
// Sun visual
const sunGeo = new THREE.SphereGeometry(200, 16, 16);
const sunMat = new THREE.MeshBasicMaterial({ color: 0xfff8e0 });
const sunMesh = new THREE.Mesh(sunGeo, sunMat);
sunMesh.position.copy(sunLight.position);
scene.add(sunMesh);
setupInput();
setupShooting();
window.addEventListener('resize', onResize);
lastFrameTime = performance.now();
requestAnimationFrame(gameLoop);
}
function createSky() {
const skyGeo = new THREE.SphereGeometry(50000, 32, 32);
const skyMat = new THREE.ShaderMaterial({
uniforms: {
topColor: { value: new THREE.Color(0x0044aa) },
bottomColor: { value: new THREE.Color(0x87CEEB) },
offset: { value: 20 },
exponent: { value: 0.4 },
},
vertexShader: `
varying vec3 vWorldPosition;
void main() {
vec4 worldPosition = modelMatrix * vec4(position, 1.0);
vWorldPosition = worldPosition.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);
}
function createClouds() {
for (let i = 0; i < 80; i++) {
const cloudGroup = new THREE.Group();
const numPuffs = 3 + Math.floor(Math.random() * 5);
for (let j = 0; j < numPuffs; j++) {
const puffGeo = new THREE.SphereGeometry(30 + Math.random() * 50, 8, 6);
const puffMat = new THREE.MeshStandardMaterial({
color: 0xffffff, transparent: true, opacity: 0.7,
roughness: 1, metalness: 0,
});
const puff = new THREE.Mesh(puffGeo, puffMat);
puff.position.set(
(Math.random() - 0.5) * 80, (Math.random() - 0.5) * 15,
(Math.random() - 0.5) * 60
);
puff.scale.y = 0.4;
cloudGroup.add(puff);
}
cloudGroup.position.set(
(Math.random() - 0.5) * 40000,
800 + Math.random() * 2000,
(Math.random() - 0.5) * 40000
);
scene.add(cloudGroup);
cloudMeshes.push(cloudGroup);
}
}
function createStars() {
const starGeo = new THREE.BufferGeometry();
const starPositions = [];
for (let i = 0; i < 3000; i++) {
const theta = Math.random() * Math.PI * 2;
const phi = Math.random() * Math.PI;
const r = 45000;
starPositions.push(r * Math.sin(phi) * Math.cos(theta), r * Math.cos(phi), r * Math.sin(phi) * Math.sin(theta));
}
starGeo.setAttribute('position', new THREE.Float32BufferAttribute(starPositions, 3));
stars = new THREE.Points(starGeo, new THREE.PointsMaterial({ color: 0xffffff, size: 2, transparent: true, opacity: 0.8 }));
scene.add(stars);
}
function setupInput() {
const canvas = document.getElementById('gameCanvas');
canvas.addEventListener('click', () => {
if (gameState === 'playing') canvas.requestPointerLock();
});
document.addEventListener('pointerlockchange', () => {
isPointerLocked = document.pointerLockElement === canvas;
});
document.addEventListener('keydown', (e) => {
keys[e.key.toLowerCase()] = true;
if (e.key.toLowerCase() === 'r' && gameState === 'crashed') restartGame();
});
document.addEventListener('keyup', (e) => { keys[e.key.toLowerCase()] = false; });
document.addEventListener('mousemove', (e) => {
if (isPointerLocked) { mouse.dx += e.movementX; mouse.dy += e.movementY; }
});
}
function onResize() {
camera.aspect = window.innerWidth / window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth, window.innerHeight);
}
function startScenario() {
const aircraft = createAircraft(selectedPlane);
playerAircraft = aircraft;
playerMesh = createAircraftMesh(selectedPlane);
scene.add(playerMesh);
if (selectedScenario === 'takeoff') {
aircraft.pos.set(RUNWAY_START, getRunwayHeight(RUNWAY_START, 0) + 1, 0);
aircraft.vel.set(0, 0, 0);
aircraft.targetThrottle = 0;
missionState = {
phase: 'takeoff',
message: 'Press Shift to apply throttle. Rotate at 80kt!',
takeoffSpeed: 80 * KT2MS,
targetAlt: 500,
approachAlt: 300,
approachSpeed: 120 * KT2MS,
};
} else if (selectedScenario === 'dogfight') {
aircraft.pos.set(0, 200, 0);
aircraft.vel.set(20, 0, 0);
missionState = {
phase: 'dogfight',
message: 'Lock on red enemies and shoot! (Left Click)',
kills: 0,
};
killCount = 0;
turnCount = 0;
dogfightEnemies = [];
for (let i = 0; i < 5; i++) {
const enemy = createEnemyAI();
const angle = (i / 5) * Math.PI * 2;
const dist = 1000 + Math.random() * 2000;
enemy.pos.set(Math.cos(angle) * dist, 200 + Math.random() * 400, Math.sin(angle) * dist);
enemy.vel.set(-Math.cos(angle) * 20, 0, -Math.sin(angle) * 20);
enemy.targetPos.set(
playerAircraft.pos.x + Math.cos(angle + Math.PI) * 800,
300 + Math.random() * 300,
playerAircraft.pos.z + Math.sin(angle + Math.PI) * 800
);
const mesh = createEnemyMesh();
mesh.position.copy(enemy.pos);
scene.add(mesh);
enemyMeshes.push(mesh);
dogfightEnemies.push(enemy);
}
}
gameState = 'playing';
document.getElementById('hud').classList.remove('hidden');
}
function restartGame() {
if (!scene || !playerAircraft) return;
if (playerMesh) { scene.remove(playerMesh); playerMesh = null; }
enemyMeshes.forEach(m => scene.remove(m));
enemyMeshes = [];
dogfightEnemies = [];
resetAircraft(playerAircraft);
playerAircraft.pos.set(RUNWAY_START, getRunwayHeight(RUNWAY_START, 0) + 1, 0);
gameState = 'playing';
missionState.phase = 'takeoff';
missionState.message = 'Restarted. Apply throttle!';
}
function updateDogfight() {
for (let i = dogfightEnemies.length - 1; i >= 0; i--) {
const enemy = dogfightEnemies[i];
if (!enemy.alive) continue;
const action = updateEnemyAI(enemy, playerAircraft, 1/60);
if (action === 'shoot') {
const dist = enemy.pos.distanceTo(playerAircraft.pos);
if (dist < 600 && Math.random() < 0.1) {
missionState.message = 'WARNING: Incoming fire!';
}
}
if (enemyMeshes[i]) {
enemyMeshes[i].position.copy(enemy.pos);
enemyMeshes[i].rotation.copy(enemy.rot);
}
}
const absRoll = Math.abs(playerAircraft.rot.z);
if (absRoll > 0.5) {
turnCount += 0.01;
document.getElementById('turnCount').textContent = Math.round(turnCount);
}
}
function updateTakeoffLanding() {
const ms = missionState;
const speed = playerAircraft.vel.length();
const speedKts = speed * MS2KT;
const alt = playerAircraft.pos.y;
if (ms.phase === 'takeoff') {
if (speedKts > ms.takeoffSpeed && alt > 2) {
ms.phase = 'climb';
ms.message = 'Climb to 500ft, then circle for approach.';
} else if (speedKts < ms.takeoffSpeed) {
ms.message = 'Speed: ' + Math.round(speedKts) + 'kt. Keep accelerating!';
}
} else if (ms.phase === 'climb') {
if (alt > ms.targetAlt) {
ms.phase = 'circle';
ms.message = 'Reach altitude. Turn to head back to runway.';
}
ms.message = 'Alt: ' + Math.round(alt * M2FT) + 'ft. Climb to 500ft.';
} else if (ms.phase === 'circle') {
const distToRunway = Math.sqrt(playerAircraft.pos.x ** 2 + playerAircraft.pos.z ** 2);
if (distToRunway < 2000 && alt > ms.approachAlt) {
ms.phase = 'approach';
ms.message = 'Begin descent. Approach at 120kt, 500ft AGL.';
}
ms.message = 'Head to runway. Speed: ' + Math.round(speedKts) + 'kt.';
} else if (ms.phase === 'approach') {
if (alt < ms.approachAlt) {
ms.phase = 'final';
ms.message = 'Final approach! Flare at 50ft!';
}
ms.message = 'Descend to 120kt approach speed. Alt: ' + Math.round(alt * M2FT) + 'ft.';
} else if (ms.phase === 'final') {
if (alt < 50) {
ms.phase = 'flare';
ms.message = 'FLARE! Pull up!';
}
ms.message = 'Alt: ' + Math.round(alt * M2FT) + 'ft. Flare at 50ft!';
} else if (ms.phase === 'flare') {
if (alt < 10 && speedKts < 60) {
ms.phase = 'landed';
ms.message = 'Touchdown! Press R to restart.';
}
ms.message = 'Flare! Pull up gently!';
} else if (ms.phase === 'landed') {
ms.message = 'Landed successfully! Press R to fly again.';
}
if (ms.phase === 'approach' && playerAircraft.def.maxThrust > 0) {
playerAircraft.targetThrottle = 0.3;
}
}
function updateCamera() {
const aircraft = playerAircraft;
const speed = aircraft.vel.length();
const camDist = 15 + speed * 0.1;
const camHeight = 5 + speed * 0.05;
const camOffset = new THREE.Vector3(-camDist, camHeight, 0);
camOffset.applyEuler(aircraft.rot);
const targetCamPos = aircraft.pos.clone().add(camOffset);
camera.position.lerp(targetCamPos, 0.08);
const lookAhead = aircraft.vel.clone().multiplyScalar(0.3);
const targetLook = aircraft.pos.clone().add(lookAhead);
camera.lookAt(targetLook);
}
function gameLoop(timestamp) {
requestAnimationFrame(gameLoop);
const dt = Math.min((timestamp - lastFrameTime) / 1000, 0.05);
lastFrameTime = timestamp;
if (gameState !== 'playing') {
renderer.render(scene, camera);
return;
}
updatePlayerControls();
updateAircraftPhysics(playerAircraft, dt);
playerMesh.position.copy(playerAircraft.pos);
playerMesh.rotation.copy(playerAircraft.rot);
updateEngineGlow();
updateCamera();
updateAudio(playerAircraft, playerAircraft.vel.length());
if (selectedScenario === 'takeoff') {
updateTakeoffLanding();
} else if (selectedScenario === 'dogfight') {
updateDogfight();
}
updateHUD(playerAircraft, missionState);
if (skyMesh) {
const alt = playerAircraft.pos.y;
const skyDarkness = Math.max(0, Math.min(1, alt / 15000));
skyMesh.material.uniforms.topColor.value.setHSL(0.6, 0.8, 0.6 - skyDarkness * 0.5);
skyMesh.material.uniforms.bottomColor.value.setHSL(0.55, 0.6, 0.7 - skyDarkness * 0.4);
}
if (stars) {
const alt = playerAircraft.pos.y;
stars.material.opacity = Math.max(0, Math.min(1, (alt - 5000) / 10000));
}
scene.fog.color.setHSL(0.55, 0.3, 0.7 - Math.max(0, Math.min(0.5, playerAircraft.pos.y / 20000)));
if (playerAircraft.crashed) {
gameState = 'crashed';
missionState.message = 'CRASHED! Press R to restart.';
updateHUD(playerAircraft, missionState);
}
renderer.render(scene, camera);
}
// Initialize on load
init();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.6-35b-a3b","provider":"lmstudio","persona":"composer","sessionId":"6748abbd-d84b-4589-8e83-9fe2b5cf5288","tokensIn":2271826,"tokensOut":52460,"tokensTotal":2324286,"turns":49,"toolCalls":58,"failedToolCalls":5,"timestamp":"2026-07-20T22:27:14.939Z"} -->