634 lines
28 KiB
TypeScript
634 lines
28 KiB
TypeScript
import pkg from '@/package.json';
|
||
|
||
// OpenAPI 3.1 document for the app's public JSON API, kept in code so it can be
|
||
// served machine-readable at /api/openapi (see AGENTS.md "## API"). Must be updated
|
||
// whenever an endpoint changes.
|
||
export const openApiSpec = {
|
||
openapi: '3.1.0',
|
||
info: {
|
||
title: 'Flight Ops API',
|
||
description:
|
||
'JSON API for Flight Ops — world airport data for the map, the live game-state snapshot, player actions (load / fly / abandon / buy / sell), and app metadata. Every action endpoint ticks the sim clock first and returns the full state snapshot on success.',
|
||
// Sourced from package.json so it stays in sync with every version bump.
|
||
version: pkg.version,
|
||
},
|
||
// Empty url = current origin (the spec is served from the same host that runs the API).
|
||
servers: [{ url: '' }],
|
||
paths: {
|
||
'/api/airports': {
|
||
get: {
|
||
summary: 'List world airports',
|
||
description:
|
||
'The curated world airport dataset — one marker per metro area (large airports only, close clusters merged to the biggest; see scripts/build-world-airports.mjs) — as a GeoJSON FeatureCollection.',
|
||
operationId: 'listAirports',
|
||
responses: {
|
||
200: {
|
||
description: 'GeoJSON FeatureCollection of world airports',
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/AirportFeatureCollection' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/state': {
|
||
get: {
|
||
summary: 'Game state snapshot',
|
||
description:
|
||
'Full current game state for client polling — account, sim clock, planes and packages, plus the static market reference data (catalog + economy constants) so clients can compute buy/sell previews locally. The world clock advances on a ~1 s background tick at a fixed rate (v0.52.0, ISS-117: no player-facing time controls), clamped per tick; while no plane is flying or in repair the clock stays frozen. Each call ticks first, so expiry/delivery/spawn are always applied before the snapshot is returned.',
|
||
operationId: 'getState',
|
||
responses: {
|
||
200: {
|
||
description: 'Current game state',
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/StateSnapshot' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/events': {
|
||
get: {
|
||
summary: 'Live state stream (SSE)',
|
||
description:
|
||
'Server-Sent Events feed of full game-state snapshots (v0.57.0, ISS-129) — the current snapshot is sent immediately on connect and a fresh one after every sim tick (~1 s), so clients can subscribe instead of polling /api/state. Each event\'s data payload is JSON with exactly the StateSnapshot shape; comment lines starting with ":" are heartbeats.',
|
||
operationId: 'subscribeToEvents',
|
||
responses: {
|
||
200: {
|
||
description: 'SSE stream of state snapshots (text/event-stream)',
|
||
content: {
|
||
'text/event-stream': { schema: { type: 'string' } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/version': {
|
||
get: {
|
||
summary: 'App version',
|
||
description: 'The running app version, read from package.json.',
|
||
operationId: 'getVersion',
|
||
responses: {
|
||
200: {
|
||
description: 'Current app version',
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/AppVersion' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/packages/{id}/load': {
|
||
post: {
|
||
summary: 'Load a package onto a plane',
|
||
description:
|
||
'Bind an OPEN package to a GROUNDED ("available") plane at the package\'s origin (v0.12.0). The plane needs enough payload room for the package weight — maxPayloadKg per catalog model; expired packages still occupy theirs (v0.27.0). The sim clock is ticked first, so the returned snapshot reflects current game time.',
|
||
operationId: 'loadPackage',
|
||
parameters: [
|
||
{ name: 'id', in: 'path', required: true, schema: { type: 'string' } },
|
||
],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/LoadPackageRequest' },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: 'Loaded — full state snapshot (package now "loaded" on that plane)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
400: {
|
||
description: 'Invalid body (not an object, unknown field, or missing/non-string planeId)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown package id or plane id',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description:
|
||
'Package not "open", plane not grounded, plane not at the origin, or no free slot on the plane',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/packages/{id}/abandon': {
|
||
post: {
|
||
summary: 'Abandon a loaded package',
|
||
description:
|
||
'Drop a LOADED (or already EXPIRED) package while its plane is GROUNDED (v0.12.0). Penalty = fraction of the payout scaling with time pressure — 15% at creation up to 60% at the deadline; expired packages cost nothing and cash floors at €0. The slot is freed immediately. No body.',
|
||
operationId: 'abandonPackage',
|
||
parameters: [
|
||
{ name: 'id', in: 'path', required: true, schema: { type: 'string' } },
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: 'Abandoned — full state snapshot (package "abandoned", cash reduced by the penalty)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown package id',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description: 'Package not "loaded"/"expired", or its plane is in flight',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/packages/{id}/ignore': {
|
||
post: {
|
||
summary: 'Ignore an open job',
|
||
description:
|
||
'Dismiss an OPEN job so it leaves the market permanently (v0.29.0, ISS-047 part B). No penalty and nothing else changes — the top-up logic spawns a replacement on the next tick. No body.',
|
||
operationId: 'ignorePackage',
|
||
parameters: [
|
||
{ name: 'id', in: 'path', required: true, schema: { type: 'string' } },
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: 'Ignored — full state snapshot (package "ignored", gone from the open list)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown package id',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description: 'Package not "open"',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/planes/{planeId}/fly': {
|
||
post: {
|
||
summary: 'Fly a plane to an airport',
|
||
description:
|
||
'Manual flight (v0.12.0): send a GROUNDED ("available") plane to any known airport — its loaded packages\' destinations or pure repositioning. The plane becomes "flying" with departureSimTime = current sim clock and destIcao set; the engine flies it analytically, delivers bound cargo on arrival, and grounds it there. ISS-082 (v0.42.0): dispatch auto-refuels first — the tank is topped up to min(maxFuel, max(fuel, 1.5 × required)) at fuelPricePerL per liter minus a reputation-scaled discount, deducted from cash; no separate refuel endpoint exists anymore.',
|
||
operationId: 'flyPlane',
|
||
parameters: [
|
||
{ name: 'planeId', in: 'path', required: true, schema: { type: 'string' } },
|
||
],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/FlyPlaneRequest' },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: 'Flying — full state snapshot (plane "flying", departure + destination set)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
400: {
|
||
description:
|
||
'Invalid body (not an object, unknown field, or missing/non-string destIcao) or the ICAO is not in the airport dataset',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown plane id',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description:
|
||
'Plane not grounded, plane broken (health ≤ 0 — repair it first, v0.45.0), or the leg does not fit even a FULL tank (v0.16.0 weight-proportional burn — the fuel required to cover the great-circle distance arriving empty; dispatch auto-refuels up to min(maxFuel, max(fuel, 1.5 × required)) first, ISS-082), or cash is insufficient for that top-up',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/planes/{planeId}/queue': {
|
||
post: {
|
||
summary: 'Queue a flight leg for a plane',
|
||
description:
|
||
'Append a NOT-yet-started leg to the plane\'s flight queue (v0.25.0, ISS-043). Works whether the plane is grounded or flying — you can pre-build a route; the active leg still starts via /fly and nothing auto-starts here. On landing, the engine auto-chains the first queued leg when fuel covers it and the plane is not broken by the landing\'s wear (v0.45.0) — with a short tank it auto-refuels to 150% of the requirement (capped at tank capacity) when cash allows (ISS-082); only an unaffordable top-up (or a broken plane) grounds the plane with the queue kept intact for a later dispatch.',
|
||
operationId: 'queueLeg',
|
||
parameters: [
|
||
{ name: 'planeId', in: 'path', required: true, schema: { type: 'string' } },
|
||
],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/QueueLegRequest' },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: 'Queued — full state snapshot (plane.queue now includes the new leg)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
400: {
|
||
description:
|
||
'Invalid body (not an object, unknown field, or missing/non-string destIcao) or the ICAO is not in the airport dataset',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown plane id',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description:
|
||
'The destination is already in the flight queue, or (while flying) it equals the active leg\'s destination (v0.34.0, ISS-065)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/planes/{planeId}/queue/cancel': {
|
||
post: {
|
||
summary: 'Cancel a queued flight leg',
|
||
description:
|
||
'Remove a NOT-yet-started leg from the plane\'s queue by index (v0.25.0, ISS-043). Only queued legs are cancellable — the active in-flight leg (destIcao) is never affected.',
|
||
operationId: 'cancelQueuedLeg',
|
||
parameters: [
|
||
{ name: 'planeId', in: 'path', required: true, schema: { type: 'string' } },
|
||
],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/CancelQueuedLegRequest' },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: 'Cancelled — full state snapshot (plane.queue without the removed leg)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
400: {
|
||
description:
|
||
'Invalid body (not an object, unknown field, or non-integer index) or the index is outside [0, queue.length)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown plane id',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/market/buy-plane': {
|
||
post: {
|
||
summary: 'Buy a plane from the market',
|
||
description:
|
||
'Buy an aircraft at its catalog price minus a reputation-scaled discount (v0.38.0, ISS-072 — higher reputation buys cheaper; no discount at the starting reputation). The new plane arrives "available" with a full tank at the home base (EDDF). Only models in the in-code catalog are known.',
|
||
operationId: 'buyPlane',
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/BuyPlaneRequest' },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: 'Purchased — full state snapshot (new plane, cash reduced)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
400: {
|
||
description: 'Invalid body or unknown catalog model',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description: 'Insufficient funds',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/market/sell-plane': {
|
||
post: {
|
||
summary: 'Sell a plane on the market',
|
||
description:
|
||
'Sell a grounded plane for 50% of its catalog price (v0.8.0). The row is deleted and the payout added to cash. A flying plane cannot be sold, nor can the last remaining plane.',
|
||
operationId: 'sellPlane',
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/SellPlaneRequest' },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: 'Sold — full state snapshot (plane gone, cash increased by the payout)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
400: {
|
||
description: 'Invalid body (not an object, unknown field, or missing/non-string planeId)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown plane id',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description: 'Plane is flying, or it is the last remaining plane',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/planes/{planeId}/repair': {
|
||
post: {
|
||
summary: 'Repair a grounded plane',
|
||
description:
|
||
'Ground a worn-out plane in "repairing" status for the flat repair cost (v0.39.0, ISS-069): its health climbs back to 100 over 3 sim days — the engine\'s tick applies the curve each tick and flips it back to "available" exactly when full health is reached (v0.45.0, ISS-089). While repairing it cannot load, fly or be sold; a broken plane (health ≤ 0) cannot fly at all until repaired. A full-health repair is a free no-op (cost 0). No body.',
|
||
operationId: 'repairPlane',
|
||
parameters: [
|
||
{ name: 'planeId', in: 'path', required: true, schema: { type: 'string' } },
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: 'Repair started (or free no-op) — full state snapshot (status "repairing" with the gate set, health unchanged until the engine\'s tick heals it back to 100 over the duration, cash reduced by the flat cost)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown plane id',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description: 'Plane is not grounded (flying or already repairing), or cash is insufficient for the repair cost',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
components: {
|
||
schemas: {
|
||
AppVersion: {
|
||
type: 'object',
|
||
properties: {
|
||
version: { type: 'string', description: 'Semver of the running build' },
|
||
},
|
||
required: ['version'],
|
||
},
|
||
AirportFeatureCollection: {
|
||
type: 'object',
|
||
properties: {
|
||
type: { const: 'FeatureCollection' },
|
||
features: {
|
||
type: 'array',
|
||
items: { $ref: '#/components/schemas/AirportFeature' },
|
||
},
|
||
},
|
||
required: ['type', 'features'],
|
||
},
|
||
AirportFeature: {
|
||
type: 'object',
|
||
properties: {
|
||
type: { const: 'Feature' },
|
||
geometry: { $ref: '#/components/schemas/PointGeometry' },
|
||
properties: { $ref: '#/components/schemas/AirportProperties' },
|
||
},
|
||
required: ['type', 'geometry', 'properties'],
|
||
},
|
||
PointGeometry: {
|
||
type: 'object',
|
||
properties: {
|
||
type: { const: 'Point' },
|
||
coordinates: {
|
||
type: 'array',
|
||
description: '[longitude, latitude]',
|
||
items: { type: 'number' },
|
||
minItems: 2,
|
||
maxItems: 2,
|
||
},
|
||
},
|
||
required: ['type', 'coordinates'],
|
||
},
|
||
AirportProperties: {
|
||
type: 'object',
|
||
properties: {
|
||
name: { type: 'string' },
|
||
icao: { type: 'string' },
|
||
lat: { type: 'number' },
|
||
lon: { type: 'number' },
|
||
},
|
||
required: ['name', 'icao', 'lat', 'lon'],
|
||
},
|
||
StateSnapshot: {
|
||
type: 'object',
|
||
properties: {
|
||
account: { $ref: '#/components/schemas/AccountState' },
|
||
simState: { $ref: '#/components/schemas/SimState' },
|
||
planes: {
|
||
type: 'array',
|
||
items: { $ref: '#/components/schemas/PlaneState' },
|
||
},
|
||
packages: {
|
||
type: 'array',
|
||
items: { $ref: '#/components/schemas/PackageState' },
|
||
},
|
||
catalog: {
|
||
type: 'array',
|
||
description: 'Aircraft catalog (v0.10.0) — static per build, one entry per plane type.',
|
||
items: { $ref: '#/components/schemas/CatalogEntry' },
|
||
},
|
||
fuelPricePerL: { type: 'number', description: 'Jet-A1 price in €/liter; the automatic dispatch top-up costs liters × this, reduced by a reputation-scaled discount at refuel time (v0.38.0) — ISS-082 (v0.42.0): it now applies to the auto-refuel on /fly and the engine\'s chained-leg top-ups.' },
|
||
sellFraction: { type: 'number', description: 'Fraction of the catalog price paid out when selling a plane.' },
|
||
},
|
||
required: ['account', 'simState', 'planes', 'packages', 'catalog', 'fuelPricePerL', 'sellFraction'],
|
||
},
|
||
CatalogEntry: {
|
||
type: 'object',
|
||
properties: {
|
||
model: { type: 'string', description: 'Catalog model id (e.g. "atr72") — used by buy/fly.' },
|
||
label: { type: 'string', description: 'Display name (e.g. "ATR 72-600").' },
|
||
price: { type: 'number', description: 'Market price in € — the full catalog list price. Buying costs this minus a reputation-scaled discount at purchase time (v0.38.0); selling pays out sellFraction × this.' },
|
||
maxFuel: { type: 'number', description: 'Fuel capacity, liters.' },
|
||
burnPerKm: { type: 'number', description: 'Cruise fuel burn at a full tank, liters/km (v0.16.0 weighted model calibrates to this). Actual per-leg burn varies ±20% with wind — departure weather vs route heading (v0.36.0).' },
|
||
emptyWeightKg: { type: 'number', description: 'Operating empty weight, kg — with burnPerKm and maxFuel it drives the v0.16.0 weight-proportional burn model.' },
|
||
speedKmh: { type: 'number', description: 'Cruise speed, km/h.' },
|
||
maxPayloadKg: { type: 'number', description: 'Max payload in kg — total package weight that can be loaded on one plane (v0.27.0, replaces the v0.12.0 slot count).' },
|
||
},
|
||
required: ['model', 'label', 'price', 'maxFuel', 'burnPerKm', 'emptyWeightKg', 'speedKmh', 'maxPayloadKg'],
|
||
},
|
||
AccountState: {
|
||
type: 'object',
|
||
properties: {
|
||
cash: { type: 'number' },
|
||
reputation: { type: 'number', description: 'Reputation — grows with delivered packages; drives the purchase/fuel discounts and job reward quality (v0.38.0, ISS-072).' },
|
||
},
|
||
required: ['cash', 'reputation'],
|
||
},
|
||
SimState: {
|
||
type: 'object',
|
||
properties: {
|
||
clockMs: { type: 'integer', description: 'Sim-clock epoch ms (game time). The world runs at a fixed rate with no player-facing controls (v0.52.0, ISS-117).' },
|
||
},
|
||
required: ['clockMs'],
|
||
},
|
||
LoadPackageRequest: {
|
||
type: 'object',
|
||
properties: {
|
||
planeId: { type: 'string', description: 'A GROUNDED ("available") plane at the package\'s origin with enough payload room for it (v0.27.0).' },
|
||
},
|
||
required: ['planeId'],
|
||
},
|
||
FlyPlaneRequest: {
|
||
type: 'object',
|
||
properties: {
|
||
destIcao: { type: 'string', description: 'Any airport ICAO from the dataset — a loaded package\'s destination or repositioning.' },
|
||
},
|
||
required: ['destIcao'],
|
||
},
|
||
QueueLegRequest: {
|
||
type: 'object',
|
||
properties: {
|
||
destIcao: { type: 'string', description: 'Any airport ICAO from the dataset — appended to the queue as a not-yet-started leg (v0.25.0).' },
|
||
},
|
||
required: ['destIcao'],
|
||
},
|
||
CancelQueuedLegRequest: {
|
||
type: 'object',
|
||
properties: {
|
||
index: { type: 'integer', description: 'Zero-based position of the queued (not-yet-started) leg to remove; must be within [0, queue.length) (v0.25.0).' },
|
||
},
|
||
required: ['index'],
|
||
},
|
||
BuyPlaneRequest: {
|
||
type: 'object',
|
||
properties: {
|
||
model: { type: 'string', description: 'Catalog model id (e.g. "atr72").' },
|
||
},
|
||
required: ['model'],
|
||
},
|
||
SellPlaneRequest: {
|
||
type: 'object',
|
||
properties: {
|
||
planeId: { type: 'string', description: 'A grounded ("available") plane that is not the last remaining one.' },
|
||
},
|
||
required: ['planeId'],
|
||
},
|
||
Error: {
|
||
type: 'object',
|
||
properties: {
|
||
error: { type: 'string' },
|
||
},
|
||
required: ['error'],
|
||
},
|
||
PlaneState: {
|
||
type: 'object',
|
||
properties: {
|
||
id: { type: 'string' },
|
||
model: { type: 'string' },
|
||
status: { enum: ['available', 'flying', 'repairing'] }, // v0.39.0 (ISS-069) — "repairing" = grounded in the shop until repairUntilSimTime passes
|
||
fuel: { type: 'number' },
|
||
maxFuel: { type: 'number' },
|
||
locationIcao: { type: 'string' },
|
||
locationName: { type: 'string' },
|
||
destIcao: {
|
||
type: ['string', 'null'],
|
||
description: 'Current flight destination (set by fly, cleared on arrival); null when grounded.',
|
||
},
|
||
destName: {
|
||
type: ['string', 'null'],
|
||
description: 'Display name of destIcao; null when grounded.',
|
||
},
|
||
departureSimTime: {
|
||
type: ['integer', 'null'],
|
||
description: 'Sim-clock epoch ms of the current flight (set by fly); null when grounded.',
|
||
},
|
||
lat: { type: 'number' },
|
||
lon: { type: 'number' },
|
||
queue: {
|
||
type: 'array',
|
||
items: { type: 'string' },
|
||
description:
|
||
'Upcoming flight legs NOT yet started (v0.25.0) — ordered ICAOs; the active leg is destIcao. The engine auto-chains queue[0] on landing when fuel covers it.',
|
||
},
|
||
health: {
|
||
type: 'number',
|
||
description:
|
||
'Structural health in [0, 100] (v0.39.0) — wears down by a fixed amount per km of landed legs; at 0 the plane is broken and cannot fly until repaired (v0.45.0). While "repairing" this doubles as repair progress: it climbs back to 100 over the repair duration.',
|
||
},
|
||
repairUntilSimTime: {
|
||
type: ['integer', 'null'],
|
||
description:
|
||
'Sim-clock epoch ms when an active repair finishes (v0.39.0) — set with status "repairing", cleared by the engine\'s flip-back; null when not in repair.',
|
||
},
|
||
},
|
||
required: [
|
||
'id',
|
||
'model',
|
||
'status',
|
||
'fuel',
|
||
'maxFuel',
|
||
'locationIcao',
|
||
'locationName',
|
||
'destIcao',
|
||
'destName',
|
||
'departureSimTime',
|
||
'lat',
|
||
'lon',
|
||
'queue',
|
||
'health', // v0.39.0 (ISS-069) — structural health in [0, 100]
|
||
'repairUntilSimTime', // v0.39.0 (ISS-069) — active repair gate; null when not repairing
|
||
],
|
||
},
|
||
PackageState: {
|
||
type: 'object',
|
||
properties: {
|
||
id: { type: 'string' },
|
||
originIcao: { type: 'string' },
|
||
originName: { type: 'string' },
|
||
destIcao: { type: 'string' },
|
||
destName: { type: 'string' },
|
||
reward: { type: 'number' },
|
||
cargoType: { type: 'string' },
|
||
cargoCount: { type: 'integer' },
|
||
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.' },
|
||
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)' },
|
||
// 'expired' = an open OR loaded package whose deadline passed on a tick;
|
||
// loaded expired packages keep their slot, deliver no reward, abandon free.
|
||
// 'ignored' (v0.29.0, ISS-047 part B) = a dismissed open job — terminal, leaves the market.
|
||
status: { enum: ['open', 'loaded', 'delivered', 'expired', 'abandoned', 'ignored'] },
|
||
planeId: { type: ['string', 'null'] },
|
||
createdSimTime: { type: 'integer' },
|
||
expiresRealMs: { type: ['integer', 'null'], description: 'Wall-clock epoch ms when an OPEN job expires (v0.65.0) — createdRealMs + lifetime; null for every non-open status.' },
|
||
},
|
||
required: [
|
||
'id',
|
||
'originIcao',
|
||
'originName',
|
||
'destIcao',
|
||
'destName',
|
||
'reward',
|
||
'cargoType',
|
||
'cargoCount',
|
||
'weightKg', // v0.27.0 (ISS-045) — package weight in kg
|
||
'distanceKm', // v0.48.0 (ISS-095) — great-circle leg distance in km
|
||
'deadlineSimTime',
|
||
'status',
|
||
'planeId',
|
||
'createdSimTime',
|
||
'expiresRealMs', // v0.65.0 — wall-clock expiry of open jobs; null otherwise
|
||
],
|
||
},
|
||
},
|
||
},
|
||
};
|