Files
Flight-Ops/lib/sim/movement.ts
T

73 lines
2.9 KiB
TypeScript

// Pure analytic flight math (v0.7.0). Position and fuel of an in-flight plane are
// functions of the sim clock — no incremental stepping, so a tick can jump any
// distance in game time without losing accuracy. All inputs are explicit; there
// is no DB/IO here, which keeps this fully unit-testable with synthetic routes.
const toRad = (d: number) => (d * Math.PI) / 180;
const toDeg = (r: number) => (r * 180) / Math.PI;
// Flight duration in ms for a leg of distanceKm at speedKmh.
export function travelDurationMs(distanceKm: number, speedKmh: number): number {
if (!(speedKmh > 0)) throw new Error('speedKmh must be > 0');
return (distanceKm / speedKmh) * 3_600_000;
}
// Sim-clock time at which the leg completes.
export function arrivalTimeFor(departureSimTime: number, distanceKm: number, speedKmh: number): number {
return departureSimTime + travelDurationMs(distanceKm, speedKmh);
}
// Flight progress in [0, 1] at a given sim-clock time — clamped to 0 before
// departure and 1 after arrival. A zero-distance leg is complete immediately.
export function progressAt(
departureSimTime: number,
distanceKm: number,
speedKmh: number,
clockMs: number,
): number {
const duration = travelDurationMs(distanceKm, speedKmh);
if (duration <= 0) return 1;
return Math.min(1, Math.max(0, (clockMs - departureSimTime) / duration));
}
// Great-circle interpolation between two WGS84 points at progress t ∈ [0,1]
// (slerp on the unit sphere). Exact endpoints: t=0 → from, t=1 → to.
export function positionAt(
fromLat: number,
fromLon: number,
toLat: number,
toLon: number,
t: number,
): { lat: number; lon: number } {
if (t <= 0) return { lat: fromLat, lon: fromLon };
if (t >= 1) return { lat: toLat, lon: toLon };
const vec = (lat: number, lon: number): [number, number, number] => {
const phi = toRad(lat);
const lambda = toRad(lon);
return [Math.cos(phi) * Math.cos(lambda), Math.cos(phi) * Math.sin(lambda), Math.sin(phi)];
};
const a = vec(fromLat, fromLon);
const b = vec(toLat, toLon);
const dot = Math.max(-1, Math.min(1, a[0] * b[0] + a[1] * b[1] + a[2] * b[2]));
const theta = Math.acos(dot);
if (theta < 1e-9) return { lat: fromLat, lon: fromLon }; // same point
const sinTheta = Math.sin(theta);
const wA = Math.sin((1 - t) * theta) / sinTheta;
const wB = Math.sin(t * theta) / sinTheta;
const v: [number, number, number] = [a[0] * wA + b[0] * wB, a[1] * wA + b[1] * wB, a[2] * wA + b[2] * wB];
return {
lat: toDeg(Math.asin(Math.max(-1, Math.min(1, v[2])))),
lon: toDeg(Math.atan2(v[1], v[0])),
};
}
// Fuel remaining after flying `progress` of a leg of distanceKm. Clamped at 0 —
// range exhaustion is not modeled yet (fuel management/refueling comes later).
export function fuelAt(startFuel: number, burnPerKm: number, distanceKm: number, progress: number): number {
return Math.max(0, startFuel - burnPerKm * distanceKm * progress);
}