452 lines
16 KiB
TypeScript
452 lines
16 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 (accept / dispatch / 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 contracts. Tick-on-read: each call first advances the sim clock by (real time since last read) × timeScale (clamped per tick), so expiry/finalization/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; contract expiry, arrival finalization 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/contracts/{id}/accept': {
|
||
post: {
|
||
summary: 'Accept a contract',
|
||
description:
|
||
'Claim an open contract: status flips open → accepted (v0.8.0). The sim clock is ticked first, so the returned snapshot reflects current game time. No body.',
|
||
operationId: 'acceptContract',
|
||
parameters: [
|
||
{ name: 'id', in: 'path', required: true, schema: { type: 'string' } },
|
||
],
|
||
responses: {
|
||
200: {
|
||
description: 'Accepted — full state snapshot (contract now "accepted")',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown contract id',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description: 'Contract is not "open" (accepted / completed / expired)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
},
|
||
},
|
||
},
|
||
'/api/planes/{planeId}/dispatch': {
|
||
post: {
|
||
summary: 'Dispatch a plane on an accepted contract',
|
||
description:
|
||
'Bind an ACCEPTED contract to a grounded plane at its origin (v0.8.0). The plane becomes "flying" with departureSimTime = current sim clock; the engine then flies it analytically and pays out reward + reputation on arrival. Validation: contract must be "accepted", plane "available" and located at the contract origin, with enough fuel for the leg (burnPerKm × great-circle km).',
|
||
operationId: 'dispatchPlane',
|
||
parameters: [
|
||
{ name: 'planeId', in: 'path', required: true, schema: { type: 'string' } },
|
||
],
|
||
requestBody: {
|
||
required: true,
|
||
content: {
|
||
'application/json': {
|
||
schema: { $ref: '#/components/schemas/DispatchRequest' },
|
||
},
|
||
},
|
||
},
|
||
responses: {
|
||
200: {
|
||
description: 'Dispatched — full state snapshot (plane "flying", departure set)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/StateSnapshot' } } },
|
||
},
|
||
400: {
|
||
description: 'Invalid body (not an object, unknown field, or missing/non-string contractId)',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
404: {
|
||
description: 'Unknown plane id or contract id',
|
||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||
},
|
||
409: {
|
||
description:
|
||
'Contract not "accepted", plane not "available", plane not at the origin, or insufficient fuel for the leg',
|
||
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' },
|
||
},
|
||
contracts: {
|
||
type: 'array',
|
||
items: { $ref: '#/components/schemas/ContractState' },
|
||
},
|
||
},
|
||
required: ['account', 'simState', 'planes', 'contracts'],
|
||
},
|
||
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: 60 },
|
||
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: 60,
|
||
description: 'Game-time multiplier; values outside [1, 60] are clamped.',
|
||
},
|
||
paused: { type: 'boolean' },
|
||
},
|
||
},
|
||
DispatchRequest: {
|
||
type: 'object',
|
||
properties: {
|
||
contractId: { type: 'string', description: 'An ACCEPTED contract whose origin the plane is at.' },
|
||
},
|
||
required: ['contractId'],
|
||
},
|
||
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' },
|
||
contractId: { type: ['string', 'null'] },
|
||
departureSimTime: {
|
||
type: ['integer', 'null'],
|
||
description: 'Sim-clock epoch ms of the current flight (set by dispatch); null when grounded.',
|
||
},
|
||
lat: { type: 'number' },
|
||
lon: { type: 'number' },
|
||
},
|
||
required: [
|
||
'id',
|
||
'model',
|
||
'status',
|
||
'fuel',
|
||
'maxFuel',
|
||
'locationIcao',
|
||
'locationName',
|
||
'contractId',
|
||
'departureSimTime',
|
||
'lat',
|
||
'lon',
|
||
],
|
||
},
|
||
ContractState: {
|
||
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 contract whose deadline passed; accepted/in-flight contracts never expire.
|
||
status: { enum: ['open', 'accepted', 'completed', 'expired'] },
|
||
planeId: { type: ['string', 'null'] },
|
||
createdSimTime: { type: 'integer' },
|
||
},
|
||
required: [
|
||
'id',
|
||
'originIcao',
|
||
'originName',
|
||
'destIcao',
|
||
'destName',
|
||
'reward',
|
||
'cargoType',
|
||
'cargoCount',
|
||
'deadlineSimTime',
|
||
'status',
|
||
'planeId',
|
||
'createdSimTime',
|
||
],
|
||
},
|
||
},
|
||
},
|
||
};
|