Flight Simulator
Ornith 1.5 35B A3B · full

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 51,425 bytes · SHA-256 fd03f081baa7
<!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:#05070d; font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif; color:#e8f0ff; }
#app { position:fixed; inset:0; }
canvas { display:block; }
/* ---------- MENUS ---------- */
.screen { position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center;
background:radial-gradient(ellipse at 50% 30%, #1a2a4a 0%, #0a0f1c 60%, #04060d 100%); z-index:50; }
.hidden { display:none !important; }
.title { font-size:64px; font-weight:800; letter-spacing:6px; background:linear-gradient(90deg,#4fc3ff,#8affc0,#4fc3ff);
-webkit-background-clip:text; background-clip:text; color:transparent; text-shadow:0 0 40px rgba(79,195,255,.25); }
.subtitle { font-size:15px; letter-spacing:8px; color:#7fa8d4; margin-top:6px; text-transform:uppercase; }
.panel { margin-top:34px; width:min(920px,92vw); }
.section-label { font-size:12px; letter-spacing:4px; color:#6f8fb8; text-transform:uppercase; margin:18px 0 10px; }
.card-grid { display:grid; grid-template-columns:repeat(3,1fr); gap:14px; }
.card { background:linear-gradient(160deg,#141d31,#0c1220); border:1px solid #24354f; border-radius:12px;
padding:16px; cursor:pointer; transition:.18s; user-select:none; }
.card:hover { border-color:#4fc3ff; transform:translateY(-3px); box-shadow:0 10px 30px rgba(79,195,255,.18); }
.card.selected { border-color:#8affc0; box-shadow:0 0 0 2px #8affc0, 0 10px 30px rgba(138,255,192,.25); }
.card h3 { font-size:18px; margin-bottom:2px; }
.card .role { font-size:11px; color:#7fa8d4; text-transform:uppercase; letter-spacing:2px; margin-bottom:12px; }
.stat { display:flex; justify-content:space-between; font-size:12px; margin:5px 0; color:#b9cbe4; }
.stat .bar { flex:1; margin:0 8px; height:6px; background:#1c2840; border-radius:3px; overflow:hidden; }
.stat .bar i { display:block; height:100%; background:linear-gradient(90deg,#4fc3ff,#8affc0); }
.stat .v { width:34px; text-align:right; color:#8fd0ff; font-variant-numeric:tabular-nums; }
.mode-grid { display:grid; grid-template-columns:repeat(2,1fr); gap:14px; }
.mode { background:linear-gradient(160deg,#141d31,#0c1220); border:1px solid #24354f; border-radius:12px;
padding:20px; cursor:pointer; transition:.18s; }
.mode:hover { border-color:#4fc3ff; box-shadow:0 10px 30px rgba(79,195,255,.18); }
.mode.selected { border-color:#8affc0; box-shadow:0 0 0 2px #8affc0; }
.mode h3 { font-size:18px; margin-bottom:6px; }
.mode p { font-size:13px; color:#93aecf; line-height:1.5; }
button.play { margin-top:30px; padding:16px 52px; font-size:18px; font-weight:700; letter-spacing:3px; color:#04121e;
background:linear-gradient(90deg,#4fc3ff,#8affc0); border:none; border-radius:10px; cursor:pointer;
transition:.18s; text-transform:uppercase; }
button.play:hover { transform:scale(1.04); box-shadow:0 0 40px rgba(138,255,192,.4); }
.controls-hint { margin-top:26px; font-size:12px; color:#6f8fb8; text-align:center; line-height:1.9; }
.controls-hint kbd { background:#141d31; border:1px solid #2a3d5c; border-bottom-width:2px; border-radius:4px;
padding:1px 7px; font-family:inherit; color:#cfe3ff; }
/* ---------- HUD ---------- */
#hud { position:absolute; inset:0; pointer-events:none; z-index:30; font-variant-numeric:tabular-nums; }
.hud-el { position:absolute; font-size:13px; }
.hud-box { background:rgba(6,14,24,.55); border:1px solid rgba(79,195,255,.35); border-radius:8px; padding:10px 14px;
backdrop-filter:blur(3px); }
.hud-box .lbl { font-size:10px; letter-spacing:2px; color:#6f8fb8; text-transform:uppercase; }
.hud-box .val { font-size:22px; font-weight:700; color:#cfe9ff; }
.hud-box .val small { font-size:12px; font-weight:400; color:#7fa8d4; }
#hud-alt { top:20px; left:20px; }
#hud-speed { top:20px; right:20px; text-align:right; }
#hud-engine { bottom:20px; left:20px; min-width:210px; }
#hud-engine .lbl { margin-top:8px; }
#hud-engine .bar { width:180px; height:8px; background:#1c2840; border-radius:4px; overflow:hidden; margin-top:3px; }
#hud-engine .bar i { display:block; height:100%; width:0%; background:linear-gradient(90deg,#40e070,#f7ff50,#ff5050); transition:.1s; }
#hud-throttle { bottom:20px; right:20px; text-align:right; min-width:190px; }
#hud-throttle .bar { width:180px; height:8px; background:#1c2840; border-radius:4px; overflow:hidden; margin-left:auto; margin-top:3px; }
#hud-throttle .bar i { display:block; height:100%; width:0%; background:linear-gradient(90deg,#4fc3ff,#8affc0); transition:.1s; }
#attitude { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:220px; height:220px; border-radius:50%;
border:2px solid rgba(138,255,192,.5); overflow:hidden; opacity:.85; }
#att-canvas { width:100%; height:100%; }
#hud-pitch { position:absolute; top:24px; left:50%; transform:translateX(-50%); text-align:center; }
#hud-hdg { position:absolute; bottom:20px; left:50%; transform:translateX(-50%); text-align:center; }
#stall-warn { position:absolute; top:120px; left:50%; transform:translateX(-50%); font-size:26px; font-weight:800;
letter-spacing:4px; color:#ff5050; text-shadow:0 0 20px rgba(255,80,80,.7); opacity:0; transition:.15s; }
#stall-warn.show { opacity:1; animation:blink .5s infinite; }
@keyframes blink { 50% { opacity:.3; } }
#crosshair { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); width:30px; height:30px; opacity:0; }
#crosshair::before,#crosshair::after { content:''; position:absolute; background:rgba(138,255,192,.7); }
#crosshair::before { left:50%; top:0; width:1px; height:100%; transform:translateX(-50%); }
#crosshair::after { top:50%; left:0; height:1px; width:100%; transform:translateY(-50%); }
#dogfight-ui { position:absolute; inset:0; }
#hud-timer { top:20px; left:50%; transform:translateX(-50%); text-align:center; }
#hud-timer .val { font-size:30px; }
#hud-kills { top:70px; left:50%; transform:translateX(-50%); text-align:center; }
#hud-locked { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); margin-top:40px; font-size:16px;
color:#ff5050; letter-spacing:2px; opacity:0; transition:.15s; }
#hud-locked.show { opacity:1; }
#message { position:absolute; top:180px; left:50%; transform:translateX(-50%); font-size:20px; font-weight:600;
color:#eaf4ff; text-shadow:0 2px 10px rgba(0,0,0,.6); opacity:0; transition:.4s; text-align:center; max-width:80vw; }
#message.show { opacity:1; }
#banner { position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); text-align:center; z-index:60;
background:rgba(4,8,16,.9); border:1px solid #2a3d5c; border-radius:16px; padding:40px 60px; pointer-events:auto; }
#banner h2 { font-size:34px; margin-bottom:8px; }
#banner p { color:#93aecf; margin-bottom:24px; font-size:15px; }
#banner button { padding:14px 40px; font-size:16px; font-weight:700; letter-spacing:2px; background:linear-gradient(90deg,#4fc3ff,#8affc0);
border:none; border-radius:10px; cursor:pointer; text-transform:uppercase; }
#pause-hint { position:absolute; bottom:8px; left:50%; transform:translateX(-50%); font-size:11px; color:#56708f; letter-spacing:1px; }
#load-msg { position:absolute; bottom:40px; left:50%; transform:translateX(-50%); color:#8fd0ff; letter-spacing:3px; font-size:14px; }
</style>
</head>
<body>
<div id="app"></div>
<!-- ================= START / MENU SCREEN ================= -->
<div id="menu" class="screen">
<div class="title">APEX AERO</div>
<div class="subtitle">High-Fidelity Flight Simulator</div>
<div class="panel">
<div class="section-label">Select Aircraft</div>
<div class="card-grid" id="aircraft-grid"></div>
</div>
<div class="panel">
<div class="section-label">Mission</div>
<div class="mode-grid" id="mode-grid"></div>
</div>
<button class="play" id="fly-btn">Deploy</button>
<div class="controls-hint">
<kbd>Mouse</kbd> Pitch & Roll · <kbd>A</kbd>/<kbd>D</kbd> Yaw · <kbd>W</kbd>/<kbd>S</kbd> Throttle · <kbd>Shift</kbd>/<kbd>Ctrl</kbd> Boost/Turbulence · <kbd>Space</kbd> Brake · <kbd>F</kbd> Cannons · <kbd>C</kbd> Camera · <kbd>P</kbd> Pause
</div>
</div>
<!-- ================= HUD ================= -->
<div id="hud" class="hidden">
<div id="hud-alt" class="hud-el hud-box">
<div class="lbl">Altitude</div>
<div class="val"><span id="v-alt">0</span> <small>m</small></div>
</div>
<div id="hud-speed" class="hud-el hud-box">
<div class="lbl">Airspeed</div>
<div class="val"><span id="v-spd">0</span> <small>km/h</small></div>
</div>
<div id="hud-pitch" class="hud-el hud-box">
<div class="lbl">Pitch</div>
<div class="val"><span id="v-pitch">0</span><small>°</small></div>
</div>
<div id="hud-hdg" class="hud-el hud-box">
<div class="lbl">Heading</div>
<div class="val"><span id="v-hdg">036</span><small>°</small></div>
</div>
<div id="attitude"><canvas id="att-canvas" width="220" height="220"></canvas></div>
<div id="hud-engine" class="hud-el hud-box">
<div class="lbl">Engine</div>
<div class="val"><span id="v-rpm">0</span> <small>%</small></div>
<div class="bar"><i id="rpm-bar"></i></div>
<div class="lbl">Temp</div>
<div class="bar"><i id="heat-bar" style="background:linear-gradient(90deg,#4fc3ff,#f7ff50,#ff5050)"></i></div>
<div class="lbl">Fuel</div>
<div class="bar"><i id="fuel-bar" style="background:linear-gradient(90deg,#8affc0,#4fc3ff)"></i></div>
</div>
<div id="hud-throttle" class="hud-el hud-box">
<div class="lbl">Throttle</div>
<div class="val"><span id="v-thr">0</span> <small>%</small></div>
<div class="bar"><i id="thr-bar"></i></div>
</div>
<div id="stall-warn">⚠ STALL ⚠</div>
<div id="hud-timer" class="hud-el hud-box hidden"><div class="lbl">Time</div><div class="val" id="v-timer">90</div></div>
<div id="hud-kills" class="hud-el hud-box hidden"><div class="lbl">Enemies</div><div class="val" id="v-kills">0</div></div>
<div id="hud-locked" class="hud-el">TARGET LOCKED</div>
<div id="message"></div>
<div id="pause-hint">[P] Pause · [C] Camera · [F] Cannons</div>
<div id="crosshair"></div>
</div>
<div id="banner" class="hidden">
<h2 id="banner-title">Paused</h2>
<p id="banner-text"></p>
<button id="banner-btn">Resume</button>
</div>
<script src="https://unpkg.com/three@0.160.0/build/three.min.js"></script>
<script>
/* ============================================================
APEX AERO — single-file flight simulator
Three.js scene + custom Newtonian aerodynamics
============================================================ */
'use strict';
/* ============================================================
1. AIRCRAFT DEFINITIONS (differentiated physics)
============================================================ */
const AIRCRAFT = [
{
id:'falcon', name:'Falcon M-9', role:'Air Superiority',
desc:'Balanced fighter. Strong thrust, agile, high top speed.',
mass: 8200, // kg
wingArea: 28, // m^2
maxLift: 1.55, // Cl max
inducedFactor: 0.085,// CDi = ki * Cl^2
profileDrag: 0.022, // CD0
maxThrust: 165000, // N
weightRatio: 0.19, // thrust/mass
liftSlope: 5.6, // per radian
stallAoA: 0.28, // rad (~16°)
rollRate: 2.4, // rad/s at full control
pitchRate: 1.5, // rad/s
yawRate: 1.1,
dragCoeffBonus: 0.0, // flaps/gear extra
color: 0x3a7bd5, accent: 0x8affc0,
},
{
id:'viper', name:'Viper S', role:'Interceptor',
desc:'Lightning-fast, low maneuverability. Built for speed & diving.',
mass: 6100,
wingArea: 19,
maxLift: 1.25,
inducedFactor: 0.055,
profileDrag: 0.015,
maxThrust: 195000,
weightRatio: 0.31,
liftSlope: 6.2,
stallAoA: 0.24,
rollRate: 3.0,
pitchRate: 1.9,
yawRate: 1.3,
dragCoeffBonus: 0.0,
color: 0xff7043, accent: 0xffd74f,
},
{
id:'sentinel', name:'Sentinel G', role:'Glider',
desc:'Ultra-light. Excellent lift & turn rate, low thrust. Rides thermals.',
mass: 1150,
wingArea: 16,
maxLift: 1.95,
inducedFactor: 0.042,
profileDrag: 0.018,
maxThrust: 18000,
weightRatio: 0.015,
liftSlope: 4.9,
stallAoA: 0.34,
rollRate: 2.1,
pitchRate: 1.2,
yawRate: 0.9,
dragCoeffBonus: 0.0,
color: 0xeceff1, accent: 0x4fc3ff,
}
];
/* ============================================================
2. ATMOSPHERE / CONSTANTS
============================================================ */
const G = 9.81; // gravity m/s^2
const SEA_DENSITY = 1.225; // kg/m^3
// Exponential atmosphere model
function airDensity(alt){ return SEA_DENSITY * Math.exp(-alt/8500); }
function temperature(alt){ return 15 - alt/100*0.65; }
/* ============================================================
3. NOISE (procedural terrain)
============================================================ */
function hash2(x,y){
const s = Math.sin(x*127.1 + y*311.7)*43758.5453;
return s - Math.floor(s);
}
function smoothNoise(x,y){
const xi=Math.floor(x), yi=Math.floor(y);
const xf=x-xi, yf=y-yi;
const u=xf*xf*(3-2*xf), v=yf*yf*(3-2*yf);
const a=hash2(xi,yi), b=hash2(xi+1,yi), c=hash2(xi,yi+1), d=hash2(xi+1,yi+1);
return a*(1-u)*(1-v)+b*u*(1-v)+c*(1-u)*v+d*u*v;
}
function fbm(x,y){
let amp=0.5, freq=1, sum=0, norm=0;
for(let i=0;i<6;i++){ sum+=amp*smoothNoise(x*freq,y*freq); norm+=amp; amp*=0.5; freq*=2; }
return sum/norm;
}
// Deterministic RNG for terrain seed
let TERRAIN_SEED = 1;
function seededNoise(x,y){
const nx = x + TERRAIN_SEED*31.7, ny = y + TERRAIN_SEED*47.3;
return fbm(nx*0.0009, ny*0.0009);
}
// Terrain height (meters). Flat runway plateau near origin; dramatic mountains farther out.
function terrainHeight(x,z){
const d = Math.hypot(x,z);
const fade = Math.min(1, d/7000); // 0 near origin, 1 far away
const flat = Math.max(0, 1 - d/3500); // fully flat within 3.5km
const n = seededNoise(x,z);
const rid = seededNoise(x*1.7+100, z*1.7+100);
let h = (n*2-1)*0.6 + rid*rid*0.8; // -0.6..1.4
const relief = 0.15 + 0.85*fade; // gentle near origin, dramatic far
let alt = h*1400*relief;
const dome = Math.max(0, 1 - d/5000);
alt += 30*dome*dome; // +30m runway plateau
alt = alt*(1-flat) + 30*flat; // blend to flat plateau near origin
if(alt<SEA_LEVEL) alt *= 0.4; // flatten ocean floor
return alt;
}
function isOcean(x,z){ return terrainHeight(x,z) < 2; }
const SEA_LEVEL = 2;
/* ============================================================
4. THREE.JS SETUP
============================================================ */
let scene, camera, renderer, clock;
let terrainMesh, skyMesh, sun;
const terrainVels = []; // vertex positions reference
let aircraftGroup, enemyGroup;
let runwayStart, runwayDir, runwayLen=1400, runwayWidth=45;
function initThree(){
scene = new THREE.Scene();
scene.fog = new THREE.FogExp2(0x9fbfe0, 0.00028);
camera = new THREE.PerspectiveCamera(62, innerWidth/innerHeight, 1, 30000);
renderer = new THREE.WebGLRenderer({ antialias:true, powerPreference:'high-performance' });
renderer.setPixelRatio(Math.min(devicePixelRatio,2));
renderer.setSize(innerWidth, innerHeight);
renderer.shadowMap.enabled = true;
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
renderer.outputColorSpace = THREE.SRGBColorSpace;
document.getElementById('app').appendChild(renderer.domElement);
clock = new THREE.Clock();
// Lighting — PBR-ish
const hemi = new THREE.HemisphereLight(0xbfd8ff, 0x4a6a45, 0.9);
scene.add(hemi);
sun = new THREE.DirectionalLight(0xfff2d6, 2.1);
sun.position.set(400,600,200);
sun.castShadow = true;
sun.shadow.mapSize.set(2048,2048);
const d=1200;
sun.shadow.camera.left=-d; sun.shadow.camera.right=d;
sun.shadow.camera.top=d; sun.shadow.camera.bottom=-d;
sun.shadow.camera.far=6000;
sun.shadow.bias=-0.0004;
scene.add(sun);
scene.add(new THREE.AmbientLight(0x405060, 0.4));
buildSky();
buildTerrain();
buildRunway();
}
function buildSky(){
const geo = new THREE.SphereGeometry(26000, 32, 16);
const mat = new THREE.ShaderMaterial({
side: THREE.BackSide, depthWrite:false,
uniforms:{ top:{value:new THREE.Color(0x1b4fa8)}, bot:{value:new THREE.Color(0xbfe0ff)} },
vertexShader:`varying vec3 vp; void main(){ vp=position; gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
fragmentShader:`varying vec3 vp; uniform vec3 top; uniform vec3 bot;
void main(){ float h=clamp((normalize(vp).y+0.15)/1.1,0.0,1.0); gl_FragColor=vec4(mix(bot,top,h),1.0);}`
});
skyMesh = new THREE.Mesh(geo, mat);
scene.add(skyMesh);
// Sun sprite
const c=document.createElement('canvas'); c.width=c.height=128;
const g=c.getContext('2d');
const grd=g.createRadialGradient(64,64,4,64,64,64);
grd.addColorStop(0,'rgba(255,250,230,1)'); grd.addColorStop(0.2,'rgba(255,230,150,.9)');
grd.addColorStop(1,'rgba(255,220,120,0)');
g.fillStyle=grd; g.fillRect(0,0,128,128);
const tex=new THREE.CanvasTexture(c);
const sunSprite=new THREE.Sprite(new THREE.SpriteMaterial({map:tex,transparent:true,depthWrite:false,blending:THREE.AdditiveBlending}));
sunSprite.scale.set(1600,1600,1);
sunSprite.position.copy(sun.position).multiplyScalar(8);
scene.add(sunSprite);
}
let terrainGeo, terrainPosAttr;
function buildTerrain(){
const SEG=220, EXTENT=12000;
terrainGeo = new THREE.PlaneGeometry(EXTENT*2, EXTENT*2, SEG, SEG);
terrainGeo.rotateX(-Math.PI/2);
terrainPosAttr = terrainGeo.attributes.position;
const colors=new Float32Array(terrainPosAttr.count*3);
const c=new THREE.Color();
for(let i=0;i<terrainPosAttr.count;i++){
const x=terrainPosAttr.getX(i), z=terrainPosAttr.getZ(i);
const h=terrainHeight(x,z);
terrainPosAttr.setY(i,h);
// color by height & slope
let col;
if(h<SEA_LEVEL+1){ col=new THREE.Color(0xc2b280); } // sand
else if(h<200){ col=new THREE.Color(0x4a7a3a); } // grass
else if(h<700){ col=new THREE.Color(0x5a6b4a); } // hills
else if(h<1200){ col=new THREE.Color(0x7a7468); } // rock
else { col=new THREE.Color(0xf2f6ff); } // snow
// blend toward water near ocean
if(isOcean(x,z)){ col.setHex(0x2a5a86); col.lerp(new THREE.Color(0x123a63),0.5); }
colors[i*3]=col.r; colors[i*3+1]=col.g; colors[i*3+2]=col.b;
}
terrainGeo.setAttribute('color',new THREE.BufferAttribute(colors,3));
terrainGeo.computeVertexNormals();
const mat=new THREE.MeshStandardMaterial({ vertexColors:true, roughness:0.95, metalness:0.0, flatShading:false });
terrainMesh=new THREE.Mesh(terrainGeo, mat);
terrainMesh.receiveShadow=true;
scene.add(terrainMesh);
// Water plane
const waterMat=new THREE.MeshStandardMaterial({ color:0x1c4a78, transparent:true, opacity:0.82, roughness:0.25, metalness:0.4 });
const water=new THREE.Mesh(new THREE.PlaneGeometry(26000,26000), waterMat);
water.rotateX(-Math.PI/2); water.position.y=SEA_LEVEL;
scene.add(water);
}
function buildRunway(){
// Runway aligned along +X, at a flat clearing near origin (we flatten area)
const geo = new THREE.PlaneGeometry(runwayLen, runwayWidth, 1, 1);
geo.rotateX(-Math.PI/2);
const mat = new THREE.MeshStandardMaterial({ color:0x2b2f36, roughness:0.9 });
const mesh = new THREE.Mesh(geo, mat);
const ry = terrainHeight(runwayLen/2+40, 0)+1.2;
mesh.position.set(runwayLen/2+40, ry, 0);
mesh.receiveShadow=true;
scene.add(mesh);
// Center line markings
const lineMat=new THREE.MeshBasicMaterial({color:0xdfe7ef});
for(let i=0;i<28;i++){
const m=new THREE.Mesh(new THREE.PlaneGeometry(12,2), lineMat);
m.rotateX(-Math.PI/2);
m.position.set(-runwayLen/2+20+i*54, ry+0.15, 0);
scene.add(m);
}
// Edge lights
const edgeMat=new THREE.MeshBasicMaterial({color:0x8affc0});
for(let i=0;i<=20;i++){
const t=i/20; const x=-runwayLen/2+20+t*runwayLen;
for(const s of [-1,1]){
const l=new THREE.Mesh(new THREE.SphereGeometry(0.8,6,6), edgeMat);
l.position.set(x, ry+1, s*runwayWidth/2);
scene.add(l);
}
}
runwayStart = new THREE.Vector3(-runwayLen/2+40, ry, 0);
runwayDir = new THREE.Vector3(1,0,0);
}
// Draw a simple but recognizable plane mesh
function buildPlaneMesh(def, isEnemy){
const g=new THREE.Group();
const body=new THREE.MeshStandardMaterial({ color:def.color, roughness:0.4, metalness:0.5 });
const accent=new THREE.MeshStandardMaterial({ color:def.accent, roughness:0.3, metalness:0.6, emissive:def.accent, emissiveIntensity:0.15 });
const dark=new THREE.MeshStandardMaterial({ color:0x222833, roughness:0.6, metalness:0.3 });
// Fuselage runs along +X (forward). Nose at +X, engine at -X.
const fus=new THREE.Mesh(new THREE.CylinderGeometry(0.55,0.75,7,12), body);
fus.rotation.z=-Math.PI/2; g.add(fus);
// Nose cone at +X
const nose=new THREE.Mesh(new THREE.ConeGeometry(0.55,2.2,12), accent);
nose.rotation.y=Math.PI/2; nose.position.x=4.5; g.add(nose);
// Cockpit
const cock=new THREE.Mesh(new THREE.SphereGeometry(0.5,12,8), new THREE.MeshStandardMaterial({color:0x8fd0ff, roughness:0.1, metalness:0.8, transparent:true, opacity:0.75}));
cock.scale.set(1.6,0.8,1); cock.position.set(1.2,0.5,0); g.add(cock);
// Main wings (span along +Y/-Y, chord along X)
const wspan=Math.max(4, Math.sqrt(def.wingArea)*1.6);
const wingShape=new THREE.Mesh(new THREE.BoxGeometry(2.0,0.12,wspan), body);
wingShape.position.set(0.2,0,0); g.add(wingShape);
// Tail wing
const tw=new THREE.Mesh(new THREE.BoxGeometry(1.0,0.1,wspan*0.42), body);
tw.position.set(-3.2,0.1,0); g.add(tw);
// Vertical stabilizer
const tailV=new THREE.Mesh(new THREE.BoxGeometry(1.2,1.4,0.15), accent);
tailV.position.set(-3.0,0.7,0); g.add(tailV);
// Engine bell at -X
const bell=new THREE.Mesh(new THREE.TorusGeometry(0.5,0.12,8,12), dark);
bell.rotation.y=Math.PI/2; bell.position.x=-3.5; g.add(bell);
// Engine glow at -X
const glow=new THREE.Mesh(new THREE.CircleGeometry(0.45,12), new THREE.MeshBasicMaterial({color:0xff9030, transparent:true, opacity:0.85}));
glow.rotation.y=Math.PI/2; glow.rotation.z=-Math.PI/2; glow.position.x=-3.55; glow.name='glow'; g.add(glow);
g.traverse(o=>{ if(o.isMesh){ o.castShadow=true; }});
return g;
}
/* ============================================================
5. INPUT
============================================================ */
const input={ mx:0, my:0, throttle:0, throttleTarget:0, yaw:0, boost:false, brake:false, cannons:false, fire:false };
let mouseLocked=false;
function initInput(){
addEventListener('resize',()=>{ camera.aspect=innerWidth/innerHeight; camera.updateProjectionMatrix(); renderer.setSize(innerWidth,innerHeight); });
addEventListener('mousemove',e=>{
if(mouseLocked){ input.mx += (e.movementX||0); input.my += (e.movementY||0); }
});
addEventListener('keydown',e=>{
switch(e.code){
case 'KeyW': input.throttleTarget=Math.min(1,input.throttleTarget+0.08); break;
case 'KeyS': input.throttleTarget=Math.max(0,input.throttleTarget-0.08); break;
case 'KeyA': input.yaw=1; break;
case 'KeyD': input.yaw=-1; break;
case 'ShiftLeft': case 'ShiftRight': input.boost=true; break;
case 'ControlLeft': case 'ControlRight': input.boost=false; break;
case 'Space': input.brake=true; break;
case 'KeyF': input.fire=true; break;
case 'KeyC': switchCamera(); break;
case 'KeyP': togglePause(); break;
}
});
addEventListener('keyup',e=>{
switch(e.code){
case 'KeyW': case 'KeyS': input.throttleTarget=0.5; break; // momentary
case 'KeyA': input.yaw=0; break;
case 'KeyD': input.yaw=0; break;
case 'ShiftLeft': case 'ShiftRight': input.boost=false; break;
case 'ControlLeft': case 'ControlRight': input.boost=false; break;
case 'Space': input.brake=false; break;
case 'KeyF': input.fire=false; break;
}
});
renderer && renderer.domElement.addEventListener('click',()=>{ if(game.state==='fly'&&!game.paused) renderer.domElement.requestPointerLock(); });
}
/* ============================================================
6. FLIGHT CONTROLLER (physics state)
============================================================ */
function makePlane(def, isEnemy){
return {
def, isEnemy,
pos:new THREE.Vector3(),
vel:new THREE.Vector3(),
quat:new THREE.Quaternion(),
angularVel:new THREE.Vector3(), // body space (pqr)
throttle:0, rpm:0, heat:0.2, fuel:1,
onGround:false, stalled:false,
alive:true,
// enemy AI
aiState:'hunt', aiTarget:null, aiTimer:0,
// trail
canFire:true, cooldown:0,
};
}
// The physics core. dt in seconds.
function updatePlane(p, dt){
const def=p.def;
dt=Math.min(dt,0.033); // clamp for stability
// ---- Orientation helpers ----
const forward=new THREE.Vector3(1,0,0).applyQuaternion(p.quat);
const up=new THREE.Vector3(0,1,0).applyQuaternion(p.quat);
const right=new THREE.Vector3(0,0,1).applyQuaternion(p.quat);
// ---- Angle of attack & sideslip ----
// velocity in body frame
const velBody=p.vel.clone().applyQuaternion(p.quat.conjugate());
const vMag=velBody.length();
const vx=velBody.x, vy=velBody.y, vz=velBody.z;
// Angle of attack (pitch of velocity relative to fuselage)
const aoa=Math.atan2(vy, Math.hypot(vx,vz));
// sideslip
const beta=Math.atan2(vz, Math.hypot(vx,vy));
// ---- Air density ----
const rho=airDensity(p.pos.y);
// ---- Lift ----
// Effective Cl = liftSlope * aoa (linear region), capped at stall
let Cl;
const aoaStall=def.stallAoA;
if(Math.abs(aoa)<=aoaStall){
Cl=def.liftSlope*aoa;
} else {
// Post-stall: lift drops
const s=Math.sign(aoa);
const overAoA=Math.max(0,Math.abs(aoa)-aoaStall);
Cl= s*def.maxLift*Math.exp(-2.5*overAoA);
}
p.stalled = Math.abs(aoa)>aoaStall*0.9 && vMag>20;
const q=0.5*rho*vMag*vMag; // dynamic pressure
const liftMag=q*def.wingArea*Cl;
// Lift direction: perpendicular to velocity, in the plane of up & velocity (pull up)
// Use "up" projected perpendicular to velocity
const vNorm=velBody.clone().normalize();
let liftDir=up.clone().sub(vNorm.clone().multiplyScalar(up.dot(vNorm))).normalize();
if(liftDir.lengthSq()<1e-6) liftDir=up.clone();
const lift=liftDir.multiplyScalar(liftMag);
// ---- Drag ----
// Profile drag (CD0) + induced drag (ki*Cl^2) + extra (speed brakes)
let CD=def.profileDrag + def.inducedFactor*Cl*Cl + (p.brake?0.06:0);
const dragDir=velBody.lengthSq()>1e-6? velBody.clone().normalize().multiplyScalar(-1):new THREE.Vector3();
const dragMag=q*def.wingArea*CD;
const drag=dragDir.multiplyScalar(dragMag);
// ---- Thrust ----
// Thrust falls off with air density (jet/prop)
const thrustMag=p.throttle*def.maxThrust*Math.max(0.35, rho/SEA_DENSITY);
const thrust=forward.clone().multiplyScalar(thrustMag);
// ---- Gravity ----
const weight=new THREE.Vector3(0,-def.mass*G,0);
// ---- Sum forces -> acceleration ----
const acc=lift.add(drag).add(thrust).add(weight).divideScalar(def.mass);
// Integrate velocity
p.vel.addScaledVector(acc,dt);
// ---- Aerodynamic damping / control torques ----
// Control effectiveness scales with airspeed (need airflow on surfaces)
const controlEff=Math.min(1, vMag/40); // needs ~40 m/s for full authority
const boostMul=input.boost?1.6:1;
// Desired angular accel from input (enemy handled separately)
// Body axes: x=forward(pitch about Y), z=right(roll about X)
let pitchCmd=0, rollCmd=0, yawCmd=0;
if(!p.isEnemy){
pitchCmd=input.my*def.pitchRate; // pull back (my>0) -> nose up (positive AOA)
rollCmd=input.mx*def.rollRate; // move right (mx>0) -> bank right
yawCmd=input.yaw*def.yawRate;
} else {
applyEnemyControl(p, dt);
}
// Angular acceleration toward commanded rates, with inertia (damped 2nd-order)
const targetOmega=new THREE.Vector3(
rollCmd*controlEff*boostMul, // roll about body X
yawCmd*controlEff*boostMul, // yaw about body Y
pitchCmd*controlEff*boostMul // pitch about body Z
);
// Yaw about body-y is small effect; add weathercock stability (align beta)
// Damp angular velocity toward commanded (inertia lag)
const damp=1-Math.exp(-6*dt);
const newOm=new THREE.Vector3();
newOm.x=p.angularVel.x+(targetOmega.x-p.angularVel.x)*damp;
newOm.y=p.angularVel.y+(targetOmega.y-p.angularVel.y)*damp;
newOm.z=p.angularVel.z+(targetOmega.z-p.angularVel.z)*damp;
// Add sideslip weathercock (yaw to zero beta)
newOm.y += -beta*2.5*controlEff*dt*(1-damp);
p.angularVel.copy(newOm);
// Natural damping of spin (gentle decay toward zero rotation)
p.angularVel.multiplyScalar(Math.exp(-2*dt));
// ---- Apply rotation (body rates) ----
// qdot = 0.5 * omega ⊗ q
const w=p.angularVel;
const quat=p.quat;
let qdot=new THREE.Quaternion(w.x*0.5, w.y*0.5, w.z*0.5, 0).multiply(quat);
quat.x+=qdot.x*dt; quat.y+=qdot.y*dt; quat.z+=qdot.z*dt; quat.w+=qdot.w*dt;
quat.normalize();
// ---- Integrate position ----
p.pos.addScaledVector(p.vel,dt);
// ---- Terrain / ground collision ----
const ground=terrainHeight(p.pos.x,p.pos.z)+ (p.onGround?1.5:0.5);
if(p.pos.y<ground){
p.pos.y=ground;
// Ground interaction: kill downward velocity, apply friction
if(p.vel.y<0){
if(p.onGround){
p.vel.y=0;
// Rolling resistance (small) + speed brakes; thrust must overcome this to accelerate.
const fric = p.brake ? 0.12 : 0.02;
const hs = Math.hypot(p.vel.x, p.vel.z);
if(hs > 0.05){
const factor = Math.max(0, 1 - fric*G*dt/hs);
p.vel.x *= factor;
p.vel.z *= factor;
}
} else {
p.vel.y=0;
p.vel.multiplyScalar(0.7);
}
}
if(!p.onGround){
// Just touched ground
p.onGround=true;
}
} else {
p.onGround=false;
}
// ---- Fuel & heat & rpm ----
p.rpm = Math.min(100, p.rpm + (p.throttle*100 - p.rpm)*(1-Math.exp(-3*dt)));
p.heat = Math.min(1.6, p.heat + (p.throttle*0.9 + p.rpm*0.002 - 0.15)*(1-Math.exp(-2*dt)));
const fuelUse = (0.00002 + p.throttle*0.00025)*def.mass*dt;
p.fuel = Math.max(0, p.fuel - fuelUse);
return { aoa, vMag, q, liftMag, dragMag, Cl, CD };
}
/* ============================================================
7. ENEMY AI
============================================================ */
function applyEnemyControl(p, dt){
const target=game.player;
if(!target||!target.alive){ p.aiState='circle'; return; }
p.aiTimer-=dt;
const toEnemy=target.pos.clone().sub(p.pos);
const dist=toEnemy.length();
const ahead=toEnemy.normalize();
// Compute desired orientation via lookAt
const desired=new THREE.Object3D();
desired.position.copy(p.pos);
desired.lookAt(target.pos);
const desiredQuat=new THREE.Quaternion().setFromEuler(new THREE.Euler(desired.rotation.x, desired.rotation.y, desired.rotation.z));
// Blend current heading toward desired (turn limited by roll/pitch rate)
const cur=new THREE.Object3D(); cur.quaternion.copy(p.quat);
const curF=new THREE.Vector3(1,0,0).applyQuaternion(p.quat);
// Lead target slightly
const leadTime=dist/(game.playerBulletSpeed||900);
// (simplified: aim at current pos with small lead)
// Turn rate limit
const turnRate=1.8*dt;
const newF=curF.clone().lerp(ahead, Math.min(1,turnRate)); newF.normalize();
// Rebuild quaternion: keep up roughly level
const q=new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(1,0,0), newF);
// blend
p.quat.copy(p.quat.clone().slerp(q, Math.min(1,turnRate*1.5))).normalize();
// Maintain distance: climb/descend to stay in band
const desiredAlt=600+Math.sin(game.time*0.3)*200;
const altErr=desiredAlt-p.pos.y;
// pitch up/down by adjusting target euler
const up=new THREE.Vector3(0,1,0).applyQuaternion(p.quat);
// banking for turn
const dotF=curF.dot(ahead);
const bank=Math.max(-1,Math.min(1,(dotF-0.7)/0.3));
p.bank=bank;
// Fire when roughly aimed & in range
if(dotF>0.98 && dist<1200){
p.fireTimer=(p.fireTimer||0)-dt;
if(p.fireTimer<=0){ fireProjectile(p, false); p.fireTimer=1.2; }
}
// throttle: maintain speed
if(p.vel.length()<120) p.throttle=Math.min(1,p.throttle+0.02*dt*30);
else p.throttle=Math.max(0.4,p.throttle-0.01*dt*30);
}
/* ============================================================
8. PROJECTILES (cannons)
============================================================ */
let projectiles=[];
const playerBulletSpeed=1100;
function fireProjectile(p, isPlayer){
const f=new THREE.Vector3(1,0,0).applyQuaternion(p.quat);
const origin=p.pos.clone().addScaledVector(f,6);
const geo=new THREE.SphereGeometry(0.6,6,6);
const mat=new THREE.MeshBasicMaterial({ color:isPlayer?0x8affc0:0xff5050 });
const m=new THREE.Mesh(geo,mat);
m.position.copy(origin);
scene.add(m);
projectiles.push({ mesh:m, vel:f.clone().multiplyScalar(playerBulletSpeed + p.vel.length()*0.3), life:2.5, isPlayer });
}
/* ============================================================
9. GAME STATE / FLOW
============================================================ */
const game={
state:'menu', // menu | fly | over
mode:'takeoff', // takeoff | dogfight
aircraftIndex:0,
player:null,
enemies:[],
time:0,
paused:false,
camMode:0,
kills:0,
timeLeft:90,
missionDone:false,
playerBulletSpeed,
};
// Menu building
let selectedAircraft=0, selectedMode='takeoff';
function buildMenu(){
const grid=document.getElementById('aircraft-grid');
grid.innerHTML='';
AIRCRAFT.forEach((a,i)=>{
const c=document.createElement('div');
c.className='card'+(i===0?' selected':'');
c.dataset.i=i;
const pct=v=>Math.round(v*100)+'%';
const bar=(label,val)=>`<div class="stat"><span>${label}</span><span class="bar"><i style="width:${pct(val)}"></i></span><span class="v">${Math.round(val*100)}</span></div>`;
c.innerHTML=`<h3>${a.name}</h3><div class="role">${a.role}</div>
${bar('Speed',a.maxThrust/a.mass)}
${bar('Agility',a.rollRate)}
${bar('Lift',a.maxLift)}
${bar('Thrust',a.weightRatio)}`;
c.onclick=()=>{ document.querySelectorAll('#aircraft-grid .card').forEach(x=>x.classList.remove('selected')); c.classList.add('selected'); selectedAircraft=i; };
grid.appendChild(c);
});
const mg=document.getElementById('mode-grid');
mg.innerHTML='';
const modes=[
{id:'takeoff',t:'Takeoff & Landing',d:'Approach the runway in wind, rotate at the right speed, and touch down gently on the centerline.'},
{id:'dogfight',t:'Dogfight',d:'Pursue and shoot down enemy fighters while managing speed, energy and G-load.'},
];
modes.forEach(mo=>{
const d=document.createElement('div');
d.className='mode'+(mo.id===selectedMode?' selected':'');
d.dataset.id=mo.id;
d.innerHTML=`<h3>${mo.t}</h3><p>${mo.d}</p>`;
d.onclick=()=>{ document.querySelectorAll('#mode-grid .mode').forEach(x=>x.classList.remove('selected')); d.classList.add('selected'); selectedMode=mo.id; };
mg.appendChild(d);
});
document.getElementById('fly-btn').onclick=startMission;
}
function message(txt, dur){
const el=document.getElementById('message');
el.textContent=txt; el.classList.add('show');
clearTimeout(message._t);
message._t=setTimeout(()=>el.classList.remove('show'), dur||3000);
}
function startMission(){
game.aircraftIndex=selectedAircraft;
game.mode=selectedMode;
game.state='fly';
game.time=0; game.kills=0; game.timeLeft=90; game.missionDone=false; game.paused=false;
document.getElementById('menu').classList.add('hidden');
document.getElementById('hud').classList.remove('hidden');
// Build player
const def=AIRCRAFT[game.aircraftIndex];
game.player=makePlane(def,false);
if(game.mode==='takeoff'){
game.player.pos.copy(runwayStart);
game.player.vel.set(0,0,0);
} else {
game.player.pos.set(0, 700, 0);
game.player.vel.set(150,0,0);
}
// Build mesh
aircraftGroup=buildPlaneMesh(def,false);
scene.add(aircraftGroup);
// Enemies for dogfight
game.enemies=[];
if(game.mode==='dogfight'){
enemyGroup=new THREE.Group(); scene.add(enemyGroup);
const defs=[AIRCRAFT[1], AIRCRAFT[0], AIRCRAFT[1]];
defs.forEach((d,i)=>{
const e=makePlane(d,true);
const ang=i*(Math.PI*2/3);
e.pos.set(Math.cos(ang)*2200, 650+Math.random()*300, Math.sin(ang)*2200);
e.vel.set(Math.cos(ang+1.5)*220,0,Math.sin(ang+1.5)*220);
game.enemies.push(e);
enemyGroup.add(buildPlaneMesh(d,true));
});
document.getElementById('hud-timer').classList.remove('hidden');
document.getElementById('hud-kills').classList.remove('hidden');
document.getElementById('crosshair').style.opacity='1';
message('DOWN ENEMIES — 90s',3000);
}
// pointer lock + audio start
setTimeout(()=>{ if(renderer.domElement) renderer.domElement.requestPointerLock(); },50);
initAudio();
audio.start();
}
function endMission(win, title, text){
game.state='over';
const b=document.getElementById('banner');
document.getElementById('banner-title').textContent=title;
document.getElementById('banner-text').textContent=text;
document.getElementById('banner-btn').textContent='Play Again';
b.classList.remove('hidden');
document.exitPointerLock && document.exitPointerLock();
document.getElementById('banner-btn').onclick=()=>{ b.classList.add('hidden'); document.getElementById('hud').classList.add('hidden'); document.getElementById('menu').classList.remove('hidden'); game.state='menu'; audio.stop(); };
}
function togglePause(){
if(game.state!=='fly') return;
game.paused=!game.paused;
const b=document.getElementById('banner');
if(game.paused){
document.getElementById('banner-title').textContent='Paused';
document.getElementById('banner-text').textContent='Take a breath, pilot.';
document.getElementById('banner-btn').textContent='Resume';
b.classList.remove('hidden');
} else {
b.classList.add('hidden');
}
}
document.getElementById('banner-btn').addEventListener('click',()=>{ if(game.state==='fly') togglePause(); });
/* ============================================================
10. CAMERA
============================================================ */
let camDist=22, camHeight=7;
function switchCamera(){ game.camMode=(game.camMode+1)%3; }
function updateCamera(dt){
const p=game.player;
const f=new THREE.Vector3(1,0,0).applyQuaternion(p.quat);
const u=new THREE.Vector3(0,1,0).applyQuaternion(p.quat);
const r=new THREE.Vector3(0,0,1).applyQuaternion(p.quat);
if(game.camMode===0){ // chase
const target=p.pos.clone().addScaledVector(f,camDist).addScaledVector(u,-camHeight*0.4);
camera.position.lerp(target, 1-Math.exp(-6*dt));
const look=p.pos.clone().addScaledVector(f,30);
camera.lookAt(look);
} else if(game.camMode===1){ // cockpit
camera.position.copy(p.pos.clone().addScaledVector(f,1.2).addScaledVector(u,0.5));
camera.quaternion.copy(p.quat);
} else { // orbit-ish top
const target=p.pos.clone().addScaledVector(u,40);
camera.position.lerp(target,1-Math.exp(-4*dt));
camera.lookAt(p.pos);
}
// Keep camera above terrain
if(camera.position.y<terrainHeight(camera.position.x,camera.position.z)+8){
camera.position.y=terrainHeight(camera.position.x,camera.position.z)+8;
}
}
/* ============================================================
11. AUDIO (WebAudio, dynamic)
============================================================ */
let audio=null;
function initAudio(){
if(audio) return;
const AC=window.AudioContext||window.webkitAudioContext;
audio=new AC();
const ctx=audio.ctx;
audio.master=ctx.createGain(); audio.master.gain.value=0.9; audio.master.connect(ctx.destination);
// --- Engine: 2 oscillators (base + harmonic) + sub, filtered ---
const engineGain=ctx.createGain(); engineGain.gain.value=0.25; engineGain.connect(audio.master);
const engineFilter=ctx.createBiquadFilter();
engineFilter.type='lowpass'; engineFilter.frequency.value=900; engineFilter.Q.value=1.5;
engineFilter.connect(engineGain);
const osc1=ctx.createOscillator(); osc1.type='sawtooth'; osc1.frequency.value=60;
const osc2=ctx.createOscillator(); osc2.type='square'; osc2.frequency.value=120;
const sub=ctx.createOscillator(); sub.type='sine'; sub.frequency.value=40;
const subGain=ctx.createGain(); subGain.gain.value=0.3; sub.connect(engineGain);
osc1.connect(engineFilter); osc2.connect(engineFilter);
osc1.start(); osc2.start(); sub.start();
// --- Wind: noise through lowpass ---
const bufferSize=2*ctx.sampleRate;
const noiseBuffer=ctx.createBuffer(1,bufferSize,ctx.sampleRate);
const nd=noiseBuffer.getChannelData(0);
for(let i=0;i<bufferSize;i++) nd[i]=Math.random()*2-1;
const windSrc=ctx.createBufferSource(); windSrc.buffer=noiseBuffer; windSrc.loop=true;
const windFilter=ctx.createBiquadFilter(); windFilter.type='lowpass'; windFilter.frequency.value=500; windFilter.Q.value=0.5;
const windGain=ctx.createGain(); windGain.gain.value=0.0;
windSrc.connect(windFilter); windFilter.connect(windGain); windGain.connect(audio.master); windSrc.start();
audio.engineGain=engineGain; audio.engineFilter=engineFilter;
audio.osc1=osc1; audio.osc2=osc2; audio.sub=sub; audio.subGain=subGain;
audio.windGain=windGain; audio.windFilter=windFilter;
// --- Alarm / stall ---
const alarm=ctx.createOscillator(); alarm.type='square'; alarm.frequency.value=1200;
const alarmGain=ctx.createGain(); alarmGain.gain.value=0;
alarm.connect(alarmGain); alarmGain.connect(audio.master); alarm.start();
audio.alarm=alarm; audio.alarmGain=alarmGain;
// --- Explosion ---
audio.explosion=()=>{
const n=ctx.createBufferSource();
const b=ctx.createBuffer(1,ctx.sampleRate*0.6,ctx.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);
n.buffer=b;
const g=ctx.createGain(); g.gain.value=0.6;
const f=ctx.createBiquadFilter(); f.type='lowpass'; f.frequency.value=800;
n.connect(f); f.connect(g); g.connect(audio.master); n.start();
};
audio._t=0;
}
function updateAudio(dt,p){
if(!audio) return;
const ctx=audio.ctx; const t=ctx.currentTime;
const rpm=p.rpm/100, thr=p.throttle, spd=p.vel.length();
// Engine pitch ~ idle 55Hz to redline ~ 320Hz scaled by rpm
const baseFreq=55+rpm*280;
audio.osc1.frequency.setTargetAtTime(baseFreq,t,0.08);
audio.osc2.frequency.setTargetAtTime(baseFreq*2,t,0.08);
audio.sub.frequency.setTargetAtTime(baseFreq*0.5,t,0.08);
audio.engineFilter.frequency.setTargetAtTime(600+thr*1400,t,0.1);
audio.engineGain.gain.setTargetAtTime(0.15+thr*0.35+rpm*0.05,t,0.1);
// Wind with speed & altitude (thinner air = less wind even at speed)
const rho=airDensity(p.pos.y);
const windAmt=Math.min(0.5, (spd/250)*(rho/SEA_DENSITY)+0.03);
audio.windGain.gain.setTargetAtTime(windAmt,t,0.15);
audio.windFilter.frequency.setTargetAtTime(300+spd*6,t,0.15);
// Stall alarm
if(p.stalled){
audio.alarmGain.gain.setTargetAtTime(0.12,t,0.05);
audio.alarm.frequency.setTargetAtTime(1100+Math.sin(t*12)*300,t,0.05);
} else {
audio.alarmGain.gain.setTargetAtTime(0,t,0.1);
}
}
/* ============================================================
12. HUD UPDATE
============================================================ */
const attCanvas=document.getElementById('att-canvas');
const attCtx=attCanvas.getContext('2d');
function drawAttitude(p){
const w=220,h=220,cx=w/2,cy=h/2;
attCtx.clearRect(0,0,w,h);
attCtx.save();
attCtx.beginPath(); attCtx.arc(cx,cy,w/2,0,true); attCtx.clip();
// rotate by roll, translate by pitch
const roll=-p.quat.toEuler(new THREE.Euler(0,0,0,'YXZ')).z;
const pitch=(p.quat.toEuler(new THREE.Euler('YXZ')).x);
attCtx.translate(cx,cy);
attCtx.rotate(roll);
const off=pitch*140;
// sky & ground
attCtx.fillStyle='#3a7bd5'; attCtx.fillRect(-200,-200,400,200+off);
attCtx.fillStyle='#6b4a2a'; attCtx.fillRect(-200,off,400,200);
attCtx.strokeStyle='#ffffff'; attCtx.lineWidth=2;
attCtx.beginPath(); attCtx.moveTo(-200,off); attCtx.lineTo(200,off); attCtx.stroke();
attCtx.restore();
// Fixed aircraft symbol
attCtx.strokeStyle='#8affc0'; attCtx.lineWidth=2.5;
attCtx.beginPath(); attCtx.moveTo(cx-40,cy); attCtx.lineTo(cx-12,cy); attCtx.moveTo(cx+12,cy); attCtx.lineTo(cx+40,cy); attCtx.stroke();
attCtx.beginPath(); attCtx.moveTo(cx,cy-3); attCtx.lineTo(cx,cy+3); attCtx.stroke();
// Roll ticks
attCtx.fillStyle='#8affc0';
for(const a of [-60,-30,30,60]){ attCtx.save(); attCtx.translate(cx,cy); attCtx.rotate(a*Math.PI/180); attCtx.fillRect(-1,-96,4,10); attCtx.restore(); }
}
function updateHUD(){
const p=game.player;
const spd=Math.round(p.vel.length()*3.6); // m/s -> km/h
const alt=Math.max(0,Math.round(p.pos.y));
document.getElementById('v-alt').textContent=alt;
document.getElementById('v-spd').textContent=spd;
// pitch in degrees
const euler=new THREE.Euler().setFromQuaternion(p.quat,'YXZ');
let degPitch=Math.round(THREE.MathUtils.radToDeg(euler.x));
let degHdg=Math.round(THREE.MathUtils.radToDeg(euler.y));
degHdg=((degHdg%360)+360)%360;
document.getElementById('v-pitch').textContent=(degPitch>=0?'+':'')+degPitch;
document.getElementById('v-hdg').textContent=String(degHdg).padStart(3,'0');
// engine
document.getElementById('v-rpm').textContent=Math.round(p.rpm);
document.getElementById('rpm-bar').style.width=Math.min(100,p.rpm)+'%';
document.getElementById('heat-bar').style.width=Math.min(100,p.heat*100)+'%';
document.getElementById('fuel-bar').style.width=(p.fuel*100)+'%';
document.getElementById('v-thr').textContent=Math.round(p.throttle*100);
document.getElementById('thr-bar').style.width=(p.throttle*100)+'%';
// stall
document.getElementById('stall-warn').classList.toggle('show', p.stalled && !p.onGround);
// timer / kills
if(game.mode==='dogfight'){
document.getElementById('v-timer').textContent=Math.max(0,Math.ceil(game.timeLeft));
document.getElementById('v-kills').textContent=game.kills+'/'+game.enemies.length;
}
drawAttitude(p);
}
/* ============================================================
13. MAIN LOOP
============================================================ */
let lastFireShot=0;
function animate(){
requestAnimationFrame(animate);
const dt=Math.min(clock.getDelta(),0.05);
if(game.state==='fly' && !game.paused){
game.time+=dt;
// --- Player input smoothing (throttle) ---
const p=game.player;
p.throttleTarget=input.throttleTarget;
p.throttle += (p.throttleTarget-p.throttle)*(1-Math.exp(-4*dt));
// --- Fire cannons ---
p.cooldown-=dt;
if(p.fire && p.cooldown<=0 && p.fuel>0){
fireProjectile(p,true);
p.cooldown=0.12;
}
// --- Physics ---
const info=updatePlane(p,dt);
// sync mesh
if(aircraftGroup){
aircraftGroup.position.copy(p.pos);
aircraftGroup.quaternion.copy(p.quat);
const glow=aircraftGroup.getObjectByName('glow');
if(glow) glow.material.opacity=0.3+p.throttle*0.7;
}
// --- Dogfight logic ---
if(game.mode==='dogfight'){
game.timeLeft-=dt;
// update enemies
game.enemies.forEach((e,idx)=>{
if(!e.alive) return;
updatePlane(e,dt);
const m=enemyGroup.children[idx];
if(m){ m.position.copy(e.pos); m.quaternion.copy(e.quat); }
});
// projectiles
for(let i=projectiles.length-1;i>=0;i--){
const pr=projectiles[i];
pr.mesh.position.addScaledVector(pr.vel,dt);
pr.life-=dt;
let hit=false;
if(pr.isPlayer){
for(let j=0;j<game.enemies.length;j++){
const e=game.enemies[j];
if(e.alive && pr.mesh.position.distanceTo(e.pos)<12){
e.alive=false; e.state='dead';
game.kills++;
if(game.enemies[j].isEnemy && enemyGroup.children[j]) enemyGroup.children[j].visible=false;
audio&&audio.explosion();
message('ENEMY DOWN!');
hit=true;
break;
}
}
} else {
if(pr.mesh.position.distanceTo(p.pos)<10){
// player hit — reduce fuel, flash
p.fuel=Math.max(0,p.fuel-0.15);
audio&&audio.explosion();
hit=true;
}
}
if(pr.life<=0||hit){ scene.remove(pr.mesh); projectiles.splice(i,1); }
}
// lock-on display
// (simplified)
if(game.timeLeft<=0 && !game.missionDone){
game.missionDone=true;
endMission(game.kills>=game.enemies.length*0.6?1:0,
game.kills>=Math.ceil(game.enemies.length*0.6)?'VICTORY':'MISSION OVER',
`Enemies down: ${game.kills}/${game.enemies.length}`);
}
}
// --- Takeoff/Landing checks ---
if(game.mode==='takeoff'){
if(!p.onGround && p.vel.length()>5){
p.groundedJustLeft=true;
}
// landing scoring
if(p.pos.y<=terrainHeight(p.pos.x,p.pos.z)+1.6 && p.vel.length()<45){
// on runway & slow -> good
}
// Game over if crashed hard
const eulerY=new THREE.Euler().setFromQuaternion(p.quat,'YXZ');
const bank=Math.abs(eulerY.z);
if(p.onGround && bank>1.4 && p.vel.length()>30){
message('CRASHED — wing struck ground',4000);
endMission(0,'CRASH','You spun out on the runway. Manage bank angle on touchdown.');
}
}
// --- Out of bounds reset ---
if(p.pos.y<-50){ p.pos.set(0,800,0); p.vel.set(120,0,0); message('Bail out — respawned'); }
// --- Camera ---
updateCamera(dt);
// --- Audio ---
updateAudio(dt,p);
// --- HUD ---
updateHUD();
} else if(game.state==='fly' && game.paused){
// keep camera static
}
renderer.render(scene,camera);
}
/* ============================================================
14. BOOT
============================================================ */
function boot(){
buildMenu();
initThree();
initInput();
animate();
message('Select your aircraft and mission.',4000);
}
window.addEventListener('DOMContentLoaded', boot);
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"a1e853c1-ad77-4abb-bd16-75f97d70fbcc","tokensIn":4472688,"tokensOut":134258,"tokensTotal":4606946,"cost":0,"turns":59,"toolCalls":58,"failedToolCalls":1,"timestamp":"2026-08-23T22:20:07.631Z"} -->