v1.6.0: leaderboard pane from the top-bar additional-panels button (ISS-115)
CI / build + unit + e2e (pull_request) Successful in 14m2s
CI / build + unit + e2e (pull_request) Successful in 14m2s
This commit is contained in:
+55
-2
@@ -1,5 +1,12 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { SSE_PACKAGE_STATUSES, sseFrame, type PackageState, type StateSnapshot } from './state';
|
||||
import fs from 'node:fs/promises';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import * as schema from '@/lib/db/schema';
|
||||
import { getGameDb } from '@/lib/db/client';
|
||||
import { PLANE_TYPES } from './catalog';
|
||||
import { STARTING_CASH, STARTING_REPUTATION } from './seed';
|
||||
import { getStateSnapshot, SSE_PACKAGE_STATUSES, sseFrame, type PackageState, type StateSnapshot } from './state';
|
||||
|
||||
// ISS-133 (v0.59.0) — the SSE projection keeps only the package statuses any component renders.
|
||||
|
||||
@@ -30,6 +37,7 @@ const snapshot = (packages: PackageState[]): StateSnapshot => ({
|
||||
fuelPricePerL: 0.8,
|
||||
sellFraction: 0.5,
|
||||
planes: [],
|
||||
players: [], // v1.6.0 (ISS-115) — leaderboard rows; empty in these sseFrame fixtures
|
||||
packages,
|
||||
});
|
||||
|
||||
@@ -60,3 +68,48 @@ describe('sseFrame', () => {
|
||||
expect([...SSE_PACKAGE_STATUSES].sort()).toEqual(['expired', 'loaded', 'open']);
|
||||
});
|
||||
});
|
||||
|
||||
// v1.6.0 (ISS-115) — the snapshot's players array: one row per player with cash/reputation and
|
||||
// how many planes they own. Runs against a throwaway seeded DB via the app singleton, exactly
|
||||
// like the /api/state route tests (fresh module registry per file ⇒ fresh singleton).
|
||||
describe('getStateSnapshot players', () => {
|
||||
let dir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
dir = await fs.mkdtemp(path.join(os.tmpdir(), 'flight-snapshot-players-'));
|
||||
process.env.FLIGHT_DB_PATH = path.join(dir, 'players.db');
|
||||
await getGameDb(); // seeds the local player + starter plane
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
delete process.env.FLIGHT_DB_PATH;
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('single-player regression guard — one row for the seeded local account', async () => {
|
||||
const snap = await getStateSnapshot('local');
|
||||
expect(snap.players).toEqual([
|
||||
{ id: 'local', username: 'kamilkrzywda', cash: STARTING_CASH, reputation: STARTING_REPUTATION, planeCount: 1 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('two players — cash/reputation/planeCount per owner (v1.6.0, ISS-115)', async () => {
|
||||
const inst = await getGameDb();
|
||||
// A second account with a distinct economy and two planes of its own — rows minted like /api/register does.
|
||||
await inst.db.insert(schema.players).values({ id: 'b', provider: 'local', username: 'player-b', cash: 5_000, reputation: 3 });
|
||||
const type = PLANE_TYPES.atr72;
|
||||
for (const id of ['plane-b-1', 'plane-b-2']) {
|
||||
await inst.db.insert(schema.planes).values({
|
||||
id, ownerId: 'b', model: 'atr72', status: 'available',
|
||||
fuel: type.maxFuel, maxFuel: type.maxFuel, locationIcao: 'EDDF', lat: 50.03, lon: 8.56,
|
||||
});
|
||||
}
|
||||
|
||||
const snap = await getStateSnapshot('b'); // any session sees the whole world's players
|
||||
expect(snap.players).toHaveLength(2);
|
||||
const local = snap.players.find((p) => p.id === 'local')!;
|
||||
expect(local).toEqual({ id: 'local', username: 'kamilkrzywda', cash: STARTING_CASH, reputation: STARTING_REPUTATION, planeCount: 1 }); // starter still counted once…
|
||||
const b = snap.players.find((p) => p.id === 'b')!;
|
||||
expect(b).toEqual({ id: 'b', username: 'player-b', cash: 5_000, reputation: 3, planeCount: 2 }); // …and both of B's planes count for B.
|
||||
});
|
||||
});
|
||||
|
||||
@@ -97,12 +97,25 @@ export interface PackageState {
|
||||
carrierName: string | null;
|
||||
}
|
||||
|
||||
// v1.6.0 (ISS-115): per-player summary for the leaderboard panel — rides on every snapshot
|
||||
// (GET /api/state + SSE frames), so no new endpoint is needed. planeCount = planes owned.
|
||||
export interface PlayerSummary {
|
||||
id: string;
|
||||
username: string;
|
||||
cash: number;
|
||||
reputation: number;
|
||||
planeCount: number;
|
||||
}
|
||||
|
||||
export interface StateSnapshot {
|
||||
account: { cash: number; reputation: number };
|
||||
// ISS-117 (v0.52.0): the world rate is a fixed constant (WORLD_TIME_SCALE), not per-save state —
|
||||
// only the clock itself is reported here.
|
||||
simState: { clockMs: number };
|
||||
planes: PlaneState[];
|
||||
// v1.6.0 (ISS-115): leaderboard data — one row per player, projected from the same players +
|
||||
// planes reads as everything else in this snapshot (no extra query).
|
||||
players: PlayerSummary[];
|
||||
packages: PackageState[];
|
||||
// Market reference data (v0.10.0) — static per build, sent with every snapshot so
|
||||
// the client can compute buy/sell previews without a separate endpoint.
|
||||
@@ -166,6 +179,10 @@ export async function getStateSnapshot(playerId: string): Promise<StateSnapshot>
|
||||
const usernameByPlayerId = new Map(playerRows.map((pl) => [pl.id, pl.username]));
|
||||
const planeById = new Map(planeRows.map((p) => [p.id, p]));
|
||||
|
||||
// v1.6.0 (ISS-115): leaderboard — count owned planes per player from the rows already loaded above.
|
||||
const planeCountByOwner = new Map<string, number>();
|
||||
for (const p of planeRows) planeCountByOwner.set(p.ownerId, (planeCountByOwner.get(p.ownerId) ?? 0) + 1);
|
||||
|
||||
// Analytic movement (v0.7.0): a flying plane's stored lat/lon/fuel are its
|
||||
// departure values — live position and fuel are pure functions of the clock.
|
||||
const livePlane = (p: schema.PlaneRow) => {
|
||||
@@ -218,6 +235,14 @@ export async function getStateSnapshot(playerId: string): Promise<StateSnapshot>
|
||||
ownerName: usernameByPlayerId.get(p.ownerId) ?? p.ownerId, // missing players row → raw id, never a crash
|
||||
};
|
||||
}),
|
||||
// v1.6.0 (ISS-115): leaderboard rows — every player in the world with their economy + fleet size.
|
||||
players: playerRows.map((pl) => ({
|
||||
id: pl.id,
|
||||
username: pl.username,
|
||||
cash: pl.cash,
|
||||
reputation: pl.reputation,
|
||||
planeCount: planeCountByOwner.get(pl.id) ?? 0,
|
||||
})),
|
||||
packages: packageRows.filter((p) => p.status !== 'open' || pool.has(p.originIcao)).map((p) => ({
|
||||
id: p.id,
|
||||
originIcao: p.originIcao,
|
||||
|
||||
Reference in New Issue
Block a user