Files
Flight-Ops/lib/openapi.ts
T

526 lines
20 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 a free slot — capacity per catalog model; expired packages still occupy theirs. 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/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 (burnPerKm × great-circle km)',
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:
'Fill a grounded plane to maxFuel (v0.8.0). Cost = (maxFuel − fuel) × fuel price per liter, deducted from cash; no body.',
operationId: 'refuelPlane',
parameters: [
{ name: 'planeId', in: 'path', required: true, schema: { type: 'string' } },
],
responses: {
200: {
description: 'Refueled — full state snapshot (fuel at max, cash reduced by the 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 flying, or cash is insufficient for the fill-up',
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, liters/km.' },
speedKmh: { type: 'number', description: 'Cruise speed, km/h.' },
capacity: { type: 'integer', description: 'Cargo slot capacity — packages that can be loaded on one plane (v0.12.0).' },
},
required: ['model', 'label', 'price', 'maxFuel', 'burnPerKm', 'speedKmh', 'capacity'],
},
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: 64 },
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: 64,
description: 'Game-time multiplier; values outside [1, 64] are clamped. The UI offers the power-of-two ladder 1/2/4/8/16/32/64.',
},
paused: { type: 'boolean' },
},
},
LoadPackageRequest: {
type: 'object',
properties: {
planeId: { type: 'string', description: 'A GROUNDED ("available") plane at the package\'s origin with a free slot.' },
},
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'],
},
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'] },
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' },
},
required: [
'id',
'model',
'status',
'fuel',
'maxFuel',
'locationIcao',
'locationName',
'destIcao',
'destName',
'departureSimTime',
'lat',
'lon',
],
},
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' },
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.
status: { enum: ['open', 'loaded', 'delivered', 'expired', 'abandoned'] },
planeId: { type: ['string', 'null'] },
createdSimTime: { type: 'integer' },
},
required: [
'id',
'originIcao',
'originName',
'destIcao',
'destName',
'reward',
'cargoType',
'cargoCount',
'deadlineSimTime',
'status',
'planeId',
'createdSimTime',
],
},
},
},
};