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, toApiShip } from "./mappers"; import { loadUpgradeLevels } from "./upgrades"; /** Reads everything the client needs about one player. */ export async function loadPlayerState( db: Queryable, playerId: string, now: RealTime, ): Promise { 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 upgradesByShip = await loadUpgradeLevels( db, shipRows.map((s) => s.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) => toApiShip( s, { dispatchId: activeByShip.get(s.id) ?? null, pilotId: pilotByShip.get(s.id) ?? null, lastArrivalAt: lastArrivalByShip.get(s.id) ?? s.createdAt, upgrades: upgradesByShip.get(s.id) ?? {}, }, now, ), ), 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, deadhead: d.record.contract === null, fuelCost: d.record.fuelCost, goodId: d.record.contract?.goodId ?? null, massTonnes: d.record.contract?.massTonnes ?? null, shipper: d.record.contract?.shipper ?? null, credits: d.resolution?.credits ?? 0, loanRepayment: d.resolution?.loanRepayment ?? 0, pilotPay: d.resolution?.pilotPay ?? 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), }; }