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:
+19
-22
@@ -231,7 +231,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
const { inst, km, type } = await setupFlight('deliver-early.db');
|
||||
const playerBefore = (await getLocalPlayer(inst));
|
||||
|
||||
const clock = T0 + travelMs(km, type.speedKmh) - 1; // one ms before arrival
|
||||
const clock = arrivalTimeFor(T0, km, type.speedKmh, FROM, TO) - 1; // one ms before arrival
|
||||
expect(await deliverPackages(inst, clock)).toBe(0);
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
@@ -252,7 +252,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
{ id: 'p-elsewhere', originIcao: FROM, destIcao: 'EHAM', reward: 9_000, cargoType: 'Mail', cargoCount: 10, deadlineSimTime: T0 + 24 * 3_600_000, status: 'loaded', planeId: 'plane-starter', createdSimTime: T0 },
|
||||
]);
|
||||
|
||||
const clock = T0 + travelMs(km, type.speedKmh); // exactly at arrival
|
||||
const clock = arrivalTimeFor(T0, km, type.speedKmh, FROM, TO); // exactly at arrival
|
||||
expect(await deliverPackages(inst, clock)).toBe(1);
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
@@ -298,7 +298,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
{ id: 'p-b', originIcao: FROM, destIcao: TO, reward: 2_500, cargoType: 'Mail', cargoCount: 1, deadlineSimTime: T0 + 24 * 3_600_000, status: 'loaded', planeId: 'plane-starter', createdSimTime: T0 },
|
||||
]);
|
||||
|
||||
const clock = T0 + travelMs(km, PLANE_TYPES.atr72.speedKmh);
|
||||
const clock = arrivalTimeFor(T0, km, PLANE_TYPES.atr72.speedKmh, FROM, TO);
|
||||
expect(await deliverPackages(inst, clock)).toBe(1);
|
||||
|
||||
const player = (await getLocalPlayer(inst));
|
||||
@@ -317,7 +317,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
{ id: 'p-prio', originIcao: FROM, destIcao: TO, reward: 5_000, cargoType: 'Mail', cargoCount: 10, special: 'priority', deadlineSimTime: T0 + 24 * 3_600_000, status: 'loaded', planeId: 'plane-starter', createdSimTime: T0 },
|
||||
]);
|
||||
|
||||
const clock = T0 + travelMs(km, PLANE_TYPES.atr72.speedKmh); // exactly at arrival
|
||||
const clock = arrivalTimeFor(T0, km, PLANE_TYPES.atr72.speedKmh, FROM, TO); // exactly at arrival
|
||||
expect(await deliverPackages(inst, clock)).toBe(1);
|
||||
|
||||
const player = (await getLocalPlayer(inst));
|
||||
@@ -358,7 +358,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
.set({ queue: JSON.stringify(['EHAM', 'EDDV']) })
|
||||
.where(eq(schema.planes.id, 'plane-starter'));
|
||||
|
||||
const clock = T0 + travelMs(km, type.speedKmh); // exactly at arrival
|
||||
const clock = arrivalTimeFor(T0, km, type.speedKmh, FROM, TO); // exactly at arrival
|
||||
expect(await deliverPackages(inst, clock)).toBe(1);
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
@@ -383,7 +383,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
.where(eq(schema.planes.id, 'plane-starter'));
|
||||
|
||||
const playerBefore = (await getLocalPlayer(inst)); // seeded €20k — the top-up is affordable
|
||||
const clock = T0 + travelMs(km, type.speedKmh); // exactly at arrival
|
||||
const clock = arrivalTimeFor(T0, km, type.speedKmh, FROM, TO); // exactly at arrival
|
||||
expect(await deliverPackages(inst, clock)).toBe(1);
|
||||
|
||||
// Expected top-up from the SAME helpers the engine uses: landed fuel (≈ 0), wind-adjusted
|
||||
@@ -429,7 +429,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
.where(eq(schema.planes.id, 'plane-starter'));
|
||||
await updateLocalPlayerEconomy(inst, { cash: 0 });
|
||||
|
||||
const clock = T0 + travelMs(km, type.speedKmh); // exactly at arrival
|
||||
const clock = arrivalTimeFor(T0, km, type.speedKmh, FROM, TO); // exactly at arrival
|
||||
expect(await deliverPackages(inst, clock)).toBe(1);
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
@@ -445,7 +445,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
it('grounds as before when the queue is empty', async () => {
|
||||
const { inst, km } = await setupFlight('chain-empty.db'); // seeded default queue '[]'
|
||||
|
||||
expect(await deliverPackages(inst, T0 + travelMs(km, PLANE_TYPES.atr72.speedKmh))).toBe(1);
|
||||
expect(await deliverPackages(inst, arrivalTimeFor(T0, km, PLANE_TYPES.atr72.speedKmh, FROM, TO))).toBe(1);
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.status).toBe('available'); // unchanged landing behavior…
|
||||
@@ -473,7 +473,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
.set({ queue: JSON.stringify(['FGSL']) })
|
||||
.where(eq(schema.planes.id, 'plane-starter'));
|
||||
|
||||
const clock = T0 + travelMs(km, type.speedKmh); // exactly at arrival
|
||||
const clock = arrivalTimeFor(T0, km, type.speedKmh, FROM, TO); // exactly at arrival
|
||||
expect(await deliverPackages(inst, clock)).toBe(1);
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
@@ -490,7 +490,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
.set({ queue: JSON.stringify(['LEMD', 'EHAM']) })
|
||||
.where(eq(schema.planes.id, 'plane-starter'));
|
||||
|
||||
expect(await deliverPackages(inst, T0 + travelMs(km, type.speedKmh))).toBe(1);
|
||||
expect(await deliverPackages(inst, arrivalTimeFor(T0, km, type.speedKmh, FROM, TO))).toBe(1);
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.status).toBe('available'); // grounded — the self-leg was dropped, not flown…
|
||||
@@ -514,7 +514,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
{ id: 'p-owner', originIcao: FROM, destIcao: TO, reward: 5_000, cargoType: 'Mail', cargoCount: 1, deadlineSimTime: T0 + 24 * 3_600_000, status: 'loaded', planeId: 'plane-starter', createdSimTime: T0 },
|
||||
]);
|
||||
|
||||
const clock = T0 + travelMs(km, PLANE_TYPES.atr72.speedKmh); // exactly at arrival
|
||||
const clock = arrivalTimeFor(T0, km, PLANE_TYPES.atr72.speedKmh, FROM, TO); // exactly at arrival
|
||||
expect(await deliverPackages(inst, clock)).toBe(1);
|
||||
|
||||
const bAfter = (await getPlayer(inst, 'player-b'));
|
||||
@@ -540,7 +540,7 @@ describe('deliverPackages (v0.12.0 arrival delivery)', () => {
|
||||
|
||||
const localBefore = (await getPlayer(inst, LOCAL_PLAYER.id));
|
||||
const bBefore = (await getPlayer(inst, 'player-b'));
|
||||
const clock = T0 + travelMs(km, type.speedKmh); // exactly at arrival
|
||||
const clock = arrivalTimeFor(T0, km, type.speedKmh, FROM, TO); // exactly at arrival
|
||||
expect(await deliverPackages(inst, clock)).toBe(1);
|
||||
|
||||
// Expected cost from the SAME helpers the engine uses — computed with B's rep discount.
|
||||
@@ -594,7 +594,7 @@ describe('wear on landed legs (v0.39.0, ISS-069)', () => {
|
||||
it('drops health by exactly WEAR_PER_KM × km on a landed leg', async () => {
|
||||
const { inst, km } = await setupFlight('wear-leg.db');
|
||||
|
||||
expect(await deliverPackages(inst, T0 + travelMs(km, PLANE_TYPES.atr72.speedKmh))).toBe(1);
|
||||
expect(await deliverPackages(inst, arrivalTimeFor(T0, km, PLANE_TYPES.atr72.speedKmh, FROM, TO))).toBe(1);
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.health).toBeCloseTo(100 - WEAR_PER_KM * km, 9); // one mid-range hop ≈ −0.98 health (v0.43.0, ISS-085)
|
||||
@@ -607,18 +607,19 @@ describe('wear on landed legs (v0.39.0, ISS-069)', () => {
|
||||
.set({ health: WEAR_PER_KM * km / 2 })
|
||||
.where(eq(schema.planes.id, 'plane-starter'));
|
||||
|
||||
expect(await deliverPackages(inst, T0 + travelMs(km, type.speedKmh))).toBe(1);
|
||||
expect(await deliverPackages(inst, arrivalTimeFor(T0, km, type.speedKmh, FROM, TO))).toBe(1);
|
||||
let plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.health).toBe(0); // clamped — never negative…
|
||||
|
||||
// …and a second leg keeps it pinned at 0.
|
||||
const eham = (await loadAirportsGeoJson()).features.find((f) => f.properties.icao === 'EHAM')!.properties;
|
||||
const nextKm = haversineKm(to.lat, to.lon, eham.lat, eham.lon);
|
||||
const arr1 = arrivalTimeFor(T0, km, type.speedKmh, FROM, TO); // leg 1's exact arrival (v1.12.0 phases)
|
||||
await inst.db.update(schema.planes)
|
||||
.set({ status: 'flying', destIcao: 'EHAM', departureSimTime: T0 + travelMs(km, type.speedKmh), fuel: type.maxFuel })
|
||||
.set({ status: 'flying', destIcao: 'EHAM', departureSimTime: arr1, fuel: type.maxFuel })
|
||||
.where(eq(schema.planes.id, 'plane-starter'));
|
||||
|
||||
expect(await deliverPackages(inst, T0 + travelMs(km, type.speedKmh) + travelMs(nextKm, type.speedKmh))).toBe(1);
|
||||
expect(await deliverPackages(inst, arrivalTimeFor(arr1, nextKm, type.speedKmh, TO, 'EHAM'))).toBe(1);
|
||||
plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.health).toBe(0); // still clamped after the second leg
|
||||
});
|
||||
@@ -632,7 +633,7 @@ describe('wear on landed legs (v0.39.0, ISS-069)', () => {
|
||||
.set({ health: WEAR_PER_KM * km / 2, queue: JSON.stringify(['EHAM']) })
|
||||
.where(eq(schema.planes.id, 'plane-starter'));
|
||||
|
||||
expect(await deliverPackages(inst, T0 + travelMs(km, type.speedKmh))).toBe(1);
|
||||
expect(await deliverPackages(inst, arrivalTimeFor(T0, km, type.speedKmh, FROM, TO))).toBe(1);
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.health).toBe(0); // broken by this landing's wear…
|
||||
@@ -1006,8 +1007,4 @@ describe('spawnPoolPackages (v0.65.0 per-airport pools)', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// Local helper mirroring lib/sim/movement.ts (kept private there on purpose — the
|
||||
// engine composes arrivalTimeFor; tests just need the duration for fixtures).
|
||||
function travelMs(km: number, speedKmh: number): number {
|
||||
return (km / speedKmh) * 3_600_000;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user