← Flight Simulator results

Flight Simulator

longcat 2.0 · composer

Initial view of Flight Simulator

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 &amp; 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"} -->