flight-simulator / src /Aircraft.js
underrate's picture
Refactor code structure for improved readability and maintainability. Changed the plane to a SU-30 fighter jet
e329e5a verified
Raw
History Blame Contribute Delete
23.7 kB
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;
}
}