v1.0.0: players table + migration from singleton account (ISS-105)
CI / build + unit + e2e (pull_request) Successful in 14m25s
CI / build + unit + e2e (pull_request) Successful in 14m25s
This commit is contained in:
+36
-35
@@ -5,6 +5,7 @@ import { eq } from 'drizzle-orm';
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest';
|
||||
import { openGameDb, type GameDb } from '@/lib/db/client';
|
||||
import * as schema from '@/lib/db/schema';
|
||||
import { getLocalPlayer, updateLocalPlayerEconomy } from '@/lib/db/localPlayer';
|
||||
import { loadAirportsGeoJson } from '@/lib/airports';
|
||||
import { haversineKm, initialBearingDeg } from '@/lib/geo';
|
||||
import { weatherFor } from '@/lib/weather';
|
||||
@@ -204,14 +205,14 @@ describe('flyPlane', () => {
|
||||
const inst = await freshDb('fly-broken.db'); // EDDF → LEMD
|
||||
const fuelBefore = PLANE_TYPES.atr72.maxFuel; // seeded full tank
|
||||
await inst.db.update(schema.planes).set({ health: 0 }).where(eq(schema.planes.id, 'plane-starter'));
|
||||
const cashBefore = (await inst.db.select().from(schema.account))[0].cash;
|
||||
const cashBefore = (await getLocalPlayer(inst)).cash;
|
||||
|
||||
await expectActionError(flyPlane(inst, 'plane-starter', 'LEMD'), 409, /broken \(health 0\)/);
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.status).toBe('available'); // still grounded…
|
||||
expect(plane.fuel).toBe(fuelBefore); // …no auto-refuel ran — the tank is untouched…
|
||||
expect((await inst.db.select().from(schema.account))[0].cash).toBe(cashBefore); // …and no cash left the account
|
||||
expect((await getLocalPlayer(inst)).cash).toBe(cashBefore); // …and no cash left the account
|
||||
});
|
||||
|
||||
it('still flies a worn-but-not-broken plane (health > 0) with plain burn', async () => {
|
||||
@@ -249,8 +250,8 @@ describe('flyPlane', () => {
|
||||
const startFuel = Math.round(required / 2); // partial tank below the requirement
|
||||
await inst.db.update(schema.planes).set({ fuel: startFuel }).where(eq(schema.planes.id, 'plane-starter'));
|
||||
// Elevated rep → non-zero discount rate; expectations computed with the SAME helpers flyPlane uses.
|
||||
await inst.db.update(schema.account).set({ reputation: 30 });
|
||||
const cashBefore = (await inst.db.select().from(schema.account))[0].cash;
|
||||
await updateLocalPlayerEconomy(inst, { reputation: 30 });
|
||||
const cashBefore = (await getLocalPlayer(inst)).cash;
|
||||
|
||||
await flyPlane(inst, 'plane-starter', 'LEMD');
|
||||
|
||||
@@ -263,22 +264,22 @@ describe('flyPlane', () => {
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.status).toBe('flying');
|
||||
expect(plane.fuel).toBeCloseTo(target, 6); // topped up to exactly the 150% target…
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
expect(account.cash).toBeCloseTo(cashBefore - expectedCost, 6); // …at exactly the rep-discounted cost
|
||||
const player = await getLocalPlayer(inst);
|
||||
expect(player.cash).toBeCloseTo(cashBefore - expectedCost, 6); // …at exactly the rep-discounted cost
|
||||
});
|
||||
|
||||
it('is a free no-op when the tank already covers the 150% target (cash unchanged)', async () => {
|
||||
const inst = await freshDb('fly-autorefuel-full.db'); // seeded starter is full at EDDF
|
||||
const required = await adjustedRequiredForHubToLemd();
|
||||
expect(required * 1.5).toBeLessThan(PLANE_TYPES.atr72.maxFuel); // sanity: the target stays below a full tank
|
||||
const cashBefore = (await inst.db.select().from(schema.account))[0].cash;
|
||||
const cashBefore = (await getLocalPlayer(inst)).cash;
|
||||
|
||||
await flyPlane(inst, 'plane-starter', 'LEMD');
|
||||
|
||||
const plane = (await inst.db.select().from(schema.planes))[0];
|
||||
expect(plane.status).toBe('flying');
|
||||
expect(plane.fuel).toBe(PLANE_TYPES.atr72.maxFuel); // untouched…
|
||||
expect((await inst.db.select().from(schema.account))[0].cash).toBe(cashBefore); // …and free
|
||||
expect((await getLocalPlayer(inst)).cash).toBe(cashBefore); // …and free
|
||||
});
|
||||
|
||||
it('rejects dispatch with 409 insufficient funds when cash cannot cover the top-up — nothing mutated', async () => {
|
||||
@@ -286,7 +287,7 @@ describe('flyPlane', () => {
|
||||
const required = await adjustedRequiredForHubToLemd();
|
||||
const startFuel = Math.round(required / 2);
|
||||
await inst.db.update(schema.planes).set({ fuel: startFuel }).where(eq(schema.planes.id, 'plane-starter'));
|
||||
await inst.db.update(schema.account).set({ cash: 0 });
|
||||
await updateLocalPlayerEconomy(inst, { cash: 0 });
|
||||
|
||||
await expectActionError(flyPlane(inst, 'plane-starter', 'LEMD'), 409, /insufficient funds/i);
|
||||
|
||||
@@ -451,7 +452,7 @@ describe('abandonPackage', () => {
|
||||
|
||||
it('charges exactly 15% of the payout right after creation (clock at createdSimTime)', async () => {
|
||||
const inst = await setupLoaded('ab-start.db');
|
||||
const cashBefore = (await inst.db.select().from(schema.account))[0].cash;
|
||||
const cashBefore = (await getLocalPlayer(inst)).cash;
|
||||
|
||||
const { penalty } = await abandonPackage(inst, 'p-ab');
|
||||
|
||||
@@ -459,8 +460,8 @@ describe('abandonPackage', () => {
|
||||
const row = (await inst.db.select().from(schema.packages).where(eq(schema.packages.id, 'p-ab')))[0];
|
||||
expect(row.status).toBe('abandoned');
|
||||
expect(row.planeId).toBeNull(); // slot freed
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
expect(account.cash).toBeCloseTo(cashBefore - penalty, 6);
|
||||
const player = await getLocalPlayer(inst);
|
||||
expect(player.cash).toBeCloseTo(cashBefore - penalty, 6);
|
||||
});
|
||||
|
||||
it('scales to exactly 37.5% at the midpoint of the window', async () => {
|
||||
@@ -487,24 +488,24 @@ describe('abandonPackage', () => {
|
||||
|
||||
it('is free for an already-expired package', async () => {
|
||||
const inst = await setupLoaded('ab-expired.db', 'expired');
|
||||
const cashBefore = (await inst.db.select().from(schema.account))[0].cash;
|
||||
const cashBefore = (await getLocalPlayer(inst)).cash;
|
||||
|
||||
const { penalty } = await abandonPackage(inst, 'p-ab');
|
||||
|
||||
expect(penalty).toBe(0);
|
||||
expect((await inst.db.select().from(schema.account))[0].cash).toBe(cashBefore);
|
||||
expect((await getLocalPlayer(inst)).cash).toBe(cashBefore);
|
||||
const row = (await inst.db.select().from(schema.packages).where(eq(schema.packages.id, 'p-ab')))[0];
|
||||
expect(row.status).toBe('abandoned');
|
||||
});
|
||||
|
||||
it('floors cash at €0 — no debt', async () => {
|
||||
const inst = await setupLoaded('ab-floor.db');
|
||||
await inst.db.update(schema.account).set({ cash: 10 }); // penalty is 150 > 10
|
||||
await updateLocalPlayerEconomy(inst, { cash: 10 }); // penalty is 150 > 10
|
||||
|
||||
const { penalty } = await abandonPackage(inst, 'p-ab');
|
||||
|
||||
expect(penalty).toBe(150);
|
||||
expect((await inst.db.select().from(schema.account))[0].cash).toBe(0);
|
||||
expect((await getLocalPlayer(inst)).cash).toBe(0);
|
||||
});
|
||||
|
||||
it('rejects abandoning while the plane is in flight with 409', async () => {
|
||||
@@ -532,13 +533,13 @@ describe('abandonPackage', () => {
|
||||
describe('buyPlane', () => {
|
||||
it('deducts the full price and delivers a full-tank plane at the home base', async () => {
|
||||
const inst = await freshDb('buy-ok.db');
|
||||
await inst.db.update(schema.account).set({ cash: 500_000 });
|
||||
await updateLocalPlayerEconomy(inst, { cash: 500_000 });
|
||||
|
||||
const before = (await inst.db.select().from(schema.planes)).length;
|
||||
const { planeId } = await buyPlane(inst, 'atr72');
|
||||
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
expect(account.cash).toBe(500_000 - PLANE_TYPES.atr72.price);
|
||||
const player = await getLocalPlayer(inst);
|
||||
expect(player.cash).toBe(500_000 - PLANE_TYPES.atr72.price);
|
||||
|
||||
const planes = await inst.db.select().from(schema.planes);
|
||||
expect(planes.length).toBe(before + 1);
|
||||
@@ -567,7 +568,7 @@ describe('buyPlane', () => {
|
||||
it('deducts the reputation-scaled cost at elevated rep (v0.38.0)', async () => {
|
||||
const inst = await freshDb('buy-rep.db');
|
||||
// rep 30 → rate exactly 0.2 → ATR costs 160k, not 200k.
|
||||
await inst.db.update(schema.account).set({ cash: 500_000, reputation: 30 });
|
||||
await updateLocalPlayerEconomy(inst, { cash: 500_000, reputation: 30 });
|
||||
|
||||
const before = (await inst.db.select().from(schema.planes)).length;
|
||||
await buyPlane(inst, 'atr72');
|
||||
@@ -576,23 +577,23 @@ describe('buyPlane', () => {
|
||||
expect(rate).toBeCloseTo(0.2, 12); // sanity pin on the formula input
|
||||
const expectedCost = Math.round(PLANE_TYPES.atr72.price * (1 - rate) * 100) / 100;
|
||||
expect(expectedCost).toBe(160_000); // 200k × 0.8 — exact value pin
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
expect(account.cash).toBeCloseTo(500_000 - expectedCost, 6);
|
||||
const player = await getLocalPlayer(inst);
|
||||
expect(player.cash).toBeCloseTo(500_000 - expectedCost, 6);
|
||||
expect((await inst.db.select().from(schema.planes)).length).toBe(before + 1);
|
||||
});
|
||||
|
||||
it('buys with cash between the discounted and full price; rejects below the discounted cost (v0.38.0)', async () => {
|
||||
const inst = await freshDb('buy-boundary.db');
|
||||
// rep 30 → discounted cost 160k: 170k is enough now, though it was not before v0.38.0.
|
||||
await inst.db.update(schema.account).set({ cash: 170_000, reputation: 30 });
|
||||
await updateLocalPlayerEconomy(inst, { cash: 170_000, reputation: 30 });
|
||||
|
||||
const { planeId } = await buyPlane(inst, 'atr72');
|
||||
expect(planeId).toBeTruthy(); // the old full-price check would have rejected this
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
expect(account.cash).toBeCloseTo(170_000 - 160_000, 6);
|
||||
const player = await getLocalPlayer(inst);
|
||||
expect(player.cash).toBeCloseTo(170_000 - 160_000, 6);
|
||||
|
||||
const inst2 = await freshDb('buy-boundary-poor.db'); // just below the discounted cost
|
||||
await inst2.db.update(schema.account).set({ cash: 159_999.99, reputation: 30 });
|
||||
await updateLocalPlayerEconomy(inst2, { cash: 159_999.99, reputation: 30 });
|
||||
await expectActionError(buyPlane(inst2, 'atr72'), 409, /insufficient funds/i);
|
||||
});
|
||||
});
|
||||
@@ -605,13 +606,13 @@ describe('sellPlane', () => {
|
||||
id: 'plane-extra', model: 'atr72', status: 'available', fuel: 100, maxFuel: PLANE_TYPES.atr72.maxFuel,
|
||||
locationIcao: HUB_ICAO, destIcao: null, lat: 50, lon: 8, departureSimTime: null,
|
||||
});
|
||||
const cashBefore = (await inst.db.select().from(schema.account))[0].cash;
|
||||
const cashBefore = (await getLocalPlayer(inst)).cash;
|
||||
|
||||
const { payout } = await sellPlane(inst, 'plane-extra');
|
||||
|
||||
expect(payout).toBe(Math.round(PLANE_TYPES.atr72.price * SELL_FRACTION * 100) / 100);
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
expect(account.cash).toBeCloseTo(cashBefore + payout, 6);
|
||||
const player = await getLocalPlayer(inst);
|
||||
expect(player.cash).toBeCloseTo(cashBefore + payout, 6);
|
||||
const ids = (await inst.db.select().from(schema.planes)).map((p) => p.id);
|
||||
expect(ids).not.toContain('plane-extra'); // row deleted
|
||||
expect(ids).toContain('plane-starter');
|
||||
@@ -668,7 +669,7 @@ describe('repairPlane (v0.39.0, ISS-069; gradual since v0.45.0, ISS-089)', () =>
|
||||
it('charges exactly REPAIR_COST and grounds the plane for REPAIR_DURATION_MS without touching health yet', async () => {
|
||||
const inst = await freshDb('repair-ok.db');
|
||||
await inst.db.update(schema.planes).set({ health: 42 }).where(eq(schema.planes.id, 'plane-starter'));
|
||||
const cashBefore = (await inst.db.select().from(schema.account))[0].cash;
|
||||
const cashBefore = (await getLocalPlayer(inst)).cash;
|
||||
|
||||
const { cost } = await repairPlane(inst, 'plane-starter');
|
||||
|
||||
@@ -677,13 +678,13 @@ describe('repairPlane (v0.39.0, ISS-069; gradual since v0.45.0, ISS-089)', () =>
|
||||
expect(plane.status).toBe('repairing');
|
||||
expect(plane.health).toBe(42); // NOT restored yet — the engine's finishRepairs pass heals it gradually…
|
||||
expect(plane.repairUntilSimTime).toBe(STARTING_CLOCK_MS + REPAIR_DURATION_MS); // …with the gate set from the seeded game clock (ISS-117; no tick in direct calls)
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
expect(account.cash).toBeCloseTo(cashBefore - REPAIR_COST, 6);
|
||||
const player = await getLocalPlayer(inst);
|
||||
expect(player.cash).toBeCloseTo(cashBefore - REPAIR_COST, 6);
|
||||
});
|
||||
|
||||
it('is a free no-op when health is already full', async () => {
|
||||
const inst = await freshDb('repair-full.db'); // seeded starter at 100
|
||||
const cashBefore = (await inst.db.select().from(schema.account))[0].cash;
|
||||
const cashBefore = (await getLocalPlayer(inst)).cash;
|
||||
|
||||
const { cost } = await repairPlane(inst, 'plane-starter');
|
||||
|
||||
@@ -692,7 +693,7 @@ describe('repairPlane (v0.39.0, ISS-069; gradual since v0.45.0, ISS-089)', () =>
|
||||
expect(plane.status).toBe('available'); // not grounded…
|
||||
expect(plane.health).toBe(100);
|
||||
expect(plane.repairUntilSimTime ?? null).toBeNull(); // …and no gate set
|
||||
expect((await inst.db.select().from(schema.account))[0].cash).toBe(cashBefore);
|
||||
expect((await getLocalPlayer(inst)).cash).toBe(cashBefore);
|
||||
});
|
||||
|
||||
it('rejects a flying plane with 409', async () => {
|
||||
@@ -732,7 +733,7 @@ describe('repairPlane (v0.39.0, ISS-069; gradual since v0.45.0, ISS-089)', () =>
|
||||
it('rejects the repair when cash is below REPAIR_COST with 409', async () => {
|
||||
const inst = await freshDb('repair-poor.db');
|
||||
await inst.db.update(schema.planes).set({ health: 50 }).where(eq(schema.planes.id, 'plane-starter'));
|
||||
await inst.db.update(schema.account).set({ cash: REPAIR_COST - 1 });
|
||||
await updateLocalPlayerEconomy(inst, { cash: REPAIR_COST - 1 });
|
||||
|
||||
await expectActionError(repairPlane(inst, 'plane-starter'), 409, /insufficient funds/i);
|
||||
});
|
||||
|
||||
+19
-23
@@ -1,6 +1,7 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { and, eq, inArray } from 'drizzle-orm';
|
||||
import * as schema from '@/lib/db/schema';
|
||||
import { getLocalPlayer, updateLocalPlayerEconomy } from '@/lib/db/localPlayer';
|
||||
import { persist, type GameDb } from '@/lib/db/client';
|
||||
import { loadAirportsGeoJson } from '@/lib/airports';
|
||||
import { haversineKm, initialBearingDeg } from '@/lib/geo';
|
||||
@@ -140,11 +141,10 @@ export async function flyPlane(inst: GameDb, planeId: string, destIcao: string):
|
||||
// any mutation, so a 409 leaves fuel and cash untouched.
|
||||
const target = Math.min(plane.maxFuel, Math.max(plane.fuel, requiredL * 1.5));
|
||||
const liters = Math.max(0, target - plane.fuel);
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
if (!account) throw new Error('account row missing');
|
||||
const cost = Math.round(liters * FUEL_PRICE_PER_L * (1 - repRate(account.reputation, FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT)) * 100) / 100;
|
||||
if (cost > 0 && account.cash < cost) {
|
||||
throw new GameActionError(409, `insufficient funds: refueling costs ${cost}, you have ${Math.round(account.cash)}`);
|
||||
const player = await getLocalPlayer(inst);
|
||||
const cost = Math.round(liters * FUEL_PRICE_PER_L * (1 - repRate(player.reputation, FUEL_DISCOUNT_PER_REP, MAX_FUEL_DISCOUNT)) * 100) / 100;
|
||||
if (cost > 0 && player.cash < cost) {
|
||||
throw new GameActionError(409, `insufficient funds: refueling costs ${cost}, you have ${Math.round(player.cash)}`);
|
||||
}
|
||||
// Backstop: a leg that doesn't fit even a full tank is unflyable (target caps at maxFuel).
|
||||
if (requiredL > target) {
|
||||
@@ -173,7 +173,7 @@ export async function flyPlane(inst: GameDb, planeId: string, destIcao: string):
|
||||
})
|
||||
.where(eq(schema.planes.id, planeId));
|
||||
if (cost > 0) {
|
||||
await inst.db.update(schema.account).set({ cash: Math.round((account.cash - cost) * 100) / 100 });
|
||||
await updateLocalPlayerEconomy(inst, { cash: Math.round((player.cash - cost) * 100) / 100 });
|
||||
}
|
||||
await persist(inst);
|
||||
}
|
||||
@@ -255,9 +255,8 @@ export async function abandonPackage(inst: GameDb, packageId: string): Promise<{
|
||||
if (!sim) throw new Error('sim_state row missing');
|
||||
const penalty = abandonPenalty(pkg, sim.clockMs);
|
||||
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
if (!account) throw new Error('account row missing');
|
||||
await inst.db.update(schema.account).set({ cash: Math.max(0, Math.round((account.cash - penalty) * 100) / 100) });
|
||||
const player = await getLocalPlayer(inst);
|
||||
await updateLocalPlayerEconomy(inst, { cash: Math.max(0, Math.round((player.cash - penalty) * 100) / 100) });
|
||||
await inst.db.update(schema.packages).set({ status: 'abandoned', planeId: null }).where(eq(schema.packages.id, packageId));
|
||||
await persist(inst);
|
||||
return { penalty };
|
||||
@@ -272,12 +271,11 @@ export async function buyPlane(inst: GameDb, model: string): Promise<{ planeId:
|
||||
const type = PLANE_TYPES[model];
|
||||
if (!type) throw new GameActionError(400, `unknown plane model "${model}"`);
|
||||
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
if (!account) throw new Error('account row missing');
|
||||
const player = await getLocalPlayer(inst);
|
||||
// v0.38.0 (ISS-072): the EFFECTIVE cost drives both the funds check and the deduction.
|
||||
const cost = Math.round(type.price * (1 - repRate(account.reputation, PLANE_DISCOUNT_PER_REP, MAX_PLANE_DISCOUNT)) * 100) / 100;
|
||||
if (account.cash < cost) {
|
||||
throw new GameActionError(409, `insufficient funds: a ${type.name} costs ${cost}, you have ${Math.round(account.cash)}`);
|
||||
const cost = Math.round(type.price * (1 - repRate(player.reputation, PLANE_DISCOUNT_PER_REP, MAX_PLANE_DISCOUNT)) * 100) / 100;
|
||||
if (player.cash < cost) {
|
||||
throw new GameActionError(409, `insufficient funds: a ${type.name} costs ${cost}, you have ${Math.round(player.cash)}`);
|
||||
}
|
||||
|
||||
const fc = await loadAirportsGeoJson();
|
||||
@@ -298,7 +296,7 @@ export async function buyPlane(inst: GameDb, model: string): Promise<{ planeId:
|
||||
departureSimTime: null,
|
||||
health: 100, // new planes arrive at full health (v0.39.0, ISS-069)
|
||||
});
|
||||
await inst.db.update(schema.account).set({ cash: Math.round((account.cash - cost) * 100) / 100 });
|
||||
await updateLocalPlayerEconomy(inst, { cash: Math.round((player.cash - cost) * 100) / 100 });
|
||||
await persist(inst);
|
||||
return { planeId };
|
||||
}
|
||||
@@ -337,9 +335,8 @@ export async function sellPlane(inst: GameDb, planeId: string): Promise<{ payout
|
||||
const payout = Math.round(type.price * SELL_FRACTION * 100) / 100;
|
||||
|
||||
await inst.db.delete(schema.planes).where(eq(schema.planes.id, planeId));
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
if (!account) throw new Error('account row missing');
|
||||
await inst.db.update(schema.account).set({ cash: Math.round((account.cash + payout) * 100) / 100 });
|
||||
const player = await getLocalPlayer(inst);
|
||||
await updateLocalPlayerEconomy(inst, { cash: Math.round((player.cash + payout) * 100) / 100 });
|
||||
await persist(inst);
|
||||
return { payout };
|
||||
}
|
||||
@@ -364,17 +361,16 @@ export async function repairPlane(inst: GameDb, planeId: string): Promise<{ cost
|
||||
|
||||
const sim = (await inst.db.select().from(schema.simState))[0];
|
||||
if (!sim) throw new Error('sim_state row missing');
|
||||
const account = (await inst.db.select().from(schema.account))[0];
|
||||
if (!account) throw new Error('account row missing');
|
||||
if (account.cash < REPAIR_COST) {
|
||||
throw new GameActionError(409, `insufficient funds: repair costs ${REPAIR_COST}, you have ${Math.round(account.cash)}`);
|
||||
const player = await getLocalPlayer(inst);
|
||||
if (player.cash < REPAIR_COST) {
|
||||
throw new GameActionError(409, `insufficient funds: repair costs ${REPAIR_COST}, you have ${Math.round(player.cash)}`);
|
||||
}
|
||||
|
||||
// Health is NOT set here — the engine's finishRepairs pass heals it gradually along the gate.
|
||||
await inst.db.update(schema.planes)
|
||||
.set({ status: 'repairing', repairUntilSimTime: sim.clockMs + REPAIR_DURATION_MS })
|
||||
.where(eq(schema.planes.id, planeId));
|
||||
await inst.db.update(schema.account).set({ cash: Math.round((account.cash - REPAIR_COST) * 100) / 100 });
|
||||
await updateLocalPlayerEconomy(inst, { cash: Math.round((player.cash - REPAIR_COST) * 100) / 100 });
|
||||
await persist(inst);
|
||||
return { cost: REPAIR_COST };
|
||||
}
|
||||
|
||||
+9
-6
@@ -2,6 +2,7 @@ import { loadAirportsGeoJson } from '@/lib/airports';
|
||||
import { haversineKm, initialBearingDeg } from '@/lib/geo';
|
||||
import { weatherFor } from '@/lib/weather';
|
||||
import { getGameDb } from '@/lib/db/client';
|
||||
import { getLocalPlayer } from '@/lib/db/localPlayer';
|
||||
import * as schema from '@/lib/db/schema';
|
||||
import { catalogEntries, FUEL_PRICE_PER_L, PLANE_TYPES, SELL_FRACTION, type CatalogEntry } from './catalog';
|
||||
import { fuelAtWeighted, progressAt, positionAt, windBurnFactor } from '@/lib/sim/movement';
|
||||
@@ -114,19 +115,21 @@ export function sseFrame(snapshot: StateSnapshot): StateSnapshot {
|
||||
}
|
||||
|
||||
export async function getStateSnapshot(): Promise<StateSnapshot> {
|
||||
const { db } = await getGameDb();
|
||||
const inst = await getGameDb();
|
||||
const { db } = inst;
|
||||
// NOTE: use plain select(), not the relational db.query.* API — in drizzle-orm
|
||||
// 0.45.x the sql-js driver drops the result mapper for relational queries and
|
||||
// returns raw rows keyed by SQL column names (snake_case, no type decoding).
|
||||
const [accountRows, simRows, planeRows, packageRows] = await Promise.all([
|
||||
db.select().from(schema.account),
|
||||
// v1.0.0 (ISS-105): the snapshot's `account` field is now the local player row — same shape,
|
||||
// resolved through the temporary single-player helper until multiplayer lands.
|
||||
const [player, simRows, planeRows, packageRows] = await Promise.all([
|
||||
getLocalPlayer(inst),
|
||||
db.select().from(schema.simState),
|
||||
db.select().from(schema.planes),
|
||||
db.select().from(schema.packages),
|
||||
]);
|
||||
const accountRow = accountRows[0];
|
||||
const simRow = simRows[0];
|
||||
if (!accountRow || !simRow) throw new Error('game state missing account/sim_state row');
|
||||
if (!simRow) throw new Error('game state missing sim_state row');
|
||||
|
||||
const fc = await loadAirportsGeoJson();
|
||||
const nameByIcao = new Map(fc.features.map((f) => [f.properties.icao, f.properties.name]));
|
||||
@@ -169,7 +172,7 @@ export async function getStateSnapshot(): Promise<StateSnapshot> {
|
||||
};
|
||||
|
||||
return {
|
||||
account: { cash: accountRow.cash, reputation: accountRow.reputation },
|
||||
account: { cash: player.cash, reputation: player.reputation },
|
||||
simState: { clockMs: simRow.clockMs },
|
||||
catalog: catalogEntries(),
|
||||
fuelPricePerL: FUEL_PRICE_PER_L,
|
||||
|
||||
Reference in New Issue
Block a user