Max range display is the guaranteed (worst-case headwind) full-tank range (ISS-099, v0.51.0)
CI / build + unit + e2e (pull_request) Successful in 12m47s

This commit is contained in:
2026-09-22 08:24:09 +00:00
parent d215203988
commit dc2f2b420b
7 changed files with 69 additions and 13 deletions
+21
View File
@@ -5,6 +5,8 @@ import {
burnRateConstant,
FUEL_DENSITY_KG_PER_L,
fuelAtWeighted,
guaranteedRangeKm,
MAX_WIND_BURN_FACTOR,
maxRangeKm,
positionAt,
progressAt,
@@ -234,3 +236,22 @@ describe('maxRangeKm (v0.28.0)', () => {
expect(() => maxRangeKm(BURN, 0, TANK)).toThrow();
});
});
describe('guaranteedRangeKm (v0.51.0, ISS-099)', () => {
it("MAX_WIND_BURN_FACTOR is windBurnFactor's actual maximum over the weather domain", () => {
// Full headwind at max wind: exactly +20% — the constant must track the factor function, not a copy.
expect(MAX_WIND_BURN_FACTOR).toBe(windBurnFactor(30, 90, 90));
});
it('is maxRangeKm under worst-case headwind burn (ATR ≈ 3696 km ±5%)', () => {
const r = guaranteedRangeKm(BURN, WE, TANK);
expect(r).toBeCloseTo(maxRangeKm(BURN * MAX_WIND_BURN_FACTOR, WE, TANK), 12);
expect(r).toBeGreaterThan(3696 * 0.95);
expect(r).toBeLessThan(3696 * 1.05);
});
it('zero fuel → zero range; rejects a non-positive empty weight', () => {
expect(guaranteedRangeKm(BURN, WE, 0)).toBe(0);
expect(() => guaranteedRangeKm(BURN, 0, TANK)).toThrow();
});
});
+13
View File
@@ -137,3 +137,16 @@ export function maxRangeKm(burnPerKm: number, emptyWeightKg: number, maxFuel: nu
const rho = FUEL_DENSITY_KG_PER_L;
return Math.log(1 + (rho * maxFuel) / emptyWeightKg) * ((emptyWeightKg + rho * maxFuel) / (rho * burnPerKm));
}
// Max headwind burn multiplier over the weather domain (v0.51.0, ISS-099): the upper end of
// windBurnFactor's [0.8, 1.2] range — a full 30 kt headwind (the max in lib/weather.ts) ÷ 150.
export const MAX_WIND_BURN_FACTOR = 1 + 30 / 150;
// Guaranteed full-tank range (v0.51.0, ISS-099): the maxRangeKm closed form under worst-case
// headwind burn — any leg within this distance is flyable in ALL weather, since windBurnFactor
// never exceeds MAX_WIND_BURN_FACTOR and requiredFuelFor grows with the burn rate. This is what
// the UI displays as "Max range", so a spec-possible leg always passes flyPlane's fuel guard
// (requiredFuelFor ≤ maxFuel under the same wind-adjusted burn).
export function guaranteedRangeKm(burnPerKm: number, emptyWeightKg: number, maxFuel: number): number {
return maxRangeKm(burnPerKm * MAX_WIND_BURN_FACTOR, emptyWeightKg, maxFuel);
}