deadhead-space-freight-tycoon/apps/server/src/game/player-state.ts

121 lines
4.4 KiB
TypeScript

import { content } from "@deadhead/content";
import type { PlayerState } from "@deadhead/schema";
import type { RealTime } from "@deadhead/sim";
import { and, eq, isNull, max } from "drizzle-orm";
import type { Queryable } from "../db/client";
import {
bays,
companies,
discoveredLocations,
dispatches,
pilots,
players,
ships,
} from "../db/schema";
import { isDealAvailable } from "./deal";
import { goalStates } from "./goals";
import { toApiDispatch, toApiPilot, toApiStatus } from "./mappers";
/** Reads everything the client needs about one player. */
export async function loadPlayerState(
db: Queryable,
playerId: string,
now: RealTime,
): Promise<PlayerState> {
const [player] = await db.select().from(players).where(eq(players.id, playerId));
const [company] = await db.select().from(companies).where(eq(companies.playerId, playerId));
if (!player || !company) throw new Error(`Player ${playerId} not found`);
const shipRows = await db.select().from(ships).where(eq(ships.playerId, playerId));
const pilotRows = await db.select().from(pilots).where(eq(pilots.playerId, playerId));
const bayRows = await db.select().from(bays).where(eq(bays.playerId, playerId));
const discovered = await db
.select({ locationId: discoveredLocations.locationId })
.from(discoveredLocations)
.where(eq(discoveredLocations.playerId, playerId));
const active = await db
.select()
.from(dispatches)
.where(and(eq(dispatches.playerId, playerId), eq(dispatches.status, "in_flight")));
const arrivals = await db
.select({ shipId: dispatches.shipId, at: max(dispatches.arrivalAt) })
.from(dispatches)
.where(and(eq(dispatches.playerId, playerId), eq(dispatches.status, "resolved")))
.groupBy(dispatches.shipId);
const unacknowledged = await db
.select()
.from(dispatches)
.where(
and(
eq(dispatches.playerId, playerId),
eq(dispatches.status, "resolved"),
isNull(dispatches.acknowledgedAt),
),
);
const discoveredIds = new Set(discovered.map((d) => d.locationId));
const shipById = new Map(shipRows.map((s) => [s.id, s]));
const activeByShip = new Map(active.filter((d) => d.shipId).map((d) => [d.shipId!, d.id]));
const lastArrivalByShip = new Map(arrivals.map((a) => [a.shipId, a.at]));
const pilotByShip = new Map(
pilotRows.filter((p) => p.assignedShipId).map((p) => [p.assignedShipId!, p.id]),
);
return {
company: {
id: company.id,
name: company.name,
credits: company.credits,
loanBalance: company.loanBalance,
},
ships: shipRows.map((s) => ({
id: s.id,
modelId: s.modelId,
name: s.name,
status: toApiStatus(s.status),
locationId: s.status === "in_flight" ? null : s.locationId,
dispatchId: activeByShip.get(s.id) ?? null,
pilotId: pilotByShip.get(s.id) ?? null,
lastArrivalAt: lastArrivalByShip.get(s.id) ?? s.createdAt,
})),
pilots: pilotRows.map((p) =>
toApiPilot(
p,
p.assignedShipId ? (shipById.get(p.assignedShipId)?.locationId ?? null) : null,
now,
),
),
bays: bayRows.map((b) => ({ id: b.id, locationId: b.locationId })),
locations: content.locations.map((l) => ({ id: l.id, discovered: discoveredIds.has(l.id) })),
tutorial:
player.tutorialActive && player.loanerLocationId
? {
loanerLocationId: player.loanerLocationId as "earth" | "moon",
runInFlight: active.some((d) => d.tutorial),
runsCompleted: player.tutorialRunsCompleted,
dealAvailable: isDealAvailable(
player,
active.some((d) => d.tutorial),
),
}
: null,
activeDispatches: active.map(toApiDispatch),
summaries: unacknowledged.map((d) => ({
dispatchId: d.id,
shipId: d.record.shipId,
pilotId: d.pilotId,
originId: d.record.originId,
destinationId: d.record.destinationId,
tutorial: d.tutorial,
credits: d.resolution?.credits ?? 0,
loanRepayment: d.resolution?.loanRepayment ?? 0,
pilotXp: d.resolution?.pilotXp ?? 0,
discoveredLocationId: d.resolution?.discoversDestination ? d.record.destinationId : null,
scriptedId: d.record.contract?.scriptedId ?? null,
restEndsAt: d.resolution?.pilotRestEndsAt ?? null,
resolvedAt: d.resolvedAt ?? d.arrivalAt,
})),
goals: await goalStates(db, playerId),
};
}