import * as THREE from 'three'; const PITCH_LIMIT = 85 * Math.PI / 180; const STALL_SPEED = 25; const GROUND_LIFT_REDUCTION = 0.15; export class FlightModel { constructor() { this.position = new THREE.Vector3(0, 8, 0); this.velocity = new THREE.Vector3(0, 0, 0); this.orientation = new THREE.Euler(0, 0, 0, 'YXZ'); this.angularVelocity = new THREE.Vector3(0, 0, 0); this.thrust = 0; this.mass = 1200; this.maxThrust = 50000; this.dragCoefficient = 0.02; this.liftCoefficient = 0.15; this.gravity = 9.81; this.onGround = true; this.groundHeight = 2; } getForwardVector() { const dir = new THREE.Vector3(0, 0, -1); dir.applyEuler(this.orientation); return dir; } getUpVector() { const up = new THREE.Vector3(0, 1, 0); up.applyEuler(this.orientation); return up; } update(delta, input, terrainHeightAtPos) { this.groundHeight = terrainHeightAtPos(this.position.x, this.position.z) + 2; this.onGround = this.position.y <= this.groundHeight + 0.5; // Throttle control if (input.throttleUp) { this.thrust = Math.min(1, this.thrust + delta * 0.3); } else if (input.throttleDown) { this.thrust = Math.max(0, this.thrust - delta * 0.3); } const speed = this.velocity.length(); // Thrust force const forward = this.getForwardVector(); const thrustForce = forward.clone().multiplyScalar(this.thrust * this.maxThrust); // Drag const dragMagnitude = this.dragCoefficient * speed * speed; const dragForce = speed > 0 ? this.velocity.clone().normalize().multiplyScalar(-dragMagnitude) : new THREE.Vector3(); // Lift let liftFactor = this.liftCoefficient; if (this.onGround && speed < STALL_SPEED) { liftFactor *= GROUND_LIFT_REDUCTION * (speed / STALL_SPEED); } else if (speed < STALL_SPEED) { liftFactor *= 0.5 * (speed / STALL_SPEED); } const up = this.getUpVector(); const liftForce = up.clone().multiplyScalar(liftFactor * speed * speed); // Gravity const gravityForce = new THREE.Vector3(0, -this.gravity * this.mass, 0); // Ground friction let frictionForce = new THREE.Vector3(); if (this.onGround) { const horizontalVel = new THREE.Vector3(this.velocity.x, 0, this.velocity.z); const hSpeed = horizontalVel.length(); if (hSpeed > 0.1) { frictionForce = horizontalVel.normalize().multiplyScalar(-hSpeed * 20); } } // Net force and acceleration const netForce = new THREE.Vector3() .add(thrustForce) .add(dragForce) .add(liftForce) .add(gravityForce) .add(frictionForce); const acceleration = netForce.divideScalar(this.mass); // Integrate velocity and position this.velocity.add(acceleration.multiplyScalar(delta)); this.position.add(this.velocity.clone().multiplyScalar(delta)); // Ground collision if (this.position.y < this.groundHeight) { this.position.y = this.groundHeight; if (this.velocity.y < 0) { this.velocity.y = 0; } this.onGround = true; } // Rotation this.updateRotation(delta, input); } updateRotation(delta, input) { const pitchInput = input.pitch || 0; const rollInput = input.roll || 0; const yawInput = input.yaw || 0; // Dampen angular velocity this.angularVelocity.multiplyScalar(0.92); // Apply control inputs (per-second rates) this.angularVelocity.x += pitchInput * 1.5; this.angularVelocity.y += yawInput * 0.8; this.angularVelocity.z += rollInput * 1.2; // Clamp angular rates this.angularVelocity.x = Math.max(-2, Math.min(2, this.angularVelocity.x)); this.angularVelocity.y = Math.max(-1, Math.min(1, this.angularVelocity.y)); this.angularVelocity.z = Math.max(-2, Math.min(2, this.angularVelocity.z)); // Integrate this.orientation.x += this.angularVelocity.x * delta; this.orientation.y += this.angularVelocity.y * delta; this.orientation.z += this.angularVelocity.z * delta; // Clamp pitch this.orientation.x = Math.max(-PITCH_LIMIT, Math.min(PITCH_LIMIT, this.orientation.x)); // Auto-leveling this.orientation.x *= 0.995; this.orientation.z *= 0.985; } getAirspeedKnots() { return this.velocity.length() * 1.94384; } getAltitudeFeet() { return Math.max(0, (this.position.y - this.groundHeight) * 3.28084); } getHeadingDeg() { const forward = this.getForwardVector(); let heading = Math.atan2(-forward.x, -forward.z) * 180 / Math.PI; return ((heading % 360) + 360) % 360; } getVerticalSpeedFtMin() { return -this.velocity.y * 196.85; } getPitchDeg() { return this.orientation.x * 180 / Math.PI; } getRollDeg() { return this.orientation.z * 180 / Math.PI; } }