Merge pull request 'v0.48.0 — show leg distance (km) on job list and on-board contracts (#95)' (#100) from feat/leg-distance into master
CI / build + unit + e2e (push) Successful in 9m5s
CI / build + unit + e2e (push) Successful in 9m5s
This commit was merged in pull request #100.
This commit is contained in:
@@ -1,5 +1,9 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v0.48.0 — 2026-09-22 06:55
|
||||||
|
- **Leg distance shown on contracts (ISS-095).** `PackageState` gains `distanceKm` (`lib/game/state.ts`) — the great-circle origin→dest distance, computed server-side in `getStateSnapshot()` from the airport GeoJSON already loaded for live-plane math (same `haversineKm` as flight movement; 0 if a coord is missing). Shown in two places via new `formatKm()` (`lib/format.ts`, integer km, no space before "km"): the on-board contract line (`EGCC → GMME 554km €23,826.00 …`) and the job-list row (`€37,408.00 215km Perishables 0.4t …`). OpenAPI spec updated in lockstep; nothing stored
|
||||||
|
- **Tests.** Unit: `app/api/state/route.test.ts` asserts every package's `distanceKm` equals the haversine of its airports' coordinates (computed with the same lib/geo helper); `lib/openapi.test.ts` pinned key list gains `distanceKm`; new `formatKm` test in `lib/format.test.ts`. E2E: `right-pane.spec.ts` +2 assertions (job-row distance span matches /^\d+km$/, generic and hook-created package), `actions.spec.ts` +1 (on-board line)
|
||||||
|
|
||||||
## v0.47.0 — 2026-09-21 23:01
|
## v0.47.0 — 2026-09-21 23:01
|
||||||
- **Job payoff retuned so weight dominates (ISS-090).** `REWARD_PER_KG` in `lib/game/seed.ts` rises from €0.5/kg to €2.0/kg — the linear form is unchanged, only the constant moves. Before, the weight term was small vs base+distance, so heavier jobs barely paid more (a 1000 km leg: €3,250 @500 kg → €5,000 @4,000 kg — per-kg rate fell from €4.59 to €1.01) and there was no sense in taking heavy jobs; after the retune the same leg pays €4,000 / €11,000, so weight clearly drives payoff. Safe for viability: fuel burn (`lib/sim/movement.ts`) is weight-independent, so this only raises income
|
- **Job payoff retuned so weight dominates (ISS-090).** `REWARD_PER_KG` in `lib/game/seed.ts` rises from €0.5/kg to €2.0/kg — the linear form is unchanged, only the constant moves. Before, the weight term was small vs base+distance, so heavier jobs barely paid more (a 1000 km leg: €3,250 @500 kg → €5,000 @4,000 kg — per-kg rate fell from €4.59 to €1.01) and there was no sense in taking heavy jobs; after the retune the same leg pays €4,000 / €11,000, so weight clearly drives payoff. Safe for viability: fuel burn (`lib/sim/movement.ts`) is weight-independent, so this only raises income
|
||||||
- **Tests.** `seed.test.ts`: stale test name "… 0.5 * weightKg …" made generic; new literal pins in the `rewardFor` formula test — `rewardFor(1000, 4000) === 11_000` and `rewardFor(1000, 500) === 4_000` — so future retunes are deliberate. All other reward assertions recompute through the exported constants and pass unchanged
|
- **Tests.** `seed.test.ts`: stale test name "… 0.5 * weightKg …" made generic; new literal pins in the `rewardFor` formula test — `rewardFor(1000, 4000) === 11_000` and `rewardFor(1000, 500) === 4_000` — so future retunes are deliberate. All other reward assertions recompute through the exported constants and pass unchanged
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import fs from 'node:fs/promises';
|
|||||||
import os from 'node:os';
|
import os from 'node:os';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||||
|
import { loadAirportsGeoJson } from '@/lib/airports';
|
||||||
|
import { haversineKm } from '@/lib/geo';
|
||||||
import { FUEL_PRICE_PER_L, PLANE_TYPES, SELL_FRACTION } from '@/lib/game/catalog';
|
import { FUEL_PRICE_PER_L, PLANE_TYPES, SELL_FRACTION } from '@/lib/game/catalog';
|
||||||
import { GET } from './route';
|
import { GET } from './route';
|
||||||
|
|
||||||
@@ -58,9 +60,20 @@ describe('GET /api/state', () => {
|
|||||||
|
|
||||||
const packages = body.packages as Array<Record<string, unknown>>;
|
const packages = body.packages as Array<Record<string, unknown>>;
|
||||||
expect(packages.length).toBeGreaterThan(0);
|
expect(packages.length).toBeGreaterThan(0);
|
||||||
for (const key of ['id', 'originIcao', 'destIcao', 'reward', 'cargoType', 'weightKg', 'deadlineSimTime', 'status']) {
|
for (const key of ['id', 'originIcao', 'destIcao', 'reward', 'cargoType', 'weightKg', 'distanceKm', 'deadlineSimTime', 'status']) {
|
||||||
expect(packages[0], `package.${key}`).toHaveProperty(key);
|
expect(packages[0], `package.${key}`).toHaveProperty(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// v0.48.0 (ISS-095): distanceKm is computed server-side — it must equal the haversine of the
|
||||||
|
// fixture airports' coordinates for every package in the snapshot.
|
||||||
|
const fc = await loadAirportsGeoJson();
|
||||||
|
const coords = new Map(fc.features.map((f) => [f.properties.icao, f.properties]));
|
||||||
|
for (const pkg of packages) {
|
||||||
|
const from = coords.get(String(pkg.originIcao));
|
||||||
|
const to = coords.get(String(pkg.destIcao));
|
||||||
|
if (!from || !to) continue; // missing coord → 0 fallback (cannot happen with the committed dataset)
|
||||||
|
expect(pkg.distanceKm, `package ${String(pkg.id)} distance`).toBeCloseTo(haversineKm(from.lat, from.lon, to.lat, to.lon), 6);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('is stable across repeated calls (same seeded DB)', async () => {
|
it('is stable across repeated calls (same seeded DB)', async () => {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import type { GeoJsonFeatureCollection } from '@/lib/airports';
|
|||||||
import type { PlaneState, StateSnapshot } from '@/lib/game/state';
|
import type { PlaneState, StateSnapshot } from '@/lib/game/state';
|
||||||
import { PLANE_TYPES } from '@/lib/game/catalog';
|
import { PLANE_TYPES } from '@/lib/game/catalog';
|
||||||
import { abandonPenalty } from '@/lib/game/penalty';
|
import { abandonPenalty } from '@/lib/game/penalty';
|
||||||
import { formatCash, formatDeadlineRelative, formatFuel, formatSimClock, formatSpecialLabel } from '@/lib/format';
|
import { formatCash, formatDeadlineRelative, formatFuel, formatKm, formatSimClock, formatSpecialLabel } from '@/lib/format';
|
||||||
import { TIME_SCALES, scaleToSlider, sliderToScale } from '@/lib/timeScale';
|
import { TIME_SCALES, scaleToSlider, sliderToScale } from '@/lib/timeScale';
|
||||||
import { useGameAction } from '@/lib/useGameAction';
|
import { useGameAction } from '@/lib/useGameAction';
|
||||||
import { useSimClock } from '@/lib/useSimClock';
|
import { useSimClock } from '@/lib/useSimClock';
|
||||||
@@ -441,6 +441,8 @@ export default function LeftPane({ state, loading, error, onRefresh, applySnapsh
|
|||||||
<span data-testid={`onboard-route-${pkg.id}`} className="contract-route">
|
<span data-testid={`onboard-route-${pkg.id}`} className="contract-route">
|
||||||
{pkg.originIcao} → {pkg.destIcao}
|
{pkg.originIcao} → {pkg.destIcao}
|
||||||
</span>
|
</span>
|
||||||
|
{/* v0.48.0 (ISS-095): leg distance after the route, e.g. "EGCC → GMME 554km". */}
|
||||||
|
<span data-testid={`onboard-distance-${pkg.id}`}>{formatKm(pkg.distanceKm)}</span>
|
||||||
{/* v0.37.0 (ISS-073): special-status badge — rendered only when the package has one. */}
|
{/* v0.37.0 (ISS-073): special-status badge — rendered only when the package has one. */}
|
||||||
{pkg.special && (
|
{pkg.special && (
|
||||||
<span data-testid={`package-special-${pkg.id}`} className="special-badge">
|
<span data-testid={`package-special-${pkg.id}`} className="special-badge">
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { useState } from 'react';
|
|||||||
import type { PackageState, PlaneState, StateSnapshot } from '@/lib/game/state';
|
import type { PackageState, PlaneState, StateSnapshot } from '@/lib/game/state';
|
||||||
import { MAX_PLANE_DISCOUNT, PLANE_DISCOUNT_PER_REP, PLANE_TYPES, repRate, SELL_FRACTION } from '@/lib/game/catalog';
|
import { MAX_PLANE_DISCOUNT, PLANE_DISCOUNT_PER_REP, PLANE_TYPES, repRate, SELL_FRACTION } from '@/lib/game/catalog';
|
||||||
import { maxRangeKm } from '@/lib/sim/movement';
|
import { maxRangeKm } from '@/lib/sim/movement';
|
||||||
import { formatCargo, formatCash, formatDeadlineRelative, formatSpecialLabel } from '@/lib/format';
|
import { formatCargo, formatCash, formatDeadlineRelative, formatKm, formatSpecialLabel } from '@/lib/format';
|
||||||
import { useGameAction } from '@/lib/useGameAction';
|
import { useGameAction } from '@/lib/useGameAction';
|
||||||
import type { HoverTarget } from './hoverTypes';
|
import type { HoverTarget } from './hoverTypes';
|
||||||
|
|
||||||
@@ -234,6 +234,8 @@ export default function RightPane({ state, loading, applySnapshot, onHover, onEr
|
|||||||
</div>
|
</div>
|
||||||
<div className="contract-sub">
|
<div className="contract-sub">
|
||||||
<span data-testid={`open-package-reward-${pkg.id}`}>{formatCash(pkg.reward)}</span>
|
<span data-testid={`open-package-reward-${pkg.id}`}>{formatCash(pkg.reward)}</span>
|
||||||
|
{/* v0.48.0 (ISS-095): leg distance after the reward, e.g. "€37,408.00 215km". */}
|
||||||
|
<span data-testid={`open-package-distance-${pkg.id}`}>{formatKm(pkg.distanceKm)}</span>
|
||||||
{/* ISS-066 (v0.35.0): cargo shows as type + tonnage ("Mail 10.0t"), not "type × count". */}
|
{/* ISS-066 (v0.35.0): cargo shows as type + tonnage ("Mail 10.0t"), not "type × count". */}
|
||||||
<span data-testid={`open-package-cargo-${pkg.id}`}>{formatCargo(pkg.cargoType, pkg.weightKg)}</span>
|
<span data-testid={`open-package-cargo-${pkg.id}`}>{formatCargo(pkg.cargoType, pkg.weightKg)}</span>
|
||||||
<span data-testid={`open-package-deadline-${pkg.id}`}>
|
<span data-testid={`open-package-deadline-${pkg.id}`}>
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ function pkg(over: Partial<PackageState> & Pick<PackageState, 'id'>): PackageSta
|
|||||||
cargoCount: 4,
|
cargoCount: 4,
|
||||||
weightKg: 1_000,
|
weightKg: 1_000,
|
||||||
special: null, // v0.37.0 (ISS-073) — no special status by default in fixtures
|
special: null, // v0.37.0 (ISS-073) — no special status by default in fixtures
|
||||||
|
distanceKm: 1839, // v0.48.0 (ISS-095) — EDDF→LEMD great-circle distance
|
||||||
deadlineSimTime: 1_000_000,
|
deadlineSimTime: 1_000_000,
|
||||||
status: 'open',
|
status: 'open',
|
||||||
planeId: null,
|
planeId: null,
|
||||||
|
|||||||
@@ -197,6 +197,8 @@ test('full core loop via UI: load → fly to destination → exact reward + rep
|
|||||||
await expect(onboardRow, 'loaded package appears in the on-board list').toBeVisible();
|
await expect(onboardRow, 'loaded package appears in the on-board list').toBeVisible();
|
||||||
// ISS-081 (v0.40.0): the on-board route shows ICAO tags, not full airport names.
|
// ISS-081 (v0.40.0): the on-board route shows ICAO tags, not full airport names.
|
||||||
await expect(page.getByTestId(`onboard-route-${pkg.id}`)).toContainText(`${HUB} → ${nearest.icao}`);
|
await expect(page.getByTestId(`onboard-route-${pkg.id}`)).toContainText(`${HUB} → ${nearest.icao}`);
|
||||||
|
// v0.48.0 (ISS-095): leg distance on the on-board line, right after the route — integer km.
|
||||||
|
await expect(page.getByTestId(`onboard-distance-${pkg.id}`)).toHaveText(/^\d+km$/);
|
||||||
|
|
||||||
// FLY via the UI: click the fly-to button in the on-board list (ISS-081: ICAO label).
|
// FLY via the UI: click the fly-to button in the on-board list (ISS-081: ICAO label).
|
||||||
const flyBtn = page.getByTestId(`fly-to-${pkg.id}`);
|
const flyBtn = page.getByTestId(`fly-to-${pkg.id}`);
|
||||||
|
|||||||
@@ -210,6 +210,8 @@ test('open packages render with names, reward and deadline; Load is enabled at a
|
|||||||
await expect(rows.first(), 'an open package row appears').toBeVisible({ timeout: 15_000 });
|
await expect(rows.first(), 'an open package row appears').toBeVisible({ timeout: 15_000 });
|
||||||
await expect(rows.first()).toContainText(/→/);
|
await expect(rows.first()).toContainText(/→/);
|
||||||
await expect(page.locator('[data-testid^="open-package-reward-"]').first()).toContainText('€');
|
await expect(page.locator('[data-testid^="open-package-reward-"]').first()).toContainText('€');
|
||||||
|
// v0.48.0 (ISS-095): leg distance on the job row — integer km, no space before "km".
|
||||||
|
await expect(page.locator('[data-testid^="open-package-distance-"]').first()).toHaveText(/^\d+km$/);
|
||||||
|
|
||||||
// Deterministic target: a hook-created EDDF package — Load is enabled (starter at origin, free slot).
|
// Deterministic target: a hook-created EDDF package — Load is enabled (starter at origin, free slot).
|
||||||
const nearest = await nearestToHub(page);
|
const nearest = await nearestToHub(page);
|
||||||
@@ -220,6 +222,8 @@ test('open packages render with names, reward and deadline; Load is enabled at a
|
|||||||
// 4 t is within the starter ATR's 4.8 t maxPayloadKg, so Load stays enabled below.
|
// 4 t is within the starter ATR's 4.8 t maxPayloadKg, so Load stays enabled below.
|
||||||
await expect(page.getByTestId(`open-package-cargo-${pkg.id}`)).toContainText('4.0t');
|
await expect(page.getByTestId(`open-package-cargo-${pkg.id}`)).toContainText('4.0t');
|
||||||
await expect(page.getByTestId(`open-package-reward-${pkg.id}`)).toContainText(eur(pkg.reward));
|
await expect(page.getByTestId(`open-package-reward-${pkg.id}`)).toContainText(eur(pkg.reward));
|
||||||
|
// v0.48.0 (ISS-095): the hook-created package's row shows its leg distance too.
|
||||||
|
await expect(page.getByTestId(`open-package-distance-${pkg.id}`)).toHaveText(/^\d+km$/);
|
||||||
const loadBtn = page.getByTestId(`load-${pkg.id}`);
|
const loadBtn = page.getByTestId(`load-${pkg.id}`);
|
||||||
await expect(loadBtn).toBeEnabled();
|
await expect(loadBtn).toBeEnabled();
|
||||||
|
|
||||||
|
|||||||
@@ -36,9 +36,10 @@ const basePackage = (id: string): PackageState => ({
|
|||||||
reward: 1234.5,
|
reward: 1234.5,
|
||||||
cargoType: 'mail',
|
cargoType: 'mail',
|
||||||
cargoCount: 2,
|
cargoCount: 2,
|
||||||
weightKg: 600,
|
weightKg: 600,
|
||||||
special: null, // v0.37.0 (ISS-073) — no special status by default in fixtures
|
special: null, // v0.37.0 (ISS-073) — no special status by default in fixtures
|
||||||
deadlineSimTime: 0,
|
distanceKm: 1839, // v0.48.0 (ISS-095) — EDDF→LEMD great-circle distance
|
||||||
|
deadlineSimTime: 0,
|
||||||
status: 'open',
|
status: 'open',
|
||||||
planeId: null,
|
planeId: null,
|
||||||
createdSimTime: 0,
|
createdSimTime: 0,
|
||||||
|
|||||||
+9
-1
@@ -1,5 +1,5 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import { formatCargo, formatCash, formatDeadlineRelative, formatDuration, formatFuel, formatSimClock } from './format';
|
import { formatCargo, formatCash, formatDeadlineRelative, formatDuration, formatFuel, formatKm, formatSimClock } from './format';
|
||||||
|
|
||||||
describe('formatCash', () => {
|
describe('formatCash', () => {
|
||||||
it('formats EUR with thousands separators and two decimals', () => {
|
it('formats EUR with thousands separators and two decimals', () => {
|
||||||
@@ -30,6 +30,14 @@ describe('formatCargo', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('formatKm', () => {
|
||||||
|
it('rounds to integer km with no space before "km" (v0.48.0, ISS-095)', () => {
|
||||||
|
expect(formatKm(554)).toBe('554km');
|
||||||
|
expect(formatKm(214.6)).toBe('215km');
|
||||||
|
expect(formatKm(0)).toBe('0km');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('formatSimClock', () => {
|
describe('formatSimClock', () => {
|
||||||
it('formats epoch ms as a UTC wall-clock string with seconds (deterministic across timezones)', () => {
|
it('formats epoch ms as a UTC wall-clock string with seconds (deterministic across timezones)', () => {
|
||||||
expect(formatSimClock(Date.UTC(2026, 8, 17, 14, 5))).toBe('2026-09-17 14:05:00 UTC');
|
expect(formatSimClock(Date.UTC(2026, 8, 17, 14, 5))).toBe('2026-09-17 14:05:00 UTC');
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ export function formatCargo(cargoType: string, weightKg: number): string {
|
|||||||
return `${cargoType} ${(weightKg / 1000).toFixed(1)}t`;
|
return `${cargoType} ${(weightKg / 1000).toFixed(1)}t`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Leg distance for contract rows (v0.48.0, ISS-095): "554km" — integer km, no space before "km".
|
||||||
|
export function formatKm(km: number): string {
|
||||||
|
return `${Math.round(km)}km`;
|
||||||
|
}
|
||||||
|
|
||||||
// Special-status badge label (v0.37.0, ISS-073): "priority" → "Priority".
|
// Special-status badge label (v0.37.0, ISS-073): "priority" → "Priority".
|
||||||
export function formatSpecialLabel(special: string): string {
|
export function formatSpecialLabel(special: string): string {
|
||||||
return special.charAt(0).toUpperCase() + special.slice(1);
|
return special.charAt(0).toUpperCase() + special.slice(1);
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ export interface PackageState {
|
|||||||
weightKg: number;
|
weightKg: number;
|
||||||
// Special status (v0.37.0, ISS-073) — key into lib/game/specials.ts SPECIALS; null = none.
|
// Special status (v0.37.0, ISS-073) — key into lib/game/specials.ts SPECIALS; null = none.
|
||||||
special: string | null;
|
special: string | null;
|
||||||
|
// Great-circle leg distance in km (v0.48.0, ISS-095) — computed from airport coords, not stored.
|
||||||
|
distanceKm: number;
|
||||||
deadlineSimTime: number;
|
deadlineSimTime: number;
|
||||||
// 'ignored' (v0.29.0, ISS-047 part B) = a dismissed open job — terminal, leaves the market.
|
// 'ignored' (v0.29.0, ISS-047 part B) = a dismissed open job — terminal, leaves the market.
|
||||||
status: 'open' | 'loaded' | 'delivered' | 'expired' | 'abandoned' | 'ignored';
|
status: 'open' | 'loaded' | 'delivered' | 'expired' | 'abandoned' | 'ignored';
|
||||||
@@ -100,6 +102,12 @@ export async function getStateSnapshot(): Promise<StateSnapshot> {
|
|||||||
const nameByIcao = new Map(fc.features.map((f) => [f.properties.icao, f.properties.name]));
|
const nameByIcao = new Map(fc.features.map((f) => [f.properties.icao, f.properties.name]));
|
||||||
const nameOf = (icao: string) => nameByIcao.get(icao) ?? icao;
|
const nameOf = (icao: string) => nameByIcao.get(icao) ?? icao;
|
||||||
const coordsByIcao = new Map(fc.features.map((f) => [f.properties.icao, f.properties]));
|
const coordsByIcao = new Map(fc.features.map((f) => [f.properties.icao, f.properties]));
|
||||||
|
// v0.48.0 (ISS-095): leg distance for the UI — same haversine as live-plane math; 0 if a coord is missing.
|
||||||
|
const legDistanceKm = (originIcao: string, destIcao: string) => {
|
||||||
|
const from = coordsByIcao.get(originIcao);
|
||||||
|
const to = coordsByIcao.get(destIcao);
|
||||||
|
return from && to ? haversineKm(from.lat, from.lon, to.lat, to.lon) : 0;
|
||||||
|
};
|
||||||
|
|
||||||
// Analytic movement (v0.7.0): a flying plane's stored lat/lon/fuel are its
|
// Analytic movement (v0.7.0): a flying plane's stored lat/lon/fuel are its
|
||||||
// departure values — live position and fuel are pure functions of the clock.
|
// departure values — live position and fuel are pure functions of the clock.
|
||||||
@@ -162,6 +170,7 @@ export async function getStateSnapshot(): Promise<StateSnapshot> {
|
|||||||
cargoCount: p.cargoCount,
|
cargoCount: p.cargoCount,
|
||||||
weightKg: p.weightKg,
|
weightKg: p.weightKg,
|
||||||
special: p.special,
|
special: p.special,
|
||||||
|
distanceKm: legDistanceKm(p.originIcao, p.destIcao), // v0.48.0 (ISS-095)
|
||||||
deadlineSimTime: p.deadlineSimTime,
|
deadlineSimTime: p.deadlineSimTime,
|
||||||
status: p.status,
|
status: p.status,
|
||||||
planeId: p.planeId,
|
planeId: p.planeId,
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ describe('OpenAPI spec', () => {
|
|||||||
'deadlineSimTime',
|
'deadlineSimTime',
|
||||||
'destIcao',
|
'destIcao',
|
||||||
'destName',
|
'destName',
|
||||||
|
'distanceKm', // v0.48.0 (ISS-095) — great-circle leg distance in km
|
||||||
'id',
|
'id',
|
||||||
'originIcao',
|
'originIcao',
|
||||||
'originName',
|
'originName',
|
||||||
|
|||||||
@@ -633,6 +633,7 @@ export const openApiSpec = {
|
|||||||
cargoCount: { type: 'integer' },
|
cargoCount: { type: 'integer' },
|
||||||
weightKg: { type: 'number', description: 'Package weight in kg (v0.27.0) — drives load validation and the per-kg payout part.' },
|
weightKg: { type: 'number', description: 'Package weight in kg (v0.27.0) — drives load validation and the per-kg payout part.' },
|
||||||
special: { type: ['string', 'null'], description: 'Special status from the specials registry (v0.37.0); null = none.' },
|
special: { type: ['string', 'null'], description: 'Special status from the specials registry (v0.37.0); null = none.' },
|
||||||
|
distanceKm: { type: 'number', description: 'Great-circle leg distance in km (v0.48.0) — computed server-side from airport coordinates, not stored.' },
|
||||||
deadlineSimTime: { type: 'integer', description: 'Sim-clock epoch ms (game time)' },
|
deadlineSimTime: { type: 'integer', description: 'Sim-clock epoch ms (game time)' },
|
||||||
// 'expired' = an open OR loaded package whose deadline passed on a tick;
|
// 'expired' = an open OR loaded package whose deadline passed on a tick;
|
||||||
// loaded expired packages keep their slot, deliver no reward, abandon free.
|
// loaded expired packages keep their slot, deliver no reward, abandon free.
|
||||||
@@ -651,6 +652,7 @@ export const openApiSpec = {
|
|||||||
'cargoType',
|
'cargoType',
|
||||||
'cargoCount',
|
'cargoCount',
|
||||||
'weightKg', // v0.27.0 (ISS-045) — package weight in kg
|
'weightKg', // v0.27.0 (ISS-045) — package weight in kg
|
||||||
|
'distanceKm', // v0.48.0 (ISS-095) — great-circle leg distance in km
|
||||||
'deadlineSimTime',
|
'deadlineSimTime',
|
||||||
'status',
|
'status',
|
||||||
'planeId',
|
'planeId',
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "flight-ops",
|
"name": "flight-ops",
|
||||||
"version": "0.47.0",
|
"version": "0.48.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "next dev -p 3000",
|
"dev": "next dev -p 3000",
|
||||||
|
|||||||
Reference in New Issue
Block a user