v1.12.0 — takeoff & landing phases on the marker
CI / build + unit + e2e (pull_request) Failing after 7m29s
CI / build + unit + e2e (pull_request) Failing after 7m29s
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.
This commit is contained in:
+25
-2
@@ -1,6 +1,7 @@
|
||||
import type { PlaneState, StateSnapshot } from './game/state';
|
||||
import { formatFuel } from './format';
|
||||
import { positionAt } from './sim/movement';
|
||||
import { haversineKm } from './geo';
|
||||
import { legPhaseAt, positionAt, type LegPhase } from './sim/movement';
|
||||
|
||||
// Pure GeoJSON builders for the map's dynamic layers (v0.11.0, package model
|
||||
// v0.12.0): one Point feature per plane, one LineString per FLYING plane's
|
||||
@@ -14,9 +15,14 @@ export interface PlaneFeatureProps {
|
||||
status: 'available' | 'flying' | 'repairing';
|
||||
fuelText: string; // "1234 / 6300 L"
|
||||
routeText: string; // "Origin → Destination" while flying, '' when grounded
|
||||
// v1.12.0: 'taking off' | 'cruising' | 'landing' while flying (derived from the sim clock), '' when grounded.
|
||||
phaseLabel: string;
|
||||
flying: boolean;
|
||||
}
|
||||
|
||||
// v1.12.0: display text for each timed leg segment (see legPhaseAt in lib/sim/movement.ts).
|
||||
const PHASE_LABELS: Record<LegPhase, string> = { takeoff: 'taking off', cruise: 'cruising', landing: 'landing' };
|
||||
|
||||
export interface RouteFeatureProps {
|
||||
planeId: string;
|
||||
}
|
||||
@@ -54,13 +60,29 @@ export function planesGeoJson(
|
||||
if (!state) return EMPTY_POINTS;
|
||||
// Catalog display name ("ATR 72-600") for the popup — raw model id as fallback.
|
||||
const labelByModel = new Map(state.catalog.map((e) => [e.model, e.label]));
|
||||
const speedByModel = new Map(state.catalog.map((e) => [e.model, e.speedKmh]));
|
||||
const clockMs = state.simState.clockMs;
|
||||
const features: MapPointFeature[] = state.planes.map((p) => {
|
||||
const flying = p.status === 'flying';
|
||||
let lon = p.lon;
|
||||
if (flying && p.destIcao) {
|
||||
// v1.12.0: the marker's phase label — same legPhases composition the engine uses, derived from
|
||||
// departureSimTime + clockMs (both in the snapshot) so no extra state is needed.
|
||||
let phaseLabel = '';
|
||||
if (flying && p.destIcao && p.departureSimTime != null) {
|
||||
const from = airportsByIcao.get(p.locationIcao);
|
||||
const to = airportsByIcao.get(p.destIcao);
|
||||
const speedKmh = speedByModel.get(p.model);
|
||||
if (from && to) lon = unwrapLon(p.lon, from.lon);
|
||||
if (from && to && speedKmh !== undefined) {
|
||||
phaseLabel = PHASE_LABELS[legPhaseAt(
|
||||
p.departureSimTime,
|
||||
haversineKm(from.lat, from.lon, to.lat, to.lon),
|
||||
speedKmh,
|
||||
p.locationIcao,
|
||||
p.destIcao,
|
||||
clockMs,
|
||||
)];
|
||||
}
|
||||
}
|
||||
return {
|
||||
type: 'Feature',
|
||||
@@ -71,6 +93,7 @@ export function planesGeoJson(
|
||||
status: p.status,
|
||||
fuelText: formatFuel(p.fuel, p.maxFuel),
|
||||
routeText: flying && p.destName ? `${p.locationName} → ${p.destName}` : '',
|
||||
phaseLabel,
|
||||
flying,
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user