Flight Simulator
thinkingcap qwen3.6 27b · basic

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 46,793 bytes · SHA-256 552a5bf7a807
<!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}
body{overflow:hidden;background:#000;font-family:'Segoe UI',Arial,sans-serif;color:#fff;user-select:none}
canvas{display:block}
#menu{position:fixed;top:0;left:0;width:100%;height:100%;background:linear-gradient(135deg,#0a0a2e 0%,#1a1a4e 50%,#0d0d3d 100%);display:flex;flex-direction:column;align-items:center;justify-content:center;z-index:1000}
#menu h1{font-size:4em;text-transform:uppercase;letter-spacing:8px;background:linear-gradient(90deg,#ff6b35,#f7c948,#ff6b35);-webkit-background-clip:text;-webkit-text-fill-color:transparent;margin-bottom:10px;text-shadow:none}
#menu h2{font-size:1.2em;color:#8888cc;letter-spacing:4px;margin-bottom:40px;font-weight:300}
.plane-select{display:flex;gap:30px;margin-bottom:40px;flex-wrap:wrap;justify-content:center;padding:0 20px}
.plane-card{width:260px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:24px;cursor:pointer;transition:all 0.3s;text-align:center}
.plane-card:hover,.plane-card.selected{background:rgba(255,107,53,0.15);border-color:#ff6b35;transform:translateY(-5px)}
.plane-card h3{color:#f7c948;margin-bottom:8px;font-size:1.3em}
.plane-card p{font-size:0.85em;color:#aaa;line-height:1.5}
.plane-stats{margin-top:12px;text-align:left}
.plane-stats div{display:flex;justify-content:space-between;margin:4px 0;font-size:0.8em}
.stat-bar{width:80px;height:6px;background:rgba(255,255,255,0.1);border-radius:3px;overflow:hidden;display:inline-block;vertical-align:middle;margin-left:8px}
.stat-fill{height:100%;background:#ff6b35;border-radius:3px}
.scenario-select{display:flex;gap:20px;margin-bottom:40px;flex-wrap:wrap;justify-content:center;padding:0 20px}
.scenario-card{width:280px;background:rgba(255,255,255,0.05);border:1px solid rgba(255,255,255,0.1);border-radius:12px;padding:24px;cursor:pointer;transition:all 0.3s;text-align:center}
.scenario-card:hover,.scenario-card.selected{background:rgba(247,201,72,0.15);border-color:#f7c948;transform:translateY(-5px)}
.scenario-card h3{color:#ff6b35;margin-bottom:8px}
#startBtn{padding:16px 60px;font-size:1.2em;background:linear-gradient(90deg,#ff6b35,#f7c948);border:none;border-radius:8px;color:#fff;cursor:pointer;text-transform:uppercase;letter-spacing:3px;font-weight:bold;transition:all 0.3s}
#startBtn:hover{transform:scale(1.05);box-shadow:0 0 30px rgba(255,107,53,0.5)}
#hud{position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:100;display:none}
.hud-top{display:flex;justify-content:space-between;padding:20px 30px}
.hud-panel{background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.15);border-radius:8px;padding:12px 16px;backdrop-filter:blur(5px)}
.hud-panel h4{font-size:0.7em;text-transform:uppercase;color:#f7c948;letter-spacing:2px;margin-bottom:6px}
.hud-value{font-size:1.4em;font-weight:bold;font-family:'Courier New',monospace}
.hud-unit{font-size:0.6em;color:#aaa;margin-left:4px}
.hud-center{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);text-align:center}
.hud-pitch-line{width:200px;height:2px;background:rgba(255,255,255,0.3);position:relative;margin:0 auto}
.hud-crosshair{width:20px;height:20px;border:2px solid rgba(255,107,53,0.8);border-radius:50%;position:absolute;top:-9px;left:-90px}
.hud-bottom{position:absolute;bottom:20px;left:50%;transform:translateX(-50%);display:flex;gap:20px}
.hud-bar-container{width:150px}
.hud-bar-label{font-size:0.7em;text-transform:uppercase;color:#aaa;margin-bottom:4px;display:flex;justify-content:space-between}
.hud-bar{height:8px;background:rgba(255,255,255,0.1);border-radius:4px;overflow:hidden}
.hud-bar-fill{height:100%;border-radius:4px;transition:width 0.1s}
.throttle-fill{background:linear-gradient(90deg,#4CAF50,#f7c948,#ff6b35)}
.g-meter-fill{background:linear-gradient(90deg,#4CAF50,#2196F3,#f44336)}
.hud-compass{position:absolute;top:20px;left:50%;transform:translateX(-50%);width:300px;height:30px;background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.15);border-radius:8px;display:flex;align-items:center;justify-content:center;font-family:'Courier New',monospace;font-size:0.9em}
.hud-compass-mark{position:absolute;width:2px;height:8px;background:#f7c948}
.hud-warning{position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);font-size:2em;color:#ff3333;text-shadow:0 0 20px #ff0000;display:none;animation:pulse 0.5s infinite alternate}
@keyframes pulse{from{opacity:1}to{opacity:0.5}}
.hud-controls-hint{position:absolute;bottom:80px;left:50%;transform:translateX(-50%);font-size:0.7em;color:rgba(255,255,255,0.4);text-align:center}
#minimap{position:absolute;top:60px;right:30px;width:150px;height:150px;background:rgba(0,0,0,0.5);border:1px solid rgba(255,255,255,0.2);border-radius:8px}
#minimapCanvas{width:100%;height:100%;border-radius:7px}
.hud-ai-targets{position:absolute;top:60px;left:30px;background:rgba(0,0,0,0.4);border:1px solid rgba(255,255,255,0.15);border-radius:8px;padding:10px;font-size:0.8em;display:none}
.hud-ai-targets h4{color:#ff6b35;margin-bottom:6px;font-size:0.7em;text-transform:uppercase;letter-spacing:2px}
.target-entry{display:flex;align-items:center;gap:8px;margin:4px 0}
.target-dot{width:8px;height:8px;border-radius:50%;background:#ff3333}
.hud-mission{position:absolute;top:60px;left:50%;transform:translateX(-50%);text-align:center;font-size:0.9em;color:#f7c948;display:none}
</style>
</head>
<body>
<div id="menu">
<h1>Apex Aero</h1>
<h2>Flight Simulator</h2>
<div class="plane-select" id="planeSelect"></div>
<div class="scenario-select" id="scenarioSelect"></div>
<button id="startBtn">Launch Mission</button>
</div>
<div id="hud">
<div class="hud-top">
<div class="hud-panel">
<h4>Airspeed</h4>
<span class="hud-value" id="hudSpeed">0</span><span class="hud-unit">KTS</span>
</div>
<div class="hud-panel">
<h4>Altitude</h4>
<span class="hud-value" id="hudAlt">0</span><span class="hud-unit">FT</span>
</div>
<div class="hud-panel">
<h4>Vertical Speed</h4>
<span class="hud-value" id="hudVS">0</span><span class="hud-unit">FPM</span>
</div>
<div class="hud-panel">
<h4>G-Force</h4>
<span class="hud-value" id="hudG">1.0</span><span class="hud-unit">G</span>
</div>
</div>
<div class="hud-compass" id="hudCompass">
<span id="headingText">360°</span>
</div>
<div class="hud-center">
<div class="hud-pitch-line"><div class="hud-crosshair"></div></div>
</div>
<div class="hud-bottom">
<div class="hud-bar-container">
<div class="hud-bar-label"><span>Throttle</span><span id="throttleVal">0%</span></div>
<div class="hud-bar"><div class="hud-bar-fill throttle-fill" id="throttleBar" style="width:0%"></div></div>
</div>
<div class="hud-bar-container">
<div class="hud-bar-label"><span>G-Meter</span><span id="gVal">1.0G</span></div>
<div class="hud-bar"><div class="hud-bar-fill g-meter-fill" id="gBar" style="width:50%"></div></div>
</div>
<div class="hud-panel" style="min-width:120px">
<h4>Aircraft</h4>
<span class="hud-value" id="hudPlane" style="font-size:1em">Falcon</span>
</div>
</div>
<div class="hud-warning" id="hudWarning">STALL WARNING</div>
<div class="hud-controls-hint">W/S: Pitch | A/D: Roll | Q/E: Yaw | Shift/Ctrl: Throttle | R: Reset View</div>
<canvas id="minimapCanvas"></canvas>
<div class="hud-ai-targets" id="hudTargets"><h4>Hostiles</h4><div id="targetList"></div></div>
<div class="hud-mission" id="hudMission"></div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
<script>
// ==================== SIMPLEX NOISE ====================
class SimplexNoise{constructor(e){this.grad3=[[1,1,0],[-1,1,0],[1,-1,0],[-1,-1,0],[1,0,1],[-1,0,1],[1,0,-1],[-1,0,-1],[0,1,1],[0,-1,1],[0,1,-1],[0,-1,-1]];this.p=[];for(let i=0;i<256;i++)this.p[i]=Math.floor(Math.random()*256);this.perm=new Array(512);for(let i=0;i<512;i++)this.perm[i]=this.p[i&255]}noise2D(e,f){var n=Math.floor(e),t=Math.floor(f),x=e-n,y=f-t;var u=this.grad3[this.p[n+this.p[t]]%12],v=this.grad3[this.p[n+1+this.p[t]]%12],w=this.grad3[this.p[n+this.p[t+1]]%12],z=this.grad3[this.p[n+1+this.p[t+1]]%12];var a=0,b=0,c=0,d=0;return this._dot(u,x,y)+this._dot(v,x-1,y)+this._dot(w,x,y-1)+this._dot(z,x-1,y-1)}noise3D(e,f,g){var n=Math.floor(e),t=Math.floor(f),r=Math.floor(g);var x=e-n,y=f-t,z=g-r;return 0}_dot(e,f,g){return e[0]*f+e[1]*g}}
// ==================== AIRCRAFT DEFINITIONS ====================
const AIRCRAFT={
falcon:{name:"Falcon",type:"Fighter Jet",color:0x4488ff,
maxSpeed:650,thrust:95000,weight:12000,wingArea:35,clMax:1.8,cd0:0.02,oe:0.35,
rollRate:2.5,pitchRate:2.0,yawRate:1.2,turnRadius:400,ceiling:50000,
desc:"High-speed interceptor with exceptional thrust-to-weight ratio"},
eagle:{name:"Eagle",type:"Heavy Bomber",color:0x88cc44,
maxSpeed:380,thrust:120000,weight:45000,wingArea:160,clMax:2.2,cd0:0.035,oe:0.45,
rollRate:0.8,pitchRate:0.7,yawRate:0.5,turnRadius:900,ceiling:35000,
desc:"Heavy bomber with massive payload capacity and long range"},
swift:{name:"Swift",type:"Agile Trainer",color:0xff6644,
maxSpeed:480,thrust:45000,weight:5500,wingArea:22,clMax:1.6,cd0:0.018,oe:0.3,
rollRate:3.2,pitchRate:2.8,yawRate:1.5,turnRadius:300,ceiling:45000,
desc:"Light trainer with unmatched agility and tight turning radius"}
};
const SCENARIOS={
takeoff:{name:"Takeoff & Landing",desc:"Master precision flight on the runway",icon:"✈️"},
dogfight:{name:"Dogfight",desc:"Engage enemy aircraft in aerial combat",icon:"⚔️"}
};
// ==================== GAME STATE ====================
let selectedPlane='falcon',selectedScenario='takeoff';
let gameRunning=false,gamePaused=false;
let scene,camera,renderer,clock;
let terrainMesh,skybox;
let aircraft,currentAircraftData;
let audioCtx,masterGain,engineOsc1,engineOsc2,windNoise,windGain;
let aiPlanes=[];
let missionState={phase:'active',score:0,kills:0,time:0};
// ==================== PHYSICS STATE ====================
const physics={
position:new THREE.Vector3(0,50,0),
velocity:new THREE.Vector3(0,0,0),
orientation:new THREE.Quaternion(),
angularVelocity:new THREE.Vector3(0,0,0),
throttle:0,targetThrottle:0,rpm:0,
airspeed:0,gForce:1,stalled:false,
onGround:true,groundAltitude:50,
aoa:0,sideslipAngle:0
};
// ==================== INPUT STATE ====================
const keys={};
let mouseX=0,mouseY=0;
// ==================== INIT MENU ====================
function initMenu(){
const ps=document.getElementById('planeSelect');
Object.entries(AIRCRAFT).forEach(([key,ac])=>{
const card=document.createElement('div');
card.className='plane-card'+(key===selectedPlane?' selected':'');
card.innerHTML=`<h3>${ac.name}</h3><p style="color:#f7c948;font-size:0.8em">${ac.type}</p>
<p>${ac.desc}</p>
<div class="plane-stats">
<div>Speed<span class="stat-bar"><span class="stat-fill" style="width:${(ac.maxSpeed/650)*100}%"></span></span></div>
<div>Thrust<span class="stat-bar"><span class="stat-fill" style="width:${(ac.thrust/120000)*100}%"></span></span></div>
<div>Agility<span class="stat-bar"><span class="stat-fill" style="width:${((ac.rollRate+ac.pitchRate)/6)*50}%"></span></span></div>
</div>`;
card.onclick=()=>{selectedPlane=key;document.querySelectorAll('.plane-card').forEach(c=>c.classList.remove('selected'));card.classList.add('selected')};
ps.appendChild(card);
});
const ss=document.getElementById('scenarioSelect');
Object.entries(SCENARIOS).forEach(([key,sc])=>{
const card=document.createElement('div');
card.className='scenario-card'+(key===selectedScenario?' selected':'');
card.innerHTML=`<h3>${sc.icon} ${sc.name}</h3><p>${sc.desc}</p>`;
card.onclick=()=>{selectedScenario=key;document.querySelectorAll('.scenario-card').forEach(c=>c.classList.remove('selected'));card.classList.add('selected')};
ss.appendChild(card);
});
document.getElementById('startBtn').onclick=startGame;
}
// ==================== START GAME ====================
function startGame(){
document.getElementById('menu').style.display='none';
document.getElementById('hud').style.display='block';
gameRunning=true;
initThree();
initAudio();
setupScenario();
animate();
}
// ==================== THREE.JS SETUP ====================
function initThree(){
scene=new THREE.Scene();
scene.fog=new THREE.FogExp2(0x87CEEB,0.00015);
clock=new THREE.Clock();
camera=new THREE.PerspectiveCamera(65,window.innerWidth/window.innerHeight,1,20000);
renderer=new THREE.WebGLRenderer({antialias:true,alpha:false});
renderer.setSize(window.innerWidth,window.innerHeight);
renderer.setPixelRatio(Math.min(window.devicePixelRatio,2));
renderer.shadowMap.enabled=true;
renderer.shadowMap.type=THREE.PCFSoftShadowMap;
renderer.toneMapping=THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure=1.0;
document.body.appendChild(renderer.domElement);
// Lighting
const ambientLight=new THREE.AmbientLight(0x6688aa,0.5);
scene.add(ambientLight);
const sunLight=new THREE.DirectionalLight(0xffeedd,1.2);
sunLight.position.set(500,800,300);
sunLight.castShadow=true;
sunLight.shadow.mapSize.width=4096;
sunLight.shadow.mapSize.height=4096;
sunLight.shadow.camera.near=10;
sunLight.shadow.camera.far=2000;
sunLight.shadow.camera.left=-500;
sunLight.shadow.camera.right=500;
sunLight.shadow.camera.top=500;
sunLight.shadow.camera.bottom=-500;
scene.add(sunLight);
const hemiLight=new THREE.HemisphereLight(0x87CEEB,0x445522,0.6);
scene.add(hemiLight);
// Sky
createSky();
createTerrain();
createAircraftModel();
setupControls();
window.addEventListener('resize',()=>{
camera.aspect=window.innerWidth/window.innerHeight;
camera.updateProjectionMatrix();
renderer.setSize(window.innerWidth,window.innerHeight);
});
}
function createSky(){
const skyGeo=new THREE.SphereGeometry(9000,32,32);
const skyMat=new THREE.ShaderMaterial({
uniforms:{topColor:{value:new THREE.Color(0x0044aa)},bottomColor:{value:new THREE.Color(0xaaddff)},offset:{value:20},exponent:{value:0.4}},
vertexShader:`varying vec3 vWorldPosition;void main(){vec4 worldPos=modelMatrix*vec4(position,1.0);vWorldPosition=worldPos.xyz;gl_Position=projectionMatrix*modelViewMatrix*vec4(position,1.0);}`,
fragmentShader:`uniform vec3 topColor;uniform vec3 bottomColor;uniform float offset;uniform float exponent;varying vec3 vWorldPosition;void main(){float h=normalize(vWorldPosition+offset).y;gl_FragColor=vec4(mix(bottomColor,topColor,max(pow(max(h,0.0),exponent),0.0)),1.0);}`,
side:THREE.BackSide,depthWrite:false
});
skybox=new THREE.Mesh(skyGeo,skyMat);
scene.add(skybox);
// Sun disc
const sunGeo=new THREE.CircleGeometry(80,32);
const sunMat=new THREE.MeshBasicMaterial({color:0xffffcc,transparent:true,opacity:0.9});
const sunMesh=new THREE.Mesh(sunGeo,sunMat);
sunMesh.position.set(500,600,-400);
sunMesh.lookAt(0,0,0);
scene.add(sunMesh);
// Clouds
for(let i=0;i<30;i++){
const cloudGroup=new THREE.Group();
const numPuffs=Math.floor(Math.random()*5)+3;
for(let j=0;j<numPuffs;j++){
const puffGeo=new THREE.SphereGeometry(20+Math.random()*40,8,6);
const puffMat=new THREE.MeshLambertMaterial({color:0xffffff,transparent:true,opacity:0.7});
const puff=new THREE.Mesh(puffGeo,puffMat);
puff.position.set(Math.random()*100-50,Math.random()*20,Math.random()*60-30);
cloudGroup.add(puff);
}
cloudGroup.position.set(Math.random()*4000-2000,800+Math.random()*400,Math.random()*4000-2000);
scene.add(cloudGroup);
}
}
function createTerrain(){
const size=16000;
const segments=512;
const geo=new THREE.PlaneGeometry(size,size,segments,segments);
geo.rotateX(-Math.PI/2);
const noise=new SimplexNoise();
const colors=[];
const posAttr=geo.attributes.position;
for(let i=0;i<posAttr.count;i++){
const x=posAttr.getX(i);
const z=posAttr.getZ(i);
let h=0;
h+=noise.noise2D(x*0.0003,z*0.0003)*800;
h+=noise.noise2D(x*0.001,z*0.001)*200;
h+=noise.noise2D(x*0.005,z*0.005)*40;
h+=noise.noise2D(x*0.02,z*0.02)*8;
// Flatten runway area
const distToRunway=Math.sqrt((x-0)*(x-0)+(z+1000)*(z+1000));
if(distToRunway<300){
h*=distToRunway/300;
}
posAttr.setY(i,Math.max(h,-50));
// Color based on height
const t=Math.min(1,Math.max(0,(h+50)/800));
let r,g,b;
if(t<0.2){r=0.35;g=0.45;b=0.2;}
else if(t<0.4){r=0.3;g=0.5;b=0.15;}
else if(t<0.6){r=0.4;g=0.45;b=0.25;}
else if(t<0.8){r=0.5;g=0.45;b=0.35;}
else{r=0.7;g=0.7;b=0.75;}
colors.push(r,g,b);
}
geo.setAttribute('color',new THREE.Float32BufferAttribute(colors,3));
const mat=new THREE.MeshStandardMaterial({vertexColors:true,roughness:0.85,metalness:0.1});
terrainMesh=new THREE.Mesh(geo,mat);
terrainMesh.receiveShadow=true;
scene.add(terrainMesh);
// Runway
const runwayGeo=new THREE.PlaneGeometry(60,2000);
runwayGeo.rotateX(-Math.PI/2);
const runwayMat=new THREE.MeshStandardMaterial({color:0x333333,roughness:0.9});
const runway=new THREE.Mesh(runwayGeo,runwayMat);
runway.position.set(0,1,-1000);
scene.add(runway);
// Runway markings
for(let i=-900;i<900;i+=80){
const markGeo=new THREE.PlaneGeometry(4,30);
markGeo.rotateX(-Math.PI/2);
const markMat=new THREE.MeshStandardMaterial({color:0xffffff});
const mark=new THREE.Mesh(markGeo,markMat);
mark.position.set(0,1.5,-1000+i);
scene.add(mark);
}
// Runway edge lights
for(let i=-950;i<=950;i+=50){
[-28,28].forEach(xOff=>{
const lightGeo=new THREE.SphereGeometry(1,6,4);
const lightMat=new THREE.MeshBasicMaterial({color:0x00ff00});
const lightMesh=new THREE.Mesh(lightGeo,lightMat);
lightMesh.position.set(xOff,2,-1000+i);
scene.add(lightMesh);
});
}
// Trees (simple cones)
for(let i=0;i<500;i++){
const tx=Math.random()*8000-4000;
const tz=Math.random()*8000-4000;
const distToRunway=Math.sqrt(tx*tx+(tz+1000)*(tz+1000));
if(distToRunway<200)continue;
const ty=getTerrainHeight(tx,tz);
if(ty>300||ty<-20)continue;
const treeGroup=new THREE.Group();
const trunkGeo=new THREE.CylinderGeometry(1,1.5,8,6);
const trunkMat=new THREE.MeshStandardMaterial({color:0x8B4513});
const trunk=new THREE.Mesh(trunkGeo,trunkMat);
trunk.position.y=4;
treeGroup.add(trunk);
const leavesGeo=new THREE.ConeGeometry(6+Math.random()*4,12+Math.random()*8,6);
const leavesMat=new THREE.MeshStandardMaterial({color:0x2d5a1e});
const leaves=new THREE.Mesh(leavesGeo,leavesMat);
leaves.position.y=12;
treeGroup.add(leaves);
treeGroup.position.set(tx,ty,tz);
scene.add(treeGroup);
}
}
function getTerrainHeight(x,z){
if(!terrainMesh)return 0;
const noise=new SimplexNoise();
let h=0;
h+=noise.noise2D(x*0.0003,z*0.0003)*800;
h+=noise.noise2D(x*0.001,z*0.001)*200;
h+=noise.noise2D(x*0.005,z*0.005)*40;
h+=noise.noise2D(x*0.02,z*0.02)*8;
const distToRunway=Math.sqrt((x-0)*(x-0)+(z+1000)*(z+1000));
if(distToRunway<300)h*=distToRunway/300;
return Math.max(h,-50);
}
// ==================== AIRCRAFT MODEL ====================
function createAircraftModel(){
currentAircraftData=AIRCRAFT[selectedPlane];
aircraft=new THREE.Group();
const bodyColor=currentAircraftData.color;
const matBody=new THREE.MeshStandardMaterial({color:bodyColor,roughness:0.3,metalness:0.7});
const matDark=new THREE.MeshStandardMaterial({color:0x222222,roughness:0.5,metalness:0.8});
const matGlass=new THREE.MeshStandardMaterial({color:0x88ccff,roughness:0.1,metalness:0.3,transparent:true,opacity:0.6});
if(selectedPlane==='falcon'){
// Fuselage
const fuselageGeo=new THREE.CylinderGeometry(1.2,1.5,18,8);
fuselageGeo.rotateZ(Math.PI/2);
const fuselage=new THREE.Mesh(fuselageGeo,matBody);
aircraft.add(fuselage);
// Nose cone
const noseGeo=new THREE.ConeGeometry(1.2,6,8);
noseGeo.rotateZ(-Math.PI/2);
const nose=new THREE.Mesh(noseGeo,matBody);
nose.position.x=12;
aircraft.add(nose);
// Cockpit
const cockpitGeo=new THREE.SphereGeometry(1.3,8,6,0,Math.PI*2,0,Math.PI/2);
const cockpit=new THREE.Mesh(cockpitGeo,matGlass);
cockpit.position.set(4,1.5,0);
aircraft.add(cockpit);
// Wings (swept)
const wingShape=new THREE.Shape();
wingShape.moveTo(0,0);wingShape.lineTo(-8,-2);wingShape.lineTo(-8,2);wingShape.lineTo(0,3);
const wingGeo=new THREE.ExtrudeGeometry(wingShape,{depth:1.5,bevelEnabled:false});
[-1,1].forEach(side=>{
const wing=new THREE.Mesh(wingGeo,matBody);
wing.position.set(-2,0,side*4);
wing.rotation.x=Math.PI/2;
if(side===-1)wing.scale.z=-1;
aircraft.add(wing);
});
// Tail fins
const tailFinGeo=new THREE.BoxGeometry(3,5,0.3);
const tailFin=new THREE.Mesh(tailFinGeo,matBody);
tailFin.position.set(-8,2.5,0);
aircraft.add(tailFin);
[-1,1].forEach(side=>{
const hStabGeo=new THREE.BoxGeometry(4,1.5,0.3);
const hStab=new THREE.Mesh(hStabGeo,matBody);
hStab.position.set(-8,0.5,side*2.5);
aircraft.add(hStab);
});
// Engine exhausts
[-1,1].forEach(side=>{
const exhaustGeo=new THREE.CylinderGeometry(0.8,1,4,8);
exhaustGeo.rotateZ(Math.PI/2);
const exhaust=new THREE.Mesh(exhaustGeo,matDark);
exhaust.position.set(-10,-0.5,side*2);
aircraft.add(exhaust);
// Exhaust glow
const glowGeo=new THREE.SphereGeometry(0.9,8,6);
const glowMat=new THREE.MeshBasicMaterial({color:0xff4400,transparent:true,opacity:0});
const glow=new THREE.Mesh(glowGeo,glowMat);
glow.position.set(-12,-0.5,side*2);
glow.name='exhaustGlow';
aircraft.add(glow);
});
}else if(selectedPlane==='eagle'){
// Large fuselage
const fuselageGeo=new THREE.CylinderGeometry(2.5,3,30,8);
fuselageGeo.rotateZ(Math.PI/2);
const fuselage=new THREE.Mesh(fuselageGeo,matBody);
aircraft.add(fuselage);
// Nose
const noseGeo=new THREE.SphereGeometry(2.5,8,6,0,Math.PI*2,0,Math.PI/2);
noseGeo.rotateZ(-Math.PI/2);
const nose=new THREE.Mesh(noseGeo,matBody);
nose.position.x=15;
aircraft.add(nose);
// Cockpit
const cockpitGeo=new THREE.BoxGeometry(4,2,3);
const cockpit=new THREE.Mesh(cockpitGeo,matGlass);
cockpit.position.set(8,2.5,0);
aircraft.add(cockpit);
// Large wings
[-1,1].forEach(side=>{
const wingGeo=new THREE.BoxGeometry(12,1.5,16);
const wing=new THREE.Mesh(wingGeo,matBody);
wing.position.set(-2,-0.5,side*8);
aircraft.add(wing);
// Wingtip tanks
const tankGeo=new THREE.CylinderGeometry(1,1,4,6);
tankGeo.rotateX(Math.PI/2);
const tank=new THREE.Mesh(tankGeo,matDark);
tank.position.set(-2,-0.5,side*14);
aircraft.add(tank);
});
// Tail assembly
const tailFinGeo=new THREE.BoxGeometry(3,8,1);
const tailFin=new THREE.Mesh(tailFinGeo,matBody);
tailFin.position.set(-13,4,0);
aircraft.add(tailFin);
[-1,1].forEach(side=>{
const hStabGeo=new THREE.BoxGeometry(6,2,8);
const hStab=new THREE.Mesh(hStabGeo,matBody);
hStab.position.set(-13,1,side*5);
aircraft.add(hStab);
});
// 4 engines
[-1,1].forEach(side=>{
[-0.6,0.6].forEach(xOff=>{
const engGeo=new THREE.CylinderGeometry(1.2,1.5,6,8);
engGeo.rotateZ(Math.PI/2);
const eng=new THREE.Mesh(engGeo,matDark);
eng.position.set(-4,-2,side*5+xOff*3);
aircraft.add(eng);
const glowGeo=new THREE.SphereGeometry(1.3,8,6);
const glowMat=new THREE.MeshBasicMaterial({color:0xff4400,transparent:true,opacity:0});
const glow=new THREE.Mesh(glowGeo,glowMat);
glow.position.set(-7,-2,side*5+xOff*3);
glow.name='exhaustGlow';
aircraft.add(glow);
});
});
}else{
// Swift - agile trainer
const fuselageGeo=new THREE.CylinderGeometry(0.9,1.2,14,8);
fuselageGeo.rotateZ(Math.PI/2);
const fuselage=new THREE.Mesh(fuselageGeo,matBody);
aircraft.add(fuselage);
const noseGeo=new THREE.ConeGeometry(0.9,5,8);
noseGeo.rotateZ(-Math.PI/2);
const nose=new THREE.Mesh(noseGeo,matBody);
nose.position.x=9.5;
aircraft.add(nose);
// Tandem cockpits
[-1,1].forEach(side=>{
const cockpitGeo=new THREE.SphereGeometry(0.8,8,6,0,Math.PI*2,0,Math.PI/2);
const cockpit=new THREE.Mesh(cockpitGeo,matGlass);
cockpit.position.set(side*3,1.2,0);
aircraft.add(cockpit);
});
// Straight wings
[-1,1].forEach(side=>{
const wingGeo=new THREE.BoxGeometry(6,0.8,10);
const wing=new THREE.Mesh(wingGeo,matBody);
wing.position.set(-1,-0.3,side*5);
aircraft.add(wing);
// Ailerons
const ailGeo=new THREE.BoxGeometry(2,0.4,3);
const ail=new THREE.Mesh(ailGeo,matDark);
ail.position.set(-1,-0.6,side*8);
ail.name='aileron'+side;
aircraft.add(ail);
});
// Tail
const tailFinGeo=new THREE.BoxGeometry(2,4,0.3);
const tailFin=new THREE.Mesh(tailFinGeo,matBody);
tailFin.position.set(-6,2,0);
aircraft.add(tailFin);
[-1,1].forEach(side=>{
const hStabGeo=new THREE.BoxGeometry(3,1.2,0.3);
const hStab=new THREE.Mesh(hStabGeo,matBody);
hStab.position.set(-6,0.5,side*2);
aircraft.add(hStab);
// Elevators
const elevGeo=new THREE.BoxGeometry(1.5,0.4,0.3);
const elev=new THREE.Mesh(elevGeo,matDark);
elev.position.set(-7,0.5,side*2);
elev.name='elevator'+side;
aircraft.add(elev);
});
// Single engine
const engGeo=new THREE.CylinderGeometry(0.8,1,3,8);
engGeo.rotateZ(Math.PI/2);
const eng=new THREE.Mesh(engGeo,matDark);
eng.position.set(-8,-0.5,0);
aircraft.add(eng);
const glowGeo=new THREE.SphereGeometry(0.9,8,6);
const glowMat=new THREE.MeshBasicMaterial({color:0xff4400,transparent:true,opacity:0});
const glow=new THREE.Mesh(glowGeo,glowMat);
glow.position.set(-10,-0.5,0);
glow.name='exhaustGlow';
aircraft.add(glow);
}
// Landing gear
[-1,0,1].forEach(xOff=>{
const gearGeo=new THREE.CylinderGeometry(0.2,0.2,3,4);
const gear=new THREE.Mesh(gearGeo,matDark);
gear.position.set(xOff*5,-2,0);
aircraft.add(gear);
const wheelGeo=new THREE.SphereGeometry(0.6,8,6);
const wheelMat=new THREE.MeshStandardMaterial({color:0x111111});
const wheel=new THREE.Mesh(wheelGeo,wheelMat);
wheel.position.set(xOff*5,-3.5,0);
aircraft.add(wheel);
});
// Navigation lights
[-1,1].forEach(side=>{
const navLightGeo=new THREE.SphereGeometry(0.2,6,4);
const navLightMat=new THREE.MeshBasicMaterial({color:side===-1?0xff0000:0x00ff00});
const navLight=new THREE.Mesh(navLightGeo,navLightMat);
if(selectedPlane==='eagle')navLight.position.set(-2,-0.5,side*14);
else if(selectedPlane==='falcon')navLight.position.set(-2,-0.5,side*6);
else navLight.position.set(-1,-0.3,side*8);
aircraft.add(navLight);
});
// Strobe light
const strobeGeo=new THREE.SphereGeometry(0.15,6,4);
const strobeMat=new THREE.MeshBasicMaterial({color:0xffffff});
const strobe=new THREE.Mesh(strobeGeo,strobeMat);
strobe.position.set(-9,-2,0);
strobe.name='strobe';
aircraft.add(strobe);
scene.add(aircraft);
}
// ==================== AUDIO SYSTEM ====================
function initAudio(){
audioCtx=new(window.AudioContext||window.webkitAudioContext)();
masterGain=audioCtx.createGain();
masterGain.gain.value=0.3;
masterGain.connect(audioCtx.destination);
// Engine sound - layered oscillators
engineOsc1=audioCtx.createOscillator();
engineOsc1.type='sawtooth';
engineOsc1.frequency.value=80;
const engineGain1=audioCtx.createGain();
engineGain1.gain.value=0.15;
engineOsc1.connect(engineGain1);
engineGain1.connect(masterGain);
engineOsc1.start();
engineOsc2=audioCtx.createOscillator();
engineOsc2.type='square';
engineOsc2.frequency.value=40;
const engineGain2=audioCtx.createGain();
engineGain2.gain.value=0.08;
engineOsc2.connect(engineGain2);
engineGain2.connect(masterGain);
engineOsc2.start();
// Wind noise
const bufferSize=audioCtx.sampleRate*2;
const noiseBuffer=audioCtx.createBuffer(1,bufferSize,audioCtx.sampleRate);
const data=noiseBuffer.getChannelData(0);
for(let i=0;i<bufferSize;i++)data[i]=Math.random()*2-1;
windNoise=audioCtx.createBufferSource();
windNoise.buffer=noiseBuffer;
windNoise.loop=true;
const windFilter=audioCtx.createBiquadFilter();
windFilter.type='bandpass';
windFilter.frequency.value=500;
windFilter.Q.value=0.5;
windGain=audioCtx.createGain();
windGain.gain.value=0;
windNoise.connect(windFilter);
windFilter.connect(windGain);
windGain.connect(masterGain);
windNoise.start();
// Ambient drone
const ambOsc=audioCtx.createOscillator();
ambOsc.type='sine';
ambOsc.frequency.value=55;
const ambGain=audioCtx.createGain();
ambGain.gain.value=0.02;
ambOsc.connect(ambGain);
ambGain.connect(masterGain);
ambOsc.start();
}
function updateAudio(){
if(!audioCtx)return;
const t=audioCtx.currentTime;
// Engine RPM based on throttle and airspeed
const targetRpm=800+physics.throttle*4200+physics.airspeed*2;
physics.rpm+=(targetRpm-physics.rpm)*0.05;
engineOsc1.frequency.setTargetAtTime(60+physics.rpm*0.03,t,0.1);
engineOsc2.frequency.setTargetAtTime(30+physics.rpm*0.015,t,0.1);
const engineVol=Math.min(1,physics.throttle*0.7+physics.airspeed/800);
masterGain.gain.setTargetAtTime(engineVol*0.4,t,0.1);
// Wind noise based on airspeed
windGain.gain.setTargetAtTime(Math.min(0.3,physics.airspeed/2000),t,0.1);
}
function playExplosionSound(){
if(!audioCtx)return;
const osc=audioCtx.createOscillator();
osc.type='sawtooth';
osc.frequency.value=200;
const gain=audioCtx.createGain();
gain.gain.setValueAtTime(0.5,audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.01,audioCtx.currentTime+1);
osc.connect(gain);
gain.connect(masterGain);
osc.start();
osc.stop(audioCtx.currentTime+1);
const bufferSize=audioCtx.sampleRate;
const noiseBuffer=audioCtx.createBuffer(1,bufferSize,audioCtx.sampleRate);
const data=noiseBuffer.getChannelData(0);
for(let i=0;i<bufferSize;i++)data[i]=(Math.random()*2-1)*Math.exp(-i/audioCtx.sampleRate*3);
const noise=audioCtx.createBufferSource();
noise.buffer=noiseBuffer;
const nGain=audioCtx.createGain();
nGain.gain.value=0.8;
noise.connect(nGain);
nGain.connect(masterGain);
noise.start();
}
// ==================== SCENARIO SETUP ====================
function setupScenario(){
if(selectedScenario==='takeoff'){
physics.position.set(0,5,-1000);
physics.velocity.set(0,0,0);
physics.onGround=true;
document.getElementById('hudMission').style.display='block';
document.getElementById('hudMission').textContent='🛫 Taxi to runway and take off. Climb to 3000ft.';
}else{
physics.position.set(0,5000,-2000);
physics.velocity.set(150,0,0);
physics.onGround=false;
// Spawn AI enemies
for(let i=0;i<4;i++){
spawnAIPlane();
}
document.getElementById('hudTargets').style.display='block';
document.getElementById('hudMission').style.display='block';
document.getElementById('hudMission').textContent='⚔️ Engage and destroy all hostile aircraft!';
}
}
function spawnAIPlane(){
const aiGroup=new THREE.Group();
const matBody=new THREE.MeshStandardMaterial({color:0xcc3333,roughness:0.4,metalness:0.6});
// Simple enemy plane model
const fuselageGeo=new THREE.CylinderGeometry(1,1.2,12,6);
fuselageGeo.rotateZ(Math.PI/2);
aiGroup.add(new THREE.Mesh(fuselageGeo,matBody));
const noseGeo=new THREE.ConeGeometry(1,4,6);
noseGeo.rotateZ(-Math.PI/2);
const nose=new THREE.Mesh(noseGeo,matBody);
nose.position.x=8;
aiGroup.add(nose);
[-1,1].forEach(side=>{
const wingGeo=new THREE.BoxGeometry(5,0.6,8);
const wing=new THREE.Mesh(wingGeo,matBody);
wing.position.set(-1,-0.3,side*4);
aiGroup.add(wing);
});
const tailFinGeo=new THREE.BoxGeometry(2,3,0.3);
const tailFin=new THREE.Mesh(tailFinGeo,matBody);
tailFin.position.set(-5,1.5,0);
aiGroup.add(tailFin);
const angle=Math.random()*Math.PI*2;
const dist=2000+Math.random()*3000;
aiGroup.position.set(
physics.position.x+Math.cos(angle)*dist,
2000+Math.random()*4000,
physics.position.z+Math.sin(angle)*dist
);
scene.add(aiGroup);
aiPlanes.push({
mesh:aiGroup,
velocity:new THREE.Vector3(Math.cos(angle)*100,0,-Math.sin(angle)*100),
health:100,
alive:true,
turnTimer:0,
targetHeading:angle
});
}
// ==================== CONTROLS ====================
function setupControls(){
document.addEventListener('keydown',e=>{keys[e.code]=true;e.preventDefault()});
document.addEventListener('keyup',e=>{keys[e.code]=false});
document.addEventListener('mousemove',e=>{
mouseX=(e.clientX/window.innerWidth)*2-1;
mouseY=-(e.clientY/window.innerHeight)*2+1;
});
}
// ==================== PHYSICS ENGINE ====================
function updatePhysics(dt){
if(!gameRunning||dt>0.05)dt=Math.min(dt,0.05);
const ac=currentAircraftData;
const pos=physics.position;
const vel=physics.velocity;
const quat=physics.orientation;
// Throttle management
if(keys['ShiftLeft']||keys['ShiftRight'])physics.targetThrottle=Math.min(1,physics.targetThrottle+dt*0.8);
if(keys['ControlLeft']||keys['ControlRight'])physics.targetThrottle=Math.max(0,physics.targetThrottle-dt*0.8);
physics.throttle+=(physics.targetThrottle-physics.throttle)*dt*3;
// Control surface deflections (rate-based)
let pitchInput=0,rollInput=0,yawInput=0;
if(keys['KeyW'])pitchInput=-1;
if(keys['KeyS'])pitchInput=1;
if(keys['KeyA'])rollInput=-1;
if(keys['KeyD'])rollInput=1;
if(keys['KeyQ'])yawInput=-1;
if(keys['KeyE'])yawInput=1;
// Mouse override (optional - can be used for more precise control)
pitchInput+=mouseY*0.5;
rollInput-=mouseX*0.5;
// Angular velocity integration with rate limits
const maxRollRate=ac.rollRate;
const maxPitchRate=ac.pitchRate;
const maxYawRate=ac.yawRate;
physics.angularVelocity.x+=(pitchInput*maxPitchRate-physics.angularVelocity.x)*dt*8;
physics.angularVelocity.y+=(yawInput*maxYawRate-physics.angularVelocity.y)*dt*6;
physics.angularVelocity.z+=(rollInput*maxRollRate-physics.angularVelocity.z)*dt*8;
// Dampen angular velocity
physics.angularVelocity.multiplyScalar(0.95);
// Update orientation using angular velocity
const angVelQuat=new THREE.Quaternion().setFromEuler(
new THREE.Euler(physics.angularVelocity.x*dt,physics.angularVelocity.y*dt,physics.angularVelocity.z*dt)
);
quat.multiply(angVelQuat).normalize();
// Forward vector in world space
const forward=new THREE.Vector3(1,0,0).applyQuaternion(quat);
const up=new THREE.Vector3(0,1,0).applyQuaternion(quat);
const right=new THREE.Vector3(0,0,1).applyQuaternion(quat);
// Airspeed (magnitude of velocity relative to air)
physics.airspeed=vel.length();
// Angle of attack: angle between velocity vector and aircraft's x-z plane
const velDir=vel.clone().normalize();
if(!velDir.equals(new THREE.Vector3())){
const projVel=velDir.clone().sub(velDir.dot(up).multiplyScalar(1)).normalize();
physics.aoa=Math.acos(Math.max(-1,Math.min(1,forward.dot(projVel))));
}
// Sideslip angle
const velSideComp=velDir.dot(right);
physics.sideslipAngle=Math.asin(Math.max(-1,Math.min(1,velSideComp)));
// Air density (decreases with altitude)
const altMeters=pos.y;
const airDensity=1.225*Math.exp(-altMeters/8500);
// LIFT CALCULATION
// Lift = CL * 0.5 * rho * v^2 * S
let cl=physics.aoa*ac.clMax/Math.PI;// Linear lift curve approximation
if(physics.aoa>1.2)cl*=Math.max(0,1-(physics.aoa-1.2)/0.8);// Stall model
const dynamicPressure=0.5*airDensity*physics.airspeed*physics.airspeed;
const liftForce=cl*dynamicPressure*ac.wingArea;
// Lift acts perpendicular to velocity in the aircraft's vertical plane
const liftDir=new THREE.Vector3();
if(physics.airspeed>1){
liftDir.copy(up).normalize();
}
const liftVec=liftDir.multiplyScalar(liftForce);
// DRAG CALCULATION
// Total drag = profile drag + induced drag
const profileDrag=ac.cd0*dynamicPressure*ac.wingArea;
const inducedDrag=(cl*cl)/(Math.PI*ac.oe*ac.wingArea)*dynamicPressure*ac.wingArea;
const totalDrag=profileDrag+inducedDrag;
// Drag opposes velocity
let dragVec=new THREE.Vector3();
if(physics.airspeed>0.1){
dragVec.copy(vel).normalize().multiplyScalar(-totalDrag);
}
// THRUST
const thrustForce=physics.throttle*ac.thrust;
const thrustVec=forward.clone().multiplyScalar(thrustForce);
// GRAVITY
const gravity=new THREE.Vector3(0,-9.81,0).multiplyScalar(ac.weight/1000);// Convert to force (N)
// SIDE FORCE (from sideslip)
const sideForce=-physics.sideslipAngle*dynamicPressure*ac.wingArea*0.5;
const sideVec=right.clone().multiplyScalar(sideForce);
// TOTAL FORCE
const totalForce=new THREE.Vector3();
totalForce.add(liftVec).add(dragVec).add(thrustVec).add(gravity).add(sideVec);
// ACCELERATION (F = ma)
const mass=ac.weight/1000;// kg
const acceleration=totalForce.divideScalar(mass);
// Update velocity
vel.add(acceleration.multiplyScalar(dt));
// Ground collision / landing
physics.groundAltitude=getTerrainHeight(pos.x,pos.z);
if(pos.y<=physics.groundAltitude+3){
pos.y=physics.groundAltitude+3;
if(vel.y<0)vel.y=0;
// Check if on ground (low speed, low altitude)
const isOnGround=physics.airspeed<50&&Math.abs(pos.y-physics.groundAltitude-3)<5;
if(isOnGround){
physics.onGround=true;
vel.multiplyScalar(0.97);// Ground friction
}else{
// Crash!
if(physics.airspeed>80||pos.y<physics.groundAltitude+2){
crashAircraft();
return;
}
physics.onGround=false;
}
// Ground effect: extra lift near ground
const groundEffect=Math.max(0,1-(pos.y-physics.groundAltitude)/50);
liftVec.multiplyScalar(1+groundEffect*0.3);
}else{
physics.onGround=false;
}
// Update position
pos.add(vel.clone().multiplyScalar(dt));
// G-force calculation
const gForce=Math.abs(liftForce/ac.weight)+Math.abs(physics.angularVelocity.length()*0.05);
physics.gForce=gForce;
// Stall detection
physics.stalled=physics.airspeed<60&&pos.y>physics.groundAltitude+10&&physics.aoa>0.8;
// Update aircraft visual orientation
aircraft.quaternion.copy(quat);
aircraft.position.copy(pos);
// Exhaust glow based on throttle
aircraft.traverse(child=>{
if(child.name==='exhaustGlow'){
child.material.opacity=physics.throttle*0.8;
}
});
// Strobe light blink
const strobe=aircraft.getObjectByName('strobe');
if(strobe){
strobe.material.opacity=Math.sin(Date.now()*0.01)>0?1:0;
}
// Control surface animation (Swift only)
if(selectedPlane==='swift'){
[-1,1].forEach(side=>{
const elev=aircraft.getObjectByName('elevator'+side);
if(elev)elev.rotation.z=pitchInput*0.3;
const ail=aircraft.getObjectByName('aileron'+side);
if(ail)ail.rotation.z=-rollInput*side*0.3;
});
}
// Camera follow
updateCamera(dt);
}
function crashAircraft(){
playExplosionSound();
missionState.phase='crashed';
document.getElementById('hudWarning').textContent='CRASHED! Press R to restart.';
document.getElementById('hudWarning').style.display='block';
// Explosion particles
for(let i=0;i<30;i++){
const pGeo=new THREE.SphereGeometry(1+Math.random()*2,4,4);
const pMat=new THREE.MeshBasicMaterial({color:Math.random()>0.5?0xff4400:0xffaa00});
const particle=new THREE.Mesh(pGeo,pMat);
particle.position.copy(physics.position);
particle.userData.velocity=new THREE.Vector3(
(Math.random()-0.5)*100,
Math.random()*80,
(Math.random()-0.5)*100
);
scene.add(particle);
const pInterval=setInterval(()=>{
if(!gameRunning)clearInterval(pInterval);
particle.position.add(particle.userData.velocity.clone().multiplyScalar(0.016));
particle.userData.velocity.y-=9.8*0.016;
particle.scale.multiplyScalar(0.97);
if(particle.scale.x<0.1){scene.remove(particle);clearInterval(pInterval)}
},16);
}
setTimeout(()=>{
document.getElementById('hudWarning').style.display='none';
},3000);
}
function updateCamera(dt){
const camDist=40;
const camHeight=15;
// Third person camera behind aircraft
const forward=new THREE.Vector3(1,0,0).applyQuaternion(physics.orientation);
const up=new THREE.Vector3(0,1,0).applyQuaternion(physics.orientation);
const camTargetPos=physics.position.clone()
.sub(forward.multiplyScalar(camDist))
.add(up.multiplyScalar(camHeight));
// Smooth camera follow
camera.position.lerp(camTargetPos,dt*5);
// Look slightly ahead of aircraft
const lookTarget=physics.position.clone().add(
new THREE.Vector3(1,0,0).applyQuaternion(physics.orientation).multiplyScalar(50)
);
camera.lookAt(lookTarget);
}
// ==================== AI LOGIC ====================
function updateAI(dt){
aiPlanes.forEach(ai=>{
if(!ai.alive)return;
const toPlayer=new THREE.Vector3().subVectors(physics.position,ai.mesh.position);
const dist=toPlayer.length();
// Simple AI behavior: fly toward player and try to get behind
let targetDir=toPlayer.normalize();
// Add some turning behavior
ai.turnTimer-=dt;
if(ai.turnTimer<=0){
ai.targetHeading=Math.atan2(targetDir.z,targetDir.x)+(Math.random()-0.5)*1.5;
ai.turnTimer=2+Math.random()*3;
}
const headingDiff=ai.targetHeading-Math.atan2(-ai.velocity.z,ai.velocity.x);
const turnRate=1.5*dt;
let newHeading=Math.atan2(-ai.velocity.z,ai.velocity.x)+Math.sign(headingDiff)*Math.min(Math.abs(headingDiff),turnRate);
ai.velocity.set(
Math.cos(newHeading)*120,
ai.velocity.y+(physics.position.y-ai.mesh.position.y>500?-30:30)*dt,
-Math.sin(newHeading)*120
);
// Clamp altitude
if(ai.mesh.position.y<500)ai.velocity.y=50;
if(ai.mesh.position.y>8000)ai.velocity.y=-50;
ai.mesh.position.add(ai.velocity.clone().multiplyScalar(dt));
// Update orientation to face velocity direction
const velDir=ai.velocity.clone().normalize();
const aiForward=new THREE.Vector3(1,0,0);
const quat=new THREE.Quaternion().setFromUnitVectors(aiForward,velDir);
ai.mesh.quaternion.copy(quat);
// Shooting at player (if close enough)
if(dist<800&&Math.random()<0.02){
fireAtPlayer(ai);
}
// Check if player is shooting at AI
if(dist<600&&keys['Space']){
ai.health-=dt*50;
if(ai.health<=0){
destroyAIPlane(ai);
}
}
});
}
function fireAtPlayer(ai){
const bulletGeo=new THREE.SphereGeometry(0.3,4,4);
const bulletMat=new THREE.MeshBasicMaterial({color:0xffff00});
const bullet=new THREE.Mesh(bulletGeo,bulletMat);
bullet.position.copy(ai.mesh.position);
const dir=new THREE.Vector3().subVectors(physics.position,ai.mesh.position).normalize();
bullet.userData.velocity=dir.multiplyScalar(500);
scene.add(bullet);
const bInterval=setInterval(()=>{
if(!gameRunning)clearInterval(bInterval);
bullet.position.add(bullet.userData.velocity.clone().multiplyScalar(0.016));
if(bullet.position.distanceTo(physics.position)<10){
// Hit player
crashAircraft();
scene.remove(bullet);
clearInterval(bInterval);
}
if(bullet.position.distanceTo(ai.mesh.position)>2000){
scene.remove(bullet);
clearInterval(bInterval);
}
},16);
}
function destroyAIPlane(ai){
ai.alive=false;
playExplosionSound();
missionState.kills++;
missionState.score+=1000;
// Explosion effect
for(let i=0;i<20;i++){
const pGeo=new THREE.SphereGeometry(0.5+Math.random(),4,4);
const pMat=new THREE.MeshBasicMaterial({color:Math.random()>0.5?0xff3300:0xffaa00});
const particle=new THREE.Mesh(pGeo,pMat);
particle.position.copy(ai.mesh.position);
particle.userData.velocity=new THREE.Vector3(
(Math.random()-0.5)*60,
Math.random()*40,
(Math.random()-0.5)*60
);
scene.add(particle);
let life=2;
const pInterval=setInterval(()=>{
life-=0.016;
if(life<=0||!gameRunning){scene.remove(particle);clearInterval(pInterval)}
particle.position.add(particle.userData.velocity.clone().multiplyScalar(0.016));
particle.userData.velocity.y-=9.8*0.016;
},16);
}
scene.remove(ai.mesh);
// Check win condition
const aliveCount=aiPlanes.filter(a=>a.alive).length;
if(aliveCount===0){
missionState.phase='victory';
document.getElementById('hudMission').textContent='🏆 VICTORY! All hostiles eliminated! Score: '+missionState.score;
}
updateTargetList();
}
function updateTargetList(){
const list=document.getElementById('targetList');
list.innerHTML='';
aiPlanes.forEach((ai,i)=>{
if(!ai.alive)return;
const dist=Math.round(physics.position.distanceTo(ai.mesh.position)/10);
const entry=document.createElement('div');
entry.className='target-entry';
entry.innerHTML=`<span class="target-dot"></span><span>Target ${i+1}: ${dist}km</span>`;
list.appendChild(entry);
});
}
// ==================== HUD UPDATE ====================
function updateHUD(){
const speedKts=Math.round(physics.airspeed*1.94384);
const altFt=Math.round(physics.position.y*3.28084);
const vsFpm=Math.round(physics.velocity.y*196.85);
document.getElementById('hudSpeed').textContent=speedKts;
document.getElementById('hudAlt').textContent=altFt;
document.getElementById('hudVS').textContent=vsFpm>0?'+'+vsFpm:vsFpm;
document.getElementById('hudG').textContent=physics.gForce.toFixed(1);
document.getElementById('hudPlane').textContent=currentAircraftData.name;
// Throttle bar
const throttlePct=Math.round(physics.throttle*100);
document.getElementById('throttleBar').style.width=throttlePct+'%';
document.getElementById('throttleVal').textContent=throttlePct+'%';
// G-meter bar
const gPct=Math.min(100,((physics.gForce-0)/6)*50);
document.getElementById('gBar').style.width=gPct+'%';
document.getElementById('gVal').textContent=physics.gForce.toFixed(1)+'G';
// Heading
const forward=new THREE.Vector3(1,0,0).applyQuaternion(physics.orientation);
let heading=Math.round(Math.atan2(forward.z,forward.x)*180/Math.PI);
if(heading<0)heading+=360;
document.getElementById('headingText').textContent=heading+'°';
// Stall warning
const stallWarning=document.getElementById('hudWarning');
if(physics.stalled){
stallWarning.style.display='block';
stallWarning.textContent='⚠ STALL WARNING ⚠';
}else if(physics.gForce>4){
stallWarning.style.display='block';
stallWarning.textContent='⚠ HIGH G ⚠';
}else{
stallWarning.style.display='none';
}
// Minimap
updateMinimap();
}
function updateMinimap(){
const canvas=document.getElementById('minimapCanvas');
const ctx=canvas.getContext('2d');
canvas.width=150;canvas.height=150;
ctx.fillStyle='rgba(0,20,40,0.8)';
ctx.fillRect(0,0,150,150);
const scale=0.03;
const cx=75,cy=75;
// Draw terrain features (simplified)
ctx.fillStyle='rgba(40,80,40,0.3)';
for(let i=0;i<20;i++){
const tx=Math.sin(i*1.7)*60+cx;
const ty=Math.cos(i*2.3)*60+cy;
ctx.beginPath();
ctx.arc(tx,ty,5+Math.random()*10,0,Math.PI*2);
ctx.fill();
}
// Draw AI targets
aiPlanes.forEach(ai=>{
if(!ai.alive)return;
const dx=(ai.mesh.position.x-physics.position.x)*scale;
const dz=(ai.mesh.position.z-physics.position.z)*scale;
ctx.fillStyle='#ff3333';
ctx.beginPath();
ctx.arc(cx+dx,cy+dz,4,0,Math.PI*2);
ctx.fill();
});
// Draw player (center)
ctx.fillStyle='#4488ff';
ctx.beginPath();
ctx.arc(cx,cy,5,0,Math.PI*2);
ctx.fill();
// Player direction indicator
const forward=new THREE.Vector3(1,0,0).applyQuaternion(physics.orientation);
ctx.strokeStyle='#4488ff';
ctx.lineWidth=2;
ctx.beginPath();
ctx.moveTo(cx,cy);
ctx.lineTo(cx+forward.x*15,cy+forward.z*15);
ctx.stroke();
// Border
ctx.strokeStyle='rgba(255,255,255,0.3)';
ctx.lineWidth=1;
ctx.strokeRect(0,0,150,150);
}
// ==================== GAME LOOP ====================
function animate(){
if(!gameRunning)return;
requestAnimationFrame(animate);
const dt=clock.getDelta();
if(keys['KeyR']&&missionState.phase==='crashed'){
resetGame();
return;
}
updatePhysics(dt);
updateAI(dt);
updateAudio();
updateHUD();
// Update skybox position to follow aircraft
skybox.position.copy(physics.position);
renderer.render(scene,camera);
}
function resetGame(){
document.getElementById('hudWarning').style.display='none';
missionState={phase:'active',score:0,kills:0,time:0};
// Remove old AI planes
aiPlanes.forEach(ai=>{if(ai.alive)scene.remove(ai.mesh)});
aiPlanes=[];
physics.position.set(selectedScenario==='takeoff'?new THREE.Vector3(0,5,-1000):new THREE.Vector3(0,5000,-2000));
physics.velocity.set(0,0,0);
physics.orientation.identity();
physics.angularVelocity.set(0,0,0);
physics.throttle=0;
physics.targetThrottle=0;
physics.onGround=true;
if(selectedScenario==='dogfight'){
for(let i=0;i<4;i++)spawnAIPlane();
}
}
// ==================== INIT ====================
initMenu();
</script>
</body>
</html>
<!-- agent-meta {"model":"thinkingcap-qwen3.6-27b","provider":"lmstudio","persona":"basic","sessionId":"e961b67f-9f7a-454b-8440-c18ab6a9dd77","tokensIn":1243,"tokensOut":16061,"tokensTotal":17304,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-07-19T01:47:52.324Z"} -->