import { content } from "@deadhead/content"; import { contractBoardResponseSchema, dispatchResponseSchema, errorResponseSchema, } from "@deadhead/schema"; import { eq } from "drizzle-orm"; import { describe, expect, it } from "vitest"; import { pilots, players } from "../src/db/schema"; import { setCredits, useTestDatabase } from "./db"; import { call, getState, signUp, useTestApp, type Guest } from "./game"; const database = useTestDatabase(); const testApp = useTestApp(database); async function buyShipAndEarthBay(guest: Guest) { const { app } = testApp(); expect((await call(app, guest, "POST", "/api/ships", { modelId: "courier" })).statusCode).toBe( 200, ); expect((await call(app, guest, "POST", "/api/bays", { locationId: "earth" })).statusCode).toBe( 200, ); } describe("end of tutorial", () => { it("ends when the player owns a ship and an Earth bay, moving the avatar to Earth unassigned", async () => { const { app } = testApp(); const { db } = database(); const guest = await signUp(app); await setCredits(db, guest.playerId, 1_000_000); // Put the loaner and avatar at the Moon. await db .update(players) .set({ loanerLocationId: "moon" }) .where(eq(players.id, guest.playerId)); await db.update(pilots).set({ locationId: "moon" }).where(eq(pilots.id, guest.avatarId)); await call(app, guest, "POST", "/api/ships", { modelId: "courier" }); expect((await getState(app, guest)).tutorial).not.toBeNull(); await call(app, guest, "POST", "/api/bays", { locationId: "earth" }); const state = await getState(app, guest); expect(state.tutorial).toBeNull(); expect(state.pilots[0]).toMatchObject({ isAvatar: true, status: "idle", locationId: "earth", assignedShipId: null, }); const board = await call(app, guest, "GET", "/api/tutorial/board"); expect(board.statusCode).toBe(409); expect(errorResponseSchema.parse(board.json()).error.code).toBe("not-in-tutorial"); }); it("waits for an in-flight tutorial run to land before ending", async () => { const { app, clock } = testApp(); const guest = await signUp(app); await setCredits(database().db, guest.playerId, 1_000_000); const [contract] = contractBoardResponseSchema.parse( (await call(app, guest, "GET", "/api/tutorial/board")).json(), ).contracts; const { dispatch } = dispatchResponseSchema.parse( ( await call(app, guest, "POST", "/api/dispatches", { kind: "tutorial", contractId: contract!.id, }) ).json(), ); await buyShipAndEarthBay(guest); expect((await getState(app, guest)).tutorial).toEqual({ loanerLocationId: "earth", runInFlight: true, }); clock.now = dispatch.arrivalAt; const state = await getState(app, guest); expect(state.tutorial).toBeNull(); // The run still paid out, and the avatar was brought back to Earth. expect(state.company.credits).toBe( 1_000_000 - content.shipModel("courier")!.price - content.location("earth")!.bayLeasePrice + contract!.payout, ); expect(state.pilots[0]).toMatchObject({ locationId: "earth", status: "idle", assignedShipId: null, }); }); it("does not end with only a ship, or only an Earth bay", async () => { const { app } = testApp(); const shipOnly = await signUp(app); await setCredits(database().db, shipOnly.playerId, 1_000_000); await call(app, shipOnly, "POST", "/api/ships", { modelId: "courier" }); expect((await getState(app, shipOnly)).tutorial).not.toBeNull(); const bayOnly = await signUp(app); await setCredits(database().db, bayOnly.playerId, 1_000_000); await call(app, bayOnly, "POST", "/api/bays", { locationId: "earth" }); expect((await getState(app, bayOnly)).tutorial).not.toBeNull(); }); });