Flight Simulator
ox alpha · 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 38,751 bytes · SHA-256 5a87893efb5b
<!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:#000; font-family:'Segoe UI',system-ui,sans-serif; }
#app { position:fixed; inset:0; }
canvas { display:block; }
/* ---------- MENU ---------- */
#menu { position:fixed; inset:0; z-index:50; display:flex; flex-direction:column; align-items:center;
justify-content:center; gap:18px; background:radial-gradient(ellipse at 30% 20%, #16324f 0%, #060d16 70%);
color:#cfe8ff; transition:opacity .5s; }
#menu.hidden { opacity:0; pointer-events:none; }
#menu h1 { font-size:52px; letter-spacing:10px; font-weight:800; color:#eaf6ff;
text-shadow:0 0 30px rgba(90,180,255,.55), 0 2px 0 #123; }
#menu h1 span { color:#57c7ff; }
#menu .sub { font-size:13px; letter-spacing:4px; color:#7fa8c9; margin-top:-14px; }
.section-label { margin-top:10px; font-size:11px; letter-spacing:3px; color:#6fa3cc; }
#fleet { display:flex; gap:16px; }
.card { width:250px; background:rgba(15,35,55,.75); border:1px solid rgba(90,170,255,.25);
border-radius:10px; padding:14px 16px; cursor:pointer; transition:.18s; backdrop-filter:blur(4px); }
.card:hover { border-color:#57c7ff; transform:translateY(-3px); box-shadow:0 8px 28px rgba(40,140,255,.25); }
.card.sel { border-color:#ffd76a; box-shadow:0 0 22px rgba(255,210,90,.28); background:rgba(45,45,25,.7); }
.card h3 { font-size:17px; color:#eaf6ff; margin-bottom:2px; }
.card .role { font-size:11px; letter-spacing:2px; color:#57c7ff; margin-bottom:8px; }
.bar { height:5px; background:#12283d; border-radius:3px; overflow:hidden; margin:3px 0 7px; }
.bar i { display:block; height:100%; background:linear-gradient(90deg,#2e9bd6,#7fe3ff); border-radius:3px; }
.stat { display:flex; justify-content:space-between; font-size:11px; color:#9fc4de; }
.stat b { color:#dff2ff; font-weight:600; }
#scenarios { display:flex; gap:14px; }
.scn { padding:10px 22px; border:1px solid rgba(90,170,255,.3); border-radius:8px; cursor:pointer;
font-size:13px; letter-spacing:1px; color:#bfe2ff; background:rgba(15,35,55,.6); transition:.15s; }
.scn:hover { border-color:#57c7ff; }
.scn.sel { background:#1d4d74; border-color:#57c7ff; color:#fff; }
.scn small { display:block; font-size:10px; color:#7fa8c9; margin-top:3px; }
#launchBtn { margin-top:6px; padding:13px 64px; font-size:17px; letter-spacing:5px; font-weight:700;
color:#04121f; background:linear-gradient(180deg,#8fdcff,#37a8e8); border:none; border-radius:8px;
cursor:pointer; transition:.15s; box-shadow:0 4px 24px rgba(60,170,255,.4); }
#launchBtn:hover { filter:brightness(1.15); transform:scale(1.03); }
#controlsHint { font-size:11px; color:#6f93ad; text-align:center; line-height:1.7; }
/* ---------- HUD ---------- */
#hud { position:fixed; inset:0; z-index:10; pointer-events:none; display:none; color:#bfffe0;
font-family:'Consolas','Courier New',monospace; }
#hud.on { display:block; }
.hudtxt { position:absolute; font-size:14px; text-shadow:0 0 6px rgba(0,255,140,.6), 1px 1px 2px #000; white-space:pre; }
#hudSpeed { left:6%; top:46%; text-align:right; }
#hudAlt { right:6%; top:42%; }
#hudHead { left:50%; top:6%; transform:translateX(-50%); }
#hudThr { left:6%; bottom:12%; }
#hudState { left:50%; bottom:8%; transform:translateX(-50%); text-align:center; font-size:13px; color:#ffe9a8; }
#hudMsg { left:50%; top:34%; transform:translateX(-50%); font-size:26px; letter-spacing:4px;
color:#ffd76a; text-align:center; text-shadow:0 0 18px rgba(255,200,80,.8); opacity:0; transition:opacity .4s; }
#hudHp { left:6%; top:6%; }
#hudTarget{ right:6%; bottom:12%; }
/* artificial horizon */
#horizonWrap { position:absolute; left:50%; top:50%; width:230px; height:230px; transform:translate(-50%,-50%); }
#horizonClip { position:absolute; inset:0; border-radius:50%; overflow:hidden; opacity:.85; }
#horizon { position:absolute; left:-50%; top:-50%; width:200%; height:200%; }
.sky { position:absolute; inset:0; background:linear-gradient(#1c6ec8,#79c2ef); }
.gnd { position:absolute; left:0; right:0; top:50%; bottom:-10%; background:linear-gradient(#8a6234,#5c3f1e); }
.hline { position:absolute; left:0; right:0; top:50%; height:3px; margin-top:-1px; background:#fff; }
#pitchLadder { position:absolute; left:50%; top:50%; }
.pl { position:absolute; left:-45px; width:90px; height:0; border-top:1.5px solid rgba(255,255,255,.9);
text-align:center; font-size:10px; color:#fff; }
.pl span{ position:relative; top:-14px; background:rgba(0,0,0,.25); padding:0 3px; }
#reticle { position:absolute; left:50%; top:50%; transform:translate(-50%,-50%); width:36px; height:36px;
border:2px solid rgba(120,255,180,.9); border-radius:50%; box-shadow:0 0 8px rgba(0,255,120,.5); }
#reticle::before,#reticle::after { content:''; position:absolute; background:rgba(120,255,180,.9); }
#reticle::before { left:50%; top:-12px; width:2px; height:8px; margin-left:-1px; }
#reticle::after { left:-12px; top:50%; height:2px; width:8px; margin-top:-1px; }
#vignette { position:fixed; inset:0; pointer-events:none; z-index:9;
box-shadow:inset 0 0 220px rgba(0,0,0,.55); }
#redflash { position:fixed; inset:0; pointer-events:none; z-index:11; background:radial-gradient(circle,transparent 40%,rgba(255,30,30,.55));
opacity:0; transition:opacity .2s; }
</style>
</head>
<body>
<div id="app"></div>
<div id="hud">
<div id="horizonWrap"><div id="horizonClip"><div id="horizon">
<div class="sky"></div><div class="gnd"></div><div class="hline"></div></div></div></div>
<div id="pitchLadder"></div>
<div id="reticle"></div>
<div class="hudtxt" id="hudSpeed"></div>
<div class="hudtxt" id="hudAlt"></div>
<div class="hudtxt" id="hudHead"></div>
<div class="hudtxt" id="hudThr"></div>
<div class="hudtxt" id="hudHp"></div>
<div class="hudtxt" id="hudState"></div>
<div class="hudtxt" id="hudTarget"></div>
<div id="hudMsg"></div>
</div>
<div id="vignette"></div>
<div id="redflash"></div>
<div id="menu">
<h1>APEX <span>AERO</span></h1>
<div class="sub">HIGH-FIDELITY FLIGHT SIMULATOR</div>
<div class="section-label">SELECT AIRCRAFT</div>
<div id="fleet"></div>
<div class="section-label">SELECT MISSION</div>
<div id="scenarios">
<div class="scn sel" data-s="takeoff">TAKEOFF<small>Climb to 1,000 ft</small></div>
<div class="scn" data-s="landing">LANDING<small>Grease it on the runway</small></div>
<div class="scn" data-s="dogfight">DOGFIGHT<small>3 hostiles inbound</small></div>
</div>
<button id="launchBtn">LAUNCH</button>
<div id="controlsHint">
W/S throttle · ↑↓ pitch · ←→ roll · A/D yaw · SPACE guns (dogfight)<br>
C camera · ESC pause / return to hangar · G gear (visual)
</div>
</div>
<script type="importmap">
{ "imports": { "three": "https://unpkg.com/three@0.160.0/build/three.module.js" } }
</script>
<script type="module">
import * as THREE from 'three';
/* ============================================================
APEX AERO — custom Newtonian flight dynamics on Three.js
============================================================ */
const G = 9.81, RHO0 = 1.225;
const clamp = (v,a,b)=>Math.min(b,Math.max(a,v));
const lerp = (a,b,t)=>a+(b-a)*t;
/* ---------- procedural noise (value noise + fBm) ---------- */
function hash2(x,y){ let n = Math.sin(x*127.1+y*311.7)*43758.5453; return n-Math.floor(n); }
function vnoise(x,y){
const xi=Math.floor(x), yi=Math.floor(y), xf=x-xi, yf=y-yi;
const u=xf*xf*(3-2*xf), v=yf*yf*(3-2*yf);
return lerp(lerp(hash2(xi,yi),hash2(xi+1,yi),u), lerp(hash2(xi,yi+1),hash2(xi+1,yi+1),u), v);
}
function fbm(x,y,oct=5){
let s=0,a=.5,f=1;
for(let i=0;i<oct;i++){ s+=a*vnoise(x*f,y*f); f*=2.03; a*=.5; }
return s;
}
const WORLD = 24000; // terrain square size (m)
function terrainH(x,z){
const nx=x/WORLD, nz=z/WORLD;
// continent mask: coastlines (ocean toward +x edge)
let cont = clamp((nx+0.5)*1.15-0.25, 0, 1);
let h = (fbm(nx*3+7.3, nz*3+2.1)-0.32)*900*cont + (fbm(nx*9,nz*9,4)-0.5)*160;
// mountain ridge band
const ridge = Math.pow(fbm(nx*2.2+31.7, nz*2.2+11.9), 2.2)*1500*cont;
h += ridge;
if(h<0) h*=0.12; // shallow sea floor
// flatten runway corridor (along Z at x≈0)
const dx=Math.abs(x), dzMax=900;
const mx = 1-clamp((dx-70)/230,0,1);
const mz = clamp((1300-Math.abs(z))/150,0,1);
const mask = mx*mz;
let out = lerp(h, 8, mask);
// approach corridor: gentle ramp south of the runway
if(z<-1150){
const m=Math.pow(clamp((-z-1150)/4200,0,1),1.6);
out = lerp(8, out, m);
}
return out;
}
function onRunway(x,z){ return Math.abs(x)<60 && Math.abs(z)<1150 && terrainH(x,z)<9.5; }
/* ---------- aircraft fleet ---------- */
const FLEET = [
{ id:'skylark', name:'SW-1 Skylark', role:'LIGHT TRAINER', color:0xf2f4f8, accent:0x2e86d6,
mass:900, maxThrust:7200, wingArea:16.2, cl0:0.28, clSlope:5.2, stallAlpha:0.24, cd0:0.032,
oswald:0.78, span:9.5, vRef:48, spool:1.6,
pitchRate:0.45, rollRate:1.4, yawRate:0.3, stab:2.2, hp:60,
stats:{SPD:.35,THR:.25,AGI:.85,ARM:.3}, desc:'Forgiving, slow, loves the pattern.' },
{ id:'vortex', name:'FX-9 Vortex', role:'AIR SUPERIORITY JET', color:0x8d99a6, accent:0xd23b3b,
mass:11500, maxThrust:148000, wingArea:37.5, cl0:0.06, clSlope:3.9, stallAlpha:0.27, cd0:0.021,
oswald:0.82, span:11.4, vRef:95, spool:2.6,
pitchRate:0.7, rollRate:2.6, yawRate:0.35, stab:1.5, hp:100,
stats:{SPD:1,THR:1,AGI:.9,ARM:.65}, desc:'Supercruise brute. Mind the Gs.' },
{ id:'titan', name:'AC-7 Titan', role:'HEAVY ATTACK', color:0x4a5d43, accent:0xe0c341,
mass:26000, maxThrust:190000, wingArea:62, cl0:0.22, clSlope:4.6, stallAlpha:0.22, cd0:0.028,
oswald:0.75, span:12.2, vRef:78, spool:3.4,
pitchRate:0.35, rollRate:1.1, yawRate:0.25, stab:2.6, hp:200,
stats:{SPD:.6,THR:.8,AGI:.4,ARM:1}, desc:'A flying fortress. Turns like a continent.' },
];
/* ---------- three.js scene ---------- */
const app = document.getElementById('app');
const renderer = new THREE.WebGLRenderer({antialias:true});
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth,innerHeight);
renderer.outputColorSpace = THREE.SRGBColorSpace;
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.05;
app.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = new THREE.Color(0x9fc7e8);
scene.fog = new THREE.Fog(0xa8cce8, 3000, 16000);
const camera = new THREE.PerspectiveCamera(62, innerWidth/innerHeight, 0.5, 42000);
const hemi = new THREE.HemisphereLight(0xbdd8ff, 0x54492e, 0.85); scene.add(hemi);
const sun = new THREE.DirectionalLight(0xfff2dc, 2.2);
sun.position.set(-4000,5000,2500); scene.add(sun);
// sun glare sprite
{
const c=document.createElement('canvas'); c.width=c.height=128;
const g=c.getContext('2d'), gr=g.createRadialGradient(64,64,4,64,64,64);
gr.addColorStop(0,'rgba(255,250,230,1)'); gr.addColorStop(.25,'rgba(255,235,190,.55)'); gr.addColorStop(1,'rgba(255,235,190,0)');
g.fillStyle=gr; g.fillRect(0,0,128,128);
const sm=new THREE.SpriteMaterial({map:new THREE.CanvasTexture(c), transparent:true, depthWrite:false});
const sp=new THREE.Sprite(sm); sp.scale.setScalar(3500); sp.position.copy(sun.position).normalize().multiplyScalar(30000);
scene.add(sp);
}
/* ---------- terrain mesh ---------- */
let terrain=null, water=null;
function buildTerrain(){
if(terrain){ scene.remove(terrain); terrain.geometry.dispose(); }
const seg=260, geo=new THREE.PlaneGeometry(WORLD,WORLD,seg,seg);
geo.rotateX(-Math.PI/2);
const pos=geo.attributes.position, col=new Float32Array(pos.count*3);
const c=new THREE.Color();
for(let i=0;i<pos.count;i++){
const x=pos.getX(i), z=pos.getZ(i);
const h=terrainH(x,z);
pos.setY(i,h);
const sl = Math.abs(terrainH(x+40,z)-h)+Math.abs(terrainH(x,z+40)-h);
if(h<2.5) c.setHex(0xc9bb84); // beach
else if(h<180) c.setHex(0x4e7c3a); // grass
else if(h<650) c.setHex(0x6b6f45).lerp(new THREE.Color(0x7a7a68),(h-180)/470);
else if(h<1050)c.setHex(0x777168); // rock
else c.setHex(0xe8edf2); // snow
if(sl>55&&h>60) c.lerp(new THREE.Color(0x5d564b), clamp((sl-55)/90,0,.8)); // cliffs
col[i*3]=c.r; col[i*3+1]=c.g; col[i*3+2]=c.b;
}
geo.setAttribute('color', new THREE.BufferAttribute(col,3));
geo.computeVertexNormals();
const mat=new THREE.MeshStandardMaterial({vertexColors:true, roughness:.94, metalness:0, flatShading:false});
terrain=new THREE.Mesh(geo,mat); scene.add(terrain);
if(!water){
water=new THREE.Mesh(new THREE.PlaneGeometry(WORLD*1.4,WORLD*1.4),
new THREE.MeshStandardMaterial({color:0x1d5f8f, roughness:.15, metalness:.35, transparent:true, opacity:.93}));
water.rotation.x=-Math.PI/2; water.position.y=2.2; scene.add(water);
}
}
buildTerrain();
/* ---------- runway ---------- */
{
const rw=new THREE.Mesh(new THREE.BoxGeometry(52,1.2,1760),
new THREE.MeshStandardMaterial({color:0x33363b,roughness:.9}));
rw.position.set(0,8.6,0); scene.add(rw);
const mkStripe=(z)=>{ const s=new THREE.Mesh(new THREE.PlaneGeometry(2.4,26),
new THREE.MeshBasicMaterial({color:0xf0f0f0})); s.rotation.x=-Math.PI/2; s.position.set(0,9.25,z); scene.add(s); };
for(let z=-800;z<=800;z+=100) mkStripe(z);
const tower=new THREE.Mesh(new THREE.CylinderGeometry(4,5,26,8),
new THREE.MeshStandardMaterial({color:0x9aa3ab,roughness:.8}));
tower.position.set(110,21,-300); scene.add(tower);
const ttop=new THREE.Mesh(new THREE.BoxGeometry(14,6,14),
new THREE.MeshStandardMaterial({color:0x3b4a58,roughness:.4,metalness:.4}));
ttop.position.set(110,37,-300); scene.add(ttop);
}
/* ---------- aircraft 3D models (procedural, distinct silhouettes) ---------- */
function mat(c,r=.55,m=.35){ return new THREE.MeshStandardMaterial({color:c,roughness:r,metalness:m}); }
function buildModel(def, hostile=false){
const g=new THREE.Group();
const body=mat(hostile?0x8a2f2f:def.color), acc=mat(hostile?0x551111:def.accent,.4,.5),
dark=mat(0x22262b,.5,.6), glass=new THREE.MeshStandardMaterial({color:0x9fd8ff,roughness:.08,metalness:.1,transparent:true,opacity:.85});
const add=(geo,m,x,y,z,rx=0,ry=0,rz=0)=>{ const me=new THREE.Mesh(geo,m); me.position.set(x,y,z);
me.rotation.set(rx,ry,rz); g.add(me); return me; };
if(def.id==='skylark'){
add(new THREE.CapsuleGeometry(.75,4.4,6,12),body,0,0,0,Math.PI/2); // fuselage
add(new THREE.BoxGeometry(11,.22,1.7),acc,0,.35,.4); // wing (high)
add(new THREE.BoxGeometry(3.6,.16,1.1),acc,0,1.05,-2.6); // tail
add(new THREE.BoxGeometry(.16,1.5,1.1),acc,0,.5,-2.7);
add(new THREE.SphereGeometry(.62,12,10),glass,0,.55,.9);
add(new THREE.CylinderGeometry(.16,.22,1.1,8),dark,0,0,2.7,Math.PI/2); // spinner
for(const s of[-1,1]) add(new THREE.CylinderGeometry(.09,.09,.7,6),dark,s*1.4,-1.05,.4); // gear legs
} else if(def.id==='vortex'){
add(new THREE.ConeGeometry(1.05,7.6,10),body,0,0,-.4,-Math.PI/2); // nose cone fwd
add(new THREE.CylinderGeometry(1.05,.85,5.2,10),body,0,0,2.6,Math.PI/2);
add(new THREE.BoxGeometry(8.6,.16,3.4),acc,0,.1,1.2); // delta wing
for(const s of[-1,1]){ add(new THREE.BoxGeometry(.14,2.3,2.8),acc,s*1.5,.9,-2.4,0,0,s*.35); // twin tails
add(new THREE.BoxGeometry(3.2,.14,1.9),acc,s*3.4,.1,-2.2); } // stabs
add(new THREE.SphereGeometry(.72,12,10),glass,0,.5,-1.2);
add(new THREE.CylinderGeometry(.42,.55,1.4,10),dark,0,0,5.4,Math.PI/2); // nozzle
for(const s of[-1,1]) add(new THREE.BoxGeometry(1.5,.5,3),dark,s*1.1,-.55,1.6);// intakes
} else {
add(new THREE.CylinderGeometry(1.15,1.35,9,12),body,0,0,.6,Math.PI/2); // fat fuselage
add(new THREE.ConeGeometry(1.15,3.2,12),body,0,0,-5.3,-Math.PI/2);
add(new THREE.BoxGeometry(17,.5,2.9),body,0,.9,1.2); // high wing
for(const s of[-1,1]) add(new THREE.BoxGeometry(.2,3,2.2),acc,s*4.6,2.2,1.2); // struts
add(new THREE.BoxGeometry(6.4,.3,2.2),acc,0,1.4,-4.6);
add(new THREE.BoxGeometry(.2,2.2,2.2),acc,0,2.4,-4.6);
add(new THREE.SphereGeometry(.85,12,10),glass,0,.85,-3.4);
for(const s of[-1,1]) add(new THREE.CylinderGeometry(.5,.5,2.6,10),dark,s*2.6,.9,-5.6,Math.PI/2); // engines
for(const s of[-1,1]) add(new THREE.CylinderGeometry(.09,.09,1.6,6),dark,s*1.8,-1.6,2);
}
g.traverse(o=>{ if(o.isMesh){ o.castShadow=false; } });
return g;
}
/* ============================================================
FLIGHT DYNAMICS — custom Newtonian 6-DOF-lite model
============================================================ */
const keys={};
addEventListener('keydown',e=>{ keys[e.code]=true; if(e.code==='Space')e.preventDefault(); });
addEventListener('keyup',e=>keys[e.code]=false);
class Plane {
constructor(def, hostile=false){
this.def=def; this.hostile=hostile;
this.obj=buildModel(def,hostile); scene.add(this.obj);
this.vel=new THREE.Vector3();
this.angVel=new THREE.Vector3(); // body rates (x=pitch, y=yaw, z=roll)
this.quat=new THREE.Quaternion();
this.throttle=0; this.rpm=0; this.hp=def.hp; this.dead=false;
this.gear=true; this.onGround=false; this.gForce=1; this.alpha=0; this.stalled=false;
this.input={pitch:0,roll:0,yaw:0};
this.cool=0; // gun cooldown
}
fwd(){ return new THREE.Vector3(0,0,-1).applyQuaternion(this.quat); }
up(){ return new THREE.Vector3(0,1,0).applyQuaternion(this.quat); }
right(){ return new THREE.Vector3(1,0,0).applyQuaternion(this.quat); }
speed(){ return this.vel.length(); }
step(dt){
if(this.dead){ this.vel.set(0,0,0); return; }
const d=this.def, q=this.quat;
const alt=Math.max(0,this.obj.position.y);
const rho=RHO0*Math.exp(-alt/8500); // ISA-ish density
// --- engine (spool lag + altitude lapse) ---
this.rpm += (this.throttle-this.rpm)*Math.min(1,dt/d.spool);
const lapse = 0.6+0.4*(rho/RHO0);
const thrust = d.maxThrust*this.rpm*lapse;
// --- aerodynamics in body frame (fwd = -Z) ---
const qInv=q.clone().invert();
const vb=this.vel.clone().applyQuaternion(qInv);
const V=vb.length();
const alpha = V>1 ? Math.atan2(-vb.y, -vb.z) : 0;
const beta = V>1 ? Math.asin(clamp(vb.x/Math.max(V,1e-3),-1,1)) : 0;
this.alpha=alpha;
const qbar=0.5*rho*V*V;
// lift coefficient w/ stall break
const a=alpha, as=d.stallAlpha;
let cl;
if(Math.abs(a)<as) cl = d.cl0 + d.clSlope*a;
else { const over=Math.abs(a)-as; cl = Math.sign(a)*(d.cl0+d.clSlope*as)*Math.max(0.25,1-over*3.2); }
this.stalled = Math.abs(a)>as && V>15;
const cd = d.cd0 + cl*cl/(Math.PI*d.oswald*(d.span*d.span/d.wingArea)) + (this.stalled?0.06:0);
const L=qbar*d.wingArea*cl, D=qbar*d.wingArea*cd, S=qbar*d.wingArea*0.85*beta;
const F=new THREE.Vector3(0,-G*d.mass,0); // weight
F.addScaledVector(this.fwd(), thrust);
if(V>0.5){
const vDir=this.vel.clone().normalize();
// lift ⊥ velocity, in the body up-plane
let liftDir=this.up().clone().sub(vDir.clone().multiplyScalar(this.up().dot(vDir)));
if(liftDir.lengthSq()>1e-6){ liftDir.normalize(); F.addScaledVector(liftDir,L); }
F.addScaledVector(vDir,-D);
F.addScaledVector(this.right(),-S);
}
this.gForce = L/(d.mass*G) * Math.cos(0) + (this.onGround?1:0);
this.vel.addScaledVector(F,dt/d.mass);
// --- rotational dynamics: control authority ∝ dynamic pressure, inertia lag, stability ---
const qRef=0.5*RHO0*d.vRef*d.vRef;
const eff=clamp(qbar/qRef,0,1);
const tgt=new THREE.Vector3(
-this.input.pitch*d.pitchRate*eff,
this.input.yaw*d.yawRate*eff,
this.input.roll*d.rollRate*eff);
// static stability: weathervane toward relative wind
tgt.x += alpha*d.stab*eff*0.8;
tgt.y += beta*d.stab*eff*0.5;
// stall drops nose
if(this.stalled) tgt.x += 0.5;
const resp = 1-Math.exp(-dt*(2.5+eff*3)); // inertia: response lag
this.angVel.lerp(tgt,resp);
// integrate quaternion from body rates
const w=this.angVel, wl=w.length();
if(wl>1e-5){
const dq=new THREE.Quaternion().setFromAxisAngle(w.clone().normalize(),wl*dt);
q.premultiply(dq).normalize();
}
this.obj.position.addScaledVector(this.vel,dt);
this.obj.quaternion.copy(q);
// --- ground interaction ---
const p=this.obj.position;
const gh=terrainH(p.x,p.z);
const clearance=this.gear?1.7:1.0;
this.onGround=false;
if(p.y<gh+clearance){
const runway=onRunway(p.x,p.z);
const vs=this.vel.y;
const upDot=this.up().y;
const gentle = vs>-6.5 && upDot>0.6 && runway;
if(gentle || (this.onGroundPrev && runway)){
this.onGround=true;
p.y=gh+clearance;
if(this.vel.y<0) this.vel.y=0;
// constant rolling friction (μ·g) + steering
const mu = this.throttle>0.05 ? 0.02 : 0.06;
const frA = mu*G;
const sp = Math.hypot(this.vel.x,this.vel.z);
if(sp>0.01){ const dec=Math.min(sp,frA*dt);
this.vel.x-=this.vel.x/sp*dec; this.vel.z-=this.vel.z/sp*dec; }
const hdg=Math.atan2(this.fwd().x,this.fwd().z)+Math.PI;
const steer=-this.input.yaw*0.5*clamp(this.speed()/20,0,1);
const yawQ=new THREE.Quaternion().setFromAxisAngle(new THREE.Vector3(0,1,0),steer*dt);
q.premultiply(yawQ);
// level the plane on its gear
const e=new THREE.Euler().setFromQuaternion(q,'YXZ');
e.z=lerp(e.z,0,dt*3);
if(Math.abs(this.input.pitch)<0.05) e.x=lerp(e.x,0,dt*3);
q.setFromEuler(e);
if(!this.onGroundPrev && this.touchdownCb) this.touchdownCb(vs,this.speed());
} else {
this.crash();
}
}
this.onGroundPrev=this.onGround;
this.alt=alt; this.rho=rho; this.airspeed=V;
}
crash(){ if(this.dead)return; this.dead=true; explode(this.obj.position,2.2); this.hp=0; }
fire(){ if(this.cool>0||this.dead)return false; this.cool=0.09; return true; }
}
/* ---------- projectiles & explosions ---------- */
const bullets=[], booms=[];
const bulletGeo=new THREE.SphereGeometry(.35,6,6);
const bulletMat=new THREE.MeshBasicMaterial({color:0xffd98a});
function shoot(p){
const m=new THREE.Mesh(bulletGeo,bulletMat);
const dir=p.fwd();
m.position.copy(p.obj.position).addScaledVector(dir,6);
scene.add(m);
bullets.push({m, vel:dir.multiplyScalar(p.airspeed+900).add(p.vel), life:2.2, from:p});
}
function explode(pos,scale=1){
const m=new THREE.Mesh(new THREE.SphereGeometry(3*scale,12,12),
new THREE.MeshBasicMaterial({color:0xffa030,transparent:true,opacity:.95}));
m.position.copy(pos); scene.add(m);
booms.push({m,t:0,scale});
boomSound();
}
/* ============================================================
AUDIO — synthesized engine, airflow, wind (WebAudio)
============================================================ */
let AC=null, engineNodes=null, windGain=null, windFilter=null, masterGain=null;
function initAudio(){
if(AC) return;
AC=new (window.AudioContext||window.webkitAudioContext)();
masterGain=AC.createGain(); masterGain.gain.value=0.7; masterGain.connect(AC.destination);
// engine: sawtooth stack + rumble noise through lowpass
const eg=AC.createGain(); eg.gain.value=0;
const lp=AC.createBiquadFilter(); lp.type='lowpass'; lp.frequency.value=400;
const o1=AC.createOscillator(); o1.type='sawtooth'; o1.frequency.value=60;
const o2=AC.createOscillator(); o2.type='square'; o2.frequency.value=91;
const og=AC.createGain(); og.gain.value=.5;
o1.connect(og); o2.connect(og); og.connect(lp); lp.connect(eg); eg.connect(masterGain);
// rumble noise
const buf=AC.createBuffer(1,AC.sampleRate*2,AC.sampleRate);
const dat=buf.getChannelData(0); for(let i=0;i<dat.length;i++)dat[i]=Math.random()*2-1;
const ns=AC.createBufferSource(); ns.buffer=buf; ns.loop=true;
const ng=AC.createGain(); ng.gain.value=.35;
ns.connect(ng); ng.connect(lp);
o1.start(); o2.start(); ns.start();
engineNodes={eg,lp,o1,o2};
// wind / airflow: bandpassed noise, volume ∝ q̄
const wf=AC.createBiquadFilter(); wf.type='bandpass'; wf.frequency.value=700; wf.Q.value=.6;
const wg=AC.createGain(); wg.gain.value=0;
const ns2=AC.createBufferSource(); ns2.buffer=buf; ns2.loop=true; ns2.playbackRate.value=1.7;
ns2.connect(wf); wf.connect(wg); wg.connect(masterGain); ns2.start();
windGain=wg; windFilter=wf;
}
function boomSound(){
if(!AC)return;
const b=AC.createBuffer(1,AC.sampleRate*1.2,AC.sampleRate);
const d=b.getChannelData(0);
for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,2.2);
const s=AC.createBufferSource(); s.buffer=b;
const f=AC.createBiquadFilter(); f.type='lowpass'; f.frequency.value=220;
const g=AC.createGain(); g.gain.value=1.2;
s.connect(f); f.connect(g); g.connect(masterGain); s.start();
}
function gunSound(){
if(!AC)return;
const b=AC.createBuffer(1,AC.sampleRate*0.08,AC.sampleRate);
const d=b.getChannelData(0);
for(let i=0;i<d.length;i++) d[i]=(Math.random()*2-1)*Math.pow(1-i/d.length,3);
const s=AC.createBufferSource(); s.buffer=b;
const g=AC.createGain(); g.gain.value=.5;
s.connect(g); g.connect(masterGain); s.start();
}
function updateAudio(p){
if(!AC||!p)return;
const jet=p.def.id!=='skylark';
const rpm=p.rpm, rhoF=p.rho/RHO0;
const base= jet? 90+rpm*260 : 45+rpm*95;
engineNodes.o1.frequency.setTargetAtTime(base,AC.currentTime,.08);
engineNodes.o2.frequency.setTargetAtTime(base*1.51,AC.currentTime,.08);
engineNodes.lp.frequency.setTargetAtTime(300+rpm*2600,AC.currentTime,.1);
engineNodes.eg.gain.setTargetAtTime((0.05+rpm*0.30)*rhoF*(jet?1.15:1),AC.currentTime,.1);
const q=0.5*p.rho*p.airspeed*p.airspeed;
const wv=clamp(q/18000,0,1);
windGain.gain.setTargetAtTime(wv*wv*0.55,AC.currentTime,.15);
windFilter.frequency.setTargetAtTime(400+p.airspeed*9+(p.stalled?300:0),AC.currentTime,.15);
}
/* ============================================================
GAME STATE / SCENARIOS
============================================================ */
let player=null, enemies=[], mode=null, paused=false, missionState='flying', msgT=0, camMode=0;
let selPlane=1, selScn='takeoff';
const enemyModels=[];
function makeEnemy(){
const def=FLEET[1];
const e=new Plane(def,true);
e.hp=45; e.aiT=0; e.aiMode='pursue';
return e;
}
function spawnScenario(){
// clear
if(player){ scene.remove(player.obj); }
enemies.forEach(e=>scene.remove(e.obj)); enemies=[];
bullets.forEach(b=>scene.remove(b.m)); bullets.length=0;
booms.forEach(b=>scene.remove(b.m)); booms.length=0;
const def=FLEET[selPlane];
player=new Plane(def);
player.touchdownCb=(vs,spd)=>{
if(mode==='landing'){
if(onRunway(player.obj.position.x,player.obj.position.z)){
if(Math.abs(vs)<3.2){ setMsg('BUTTER! LANDING SUCCESSFUL — '+vs.toFixed(1)+' m/s'); missionState='won'; }
else if(Math.abs(vs)<5.5){ setMsg('FIRM LANDING — SURVIVABLE ('+vs.toFixed(1)+' m/s)'); missionState='won'; }
else setMsg('HARD LANDING — GEAR DAMAGED');
}
}
};
if(selScn==='takeoff'){
player.obj.position.set(0,terrainH(0,-600)+1.7,-600);
player.quat.setFromEuler(new THREE.Euler(0,Math.PI,0)); // face +z... runway along z
player.vel.set(0,0,0); player.throttle=0;
mode='takeoff'; missionState='flying';
setMsg('THROTTLE UP (W) — ROTATE AT '+Math.round(def.vRef*1.1)+' M/S',4);
} else if(selScn==='landing'){
player.obj.position.set(0,420,-5200);
player.quat.setFromEuler(new THREE.Euler(0,Math.PI,0));
player.vel.copy(player.fwd()).multiplyScalar(def.vRef*1.35);
player.throttle=.45; player.rpm=.45;
mode='landing'; missionState='flying';
setMsg('RUNWAY AHEAD — MANAGE GLIDEPATH & ENERGY',4.5);
} else {
player.obj.position.set(0,900,-2000);
player.quat.setFromEuler(new THREE.Euler(0,0,0));
player.vel.copy(player.fwd()).multiplyScalar(def.vRef*1.6);
player.throttle=.8; player.rpm=.8;
mode='dogfight'; missionState='flying';
for(let i=0;i<3;i++){
const e=makeEnemy();
e.obj.position.set(-600+i*600, 850+Math.random()*300, -1200+i*400);
e.vel.copy(e.fwd()).multiplyScalar(120);
enemies.push(e);
}
setMsg('BANDITS INBOUND — SPACE TO FIRE',4.5);
}
player.onGroundPrev = (selScn==='takeoff');
}
function setMsg(t,dur=3){ const el=document.getElementById('hudMsg'); el.textContent=t; el.style.opacity=1; msgT=dur; }
/* ---------- enemy AI ---------- */
function aiStep(e,dt){
if(e.dead)return;
e.aiT-=dt;
const toP=player.obj.position.clone().sub(e.obj.position);
const dist=toP.length();
if(e.aiT<=0){ e.aiT=2+Math.random()*2; e.aiMode = dist<250?'flee':'pursue'; }
let want;
if(e.aiMode==='pursue') want=toP.normalize();
else want=toP.normalize().negate();
// convert desired direction into body-frame inputs
const qInv=e.quat.clone().invert();
const lw=want.applyQuaternion(qInv); // fwd=-z
e.input.pitch = clamp(lw.y*3,-1,1);
e.input.roll = clamp(-lw.x*3,-1,1);
e.input.yaw = clamp(-lw.x*0.6,-1,1);
e.throttle = dist>400?1:0.65;
// guns
if(dist<420 && e.aiMode==='pursue'){
const aim=e.fwd().dot(toP.clone().normalize());
if(aim>0.9975 && Math.random()<0.04) { if(e.fire()){ shoot(e); gunSound(0.2); } }
}
}
/* ============================================================
HUD
============================================================ */
const $=id=>document.getElementById(id);
const ladder=$('pitchLadder');
for(let a=-60;a<=60;a+=10){ if(!a)continue;
const d=document.createElement('div'); d.className='pl';
d.style.top=(-a*4)+'px'; d.innerHTML='<span>'+a+'</span>'; ladder.appendChild(d); }
function updateHUD(){
const p=player; if(!p)return;
const e=new THREE.Euler().setFromQuaternion(p.quat,'YXZ');
const pitch=THREE.MathUtils.radToDeg(e.x), roll=THREE.MathUtils.radToDeg(e.z);
const hdg=(THREE.MathUtils.radToDeg(Math.atan2(p.fwd().x,-p.fwd().z))+360)%360;
$('horizon').style.transform=`rotate(${-roll}deg) translateY(${pitch*4}px)`;
ladder.style.transform=`rotate(${-roll}deg)`;
ladder.style.top='50%'; ladder.style.left='50%';
$('hudSpeed').textContent=`KIAS ${(p.airspeed*1.944).toFixed(0)}\nMACH ${(p.airspeed/340).toFixed(2)}\nα ${(THREE.MathUtils.radToDeg(p.alpha)).toFixed(1)}°\nG ${p.gForce.toFixed(1)}`;
$('hudAlt').textContent=`ALT ${Math.round(p.obj.position.y*3.281)} ft\nV/S ${Math.round(p.vel.y*196.85)} fpm\nρ ${(p.rho).toFixed(3)}`;
$('hudHead').textContent=`HDG ${hdg.toFixed(0).padStart(3,'0')}°`;
$('hudThr').textContent=`THR ${'▮'.repeat(Math.round(p.rpm*10)).padEnd(10,'▯')} ${Math.round(p.rpm*100)}%\nRPM ${Math.round(800+p.rpm*(p.def.id==='skylark'?1900:7200))}${p.stalled?'\n⚠ STALL':''}`;
$('hudHp').textContent=`HULL ${'█'.repeat(Math.max(0,Math.round(p.hp/p.def.hp*10)))}${'░'.repeat(10-Math.max(0,Math.round(p.hp/p.def.hp*10)))}`;
if(mode==='dogfight'){
let n=enemies.filter(x=>!x.dead).length;
let nearest=1e9;
enemies.forEach(x=>{ if(!x.dead) nearest=Math.min(nearest,x.obj.position.distanceTo(p.obj.position)); });
$('hudTarget').textContent=`BANDITS ${n}\nNEAREST ${nearest>9e8?'—':Math.round(nearest)+' m'}`;
} else if(mode==='takeoff'){
$('hudTarget').textContent=`CLIMB TO 1000 ft\n${Math.round(Math.min(100,p.obj.position.y*3.281/1000*100))}%`;
} else {
const d=Math.hypot(p.obj.position.x,p.obj.position.z);
$('hudTarget').textContent=`RUNWAY ${onRunway(p.obj.position.x,p.obj.position.z)?'LOCKED':Math.round(d)+' m'}`;
}
if(missionState!=='flying'){ $('hudState').textContent='ESC — HANGAR'; }
else $('hudState').textContent='';
// G-stress redout
$('redflash').style.opacity = p.gForce>7? clamp((p.gForce-7)/4,0,.9) : 0;
}
/* ============================================================
CAMERA
============================================================ */
const camPos=new THREE.Vector3(), camLook=new THREE.Vector3();
function updateCamera(dt){
const p=player; if(!p)return;
if(camMode===0){ // chase
const back=p.fwd().clone().multiplyScalar(-(14+p.def.span*0.6));
const desired=p.obj.position.clone().add(back).add(new THREE.Vector3(0,4.5,0));
camPos.lerp(desired,1-Math.exp(-dt*4));
camera.position.copy(camPos);
camLook.lerp(p.obj.position.clone().addScaledVector(p.vel,0.25),1-Math.exp(-dt*6));
camera.lookAt(camLook);
} else { // cockpit-ish
camera.position.copy(p.obj.position).add(p.up().clone().multiplyScalar(1.1)).add(p.fwd().multiplyScalar(1.5));
camera.quaternion.copy(p.quat);
}
const gh=terrainH(camera.position.x,camera.position.z)+3;
if(camera.position.y<gh) camera.position.y=gh;
}
/* ============================================================
MAIN LOOP
============================================================ */
let last=performance.now();
function loop(now){
requestAnimationFrame(loop);
const dt=Math.min(0.033,(now-last)/1000); last=now;
if(paused||!player){ renderer.render(scene,camera); return; }
// input
const p=player;
if(missionState==='flying'&&!p.dead){
p.input.pitch=(keys['ArrowUp']?1:0)-(keys['ArrowDown']?1:0);
p.input.roll =(keys['ArrowLeft']?1:0)-(keys['ArrowRight']?1:0);
p.input.yaw =(keys['KeyD']?1:0)-(keys['KeyA']?1:0);
if(keys['KeyW']) p.throttle=clamp(p.throttle+dt*0.45,0,1);
if(keys['KeyS']) p.throttle=clamp(p.throttle-dt*0.45,0,1);
if(keys['Space']&&mode==='dogfight'){ if(p.fire()){ shoot(p); gunSound(); } }
} else { p.input.pitch=p.input.roll=p.input.yaw=0; }
p.cool-=dt;
p.step(dt);
// mission logic
if(missionState==='flying'){
if(mode==='takeoff'&&!p.dead&&p.obj.position.y*3.281>1000){ missionState='won'; setMsg('TAKEOFF COMPLETE — ALT 1000 ft'); }
if(mode==='landing'&&p.dead){ missionState='lost'; setMsg('AIRFRAME LOST'); }
if(mode==='dogfight'){
enemies.forEach(e=>aiStep(e,dt));
enemies.forEach(e=>{ if(!e.dead){ e.cool-=dt; e.step(dt);
const gh=terrainH(e.obj.position.x,e.obj.position.z);
if(e.obj.position.y<gh+2){ e.crash(); } } });
// bullet hits
for(let i=bullets.length-1;i>=0;i--){
const b=bullets[i]; b.life-=dt;
b.m.position.addScaledVector(b.vel,dt);
let hit=false;
const targets=b.from===p?enemies:[p];
for(const t of targets){
if(!t.dead && b.m.position.distanceTo(t.obj.position)<t.def.span*0.7){
t.hp-=8; hit=true;
if(t.hp<=0){ t.crash(); if(t===p){missionState='lost';setMsg('YOU WERE SHOT DOWN');} else setMsg('BANDIT DOWN!'); }
break;
}
}
const gh=terrainH(b.m.position.x,b.m.position.z);
if(b.m.position.y<gh||b.life<=0) hit=true;
if(hit){ scene.remove(b.m); bullets.splice(i,1); }
}
if(!enemies.some(e=>!e.dead)){ missionState='won'; setMsg('AIRSPACE CLEAR — ALL BANDITS DESTROYED'); }
}
if(p.dead&&mode!=='dogfight'){ missionState='lost'; setMsg('AIRFRAME LOST'); }
if(p.dead&&mode==='dogfight'&&missionState==='flying'){ missionState='lost'; setMsg('YOU WERE DOWNED'); }
}
// explosions
for(let i=booms.length-1;i>=0;i--){
const b=booms[i]; b.t+=dt;
b.m.scale.setScalar(1+b.t*9); b.m.material.opacity=Math.max(0,.95-b.t*1.4);
if(b.t>0.8){ scene.remove(b.m); booms.splice(i,1); }
}
if(msgT>0){ msgT-=dt; if(msgT<=0) $('hudMsg').style.opacity=0; }
updateCamera(dt);
updateHUD();
updateAudio(p);
renderer.render(scene,camera);
}
requestAnimationFrame(loop);
/* ============================================================
MENU WIRING
============================================================ */
function buildMenu(){
const fleet=$('fleet');
FLEET.forEach((d,i)=>{
const c=document.createElement('div');
c.className='card'+(i===selPlane?' sel':'');
c.innerHTML=`<h3>${d.name}</h3><div class="role">${d.role}</div>
${['SPD','THR','AGI','ARM'].map(k=>`<div class="stat"><span>${k}</span><div class="bar" style="flex:1;margin:0 8px"><i style="width:${d.stats[k]*100}%"></i></div></div>`).join('')}
<div class="stat"><span>MTOW</span><b>${d.mass.toLocaleString()} kg</b></div>
<div class="stat"><span>T/W</span><b>${(d.maxThrust/(d.mass*G)).toFixed(2)}</b></div>
<div class="stat" style="margin-top:6px;color:#7fa8c9">${d.desc}</div>`;
c.onclick=()=>{ selPlane=i; document.querySelectorAll('.card').forEach((x,j)=>x.classList.toggle('sel',j===i)); };
fleet.appendChild(c);
});
document.querySelectorAll('.scn').forEach(s=>{
s.onclick=()=>{ selScn=s.dataset.s; document.querySelectorAll('.scn').forEach(x=>x.classList.toggle('sel',x===s)); };
});
$('launchBtn').onclick=launch;
}
function launch(){
initAudio(); if(AC.state==='suspended')AC.resume();
buildTerrain();
spawnScenario();
$('menu').classList.add('hidden');
$('hud').classList.add('on');
paused=false;
}
addEventListener('keydown',e=>{
if(e.code==='Escape'){
if($('menu').classList.contains('hidden')){ paused=false; backToMenu(); }
}
if(e.code==='KeyC') camMode=1-camMode;
});
function backToMenu(){
$('menu').classList.remove('hidden');
$('hud').classList.remove('on');
if(engineNodes) engineNodes.eg.gain.setTargetAtTime(0,AC.currentTime,.1);
if(windGain) windGain.gain.setTargetAtTime(0,AC.currentTime,.1);
player=null;
}
addEventListener('resize',()=>{
camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix();
renderer.setSize(innerWidth,innerHeight);
});
buildMenu();
// test hook
window.__sim = { get player(){return player;}, set player(v){player=v;}, get enemies(){return enemies;},
get missionState(){return missionState;}, set missionState(v){missionState=v;},
get paused(){return paused;}, set paused(v){paused=v;}, get bullets(){return bullets;},
shoot, explode };
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"e8dbf827-7c8f-4bdb-b7bc-4dbb0b869ac4","tokensIn":8108000,"tokensOut":41708,"tokensTotal":8149708,"cost":0,"turns":99,"toolCalls":98,"failedToolCalls":1,"timestamp":"2026-08-21T22:41:37.939Z"} -->