deadhead-space-freight-tycoon/packages/engine/test/ported/pilots.test.ts

180 lines
6.3 KiB
TypeScript

// Ported from apps/server/test (CR-014 Step 14.5): now runs on the engine.
import { content } from "@deadhead/content";
import { errorResponseSchema, pilotResponseSchema } from "@deadhead/schema";
import { describe, expect, it } from "vitest";
import {
TEST_NOW,
useTestApp,
signUp,
call,
getState,
edit,
setCredits,
insertDispatch,
makeOwner,
addShip,
type Guest,
} from "../support";
const randomUUID = () => globalThis.crypto.randomUUID();
const testApp = useTestApp();
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 guest = await signUp(app);
const { shipId: earthShipId } = await makeOwner(testApp().app, guest.avatarId);
await setCredits(testApp().app, 100_000);
await edit(app, (save) => {
save.bays.push({ id: randomUUID(), locationId: "moon", createdAt: TEST_NOW });
});
const moonShipId = randomUUID();
await addShip(app, {
id: moonShipId,
modelId: "courier",
name: "Moon Hopper",
locationId: "moon",
});
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(testApp().app, 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(testApp().app, {
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 addShip(app, {
id: shipId,
modelId: "courier",
name: "Gift Courier",
locationId: "earth",
});
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.
await edit(app, (save) => {
save.ships.find((s) => s.id === earthShipId)!.locationId = "mars";
});
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");
});
});