import * as THREE from 'three'; export class Aircraft { constructor() { // Aircraft properties this.position = new THREE.Vector3(0, 200, 0); this.rotation = new THREE.Euler(0, 0, 0); this.velocity = new THREE.Vector3(0, 0, -80); this.maxSpeed = 180; this.stallSpeed = 35; this.modelYawOffset = Math.PI; // Flight characteristics this.cruiseThrust = 0.65; this.boostThrust = 0; this.maxThrust = 10; this.liftCoefficient = 0.0018; this.dragCoefficient = 0.001; this.pitchRate = 1.4; this.rollRate = 1.5; this.yawRate = 0.6; this.turnAssist = 0.8; // Control inputs this.pitch = 0; this.yaw = 0; this.roll = 0; this.effectTime = 0; this.engineGlowMaterials = []; this.exhaustTrails = []; this.navLights = []; // Create aircraft mesh this.createAircraftMesh(); // Setup controls this.setupControls(); } createAircraftMesh() { // Su-30 Fighter Jet Materials - Russian Air Force blue-gray camouflage const mainBodyMat = new THREE.MeshStandardMaterial({ color: 0x4a5868, // Blue-gray base (Russian camouflage) roughness: 0.5, metalness: 0.45, flatShading: true }); const darkPanelMat = new THREE.MeshStandardMaterial({ color: 0x2a3540, // Darker blue-gray panels roughness: 0.55, metalness: 0.5, flatShading: true }); const lightPanelMat = new THREE.MeshStandardMaterial({ color: 0x6a7585, // Lighter blue-gray panels roughness: 0.45, metalness: 0.4, flatShading: true }); const noseConeMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, // Dark radar nose cone roughness: 0.3, metalness: 0.2, flatShading: true }); const canopyGlassMat = new THREE.MeshStandardMaterial({ color: 0x0a1520, emissive: 0x1a3040, emissiveIntensity: 0.12, roughness: 0.05, metalness: 0.95, transparent: true, opacity: 0.75 }); const canopyFrameMat = new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.4, metalness: 0.6, flatShading: true }); const exhaustNozzleMat = new THREE.MeshStandardMaterial({ color: 0x2a2825, roughness: 0.7, metalness: 0.3, flatShading: true }); // Camouflage secondary color for upper surfaces const camoDarkMat = new THREE.MeshStandardMaterial({ color: 0x354555, // Darker camo patches roughness: 0.5, metalness: 0.45, flatShading: true }); this.mesh = new THREE.Group(); // Helper function to create symmetrical parts const addPair = (factory) => { this.mesh.add(factory(1)); this.mesh.add(factory(-1)); }; // ==================== FUSELAGE ==================== // Nose cone (radome) - pointed conical shape const noseCone = new THREE.Mesh(new THREE.ConeGeometry(0.45, 2.2, 8), noseConeMat); noseCone.rotation.x = Math.PI / 2; noseCone.position.set(0, 0.05, 5.1); this.mesh.add(noseCone); // Forward fuselage - contains tandem cockpit const forwardFuselage = new THREE.Mesh( new THREE.CylinderGeometry(0.55, 0.72, 3.8, 8), mainBodyMat ); forwardFuselage.rotation.x = Math.PI / 2; forwardFuselage.position.set(0, 0.08, 3.0); forwardFuselage.scale.set(1.0, 0.75, 1.0); this.mesh.add(forwardFuselage); // Center fuselage - wider, contains intakes const centerFuselage = new THREE.Mesh( new THREE.CylinderGeometry(0.85, 0.78, 3.0, 8), mainBodyMat ); centerFuselage.rotation.x = Math.PI / 2; centerFuselage.position.set(0, 0.0, 0.3); centerFuselage.scale.set(1.0, 0.7, 1.0); this.mesh.add(centerFuselage); // Aft fuselage - narrowing toward engines const aftFuselage = new THREE.Mesh( new THREE.CylinderGeometry(0.78, 0.55, 3.0, 8), mainBodyMat ); aftFuselage.rotation.x = Math.PI / 2; aftFuselage.position.set(0, 0.0, -2.0); aftFuselage.scale.set(1.0, 0.65, 1.0); this.mesh.add(aftFuselage); // Engine nacelle area (between engines) const tailBoom = new THREE.Mesh( new THREE.BoxGeometry(0.6, 0.45, 2.5), darkPanelMat ); tailBoom.position.set(0, 0.1, -3.8); this.mesh.add(tailBoom); // Belly spine const bellySpine = new THREE.Mesh( new THREE.BoxGeometry(0.5, 0.25, 6.0), darkPanelMat ); bellySpine.position.set(0, -0.38, 0.3); this.mesh.add(bellySpine); // ==================== CANOPY (Tandem Two-Seat) ==================== // Front canopy (pilot) const frontCanopy = new THREE.Mesh( new THREE.SphereGeometry(0.48, 12, 10), canopyGlassMat ); frontCanopy.scale.set(1.0, 0.55, 1.2); frontCanopy.position.set(0, 0.42, 3.4); this.mesh.add(frontCanopy); // Rear canopy (WSO - Weapons Systems Officer) const rearCanopy = new THREE.Mesh( new THREE.SphereGeometry(0.48, 12, 10), canopyGlassMat ); rearCanopy.scale.set(1.0, 0.5, 1.1); rearCanopy.position.set(0, 0.42, 2.3); this.mesh.add(rearCanopy); // Canopy frames const frontFrame = new THREE.Mesh( new THREE.BoxGeometry(0.06, 0.06, 0.9), canopyFrameMat ); frontFrame.position.set(0, 0.52, 3.4); this.mesh.add(frontFrame); const rearFrame = new THREE.Mesh( new THREE.BoxGeometry(0.06, 0.06, 0.85), canopyFrameMat ); rearFrame.position.set(0, 0.50, 2.3); this.mesh.add(rearFrame); // Divider frame between front and rear canopies const dividerFrame = new THREE.Mesh( new THREE.BoxGeometry(0.7, 0.04, 0.06), canopyFrameMat ); dividerFrame.position.set(0, 0.46, 2.85); this.mesh.add(dividerFrame); // Dorsal spine hump behind cockpit (houses avionics for two-seat config) const dorsalHump = new THREE.Mesh( new THREE.BoxGeometry(0.55, 0.18, 1.2), mainBodyMat ); dorsalHump.position.set(0, 0.32, 1.5); this.mesh.add(dorsalHump); // Dorsal airbrake (behind the hump) const dorsalAirbrake = new THREE.Mesh( new THREE.BoxGeometry(0.45, 0.4, 0.08), darkPanelMat ); dorsalAirbrake.position.set(0, 0.45, 0.8); dorsalAirbrake.rotation.x = -0.15; this.mesh.add(dorsalAirbrake); // Canopy rear bulkhead const canopyRearFrame = new THREE.Mesh( new THREE.BoxGeometry(0.65, 0.05, 0.06), canopyFrameMat ); canopyRearFrame.position.set(0, 0.44, 1.8); this.mesh.add(canopyRearFrame); // ==================== IRST (Infra-Red Search and Track) ==================== // Distinctive ball in front of the windshield - Su-30 feature const irstBall = new THREE.Mesh( new THREE.SphereGeometry(0.12, 12, 12), new THREE.MeshStandardMaterial({ color: 0x1a1a1a, roughness: 0.2, metalness: 0.1 }) ); irstBall.position.set(0, 0.35, 4.2); this.mesh.add(irstBall); // IRST housing const irstHousing = new THREE.Mesh( new THREE.CylinderGeometry(0.08, 0.12, 0.15, 8), mainBodyMat ); irstHousing.rotation.x = Math.PI / 2; irstHousing.position.set(0, 0.28, 4.15); this.mesh.add(irstHousing); // ==================== LERX (Leading Edge Root Extensions) ==================== addPair((dir) => { const lerx = new THREE.Mesh( new THREE.BoxGeometry(2.2, 0.08, 1.8), mainBodyMat ); lerx.position.set(dir * 1.1, 0.0, 1.8); lerx.rotation.y = dir * 0.35; return lerx; }); // ==================== MAIN DELTA WINGS ==================== const createWing = (dir) => { const shape = new THREE.Shape(); // Delta wing planform - typical Su-27/J-16 shape if (dir === 1) { shape.moveTo(0.6, 2.2); // root leading edge shape.lineTo(6.5, -0.5); // wing tip leading edge shape.lineTo(6.5, -1.3); // wing tip trailing edge shape.lineTo(1.2, -3.2); // wing root trailing edge (flaperon) shape.lineTo(0.6, 2.2); // back to start } else { shape.moveTo(-0.6, 2.2); shape.lineTo(-1.2, -3.2); shape.lineTo(-6.5, -1.3); shape.lineTo(-6.5, -0.5); shape.lineTo(-0.6, 2.2); } const geo = new THREE.ExtrudeGeometry(shape, { depth: 0.12, bevelEnabled: true, bevelThickness: 0.02, bevelSize: 0.02, bevelSegments: 1 }); const mesh = new THREE.Mesh(geo, mainBodyMat); mesh.rotation.x = Math.PI / 2; mesh.position.set(0, 0.02, 0.3); return mesh; }; this.mesh.add(createWing(1)); this.mesh.add(createWing(-1)); // Upper wing camouflage patches addPair((dir) => { const camoPatch = new THREE.Mesh( new THREE.BoxGeometry(2.5, 0.02, 1.5), camoDarkMat ); camoPatch.position.set(dir * 3.0, 0.14, -0.8); camoPatch.rotation.y = dir * 0.15; return camoPatch; }); addPair((dir) => { const camoPatch2 = new THREE.Mesh( new THREE.BoxGeometry(1.8, 0.02, 1.2), camoDarkMat ); camoPatch2.position.set(dir * 4.5, 0.14, -0.3); camoPatch2.rotation.y = dir * 0.1; return camoPatch2; }); // Wing fuel tanks / pods addPair((dir) => { const tank = new THREE.Mesh( new THREE.CapsuleGeometry(0.18, 2.0, 6, 8), lightPanelMat ); tank.rotation.x = Math.PI / 2; tank.rotation.z = dir * 0.1; tank.position.set(dir * 4.5, -0.15, -0.8); return tank; }); // ==================== CANARDS (Front control surfaces) ==================== addPair((dir) => { const canardShape = new THREE.Shape(); if (dir === 1) { canardShape.moveTo(0, 0.6); canardShape.lineTo(1.6, 0.1); canardShape.lineTo(1.5, -0.3); canardShape.lineTo(0, -0.5); } else { canardShape.moveTo(0, 0.6); canardShape.lineTo(0, -0.5); canardShape.lineTo(-1.5, -0.3); canardShape.lineTo(-1.6, 0.1); } const canardGeo = new THREE.ExtrudeGeometry(canardShape, { depth: 0.06, bevelEnabled: true, bevelThickness: 0.01, bevelSize: 0.01, bevelSegments: 1 }); const canard = new THREE.Mesh(canardGeo, mainBodyMat); canard.rotation.x = Math.PI / 2; canard.position.set(0, 0.15, 4.2); return canard; }); // ==================== AIR INTAKES ==================== addPair((dir) => { // Main intake structure const intake = new THREE.Mesh( new THREE.BoxGeometry(0.65, 0.55, 2.8), darkPanelMat ); intake.position.set(dir * 1.05, -0.15, 1.2); this.mesh.add(intake); // Intake lip const intakeLip = new THREE.Mesh( new THREE.BoxGeometry(0.72, 0.62, 0.15), lightPanelMat ); intakeLip.position.set(dir * 1.05, -0.15, 2.5); this.mesh.add(intakeLip); // Intake splitter plate const splitter = new THREE.Mesh( new THREE.BoxGeometry(0.08, 0.48, 2.0), mainBodyMat ); splitter.position.set(dir * 0.72, -0.12, 1.8); return splitter; }); // ==================== TWIN VERTICAL STABILIZERS ==================== addPair((dir) => { // Vertical stabilizer (tail fin) const tailShape = new THREE.Shape(); tailShape.moveTo(0, 0); // leading edge bottom tailShape.lineTo(-0.8, 1.9); // leading edge top (swept) tailShape.lineTo(-1.6, 1.85); // trailing edge top tailShape.lineTo(-1.2, 0); // trailing edge bottom const tailGeo = new THREE.ExtrudeGeometry(tailShape, { depth: 0.08, bevelEnabled: true, bevelThickness: 0.015, bevelSize: 0.015, bevelSegments: 1 }); const tail = new THREE.Mesh(tailGeo, mainBodyMat); tail.rotation.y = Math.PI / 2; tail.rotation.x = dir * 0.12; // Slight outward cant tail.position.set(dir * 0.95, 0.2, -4.2); this.mesh.add(tail); // Rudder const rudder = new THREE.Mesh( new THREE.BoxGeometry(0.05, 1.4, 0.4), darkPanelMat ); rudder.position.set(dir * 1.45, 1.0, -4.8); rudder.rotation.x = dir * 0.12; return rudder; }); // ==================== HORIZONTAL STABILIZERS ==================== addPair((dir) => { const stabShape = new THREE.Shape(); if (dir === 1) { stabShape.moveTo(0, 0.5); stabShape.lineTo(2.2, 0.15); stabShape.lineTo(2.0, -0.25); stabShape.lineTo(0, -0.6); } else { stabShape.moveTo(0, 0.5); stabShape.lineTo(0, -0.6); stabShape.lineTo(-2.0, -0.25); stabShape.lineTo(-2.2, 0.15); } const stabGeo = new THREE.ExtrudeGeometry(stabShape, { depth: 0.06, bevelEnabled: true, bevelThickness: 0.01, bevelSize: 0.01, bevelSegments: 1 }); const stabilizer = new THREE.Mesh(stabGeo, mainBodyMat); stabilizer.rotation.x = Math.PI / 2; stabilizer.position.set(0, 0.15, -4.5); return stabilizer; }); // ==================== TWIN ENGINES ==================== addPair((dir) => { // Engine nacelle const nacelle = new THREE.Mesh( new THREE.CylinderGeometry(0.38, 0.42, 2.2, 10), mainBodyMat ); nacelle.rotation.x = Math.PI / 2; nacelle.position.set(dir * 0.65, -0.08, -4.0); this.mesh.add(nacelle); // Exhaust nozzle const nozzle = new THREE.Mesh( new THREE.CylinderGeometry(0.32, 0.28, 0.5, 12), exhaustNozzleMat ); nozzle.rotation.x = Math.PI / 2; nozzle.position.set(dir * 0.65, -0.08, -5.15); this.mesh.add(nozzle); // Engine glow const glowMat = new THREE.MeshStandardMaterial({ color: 0xff6633, emissive: 0xff4411, emissiveIntensity: 1.0, transparent: true, opacity: 0.85, toneMapped: false }); this.engineGlowMaterials.push(glowMat); const glow = new THREE.Mesh( new THREE.CylinderGeometry(0.24, 0.26, 0.15, 12), glowMat ); glow.rotation.x = Math.PI / 2; glow.position.set(dir * 0.65, -0.08, -5.38); this.mesh.add(glow); // Afterburner flame const flameMat = new THREE.MeshBasicMaterial({ color: 0xff8844, transparent: true, opacity: 0.45, blending: THREE.AdditiveBlending, depthWrite: false }); const flame = new THREE.Mesh( new THREE.ConeGeometry(0.22, 2.5, 10, 1, true), flameMat ); flame.rotation.x = Math.PI / 2; flame.position.set(dir * 0.65, -0.08, -6.6); this.exhaustTrails.push(flame); this.mesh.add(flame); return flame; // Return to satisfy addPair expectation }); // ==================== DETAILS & ANTENNAS ==================== // IFR (In-flight refueling) probe const ifrProbe = new THREE.Mesh( new THREE.CylinderGeometry(0.02, 0.025, 0.8, 6), darkPanelMat ); ifrProbe.rotation.x = Math.PI / 2; ifrProbe.position.set(0, 0.35, 4.8); this.mesh.add(ifrProbe); // Dorsal antenna const dorsalAntenna = new THREE.Mesh( new THREE.BoxGeometry(0.08, 0.15, 0.8), darkPanelMat ); dorsalAntenna.position.set(0, 0.52, -0.5); this.mesh.add(dorsalAntenna); // ==================== NAVIGATION LIGHTS ==================== const redLightMat = new THREE.MeshStandardMaterial({ color: 0xff2222, emissive: 0xff0000, emissiveIntensity: 1.5, roughness: 0.2 }); const greenLightMat = new THREE.MeshStandardMaterial({ color: 0x22ff44, emissive: 0x00ff22, emissiveIntensity: 1.5, roughness: 0.2 }); const whiteLightMat = new THREE.MeshStandardMaterial({ color: 0xffffff, emissive: 0xffffff, emissiveIntensity: 0.8, roughness: 0.2 }); // Port (red) and Starboard (green) lights on wing tips const leftWingLight = new THREE.Mesh(new THREE.SphereGeometry(0.06, 8, 8), redLightMat); leftWingLight.position.set(-6.4, 0.08, -0.9); this.mesh.add(leftWingLight); const rightWingLight = new THREE.Mesh(new THREE.SphereGeometry(0.06, 8, 8), greenLightMat); rightWingLight.position.set(6.4, 0.08, -0.9); this.mesh.add(rightWingLight); // Tail lights (white) addPair((dir) => { const tailLight = new THREE.Mesh(new THREE.SphereGeometry(0.05, 8, 8), whiteLightMat); tailLight.position.set(dir * 1.0, 1.9, -4.8); return tailLight; }); // Belly strobe const bellyStrobe = new THREE.Mesh(new THREE.SphereGeometry(0.04, 8, 8), whiteLightMat); bellyStrobe.position.set(0, -0.42, -1.0); this.mesh.add(bellyStrobe); this.navLights.push({ material: redLightMat, mode: 'steady', base: 1.2 }); this.navLights.push({ material: greenLightMat, mode: 'steady', base: 1.2 }); this.navLights.push({ material: whiteLightMat, mode: 'strobe', base: 0.3 }); // ==================== SHADOWS ==================== this.mesh.traverse((child) => { if (child.isMesh && (!child.material || !child.material.transparent)) { child.castShadow = true; child.receiveShadow = true; } }); this.mesh.position.copy(this.position); } setupControls() { document.addEventListener('keydown', (event) => this.handleKeyDown(event)); document.addEventListener('keyup', (event) => this.handleKeyUp(event)); } handleKeyDown(event) { if (event.key === 'Shift') { this.boostThrust = 0.5; return; } if (event.key === 'ArrowUp') { this.throttleUp = true; return; } if (event.key === 'ArrowDown') { this.throttleDown = true; return; } switch (event.key.toLowerCase()) { case 'w': this.pitch = -1; break; // Pitch up (nose rises) case 's': this.pitch = 1; break; // Pitch down (nose drops) case 'a': this.roll = -1; break; // Roll left (left wing down) case 'd': this.roll = 1; break; // Roll right (right wing down) case 'q': this.yaw = 1; break; // Yaw left (turn left) case 'e': this.yaw = -1; break; // Yaw right (turn right) } } handleKeyUp(event) { if (event.key === 'Shift') { this.boostThrust = 0; return; } if (event.key === 'ArrowUp') { this.throttleUp = false; return; } if (event.key === 'ArrowDown') { this.throttleDown = false; return; } switch (event.key.toLowerCase()) { case 'w': case 's': this.pitch = 0; break; case 'a': case 'd': this.roll = 0; break; case 'q': case 'e': this.yaw = 0; break; } } update(deltaTime) { // Gradual throttle control if (this.throttleUp) { this.cruiseThrust = THREE.MathUtils.clamp(this.cruiseThrust + 0.4 * deltaTime, 0, 1); } if (this.throttleDown) { this.cruiseThrust = THREE.MathUtils.clamp(this.cruiseThrust - 0.4 * deltaTime, 0, 1); } const speed = Math.max(this.velocity.length(), 0.01); const speedRatio = THREE.MathUtils.clamp(speed / 80, 0.35, 1.6); // ── Apply rotation rates using YXZ Euler order ── this.rotation.order = 'YXZ'; this.rotation.x += this.pitch * this.pitchRate * deltaTime; this.rotation.z += this.roll * this.rollRate * deltaTime; this.rotation.y += this.yaw * this.yawRate * deltaTime; // Banked turns: roll naturally induces yaw at speed this.rotation.y -= this.rotation.z * this.turnAssist * speedRatio * deltaTime; // Clamp pitch to ±60° (well away from ±90° gimbal lock zone) this.rotation.x = THREE.MathUtils.clamp(this.rotation.x, -1.05, 1.05); this.rotation.z = THREE.MathUtils.clamp(this.rotation.z, -1.2, 1.2); // Auto-leveling: gently stabilise when no input if (this.pitch === 0) { this.rotation.x = THREE.MathUtils.lerp(this.rotation.x, 0, deltaTime * 0.4); } if (this.roll === 0) { this.rotation.z = THREE.MathUtils.lerp(this.rotation.z, 0, deltaTime * 1.0); } // Calculate body axes const forward = new THREE.Vector3(0, 0, -1).applyEuler(this.rotation).normalize(); const right = new THREE.Vector3(1, 0, 0).applyEuler(this.rotation).normalize(); // Aerodynamic forces const throttle = THREE.MathUtils.clamp(this.cruiseThrust + this.boostThrust, 0, 1); const thrust = forward.clone().multiplyScalar(throttle * this.maxThrust); const velocityDir = speed > 0.1 ? this.velocity.clone().normalize() : forward.clone(); let liftDir = new THREE.Vector3().crossVectors(right, velocityDir); if (liftDir.lengthSq() < 0.0001) { liftDir = new THREE.Vector3(0, 1, 0).applyEuler(this.rotation); } else { liftDir.normalize(); } const speedSq = speed * speed; let liftMag = this.liftCoefficient * speedSq; const stallFactor = THREE.MathUtils.clamp(speed / this.stallSpeed, 0, 1); liftMag *= stallFactor; const lift = liftDir.multiplyScalar(liftMag); const drag = velocityDir.multiplyScalar(-this.dragCoefficient * speedSq); const gravity = new THREE.Vector3(0, -9.81, 0); const acceleration = new THREE.Vector3() .add(thrust) .add(lift) .add(drag) .add(gravity); this.velocity.addScaledVector(acceleration, deltaTime); // Align velocity toward aircraft heading (makes pitch/roll actually steer) const alignTarget = forward.clone().multiplyScalar(this.velocity.length()); this.velocity.lerp(alignTarget, deltaTime * 2.5); // Update position this.position.addScaledVector(this.velocity, deltaTime); // Cap speed and keep above ground if (this.velocity.length() > this.maxSpeed) { this.velocity.setLength(this.maxSpeed); } if (this.position.y < 2) { this.position.y = 2; if (this.velocity.y < 0) this.velocity.y = 0; } this.updateVisualEffects(deltaTime, throttle, speed); // Update mesh this.mesh.position.copy(this.position); this.mesh.rotation.set( this.rotation.x, this.rotation.y + this.modelYawOffset, this.rotation.z ); } updateVisualEffects(deltaTime, throttle, speed) { this.effectTime += deltaTime; const speedRatio = THREE.MathUtils.clamp(speed / this.maxSpeed, 0, 1); const boostFactor = THREE.MathUtils.clamp((throttle - this.cruiseThrust) / (1 - this.cruiseThrust), 0, 1); this.engineGlowMaterials.forEach((material, index) => { const flicker = 0.88 + Math.sin(this.effectTime * 34 + index * 1.7) * 0.12; material.emissiveIntensity = (0.42 + throttle * 1.45 + boostFactor * 1.55) * flicker; material.opacity = THREE.MathUtils.clamp(0.22 + throttle * 0.5 + boostFactor * 0.3, 0.2, 0.96); }); this.exhaustTrails.forEach((trail, index) => { const pulse = 0.9 + Math.sin(this.effectTime * 26 + index * 2.1) * 0.1; const width = THREE.MathUtils.lerp(0.75, 1.35, throttle) * pulse; const length = THREE.MathUtils.lerp(0.9, 3.8, throttle + boostFactor * 0.3); trail.scale.set(width, width, length); trail.material.opacity = THREE.MathUtils.clamp((throttle - 0.22) * 0.58 + speedRatio * 0.26, 0, 0.72); }); this.navLights.forEach((light) => { if (light.mode === 'strobe') { light.material.emissiveIntensity = Math.sin(this.effectTime * 16.5) > 0.89 ? 2.6 : 0.15; } else { light.material.emissiveIntensity = light.base; } }); } getMesh() { return this.mesh; } }