import { content } from "@deadhead/content"; import { companyResponseSchema, 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, insertDispatch, signUp, signUpOwner, useTestApp, type Guest, } from "./game"; const database = useTestDatabase(); const testApp = useTestApp(database); const claim = (guest: Guest, goalId: string) => call(testApp().app, guest, "POST", `/api/goals/${goalId}/claim`); const errorCode = (response: { json(): unknown }) => errorResponseSchema.parse(response.json()).error.code; /** An owner who has accepted the deal (so "pay off your loan" can apply). */ async function dealOwner() { const owner = await signUpOwner(testApp().app, database().db); await database() .db.update(players) .set({ dealAccepted: true }) .where(eq(players.id, owner.playerId)); return owner; } const status = async (guest: Guest, goalId: string) => (await getState(testApp().app, guest)).goals.find((g) => g.id === goalId)?.status; describe("onboarding goals", () => { it("are absent from state during the tutorial", async () => { const guest = await signUp(testApp().app); expect((await getState(testApp().app, guest)).goals).toEqual([]); }); it("are listed in content order once the tutorial is over", async () => { const owner = await dealOwner(); const goals = (await getState(testApp().app, owner)).goals; expect(goals.map((g) => g.id)).toEqual(content.onboardingGoals.map((g) => g.id)); // Paid-off loan: the test owner has no balance. expect(goals.find((g) => g.id === "loan-paid-off")?.status).toBe("ready"); expect(goals.find((g) => g.id === "veteran-contract")?.status).toBe("incomplete"); }); it("becomes ready when its condition is met: a delivery with the player's own ship", async () => { const { clock } = testApp(); const owner = await dealOwner(); expect(await status(owner, "first-own-delivery")).toBe("incomplete"); const record = await insertDispatch(database().db, { playerId: owner.playerId, shipId: owner.shipId, pilotId: owner.avatarId, departAt: clock.now, }); clock.now = record.arrivalAt; expect(await status(owner, "first-own-delivery")).toBe("ready"); }); it("counts skill points spent", async () => { const owner = await dealOwner(); await database() .db.update(pilots) .set({ skillPointsSpent: 1 }) .where(eq(pilots.id, owner.avatarId)); expect(await status(owner, "first-skill-point")).toBe("ready"); }); it("claiming a completed goal adds its reward once", async () => { const owner = await dealOwner(); await setCredits(database().db, owner.playerId, 100); const reward = content.onboardingGoal("loan-paid-off")!.reward; const response = await claim(owner, "loan-paid-off"); expect(response.statusCode).toBe(200); expect(companyResponseSchema.parse(response.json()).company.credits).toBe(100 + reward); expect(await status(owner, "loan-paid-off")).toBe("claimed"); const again = await claim(owner, "loan-paid-off"); expect(again.statusCode).toBe(409); expect(errorCode(again)).toBe("goal-already-claimed"); expect((await getState(testApp().app, owner)).company.credits).toBe(100 + reward); }); it("refuses an incomplete goal and an unknown one", async () => { const owner = await dealOwner(); await setCredits(database().db, owner.playerId, 100); const response = await claim(owner, "veteran-contract"); expect(response.statusCode).toBe(409); expect(errorCode(response)).toBe("goal-not-ready"); expect((await claim(owner, "win-the-game")).statusCode).toBe(404); expect((await getState(testApp().app, owner)).company.credits).toBe(100); }); it("refuses claims during the tutorial", async () => { const guest = await signUp(testApp().app); expect(errorCode(await claim(guest, "loan-paid-off"))).toBe("goal-not-ready"); }); });