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:
+46
-6
@@ -27,12 +27,12 @@ CREATE TABLE IF NOT EXISTS planes (
|
||||
fuel REAL NOT NULL,
|
||||
max_fuel REAL NOT NULL,
|
||||
location_icao TEXT NOT NULL,
|
||||
contract_id TEXT,
|
||||
dest_icao TEXT,
|
||||
lat REAL NOT NULL,
|
||||
lon REAL NOT NULL,
|
||||
departure_sim_time INTEGER
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS contracts (
|
||||
CREATE TABLE IF NOT EXISTS packages (
|
||||
id TEXT PRIMARY KEY NOT NULL,
|
||||
origin_icao TEXT NOT NULL,
|
||||
dest_icao TEXT NOT NULL,
|
||||
@@ -59,7 +59,7 @@ CREATE TABLE IF NOT EXISTS sim_state (
|
||||
// v0.7.0: DBs created by earlier versions lack the new columns; CREATE TABLE IF
|
||||
// NOT EXISTS won't add them, so ALTER (guarded by PRAGMA table_info). Fresh DBs
|
||||
// already have everything from DDL above and this is a no-op for them.
|
||||
function migrate(raw: RawSqlJsDatabase): void {
|
||||
function migrateColumns(raw: RawSqlJsDatabase): void {
|
||||
const cols = (table: string): Set<string> =>
|
||||
new Set(raw.exec(`PRAGMA table_info(${table})`)[0].values.map((r) => String(r[1])));
|
||||
if (!cols('sim_state').has('last_real_tick_ms')) {
|
||||
@@ -68,6 +68,37 @@ function migrate(raw: RawSqlJsDatabase): void {
|
||||
if (!cols('planes').has('departure_sim_time')) {
|
||||
raw.exec('ALTER TABLE planes ADD COLUMN departure_sim_time INTEGER');
|
||||
}
|
||||
// v0.12.0: the flight destination column (set by fly, cleared on arrival).
|
||||
if (!cols('planes').has('dest_icao')) {
|
||||
raw.exec('ALTER TABLE planes ADD COLUMN dest_icao TEXT');
|
||||
}
|
||||
}
|
||||
|
||||
// v0.12.0 one-time migration: contracts world → packages world. Detected by the
|
||||
// presence of the old `contracts` table (fresh and already-migrated DBs never
|
||||
// have it, so this runs exactly once per file). Preserves account/sim_state rows
|
||||
// and every plane row; planes are reset to GROUNDED at their locationIcao with
|
||||
// departure_sim_time/dest_icao cleared — an in-flight plane from the old state
|
||||
// simply lands back where it took off. Old contract data is wiped (the packages
|
||||
// table starts empty and the engine's spawn rules reseed the market on the next
|
||||
// tick). The orphaned pre-v0.12.0 `contract_id` column is dropped too (SQLite
|
||||
// ≥3.35 DROP COLUMN; guarded so a failure can never block startup).
|
||||
function migrateContractsToPackages(raw: RawSqlJsDatabase): void {
|
||||
const cols = new Set(
|
||||
raw.exec('PRAGMA table_info(planes)')[0].values.map((r) => String(r[1])),
|
||||
);
|
||||
raw.exec(
|
||||
`UPDATE planes SET status = 'available', departure_sim_time = NULL, dest_icao = NULL` +
|
||||
(cols.has('contract_id') ? ', contract_id = NULL' : ''),
|
||||
);
|
||||
if (cols.has('contract_id')) {
|
||||
try {
|
||||
raw.exec('ALTER TABLE planes DROP COLUMN contract_id');
|
||||
} catch {
|
||||
// Leaving the orphaned column is harmless — nothing references it anymore.
|
||||
}
|
||||
}
|
||||
raw.exec('DROP TABLE contracts');
|
||||
}
|
||||
|
||||
// FLIGHT_DB_PATH override exists so unit tests can point at a temp file instead
|
||||
@@ -85,8 +116,17 @@ async function open(file: string): Promise<GameDb> {
|
||||
if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
|
||||
}
|
||||
const raw = bytes ? new SQL.Database(new Uint8Array(bytes)) : new SQL.Database();
|
||||
raw.exec(DDL);
|
||||
migrate(raw);
|
||||
raw.exec(DDL); // no-op for tables that already exist in an older file
|
||||
migrateColumns(raw);
|
||||
|
||||
// v0.12.0: run BEFORE the seed check — a pre-v0.12.0 file has account rows, so
|
||||
// without this it would keep the dead contracts table forever.
|
||||
const tables = new Set(
|
||||
raw.exec("SELECT name FROM sqlite_master WHERE type='table'")[0].values.map((r) => String(r[0])),
|
||||
);
|
||||
if (tables.has('contracts')) {
|
||||
migrateContractsToPackages(raw);
|
||||
}
|
||||
|
||||
const instance: GameDb = { db: drizzle(raw, { schema }), raw, file };
|
||||
|
||||
@@ -120,7 +160,7 @@ async function seedEmpty(instance: GameDb): Promise<void> {
|
||||
const data = buildSeed({ airports, rng, nowMs: Date.now() });
|
||||
|
||||
await instance.db.insert(schema.planes).values(data.plane);
|
||||
await instance.db.insert(schema.contracts).values(data.contracts);
|
||||
await instance.db.insert(schema.packages).values(data.packages);
|
||||
await instance.db.insert(schema.account).values(data.account);
|
||||
await instance.db.insert(schema.simState).values(data.simState);
|
||||
await persist(instance);
|
||||
|
||||
Reference in New Issue
Block a user