83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
import { randomUUID } from "node:crypto";
|
|
import { content } from "@deadhead/content";
|
|
import type { RealTime } from "@deadhead/sim";
|
|
import { and, count, eq } from "drizzle-orm";
|
|
import type { Queryable } from "../db/client";
|
|
import { bays, companies, dispatches, pilots, players, ships } from "../db/schema";
|
|
import { gameError } from "../http/errors";
|
|
import { shipName } from "./names";
|
|
|
|
/** The ship model and bay location the deal hands over (decision 0.5.30). */
|
|
export const DEAL_SHIP_MODEL_ID = "courier";
|
|
export const DEAL_BAY_LOCATION_ID = "earth";
|
|
|
|
/**
|
|
* The deal can be accepted now (decision 0.5.30): tutorial on, enough runs
|
|
* completed, the loaner back at Earth, and no tutorial run in flight.
|
|
*/
|
|
export function isDealAvailable(
|
|
player: typeof players.$inferSelect,
|
|
tutorialRunInFlight: boolean,
|
|
): boolean {
|
|
return (
|
|
player.tutorialActive &&
|
|
!player.dealAccepted &&
|
|
player.tutorialRunsCompleted >= content.config.tutorialRunsBeforeDeal &&
|
|
player.loanerLocationId === "earth" &&
|
|
!tutorialRunInFlight
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Accepts the deal (Step 10A.8): the loaner goes back to the company; the
|
|
* player gets the old Courier at Earth crewed by the avatar, the Earth bay, and
|
|
* the loan; the tutorial ends. Run inside the player lock, so two simultaneous
|
|
* accepts produce one deal.
|
|
*/
|
|
export async function acceptDeal(tx: Queryable, playerId: string, now: RealTime): Promise<void> {
|
|
const [player] = await tx.select().from(players).where(eq(players.id, playerId));
|
|
if (!player) throw new Error(`Player ${playerId} not found`);
|
|
const [running] = await tx
|
|
.select({ id: dispatches.id })
|
|
.from(dispatches)
|
|
.where(
|
|
and(
|
|
eq(dispatches.playerId, playerId),
|
|
eq(dispatches.tutorial, true),
|
|
eq(dispatches.status, "in_flight"),
|
|
),
|
|
);
|
|
if (!isDealAvailable(player, running !== undefined)) {
|
|
throw gameError("deal-not-available", "There is no deal on the table right now.");
|
|
}
|
|
|
|
const [owned] = await tx.select({ n: count() }).from(ships).where(eq(ships.playerId, playerId));
|
|
const shipId = randomUUID();
|
|
await tx.insert(ships).values({
|
|
id: shipId,
|
|
playerId,
|
|
modelId: DEAL_SHIP_MODEL_ID,
|
|
name: shipName(player.worldSeed, owned!.n),
|
|
locationId: DEAL_BAY_LOCATION_ID,
|
|
createdAt: now,
|
|
});
|
|
await tx.insert(bays).values({
|
|
id: randomUUID(),
|
|
playerId,
|
|
locationId: DEAL_BAY_LOCATION_ID,
|
|
createdAt: now,
|
|
});
|
|
await tx
|
|
.update(pilots)
|
|
.set({ assignedShipId: shipId, locationId: null, status: "idle" })
|
|
.where(and(eq(pilots.playerId, playerId), eq(pilots.isAvatar, true)));
|
|
await tx
|
|
.update(companies)
|
|
.set({ loanBalance: content.config.dealLoanAmount })
|
|
.where(eq(companies.playerId, playerId));
|
|
await tx
|
|
.update(players)
|
|
.set({ tutorialActive: false, loanerLocationId: null, dealAccepted: true })
|
|
.where(eq(players.id, playerId));
|
|
}
|