76bad46d09
Loading screen, main menu, 3D flight sim with CRT post-processing, procedural terrain, airport with buildings, low-poly aircraft, flight physics, HUD instruments, and sound. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
164 lines
4.7 KiB
JavaScript
164 lines
4.7 KiB
JavaScript
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;
|
|
}
|
|
}
|