feat: v0.12.0 — contracts → packages: load/fly/abandon gameplay (ATR 72-600 capacity 6, manual flight to any airport, time-scaled abandon penalty 15–60%), one-time DB migration preserving wallet/clock; new endpoints /api/packages/:id/load|abandon + /api/planes/:planeId/fly
This commit is contained in:
+89
-34
@@ -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, 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.',
|
||||
'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,
|
||||
},
|
||||
@@ -37,7 +37,7 @@ export const openApiSpec = {
|
||||
get: {
|
||||
summary: 'Game state snapshot',
|
||||
description:
|
||||
'Full current game state for client polling — account, sim clock, planes and contracts, 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/finalization/spawn are always applied before the snapshot is returned.',
|
||||
'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: {
|
||||
@@ -55,7 +55,7 @@ export const openApiSpec = {
|
||||
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.',
|
||||
'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,
|
||||
@@ -102,37 +102,75 @@ export const openApiSpec = {
|
||||
},
|
||||
},
|
||||
},
|
||||
'/api/contracts/{id}/accept': {
|
||||
'/api/packages/{id}/load': {
|
||||
post: {
|
||||
summary: 'Accept a contract',
|
||||
summary: 'Load a package onto a plane',
|
||||
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',
|
||||
'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: 'Accepted — full state snapshot (contract now "accepted")',
|
||||
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 contract id',
|
||||
description: 'Unknown package id or plane id',
|
||||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||||
},
|
||||
409: {
|
||||
description: 'Contract is not "open" (accepted / completed / expired)',
|
||||
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/planes/{planeId}/dispatch': {
|
||||
'/api/packages/{id}/abandon': {
|
||||
post: {
|
||||
summary: 'Dispatch a plane on an accepted contract',
|
||||
summary: 'Abandon a loaded package',
|
||||
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',
|
||||
'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' } },
|
||||
],
|
||||
@@ -140,26 +178,26 @@ export const openApiSpec = {
|
||||
required: true,
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: { $ref: '#/components/schemas/DispatchRequest' },
|
||||
schema: { $ref: '#/components/schemas/FlyPlaneRequest' },
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Dispatched — full state snapshot (plane "flying", departure set)',
|
||||
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 contractId)',
|
||||
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 or contract id',
|
||||
description: 'Unknown plane 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',
|
||||
description: 'Plane not grounded, or insufficient fuel for the leg (burnPerKm × great-circle km)',
|
||||
content: { 'application/json': { schema: { $ref: '#/components/schemas/Error' } } },
|
||||
},
|
||||
},
|
||||
@@ -317,9 +355,9 @@ export const openApiSpec = {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/PlaneState' },
|
||||
},
|
||||
contracts: {
|
||||
packages: {
|
||||
type: 'array',
|
||||
items: { $ref: '#/components/schemas/ContractState' },
|
||||
items: { $ref: '#/components/schemas/PackageState' },
|
||||
},
|
||||
catalog: {
|
||||
type: 'array',
|
||||
@@ -329,19 +367,20 @@ export const openApiSpec = {
|
||||
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', 'contracts', 'catalog', 'fuelPricePerL', 'sellFraction'],
|
||||
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/dispatch.' },
|
||||
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'],
|
||||
required: ['model', 'label', 'price', 'maxFuel', 'burnPerKm', 'speedKmh', 'capacity'],
|
||||
},
|
||||
AccountState: {
|
||||
type: 'object',
|
||||
@@ -373,12 +412,19 @@ export const openApiSpec = {
|
||||
paused: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
DispatchRequest: {
|
||||
LoadPackageRequest: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
contractId: { type: 'string', description: 'An ACCEPTED contract whose origin the plane is at.' },
|
||||
planeId: { type: 'string', description: 'A GROUNDED ("available") plane at the package\'s origin with a free slot.' },
|
||||
},
|
||||
required: ['contractId'],
|
||||
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',
|
||||
@@ -411,10 +457,17 @@ export const openApiSpec = {
|
||||
maxFuel: { type: 'number' },
|
||||
locationIcao: { type: 'string' },
|
||||
locationName: { type: 'string' },
|
||||
contractId: { type: ['string', 'null'] },
|
||||
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 dispatch); null when grounded.',
|
||||
description: 'Sim-clock epoch ms of the current flight (set by fly); null when grounded.',
|
||||
},
|
||||
lat: { type: 'number' },
|
||||
lon: { type: 'number' },
|
||||
@@ -427,13 +480,14 @@ export const openApiSpec = {
|
||||
'maxFuel',
|
||||
'locationIcao',
|
||||
'locationName',
|
||||
'contractId',
|
||||
'destIcao',
|
||||
'destName',
|
||||
'departureSimTime',
|
||||
'lat',
|
||||
'lon',
|
||||
],
|
||||
},
|
||||
ContractState: {
|
||||
PackageState: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
id: { type: 'string' },
|
||||
@@ -445,8 +499,9 @@ export const openApiSpec = {
|
||||
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'] },
|
||||
// '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' },
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user