feat: v0.7.0 — server-authoritative sim engine: tick-on-read clock (time-scale + pause via POST /api/sim), contract expiry + market spawn, analytic plane movement/finalization
This commit is contained in:
@@ -0,0 +1,72 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user