import { content } from "@deadhead/content"; import type { Contract, Pilot, Ship } from "@deadhead/schema"; import { realToGame, routeDistance, travelTimeGameHours } from "@deadhead/sim"; import { screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useClock } from "../src/clock/clock"; import { formatGameHours } from "../src/format"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const ship = (id: string, name: string, pilotId: string): Ship => ({ id, name, modelId: "courier", status: "idle", locationId: "earth", dispatchId: null, lastArrivalAt: 0, ...shipExtras("courier"), pilotId, }); const pilot = (id: string, name: string, shipId: string, extra: Partial): Pilot => ({ id, name, isAvatar: false, xp: 0, level: 0, skills: {}, skillPointsAvailable: 0, assignedShipId: shipId, status: "idle", locationId: "earth", dutyHours: 0, dutyLimit: 12, resting: false, restEndsAt: null, ...extra, }); const moonJob: Contract = { id: "c-moon", originId: "earth", destinationId: "moon", cargoTypeId: "standard", goodId: "consumer-goods", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: 0, massTonnes: 10, payout: 900, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: NOW, expiresAt: NOW + 3_600_000, firstContact: false, }; const state = samplePlayerState({ tutorial: null, company: { id: "c", name: "D", credits: 100_000, loanBalance: 0 }, bays: [{ id: "b1", locationId: "earth" }], ships: [ ship("s-rest", "Sleepy Gull", "p-rest"), ship("s-tired", "Tired Heron", "p-tired"), ship("s-fresh", "Fresh Lark", "p-fresh"), ], pilots: [ pilot("p-rest", "Ana Resting", "s-rest", { resting: true, restEndsAt: NOW + 12 * 60_000 + 5_000, }), pilot("p-tired", "Bo Tired", "s-tired", { dutyHours: 11 }), pilot("p-fresh", "Cy Fresh", "s-fresh", { dutyHours: 1 }), ], }); /** The Courier's game hours for the Moon job, straight from the sim. */ const tripHours = () => travelTimeGameHours( content.shipModel("courier")!, routeDistance( content.location("earth")!, content.location("moon")!, realToGame(NOW, content.config), ), moonJob.massTonnes, content.config, ); async function openSheet() { renderApp( fakeApi({ getPlayerState: () => state, getContractBoard: () => ({ contracts: [moonJob], periodEndsAt: NOW + 3_600_000 }), }), "/contracts?location=earth", ); await userEvent.click(await screen.findByRole("button", { name: "Dispatch" })); return screen.getByRole("dialog"); } const option = (dialog: HTMLElement, shipName: string) => within(dialog).getByText(shipName).closest("li")!; describe("duty in the dispatch sheet (Step 10D.7)", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("disables a resting pilot's pair with the time left", async () => { const resting = option(await openSheet(), "Sleepy Gull"); expect(within(resting).getByRole("radio")).toBeDisabled(); expect(resting).toHaveTextContent("Resting — 12:05"); }); it("warns when the trip takes the pilot to the limit, with the sim's duty numbers", async () => { const dialog = await openSheet(); const tired = option(dialog, "Tired Heron"); expect(tired).toHaveTextContent( `Duty after${formatGameHours(11 + tripHours())} of ${formatGameHours(12)}`, ); expect(tired).toHaveTextContent("Will need rest on arrival"); }); it("shows no warning when the pilot stays under the limit", async () => { const fresh = option(await openSheet(), "Fresh Lark"); expect(fresh).toHaveTextContent( `Duty after${formatGameHours(1 + tripHours())} of ${formatGameHours(12)}`, ); expect(fresh).not.toHaveTextContent("Will need rest"); }); });