feat: v0.8.0 — player-actions API: accept/dispatch/buy/sell/refuel endpoints (tick-first, full-snapshot responses), spawn coverage rule, test create-contract hook

This commit is contained in:
2026-09-17 13:54:13 +00:00
parent 96835e935d
commit aa70eb5286
25 changed files with 1843 additions and 8 deletions
+179 -1
View File
@@ -8,7 +8,7 @@ export const openApiSpec = {
info: {
title: 'Flight Ops API',
description:
'JSON API for Flight Ops — EU airport data for the map, the live game-state snapshot, and app metadata.',
'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,
},
@@ -102,6 +102,158 @@ export const openApiSpec = {
},
},
},
'/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: {
@@ -202,6 +354,27 @@ export const openApiSpec = {
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: {
@@ -220,6 +393,10 @@ export const openApiSpec = {
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' },
},
@@ -232,6 +409,7 @@ export const openApiSpec = {
'locationIcao',
'locationName',
'contractId',
'departureSimTime',
'lat',
'lon',
],