deadhead-space-freight-tycoon/apps/server/test/pilots.test.ts

179 lines
6.6 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { content } from "@deadhead/content";
import { errorResponseSchema, pilotResponseSchema } from "@deadhead/schema";
import { eq } from "drizzle-orm";
import { describe, expect, it } from "vitest";
import { bays, ships } from "../src/db/schema";
import { makeOwner, setCredits, TEST_NOW, useTestDatabase } from "./db";
import { call, getState, insertDispatch, signUp, useTestApp, type Guest } from "./game";
const database = useTestDatabase();
const testApp = useTestApp(database);
const errorCode = (response: { json(): unknown }) =>
errorResponseSchema.parse(response.json()).error.code;
/** An owner with a Moon bay, plenty of credits, and an extra uncrewed Courier at the Moon. */
async function ownerWithMoonBay() {
const { app } = testApp();
const { db } = database();
const guest = await signUp(app);
const { shipId: earthShipId } = await makeOwner(db, guest.playerId, guest.avatarId);
await setCredits(db, guest.playerId, 100_000);
await db.insert(bays).values({
id: randomUUID(),
playerId: guest.playerId,
locationId: "moon",
createdAt: TEST_NOW,
});
const moonShipId = randomUUID();
await db.insert(ships).values({
id: moonShipId,
playerId: guest.playerId,
modelId: "courier",
name: "Moon Hopper",
locationId: "moon",
createdAt: TEST_NOW,
});
return { guest, earthShipId, moonShipId };
}
async function hire(guest: Guest, locationId: string) {
return call(testApp().app, guest, "POST", "/api/pilots", { locationId });
}
describe("hiring pilots", () => {
it("hires a level-0 pilot at the Moon bay and deducts the fee", async () => {
const { app } = testApp();
const { guest } = await ownerWithMoonBay();
const response = await hire(guest, "moon");
expect(response.statusCode).toBe(200);
const { pilot } = pilotResponseSchema.parse(response.json());
expect(pilot).toMatchObject({
isAvatar: false,
level: 0,
xp: 0,
locationId: "moon",
assignedShipId: null,
status: "idle",
});
expect(pilot.name.length).toBeGreaterThan(0);
expect((await getState(app, guest)).company.credits).toBe(
100_000 - content.config.pilotHireCost,
);
});
it("refuses to hire where the player has no bay", async () => {
const { guest } = await ownerWithMoonBay();
const response = await hire(guest, "mars");
expect(response.statusCode).toBe(409);
expect(errorCode(response)).toBe("no-bay-at-location");
});
it("refuses to hire without enough credits", async () => {
const { guest } = await ownerWithMoonBay();
await setCredits(database().db, guest.playerId, content.config.pilotHireCost - 1);
expect(errorCode(await hire(guest, "moon"))).toBe("insufficient-funds");
});
});
describe("assigning pilots", () => {
it("assigns a pilot to an idle, uncrewed ship at the same location", async () => {
const { app } = testApp();
const { guest, moonShipId } = await ownerWithMoonBay();
const { pilot } = pilotResponseSchema.parse((await hire(guest, "moon")).json());
const response = await call(app, guest, "POST", `/api/pilots/${pilot.id}/assign`, {
shipId: moonShipId,
});
expect(response.statusCode).toBe(200);
expect(pilotResponseSchema.parse(response.json()).pilot).toMatchObject({
assignedShipId: moonShipId,
locationId: "moon",
});
const state = await getState(app, guest);
expect(state.ships.find((s) => s.id === moonShipId)!.pilotId).toBe(pilot.id);
});
it("refuses a ship at another location", async () => {
const { app } = testApp();
const { guest, moonShipId } = await ownerWithMoonBay();
const { pilot } = pilotResponseSchema.parse((await hire(guest, "earth")).json());
const response = await call(app, guest, "POST", `/api/pilots/${pilot.id}/assign`, {
shipId: moonShipId,
});
expect(errorCode(response)).toBe("ship-not-idle-here");
});
it("refuses a crewed ship", async () => {
const { app } = testApp();
const { guest, earthShipId } = await ownerWithMoonBay();
const { pilot } = pilotResponseSchema.parse((await hire(guest, "earth")).json());
const response = await call(app, guest, "POST", `/api/pilots/${pilot.id}/assign`, {
shipId: earthShipId,
});
expect(errorCode(response)).toBe("ship-crewed");
});
it("refuses while the ship is in flight", async () => {
const { app, clock } = testApp();
const { guest, earthShipId } = await ownerWithMoonBay();
const { pilot } = pilotResponseSchema.parse((await hire(guest, "earth")).json());
await insertDispatch(database().db, {
playerId: guest.playerId,
shipId: earthShipId,
pilotId: guest.avatarId,
departAt: clock.now,
});
const response = await call(app, guest, "POST", `/api/pilots/${pilot.id}/assign`, {
shipId: earthShipId,
});
expect(errorCode(response)).toBe("ship-not-idle");
});
it("refuses to assign the avatar during the tutorial", async () => {
const { app } = testApp();
const guest = await signUp(app);
// Ships cannot be bought during the tutorial, so put one at Earth directly.
const shipId = randomUUID();
await database().db.insert(ships).values({
id: shipId,
playerId: guest.playerId,
modelId: "courier",
name: "Gift Courier",
locationId: "earth",
createdAt: TEST_NOW,
});
const response = await call(app, guest, "POST", `/api/pilots/${guest.avatarId}/assign`, {
shipId,
});
expect(errorCode(response)).toBe("avatar-in-tutorial");
});
});
describe("unassigning pilots", () => {
it("unassigns at a bay, leaving the pilot at the ship's location", async () => {
const { app } = testApp();
const { guest, earthShipId } = await ownerWithMoonBay();
const response = await call(app, guest, "POST", `/api/pilots/${guest.avatarId}/unassign`);
expect(response.statusCode).toBe(200);
expect(pilotResponseSchema.parse(response.json()).pilot).toMatchObject({
assignedShipId: null,
locationId: "earth",
});
const state = await getState(app, guest);
expect(state.ships.find((s) => s.id === earthShipId)!.pilotId).toBeNull();
});
it("refuses where the player has no bay", async () => {
const { app } = testApp();
const { guest, earthShipId } = await ownerWithMoonBay();
// Park the crewed ship at Mars, where there is no bay.
const { db } = database();
await db.update(ships).set({ locationId: "mars" }).where(eq(ships.id, earthShipId));
const response = await call(app, guest, "POST", `/api/pilots/${guest.avatarId}/unassign`);
expect(response.statusCode).toBe(409);
expect(errorCode(response)).toBe("no-bay-at-location");
});
});