v1.12.0: takeoff & landing phases on the marker #166

Merged
kamilkrzywda merged 3 commits from feat/takeoff-landing-phases into master 2026-09-28 12:15:37 +02:00
Owner

What

Every leg now has a 10-minute takeoff phase at the origin and a 10-minute landing phase at the destination, on top of the pure cruise time. Each phase is weather-scaled up to 2x by a monotone condition rule (Clear < Cloudy < Rain < Fog; clear stays 1x). The phases burn no fuel — pure time.

  • lib/sim/movement.ts — single composition: BASE_PHASE_MS, phaseFactor(weather) in [1,2], MAX_PHASE_FACTOR, legPhases(distanceKm, speedKmh, originIcao, destIcao) -> {takeoffMs, cruiseMs, landingMs, totalMs}. Takeoff scales on ORIGIN weather, landing on DESTINATION (both deterministic via weatherFor(icao) — no stored state). arrivalTimeFor and progressAt route through it; progress is now piecewise — 0 while holding at the origin during takeoff, linear over cruise, 1 while holding at the destination during landing.
  • lib/sim/engine.ts — arrival gate includes the phases.
  • lib/game/state.ts — client live position/fuel recompute uses piecewise progress (markers park at each gate instead of crawling the whole route).
  • lib/game/seed.ts — job expiry floor is phase-inclusive (a package must be finishable even when both airports are in worst conditions; max leg ~23.5 h < 36 h ceiling).
  • lib/mapData.ts + components/Map.tsx — the plane popup shows the current phase next to the status: "flying · taking off" / "cruising" / "landing" (new data-testid="plane-phase" sibling; plane-status keeps the raw status so existing assertions stay exact).

Tests

  • Unit — movement.test.ts: pinned cruise duration unchanged, new suites for phaseFactor bounds/monotonicity (MAX_PHASE_FACTOR pinned to the factor function), origin-vs-dest composition on a non-vacuous Rain/Cloudy pair, piecewise progressAt holds. engine.test.ts: deleted the local travelMs mirror — all ~18 "exactly at arrival" fixture clocks now use the real arrivalTimeFor(...) composition (wear-clamp second leg chains off leg 1's exact phase-inclusive arrival). seed.test.ts: expiry-floor mirrors updated to the phase-inclusive floor.
  • E2E — new e2e/phases.spec.ts: dispatches the starter on EDDF->LEMD and, using the same legPhases boundaries as the engine, asserts "taking off" while the plane holds at the origin gate and "landing" while it holds at the destination gate (scale dropped to 30x so each 600-1200 game-s window is observable), then "available" at Madrid; relocates home and restores the world rate.

Local verification

  • npx tsc --noEmit — clean
  • npx vitest run — 41 files, 455 tests passed
  • npx playwright test --list — 69 tests in 19 files (incl. the new spec)

Full e2e runs in CI against the isolated dind stack.

## What Every leg now has a **10-minute takeoff phase** at the origin and a **10-minute landing phase** at the destination, on top of the pure cruise time. Each phase is weather-scaled up to 2x by a monotone condition rule (Clear < Cloudy < Rain < Fog; clear stays 1x). The phases burn no fuel — pure time. - `lib/sim/movement.ts` — single composition: `BASE_PHASE_MS`, `phaseFactor(weather)` in [1,2], `MAX_PHASE_FACTOR`, `legPhases(distanceKm, speedKmh, originIcao, destIcao) -> {takeoffMs, cruiseMs, landingMs, totalMs}`. Takeoff scales on ORIGIN weather, landing on DESTINATION (both deterministic via `weatherFor(icao)` — no stored state). `arrivalTimeFor` and `progressAt` route through it; progress is now **piecewise** — 0 while holding at the origin during takeoff, linear over cruise, 1 while holding at the destination during landing. - `lib/sim/engine.ts` — arrival gate includes the phases. - `lib/game/state.ts` — client live position/fuel recompute uses piecewise progress (markers park at each gate instead of crawling the whole route). - `lib/game/seed.ts` — job expiry floor is phase-inclusive (a package must be finishable even when both airports are in worst conditions; max leg ~23.5 h < 36 h ceiling). - `lib/mapData.ts` + `components/Map.tsx` — the plane popup shows the current phase next to the status: "flying · taking off" / "cruising" / "landing" (new `data-testid="plane-phase"` sibling; `plane-status` keeps the raw status so existing assertions stay exact). ## Tests - **Unit** — `movement.test.ts`: pinned cruise duration unchanged, new suites for phaseFactor bounds/monotonicity (`MAX_PHASE_FACTOR` pinned to the factor function), origin-vs-dest composition on a non-vacuous Rain/Cloudy pair, piecewise progressAt holds. `engine.test.ts`: deleted the local `travelMs` mirror — all ~18 "exactly at arrival" fixture clocks now use the real `arrivalTimeFor(...)` composition (wear-clamp second leg chains off leg 1's exact phase-inclusive arrival). `seed.test.ts`: expiry-floor mirrors updated to the phase-inclusive floor. - **E2E** — new `e2e/phases.spec.ts`: dispatches the starter on EDDF->LEMD and, using the same `legPhases` boundaries as the engine, asserts "taking off" while the plane holds at the origin gate and "landing" while it holds at the destination gate (scale dropped to 30x so each 600-1200 game-s window is observable), then "available" at Madrid; relocates home and restores the world rate. ## Local verification - `npx tsc --noEmit` — clean - `npx vitest run` — 41 files, 455 tests passed - `npx playwright test --list` — 69 tests in 19 files (incl. the new spec) Full e2e runs in CI against the isolated dind stack.
kamilkrzywda added 1 commit 2026-09-28 09:10:06 +02:00
v1.12.0 — takeoff & landing phases on the marker
CI / build + unit + e2e (pull_request) Failing after 7m29s
82e4c64206
Every leg gains a 10-min takeoff phase at the origin and a 10-min
landing phase at the destination, each weather-scaled up to 2x via a
monotone condition rule (Clear < Cloudy < Rain < Fog). Single
composition in lib/sim/movement.ts: BASE_PHASE_MS, phaseFactor,
MAX_PHASE_FACTOR, legPhases; arrivalTimeFor and progressAt route
through it (progress is now piecewise — hold at origin / linear cruise
/ hold at destination), so engine arrival gate, client position/fuel
recompute and the job expiry floor all agree. Phases burn no fuel.

Marker shows the current phase next to the status in the popup
(mapData derives it from departureSimTime + clockMs; new
data-testid=plane-phase sibling). plane-status keeps the raw status.

Tests: movement.test.ts (phaseFactor bounds/monotonicity, origin-vs-dest
composition, piecewise progressAt), engine.test.ts (deleted local travelMs
mirror — all ~18 fixture clocks use the real composition), seed.test.ts
(phase-inclusive expiry floor mirrors). New e2e/phases.spec.ts asserts
'taking off' at the origin gate and 'landing' at the destination gate on
EDDF→LEMD using the same legPhases boundaries as the engine.
kamilkrzywda scheduled this pull request to auto merge when all checks succeed 2026-09-28 09:13:20 +02:00
kamilkrzywda added 1 commit 2026-09-28 11:53:09 +02:00
kamilkrzywda added 1 commit 2026-09-28 12:08:09 +02:00
kamilkrzywda merged commit ff432da97e into master 2026-09-28 12:15:37 +02:00
kamilkrzywda deleted branch feat/takeoff-landing-phases 2026-09-28 12:15:37 +02:00
Sign in to join this conversation.