179 lines
6.6 KiB
TypeScript
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 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");
|
|
});
|
|
});
|