73 lines
2.9 KiB
TypeScript
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);
|
|
}
|