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:
@@ -1,7 +1,7 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { WORLD_TIME_SCALE } from '@/lib/worldClock';
|
||||
import { haversineKm } from '@/lib/geo';
|
||||
import { travelDurationMs } from '@/lib/sim/movement';
|
||||
import { legPhases } from '@/lib/sim/movement';
|
||||
import { FASTEST_CATALOG_SPEED_KMH, PLANE_TYPES, repRate, STARTER_MODEL } from './catalog';
|
||||
import { SPECIALS } from './specials';
|
||||
import {
|
||||
@@ -478,7 +478,8 @@ describe('expiry floor (v0.54.0, ISS-119)', () => {
|
||||
const c = generatePackage({ id: `c-floor-${seed}`, airports, rng: mulberry32(seed), nowMs: NOW }); // special rolls naturally (~15% priority)
|
||||
const o = airports.find((a) => a.icao === c.originIcao)!;
|
||||
const d = airports.find((a) => a.icao === c.destIcao)!;
|
||||
const floorMs = travelDurationMs(haversineKm(o.lat, o.lon, d.lat, d.lon), FASTEST_CATALOG_SPEED_KMH) + EXPIRY_BUFFER_MS;
|
||||
// v1.12.0: the floor includes the weather-scaled takeoff/landing phases (worst case for this pair).
|
||||
const floorMs = legPhases(haversineKm(o.lat, o.lon, d.lat, d.lon), FASTEST_CATALOG_SPEED_KMH, o.icao, d.icao).totalMs + EXPIRY_BUFFER_MS;
|
||||
expect(c.deadlineSimTime).toBeGreaterThanOrEqual(Math.round(NOW + floorMs));
|
||||
}
|
||||
});
|
||||
@@ -503,7 +504,8 @@ describe('expiry floor (v0.54.0, ISS-119)', () => {
|
||||
}
|
||||
expect(bestKm).toBeGreaterThan(8_000); // the dataset really spans near-cap legs
|
||||
|
||||
const floorMs = travelDurationMs(bestKm, FASTEST_CATALOG_SPEED_KMH) + EXPIRY_BUFFER_MS;
|
||||
// v1.12.0: phase-inclusive floor — same composition seed.ts uses for this pair.
|
||||
const floorMs = legPhases(bestKm, FASTEST_CATALOG_SPEED_KMH, airports[bestI].icao, airports[bestJ].icao).totalMs + EXPIRY_BUFFER_MS;
|
||||
for (const special of ['priority', null] as const) {
|
||||
let clamped = 0;
|
||||
for (let seed = 1; seed <= 50; seed++) {
|
||||
|
||||
+6
-4
@@ -1,5 +1,5 @@
|
||||
import { haversineKm } from '@/lib/geo';
|
||||
import { travelDurationMs } from '@/lib/sim/movement';
|
||||
import { legPhases } from '@/lib/sim/movement';
|
||||
import { WORLD_TIME_SCALE } from '@/lib/worldClock';
|
||||
import { FASTEST_CATALOG_SPEED_KMH, PLANE_TYPES, repRate, STARTER_MODEL } from './catalog';
|
||||
import { pickSpecial, SPECIALS } from './specials';
|
||||
@@ -284,9 +284,11 @@ export function generatePackage(input: {
|
||||
// single uniform value, so clamping it up AFTER special scaling covers both requirements: short legs
|
||||
// (drawn × scale already above the floor) are byte-identical to before, and when the raw draw falls
|
||||
// below the floor the deadline lands exactly on it — which stays ≤ the scaled ceiling for any pair of
|
||||
// airports on Earth (max leg ~20000 km → floor ≈ 22.8 h < the 36 h priority ceiling), so no separate
|
||||
// ceiling raise is needed. Forced offsets skip the floor, same as they skip the special scaling.
|
||||
const expiryFloorMs = travelDurationMs(km, FASTEST_CATALOG_SPEED_KMH) + EXPIRY_BUFFER_MS;
|
||||
// airports on Earth (max leg ~20000 km → floor ≈ 23.5 h incl. worst-case phases < the 36 h priority
|
||||
// ceiling), so no separate ceiling raise is needed. Forced offsets skip the floor, same as they skip
|
||||
// the special scaling. v1.12.0: the travel time includes the weather-scaled takeoff/landing phases —
|
||||
// a package must be finishable even when both airports are in their worst (Fog) conditions.
|
||||
const expiryFloorMs = legPhases(km, FASTEST_CATALOG_SPEED_KMH, origin.icao, dest.icao).totalMs + EXPIRY_BUFFER_MS;
|
||||
const deadlineMs = input.deadlineOffsetMs ?? Math.max(drawnWindowMs * (mods?.deadlineScale ?? 1), expiryFloorMs);
|
||||
|
||||
// v0.38.0 (ISS-072): reputation bonus composed with the special multiplier, single round at the end.
|
||||
|
||||
+3
-1
@@ -192,7 +192,9 @@ export async function getStateSnapshot(playerId: string): Promise<StateSnapshot>
|
||||
const to = coordsByIcao.get(p.destIcao);
|
||||
if (!type || !from || !to) return null;
|
||||
const distanceKm = haversineKm(from.lat, from.lon, to.lat, to.lon);
|
||||
const progress = progressAt(p.departureSimTime, distanceKm, type.speedKmh, simRow.clockMs);
|
||||
// v1.12.0: piecewise cruise progress — holds at the origin during takeoff and at the
|
||||
// destination during landing (weather-scaled phase durations from legPhases).
|
||||
const progress = progressAt(p.departureSimTime, distanceKm, type.speedKmh, p.locationIcao, p.destIcao, simRow.clockMs);
|
||||
const pos = positionAt(from.lat, from.lon, to.lat, to.lon, progress);
|
||||
// Wind-adjusted burn (v0.36.0) — weather is a pure function of the departure ICAO, so this
|
||||
// recomputes exactly the factor the engine used for the leg; no stored state needed.
|
||||
|
||||
Reference in New Issue
Block a user