Files
retro-flight-sim/js/flight-model.js
T
mac-container-dev 76bad46d09 Initial commit: Retro 90s flight simulator MVP
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>
2026-05-05 10:43:22 +00:00

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;
}
}