import { content } from "@deadhead/content"; import type { Dispatch, Pilot, PlayerState, Ship } from "@deadhead/schema"; import { screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { describe, expect, it } from "vitest"; import { independentStatus } from "../src/components/IndependentPilots"; import { formatLocalTime } from "../src/screens/DispatchSheet"; import { pilotExtras } from "./fixtures"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const NOW = Date.now(); const ship = (id: string, pilotId: string | null): Ship => ({ id, modelId: "courier", name: `Ship ${id}`, status: "idle", locationId: "earth", dispatchId: null, lastArrivalAt: 0, ...shipExtras("courier"), pilotId, }); const hired = (over: Partial = {}): Pilot => ({ id: "p1", name: "Ada Vance", isAvatar: false, isVeteran: false, xp: 0, level: 0, skills: {}, skillPointsAvailable: 0, assignedShipId: "s1", status: "idle", locationId: "earth", dutyHours: 0, dutyLimit: 12, resting: false, restEndsAt: null, ...pilotExtras, ...over, }); function owner(over: Partial = {}): PlayerState { const base = samplePlayerState({ tutorial: null }); return { ...base, company: { ...base.company, credits: 100_000 }, bays: [{ id: "b1", locationId: "earth" }], ships: [ship("s0", "avatar"), ship("s1", "p1")], pilots: [{ ...base.pilots[0]!, assignedShipId: "s0", locationId: "earth" }, hired()], ...over, }; } const ownedOffice = { dispatchOffice: { owned: true, spendingFloor: 500 } }; describe("Dispatch Office and pilot controls (Step 21.6, decisions 0.5.122–0.5.124)", () => { it("disables Independent with a reason before the Dispatch Office, and hides it for you", async () => { renderApp(fakeApi({ getPlayerState: () => owner() }), "/pilots"); const card = (await screen.findByText("Ada Vance")).closest("li")!; expect(within(card).getByRole("radio", { name: "Independent" })).toBeDisabled(); expect(card).toHaveTextContent("Buy a Dispatch Office on the Bridge"); const yours = screen.getByText("Captain").closest("li")!; expect(within(yours).queryByRole("radio", { name: "Independent" })).not.toBeInTheDocument(); }); it("sets a pilot Independent once the office is bought", async () => { const api = fakeApi({ getPlayerState: () => owner(ownedOffice), setPilotMode: ({ pilotId, mode }) => ({ pilot: hired({ id: pilotId, mode }) }), }); renderApp(api, "/pilots"); const card = (await screen.findByText("Ada Vance")).closest("li")!; await userEvent.click(within(card).getByRole("radio", { name: "Independent" })); expect(api.call).toHaveBeenCalledWith("setPilotMode", { pilotId: "p1", mode: "independent" }); }); it("saves edited orders through the engine", async () => { const api = fakeApi({ getPlayerState: () => owner(ownedOffice), setPilotOrders: ({ pilotId, orders }) => ({ pilot: hired({ id: pilotId, orders }) }), }); renderApp(api, "/pilots"); const card = (await screen.findByText("Ada Vance")).closest("li")!; await userEvent.click(within(card).getByText("Orders")); const form = within(card).getByRole("form", { name: "Orders for Ada Vance" }); await userEvent.click(within(form).getByRole("checkbox", { name: "Take Priority jobs" })); await userEvent.click(within(form).getByRole("checkbox", { name: "Anywhere you know" })); await userEvent.click(within(form).getByRole("checkbox", { name: "Earth" })); await userEvent.clear(within(form).getByRole("spinbutton")); await userEvent.type(within(form).getByRole("spinbutton"), "300"); await userEvent.click(within(form).getByRole("button", { name: "Save orders" })); expect(api.call).toHaveBeenCalledWith("setPilotOrders", { pilotId: "p1", orders: { ...pilotExtras.orders, destinationIds: ["moon"], takePriority: false, minProfit: 300, }, }); }); it("describes what an Independent pilot is doing", () => { const flight: Dispatch = { id: "d1", shipId: "s1", pilotId: "p1", contract: { id: "c1", originId: "earth", destinationId: "moon", cargoTypeId: "high-value", goodId: "medical-supplies", shipper: "Meridian Health", priority: false, deliveryWindowGameHours: null, tier: 1, massTonnes: 6, payout: 3_000, requiredSkill: { skillId: "expensive-cargo", level: 1 }, requiredLongHaulLevel: 0, offeredAt: NOW, expiresAt: NOW + 600_000, firstContact: false, }, originId: "earth", destinationId: "moon", tutorial: false, departureAt: NOW, arrivalAt: NOW + 120_000, distanceMillionKm: 0.38, fuelCost: 50, fuelBurnedFu: 5, expectedPayout: 3_000, seed: 1, status: "in-flight", resolvedAt: null, }; const independent = hired({ mode: "independent" }); expect( independentStatus(owner({ activeDispatches: [flight] }), { ...independent, status: "in-flight", locationId: null, }), ).toBe("Independent: flying Medical Supplies to Moon"); expect(independentStatus(owner(), { ...independent, locationId: "mars" })).toBe( "Waiting for new contracts at Mars", ); const wakes = NOW + 30 * 60_000; expect(independentStatus(owner(), { ...independent, resting: true, restEndsAt: wakes })).toBe( `Resting until ${formatLocalTime(wakes)}`, ); expect(independentStatus(owner(), hired())).toBeNull(); }); it("shows the Independent badge only on ships crewed by Independent pilots", async () => { const state = owner({ pilots: [{ ...owner().pilots[0]! }, hired({ mode: "independent" })], }); renderApp(fakeApi({ getPlayerState: () => state }), "/fleet"); const theirs = (await screen.findByText("Ship s1")).closest("li")!; const yours = screen.getByText("Ship s0").closest("li")!; expect(within(theirs).getByText("Independent")).toBeInTheDocument(); expect(within(yours).queryByText("Independent")).not.toBeInTheDocument(); }); it("sells the Dispatch Office on the Bridge and sets the spending floor in Settings", async () => { const api = fakeApi({ getPlayerState: () => owner(), buyDispatchOffice: () => ({ company: owner().company, dispatchOffice: { owned: true, spendingFloor: 500 }, }), setSpendingFloor: ({ credits }) => ({ company: owner().company, dispatchOffice: { owned: false, spendingFloor: credits }, }), }); const { unmount } = renderApp(api, "/"); const office = await screen.findByLabelText("Dispatch Office"); expect(office).toHaveTextContent("Lets your hired pilots run jobs on their own"); const price = content.config.independentPilots.dispatchOfficePrice.toLocaleString("en-US"); await userEvent.click( within(office).getByRole("button", { name: `Buy Dispatch Office (${price} cr)` }), ); expect(api.call).toHaveBeenCalledWith("buyDispatchOffice", {}); unmount(); renderApp(api, "/settings"); const floor = await screen.findByRole("spinbutton"); await userEvent.clear(floor); await userEvent.type(floor, "2000"); await userEvent.click(screen.getByRole("button", { name: "Save floor" })); expect(api.call).toHaveBeenCalledWith("setSpendingFloor", { credits: 2000 }); }); });