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:
@@ -1,5 +1,18 @@
|
||||
# Changelog
|
||||
|
||||
## v0.16.0 — 2026-09-19 12:18
|
||||
- **Weight-proportional fuel burn (ISS-005, part A).** Fuel no longer burns at a constant L/km — the instantaneous rate is proportional to total weight, `r(F) = k·(We + ρF)` L/km with We the empty weight and ρ = 0.8 kg/L (Jet-A1), so a heavier plane burns more per km and a tank gets lighter as it empties. The constant `k` is calibrated so a FULL tank burns exactly the catalog's old `burnPerKm`, keeping full-tank departure behavior identical to before; divergence only grows in flight as fuel drops off. Closed forms of `dF/dx = −k(We + ρF)` live in `lib/sim/movement.ts` (single source of truth):
|
||||
- `fuelAtWeighted(startFuel, burnPerKm, emptyWeightKg, maxFuel, distanceKm, progress)` — fuel remaining after flying `progress` of the leg (replaces the old constant-rate `fuelAt`, which is removed)
|
||||
- `requiredFuelFor(burnPerKm, emptyWeightKg, maxFuel, distanceKm)` — liters needed to cover the distance arriving empty, independent of starting load; a leg is flyable iff current fuel ≥ this and fits at all iff it ≤ tank capacity
|
||||
- `burnRateConstant(...)` + exported `FUEL_DENSITY_KG_PER_L` (0.8)
|
||||
- **Catalog gains `emptyWeightKg`** (`lib/game/catalog.ts`, ATR 72-600 OEW ≈ 9300 kg), exposed in the snapshot's `catalog[]` so clients can mirror the model; OpenAPI `CatalogEntry` follows
|
||||
- **All three burn call sites moved to the weighted model**: landing fuel on arrival (`lib/sim/engine.ts`), live in-flight fuel in the state snapshot (`lib/game/state.ts`), and fly validation (`lib/game/actions.ts` — a leg now needs `requiredFuelFor`, not `burnPerKm × km`)
|
||||
- **Refuel "just enough for the loaded package" (ISS-005, part B).** `POST /api/planes/{planeId}/refuel` takes an OPTIONAL JSON body `{ mode?: 'full' | 'forFlight', packageId?: string }`: no body or `mode:'full'` is exactly today's fill-to-max behavior; `mode:'forFlight'` adds just enough fuel to fly that loaded package's destination leg — liters = `requiredFuelFor(haversine(plane → pkg.destIcao)) − current fuel`, 409 if the package isn't loaded on this plane / is expired, or the leg doesn't fit even a full tank; bad bodies (unknown field/mode, forFlight without packageId) are 400. OpenAPI gains `RefuelRequest` + the 400
|
||||
- **UI** (`components/RightPane.tsx`): each on-board cargo row gets a "Refuel" button (`refuel-for-{pkg.id}` → `{mode:'forFlight', packageId}`, disabled while flying or busy, mirroring fly-to/abandon); the market fill-up button is renamed **"Refuel to full"** (testid `market-refuel` and behavior unchanged)
|
||||
- Tests: unit — new weighted-burn suite in `lib/sim/movement.test.ts` (full-tank calibration ≈ old constant, closed form vs numeric integration of r(F), F_req monotonic + empty-arrival identity, lighter-start burns less, clamping); `refuelPlane` forFlight cases in `lib/game/actions.test.ts` (exact liters/cost, no-op when enough, 409 not-loaded/expired/doesn't-fit via the ~7670 km UNNT↔LPPD pair, 404 unknown package, 400 missing packageId); refuel route body-validation tests; `lib/openapi.test.ts` updated (optional RefuelRequest, errorCodes `['400','404','409']`, CatalogEntry.emptyWeightKg). Adjusted where the model legitimately shifts numbers: landing-fuel assertions in `lib/sim/engine.test.ts` and `e2e/actions.spec.ts` now use the weighted closed form; the e2e "return leg unfuelable" scenario is reworked — under weight-proportional burn a full tank covers the EDDF↔LEZL round trip (the plane lightens as it flies), so the 409 insufficient-fuel assertion moves to the SECOND outbound after landing back with a partial tank
|
||||
- e2e (`right-pane.spec.ts`): new test — fly the longest hub leg on a full tank, load a package back to the hub at the far end, click the row "Refuel" and assert via the endpoint's own response snapshot that cash drops by exactly L×price and fuel rises by exactly L (L from the mirrored closed form); also asserts the market button label is "Refuel to full". `e2e/api.spec.ts` path list unchanged
|
||||
- Verified: `tsc --noEmit` clean; unit suite green locally; e2e not run locally by design — CI verifies against an isolated stack
|
||||
|
||||
## v0.15.1 — 2026-09-19 11:04
|
||||
- **Right pane widened, "On board" wrapping fixed (ISS-004).** The 3-pane shell grid (`app/globals.css` `.app-shell`) moves from `300px / 1fr / 320px` to `280px / 1fr / 360px` — the map column absorbs the delta (MapLibre sizes itself from its container, no JS change). Three wrap offenders in the right pane's "On board" blocks are fixed:
|
||||
- In-flight leg header (`components/RightPane.tsx`, `.contract-route` span): the class already had `white-space: nowrap; overflow: hidden; text-overflow: ellipsis`, but as a flex item with default `min-width: auto` it could not shrink below its full text width, so long routes (e.g. "Frankfurt → Madrid–Barajas") overflowed instead of ellipsizing — `.contract-route` now has `min-width: 0`
|
||||
|
||||
Reference in New Issue
Block a user