Flight Simulator
muse spark 1.2 · basic

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 44,150 bytes · SHA-256 93d579784f1e
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>APEX AERO — High-Fidelity Flight Simulator</title>
<script async src="https://unpkg.com/es-module-shims@1.8.0/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}
}
</script>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Orbitron:wght@600;800&family=Inter:wght@400;600;800&display=swap" rel="stylesheet">
<style>
*{margin:0;padding:0;box-sizing:border-box}
:root{--cyan:#00e5ff;--orange:#ff6a00;--bg:#060a12;--panel:rgba(14,22,38,0.82);--line:rgba(0,229,255,0.18)}
body{font-family:'Inter',sans-serif;background:#050a14;color:#e6f0ff;overflow:hidden;height:100vh;width:100vw}
#canvas{position:absolute;inset:0;width:100%;height:100%;display:block}
.topbar{position:absolute;top:0;left:0;right:0;height:56px;display:flex;align-items:center;justify-content:space-between;padding:0 18px;z-index:20;background:linear-gradient(180deg,rgba(5,10,20,0.92),rgba(5,10,20,0.35));border-bottom:1px solid rgba(255,255,255,0.07);backdrop-filter:blur(10px)}
.brand{display:flex;align-items:center;gap:12px}
.logo{width:36px;height:36px;background:var(--cyan);display:grid;place-items:center;color:#001018;font-weight:800;transform:skewX(-8deg);border-radius:4px;box-shadow:0 0 18px rgba(0,229,255,0.5)}
.logo span{transform:skewX(8deg);font-family:'Orbitron',monospace;font-size:13px}
.brand h1{font-family:'Orbitron',monospace;font-size:15px;letter-spacing:0.18em;font-weight:800}
.brand p{font-size:10px;letter-spacing:0.2em;opacity:0.6;margin-top:2px}
.top-actions{display:flex;align-items:center;gap:10px}
.pill{font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.14em;padding:7px 12px;border-radius:999px;border:1px solid var(--line);background:rgba(255,255,255,0.04);color:#9fb8c9;cursor:pointer}
.pill.active{background:var(--cyan);color:#001018;border-color:var(--cyan);box-shadow:0 0 14px rgba(0,229,255,0.45)}
.btn-primary{background:var(--orange);color:white;border:none;padding:10px 18px;border-radius:6px;font-family:'Orbitron',monospace;font-size:11px;letter-spacing:0.14em;font-weight:800;cursor:pointer;box-shadow:0 8px 24px rgba(255,106,0,0.35)}
.btn-primary:hover{filter:brightness(1.08)}
.hud{position:absolute;inset:56px 0 0 0;pointer-events:none;z-index:5;display:grid;grid-template-columns:300px 1fr 300px;gap:14px;padding:14px}
.panel{background:var(--panel);border:1px solid rgba(255,255,255,0.08);border-radius:14px;backdrop-filter:blur(10px);box-shadow:0 12px 40px rgba(0,0,0,0.45);overflow:hidden}
.panel-head{padding:12px 14px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid rgba(255,255,255,0.06)}
.panel-head h3{font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.18em;color:#8fb0c3}
.dot{width:8px;height:8px;border-radius:50%;background:#18e07a;box-shadow:0 0 10px #18e07a;animation:pulse 1.5s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:0.5}}
.left{display:flex;flex-direction:column;gap:14px}
.readouts{display:grid;grid-template-columns:1fr 1fr;gap:10px;padding:12px}
.mini{background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06);border-radius:10px;padding:10px}
.mini label{font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:0.14em;color:#7fa0b6}
.mini .val{font-family:'Orbitron',monospace;font-size:20px;font-weight:800;margin-top:4px}
.mini .unit{font-family:'JetBrains Mono',monospace;font-size:10px;color:#7fa0b6}
.bar{height:6px;background:rgba(255,255,255,0.08);border-radius:999px;overflow:hidden;margin-top:8px}
.bar>i{display:block;height:100%;background:linear-gradient(90deg,var(--cyan),#7af4ff);width:50%;transition:width 0.15s}
.horizon{height:168px;background:radial-gradient(400px 200px at 50% 30%, rgba(0,229,255,0.08), transparent), linear-gradient(180deg,#0a3a5a 0%, #0a3a5a 50%, #2a1a0a 50%, #3a2a12 100%);position:relative;overflow:hidden;border-top:1px solid rgba(255,255,255,0.06);border-bottom:1px solid rgba(255,255,255,0.06)}
.horizon-line{position:absolute;left:0;right:0;top:50%;height:2px;background:#fff;box-shadow:0 0 8px rgba(255,255,255,0.8)}
.ladder{position:absolute;inset:0;display:grid;place-items:center}
.ladder-inner{width:140px;height:120px;position:relative;transform-origin:center}
.ladder-inner span{position:absolute;left:50%;width:40px;height:1px;background:rgba(255,255,255,0.9);transform:translateX(-50%)}
.center-hud{display:flex;flex-direction:column;pointer-events:none}
.center-top{flex:1;display:grid;place-items:center;position:relative}
.reticle{width:180px;height:180px;position:relative}
.reticle::before{content:'';position:absolute;inset:0;border:1px solid rgba(0,229,255,0.22);border-radius:50%}
.cross{position:absolute;left:50%;top:50%;width:20px;height:20px;transform:translate(-50%,-50%)}
.cross::before,.cross::after{content:'';position:absolute;background:var(--cyan);box-shadow:0 0 8px var(--cyan)}
.cross::before{left:9px;top:0;width:2px;height:20px}
.cross::after{left:0;top:9px;width:20px;height:2px}
.vv{position:absolute;left:50%;top:50%;width:14px;height:14px;border:2px solid #ff6a00;border-radius:50%;transform:translate(-50%,-50%);box-shadow:0 0 10px rgba(255,106,0,0.8)}
.bottom-strip{height:56px;display:flex;align-items:center;justify-content:space-between;padding:0 12px;background:rgba(6,10,18,0.72);border-top:1px solid rgba(255,255,255,0.06)}
.kbd{font-family:'JetBrains Mono',monospace;font-size:10px;color:#8fb0c3;display:flex;gap:8px;align-items:center}
.kbd b{border:1px solid rgba(255,255,255,0.15);background:rgba(255,255,255,0.06);padding:4px 6px;border-radius:6px;color:#d8eaff;font-size:10px}
.right{display:flex;flex-direction:column;gap:14px}
.throttle{padding:12px}
.scale{height:140px;width:18px;background:rgba(255,255,255,0.06);border-radius:999px;position:relative;overflow:hidden;border:1px solid rgba(255,255,255,0.08)}
.scale-fill{position:absolute;bottom:0;left:0;right:0;background:linear-gradient(180deg,#ff6a00,#ffb700);transition:height 0.12s}
.g-meter{display:flex;gap:10px;padding:12px;align-items:center}
.g-ring{width:64px;height:64px;border-radius:50%;border:3px solid rgba(255,255,255,0.08);display:grid;place-items:center;position:relative}
.g-ring::after{content:'';position:absolute;inset:-3px;border-radius:50%;border:3px solid var(--cyan);clip-path:polygon(0 0,100% 0,100% 55%,0 55%);opacity:0.9}
.select-modal{position:absolute;inset:0;z-index:30;display:grid;place-items:center;background:radial-gradient(800px 600px at 50% 20%, rgba(0,229,255,0.12), transparent 60%), linear-gradient(180deg, rgba(5,10,20,0.88), rgba(5,10,20,0.92));backdrop-filter:blur(6px);padding:20px}
.card{width:min(1060px,100%);background:rgba(10,18,32,0.92);border:1px solid rgba(255,255,255,0.08);border-radius:18px;overflow:hidden;box-shadow:0 30px 80px rgba(0,0,0,0.6)}
.card-head{padding:22px 22px 0 22px;display:flex;justify-content:space-between;align-items:start;gap:16px}
.card-head h2{font-family:'Orbitron',monospace;font-size:22px;letter-spacing:0.1em}
.card-head p{font-family:'JetBrains Mono',monospace;font-size:11px;color:#8fb0c3;margin-top:6px;max-width:520px;line-height:1.6}
.fleet{display:grid;grid-template-columns:repeat(3,1fr);gap:14px;padding:18px}
.ship{border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);border-radius:14px;padding:14px;cursor:pointer;transition:all 0.2s;position:relative;overflow:hidden}
.ship.active{border-color:var(--cyan);background:linear-gradient(180deg, rgba(0,229,255,0.14), rgba(255,255,255,0.02));box-shadow:0 0 0 1px rgba(0,229,255,0.3), 0 12px 30px rgba(0,229,255,0.18)}
.ship h4{font-family:'Orbitron',monospace;font-size:12px;letter-spacing:0.12em}
.ship .role{font-family:'JetBrains Mono',monospace;font-size:10px;color:#7fa0b6;letter-spacing:0.12em;margin-top:4px}
.specs{display:grid;grid-template-columns:1fr 1fr;gap:8px;margin-top:12px}
.specs div{background:rgba(0,0,0,0.22);border:1px solid rgba(255,255,255,0.06);border-radius:8px;padding:8px}
.specs label{font-family:'JetBrains Mono',monospace;font-size:9px;color:#7fa0b6;letter-spacing:0.08em}
.specs strong{font-family:'Orbitron',monospace;font-size:11px;display:block;margin-top:2px}
.modes{display:flex;gap:10px;padding:0 18px 18px 18px}
.mode{flex:1;padding:14px;border-radius:12px;border:1px solid rgba(255,255,255,0.08);background:rgba(255,255,255,0.03);cursor:pointer}
.mode.active{border-color:var(--orange);background:rgba(255,106,0,0.12)}
.mode b{font-family:'Orbitron',monospace;font-size:11px;letter-spacing:0.1em}
.mode p{font-family:'JetBrains Mono',monospace;font-size:10px;color:#8fb0c3;margin-top:6px;line-height:1.5}
.card-foot{padding:14px 18px;display:flex;justify-content:space-between;align-items:center;border-top:1px solid rgba(255,255,255,0.06);background:rgba(0,0,0,0.18)}
.hint{font-family:'JetBrains Mono',monospace;font-size:10px;color:#8fb0c3;line-height:1.6}
.warning{position:absolute;left:50%;top:88px;transform:translateX(-50%);background:rgba(255,40,40,0.92);color:white;font-family:'Orbitron',monospace;font-size:11px;letter-spacing:0.16em;padding:10px 16px;border-radius:999px;display:none;z-index:10;box-shadow:0 8px 24px rgba(255,0,0,0.4)}
.score{position:absolute;top:70px;left:50%;transform:translateX(-50%);z-index:6;background:rgba(6,10,18,0.72);border:1px solid rgba(255,255,255,0.08);border-radius:999px;padding:8px 14px;font-family:'JetBrains Mono',monospace;font-size:11px;display:flex;gap:14px;backdrop-filter:blur(8px)}
@media(max-width:980px){
.hud{grid-template-columns:1fr;grid-template-rows:auto 320px auto}
.left,.right{order:2}
.center-hud{order:1}
.fleet{grid-template-columns:1fr}
.topbar{height:auto;padding:10px;flex-wrap:wrap;gap:8px}
.hud{inset:92px 0 0 0}
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
<div class="topbar">
<div class="brand">
<div class="logo"><span>AA</span></div>
<div>
<h1>APEX AERO</h1>
<p>PHYSICS SIMULATOR • BUILD 4.2.1</p>
</div>
<div style="margin-left:14px;display:flex;gap:8px">
<span class="pill" id="weather">WIND 12KT • VIS 10KM</span>
<span class="pill" id="fps">60 FPS</span>
</div>
</div>
<div class="top-actions">
<button class="pill" id="btnAudio">🔊 AUDIO ON</button>
<button class="pill" id="btnReset">↺ RESET</button>
<button class="btn-primary" id="btnLaunch">ENGAGE ▸</button>
</div>
</div>
<div class="warning" id="stallWarn">⚠ STALL — NOSE DOWN • THROTTLE UP</div>
<div class="score" id="scoreBar" style="display:none">
<span id="scoreTxt">SCORE 0000</span><span style="opacity:0.35">|</span><span id="modeTxt">MODE: CIRCUIT</span><span style="opacity:0.35">|</span><span id="targetTxt">TGT 1/3</span>
</div>
<div class="hud" id="hud">
<div class="left">
<div class="panel">
<div class="panel-head"><h3>FLIGHT DATA</h3><div class="dot"></div></div>
<div class="readouts">
<div class="mini"><label>AIRSPEED</label><div class="val" id="vAir">0</div><div class="unit">KNOTS</div><div class="bar"><i id="bAir"></i></div></div>
<div class="mini"><label>ALTITUDE (MSL)</label><div class="val" id="vAlt">0</div><div class="unit">FEET</div><div class="bar"><i id="bAlt" style="background:linear-gradient(90deg,#ff6a00,#ffd36a)"></i></div></div>
<div class="mini"><label>VERTICAL SPEED</label><div class="val" id="vVs">0</div><div class="unit">FT/MIN</div></div>
<div class="mini"><label>G-FORCE</label><div class="val" id="vG">1.0</div><div class="unit">LOAD</div></div>
</div>
<div class="horizon" id="horizon">
<div class="horizon-line"></div>
<div class="ladder"><div class="ladder-inner" id="ladder">
<span style="top:18px;width:56px"></span>
<span style="top:42px"></span>
<span style="top:66px;width:22px"></span>
<span style="top:90px"></span>
<span style="top:114px;width:56px"></span>
</div></div>
<div style="position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);width:36px;height:36px;border:2px solid rgba(255,255,255,0.9);border-left-color:transparent;border-right-color:transparent;border-radius:50%"></div>
<div style="position:absolute;bottom:8px;left:10px;font-family:'JetBrains Mono',monospace;font-size:9px;color:rgba(255,255,255,0.7)">PITCH <b id="vPitch" style="color:#fff">0°</b> • ROLL <b id="vRoll" style="color:#fff">0°</b></div>
<div style="position:absolute;bottom:8px;right:10px;font-family:'JetBrains Mono',monospace;font-size:9px;color:#ff6a00">AOA <b id="vAoA" style="color:#fff">0°</b></div>
</div>
<div style="padding:10px 12px;display:flex;gap:8px">
<div style="flex:1;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06);border-radius:8px;padding:8px;text-align:center"><div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:#7fa0b6">HEADING</div><div id="vHdg" style="font-family:'Orbitron',monospace;font-size:16px;font-weight:800">090°</div></div>
<div style="flex:1;background:rgba(255,255,255,0.04);border:1px solid rgba(255,255,255,0.06);border-radius:8px;padding:8px;text-align:center"><div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:#7fa0b6">WIND</div><div style="font-family:'Orbitron',monospace;font-size:12px">12 KT NW</div></div>
</div>
</div>
</div>
<div class="center-hud">
<div class="center-top">
<div class="reticle">
<div class="cross"></div>
<div class="vv" id="vv"></div>
<div style="position:absolute;left:50%;top:-10px;transform:translateX(-50%);font-family:'JetBrains Mono',monospace;font-size:9px;letter-spacing:0.14em;color:var(--cyan)">APEX HUD</div>
<div style="position:absolute;bottom:-18px;left:50%;transform:translateX(-50%);font-family:'JetBrains Mono',monospace;font-size:9px;color:rgba(255,255,255,0.6);white-space:nowrap">• CLICK TO FIRE • SPACE BRAKE •</div>
</div>
</div>
<div class="bottom-strip">
<div class="kbd"><b>W</b><b>S</b> PITCH <b>A</b><b>D</b> ROLL <b>Q</b><b>E</b> YAW</div>
<div class="kbd"><b>SHIFT</b> THR+ <b>CTRL</b> THR- <b>R</b> GEAR</div>
</div>
</div>
<div class="right">
<div class="panel throttle">
<div style="display:flex;justify-content:space-between;align-items:center"><h3 style="font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.14em;color:#8fb0c3">THRUST</h3><span id="vThr" style="font-family:'Orbitron',monospace;font-size:12px;color:var(--orange)">0%</span></div>
<div style="display:flex;gap:12px;margin-top:10px">
<div class="scale"><div class="scale-fill" id="thrFill" style="height:35%"></div></div>
<div style="flex:1">
<div style="display:flex;justify-content:space-between;font-family:'JetBrains Mono',monospace;font-size:9px;color:#7fa0b6"><span>IDLE</span><span>MIL</span><span>AB</span></div>
<div style="height:6px;background:rgba(255,255,255,0.08);border-radius:999px;overflow:hidden;margin-top:6px"><i id="rpmBar" style="display:block;height:100%;width:35%;background:linear-gradient(90deg,#ff6a00,#ffd36a)"></i></div>
<div style="margin-top:10px;display:grid;grid-template-columns:1fr 1fr;gap:8px">
<div style="background:rgba(255,255,255,0.04);border-radius:8px;padding:8px"><div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:#7fa0b6">RPM</div><div id="vRpm" style="font-family:'Orbitron',monospace;font-size:13px">3400</div></div>
<div style="background:rgba(255,255,255,0.04);border-radius:8px;padding:8px"><div style="font-family:'JetBrains Mono',monospace;font-size:9px;color:#7fa0b6">FUEL FLOW</div><div id="vFuel" style="font-family:'Orbitron',monospace;font-size:13px">0.4</div></div>
</div>
<div style="margin-top:10px;font-family:'JetBrains Mono',monospace;font-size:10px;color:#8fb0c3">FLAPS <b id="vFlap" style="color:#fff">UP</b> • GEAR <b id="vGear" style="color:#18e07a">DOWN</b></div>
</div>
</div>
</div>
<div class="panel" style="padding:12px">
<h3 style="font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.14em;color:#8fb0c3">AIRCRAFT LOADOUT</h3>
<div style="display:flex;gap:10px;margin-top:10px;align-items:center">
<div class="g-ring" id="gRing"><span id="gBig" style="font-family:'Orbitron',monospace;font-weight:800">1.0</span></div>
<div style="font-family:'JetBrains Mono',monospace;font-size:10px;line-height:1.6;color:#9fb8c9">G-LIMIT 9.0<br><span id="loadTxt" style="color:#18e07a">● NOMINAL</span><br><span style="opacity:0.6">AOA CRIT 15°</span></div>
</div>
<div style="margin-top:12px;display:flex;gap:8px">
<span style="flex:1;text-align:center;padding:8px;border-radius:8px;background:rgba(0,229,255,0.12);border:1px solid rgba(0,229,255,0.2);font-family:'JetBrains Mono',monospace;font-size:10px">MACH <b id="vMach">0.00</b></span>
<span style="flex:1;text-align:center;padding:8px;border-radius:8px;background:rgba(255,106,0,0.12);border:1px solid rgba(255,106,0,0.22);font-family:'JetBrains Mono',monospace;font-size:10px">DENSITY <b id="vRho">1.22</b></span>
</div>
</div>
</div>
</div>
<div class="select-modal" id="modal">
<div class="card">
<div class="card-head">
<div>
<div style="font-family:'JetBrains Mono',monospace;font-size:10px;letter-spacing:0.18em;color:var(--cyan)">APEX AERO SYSTEMS • SELECT CONFIGURATION</div>
<h2>CHOOSE YOUR AIRFRAME</h2>
<p>Each airframe uses a distinct Newtonian flight model. Thrust-to-weight, lift slope, drag polar and inertia are simulated per frame with realistic stall, density and momentum effects.</p>
</div>
<div style="text-align:right;font-family:'JetBrains Mono',monospace;font-size:10px;color:#8fb0c3">PHYSICS: CUSTOM NEWTONIAN<br>RENDER: THREE.JS PBR</div>
</div>
<div class="fleet" id="fleet">
<div class="ship active" data-id="kestrel">
<div style="height:56px;background:linear-gradient(90deg,rgba(0,229,255,0.18),transparent);border-radius:10px;display:grid;place-items:center;font-size:28px">✈</div>
<h4>KESTREL T-1</h4><div class="role">TRAINER • AGILE</div>
<div class="specs">
<div><label>THRUST</label><strong>48 kN</strong></div><div><label>MASS</label><strong>3,200 kg</strong></div>
<div><label>MAX SPEED</label><strong>412 KT</strong></div><div><label>CL MAX</label><strong>1.45</strong></div>
</div>
<div style="margin-top:10px;font-family:'JetBrains Mono',monospace;font-size:10px;color:#9fb8c9">Forgiving stall, light inertia. Perfect for takeoff/landing precision.</div>
</div>
<div class="ship" data-id="falcon">
<div style="height:56px;background:linear-gradient(90deg,rgba(255,106,0,0.18),transparent);border-radius:10px;display:grid;place-items:center;font-size:28px">🛩</div>
<h4>FALCON X</h4><div class="role">AIR SUPERIORITY • 9G</div>
<div class="specs">
<div><label>THRUST</label><strong>118 kN</strong></div><div><label>MASS</label><strong>5,800 kg</strong></div>
<div><label>MAX SPEED</label><strong>780 KT</strong></div><div><label>CL MAX</label><strong>1.65</strong></div>
</div>
<div style="margin-top:10px;font-family:'JetBrains Mono',monospace;font-size:10px;color:#9fb8c9">High wing loading, savage acceleration. Built for dogfight energy fighting.</div>
</div>
<div class="ship" data-id="hauler">
<div style="height:56px;background:linear-gradient(90deg,rgba(120,180,255,0.18),transparent);border-radius:10px;display:grid;place-items:center;font-size:28px">🛫</div>
<h4>STRATO C-9</h4><div class="role">HEAVY LIFT • STABLE</div>
<div class="specs">
<div><label>THRUST</label><strong>82 kN</strong></div><div><label>MASS</label><strong>9,400 kg</strong></div>
<div><label>MAX SPEED</label><strong>310 KT</strong></div><div><label>CL MAX</label><strong>1.85</strong></div>
</div>
<div style="margin-top:10px;font-family:'JetBrains Mono',monospace;font-size:10px;color:#9fb8c9">Heavy inertia, huge lift. Requires long runway and careful energy management.</div>
</div>
</div>
<div class="modes" id="modes">
<div class="mode active" data-mode="circuit"><b>◉ CIRCUIT — TAKEOFF / LANDING</b><p>Take off from RWY 27, fly the box pattern, manage AoA and sink rate for a greased landing. Stall and ground collision modeled.</p></div>
<div class="mode" data-mode="dogfight"><b>◎ DOGFIGHT — HIGH-G MANEUVER</b><p>Engage a maneuvering bandit. Lead the pipper, manage corner speed and G-load. First to 5 kills wins.</p></div>
</div>
<div class="card-foot">
<div class="hint">CONTROLS: <b>W/S</b> Pitch • <b>A/D</b> Roll • <b>Q/E</b> Yaw • <b>SHIFT/CTRL</b> Throttle • Mouse drag look • Click fire</div>
<button class="btn-primary" id="btnStart">INITIALIZE FLIGHT →</button>
</div>
</div>
</div>
<script type="module">
import * as THREE from 'three';
const canvas = document.getElementById('canvas');
const modal = document.getElementById('modal');
const scoreBar = document.getElementById('scoreBar');
const aircraftDefs = {
kestrel:{ name:'KESTREL T-1', mass:3200, thrust:48000, wingArea:18, cl0:0.22, clAlpha:5.2, cd0:0.021, k:0.045, maxCl:1.45, color:0x00e5ff },
falcon:{ name:'FALCON X', mass:5800, thrust:118000, wingArea:24, cl0:0.25, clAlpha:5.6, cd0:0.027, k:0.039, maxCl:1.65, color:0xff6a00 },
hauler:{ name:'STRATO C-9', mass:9400, thrust:82000, wingArea:42, cl0:0.32, clAlpha:5.0, cd0:0.032, k:0.052, maxCl:1.85, color:0x8fb8ff }
};
let selectedId='kestrel';
let mode='circuit';
// UI selects
document.querySelectorAll('.ship').forEach(el=>{
el.addEventListener('click',()=>{
document.querySelectorAll('.ship').forEach(s=>s.classList.remove('active'));
el.classList.add('active'); selectedId=el.dataset.id;
});
});
document.querySelectorAll('.mode').forEach(el=>{
el.addEventListener('click',()=>{
document.querySelectorAll('.mode').forEach(s=>s.classList.remove('active'));
el.classList.add('active'); mode=el.dataset.mode;
});
});
const scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x0a1a2e, 0.0009);
const renderer = new THREE.WebGLRenderer({canvas, antialias:true, alpha:false});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.shadowMap.enabled = true;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.15;
const camera = new THREE.PerspectiveCamera(68, innerWidth/innerHeight, 0.1, 40000);
window.addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
renderer.setSize(innerWidth, innerHeight);
scene.background = new THREE.Color(0x6fa8dc);
// sky gradient via fog + hemisphere
const hemi = new THREE.HemisphereLight(0x9fd4ff, 0x0a1a0a, 1.2);
scene.add(hemi);
const dir = new THREE.DirectionalLight(0xffffff, 1.6);
dir.position.set(400,900,300); dir.castShadow=true; dir.shadow.mapSize.set(2048,2048);
scene.add(dir);
// terrain procedural
function pseudoNoise(x,z){
return Math.sin(x*0.0017)*Math.cos(z*0.0013)*42 + Math.sin(x*0.0031+z*0.002)*18 + Math.cos(x*0.008 - z*0.005)*9 + Math.sin(x*0.02)*2;
}
const TSize=7000, TSeg=220;
const tGeo = new THREE.PlaneGeometry(TSize, TSize, TSeg, TSeg);
const pos = tGeo.attributes.position;
for(let i=0;i<pos.count;i++){
const x=pos.getX(i), y=pos.getY(i);
const h = pseudoNoise(x,y);
// flatten runway area
const d = Math.sqrt(x*x + y*y);
let fh=h;
if(Math.abs(y)<70 && x>-1200 && x<1200) fh = pseudoNoise(x,y)*0.08; // runway strip
pos.setZ(i, fh);
}
tGeo.computeVertexNormals();
const tMat = new THREE.MeshStandardMaterial({color:0x4a6b3a, roughness:0.92, metalness:0.02, flatShading:false});
const terrain = new THREE.Mesh(tGeo, tMat);
terrain.rotation.x = -Math.PI/2;
terrain.receiveShadow=true;
scene.add(terrain);
// runway
const runwayGeo = new THREE.BoxGeometry(2400,2,120);
const runwayMat = new THREE.MeshStandardMaterial({color:0x252b33, roughness:0.6});
const runway = new THREE.Mesh(runwayGeo, runwayMat);
runway.position.set(0,0.8,0); runway.receiveShadow=true;
scene.add(runway);
const lineMat = new THREE.MeshStandardMaterial({color:0xffffff});
for(let i=-1100;i<=1100;i+=80){
const m=new THREE.Mesh(new THREE.BoxGeometry(36,0.6,4), lineMat); m.position.set(i,1.5,0); scene.add(m);
}
// mountains decoration (low poly cones)
for(let i=0;i<26;i++){
const r= 1800+Math.random()*1600, ang=Math.random()*Math.PI*2;
const x=Math.cos(ang)*r, z=Math.sin(ang)*r;
const h= 80+Math.random()*220;
const cone=new THREE.Mesh(new THREE.ConeGeometry(90+Math.random()*70, h, 6), new THREE.MeshStandardMaterial({color:0x6b7d6e}));
cone.position.set(x, h/2 + pseudoNoise(x,z)-6, z); scene.add(cone);
}
// clouds
const cloudGroup=new THREE.Group();
for(let i=0;i<18;i++){
const g=new THREE.Group();
for(let k=0;k<4;k++){
const s=new THREE.Mesh(new THREE.SphereGeometry(90+Math.random()*70, 10,10), new THREE.MeshStandardMaterial({color:0xffffff, transparent:true, opacity:0.42}));
s.position.set((Math.random()-0.5)*220, (Math.random()-0.5)*40, (Math.random()-0.5)*120); g.add(s);
}
g.position.set((Math.random()-0.5)*5000, 900+Math.random()*700, (Math.random()-0.5)*5000); cloudGroup.add(g);
}
scene.add(cloudGroup);
// checkpoints / gates for circuit
const gates=[];
function makeGate(x,z){
const g=new THREE.Mesh(new THREE.TorusGeometry(160,10,10,24), new THREE.MeshStandardMaterial({color:0x00e5ff, emissive:0x00e5ff, emissiveIntensity:0.7, transparent:true, opacity:0.85}));
g.rotation.y=Math.PI/2; g.position.set(x, 260, z); scene.add(g); gates.push(g);
}
makeGate(1200, 0);
makeGate(1600, 1100);
makeGate(-400, 1400);
makeGate(-1200, 0);
// enemy for dogfight
let enemy=null, enemyVel=new THREE.Vector3();
function spawnEnemy(){
if(enemy) scene.remove(enemy);
enemy=new THREE.Group();
const body=new THREE.Mesh(new THREE.CylinderGeometry(6,8,58,10), new THREE.MeshStandardMaterial({color:0xff2a2a}));
body.rotation.z=Math.PI/2; enemy.add(body);
const wing=new THREE.Mesh(new THREE.BoxGeometry(44,2,22), new THREE.MeshStandardMaterial({color:0x7a0a0a})); wing.position.y=-2; enemy.add(wing);
enemy.position.set(900+Math.random()*600, 520+Math.random()*240, (Math.random()-0.5)*800);
enemyVel.set(-60 -Math.random()*40, 0, (Math.random()-0.5)*30);
scene.add(enemy);
}
// aircraft group (player)
let player = new THREE.Group();
function buildPlayer(def){
player.clear();
const fus = new THREE.Mesh(new THREE.CylinderGeometry(5,7,62,12), new THREE.MeshStandardMaterial({color:def.color, metalness:0.35, roughness:0.45}));
fus.rotation.z=Math.PI/2; fus.castShadow=true; player.add(fus);
const nose=new THREE.Mesh(new THREE.ConeGeometry(7,18,12), new THREE.MeshStandardMaterial({color:def.color})); nose.rotation.z=-Math.PI/2; nose.position.x=34; player.add(nose);
const wing=new THREE.Mesh(new THREE.BoxGeometry(28,1.6,56), new THREE.MeshStandardMaterial({color:0xe6f0ff, metalness:0.1, roughness:0.5})); wing.position.set(-4, -3, 0); player.add(wing);
const tailV=new THREE.Mesh(new THREE.BoxGeometry(14,18,1.6), new THREE.MeshStandardMaterial({color:def.color})); tailV.position.set(-28,6,0); player.add(tailV);
const tailH=new THREE.Mesh(new THREE.BoxGeometry(16,1.2,18), new THREE.MeshStandardMaterial({color:0xd9e8ff})); tailH.position.set(-30,0,0); player.add(tailH);
const can=new THREE.Mesh(new THREE.CapsuleGeometry(4.2,14,4,8), new THREE.MeshStandardMaterial({color:0x0b1a28, metalness:0.6, roughness:0.25})); can.rotation.z=Math.PI/2; can.position.set(10,5,0); player.add(can);
}
scene.add(player);
// physics state
let state={
pos:new THREE.Vector3( -1050, 18, 8),
vel:new THREE.Vector3( 18, 0, 0),
quat:new THREE.Quaternion(),
euler:new THREE.Euler(0,0,0,'YXZ'),
throttle:0.35,
aoa:0,
stall:false,
gear:true,
flaps:0, //0 up 1 down
onGround:true,
rpm:3200,
score:0,
gateIdx:0,
kills:0
};
let keys={};
addEventListener('keydown',e=>{
keys[e.key.toLowerCase()]=true;
if(e.key===' ') {keys[' ']=true; e.preventDefault();}
if(e.code==='Space') keys[' ']=true;
});
addEventListener('keyup',e=>{
keys[e.key.toLowerCase()]=false;
if(e.key===' ') keys[' ']=false;
if(e.code==='Space') keys[' ']=false;
});
let mouseDown=false, mouseX=0;
canvas.addEventListener('mousedown',e=>{mouseDown=true; if(mode==='dogfight' && started) fire();});
canvas.addEventListener('mouseup',()=>mouseDown=false);
canvas.addEventListener('mousemove',e=>{mouseX = (e.clientX/innerWidth-0.5)*2});
let started=false;
let audioCtx, osc, gain, windGain, windOsc;
let audioOn=true;
function initAudio(){
if(audioCtx) return;
audioCtx=new (window.AudioContext||window.webkitAudioContext)();
osc=audioCtx.createOscillator(); gain=audioCtx.createGain();
osc.type='sawtooth'; osc.frequency.value=80; gain.gain.value=0.0001;
osc.connect(gain).connect(audioCtx.destination); osc.start();
windOsc=audioCtx.createOscillator(); windGain=audioCtx.createGain();
windOsc.type='triangle'; windOsc.frequency.value=180; windGain.gain.value=0;
windOsc.connect(windGain).connect(audioCtx.destination); windOsc.start();
}
function updateAudio(airspeed, throttle){
if(!audioCtx || !audioOn) return;
const f = 60 + throttle*180 + airspeed*0.35;
osc.frequency.linearRampToValueAtTime(f, audioCtx.currentTime+0.08);
gain.gain.linearRampToValueAtTime(0.0008 + throttle*0.045, audioCtx.currentTime+0.08);
windGain.gain.linearRampToValueAtTime(Math.min(0.06, airspeed*0.00018), audioCtx.currentTime+0.1);
windOsc.frequency.linearRampToValueAtTime(120+airspeed*0.6, audioCtx.currentTime+0.1);
}
document.getElementById('btnAudio').addEventListener('click',()=>{
audioOn=!audioOn;
document.getElementById('btnAudio').textContent = audioOn ? '🔊 AUDIO ON' : '🔇 AUDIO OFF';
if(!audioOn && gain) gain.gain.value=0.00001;
if(audioOn) initAudio();
});
function resetState(){
const d=aircraftDefs[selectedId];
state.pos.set(-1050, 10, (Math.random()-0.5)*6);
state.vel.set(14,0,0);
state.euler.set(0, -Math.PI/2 + 1.57, 0); // facing +X down runway ; correction
state.euler.set(0, 0, 0);
state.quat.identity();
state.throttle=0.34; state.aoa=0; state.stall=false; state.onGround=true; state.gear=true; state.flaps=0; state.score=0; state.gateIdx=0; state.kills=0;
buildPlayer(d);
gates.forEach(g=>{g.material.color.set(0x00e5ff); g.material.emissive.set(0x00e5ff);});
if(mode==='dogfight' && started) spawnEnemy();
scoreBar.style.display = started? 'flex':'none';
}
document.getElementById('btnReset').addEventListener('click', resetState);
// start
document.getElementById('btnStart').addEventListener('click',()=>{
modal.style.display='none';
started=true; scoreBar.style.display='flex';
initAudio();
resetState();
document.getElementById('modeTxt').textContent = mode==='dogfight' ? 'MODE: DOGFIGHT' : 'MODE: CIRCUIT';
});
document.getElementById('btnLaunch').addEventListener('click',()=>{
if(!started){ modal.style.display='grid'; }
else { resetState(); }
});
// bullets
const bullets=[];
function fire(){
if(!started) return;
const b=new THREE.Mesh(new THREE.SphereGeometry(3,6,6), new THREE.MeshStandardMaterial({color:0xffff6a, emissive:0xffaa00, emissiveIntensity:1}));
b.position.copy(state.pos);
const fwd=new THREE.Vector3(1,0,0).applyQuaternion(state.quat);
b.userData.vel = fwd.multiplyScalar(520).add(state.vel.clone());
b.userData.life=1.6;
scene.add(b); bullets.push(b);
// muzzle flash
if(audioCtx && audioOn){
const o=audioCtx.createOscillator(), g=audioCtx.createGain();
o.frequency.value=880; g.gain.value=0.12; o.connect(g).connect(audioCtx.destination); o.start(); g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime+0.12); o.stop(audioCtx.currentTime+0.13);
}
}
// terrain height function for collision
function terrainHeight(x,z){
if(Math.abs(z)<70 && x>-1200 && x<1200) return 0.5;
return pseudoNoise(x,z);
}
let last=performance.now();
let frame=0;
function animate(){
requestAnimationFrame(animate);
const now=performance.now(); const dt=Math.min(0.033, (now-last)/1000); last=now;
frame++;
// input -> controls
const d=aircraftDefs[selectedId];
if(started){
// throttle
if(keys['shift']) state.throttle = Math.min(1, state.throttle + dt*0.55);
if(keys['control']) state.throttle = Math.max(0, state.throttle - dt*0.7);
if(keys['r']){ if(frame%12===0) state.gear=!state.gear; keys['r']=false; }
if(keys['f']){ if(frame%12===0) state.flaps = state.flaps?0:1; keys['f']=false; }
// control surfaces - torque depending on airspeed and throttle
const airspeed = state.vel.length(); // m/s ~ knots conversion later
const controlEff = THREE.MathUtils.clamp( (airspeed/25), 0.15, 1.6);
const pitchIn = (keys['w']||keys['arrowup']? -1:0) + (keys['s']||keys['arrowdown']? 1:0);
const rollIn = (keys['a']? -1:0) + (keys['d']? 1:0);
const yawIn = (keys['q']? -1:0) + (keys['e']? 1:0);
// apply angular velocity via euler integration (with inertia)
const pitchRate = pitchIn * 0.9 * controlEff;
const rollRate = rollIn * 1.15 * controlEff + mouseX*0.7*controlEff*0.6;
const yawRate = yawIn * 0.55 * controlEff;
state.euler.x += pitchRate*dt;
state.euler.z += rollRate*dt;
state.euler.y += yawRate*dt;
// auto return to level a bit, stall coupling
state.euler.x = THREE.MathUtils.clamp(state.euler.x, -0.55, 0.55);
state.euler.z = THREE.MathUtils.clamp(state.euler.z, -1.0, 1.0);
// damping
state.euler.x *= (1 - 0.45*dt);
// update quat
state.quat.setFromEuler(state.euler);
player.quaternion.copy(state.quat);
// --- physics ---
const altitude = Math.max(0, state.pos.y - terrainHeight(state.pos.x, state.pos.z));
const rho = 1.225 * Math.exp(-altitude*0.3048 / 8500); // altitude feet->m
const speed = Math.max(0.1, airspeed);
const q = 0.5 * rho * speed * speed;
// angle of attack = angle between forward and velocity
const fwd = new THREE.Vector3(1,0,0).applyQuaternion(state.quat);
const velDir = state.vel.clone().normalize();
let aoaRad = Math.acos(THREE.MathUtils.clamp(fwd.dot(velDir), -1,1));
// sign by pitch diff approx
const up = new THREE.Vector3(0,1,0).applyQuaternion(state.quat);
const aoaSigned = fwd.dot(velDir) < 0.98 ? aoaRad * Math.sign(up.dot(velDir) - 0.02) : 0;
state.aoa = THREE.MathUtils.radToDeg(aoaRad);
// lift coeff with stall
const alphaDeg = state.aoa + state.euler.x*18; // pitch contributes
let Cl = d.cl0 + THREE.MathUtils.degToRad(alphaDeg) * d.clAlpha;
// flap boost
if(state.flaps) Cl += 0.45;
// stall
const stallAoA = 15 - (state.flaps?2:0);
if(alphaDeg > stallAoA){
const excess = alphaDeg - stallAoA;
Cl = d.maxCl - excess*0.08; // sharp drop
state.stall = true;
} else if(alphaDeg < -8){
Cl = d.cl0 + THREE.MathUtils.degToRad(alphaDeg)*d.clAlpha*0.6;
state.stall = alphaDeg < -12;
} else {
state.stall = false;
}
Cl = THREE.MathUtils.clamp(Cl, -0.6, d.maxCl);
const Cd = d.cd0 + d.k * Cl*Cl + (state.gear?0.012:0) + (state.flaps?0.018:0) + (state.stall?0.04:0);
const liftMag = Cl * q * d.wingArea;
const dragMag = Cd * q * 9.2; // ref area approx
const liftDir = up.clone().multiplyScalar(Math.max(0, liftMag));
// drag opposite velocity
const dragDir = velDir.clone().multiplyScalar(-dragMag);
// thrust along forward
const thrustMag = state.throttle * d.thrust * (0.92 + rho/1.225*0.08); // density effect
const thrust = fwd.clone().multiplyScalar(thrustMag);
// weight
const weight = new THREE.Vector3(0, -d.mass*9.81, 0);
// sum forces
const force = new THREE.Vector3().add(liftDir).add(dragDir).add(thrust).add(weight);
// ground friction / normal
const groundY = terrainHeight(state.pos.x, state.pos.z) + (state.gear? 6.5 : 2.5);
if(state.pos.y <= groundY + 0.2){
state.onGround=true;
if(state.vel.y < 0) state.vel.y *= 0.2;
// friction
force.x -= state.vel.x * 420 * (state.gear?1:0.3);
force.z -= state.vel.z * 420 * (state.gear?1:0.3);
// prevent sinking
if(force.y < 0 && liftMag < d.mass*9.81*0.96) force.y = Math.max(force.y, 0);
} else state.onGround=false;
const accel = force.clone().divideScalar(d.mass);
// integrate
state.vel.add(accel.multiplyScalar(dt));
// momentum carry - limit vertical if stall
if(state.stall) state.vel.y -= 3.2*dt;
state.pos.add(state.vel.clone().multiplyScalar(dt));
// clamp ground
if(state.pos.y < groundY) { state.pos.y = groundY; if(state.vel.y<0) state.vel.y=0; }
// brake with space
if(keys[' ']){
state.vel.multiplyScalar(0.992);
}
player.position.copy(state.pos);
// enemy logic
if(mode==='dogfight' && enemy){
enemy.position.add(enemyVel.clone().multiplyScalar(dt));
enemyVel.y = Math.sin(now*0.0006)*6;
// simple evade when close
const toPlayer = state.pos.clone().sub(enemy.position);
const dist = toPlayer.length();
if(dist < 420){
enemyVel.add(toPlayer.normalize().multiplyScalar(-42*dt));
enemyVel.clampLength(70, 210);
}
// wrap
if(enemy.position.x < -2000) enemy.position.x=2000;
if(enemy.position.x > 2000) enemy.position.x=-2000;
if(enemy.position.z < -2000) enemy.position.z=2000;
if(enemy.position.z > 2000) enemy.position.z=-2000;
enemy.lookAt(enemy.position.clone().add(enemyVel));
// gate pulse
}
// bullets
for(let i=bullets.length-1;i>=0;i--){
const b=bullets[i];
b.position.add(b.userData.vel.clone().multiplyScalar(dt));
b.userData.vel.y -= 18*dt;
b.userData.life -= dt;
if(mode==='dogfight' && enemy && b.position.distanceTo(enemy.position) < 42){
scene.remove(b); bullets.splice(i,1);
state.kills++; state.score+=250;
// explosion flash
const ex=new THREE.Mesh(new THREE.SphereGeometry(28,10,10), new THREE.MeshStandardMaterial({color:0xffaa00, emissive:0xff5500, transparent:true, opacity:0.9}));
ex.position.copy(enemy.position); scene.add(ex);
setTimeout(()=>scene.remove(ex),220);
spawnEnemy();
updateScore();
continue;
}
if(b.userData.life<=0 || b.position.y<0){ scene.remove(b); bullets.splice(i,1); }
}
// circuit gates
if(mode==='circuit' && gates[state.gateIdx]){
const g=gates[state.gateIdx];
if(state.pos.distanceTo(g.position) < 190){
g.material.color.set(0x19ff7a); g.material.emissive.set(0x19ff7a);
state.gateIdx=(state.gateIdx+1)%gates.length;
state.score+=500; updateScore();
// if completed lap, require landing
if(state.gateIdx===0 && altitude<90 && Math.abs(state.vel.length())<90 && state.onGround){
state.score+=1500; updateScore();
}
}
}
updateScore();
// HUD
const knots = Math.round(state.vel.length()*1.94384);
const altFt = Math.round(state.pos.y*3.28084);
const vs = Math.round(state.vel.y*196.85);
const gLoad = ( (liftMag/(d.mass*9.81)) ).toFixed(1);
document.getElementById('vAir').textContent=knots;
document.getElementById('vAlt').textContent=altFt;
document.getElementById('vVs').textContent=(vs>0?'+':'')+vs;
document.getElementById('vG').textContent=gLoad;
document.getElementById('gBig').textContent=gLoad;
document.getElementById('vPitch').textContent=Math.round(THREE.MathUtils.radToDeg(state.euler.x))+'°';
document.getElementById('vRoll').textContent=Math.round(THREE.MathUtils.radToDeg(state.euler.z))+'°';
document.getElementById('vAoA').textContent=Math.round(state.aoa)+'°';
document.getElementById('vHdg').textContent=String(Math.round((THREE.MathUtils.radToDeg(state.euler.y)+360)%360)).padStart(3,'0')+'°';
document.getElementById('vThr').textContent=Math.round(state.throttle*100)+'%';
document.getElementById('vRho').textContent=rho.toFixed(2);
document.getElementById('vMach').textContent=(state.vel.length()/340).toFixed(2);
document.getElementById('vRpm').textContent=Math.round(2600+state.throttle*5200 + (state.stall? -400:0));
document.getElementById('vFuel').textContent=(0.32+state.throttle*2.4).toFixed(1);
document.getElementById('vGear').textContent=state.gear?'DOWN':'UP';
document.getElementById('vGear').style.color=state.gear?'#18e07a':'#ff6a00';
document.getElementById('vFlap').textContent=state.flaps?'DOWN':'UP';
document.getElementById('bAir').style.width=THREE.MathUtils.clamp(knots/520*100,4,100)+'%';
document.getElementById('bAlt').style.width=THREE.MathUtils.clamp(altFt/3500*100,4,100)+'%';
document.getElementById('thrFill').style.height=(state.throttle*100)+'%';
document.getElementById('rpmBar').style.width=(state.throttle*100)+'%';
document.getElementById('loadTxt').textContent = parseFloat(gLoad)>7 ? '● HIGH-G' : parseFloat(gLoad)>3? '● MANEUVER' : '● NOMINAL';
document.getElementById('loadTxt').style.color = parseFloat(gLoad)>7 ? '#ff3b3b' : '#18e07a';
// artificial horizon tilt
document.getElementById('ladder').style.transform=`rotate(${-THREE.MathUtils.radToDeg(state.euler.z)}deg) translateY(${ -THREE.MathUtils.radToDeg(state.euler.x)*2 }px)`;
// velocity vector indicator offset from forward vs velocity
const vv = document.getElementById('vv');
const velAng = Math.atan2(state.vel.z, state.vel.x);
const fwdAng = state.euler.y;
const diff = THREE.MathUtils.clamp((velAng - fwdAng)*220, -70,70);
vv.style.transform=`translate(-50%,-50%) translate(${diff}px, ${ -state.vel.y*1.4 }px)`;
document.getElementById('stallWarn').style.display = state.stall ? 'block' : 'none';
if(state.stall) document.getElementById('horizon').style.boxShadow='inset 0 0 0 2px #ff2a2a';
else document.getElementById('horizon').style.boxShadow='none';
updateAudio(knots*0.514, state.throttle);
// crash check
if(!state.onGround && state.pos.y < terrainHeight(state.pos.x, state.pos.z)+4){
// hard landing
state.vel.multiplyScalar(0.4); state.pos.y = terrainHeight(state.pos.x, state.pos.z)+7;
state.score=Math.max(0, state.score-200); updateScore();
}
}
// camera chase
const chaseOffset = new THREE.Vector3(-118, 28, 0).applyQuaternion(player.quaternion);
const camPos = state.pos.clone().add(chaseOffset);
// add look ahead
const lookAt = state.pos.clone().add(new THREE.Vector3(160, 6, 0).applyQuaternion(player.quaternion));
camera.position.lerp(camPos, 0.08);
camera.lookAt(lookAt);
// slight shake at high speed / stall
if(state.stall) camera.position.y += Math.sin(now*0.02)*0.9;
cloudGroup.rotation.y += dt*0.004;
// fps
if(frame%18===0) document.getElementById('fps').textContent = Math.round(1000/(dt*1000))+' FPS';
renderer.render(scene, camera);
}
function updateScore(){
document.getElementById('scoreTxt').textContent='SCORE '+String(state.score).padStart(4,'0');
if(mode==='dogfight') document.getElementById('targetTxt').textContent='KILLS '+state.kills+'/5';
else document.getElementById('targetTxt').textContent='GATE '+(state.gateIdx+1)+'/'+gates.length;
}
animate();
resetState();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"2009bef1-8ced-4577-ae34-82a398992000","tokensIn":1178,"tokensOut":14991,"tokensTotal":16169,"cost":0.06505995,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:17:32.036Z"} -->