Flight Simulator
longcat 2.0 · composer

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 57,949 bytes · SHA-256 b15fdbda534a
<!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://cdn.jsdelivr.net/npm/three@0.160.0/build/three.min.js"></script>
<style>
/* ── css: layout ── */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
overflow: hidden;
background: #000;
font-family: 'Courier New', monospace;
color: #00ff88;
}
#app {
width: 100vw;
height: 100vh;
position: relative;
}
#game-canvas {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: block;
}
/* ── css: menu ── */
#menu {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(135deg, #0a0a1a 0%, #1a1a3a 50%, #0a1a2a 100%);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
#menu-content {
max-width: 800px;
width: 90%;
text-align: center;
}
#menu h1 {
font-size: 4em;
color: #00ffcc;
text-shadow: 0 0 20px #00ffcc, 0 0 40px #00ff88;
letter-spacing: 8px;
margin-bottom: 5px;
}
#menu .subtitle {
font-size: 1.2em;
color: #66aaff;
letter-spacing: 4px;
margin-bottom: 40px;
}
#aircraft-select {
margin-bottom: 30px;
}
#aircraft-select h2, #scenario-select h2 {
font-size: 1.1em;
color: #88ccff;
margin-bottom: 15px;
text-transform: uppercase;
letter-spacing: 2px;
}
#aircraft-list {
display: flex;
gap: 15px;
justify-content: center;
flex-wrap: wrap;
}
.aircraft-card {
background: rgba(0, 50, 100, 0.4);
border: 2px solid #336;
border-radius: 10px;
padding: 15px;
width: 200px;
cursor: pointer;
transition: all 0.3s;
}
.aircraft-card:hover {
border-color: #00ffcc;
background: rgba(0, 100, 150, 0.4);
transform: translateY(-3px);
}
.aircraft-card.selected {
border-color: #00ffcc;
box-shadow: 0 0 15px rgba(0, 255, 200, 0.3);
}
.aircraft-card h3 {
color: #00ffcc;
font-size: 1em;
margin-bottom: 5px;
}
.aircraft-card p {
color: #88aacc;
font-size: 0.75em;
margin-bottom: 8px;
}
.aircraft-card .stats {
font-size: 0.7em;
color: #6688aa;
text-align: left;
}
.aircraft-card .stats div {
margin: 2px 0;
}
#scenario-select {
margin-bottom: 30px;
}
.scenario-btn {
display: block;
width: 100%;
max-width: 500px;
margin: 10px auto;
padding: 15px 20px;
background: rgba(0, 80, 120, 0.3);
border: 2px solid #336;
border-radius: 8px;
color: #aaddff;
cursor: pointer;
transition: all 0.3s;
text-align: left;
}
.scenario-btn:hover {
border-color: #00ffcc;
background: rgba(0, 100, 150, 0.4);
}
.scenario-title {
display: block;
font-size: 1.1em;
color: #00ffcc;
margin-bottom: 5px;
}
.scenario-desc {
display: block;
font-size: 0.8em;
color: #6688aa;
}
#controls-info {
margin-top: 20px;
padding: 15px;
background: rgba(0, 20, 40, 0.5);
border-radius: 8px;
}
#controls-info h3 {
color: #88ccff;
font-size: 0.9em;
margin-bottom: 10px;
}
.control-grid {
display: grid;
grid-template-columns: auto auto;
gap: 5px 15px;
font-size: 0.75em;
color: #6688aa;
max-width: 400px;
margin: 0 auto;
}
.control-grid span:nth-child(odd) {
color: #00ff88;
font-weight: bold;
}
/* ── css: hud ── */
#hud {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 100;
font-family: 'Courier New', monospace;
font-size: 14px;
}
#hud-crosshair {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 60px;
height: 60px;
}
.crosshair-h {
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background: rgba(0, 255, 136, 0.8);
transform: translateY(-50%);
}
.crosshair-v {
position: absolute;
left: 50%;
top: 0;
bottom: 0;
width: 1px;
background: rgba(0, 255, 136, 0.8);
transform: translateX(-50%);
}
.crosshair-dot {
position: absolute;
top: 50%;
left: 50%;
width: 4px;
height: 4px;
background: rgba(0, 255, 136, 0.9);
border-radius: 50%;
transform: translate(-50%, -50%);
}
#hud-top {
position: absolute;
top: 10px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 20px;
}
#hud-aircraft {
color: #00ffcc;
font-size: 1.1em;
text-shadow: 0 0 5px rgba(0, 255, 200, 0.5);
}
#hud-fps {
color: #6688aa;
font-size: 0.85em;
}
#hud-left {
position: absolute;
left: 20px;
top: 50%;
transform: translateY(-50%);
}
#hud-right {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
}
#hud-bottom {
position: absolute;
bottom: 20px;
left: 50%;
transform: translateX(-50%);
width: 300px;
}
.hud-row {
display: flex;
justify-content: space-between;
align-items: center;
margin: 5px 0;
padding: 3px 10px;
background: rgba(0, 20, 40, 0.6);
border-radius: 3px;
}
.hud-label {
color: #6688aa;
font-size: 0.85em;
}
.hud-row span:last-child {
color: #00ff88;
font-weight: bold;
text-shadow: 0 0 3px rgba(0, 255, 136, 0.5);
}
#hud-throttle-container {
flex: 1;
height: 8px;
background: rgba(0, 50, 80, 0.5);
border-radius: 4px;
margin: 0 10px;
overflow: hidden;
}
#hud-throttle-bar {
height: 100%;
width: 0%;
background: linear-gradient(90deg, #00ff88, #00ffcc);
transition: width 0.1s;
}
#hud-warnings {
position: absolute;
top: 100px;
left: 50%;
transform: translateX(-50%);
text-align: center;
}
.warning {
color: #ff4444;
font-size: 1.1em;
font-weight: bold;
text-shadow: 0 0 10px rgba(255, 50, 50, 0.8);
animation: blink 0.5s infinite alternate;
}
@keyframes blink {
from { opacity: 1; }
to { opacity: 0.4; }
}
#hud-status {
position: absolute;
bottom: 60px;
left: 50%;
transform: translateX(-50%);
color: #88ccff;
font-size: 0.9em;
}
/* Dogfight HUD */
#hud-dogfight {
position: absolute;
top: 50px;
right: 20px;
text-align: right;
}
#hud-target-info {
color: #ff8848;
font-size: 0.9em;
margin-bottom: 10px;
}
#hud-player-hp-bar {
width: 150px;
height: 10px;
background: rgba(100, 0, 0, 0.5);
border-radius: 5px;
overflow: hidden;
margin: 5px 0 5px auto;
}
#hud-player-hp-fill {
height: 100%;
width: 100%;
background: linear-gradient(90deg, #ff4444, #00ff88);
transition: width 0.3s;
}
#hud-score {
position: absolute;
top: 10px;
right: 20px;
color: #00ffcc;
font-size: 1.2em;
}
/* ── css: pause ── */
#pause-menu {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 200;
}
#pause-content {
text-align: center;
}
#pause-content h2 {
font-size: 2.5em;
color: #00ffcc;
margin-bottom: 30px;
}
#pause-content button {
display: block;
width: 200px;
margin: 10px auto;
padding: 12px 20px;
background: rgba(0, 80, 120, 0.4);
border: 2px solid #00ffcc;
border-radius: 5px;
color: #00ffcc;
font-family: 'Courier New', monospace;
font-size: 1em;
cursor: pointer;
transition: all 0.3s;
}
#pause-content button:hover {
background: rgba(0, 150, 200, 0.4);
box-shadow: 0 0 10px rgba(0, 255, 200, 0.3);
}
</style>
</head>
<body>
<!-- Main Menu -->
<div id="menu">
<div id="menu-content">
<h1>APEX AERO</h1>
<p class="subtitle">Flight Simulator</p>
<div id="aircraft-select">
<h2>Select Aircraft</h2>
<div id="aircraft-list"></div>
</div>
<div id="scenario-select">
<h2>Select Scenario</h2>
<button class="scenario-btn" data-scenario="takeoff">
<span class="scenario-title">Takeoff & Landing</span>
<span class="scenario-desc">Master precision flight - manage altitude, speed, and power</span>
</button>
<button class="scenario-btn" data-scenario="dogfight">
<span class="scenario-title">Dogfight</span>
<span class="scenario-desc">High-G combat maneuvering against enemy aircraft</span>
</button>
</div>
<div id="controls-info">
<h3>Controls</h3>
<div class="control-grid">
<span>W/S</span><span>Pitch Up/Down</span>
<span>A/D</span><span>Roll Left/Right</span>
<span>Q/E</span><span>Yaw Left/Right</span>
<span>Space</span><span>Throttle Up</span>
<span>Shift</span><span>Throttle Down</span>
<span>Mouse</span><span>Aim (click to lock)</span>
</div>
</div>
</div>
</div>
<!-- HUD -->
<div id="hud" style="display:none;">
<div id="hud-crosshair">
<div class="crosshair-h"></div>
<div class="crosshair-v"></div>
<div class="crosshair-dot"></div>
</div>
<div id="hud-top">
<div id="hud-aircraft"></div>
<div id="hud-fps"></div>
</div>
<div id="hud-left">
<div class="hud-row"><span class="hud-label">ALT</span><span id="hud-altitude">0 m</span></div>
<div class="hud-row"><span class="hud-label">SPD</span><span id="hud-speed">0 km/h</span></div>
<div class="hud-row"><span class="hud-label">MACH</span><span id="hud-mach">0.00</span></div>
<div class="hud-row"><span class="hud-label">HDG</span><span id="hud-compass">0°</span></div>
</div>
<div id="hud-right">
<div class="hud-row"><span class="hud-label">PITCH</span><span id="hud-pitch">0.0°</span></div>
<div class="hud-row"><span class="hud-label">ROLL</span><span id="hud-roll">0.0°</span></div>
<div class="hud-row"><span class="hud-label">AOA</span><span id="hud-aoa">0.0°</span></div>
<div class="hud-row"><span class="hud-label">G</span><span id="hud-gforce">1.0 G</span></div>
</div>
<div id="hud-bottom">
<div class="hud-row"><span class="hud-label">THR</span>
<div id="hud-throttle-container">
<div id="hud-throttle-bar"></div>
</div>
<span id="hud-throttle">0%</span>
</div>
</div>
<div id="hud-warnings"></div>
<div id="hud-status"></div>
<!-- Dogfight HUD elements -->
<div id="hud-dogfight" style="display:none;">
<div id="hud-target-info">
<div>TARGET: <span id="hud-target-dist">---</span></div>
<div>HP: <span id="hud-target-hp">---</span></div>
</div>
<div id="hud-player-hp-bar">
<div id="hud-player-hp-fill"></div>
</div>
<div id="hud-score">SCORE: 0</div>
</div>
</div>
<!-- Game canvas -->
<canvas id="game-canvas"></canvas>
<!-- Pause menu -->
<div id="pause-menu" style="display:none;">
<div id="pause-content">
<h2>PAUSED</h2>
<button id="btn-resume">Resume</button>
<button id="btn-restart">Restart</button>
<button id="btn-menu">Main Menu</button>
</div>
</div>
<script>
'use strict';
// ── const: PHYSICS ──
const PHYSICS = (() => {
const SEA_LEVEL_DENSITY = 1.225;
const LAPSE_RATE = 0.0065;
const SEA_LEVEL_TEMP = 288.15;
const GRAVITY = 9.81;
const R = 287.05;
function airDensity(altitude) {
if (altitude > 11000) return 0.3639;
const temp = SEA_LEVEL_TEMP - LAPSE_RATE * altitude;
const pressure = 101325 * Math.pow(temp / SEA_LEVEL_TEMP, 5.2561);
return pressure / (R * temp);
}
function liftCoefficient(aoa, maxLiftCoeff, stallAngle) {
const stallRad = stallAngle * Math.PI / 180;
const aoaRad = aoa;
if (Math.abs(aoaRad) < stallRad) {
return maxLiftCoeff * Math.sin(2 * aoaRad);
} else {
const stallFactor = Math.max(0, 1 - (Math.abs(aoaRad) - stallRad) / (Math.PI / 4));
return maxLiftCoeff * Math.sin(2 * stallRad * Math.sign(aoaRad)) * stallFactor * 0.3;
}
}
function dragCoefficient(aoa, baseDrag, inducedDragFactor) {
const liftC = liftCoefficient(aoa, 1.5, 18);
return baseDrag + inducedDragFactor * liftC * liftC;
}
return { SEA_LEVEL_DENSITY, GRAVITY, airDensity, liftCoefficient, dragCoefficient };
})();
// ── const: NOISE ──
/**
* Simplex-like noise for procedural terrain generation
*/
const NOISE = (() => {
const perm = new Uint8Array(512);
const grad3 = [
[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],
[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],
[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]
];
function init(seed) {
const p = new Uint8Array(256);
for (let i = 0; i < 256; i++) p[i] = i;
// Fisher-Yates shuffle with seed
let s = seed || 1234;
for (let i = 255; i > 0; i--) {
s = (s * 16807 + 0) % 2147483647;
const j = s % (i + 1);
[p[i], p[j]] = [p[j], p[i]];
}
for (let i = 0; i < 512; i++) perm[i] = p[i & 255];
}
function dot2(g, x, y) { return g[0]*x + g[1]*y; }
function perlin2(x, y) {
const X = Math.floor(x) & 255;
const Y = Math.floor(y) & 255;
const xf = x - Math.floor(x);
const yf = y - Math.floor(y);
const u = fade(xf);
const v = fade(yf);
const aa = perm[perm[X] + Y] % 12;
const ab = perm[perm[X] + Y + 1] % 12;
const ba = perm[perm[X + 1] + Y] % 12;
const bb = perm[perm[X + 1] + Y + 1] % 12;
const x1 = lerp(dot2(grad3[aa], xf, yf), dot2(grad3[ba], xf-1, yf), u);
const x2 = lerp(dot2(grad3[ab], xf, yf-1), dot2(grad3[bb], xf-1, yf-1), u);
return lerp(x1, x2, v);
}
function fade(t) { return t*t*t*(t*(t*6-15)+10); }
function lerp(a, b, t) { return a + t*(b-a); }
/**
* Fractal Brownian Motion - layered noise for natural terrain
*/
function fbm(x, y, octaves, lacunarity, gain) {
let val = 0, amp = 1, freq = 1, max = 0;
for (let i = 0; i < octaves; i++) {
val += amp * perlin2(x * freq, y * freq);
max += amp;
amp *= gain;
freq *= lacunarity;
}
return val / max;
}
/**
* Terrain height at world position (x, z)
* Returns height in meters
*/
function terrainHeight(x, z) {
let h = 0;
// Large-scale mountains
h += fbm(x * 0.0008, z * 0.0008, 5, 2.0, 0.5) * 350;
// Medium features
h += fbm(x * 0.003 + 100, z * 0.003 + 100, 4, 2.0, 0.5) * 80;
// Small detail
h += fbm(x * 0.01 + 200, z * 0.01 + 200, 3, 2.0, 0.5) * 15;
// Coastline: create a valley near a region
const coastDist = Math.abs(fbm(x * 0.0005 + 500, z * 0.0005 + 500, 2, 2, 0.5));
h *= 0.6 + 0.4 * coastDist;
// Ensure flat runways area
const runwayDist = Math.sqrt((x - 0) * (x - 0) + (z - 0) * (z - 0));
if (runwayDist < 500) {
const blend = Math.max(0, (runwayDist - 100) / 400);
h *= blend;
}
return h;
}
init();
return { perlin2, fbm, terrainHeight };
})();
// ── const: AIRCRAFT ──
/**
* Three distinct aircraft with different physics profiles
*/
const AIRCRAFT = {
'falcon': {
name: 'Falcon X-1',
description: 'Light Fighter Jet',
color: 0x3366cc,
accent: 0x66aaff,
mass: 9000,
wingArea: 28,
maxThrust: 130000,
maxSpeed: 680,
stallSpeed: 55,
baseDrag: 0.018,
inducedDrag: 0.045,
maxLiftCoeff: 1.6,
stallAngle: 18,
pitchRate: 2.5,
rollRate: 5.0,
yawRate: 1.5,
maxG: 9,
scale: 1.0,
type: 'fighter'
},
'eagle': {
name: 'Eagle A-10',
description: 'Heavy Attack Aircraft',
color: 0x556b2f,
accent: 0x8fbc8f,
mass: 15000,
wingArea: 45,
maxThrust: 180000,
maxSpeed: 560,
stallSpeed: 65,
baseDrag: 0.028,
inducedDrag: 0.065,
maxLiftCoeff: 1.8,
stallAngle: 20,
pitchRate: 1.8,
rollRate: 3.5,
yawRate: 1.0,
maxG: 7,
scale: 1.3,
type: 'heavy'
},
'swift': {
name: 'Swift S-2',
description: 'Aerobatic Sport Plane',
color: 0xcc3333,
accent: 0xff6666,
mass: 4500,
wingArea: 18,
maxThrust: 45000,
maxSpeed: 350,
stallSpeed: 40,
baseDrag: 0.014,
inducedDrag: 0.035,
maxLiftCoeff: 1.9,
stallAngle: 22,
pitchRate: 4.5,
rollRate: 7.0,
yawRate: 2.5,
maxG: 12,
scale: 0.8,
type: 'sport'
}
};
// ── const: AUDIO ──
/**
* Dynamic audio system - engine sounds, wind, environment
* Uses Web Audio API for procedural sound generation
*/
const AUDIO = (() => {
let ctx = null;
let engineOsc = null;
let engineGain = null;
let windGain = null;
let windFilter = null;
let initialized = false;
function init() {
if (initialized) return;
try {
ctx = new (window.AudioContext || window.webkitAudioContext)();
// Engine sound - low frequency oscillator
engineOsc = ctx.createOscillator();
engineOsc.type = 'sawtooth';
engineOsc.frequency.value = 80;
const engineFilter = ctx.createBiquadFilter();
engineFilter.type = 'lowpass';
engineFilter.frequency.value = 400;
engineGain = ctx.createGain();
engineGain.gain.value = 0;
engineOsc.connect(engineFilter);
engineFilter.connect(engineGain);
engineGain.connect(ctx.destination);
engineOsc.start();
// Wind noise
const bufferSize = 2 * ctx.sampleRate;
const noiseBuffer = ctx.createBuffer(1, bufferSize, ctx.sampleRate);
const output = noiseBuffer.getChannelData(0);
for (let i = 0; i < bufferSize; i++) {
output[i] = Math.random() * 2 - 1;
}
const windNoise = ctx.createBufferSource();
windNoise.buffer = noiseBuffer;
windNoise.loop = true;
windFilter = ctx.createBiquadFilter();
windFilter.type = 'bandpass';
windFilter.frequency.value = 800;
windFilter.Q.value = 0.5;
windGain = ctx.createGain();
windGain.gain.value = 0;
windNoise.connect(windFilter);
windFilter.connect(windGain);
windGain.connect(ctx.destination);
windNoise.start();
initialized = true;
} catch (e) {
console.warn('Audio not available:', e);
}
}
function update(state, aircraft) {
if (!initialized || !ctx) return;
const throttle = state.throttle || 0;
const speed = state.speed || 0;
const altitude = state.altitude || 0;
// Engine pitch and volume based on throttle and speed
const engineFreq = 60 + throttle * 120 + speed * 0.3;
const engineVol = 0.02 + throttle * 0.08 + speed * 0.0001;
engineOsc.frequency.setTargetAtTime(engineFreq, ctx.currentTime, 0.05);
engineGain.gain.setTargetAtTime(Math.min(engineVol, 0.15), ctx.currentTime, 0.05);
// Wind noise increases with speed and altitude
const windVol = Math.min(0.06, speed * 0.0003 + altitude * 0.00001);
windGain.gain.setTargetAtTime(windVol, ctx.currentTime, 0.1);
// Wind filter frequency shifts with speed
const windFreq = 400 + speed * 2;
windFilter.frequency.setTargetAtTime(windFreq, ctx.currentTime, 0.1);
}
return { init, update };
})();
// ── const: MODELS ──
/**
* Build 3D aircraft models using Three.js geometry
* Each aircraft has a unique visual design
*/
const MODELS = (() => {
function createFighter(aircraft) {
const group = new THREE.Group();
const s = aircraft.scale;
// Fuselage - sleek pointed body
const fuselageGeo = new THREE.CylinderGeometry(0.3*s, 0.5*s, 8*s, 8);
fuselageGeo.rotateX(Math.PI / 2);
const fuselageMat = new THREE.MeshStandardMaterial({
color: aircraft.color, metalness: 0.7, roughness: 0.3
});
const fuselage = new THREE.Mesh(fuselageGeo, fuselageMat);
group.add(fuselage);
// Nose cone
const noseGeo = new THREE.ConeGeometry(0.3*s, 2*s, 8);
noseGeo.rotateX(-Math.PI / 2);
const nose = new THREE.Mesh(noseGeo, fuselageMat);
nose.position.z = 5*s;
group.add(nose);
// Main wings - swept back
const wingShape = new THREE.Shape();
wingShape.moveTo(0, 0);
wingShape.lineTo(5*s, -0.5*s);
wingShape.lineTo(5*s, 0.3*s);
wingShape.lineTo(0.5*s, 1*s);
wingShape.lineTo(0, 0);
const wingGeo = new THREE.ExtrudeGeometry(wingShape, { depth: 0.1*s, bevelEnabled: false });
wingGeo.translate(0, 0, -0.05*s);
const wingMat = new THREE.MeshStandardMaterial({
color: aircraft.color, metalness: 0.6, roughness: 0.4
});
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.set(0, 0, -0.5*s);
group.add(wings);
// Tail fins
const tailShape = new THREE.Shape();
tailShape.moveTo(0, 0);
tailShape.lineTo(0, 2*s);
tailShape.lineTo(1.5*s, 0);
tailShape.lineTo(0, 0);
const tailGeo = new THREE.ExtrudeGeometry(tailShape, { depth: 0.05*s, bevelEnabled: false });
const tail = new THREE.Mesh(tailGeo, fuselageMat);
tail.position.set(-0.2*s, 0, -3.5*s);
group.add(tail);
// Canopy
const canopyGeo = new THREE.SphereGeometry(0.35*s, 8, 8, 0, Math.PI*2, 0, Math.PI/2);
const canopyMat = new THREE.MeshStandardMaterial({
color: 0x88ccff, metalness: 0.9, roughness: 0.1, transparent: true, opacity: 0.7
});
const canopy = new THREE.Mesh(canopyGeo, canopyMat);
canopy.position.set(0, 0.35*s, 1.5*s);
group.add(canopy);
// Engine glow
const glowGeo = new THREE.CircleGeometry(0.3*s, 8);
const glowMat = new THREE.MeshBasicMaterial({ color: 0xff6600 });
const glow = new THREE.Mesh(glowGeo, glowMat);
glow.position.set(0, 0, -4*s);
glow.rotation.y = Math.PI;
group.add(glow);
return group;
}
function createHeavy(aircraft) {
const group = new THREE.Group();
const s = aircraft.scale;
// Wide fuselage
const fuselageGeo = new THREE.CylinderGeometry(0.7*s, 0.8*s, 9*s, 10);
fuselageGeo.rotateX(Math.PI / 2);
const fuselageMat = new THREE.MeshStandardMaterial({
color: aircraft.color, metalness: 0.4, roughness: 0.6
});
const fuselage = new THREE.Mesh(fuselageGeo, fuselageMat);
group.add(fuselage);
// Cockpit
const cockpitGeo = new THREE.SphereGeometry(0.6*s, 8, 8, 0, Math.PI*2, 0, Math.PI/2);
const cockpitMat = new THREE.MeshStandardMaterial({
color: 0x445544, metalness: 0.5, roughness: 0.3
});
const cockpit = new THREE.Mesh(cockpitGeo, cockpitMat);
cockpit.position.set(0, 0.5*s, 2*s);
group.add(cockpit);
// Straight wide wings
const wingGeo = new THREE.BoxGeometry(12*s, 0.15*s, 2.5*s);
const wingMat = new THREE.MeshStandardMaterial({
color: aircraft.color, metalness: 0.4, roughness: 0.6
});
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.set(0, 0, 0);
group.add(wings);
// Twin engines
for (let side of [-1, 1]) {
const engineGeo = new THREE.CylinderGeometry(0.3*s, 0.35*s, 2*s, 8);
engineGeo.rotateX(Math.PI / 2);
const engineMat = new THREE.MeshStandardMaterial({ color: 0x333333, metalness: 0.8, roughness: 0.2 });
const engine = new THREE.Mesh(engineGeo, engineMat);
engine.position.set(side * 3*s, -0.3*s, -1*s);
group.add(engine);
}
// Twin tail booms
for (let side of [-1, 1]) {
const boomGeo = new THREE.CylinderGeometry(0.15*s, 0.15*s, 4*s, 6);
boomGeo.rotateX(Math.PI / 2);
const boom = new THREE.Mesh(boomGeo, fuselageMat);
boom.position.set(side * 1.5*s, 0.2*s, -3*s);
group.add(boom);
}
// Tail
const tailGeo = new THREE.BoxGeometry(5*s, 0.1*s, 1.5*s);
const tail = new THREE.Mesh(tailGeo, wingMat);
tail.position.set(0, 0.5*s, -4.5*s);
group.add(tail);
// Vertical stabilizers
for (let side of [-1, 1]) {
const stabGeo = new THREE.BoxGeometry(0.1*s, 2*s, 1.5*s);
const stab = new THREE.Mesh(stabGeo, fuselageMat);
stab.position.set(side * 1.5*s, 1.5*s, -4.5*s);
group.add(stab);
}
return group;
}
function createSport(aircraft) {
const group = new THREE.Group();
const s = aircraft.scale;
// Small sleek fuselage
const fuselageGeo = new THREE.CylinderGeometry(0.25*s, 0.4*s, 6*s, 8);
fuselageGeo.rotateX(Math.PI / 2);
const fuselageMat = new THREE.MeshStandardMaterial({
color: aircraft.color, metalness: 0.5, roughness: 0.3
});
const fuselage = new THREE.Mesh(fuselageGeo, fuselageMat);
group.add(fuselage);
// Propeller
const propGeo = new THREE.BoxGeometry(2.5*s, 0.05*s, 0.3*s);
const propMat = new THREE.MeshStandardMaterial({ color: 0x222222, metalness: 0.8 });
const prop = new THREE.Mesh(propGeo, propMat);
prop.position.z = 3.2*s;
prop.name = 'propeller';
group.add(prop);
// Wings - high wing configuration
const wingGeo = new THREE.BoxGeometry(8*s, 0.08*s, 1.5*s);
const wingMat = new THREE.MeshStandardMaterial({
color: aircraft.accent, metalness: 0.3, roughness: 0.5
});
const wings = new THREE.Mesh(wingGeo, wingMat);
wings.position.set(0, 0.5*s, 0);
group.add(wings);
// Struts
for (let side of [-1, 1]) {
const strutGeo = new THREE.CylinderGeometry(0.03*s, 0.03*s, 0.8*s, 4);
const strut = new THREE.Mesh(strutGeo, fuselageMat);
strut.position.set(side * 2*s, 0.2*s, 0);
strut.rotation.z = side * 0.3;
group.add(strut);
}
// Tail
const tailGeo = new THREE.BoxGeometry(2.5*s, 0.06*s, 0.8*s);
const tail = new THREE.Mesh(tailGeo, wingMat);
tail.position.set(0, 0, -2.8*s);
group.add(tail);
// Vertical stabilizer
const stabGeo = new THREE.BoxGeometry(0.06*s, 1.2*s, 0.8*s);
const stab = new THREE.Mesh(stabGeo, fuselageMat);
stab.position.set(0, 0.6*s, -2.8*s);
group.add(stab);
// Canopy
const canopyGeo = new THREE.SphereGeometry(0.25*s, 8, 8, 0, Math.PI*2, 0, Math.PI/2);
const canopyMat = new THREE.MeshStandardMaterial({
color: 0xaaddff, metalness: 0.9, roughness: 0.1, transparent: true, opacity: 0.6
});
const canopy = new THREE.Mesh(canopyGeo, canopyMat);
canopy.position.set(0, 0.3*s, 1*s);
group.add(canopy);
return group;
}
function create(aircraft) {
switch(aircraft.type) {
case 'fighter': return createFighter(aircraft);
case 'heavy': return createHeavy(aircraft);
case 'sport': return createSport(aircraft);
default: return createFighter(aircraft);
}
}
return { create };
})();
// ── const: TERRAIN ──
/**
* Procedural terrain renderer using Three.js
*/
const TERRAIN = (() => {
let mesh = null;
let water = null;
let trees = null;
const SIZE = 8000;
const SEGMENTS = 200;
function create(scene) {
// Terrain geometry
const geometry = new THREE.PlaneGeometry(SIZE, SIZE, SEGMENTS, SEGMENTS);
geometry.rotateX(-Math.PI / 2);
const vertices = geometry.attributes.position.array;
const colors = [];
for (let i = 0; i < vertices.length; i += 3) {
const x = vertices[i];
const z = vertices[i + 2];
const h = NOISE.terrainHeight(x, z);
vertices[i + 1] = h;
// Color based on height
const color = new THREE.Color();
if (h < 1) {
color.setHex(0x2d5016); // Grass base
} else if (h < 50) {
color.setHex(0x4a7c2e); // Grass
} else if (h < 150) {
color.setHex(0x6b8c42); // Highland
} else if (h < 250) {
color.setHex(0x8b7355); // Rock
} else {
color.setHex(0xcccccc); // Snow
}
// Add some variation
const variation = (Math.random() - 0.5) * 0.08;
color.r = Math.max(0, Math.min(1, color.r + variation));
color.g = Math.max(0, Math.min(1, color.g + variation));
color.b = Math.max(0, Math.min(1, color.b + variation));
colors.push(color.r, color.g, color.b);
}
geometry.setAttribute('color', new THREE.Float32BufferAttribute(colors, 3));
geometry.computeVertexNormals();
const material = new THREE.MeshStandardMaterial({
vertexColors: true,
roughness: 0.8,
metalness: 0.1,
flatShading: false
});
mesh = new THREE.Mesh(geometry, material);
mesh.receiveShadow = true;
scene.add(mesh);
// Water plane
const waterGeo = new THREE.PlaneGeometry(SIZE * 1.5, SIZE * 1.5);
waterGeo.rotateX(-Math.PI / 2);
const waterMat = new THREE.MeshStandardMaterial({
color: 0x1a5276,
transparent: true,
opacity: 0.7,
roughness: 0.1,
metalness: 0.3
});
water = new THREE.Mesh(waterGeo, waterMat);
water.position.y = -5;
scene.add(water);
// Scatter trees/objects
createScenery(scene);
return { mesh, water };
}
function createScenery(scene) {
const treeGeo = new THREE.ConeGeometry(3, 12, 6);
const treeMat = new THREE.MeshStandardMaterial({ color: 0x2d5016 });
const trunkGeo = new THREE.CylinderGeometry(0.5, 0.7, 5, 4);
const trunkMat = new THREE.MeshStandardMaterial({ color: 0x4a3728 });
const treeCount = 500;
const dummy = new THREE.Object3D();
// Use instanced mesh for performance
const treeMesh = new THREE.InstancedMesh(treeGeo, treeMat, treeCount);
const trunkMesh = new THREE.InstancedMesh(trunkGeo, trunkMat, treeCount);
let placed = 0;
let attempts = 0;
while (placed < treeCount && attempts < treeCount * 10) {
attempts++;
const x = (Math.random() - 0.5) * SIZE * 0.8;
const z = (Math.random() - 0.5) * SIZE * 0.8;
const h = NOISE.terrainHeight(x, z);
// Only place trees in grass areas
if (h > 2 && h < 100) {
const scale = 0.5 + Math.random() * 1.5;
dummy.position.set(x, h + 6 * scale, z);
dummy.scale.setScalar(scale);
dummy.rotation.y = Math.random() * Math.PI * 2;
dummy.updateMatrix();
treeMesh.setMatrixAt(placed, dummy.matrix);
dummy.position.set(x, h + 2.5 * scale, z);
dummy.updateMatrix();
trunkMesh.setMatrixAt(placed, dummy.matrix);
placed++;
}
}
treeMesh.count = placed;
trunkMesh.count = placed;
scene.add(treeMesh);
scene.add(trunkMesh);
}
return { create, SIZE };
})();
// ── const: INPUT ──
/**
* Input handling - mouse, keyboard, and gamepad support
*/
const INPUT = (() => {
const keys = {};
const mouse = { x: 0, y: 0, dx: 0, dy: 0, down: false };
const mouseSensitivity = 0.003;
let isPointerLocked = false;
function init(canvas) {
document.addEventListener('keydown', e => {
keys[e.code] = true;
if (['KeyW','KeyS','KeyA','KeyD','Space','ShiftLeft','KeyQ','KeyE','KeyR','KeyF'].includes(e.code)) {
e.preventDefault();
}
});
document.addEventListener('keyup', e => {
keys[e.code] = false;
});
canvas.addEventListener('mousedown', e => {
if (!isPointerLocked) {
canvas.requestPointerLock();
}
if (e.button === 0) mouse.down = true;
});
canvas.addEventListener('mouseup', e => {
if (e.button === 0) mouse.down = false;
});
document.addEventListener('pointerlockchange', () => {
isPointerLocked = document.pointerLockElement === canvas;
});
document.addEventListener('mousemove', e => {
if (isPointerLocked) {
mouse.dx = e.movementX || 0;
mouse.dy = e.movementY || 0;
mouse.x += mouse.dx;
mouse.y += mouse.dy;
}
});
// Scroll wheel for throttle
canvas.addEventListener('wheel', e => {
keys['__scroll'] = e.deltaY;
});
}
function getControls() {
const pitchInput = (keys['KeyS'] ? 1 : 0) - (keys['KeyW'] ? 1 : 0);
const rollInput = (keys['KeyA'] ? 1 : 0) - (keys['KeyD'] ? 1 : 0);
const yawInput = (keys['KeyQ'] ? 1 : 0) - (keys['KeyE'] ? 1 : 0);
let mousePitch = 0, mouseRoll = 0;
if (isPointerLocked) {
mousePitch = mouse.dy * mouseSensitivity;
mouseRoll = -mouse.dx * mouseSensitivity;
mouse.dx = 0;
mouse.dy = 0;
}
return {
pitchInput: pitchInput + mousePitch,
rollInput: rollInput + mouseRoll,
yawInput: yawInput,
throttleUp: keys['Space'],
throttleDown: keys['ShiftLeft'],
boost: keys['KeyR'],
flap: keys['KeyF']
};
}
function isLocked() { return isPointerLocked; }
function consumeScroll() { const v = keys['__scroll'] || 0; keys['__scroll'] = 0; return v; }
return { init, getControls, isLocked, consumeScroll };
})();
// ── const: HUD ──
const HUD = (() => {
var elements = {};
function init() {
var ids = ['altitude','speed','throttle','pitch','roll','aoa','gforce','mach','aircraft','fps','compass','throttle-bar','warnings','status'];
ids.forEach(function(id) { elements[id] = document.getElementById('hud-' + id); });
}
function update(state, aircraft, fpsVal) {
if (!elements.altitude) return;
var alt = state.altitude || 0;
var speed = (state.speed || 0) * 3.6;
var throttle = (state.throttle || 0) * 100;
elements.altitude.textContent = Math.round(alt) + ' m';
elements.speed.textContent = Math.round(speed) + ' km/h';
elements.throttle.textContent = Math.round(throttle) + '%';
elements['throttle-bar'].style.width = throttle + '%';
elements.aoa.textContent = (state.aoa || 0).toFixed(1) + ' deg';
elements.gforce.textContent = (state.gForce || 1).toFixed(1) + ' G';
elements.mach.textContent = (state.mach || 0).toFixed(2);
elements.aircraft.textContent = aircraft.name;
elements.fps.textContent = fpsVal + ' FPS';
elements.pitch.textContent = '0.0 deg';
elements.roll.textContent = '0.0 deg';
elements.compass.textContent = '0 deg';
elements.warnings.innerHTML = '';
}
function show() { var h = document.getElementById('hud'); if(h) h.style.display = 'block'; }
function hide() { var h = document.getElementById('hud'); if(h) h.style.display = 'none'; }
return { init:init, update:update, show:show, hide:hide };
})();
// ── const: ENEMIES ──
const ENEMIES = (() => {
const enemies = [];
function create(scene, count, playerState) {
for (let i = 0; i < count; i++) {
const angle = (i / count) * Math.PI * 2;
const dist = 800 + Math.random() * 400;
const enemy = {
aircraft: AIRCRAFT.falcon,
state: {
px: Math.cos(angle) * dist,
py: playerState.py + 100 + Math.random() * 200,
pz: Math.sin(angle) * dist,
vx: (Math.random() - 0.5) * 100,
vy: 0,
vz: (Math.random() - 0.5) * 100,
throttle: 0.7, pitchVel: 0, rollVel: 0, yawVel: 0,
altitude: playerState.py + 100, speed: 0, aoa: 0, gForce: 1, mach: 0
},
model: null, hp: 100, alive: true, fireTimer: 0
};
enemy.model = MODELS.create(AIRCRAFT.falcon);
enemy.model.scale.setScalar(0.8);
enemy.model.traverse(child => {
if (child.isMesh && child.material) {
child.material = child.material.clone();
child.material.color = new THREE.Color(0xcc2222);
}
});
scene.add(enemy.model);
enemies.push(enemy);
}
return enemies;
}
function update(enemies, playerState, dt, scene) {
for (const enemy of enemies) {
if (!enemy.alive) continue;
const s = enemy.state;
const dx = playerState.px - s.px, dy = playerState.py - s.py, dz = playerState.pz - s.pz;
const dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
const targetSpeed = dist > 500 ? 400 : 200;
const spdErr = targetSpeed - s.speed;
s.throttle = Math.max(0.2, Math.min(1, 0.5 + spdErr * 0.002));
const thrustZ = s.throttle * enemy.aircraft.maxThrust / enemy.aircraft.mass;
s.vz += thrustZ * dt;
s.vy += -PHYSICS.GRAVITY * dt * 0.3;
const spd = Math.sqrt(s.vx*s.vx + s.vy*s.vy + s.vz*s.vz);
if (spd > 500) { s.vx *= 500/spd; s.vy *= 500/spd; s.vz *= 500/spd; }
s.px += s.vx * dt;
s.py += s.vy * dt;
s.pz += s.vz * dt;
const terrainH = NOISE.terrainHeight(s.px, s.pz);
if (s.py < terrainH + 20) { s.py = terrainH + 20; if (s.vy < 0) s.vy = Math.abs(s.vy) * 0.5; }
if (Math.sqrt(s.px*s.px + s.pz*s.pz) > TERRAIN.SIZE * 0.4) {
s.vx -= s.px * 0.01 * dt; s.vz -= s.pz * 0.01 * dt;
}
s.altitude = s.py - terrainH;
s.speed = Math.sqrt(s.vx*s.vx + s.vy*s.vy + s.vz*s.vz);
enemy.fireTimer -= dt;
if (dist < 600 && enemy.fireTimer <= 0) {
if (Math.random() < 0.3) return { hit: true, damage: 5 + Math.random() * 10 };
enemy.fireTimer = 0.5;
}
enemy.model.position.set(s.px, s.py, s.pz);
}
return null;
}
function removeDead(enemies, scene) {
for (let i = enemies.length - 1; i >= 0; i--) {
if (!enemies[i].alive) { scene.remove(enemies[i].model); enemies.splice(i, 1); }
}
}
return { create, update, removeDead, enemies };
})();
// ── const: PARTICLES ──
const PARTICLES = (() => {
const particles = [];
const maxParticles = 500;
const geo = new THREE.SphereGeometry(1, 4, 4);
function createExplosion(scene, pos, color) {
color = color || 0xff6600;
for (let i = 0; i < 30; i++) {
const mat = new THREE.MeshBasicMaterial({ color: color, transparent: true, opacity: 1 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(pos.x, pos.y, pos.z);
mesh.scale.setScalar(0.5 + Math.random() * 2);
const vel = { x:(Math.random()-0.5)*100, y:Math.random()*60, z:(Math.random()-0.5)*100 };
scene.add(mesh);
particles.push({ mesh, vel, life: 1.0+Math.random(), maxLife: 1.0+Math.random() });
}
}
function createContrail(scene, pos) {
if (particles.length > maxParticles) return;
const mat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.4 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(pos.x, pos.y, pos.z);
mesh.scale.setScalar(0.8);
scene.add(mesh);
particles.push({ mesh, vel: {x:0,y:0,z:0}, life: 2.0, maxLife: 2.0 });
}
function createBullet(scene, pos, dir) {
const mat = new THREE.MeshBasicMaterial({ color: 0xffff00 });
const mesh = new THREE.Mesh(geo, mat);
mesh.position.set(pos.x, pos.y, pos.z);
mesh.scale.setScalar(0.3, 0.3, 2);
scene.add(mesh);
particles.push({ mesh, vel: {x:dir.x*800, y:dir.y*800, z:dir.z*800}, life: 1.5, maxLife: 1.5, isBullet: true });
}
function update(scene, dt) {
for (let i = particles.length - 1; i >= 0; i--) {
const p = particles[i];
p.life -= dt;
if (p.life <= 0) { scene.remove(p.mesh); p.mesh.material.dispose(); particles.splice(i, 1); continue; }
if (!p.isBullet) { p.vel.y -= 20 * dt; }
p.mesh.position.x += p.vel.x * dt;
p.mesh.position.y += p.vel.y * dt;
p.mesh.position.z += p.vel.z * dt;
p.mesh.material.opacity = p.life / p.maxLife;
p.mesh.scale.multiplyScalar(0.995);
}
}
function clear(scene) {
for (const p of particles) { scene.remove(p.mesh); p.mesh.material.dispose(); }
particles.length = 0;
}
return { createExplosion, createContrail, createBullet, update, clear };
})();
// ── const: AFFINE ──
const AFFINE = (() => {
function update(aircraft, state, controls, dt) {
if (!state) return;
var pIn = controls.pitchInput || 0;
var rIn = controls.rollInput || 0;
var yIn = controls.yawInput || 0;
var inertia = Math.sqrt(aircraft.mass / 5000);
var vx = state.vx || 0, vy = state.vy || 0, vz = state.vz || 0;
var spd = Math.sqrt(vx*vx + vy*vy + vz*vz);
var spdF = Math.min(1, 200 / Math.max(spd, 50));
// Angular rates (rad/s)
state.pitchVel = (state.pitchVel || 0) + pIn * aircraft.pitchRate / inertia * dt * spdF;
state.rollVel = (state.rollVel || 0) + rIn * aircraft.rollRate / inertia * dt * spdF;
state.yawVel = (state.yawVel || 0) + yIn * aircraft.yawRate / inertia * dt * spdF;
state.pitchVel *= 0.97;
state.rollVel *= 0.97;
state.yawVel *= 0.97;
// Accumulate orientation angles
state.pitch = (state.pitch || 0) + state.pitchVel * dt;
state.roll = (state.roll || 0) + state.rollVel * dt;
state.yaw = (state.yaw || 0) + state.yawVel * dt;
// Clamp pitch
if (state.pitch > Math.PI/2) state.pitch = Math.PI/2;
if (state.pitch < -Math.PI/2) state.pitch = -Math.PI/2;
// Forward direction from yaw and pitch
var cosP = Math.cos(state.pitch);
var fwdX = Math.sin(state.yaw) * cosP;
var fwdY = Math.sin(state.pitch);
var fwdZ = Math.cos(state.yaw) * cosP;
// Thrust along forward
var throttle = state.throttle || 0;
var thrustMag = throttle * aircraft.maxThrust / aircraft.mass;
vx += fwdX * thrustMag * dt;
vy += fwdY * thrustMag * dt;
vz += fwdZ * thrustMag * dt;
// Gravity
vy += -PHYSICS.GRAVITY * dt;
// Lift (simplified - perpendicular to velocity, proportional to speed squared)
if (spd > 10) {
var cl = 0.02 * state.pitch; // lift coefficient proportional to pitch angle
var liftMag = cl * spd * spd * aircraft.wingArea / aircraft.mass;
// Lift is perpendicular to velocity, pointing up relative to aircraft
vx -= (vx/spd) * liftMag * dt * 0.1;
vy += liftMag * dt;
vz -= (vz/spd) * liftMag * dt * 0.1;
}
// Drag
var dragK = aircraft.baseDrag * aircraft.wingArea / aircraft.mass;
vx -= vx * dragK * dt;
vy -= vy * dragK * dt;
vz -= vz * dragK * dt;
// Integrate position
var px = state.px || 0, py = state.py || 5, pz = state.pz || 0;
px += vx * dt;
py += vy * dt;
pz += vz * dt;
var terrainH = NOISE.terrainHeight(px, pz);
var groundLevel = terrainH + 3;
if (py < groundLevel) {
py = groundLevel;
if (vy < 0) vy = 0;
vx *= 0.98; vz *= 0.98;
state.onGround = true;
} else {
state.onGround = false;
}
state.vx = vx; state.vy = vy; state.vz = vz;
state.px = px; state.py = py; state.pz = pz;
state.altitude = py - terrainH;
state.speed = Math.sqrt(vx*vx + vy*vy + vz*vz);
state.aoa = state.pitch * 180 / Math.PI;
state.gForce = Math.abs(thrustMag / PHYSICS.GRAVITY) + 1;
state.mach = state.speed / 343;
}
return { update };
})();
// ── main ──
/**
* Main Game Loop - Apex Aero Flight Simulator
*/
var scene, camera, renderer, canvas;
var playerModel, currentState, currentAircraft;
var gameMode = null, isPaused = false;
var score = 0, playerHP = 100;
var enemies = [];
var lastTime = 0, frameCount = 0, fps = 60, fpsTimer = 0;
var fireTimer = 0, contrailTimer = 0;
var selectedAircraft = null;
var takeoffComplete = false, maxAltitude = 0;
var missionTargetAltitude = 1000;
function init() {
canvas = document.getElementById('game-canvas');
renderer = new THREE.WebGLRenderer({ canvas: 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;
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x88aacc, 0.00015);
scene.background = new THREE.Color(0x87ceeb);
camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.5, 15000);
camera.position.set(0, 100, -200);
scene.add(new THREE.AmbientLight(0x404060, 0.5));
var sun = new THREE.DirectionalLight(0xffffee, 1.5);
sun.position.set(1000, 2000, 500);
sun.castShadow = true;
scene.add(sun);
scene.add(new THREE.HemisphereLight(0x87ceeb, 0x3a5016, 0.4));
TERRAIN.create(scene);
INPUT.init(canvas);
HUD.init();
AUDIO.init();
setupMenu();
window.addEventListener('resize', onResize);
document.addEventListener('keydown', function(e) { if (e.code === 'Escape' && gameMode) togglePause(); });
document.getElementById('btn-resume').addEventListener('click', togglePause);
document.getElementById('btn-restart').addEventListener('click', restartGame);
document.getElementById('btn-menu').addEventListener('click', returnToMenu);
lastTime = performance.now();
requestAnimationFrame(gameLoop);
}
function setupMenu() {
var list = document.getElementById('aircraft-list');
var keys = Object.keys(AIRCRAFT);
var cards = {};
keys.forEach(function(key) {
var ac = AIRCRAFT[key];
var card = document.createElement('div');
card.className = 'aircraft-card';
card.innerHTML = '<h3>' + ac.name + '</h3><p>' + ac.description + '</p><div class="stats"><div>Max Speed: ' + Math.round(ac.maxSpeed * 3.6) + ' km/h</div><div>Thrust: ' + Math.round(ac.maxThrust / 1000) + ' kN</div><div>Weight: ' + Math.round(ac.mass) + ' kg</div><div>Max G: ' + ac.maxG + '</div></div>';
card.addEventListener('click', (function(k) {
return function() {
keys.forEach(function(kk) { cards[kk].classList.remove('selected'); });
card.classList.add('selected');
selectedAircraft = k;
};
})(key));
list.appendChild(card);
cards[key] = card;
});
selectedAircraft = keys[0];
cards[keys[0]].classList.add('selected');
document.querySelectorAll('.scenario-btn').forEach(function(btn) {
btn.addEventListener('click', function() {
if (selectedAircraft) startGame(selectedAircraft, btn.dataset.scenario);
});
});
}
function startGame(aircraftKey, scenario) {
currentAircraft = AIRCRAFT[aircraftKey];
gameMode = scenario;
score = 0; playerHP = 100; takeoffComplete = false; maxAltitude = 0;
document.getElementById('menu').style.display = 'none';
HUD.show();
document.getElementById('hud-dogfight').style.display = (scenario === 'dogfight') ? 'block' : 'none';
if (playerModel) scene.remove(playerModel);
playerModel = MODELS.create(currentAircraft);
scene.add(playerModel);
currentState = { px:0, py:NOISE.terrainHeight(0,0)+5, pz:0, vx:0, vy:0, vz:0,
throttle:0.3, pitchVel:0, rollVel:0, yawVel:0, pitch:0, roll:0, yaw:0,
altitude:5, speed:0, aoa:0, gForce:1, mach:0, onGround:true };
if (scenario === 'dogfight') {
enemies = ENEMIES.create(scene, 4, currentState);
}
PARTICLES.clear(scene);
AUDIO.init();
isPaused = false;
}
function gameLoop(time) {
requestAnimationFrame(gameLoop);
var rawDt = (time - lastTime) / 1000;
var dt = Math.min(rawDt, 0.05);
lastTime = time;
frameCount++;
fpsTimer += rawDt;
if (fpsTimer >= 1) { fps = Math.round(frameCount / fpsTimer); frameCount = 0; fpsTimer = 0; }
if (!gameMode || isPaused || !currentState) {
renderer.render(scene, camera);
return;
}
var controls = INPUT.getControls();
if (controls.throttleUp) currentState.throttle = Math.min(1, currentState.throttle + dt * 0.5);
if (controls.throttleDown) currentState.throttle = Math.max(0, currentState.throttle - dt * 0.5);
AFFINE.update(currentAircraft, currentState, controls, dt);
fireTimer -= dt;
if (INPUT.isLocked() && fireTimer <= 0) { fireGuns(); fireTimer = 0.1; }
contrailTimer -= dt;
if (currentState.speed > 300 && contrailTimer <= 0) {
var right = getRight();
PARTICLES.createContrail(scene, {x:currentState.px+right.x*3, y:currentState.py+right.y*3, z:currentState.pz+right.z*3});
contrailTimer = 0.1;
}
if (gameMode === 'dogfight' && enemies.length > 0) {
var hit = ENEMIES.update(enemies, currentState, dt, scene);
if (hit && hit.hit) { playerHP -= hit.damage; playerHP = Math.max(0, playerHP); if (playerHP <= 0) { gameOver(false); return; } }
checkPlayerHits();
ENEMIES.removeDead(enemies, scene);
if (enemies.length === 0) { score += 1000; enemies = ENEMIES.create(scene, 3 + Math.floor(score / 2000), currentState); }
}
if (gameMode === 'takeoff' && currentState.altitude > maxAltitude) maxAltitude = currentState.altitude;
updateCamera(dt);
// Sync physics to Three.js model
if (playerModel) {
playerModel.position.set(currentState.px, currentState.py, currentState.pz);
var q = getQuaternion();
playerModel.quaternion.copy(q);
}
var prop = playerModel.getObjectByName('propeller');
if (prop) prop.rotation.z += currentState.throttle * 0.8;
PARTICLES.update(scene, dt);
AUDIO.update(currentState, currentAircraft);
HUD.update(currentState, currentAircraft, fps);
updateScenarioHUD();
renderer.render(scene, camera);
}
function getQuaternion() {
var q = new THREE.Quaternion();
var euler = new THREE.Euler(currentState.pitch, currentState.yaw, currentState.roll, 'YXZ');
q.setFromEuler(euler);
return q;
}
function getForward() {
var cosP = Math.cos(currentState.pitch);
return { x: Math.sin(currentState.yaw)*cosP, y: Math.sin(currentState.pitch), z: Math.cos(currentState.yaw)*cosP };
}
function getRight() {
return { x: Math.cos(currentState.yaw), y: 0, z: -Math.sin(currentState.yaw) };
}
function getUp() {
var fwd = getForward();
var right = getRight();
return {
x: fwd.y*right.z - fwd.z*right.y,
y: fwd.z*right.x - fwd.x*right.z,
z: fwd.x*right.y - fwd.y*right.x
};
}
function updateCamera(dt) {
var fwd = getForward();
var up = getUp();
var scale = currentAircraft.scale;
var cx = currentState.px + fwd.x*(-20*scale) + up.x*(6*scale);
var cy = currentState.py + fwd.y*(-20*scale) + up.y*(6*scale);
var cz = currentState.pz + fwd.z*(-20*scale) + up.z*(6*scale);
camera.position.lerp(new THREE.Vector3(cx, cy, cz), 0.05);
camera.lookAt(currentState.px + fwd.x*100, currentState.py + fwd.y*100, currentState.pz + fwd.z*100);
}
function fireGuns() {
var fwd = getForward();
PARTICLES.createBullet(scene, {x:currentState.px, y:currentState.py, z:currentState.pz}, fwd);
}
function checkPlayerHits() {
var fwd = getForward();
for (var i = 0; i < enemies.length; i++) {
var enemy = enemies[i];
if (!enemy.alive) continue;
var ex = enemy.state.px, ey = enemy.state.py, ez = enemy.state.pz;
var dx = ex - currentState.px, dy = ey - currentState.py, dz = ez - currentState.pz;
var dist = Math.sqrt(dx*dx + dy*dy + dz*dz);
if (dist < 150 && dist > 0) {
var dot = (dx*fwd.x + dy*fwd.y + dz*fwd.z) / dist;
if (dot > 0.9) {
enemy.hp -= 25;
if (enemy.hp <= 0) { enemy.alive = false; PARTICLES.createExplosion(scene, {x:ex,y:ey,z:ez}); score += 100; }
}
}
}
}
function updateScenarioHUD() {
if (gameMode === 'takeoff') {
var status = 'Target: ' + missionTargetAltitude + 'm | Max: ' + Math.round(maxAltitude) + 'm';
if (maxAltitude >= missionTargetAltitude && !takeoffComplete) { takeoffComplete = true; status = 'Takeoff complete! Land safely to score.'; }
if (takeoffComplete && currentState.onGround && currentState.speed < 20) { score += 500; status = 'Perfect landing! +500 points'; takeoffComplete = false; maxAltitude = 0; }
document.getElementById('hud-status').textContent = status;
}
document.getElementById('hud-score').textContent = 'SCORE: ' + score;
if (gameMode === 'dogfight') {
document.getElementById('hud-player-hp-fill').style.width = playerHP + '%';
}
}
function togglePause() { isPaused = !isPaused; document.getElementById('pause-menu').style.display = isPaused ? 'flex' : 'none'; }
function restartGame() {
if (gameMode) {
document.getElementById('pause-menu').style.display = 'none';
isPaused = false;
var acKey = Object.keys(AIRCRAFT).find(function(k) { return AIRCRAFT[k] === currentAircraft; });
if (enemies) enemies.forEach(function(e) { scene.remove(e.model); });
startGame(acKey, gameMode);
}
}
function returnToMenu() {
document.getElementById('pause-menu').style.display = 'none';
document.getElementById('menu').style.display = 'flex';
document.getElementById('hud').style.display = 'none';
document.getElementById('hud-dogfight').style.display = 'none';
gameMode = null; isPaused = false;
if (enemies) { enemies.forEach(function(e) { scene.remove(e.model); }); enemies = []; }
}
function gameOver(victory) {
isPaused = true;
document.getElementById('hud-status').textContent = victory ? 'MISSION COMPLETE!' : 'AIRCRAFT DESTROYED';
setTimeout(function() { returnToMenu(); }, 3000);
}
function onResize() { camera.aspect = window.innerWidth / window.innerHeight; camera.updateProjectionMatrix(); renderer.setSize(window.innerWidth, window.innerHeight); }
document.addEventListener('DOMContentLoaded', init);
</script>
</body>
</html>
<!-- agent-meta {"model":"meituan/longcat-2.0","provider":"openrouter","persona":"composer","sessionId":"f789a62f-6442-48e1-8504-01fdf164edd8","tokensIn":13297603,"tokensOut":120267,"tokensTotal":13417870,"turns":101,"toolCalls":111,"failedToolCalls":4,"timestamp":"2026-07-21T04:51:23.611Z"} -->