Flight Simulator
Tiel Coder 35B A3B GGUF MTP · full
◈Preview unavailable — try playing the result
Browser check: Preview unavailable or timed out. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 54,662 bytes · SHA-256 42f0902b7822
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Apex Aero — Flight Simulator</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { width: 100%; height: 100%; overflow: hidden; background: #05070c; font-family: 'Consolas', 'Courier New', monospace; color: #dfe9f0; }
#app { position: fixed; inset: 0; }
canvas { display: block; }
/* ---------- Menu ---------- */
#menu {
position: fixed; inset: 0; z-index: 50;
background: radial-gradient(circle at 50% 30%, #16324d 0%, #070b13 70%);
display: flex; flex-direction: column; align-items: center; justify-content: flex-start;
padding-top: 4vh; overflow-y: auto;
}
#menu h1 {
font-size: clamp(38px, 7vw, 84px); font-weight: 800; letter-spacing: 6px;
background: linear-gradient(90deg, #5fd0ff, #e9f6ff, #ff9b5f);
-webkit-background-clip: text; background-clip: text; color: transparent;
text-shadow: 0 0 40px rgba(95,208,255,0.15);
}
#menu .sub { color: #7fa3bd; letter-spacing: 4px; margin: 6px 0 26px; font-size: 14px; text-transform: uppercase; }
.row { display: flex; gap: 22px; flex-wrap: wrap; justify-content: center; max-width: 1150px; margin-bottom: 26px; }
.card {
width: 300px; background: rgba(16,30,46,0.72); border: 1px solid #26496a;
border-radius: 12px; padding: 16px 16px 14px; cursor: pointer; transition: .18s;
backdrop-filter: blur(3px);
}
.card:hover { border-color: #5fd0ff; transform: translateY(-4px); box-shadow: 0 10px 30px rgba(95,208,255,0.2); }
.card.selected { border-color: #7fe3ff; box-shadow: 0 0 0 2px #5fd0ff, 0 10px 30px rgba(95,208,255,0.3); }
.card canvas { width: 100%; height: 150px; border-radius: 8px; background: #04121f; }
.card h3 { font-size: 20px; letter-spacing: 1px; margin: 10px 0 2px; }
.card .role { font-size: 11px; color: #6fa8c8; text-transform: uppercase; letter-spacing: 2px; margin-bottom: 10px; }
.card .stat { display: flex; align-items: center; font-size: 11px; margin: 5px 0; color: #b7cddb; }
.card .stat .lbl { width: 78px; color: #83a3bb; }
.bar { flex: 1; height: 7px; background: #0c1a28; border-radius: 4px; overflow: hidden; margin-left: 8px; }
.bar > i { display: block; height: 100%; background: linear-gradient(90deg,#2f7fbf,#5fd0ff); }
.mode-title { color: #9fbfd4; letter-spacing: 3px; font-size: 13px; text-transform: uppercase; margin: 8px 0; }
#modeRow { display:flex; gap:16px; margin-bottom: 22px; }
.modebtn {
padding: 12px 22px; background: rgba(16,30,46,0.7); border: 1px solid #26496a;
border-radius: 8px; cursor: pointer; letter-spacing: 2px; font-size: 13px; font-family: inherit; color:#cfe1ef; transition:.18s;
}
.modebtn:hover { border-color:#5fd0ff; }
.modebtn.selected { border-color:#7fe3ff; background: rgba(40,90,140,0.7); box-shadow: 0 0 0 1px #5fd0ff; }
#startBtn {
padding: 16px 54px; font-size: 18px; letter-spacing: 3px; font-family: inherit; font-weight: 700;
color: #04121f; border: none; border-radius: 10px; cursor: pointer; transition: .18s;
background: linear-gradient(90deg,#5fd0ff,#a0f0ff);
box-shadow: 0 6px 24px rgba(95,208,255,0.35);
}
#startBtn:hover { transform: translateY(-2px); box-shadow: 0 10px 32px rgba(95,208,255,0.5); }
#help { max-width: 720px; text-align: center; color: #6f93ad; font-size: 12px; line-height: 1.9; margin-top: 20px; letter-spacing: .5px; }
#help b { color: #a9d0e8; }
.card .preview-canvas { width: 100%; height: 150px; border-radius: 8px; background: #04121f; overflow: hidden; }
.card .preview-canvas canvas { width: 100% !important; height: 100% !important; display: block; }
/* ---------- HUD ---------- */
#hud { position: fixed; inset: 0; z-index: 20; pointer-events: none; display: none; }
.hud-hud { position: absolute; }
#altBox { left: 18px; top: 18px; font-size: 13px; line-height: 1.7; color: #9fe0ff; text-shadow: 0 0 6px rgba(0,0,0,.9); }
#altBox b { color: #eafaff; font-size: 20px; }
#spdBox { right: 18px; top: 18px; text-align: right; font-size: 13px; line-height: 1.7; color: #9fe0ff; text-shadow: 0 0 6px rgba(0,0,0,.9); }
#spdBox b { color: #eafaff; font-size: 20px; }
#center { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%); width: 260px; height: 260px; }
#status { left: 18px; bottom: 18px; font-size: 12px; line-height: 1.7; color: #9fe0ff; max-width: 240px; text-shadow: 0 0 6px rgba(0,0,0,.9); }
#status .warn { color: #ff7a5c; font-weight: 700; }
#status .ok { color: #7dffa6; }
#msg { position: absolute; left: 50%; top: 22%; transform: translateX(-50%); font-size: 26px; letter-spacing: 3px; color: #eafaff; text-shadow: 0 0 12px rgba(0,180,255,.8); display:none; text-align:center; }
#msg small { display:block; font-size: 14px; color:#9fe0ff; letter-spacing:1px; margin-top:6px; }
#crosshair { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:34px;height:34px; }
#compass { left:50%; top:14px; transform:translateX(-50%); font-size:13px; color:#9fe0ff; letter-spacing:3px; }
#hudWin { left:50%; top:50%; transform:translate(-50%,-50%); font-size:34px; color:#7dffa6; display:none; text-align:center; text-shadow:0 0 18px rgba(0,255,150,.6); }
#hudWin small { display:block; font-size:15px; color:#9fe0ff; margin-top:10px; }
#tip { left:50%; bottom:14px; transform:translateX(-50%); font-size:11px; color:#6f93ad; letter-spacing:1px; }
#crosshair::before, #crosshair::after { content:''; position:absolute; background: rgba(120,230,255,.7); }
#crosshair::before { left:50%; top:0; width:2px; height:100%; transform:translateX(-50%); }
#crosshair::after { top:50%; left:0; height:2px; width:100%; transform:translateY(-50%); }
#hudWin2 { position:fixed; inset:0; z-index:40; display:none; align-items:center; justify-content:center; flex-direction:column; background:rgba(4,8,14,.85); }
#hudWin2 h2 { font-size:44px; letter-spacing:4px; margin-bottom:14px; }
#hudWin2 button { margin-top:20px; padding:14px 40px; font-size:16px; letter-spacing:2px; font-family:inherit; color:#04121f; border:none; border-radius:8px; cursor:pointer; background:linear-gradient(90deg,#5fd0ff,#a0f0ff); }
#hudWin2 .sub { color:#9fe0ff; font-size:14px; letter-spacing:2px; }
</style>
</head>
<body>
<div id="app"></div>
<!-- ================= MENU ================= -->
<div id="menu">
<h1>APEX AERO</h1>
<div class="sub">Realistic Flight Dynamics Simulator</div>
<div class="mode-title">Select Aircraft</div>
<div class="row" id="fleet"></div>
<div class="mode-title">Mission Profile</div>
<div id="modeRow">
<div class="modebtn selected" data-mode="0">✈ Takeoff & Landing</div>
<div class="modebtn" data-mode="1">⚔ Dogfight</div>
</div>
<button id="startBtn">LAUNCH</button>
<div id="help">
<b>W/S</b> Pitch · <b>A/D</b> Roll · <b>Q/E</b> Yaw · <b>↑/↓</b> Throttle · <b>Shift/Ctrl</b> Brake · <b>Space</b> Flaps · <b>C</b> Camera · <b>M</b> Music<br>
Realistic aerodynamics: lift &dquo; depends on air density × speed² × angle of attack. Stall below critical AoA · ground loop on hard landings · induced drag rises with lift.
</div>
</div>
<!-- ================= HUD ================= -->
<div id="hud">
<div id="status" class="hud-hud"></div>
<div id="altBox" class="hud-hud"></div>
<div id="spdBox" class="hud-hud"></div>
<div id="compass" class="hud-hud"></div>
<div id="center" class="hud-hud"><div id="crosshair"></div></div>
<div id="msg" class="hud-hud"></div>
<div id="tip" class="hud-hud"></div>
<div id="hudWin" class="hud-hud"></div>
</div>
<div id="hudWin2">
<div class="sub" id="winSub"></div>
<h2 id="winTitle"></h2>
<div class="sub" id="winStats"></div>
<button id="backBtn">BACK TO HANGAR</button>
</div>
<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>
<script type="module">
import * as THREE from 'three';
import { Sky } from 'three/addons/objects/Sky.js';
/* ============================================================
APEX AERO — custom Newtonian flight dynamics
============================================================ */
// ---------- Utilities ----------
const clamp = (v,a,b)=> v<a?a:(v>b?b:v);
const lerp = (a,b,t)=> a+(b-a)*t;
const THREE_ = THREE;
const V3 = (x,y,z)=> new THREE.Vector3(x,y,z);
let TAU = Math.PI*2;
// ---------- Aircraft definitions ----------
// Mass kg, wing m^2, max thrust N, induced-drag factor k, profile CD0,
// lift slope, stall AOA deg, max G.
const AIRCRAFT = [
{
id:'sparrow', name:'SV-7 Sparrow', role:'Fighter Jet',
color:0x8fb8d6, accent:0x2a5f8a,
mass: 7200, wing: 22, refArea: 22,
maxThrust: 20000, maxPower: 26000,
CD0: 0.026, k: 0.045, // low drag -> fast
CLa: 5.2, CLmax: 1.75, stallAoa: 17,
rollRate: 2.6, pitchRate: 1.9, yawRate: 1.3,
maxG: 9, fuelRate: 0.6,
throttleToSpeed: 1.0
},
{
id:'ranger', name:'CR-4 Ranger', role:'Cargo Transport',
color:0xc9a25f, accent:0x6b4e22,
mass: 26000, wing: 78, refArea: 78,
maxThrust: 26000, maxPower: 34000,
CD0: 0.042, k: 0.030, // big wing -> low induced drag, high lift
CLa: 4.6, CLmax: 2.1, stallAoa: 19,
rollRate: 1.35, pitchRate: 1.0, yawRate: 0.8,
maxG: 4.2, fuelRate: 1.1,
throttleToSpeed: 0.75
},
{
id:'falcon', name:'VF-3 Falcon', role:'Aerobatic',
color:0xdadada, accent:0xc42b2b,
mass: 1300, wing: 13, refArea: 13,
maxThrust: 9000, maxPower: 12000,
CD0: 0.029, k: 0.052,
CLa: 6.0, CLmax: 2.35, stallAoa: 21,
rollRate: 3.4, pitchRate: 2.7, yawRate: 1.9,
maxG: 8, fuelRate: 0.4,
throttleToSpeed: 1.15
}
];
// ============================================================
// MAIN
// ============================================================
let scene, camera, renderer, sky, sun, water, terrainMesh;
let worldGroup = new THREE.Group();
const state = {
mode: 0,
aircraftIdx: 0,
running: false,
clock: new THREE.Clock(),
camMode: 0, // 0 chase, 1 cockpit, 2 external
};
// Physics state
const P = {
pos: V3(0,0,0),
vel: V3(0,0,0), // world space m/s
quat: new THREE.Quaternion(),
bodyVel: V3(0,0,0), // body space velocity
pqr: V3(0,0,0), // roll/pitch/yaw rates (body)
throttle: 0,
rpm: 0,
heat: 0,
fuel: 1,
aoa: 0, beta: 0,
cl: 0, cd: 0, cx: 0,
lift: 0, loadG: 0,
airDensity: 1.225,
speed: 0,
onGround: true,
crashed: false,
flaps: 0,
// forces for audio
lastAlpha: 0,
};
// Controls
const keys = {};
window.addEventListener('keydown', e => { keys[e.code]=true; if(['ArrowUp','ArrowDown','Space','ShiftLeft','ShiftRight'].includes(e.code)) e.preventDefault(); });
window.addEventListener('keyup', e => { keys[e.code]=false; });
// ============================================================
// RENDERER / SCENE
// ============================================================
function initRenderer() {
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0xa9c9dd, 0.00065);
renderer = new THREE.WebGLRenderer({ antialias:true, powerPreference:'high-performance' });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 1.8));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
document.getElementById('app').appendChild(renderer.domElement);
camera = new THREE.PerspectiveCamera(60, innerWidth/innerHeight, 0.5, 60000);
camera.position.set(0, 60, 120);
// Lights
const hemi = new THREE.HemisphereLight(0xbfd8ff, 0x4a5a3a, 0.7);
scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff2d6, 2.1);
sun.position.set(300, 400, 200);
sun.castShadow = true;
sun.shadow.mapSize.set(2048,2048);
const s = 1400;
sun.shadow.camera.left=-s; sun.shadow.camera.right=s;
sun.shadow.camera.top=s; sun.shadow.camera.bottom=-s;
sun.shadow.camera.near=100; sun.shadow.camera.far=2000;
sun.shadow.bias = -0.0004;
scene.add(sun);
state.sun = sun;
// Sky
sky = new Sky();
sky.scale.setScalar(400000);
scene.add(sky);
const sunU = sky.material.uniforms['sunPosition'];
const su = V3(300,400,200).normalize();
sunU.value.set(su.x, su.y, su.z);
scene.add(worldGroup);
}
// ============================================================
// TERRAIN (procedural Perlin heightfield)
// ============================================================
const terrainSize = 8000;
const terrainSeg = 200;
let heightData = null;
function makeNoise() {
// Perlin-ish value noise on a grid, cached
const N = 256;
const perm = new Uint8Array(512);
const p = new Uint8Array(N);
for(let i=0;i<N;i++) p[i]=i;
let seed = 1337;
const rnd = ()=> (seed = (seed*1103515245+12345)&0x7fffffff)/0x7fffffff;
for(let i=N-1;i>0;i--){ const j=(rnd()*(i+1))|0; const t=p[i]; p[i]=p[j]; p[j]=t; }
for(let i=0;i<512;i++) perm[i]=p[i&255];
const fade = t=> t*t*t*(t*(t*6-15)+10);
const grad = (h,x,y)=>{ const u=(h&1)?x:-x, v=(h&2)?y:-y; return u+v; };
const noise = (x,y)=>{
const X=Math.floor(x)&255, Y=Math.floor(y)&255;
x-=Math.floor(x); y-=Math.floor(y);
const u=fade(x), v=fade(y);
const A=perm[X]+Y, B=perm[X+1]+Y;
const mix=(a,t)=>lerp(grad(perm[a],x,y),grad(perm[a+1],x-1,y),t);
return lerp(mix(A,u), mix(B,u), v);
};
return noise;
}
function terrainHeight(wx, wz) {
const n = state._noise;
let h = 0;
let amp = 1, freq = 1/1400, sum = 0;
for(let o=0;o<6;o++){
h += n(wx*freq, wz*freq)*amp;
sum += amp; amp *= 0.5; freq *= 2.05;
}
h = (h/sum); // -1..1
// Continent shaping: raise edges to ocean floor, central plateau
const cx = wx/terrainSize, cz = wz/terrainSize;
const dist = Math.sqrt(cx*cx+cz*cz);
const cont = clamp(1.4 - dist*1.3, 0, 1); // 1 at center
const ridgeline = Math.pow(clamp(1-dist*1.1,0,1), 2.2);
h = h*0.6*ridgeline + h*h*0.2*ridgeline;
const alt = h * 1900 * cont + ridgeline*260;
// flatten near runway (center)
const rdX = 0, rdZ = 900;
const d = Math.hypot(wx-rdX, wz-rdZ);
const flat = clamp(1 - d/900, 0, 1);
return alt + (0 - alt)*flat*flat;
}
function buildTerrain() {
const n = makeNoise();
state._noise = n;
const geo = new THREE.PlaneGeometry(terrainSize, terrainSize, terrainSeg, terrainSeg);
geo.rotateX(-Math.PI/2);
const pos = geo.attributes.position;
const colors = [];
const cLow = new THREE.Color(0x2f4a38);
const cMid = new THREE.Color(0x6b7a4a);
const cRock= new THREE.Color(0x706a5e);
const cSnow= new THREE.Color(0xf2f6fb);
const cSand= new THREE.Color(0xb8a868);
for(let i=0;i<pos.count;i++){
const x=pos.getX(i), z=pos.getZ(i);
const h = terrainHeight(x,z);
pos.setY(i, h);
const a = h/1900;
const col = new THREE.Color();
if(h < 4){ col.copy(cSand); }
else if(h < 300){ col.copy(cLow).lerp(cMid, (h-4)/296); }
else if(h < 950){ col.copy(cMid).lerp(cRock, (h-300)/650); }
else if(h < 1400){ col.copy(cRock).lerp(cSnow, (h-950)/450); }
else col.copy(cSnow);
// slope shading
colors.push(col.r, col.g, col.b);
}
geo.setAttribute('color', new THREE.Float32BufferAttribute(colors,3));
geo.computeVertexNormals();
const mat = new THREE.MeshStandardMaterial({ vertexColors:true, roughness:0.92, metalness:0.02, flatShading:false });
terrainMesh = new THREE.Mesh(geo, mat);
terrainMesh.receiveShadow = true;
worldGroup.add(terrainMesh);
}
// ============================================================
// WATER
// ============================================================
function buildWater() {
const geo = new THREE.PlaneGeometry(terrainSize*1.4, terrainSize*1.4, 1, 1);
geo.rotateX(-Math.PI/2);
const mat = new THREE.MeshStandardMaterial({ color:0x1c4a68, transparent:true, opacity:0.86, roughness:0.15, metalness:0.6 });
water = new THREE.Mesh(geo, mat);
water.position.y = 2.5;
worldGroup.add(water);
}
// ============================================================
// RUNWAY + markers (for landing scenario)
// ============================================================
function buildRunway() {
const grp = new THREE.Group();
const rwGeo = new THREE.PlaneGeometry(60, 900);
rwGeo.rotateX(-Math.PI/2);
const rwMat = new THREE.MeshStandardMaterial({ color:0x2b2f36, roughness:0.9 });
const rw = new THREE.Mesh(rwGeo, rwMat);
const baseY = terrainHeight(0,900);
rw.position.set(0, baseY+0.15, 900);
rw.receiveShadow = true;
grp.add(rw);
// centerline dashes
const lineMat = new THREE.MeshStandardMaterial({ color:0xe8e8e8, emissive:0x333333 });
for(let i=-440;i<440;i+=40){
const d = new THREE.Mesh(new THREE.PlaneGeometry(2.2,22), lineMat);
d.rotateX(-Math.PI/2); d.position.set(0, baseY+0.3, 900+i+11);
grp.add(d);
}
// threshold bars
const barMat = new THREE.MeshStandardMaterial({ color:0xf0f0f0 });
for(let s=-1;s<=1;s+=2){
for(let j=0;j<3;j++){
const b = new THREE.Mesh(new THREE.PlaneGeometry(3,40), barMat);
b.rotateX(-Math.PI/2);
b.position.set(s*18, baseY+0.3, 900+420 - j*55);
grp.add(b);
}
}
// Approach lights on both ends
const lMat = new THREE.MeshStandardMaterial({ color:0xffffff, emissive:0xdfeaff, emissiveIntensity:1.4 });
for(let i=-440;i<=440;i+=40){
const l = new THREE.Mesh(new THREE.SphereGeometry(1.4,6,6), lMat);
l.position.set(34, baseY+1.4, 900+i);
grp.add(l);
const l2 = l.clone(); l2.position.x=-34; grp.add(l2);
}
worldGroup.add(grp);
}
// ============================================================
// SCENERY (trees, clouds, buildings)
// ============================================================
const cloudGroups = [];
function buildScenery() {
// Trees
const trunkGeo = new THREE.CylinderGeometry(0.6,1.0,5,5);
const trunkMat = new THREE.MeshStandardMaterial({color:0x5a3d24, roughness:1});
const leafGeo = new THREE.ConeGeometry(3.2, 9, 7);
const leafMat = new THREE.MeshStandardMaterial({color:0x2e5a30, roughness:0.95});
const trees = new THREE.InstancedMesh(trunkGeo, trunkMat, 3000);
const leaves = new THREE.InstancedMesh(leafGeo, leafMat, 3000);
const m = new THREE.Matrix4();
let idx = 0;
for(let i=0;i<3000;i++){
const x = (Math.random()-0.5)*terrainSize*0.9;
const z = (Math.random()-0.5)*terrainSize*0.9;
const h = terrainHeight(x,z);
if(h < 12 || h > 420) { i--; continue; }
const sc = 0.7 + Math.random()*1.3;
m.makeTranslation(x, h+2.5*sc, z); trees.setMatrixAt(idx, m);
m.makeTranslation(x, h+7*sc, z); leaves.setMatrixAt(idx, m);
idx++;
}
trees.count = leaves.count = idx;
trees.castShadow = leaves.castShadow = true;
worldGroup.add(trees, leaves);
// Buildings near runway
const bMat = new THREE.MeshStandardMaterial({color:0x6a7280, roughness:0.8});
const towerMat = new THREE.MeshStandardMaterial({color:0x8892a0, roughness:0.5, metalness:0.3});
const tower = new THREE.Mesh(new THREE.CylinderGeometry(4,6,34,12), towerMat);
const by = terrainHeight(45,880);
tower.position.set(45, by+17, 880); tower.castShadow=true;
worldGroup.add(tower);
const dome = new THREE.Mesh(new THREE.SphereGeometry(5,12,8), new THREE.MeshStandardMaterial({color:0x3a4a5a, emissive:0x112233}));
dome.position.set(45, by+36, 880); worldGroup.add(dome);
for(let i=0;i<10;i++){
const w=8+Math.random()*14, d=8+Math.random()*14, hh=8+Math.random()*30;
const b = new THREE.Mesh(new THREE.BoxGeometry(w,hh,d), bMat);
const bx=70+ (Math.random()*160), bz=760+(Math.random()*120);
const bh=terrainHeight(bx,bz);
b.position.set(bx, bh+hh/2, bz); b.castShadow=true; b.receiveShadow=true;
worldGroup.add(b);
}
// Clouds
const cloudMat = new THREE.MeshBasicMaterial({color:0xffffff, transparent:true, opacity:0.85});
for(let i=0;i<40;i++){
const g = new THREE.Group();
const pc = 4 + (Math.random()*5|0);
for(let j=0;j<pc;j++){
const p = new THREE.Mesh(new THREE.SphereGeometry(30+Math.random()*50, 8, 6), cloudMat);
p.position.set((Math.random()-0.5)*160, (Math.random()-0.5)*30, (Math.random()-0.5)*160);
p.scale.y = 0.6;
g.add(p);
}
g.position.set((Math.random()-0.5)*terrainSize*1.4, 900+Math.random()*1600, (Math.random()-0.5)*terrainSize*1.4);
worldGroup.add(g);
cloudGroups.push(g);
}
}
// ============================================================
// AIRCRAFT MODEL
// ============================================================
let aircraftGroup = new THREE.Group();
function buildAircraftModel(def) {
const g = new THREE.Group();
const body = new THREE.MeshStandardMaterial({color:def.color, metalness:0.55, roughness:0.35});
const acc = new THREE.MeshStandardMaterial({color:def.accent, metalness:0.5, roughness:0.4});
const glass = new THREE.MeshStandardMaterial({color:0x0a1a2a, metalness:0.9, roughness:0.1, emissive:0x0a2040, emissiveIntensity:0.4});
const metal = new THREE.MeshStandardMaterial({color:0x3a3f47, metalness:0.8, roughness:0.4});
// Fuselage
let fuseLen = def.id==='ranger' ? 12 : (def.id==='sparrow'?9:6);
let fuseR = def.id==='ranger'?1.6:(def.id==='sparrow'?1.0:0.85);
const fuse = new THREE.Mesh(new THREE.CylinderGeometry(fuseR*0.7, fuseR, fuseLen, 16), body);
fuse.rotation.x = Math.PI/2;
fuse.position.z = 0;
fuse.castShadow = true;
g.add(fuse);
// Nose cone
const nose = new THREE.Mesh(new THREE.SphereGeometry(fuseR*0.72, 16, 12), body);
nose.scale.set(1,1,1.7); nose.position.z = fuseLen/2+1.2; nose.castShadow=true; g.add(nose);
// Cockpit
const cp = new THREE.Mesh(new THREE.SphereGeometry(fuseR*0.7, 14, 10), glass);
cp.scale.set(1,0.8,1.6); cp.position.set(0, fuseR*0.5, fuseLen*0.28); g.add(cp);
// Wings
let wingspan, wingChord;
if(def.id==='ranger'){ wingspan=16; wingChord=3.2; }
else if(def.id==='sparrow'){ wingspan=10; wingChord=1.7; }
else { wingspan=7.5; wingChord=1.5; }
const wingGeo = new THREE.BoxGeometry(wingspan, 0.22, wingChord);
const wing = new THREE.Mesh(wingGeo, body);
wing.position.set(0, -0.1, 0.2); wing.castShadow=true; g.add(wing);
// wing tips
for(let s=-1;s<=1;s+=2){
const t = new THREE.Mesh(new THREE.BoxGeometry(0.5,0.14,1.2), acc);
t.position.set(s*wingspan/2, -0.1, 0.2); g.add(t);
}
// Tail (horizontal + vertical)
const hTailGeo = new THREE.BoxGeometry(5, 0.15, 1.4);
const hTail = new THREE.Mesh(hTailGeo, acc);
hTail.position.set(0, 0.2, -fuseLen/2+0.8); g.add(hTail);
const vTailGeo = new THREE.BoxGeometry(0.15, 2.6, 1.8);
const vTail = new THREE.Mesh(vTailGeo, acc);
vTail.position.set(0, 1.3, -fuseLen/2+0.8); vTail.castShadow=true; g.add(vTail);
// Engine glows
for(let s=-1;s<=1;s+=2){
const eng = new THREE.Mesh(new THREE.CylinderGeometry(fuseR*0.5, fuseR*0.4, 2.2, 12), metal);
eng.rotation.x = Math.PI/2; eng.position.set(s*(def.id==='ranger'?2.2:0.9), -0.2, -fuseLen/2-0.4); g.add(eng);
const glow = new THREE.Mesh(new THREE.CircleGeometry(fuseR*0.4, 12), new THREE.MeshBasicMaterial({color:0x223344}));
glow.position.set(s*(def.id==='ranger'?2.2:0.9), -0.2, -fuseLen/2-1.5); g.add(glow);
glow.userData.glow = true;
}
// Control surfaces (for visual flap/pitch)
const eLev = new THREE.Mesh(new THREE.BoxGeometry(wingspan*0.55, 0.1, 0.5), acc);
eLev.position.set(0, -0.1, wingChord/2+0.15); g.add(eLev);
const rudd = new THREE.Mesh(new THREE.BoxGeometry(0.15, 2.2, 0.4), body);
rudd.position.set(0, 1.2, -fuseLen/2+0.6); g.add(rudd);
const elevon = new THREE.Mesh(new THREE.BoxGeometry(5,0.12,0.5), acc);
elevon.position.set(0,0.25,-fuseLen/2+1.1); g.add(elevon);
g.userData.eLev=eLev; g.userData.rudd=rudd; g.userData.elevon=elevon;
g.scale.setScalar(1.0);
return g;
}
// ============================================================
// ENEMY AI (dogfight)
// ============================================================
const enemies = [];
function spawnEnemies(n) {
for(let i=0;i<n;i++){
const g = new THREE.Group();
const body = new THREE.MeshStandardMaterial({color:0x8a3030, metalness:0.5, roughness:0.4});
const nose = new THREE.Mesh(new THREE.ConeGeometry(1.1,4,10), body);
nose.rotation.x = Math.PI/2; g.add(nose);
const wing = new THREE.Mesh(new THREE.BoxGeometry(8,0.2,2), body);
wing.position.z=0.2; g.add(wing);
const tail = new THREE.Mesh(new THREE.BoxGeometry(0.2,2,1.5), body);
tail.position.set(0,1,-1.6); g.add(tail);
g.castShadow=false;
const ang = Math.random()*TAU;
const r = 900 + Math.random()*1200;
const e = {
mesh:g, pos:V3(Math.cos(ang)*r, 500+Math.random()*700, Math.sin(ang)*r),
vel:V3(0,0,0), quat:new THREE.Quaternion(), pqr:V3(0,0,0),
throttle:0.5+Math.random()*0.4, hp:100, speed:120+Math.random()*60,
turnT:0,
};
worldGroup.add(g);
enemies.push(e);
}
}
function removeEnemies(){ enemies.forEach(e=>worldGroup.remove(e.mesh)); enemies.length=0; }
// ============================================================
// AUDIO
// ============================================================
let audioCtx = null, engineGain, windGain, masterGain;
let engineOsc, engineSub, engineFilter, windNode, windFilter;
let musicGain, musicNodes=[];
let musicOn = true;
function initAudio() {
if(audioCtx) return;
audioCtx = new (window.AudioContext||window.webkitAudioContext)();
masterGain = audioCtx.createGain(); masterGain.gain.value = 0.9; masterGain.connect(audioCtx.destination);
// --- Engine: two oscillators + filter, mapped to rpm ---
engineOsc = audioCtx.createOscillator(); engineOsc.type='sawtooth'; engineOsc.frequency.value=60;
engineSub = audioCtx.createOscillator(); engineSub.type='sine'; engineSub.frequency.value=40;
engineFilter = audioCtx.createBiquadFilter(); engineFilter.type='lowpass'; engineFilter.frequency.value=900; engineFilter.Q.value=2;
engineGain = audioCtx.createGain(); engineGain.gain.value=0.0;
engineOsc.connect(engineFilter); engineSub.connect(engineFilter);
engineFilter.connect(engineGain); engineGain.connect(masterGain);
engineOsc.start(); engineSub.start();
// --- Wind / airflow noise ---
const bufSize = audioCtx.sampleRate*2;
const buf = audioCtx.createBuffer(1, bufSize, audioCtx.sampleRate);
const d = buf.getChannelData(0);
for(let i=0;i<bufSize;i++) d[i]=Math.random()*2-1;
windNode = audioCtx.createBufferSource(); windNode.buffer=buf; windNode.loop=true;
windFilter = audioCtx.createBiquadFilter(); windFilter.type='bandpass'; windFilter.frequency.value=600; windFilter.Q.value=0.8;
windGain = audioCtx.createGain(); windGain.gain.value=0.0;
windNode.connect(windFilter); windFilter.connect(windGain); windGain.connect(masterGain);
windNode.start();
// --- Music (soft pad + arpeggio) ---
musicGain = audioCtx.createGain(); musicGain.gain.value=0.14; musicGain.connect(masterGain);
const notes=[220.00,261.63,329.63,392.00,440.00,523.25];
let step=0;
const playNote=()=>{
if(!musicOn||!audioCtx) return;
const f=notes[step%notes.length]*(step<notes.length?1:0.5);
const o=audioCtx.createOscillator(); o.type='triangle'; o.frequency.value=f;
const g=audioCtx.createGain(); g.gain.value=0;
const fl=audioCtx.createBiquadFilter(); fl.type='lowpass'; fl.frequency.value=1400;
o.connect(fl); fl.connect(g); g.connect(musicGain);
const t=audioCtx.currentTime;
g.gain.setValueAtTime(0,t); g.gain.linearRampToValueAtTime(0.25,t+0.05); g.gain.exponentialRampToValueAtTime(0.001,t+1.6);
o.start(t); o.stop(t+1.7); step++;
setTimeout(playNote, 520);
};
playNote();
// bass
const bass=audioCtx.createOscillator(); bass.type='sine'; bass.frequency.value=110;
const bg=audioCtx.createGain(); bg.gain.value=0.12;
bass.connect(bg); bg.connect(musicGain); bass.start();
audioCtx.state==='suspended'&&audioCtx.resume();
}
function updateAudio() {
if(!audioCtx) return;
const t=audioCtx.currentTime;
const rpm = P.rpm;
const baseFreq = 45 + rpm*150; // fundamental
engineOsc.frequency.setTargetAtTime(baseFreq, t, 0.08);
engineSub.frequency.setTargetAtTime(baseFreq*0.5, t, 0.08);
engineFilter.frequency.setTargetAtTime(400+rpm*2600, t, 0.1);
const engineVol = (0.05 + rpm*0.28) * (0.5+P.throttle*0.5);
engineGain.gain.setTargetAtTime(engineVol, t, 0.1);
// Wind scales with airspeed & thinness at altitude
const windVol = clamp(P.speed/600,0,1)*0.18 * (1 - clamp(P.airDensity/1.2,0,1)*0.3);
windFilter.frequency.setTargetAtTime(300 + P.speed*2.2, t, 0.15);
windGain.gain.setTargetAtTime(windVol, t, 0.12);
}
// ============================================================
// PHYSICS — the core
// ============================================================
const GRAV = 9.81;
const SEA = 1.225;
function airDensityAt(h){ return SEA * Math.exp(-h/8500); } // barometric
function stepPhysics(dt) {
const def = AIRCRAFT[state.aircraftIdx];
if(P.crashed){ dt = 0.016; }
// ---- Input -> control surface commands (body rates) ----
let cmdRoll=0, cmdPitch=0, cmdYaw=0;
const rr = def.rollRate, pr = def.pitchRate, yr = def.yawRate;
if(!P.crashed){
if(keys.KeyW) cmdPitch -= pr; // nose down (pitch)
if(keys.KeyS) cmdPitch += pr; // nose up
if(keys.KeyA) cmdRoll -= rr;
if(keys.KeyD) cmdRoll += rr;
if(keys.KeyQ) cmdYaw -= yr;
if(keys.KeyE) cmdYaw += yr;
}
// throttle
if(keys.ArrowUp) P.throttle = clamp(P.throttle + dt*0.6, 0, 1);
if(keys.ArrowDown) P.throttle = clamp(P.throttle - dt*0.6, 0, 1);
// flaps
if(keys.Space && !P._spaceLock){ P.flaps = P.flaps>0.5?0:1; P._spaceLock=true; }
if(!keys.Space) P._spaceLock=false;
// ---- Engine ----
// RPM eases toward throttle
const targetRpm = P.throttle;
P.rpm = lerp(P.rpm, targetRpm, 1-Math.pow(0.02, dt));
// Engine heat
P.heat = clamp(P.heat + (P.throttle*0.9 - P.heat*0.7)*dt*2.0, 0, 1);
// Thrust along forward (body +z), reduced by air density at altitude
const thrust = def.maxThrust * P.throttle * (0.55 + 0.45*clamp(P.airDensity/SEA,0.3,1));
const fwd = V3(0,0,1).applyQuaternion(P.quat);
// ---- Aerodynamics in body frame ----
// Body velocity: velocity of air relative to aircraft in body frame
const q = P.quat.clone().conjugate();
P.bodyVel.copy(P.vel).applyQuaternion(q);
const u = P.bodyVel.x, v = P.bodyVel.y, w = P.bodyVel.z;
P.speed = Math.hypot(u,v,w);
// Angle of attack & sideslip (small-speed guard)
let alpha=0, beta=0;
if(P.speed>5){
alpha = Math.atan2(w, u);
beta = Math.asin(clamp(v/P.speed,-1,1));
}
P.aoa = alpha*180/Math.PI;
P.beta = beta*180/Math.PI;
// Lift slope with flap boost & Reynolds (density) scaling
const flapBoost = P.flaps*0.9;
const CLa = def.CLa;
const alphaStall = def.stallAoa*Math.PI/180;
let CL = CLa*(alpha + flapBoost*0.4);
// Stall model: peak at CLmax then drop
const alphaCl = Math.atan2(def.CLmax/CLa - flapBoost*0.4, 1);
let stallFactor=1;
const aAbs = Math.abs(alpha);
if(aAbs>alphaCl){
const over = aAbs-alphaCl;
stallFactor = clamp(1 - over*2.2, 0.25, 1);
}
CL = Math.sign(alpha+flapBoost*0.4)*Math.min(Math.abs(CLa*(alpha+flapBoost*0.4)), def.CLmax)*stallFactor;
const qdyn = 0.5 * P.airDensity * P.speed*P.speed;
const S = def.refArea;
// Induced drag from lift: CDi = k * CL^2 (k = 1/(π*AR*e))
const CDi = def.k * CL*CL;
let CD = def.CD0 + CDi;
// Extra drag during flaps, high AoA, roll
CD += P.flaps*0.05 + Math.max(0,Math.abs(alpha)*0.004) + (Math.abs(P.pqr.x)*0.0008);
// Lift acts perpendicular to velocity (in vertical plane) — body frame: -Y axis
const liftMag = qdyn * S * CL;
// Drag opposite to velocity
const dragMag = qdyn * S * CD;
// Side force (yaw/sideslip) -> weathercock
const sideMag = qdyn * S * (-0.6*beta);
// Forces in body frame
const gravMass = def.mass*GRAV;
// gravity in body frame from quat
const gravBody = V3(0,-gravMass,0).applyQuaternion(P.quat);
const F = V3(0,0,0);
// lift: along -bodyY (approx, for small alpha)
F.y += -liftMag * clamp(stallFactor,0,1);
// drag: opposite body velocity
if(P.speed>0.1){
const inv = -dragMag/P.speed;
F.x += u*inv; F.y += v*inv; F.z += w*inv;
}
// side force
F.x += sideMag*0.5;
// thrust
F.x += 0; F.y += 0; F.z += thrust;
// ---- Control torques -> angular acceleration (inertia) ----
// We integrate body rates with damping toward commanded rates
const J = 1.0; // normalized moment of inertia scale (per axis, scaled by def)
const scale = def.id==='ranger'?0.6:(def.id==='sparrow'?1.3:1.8);
let tRoll = cmdRoll*scale, tPitch=cmdPitch*scale, tYaw=cmdYaw*scale;
// yaw also from sideslip (weathercocking / directional stability)
tYaw += -0.8*beta;
// roll coupling to yaw (adverse yaw)
tYaw -= cmdRoll*0.35;
// pitch from roll in a turn (load)
// Angular accelerations
const dRoll = (tRoll - P.pqr.x*3.0)/J;
const dPitch = (tPitch - P.pqr.y*3.0)/J;
const dYaw = (tYaw - P.pqr.z*3.0)/J;
P.pqr.x = clamp(P.pqr.x + dRoll*dt, -rr, rr);
P.pqr.y = clamp(P.pqr.y + dPitch*dt, -pr, pr);
P.pqr.z = clamp(P.pqr.z + dYaw*dt, -yr, yr);
// ---- Integrate orientation (body-rate quaternion integration) ----
// dQ builds rotation from body angular rates; correct order & direction
const dq = new THREE.Quaternion();
dq.setFromAxisAngle(V3(1,0,0), -P.pqr.x*dt); // roll
const dp = new THREE.Quaternion();
dp.setFromAxisAngle(V3(0,1,0), -P.pqr.y*dt); // pitch
const dy = new THREE.Quaternion();
dy.setFromAxisAngle(V3(0,0,1), -P.pqr.z*dt); // yaw
const dQ = new THREE.Quaternion().copy(dq).multiply(dp).multiply(dy);
P.quat.multiply(dQ).normalize();
// ---- Integrate translation ----
const a = F.multiplyScalar(1/def.mass);
P.vel.add(a*dt);
// gravity already in F; add nothing else
P.pos.add(P.vel.clone().multiplyScalar(dt));
// ---- Load factor / G ----
// Net non-gravity vertical force in body frame vs weight
const liftG = liftMag/(def.mass*GRAV);
P.loadG = liftG + (keys ? 0 : 0); // approx
// actual g from acceleration vs gravity
const accel = F.clone().multiplyScalar(1/def.mass);
const netLiftBody = F.y + def.mass*GRAV; // body -y lift + support
P.loadG = clamp(-F.y/(def.mass*GRAV), -2, def.maxG+1);
// ---- Ground collision ----
const baseY = terrainHeight(P.pos.x, P.pos.z);
const wheelY = def.id==='ranger'?0.9:(def.id==='sparrow'?0.7:0.6);
if(P.pos.y < baseY + wheelY){
P.pos.y = baseY + wheelY;
handleGround(def, baseY);
} else {
P.onGround = false;
}
// fuel burn
P.fuel = clamp(P.fuel - def.fuelRate*P.throttle*dt*0.01, 0, 1);
if(P.fuel<=0) P.throttle = Math.min(P.throttle, 0.1);
// crash conditions
if(!P.crashed && P.running){
if(P.onGround && P.speed>70 && P.pos.y-baseY<wheelY+0.5){
// hard landing / high speed on ground -> crash
doCrash();
}
if(P.pos.y < 6 && P.onGround && P.speed>60){
doCrash();
}
}
}
let _prevGroundSpeed=0, _prevGroundY=0;
function handleGround(def, baseY){
P.onGround = true;
const wheelY = def.id==='ranger'?0.9:(def.id==='sparrow'?0.7:0.6);
// If moving down fast into ground -> crash
if(P.pos.y < baseY+wheelY-0.01 && P.vel.y < -8){
doCrash();
return;
}
// Ground handling: wheels on runway
if(P.vel.y < 0){
// push back up, kill downward velocity
P.vel.y = 0;
}
// Rolling resistance / braking on ground (only meaningful near runway flat area)
const onFlat = baseY > 0.5 && baseY < 3; // runway area is flat ~0
let rollResist = 0.02;
if(keys.ShiftLeft||keys.ShiftRight) rollResist = 0.25;
if(P.onGround){
// decelerate
const fr = onFlat? rollResist*GRAV : 0.5*GRAV;
const sp = Math.hypot(P.vel.x, P.vel.z);
if(sp>0.01){
const dec = fr;
P.vel.x *= clamp(1-dec/P.sp*dt, 0, 1);
P.vel.z *= clamp(1-dec/P.sp*dt, 0, 1);
}
// Level the aircraft on ground (pitch up gently, wings level)
// Damp roll/pitch rates strongly on ground
P.pqr.x *= Math.pow(0.03, dt);
P.pqr.y *= Math.pow(0.2, dt);
P.pqr.z *= Math.pow(0.03, dt);
// Keep wings level-ish if on ground and slow
if(P.speed < 40){
// align forward along -z (runway heading) a bit
const q = P.quat;
// reduce roll
const right = V3(1,0,0).applyQuaternion(q);
P.pqr.x += (-right.y)*dt*2.0;
}
// Pitch: if nose too low, keep level
// Forward thrust only pushes; on ground plane constrains y
}
}
function doCrash(){
if(P.crashed) return;
P.crashed = true;
P.pqr.set(0,0,0);
// spin & sink
showMsg('CRASH', 'Aircraft destroyed — bad landing', ()=>{ endMission(false); });
}
// ============================================================
// UI / MENU
// ============================================================
const previews = {};
function buildMenu() {
const fleet = document.getElementById('fleet');
fleet.innerHTML='';
AIRCRAFT.forEach((def,i)=>{
const stats = [
['Speed', def.throttleToSpeed],
['Thrust', def.maxThrust/200],
['Lift', def.CLmax],
['Turn', 1/(def.rollRate)*3],
['Agility', def.pitchRate/2.7],
];
let statHtml = '';
stats.forEach(([l,v])=>{
statHtml += `<div class="stat"><span class="lbl">${l}</span><div class="bar"><i style="width:${clamp(v,0.05,1)*100|0}%"></i></div></div>`;
});
const card = document.createElement('div');
card.className='card'; card.dataset.idx=i;
card.innerHTML = `<div class="preview-canvas"></div><h3>${def.name}</h3><div class="role">${def.role}</div>${statHtml}`;
fleet.appendChild(card);
const cv = card.querySelector('.preview-canvas');
previews[i]=cv;
card.addEventListener('click', ()=>{
document.querySelectorAll('#fleet .card').forEach(c=>c.classList.remove('selected'));
card.classList.add('selected');
state.aircraftIdx = i;
updatePreview(i);
});
});
document.querySelectorAll('#modeRow .modebtn').forEach(b=>{
b.addEventListener('click',()=>{
document.querySelectorAll('#modeRow .modebtn').forEach(x=>x.classList.remove('selected'));
b.classList.add('selected');
state.mode = parseInt(b.dataset.mode);
});
});
document.getElementById('startBtn').addEventListener('click', startGame);
document.getElementById('backBtn').addEventListener('click', ()=>{
document.getElementById('hudWin2').style.display='none';
document.getElementById('menu').style.display='flex';
state.running=false;
});
document.querySelectorAll('#fleet .card').forEach((c,i)=>{ if(previews[i]) updatePreview(i); });
document.querySelectorAll('#fleet .card')[0].click();
}
function updatePreview(i){
const cv = previews[i]; if(!cv) return;
if(cv._r) return;
cv.width=300; cv.height=150;
const r = new THREE.WebGLRenderer({antialias:true, transparent:true});
r.setPixelRatio(1); r.setSize(300,150); r.toneMapping=THREE.ACESFilmicToneMapping;
cv.appendChild(r.domElement);
const s = new THREE.Scene();
s.add(new THREE.AmbientLight(0xffffff,0.7));
const d = new THREE.DirectionalLight(0xffffff,1.4); d.position.set(3,4,5); s.add(d);
const cam = new THREE.PerspectiveCamera(40,300/150,0.1,100); cam.position.set(12,4,12); cam.lookAt(0,0,0);
const g = new THREE.Group();
g.add(buildAircraftModel(AIRCRAFT[i]));
s.add(g);
r.render(s,cam); cv._r=r; cv._s=s; cv._c=cam; cv._g=g;
function anim(){
if(!cv._g) return;
cv._g.rotation.y += 0.01;
r.render(cv._s,cv._c);
requestAnimationFrame(anim);
}
anim();
}
// ============================================================
// HUD update
// ============================================================
const hud = { status:document.getElementById('status'), alt:document.getElementById('altBox'), spd:document.getElementById('spdBox'), compass:document.getElementById('compass'), msg:document.getElementById('msg'), tip:document.getElementById('tip'), win:document.getElementById('hudWin') };
let hudCounters = { landings:0, bestDist:0 };
function updateHUD() {
const alt = Math.max(0, P.pos.y - terrainHeight(P.pos.x,P.pos.z));
const kal = P.speed*3.6; // km/h
const mps = P.speed;
const mach = P.speed/340;
hud.alt.innerHTML = `ALTITUDE<br><b>${alt|0}</b> m<br><span style="font-size:11px;color:#6fa8c8">${(alt*3.281)|0} ft</span>`;
hud.spd.innerHTML = `AIRSPEED<br><b>${kal|0}</b> km/h<br><span style="font-size:11px;color:#6fa8c8">${mps|0} m/s · ${mach.toFixed(2)} Ma</span>`;
const ag = P.loadG;
const gcol = Math.abs(ag)>7?'#ff7a5c':(ag<-0.5?'#ffd25f':'#9fe0ff');
hud.status.innerHTML =
`THROTTLE <span style="color:#eafaff">${(P.throttle*100)|0}%</span><br>`+
`RPM <span style="color:#eafaff">${(P.rpm*100)|0}%</span> · `+
`G <span style="color:${gcol}">${ag.toFixed(1)}</span><br>`+
`AOA <span style="color:#eafaff">${P.aoa.toFixed(1)}°</span> · `+
`FLAPS <span style="color:#eafaff">${P.flaps?`ON ${P.flaps}`:'OFF'}</span><br>`+
`FUEL <span style="color:${P.fuel<0.2?'#ff7a5c':'#eafaff'}">${(P.fuel*100)|0}%</span> · `+
`HEAT <span style="color:${P.heat>0.8?'#ff7a5c':'#eafaff'}">${(P.heat*100)|0}%</span><br>`+
`${P.airDensity<0.9?`AIR <span style="color:#ffd25f">${(P.airDensity*100|0)}%</span>`:`AIR <span style="color:#7dffa6">${(P.airDensity/SEA*100)|0}%`}</span><br>`+
(P.onGround?`<span class="ok">ON GROUND</span>`:`<span style="color:#6fa8c8">IN FLIGHT</span>`)+
(P.crashed?`<br><span class="warn">✗ DESTROYED</span>`:'');
// compass
const hd = (THREE.MathUtils.radToDeg(P.quat.getAngle(new THREE.Vector4(0,0,1,0)))||0);
const dir = new THREE.Euler(0,0,0).setFromQuaternion(P.quat,'YXZ').y;
let deg = ((THREE.MathUtils.radToDeg(-dir)%360)+360)%360;
const labels = {0:'N',90:'E',180:'S',270:'W'};
let near = Object.keys(labels).map(Number).sort((a,b)=>Math.abs(a-deg)-Math.abs(b-deg))[0];
hud.compass.innerHTML = `${labels[near]} ${(deg|0).toString().padStart(3,'0')}° PITCH ${(P.aoa>=0?'+':'')}${(THREE.MathUtils.radToDeg(Math.atan2(P.bodyVel.z,Math.hypot(P.bodyVel.x,P.bodyVel.y)))).toFixed(0)}° ROLL ${(-THREE.MathUtils.radToDeg(Math.atan2(P.bodyVel.y,Math.hypot(P.bodyVel.x,P.bodyVel.z))))|0}°`;
// mission tips
if(state.mode===0){
const ap = P.pos.y - terrainHeight(P.pos.x,P.pos.z);
hud.tip.innerHTML = ap>15 ? 'ALIGN WITH RUNWAY · APPROACH AT 220 km/h · PULL UP AT DECISION' : 'FINAL APPROACH · KEEP AIRSPEED · GENTLE DESCENT';
} else {
const active = enemies.filter(e=>e.hp>0);
hud.tip.innerHTML = `HOSTILES ${active.length} · SEEK & SHOOT · MANAGE SPEED & G`;
}
// stall warning
const aAbs = Math.abs(P.aoa);
if(!P.onGround && aAbs>def.stallAoa*0.85 && P.speed>20){
hud.tip.innerHTML = `<span class="warn">⚠ STALL ⚠</span> · reduce angle of attack · add power`;
}
}
const def = AIRCRAFT[0];
// ============================================================
// MESSAGES / MISSION END
// ============================================================
let msgCb=null;
function showMsg(title, sub, cb){
hud.msg.innerHTML = title+(sub?`<small>${sub}</small>`:'');
hud.msg.style.display='block';
msgCb=cb;
}
function hideMsg(){ hud.msg.style.display='none'; }
let missionMsg = null;
function showMission(title, sub){
hud.win.innerHTML = title+`<small>${sub}</small>`;
hud.win.style.display='block';
}
// ============================================================
// START / MISSION
// ============================================================
function startGame() {
initAudio();
document.getElementById('menu').style.display='none';
document.getElementById('hud').style.display='block';
hideMsg(); hud.win.style.display='none';
// reset physics
const def = AIRCRAFT[state.aircraftIdx];
P.pos.set(0, 1.5, -400);
P.vel.set(0,0,0);
P.bodyVel.set(0,0,0);
P.quat.identity();
P.pqr.set(0,0,0);
P.throttle=0; P.rpm=0; P.heat=0; P.fuel=1;
P.aoa=0; P.beta=0; P.cl=0; P.cd=0; P.loadG=1;
P.airDensity=SEA; P.speed=0; P.onGround=true; P.crashed=false;
P.flaps=0;
// place aircraft at start depending on mode
if(state.mode===0){
P.pos.set(0, 1.5, -400); // on runway start
P.quat.identity(); // facing +z toward runway
} else {
P.pos.set(0, 700, 0);
P.vel.set(0,0,120);
P.quat.identity();
P.onGround=false;
}
// scenery
worldGroup.position.set(0,0,0);
aircraftGroup = buildAircraftModel(def);
worldGroup.add(aircraftGroup);
// enemies
removeEnemies();
if(state.mode===1) spawnEnemies(3);
state.running=true;
state.clock.getDelta();
if(state.mode===0) showMsg('TAKEOFF & LANDING', 'Climb, cruise, and land cleanly on the runway');
else showMsg('DOGFIGHT', 'Defeat all enemy fighters');
setTimeout(()=>{ if(state.running) hideMsg(); }, 4200);
}
// ============================================================
// CAMERA
// ============================================================
const camPos = V3(0,60,120);
const camLook = V3(0,0,0);
function updateCamera(dt) {
const fwd = V3(0,0,1).applyQuaternion(P.quat);
const up = V3(0,1,0).applyQuaternion(P.quat);
const right = V3(1,0,0).applyQuaternion(P.quat);
if(keys.KeyC){ if(!keys._cLock){ state.camMode=(state.camMode+1)%3; keys._cLock=true;} } else keys._cLock=false;
let targetPos, targetLook;
if(state.camMode===1){ // cockpit
targetPos = P.pos.clone().add(fwd.clone().multiplyScalar(1.2)).add(up.clone().multiplyScalar(0.6));
targetLook = P.pos.clone().add(fwd.clone().multiplyScalar(50));
} else if(state.camMode===2){ // external orbit
const ang = performance.now()*0.0002;
const off = V3(Math.cos(ang)*40, 15, Math.sin(ang)*40);
targetPos = P.pos.clone().add(off);
targetLook = P.pos.clone();
} else { // chase
const dist = 26 + P.speed*0.04;
const hgt = 9;
targetPos = P.pos.clone().add(fwd.clone().multiplyScalar(-dist)).add(V3(0,hgt,0));
targetLook = P.pos.clone().add(fwd.clone().multiplyScalar(30)).add(V3(0,2,0));
}
const k = 1-Math.pow(0.001, dt);
camPos.lerp(targetPos, k);
camLook.lerp(targetLook, k);
camera.position.copy(camPos);
camera.lookAt(camLook);
// subtle shake at high speed / high G
if(P.speed>200 || Math.abs(P.loadG-1)>4){
camera.position.x += (Math.random()-0.5)*1.2;
camera.position.y += (Math.random()-0.5)*1.2;
}
}
// ============================================================
// ENEMY AI update
// ============================================================
function updateEnemies(dt) {
const me = P.pos;
enemies.forEach(e=>{
if(e.hp<=0) return;
const toEnemy = V3().subVectors(me, e.pos);
const dist = toEnemy.length();
// steer toward player, keep some distance
e.turnT -= dt;
if(e.turnT<=0){ e.turnT = 0.6+Math.random()*1.2; e.side = (Math.random()<0.5?-1:1); }
const desired = toEnemy.clone().normalize();
// current forward
const fwd = V3(0,0,1).applyQuaternion(e.quat);
let steer = V3().subVectors(desired, fwd);
// add lateral offset
const up = V3(0,1,0);
steer.add(V3().crossVectors(fwd, up).multiplyScalar(e.side*0.4));
// normalize & turn
const len = steer.length();
if(len>1e-4){
steer.normalize();
const newFwd = V3().addVectors(fwd, steer.multiplyScalar(dt*1.5)).normalize();
const qn = new THREE.Quaternion().setFromUnitVectors(V3(0,0,1), newFwd);
e.quat.multiply(qn).normalize();
}
// maintain speed, climb if below
e.speed = lerp(e.speed, 160 + (me.y-e.pos.y)*0.05, dt*0.5);
e.speed = clamp(e.speed, 90, 260);
e.vel.copy(new THREE.Vector3(0,0,1).applyQuaternion(e.quat).multiplyScalar(e.speed));
e.pos.add(e.vel.clone().multiplyScalar(dt));
// keep altitude
if(e.pos.y < 300) e.pos.y = 300;
if(e.pos.y > 2000) e.pos.y = 2000;
e.mesh.position.copy(e.pos);
e.mesh.quaternion.copy(e.quat);
// shooting: if roughly aimed at player and close
const aim = V3().subVectors(me, e.pos).normalize();
const dot = fwd.dot(aim);
if(dist<900 && dot>0.985 && !e._cd){
e._cd = true;
setTimeout(()=>{ e._cd=false; }, 900);
// check player shields? simple: damage based on player speed/EV
if(Math.random()<0.55){
// player takes damage (not modeled with HP; instead flash)
crashFlash = 0.3;
}
}
});
}
let crashFlash = 0;
// ============================================================
// MAIN LOOP
// ============================================================
let raf;
function animate() {
raf = requestAnimationFrame(animate);
const dt = Math.min(state.clock.getDelta(), 0.033);
if(state.running){
if(!P.crashed) stepPhysics(dt);
else {
// crash animation: tumble and sink
P.pqr.x += (Math.random()-0.5)*dt*6;
P.quat.multiply(new THREE.Quaternion().setFromAxisAngle(V3(1,0,0),dt*4));
P.pos.y -= dt*40;
if(P.pos.y < terrainHeight(P.pos.x,P.pos.z)) P.pos.y = terrainHeight(P.pos.x,P.pos.z);
aircraftGroup.rotation.copy(P.quat);
}
updateEnemies(dt);
updateCamera(dt);
// aircraft model orientation
aircraftGroup.position.copy(P.pos);
aircraftGroup.quaternion.copy(P.quat);
// control surface visuals
const ud = aircraftGroup.userData;
if(ud.eLev) ud.eLev.rotation.z = -clamp(P.pqr.y*0.5,-0.6,0.6);
if(ud.rudd) ud.rudd.rotation.y = clamp(P.pqr.z*0.4,-0.7,0.7);
if(ud.elevon) ud.elevon.rotation.z = -clamp(P.pqr.x*0.6,-0.8,0.8);
if(P.flaps && ud.eLev) ud.eLev.position.y = -0.1 + P.flaps*0.25;
// engine glow
aircraftGroup.traverse(o=>{
if(o.userData.glow && o.material && o.material.color){
o.material.color.setHSL(0.08,1,0.3+P.throttle*0.5);
if(o.material.emissive) o.material.emissive.setHex(0xffaa33);
if(o.material.emissiveIntensity !== undefined) o.material.emissiveIntensity = 0.5+P.throttle*2;
}
});
// fly clouds
cloudGroups.forEach(c=>{ c.position.x += dt*4; if(c.position.x>terrainSize*0.7) c.position.x=-terrainSize*0.7; });
updateHUD();
updateAudio();
// dogfight win check
if(state.mode===1 && enemies.every(e=>e.hp<=0)){
endMission(true);
}
}
// crash flash overlay via fog color
if(crashFlash>0){ crashFlash-=dt; renderer.toneMappingExposure = 1.05 + Math.sin(performance.now()*0.05)*crashFlash; }
else renderer.toneMappingExposure = 1.05;
renderer.render(scene, camera);
}
// ============================================================
// SHOOTING (dogfight)
// ============================================================
function tryShoot() {
if(state.mode!==1 || P.crashed) return;
const fwd = V3(0,0,1).applyQuaternion(P.quat);
const mp = P.pos;
let hit=null, best=1e9;
enemies.forEach(e=>{
if(e.hp<=0) return;
const to = V3().subVectors(e.pos, mp);
const d = to.length();
to.normalize();
const ang = fwd.dot(to);
if(d<1000 && ang>0.999){ // very tight aim
// crosshair proximity
if(d<best){ best=d; hit=e; }
}
});
if(hit){
hit.hp -= 34;
muzzle = 0.08;
if(hit.hp<=0){
explode(hit.pos);
removeEnemies2(hit);
}
}
}
let muzzle=0;
function explode(pos){
// simple particle burst
const g=new THREE.Group();
const mat=new THREE.MeshBasicMaterial({color:0xffaa33});
for(let i=0;i<24;i++){
const p=new THREE.Mesh(new THREE.BoxGeometry(1.5,1.5,1.5),mat);
p.position.copy(pos);
p.userData.v = V3((Math.random()-0.5),(Math.random()-0.5),(Math.random()-0.5)).multiplyScalar(60+Math.random()*80);
g.add(p);
}
worldGroup.add(g);
const t=0;
function anim(){
t+=0.016;
g.children.forEach(p=>{ p.position.add(p.userData.v.clone().multiplyScalar(0.016)); p.userData.v.multiplyScalar(0.92); });
mat.opacity=clamp(1-t*1.5,0,1); mat.transparent=true;
if(t<0.7) requestAnimationFrame(anim); else worldGroup.remove(g);
}
anim();
}
function removeEnemies2(e){ /* mesh removed via loop below */ }
// override: when enemy dies, remove its mesh
const _origRemove = removeEnemies;
// (handled inline in explode loop above by removing mesh when done)
// firing on fire key / mouse
window.addEventListener('keydown', e=>{
if(e.code==='KeyF' || e.code==='KeyZ'){ tryShoot(); }
});
let mouseDown=false;
addEventListener('mousedown', e=>{ if(state.running){ if(e.button===0) tryShoot(); } });
addEventListener('keydown', e=>{ if(state.running && e.code==='KeyZ') mouseDown=true; });
addEventListener('keyup', e=>{ if(e.code==='KeyZ') mouseDown=false; });
// ============================================================
// END MISSION
// ============================================================
function endMission(win){
state.running=false;
const w = document.getElementById('hudWin2');
const title = document.getElementById('winTitle');
const sub = document.getElementById('winSub');
const stats = document.getElementById('winStats');
if(win){
sub.textContent = 'MISSION COMPLETE';
title.textContent = 'AIR SUPERIORITY';
title.style.color = '#7dffa6';
stats.innerHTML = 'All hostiles down. Domain of the sky is yours.';
} else {
sub.textContent = 'MISSION FAILED';
title.textContent = 'AIRCRAFT LOST';
title.style.color = '#ff7a5c';
stats.innerHTML = 'A pilot learns from every fall. Choose again.';
}
w.style.display='flex';
document.getElementById('hud').style.display='none';
}
// ============================================================
// RESIZE
// ============================================================
addEventListener('resize', ()=>{
camera.aspect = innerWidth/innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(innerWidth, innerHeight);
});
// ============================================================
// BOOT
// ============================================================
function boot() {
initRenderer();
buildTerrain();
buildWater();
buildRunway();
buildScenery();
buildMenu();
window.__APEX = { state, P, AIRCRAFT, stepPhysics, startGame, aircraftGroup, worldGroup, updatePreview, previews };
animate();
}
boot();
</script>
</body>
</html>
<!-- agent-meta {"model":"peculiar-ragdoll/Tiel-Coder-35B-A3B-GGUF-MTP","provider":"brigid","persona":"full","sessionId":"0ae10e4f-9080-451b-b868-7ff91473fce5","tokensIn":11600952,"tokensOut":92093,"tokensTotal":11693045,"cost":0,"turns":107,"toolCalls":106,"failedToolCalls":4,"timestamp":"2026-09-04T22:52:24.562Z"} -->