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>
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user