v1.3.0: per-player money & reputation in engine + actions (ISS-110)
CI / build + unit + e2e (pull_request) Successful in 13m20s
CI / build + unit + e2e (pull_request) Successful in 13m20s
This commit is contained in:
@@ -30,8 +30,10 @@ export async function GET(req: Request) {
|
||||
start(controller) {
|
||||
// Snapshot FIRST, then subscribe — a broadcast can never land before the initial event, so
|
||||
// events are strictly ordered by sim clock. A tick in the gap just means the client waits ≤1 s
|
||||
// for the next push instead of seeing an out-of-order frame.
|
||||
getStateSnapshot()
|
||||
// for the next push instead of seeing an out-of-order frame. v1.3.0 (ISS-110): the connect
|
||||
// frame carries THIS session's account row; later pushes come from the shared broadcast
|
||||
// (local player) until per-player frames land (#112).
|
||||
getStateSnapshot(auth.player.id)
|
||||
.then((snapshot) => {
|
||||
unsubscribe = subscribe((text) => {
|
||||
try {
|
||||
|
||||
@@ -5,7 +5,8 @@ import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
||||
import { loadAirportsGeoJson } from '@/lib/airports';
|
||||
import { haversineKm } from '@/lib/geo';
|
||||
import { FUEL_PRICE_PER_L, PLANE_TYPES, SELL_FRACTION } from '@/lib/game/catalog';
|
||||
import { createSession } from '@/lib/auth';
|
||||
import { createSession, hashPassword } from '@/lib/auth';
|
||||
import * as schema from '@/lib/db/schema';
|
||||
import { getGameDb } from '@/lib/db/client';
|
||||
import { GET } from './route';
|
||||
|
||||
@@ -113,4 +114,23 @@ describe('GET /api/state', () => {
|
||||
expect(snap.fuelPricePerL).toBe(FUEL_PRICE_PER_L);
|
||||
expect(snap.sellFraction).toBe(SELL_FRACTION);
|
||||
});
|
||||
|
||||
it('returns the SESSION player\'s economy in account, not the local one (v1.3.0, ISS-110)', async () => {
|
||||
const inst = await getGameDb();
|
||||
// A second account with a distinct economy — rows minted like /api/register does.
|
||||
const id = 'test-player-b';
|
||||
await inst.db.insert(schema.users).values({
|
||||
id, username: 'e2e-owner-b', passwordHash: hashPassword('unused'), admin: false, createdAt: Date.now(),
|
||||
});
|
||||
await inst.db.insert(schema.players).values({
|
||||
id, provider: 'local', username: 'e2e-owner-b', cash: 12_345.67, reputation: 42.5,
|
||||
});
|
||||
const sid = await createSession(inst, id);
|
||||
|
||||
const res = await GET(new Request('http://localhost/api/state', { headers: { 'X-Flight-Session': sid } }));
|
||||
expect(res.status).toBe(200);
|
||||
const body = (await res.json()) as { account: { cash: number; reputation: number } };
|
||||
expect(body.account.cash).toBeCloseTo(12_345.67, 9); // the session player's row…
|
||||
expect(body.account.reputation).toBeCloseTo(42.5, 9); // …not the seeded local account
|
||||
});
|
||||
});
|
||||
|
||||
@@ -3,7 +3,8 @@ import { tickNow } from '@/lib/sim/engine';
|
||||
import { getStateSnapshot } from '@/lib/game/state';
|
||||
|
||||
// v1.1.0 (ISS-107): guarded — 401 without a live X-Flight-Session. The snapshot shape is
|
||||
// unchanged; per-player snapshots are a later step.
|
||||
// unchanged; per-player snapshots are a later step (#112).
|
||||
// v1.3.0 (ISS-110): `account` carries the SESSION player's economy row.
|
||||
export async function GET(req: Request) {
|
||||
const auth = await requireAuth(req);
|
||||
if (!auth.ok) return auth.response;
|
||||
@@ -11,7 +12,7 @@ export async function GET(req: Request) {
|
||||
// Tick-on-read (v0.7.0): advance the sim clock before snapshotting so every
|
||||
// read reflects current game time (expiry/finalize/spawn run inside the tick).
|
||||
await tickNow();
|
||||
return Response.json(await getStateSnapshot());
|
||||
return Response.json(await getStateSnapshot(auth.player.id));
|
||||
} catch (err) {
|
||||
console.error('failed to load game state', err);
|
||||
return Response.json({ error: 'failed to load game state' }, { status: 500 });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getGameDb, persist } from '@/lib/db/client';
|
||||
import { getLocalPlayer, updateLocalPlayerEconomy } from '@/lib/db/localPlayer';
|
||||
import { LOCAL_PLAYER, getLocalPlayer, updateLocalPlayerEconomy } from '@/lib/db/localPlayer';
|
||||
import { getStateSnapshot } from '@/lib/game/state';
|
||||
import { tickNow } from '@/lib/sim/engine';
|
||||
|
||||
@@ -46,10 +46,12 @@ export async function POST(req: Request) {
|
||||
try {
|
||||
const inst = await getGameDb();
|
||||
await tickNow(); // clock is current before the snapshot we return (same as siblings)
|
||||
// v1.3.0 (ISS-110): admin-style hook — no session, grants to the SEEDED local account (the
|
||||
// player the e2e suite plays via mint-player-session's default).
|
||||
const player = await getLocalPlayer(inst);
|
||||
await updateLocalPlayerEconomy(inst, { cash: Math.round((player.cash + amount) * 100) / 100 });
|
||||
await persist(inst);
|
||||
return Response.json(await getStateSnapshot());
|
||||
return Response.json(await getStateSnapshot(LOCAL_PLAYER.id));
|
||||
} catch (err) {
|
||||
console.error('test hook failed', err);
|
||||
return Response.json({ error: 'test hook failed' }, { status: 500 });
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getGameDb, persist } from '@/lib/db/client';
|
||||
import { getLocalPlayer, updateLocalPlayerEconomy } from '@/lib/db/localPlayer';
|
||||
import { LOCAL_PLAYER, getLocalPlayer, updateLocalPlayerEconomy } from '@/lib/db/localPlayer';
|
||||
import { getStateSnapshot } from '@/lib/game/state';
|
||||
import { tickNow } from '@/lib/sim/engine';
|
||||
|
||||
@@ -42,10 +42,12 @@ export async function POST(req: Request) {
|
||||
try {
|
||||
const inst = await getGameDb();
|
||||
await tickNow(); // clock is current before the snapshot we return (same as siblings)
|
||||
// v1.3.0 (ISS-110): admin-style hook — no session, stages the SEEDED local account (the player
|
||||
// the e2e suite plays via mint-player-session's default).
|
||||
await getLocalPlayer(inst); // existence check — the snapshot below would fail anyway if it's gone
|
||||
await updateLocalPlayerEconomy(inst, { cash: Math.round(amount * 100) / 100 });
|
||||
await persist(inst);
|
||||
return Response.json(await getStateSnapshot());
|
||||
return Response.json(await getStateSnapshot(LOCAL_PLAYER.id));
|
||||
} catch (err) {
|
||||
console.error('test hook failed', err);
|
||||
return Response.json({ error: 'test hook failed' }, { status: 500 });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getGameDb, persist } from '@/lib/db/client';
|
||||
import { LOCAL_PLAYER } from '@/lib/db/localPlayer';
|
||||
import * as schema from '@/lib/db/schema';
|
||||
import { getStateSnapshot } from '@/lib/game/state';
|
||||
import { tickNow } from '@/lib/sim/engine';
|
||||
@@ -57,7 +58,8 @@ export async function POST(req: Request) {
|
||||
|
||||
await inst.db.update(schema.planes).set({ fuel }).where(eq(schema.planes.id, planeId));
|
||||
await persist(inst);
|
||||
return Response.json(await getStateSnapshot());
|
||||
// v1.3.0 (ISS-110): admin-style hook — no session; the snapshot reports the seeded local account.
|
||||
return Response.json(await getStateSnapshot(LOCAL_PLAYER.id));
|
||||
} catch (err) {
|
||||
console.error('test hook failed', err);
|
||||
return Response.json({ error: 'test hook failed' }, { status: 500 });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { eq } from 'drizzle-orm';
|
||||
import { getGameDb, persist } from '@/lib/db/client';
|
||||
import { LOCAL_PLAYER } from '@/lib/db/localPlayer';
|
||||
import * as schema from '@/lib/db/schema';
|
||||
import { getStateSnapshot } from '@/lib/game/state';
|
||||
import { tickNow } from '@/lib/sim/engine';
|
||||
@@ -74,7 +75,8 @@ export async function POST(req: Request) {
|
||||
if (repairUntilMs !== undefined) sets.repairUntilSimTime = Math.round(sim.clockMs + repairUntilMs);
|
||||
await inst.db.update(schema.planes).set(sets).where(eq(schema.planes.id, planeId));
|
||||
await persist(inst);
|
||||
return Response.json(await getStateSnapshot());
|
||||
// v1.3.0 (ISS-110): admin-style hook — no session; the snapshot reports the seeded local account.
|
||||
return Response.json(await getStateSnapshot(LOCAL_PLAYER.id));
|
||||
} catch (err) {
|
||||
console.error('test hook failed', err);
|
||||
return Response.json({ error: 'test hook failed' }, { status: 500 });
|
||||
|
||||
Reference in New Issue
Block a user