679 lines
28 KiB
TypeScript
679 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 — EU airport data for the map, the live game-state snapshot, player actions (load / fly / abandon / buy / sell / refuel), 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 EU airports',
|
||
description:
|
||
'All fixed-wing EU airports (large, medium and seaplane bases) as a GeoJSON FeatureCollection.',
|
||
operationId: 'listAirports',
|
||
responses: {
|
||
200: {
|
||
description: 'GeoJSON FeatureCollection of EU 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/refuel previews locally. Tick-on-read: each call first advances the sim clock by (real time since last read) × timeScale (clamped per tick), 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/sim': {
|
||
post: {
|
||
summary: 'Set sim time controls',
|
||
description:
|
||
'Advance the sim clock (tick-on-read) and set the time scale / pause state. The clock is advanced first, so the returned simState already reflects current game time; package expiry, arrival delivery and market spawn run inside that tick.',
|
||
operationId: 'setSimControls',
|
||
requestBody: {
|
||
required: false,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/SimControlsRequest' },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: 'Updated sim state',
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/SimState' },
|
||
},
|
||
},
|
||
},
|
||
400: {
|
||
description: 'Invalid body (not an object, unknown field, or wrong types)',
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/Error' },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/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.',
|
||
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, or insufficient fuel for the leg (v0.16.0 weight-proportional burn — the fuel required to cover the great-circle distance arriving empty)',
|
||
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 (otherwise the plane grounds with the queue kept intact for refuel + re-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 full catalog price (v0.8.0). 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}/refuel': {
|
||
post: {
|
||
summary: 'Refuel a grounded plane',
|
||
description:
|
||
'Refuel a grounded plane at the fuel price per added liter, deducted from cash (v0.8.0). The body is optional (v0.16.0): no body or mode "full" fills to maxFuel; mode "forFlight" with packageId buys 150% of the weather-adjusted fuel required for that loaded package\'s destination leg, capped at tank capacity — liters = min(maxFuel, max(current fuel, 1.5 × required)) − current fuel, where required is the weight-proportional burn model over the great-circle distance with a ±20% wind adjustment (v0.36.0).',
|
||
operationId: 'refuelPlane',
|
||
parameters: [
|
||
{ name: 'planeId', in: 'path', required: true, schema: { type: 'string' } },
|
||
],
|
||
requestBody: {
|
||
required: false,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/RefuelRequest' },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: 'Refueled — full state snapshot (fuel raised by exactly the added liters, cash reduced by the cost)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
400: {
|
||
description: 'Invalid body (not an object, unknown field, unknown mode, or forFlight without packageId)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown plane id (or unknown package id in forFlight mode)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description:
|
||
'Plane is flying, the package is not loaded on this plane (or is expired), the leg does not fit a full tank, or cash is insufficient for the refuel',
|
||
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; refuel cost = (maxFuel − fuel) × this.' },
|
||
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 € (buy cost; sell 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' },
|
||
},
|
||
required: ['cash', 'reputation'],
|
||
},
|
||
SimState: {
|
||
type: 'object',
|
||
properties: {
|
||
clockMs: { type: 'integer', description: 'Sim-clock epoch ms (game time)' },
|
||
timeScale: { type: 'number', minimum: 1, maximum: 1000 },
|
||
paused: { type: 'boolean' },
|
||
},
|
||
required: ['clockMs', 'timeScale', 'paused'],
|
||
},
|
||
SimControlsRequest: {
|
||
type: 'object',
|
||
description: 'Partial update — omit a field to leave it unchanged.',
|
||
properties: {
|
||
timeScale: {
|
||
type: 'number',
|
||
minimum: 1,
|
||
maximum: 1000,
|
||
description: 'Game-time multiplier; values outside [1, 1000] are clamped. The UI offers the ladder 1/2/5/10/25/50/100/250/500/1000.',
|
||
},
|
||
paused: { type: 'boolean' },
|
||
},
|
||
},
|
||
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'],
|
||
},
|
||
RefuelRequest: {
|
||
type: 'object',
|
||
description:
|
||
'Optional refuel body (v0.16.0) — omit it entirely or send {} to fill the tank; both fields are optional.',
|
||
properties: {
|
||
mode: {
|
||
enum: ['full', 'forFlight'],
|
||
description:
|
||
'"full" (default) fills the tank to maxFuel; "forFlight" buys 150% of the weather-adjusted fuel required for one loaded package\'s destination leg, capped at tank capacity (v0.36.0).',
|
||
},
|
||
packageId: {
|
||
type: 'string',
|
||
description: 'Required with mode "forFlight": a package LOADED on this plane — its destIcao is the refuel target.',
|
||
},
|
||
},
|
||
},
|
||
Error: {
|
||
type: 'object',
|
||
properties: {
|
||
error: { type: 'string' },
|
||
},
|
||
required: ['error'],
|
||
},
|
||
PlaneState: {
|
||
type: 'object',
|
||
properties: {
|
||
id: { type: 'string' },
|
||
model: { type: 'string' },
|
||
status: { enum: ['available', 'flying'] },
|
||
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.',
|
||
},
|
||
},
|
||
required: [
|
||
'id',
|
||
'model',
|
||
'status',
|
||
'fuel',
|
||
'maxFuel',
|
||
'locationIcao',
|
||
'locationName',
|
||
'destIcao',
|
||
'destName',
|
||
'departureSimTime',
|
||
'lat',
|
||
'lon',
|
||
'queue',
|
||
],
|
||
},
|
||
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.' },
|
||
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' },
|
||
},
|
||
required: [
|
||
'id',
|
||
'originIcao',
|
||
'originName',
|
||
'destIcao',
|
||
'destName',
|
||
'reward',
|
||
'cargoType',
|
||
'cargoCount',
|
||
'weightKg', // v0.27.0 (ISS-045) — package weight in kg
|
||
'deadlineSimTime',
|
||
'status',
|
||
'planeId',
|
||
'createdSimTime',
|
||
],
|
||
},
|
||
},
|
||
},
|
||
};
|