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

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));
}