feat: v0.16.0 — ISS-005 weight-proportional fuel burn + refuel just enough for the loaded package
CI / build + unit + e2e (pull_request) Failing after 5m28s
CI / build + unit + e2e (pull_request) Failing after 5m28s
Part A: r(F) = k(We + rho*F) L/km, k calibrated so a full tank burns exactly
the old constant; closed forms in lib/sim/movement.ts (fuelAtWeighted,
requiredFuelFor, burnRateConstant, FUEL_DENSITY_KG_PER_L). Catalog gains
emptyWeightKg (ATR72 OEW 9300 kg), exposed in the snapshot. All three burn
call sites (engine landing, state live fuel, fly validation) moved to the
weighted model; old constant-rate fuelAt removed.
Part B: POST /api/planes/{planeId}/refuel takes an optional body
{mode:'full'|'forFlight', packageId} — no body keeps today's fill-to-full
behavior exactly (fuel lands bit-exact on maxFuel); forFlight adds just
enough for the loaded package's destination leg (409 not-loaded/expired/
doesn't-fit, 400 bad body). UI: per-cargo-row 'Refuel' button
(refuel-for-{pkg.id}), market button renamed 'Refuel to full'.
OpenAPI: RefuelRequest schema + optional requestBody + 400 on refuel, fly's
409 description updated, CatalogEntry.emptyWeightKg. Tests: new weighted-burn
suite, forFlight action/route cases (incl. ~7670 km UNNT<->LPPD doesn't-fit),
openapi guards; landing-fuel assertions in engine.test.ts + e2e/actions.spec.ts
use the closed form; the e2e 'return leg unfuelable' scenario reworked — a full
tank now covers the EDDF<->LEZL round trip, so the 409 moves to the second
outbound. New e2e: row Refuel deducts exactly L*price / raises fuel by exactly
L via the endpoint's own response snapshot.
This commit is contained in:
+52
-4
@@ -65,8 +65,56 @@ export function positionAt(
|
||||
};
|
||||
}
|
||||
|
||||
// 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);
|
||||
// --- Weight-proportional fuel burn (v0.16.0) ---
|
||||
//
|
||||
// The instantaneous burn rate is proportional to total weight: r(F) = k·(We + ρF) L/km,
|
||||
// where We is the empty weight in kg, ρ the fuel density and F the current load in
|
||||
// liters — a heavier plane burns more per km. k is calibrated so that a FULL tank
|
||||
// burns exactly the catalog's constant burnPerKm: k = burnPerKm / (We + ρ·maxFuel),
|
||||
// so full-tank legs match the old model at departure and only diverge as fuel (and
|
||||
// weight) drops off in flight. Closed forms of dF/dx = −k(We + ρF):
|
||||
// fuel after distance D from F₀: (F₀ + We/ρ)·e^{−kρD} − We/ρ
|
||||
// consumption over D: (F₀ + We/ρ)·(1 − e^{−kρD})
|
||||
// fuel required to cover D arriving with 0, independent of F₀: (We/ρ)(e^{kρD} − 1)
|
||||
|
||||
// Jet-A1 density in kg/L — converts a liter load into its weight contribution.
|
||||
export const FUEL_DENSITY_KG_PER_L = 0.8;
|
||||
|
||||
// Calibration constant k for one plane type (see the model note above).
|
||||
export function burnRateConstant(burnPerKm: number, emptyWeightKg: number, maxFuel: number): number {
|
||||
return burnPerKm / (emptyWeightKg + FUEL_DENSITY_KG_PER_L * maxFuel);
|
||||
}
|
||||
|
||||
// Fuel remaining after flying `progress` of a leg of distanceKm from startFuel under the
|
||||
// weighted model. Clamped at 0 — range exhaustion is not modeled (a plane that runs dry
|
||||
// limps to the gate on fumes, same as before).
|
||||
export function fuelAtWeighted(
|
||||
startFuel: number,
|
||||
burnPerKm: number,
|
||||
emptyWeightKg: number,
|
||||
maxFuel: number,
|
||||
distanceKm: number,
|
||||
progress: number,
|
||||
): number {
|
||||
const d = distanceKm * progress;
|
||||
if (d <= 0) return startFuel;
|
||||
const k = burnRateConstant(burnPerKm, emptyWeightKg, maxFuel);
|
||||
const rho = FUEL_DENSITY_KG_PER_L;
|
||||
const f = (startFuel + emptyWeightKg / rho) * Math.exp(-k * rho * d) - emptyWeightKg / rho;
|
||||
return Math.max(0, f);
|
||||
}
|
||||
|
||||
// Total fuel required to cover distanceKm arriving with an empty tank — independent of the
|
||||
// starting load. A leg is flyable from a plane iff its current fuel covers this amount; it
|
||||
// fits in the aircraft at all iff this does not exceed the tank capacity.
|
||||
export function requiredFuelFor(
|
||||
burnPerKm: number,
|
||||
emptyWeightKg: number,
|
||||
maxFuel: number,
|
||||
distanceKm: number,
|
||||
): number {
|
||||
if (distanceKm <= 0) return 0;
|
||||
const k = burnRateConstant(burnPerKm, emptyWeightKg, maxFuel);
|
||||
const rho = FUEL_DENSITY_KG_PER_L;
|
||||
return (emptyWeightKg / rho) * (Math.exp(k * rho * distanceKm) - 1);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user