import { content } from "@deadhead/content"; import type { SaveDocument } from "@deadhead/schema"; import { MS_PER_MINUTE } from "@deadhead/sim"; import { describe, expect, it, vi } from "vitest"; import { addPilot, addShip, edit, newTestApp, putBoard, readSave, setCredits, signUpOwner, testContract, TEST_NOW, type TestApp, } from "./support"; // Long catch-ups: slow under coverage on CI. vi.setConfig({ testTimeout: 60_000 }); const HOUR = 60 * MS_PER_MINUTE; const FIRST_LOOK = content.config.independentPilots.firstLookRealMinutes * MS_PER_MINUTE; /** An owner (HQ at Earth) with the Dispatch Office and Independent pilot "pa" crewing a dented Courier at `at`. */ async function bookedPilot(at = "moon") { const test = newTestApp(); test.clock.now = TEST_NOW + FIRST_LOOK; await signUpOwner(test.app); await setCredits(test.app, content.config.independentPilots.dispatchOfficePrice + 200_000); await test.app.call("buyDispatchOffice", {}); await addShip(test.app, { id: "ship-pa", modelId: "courier", name: "Courier pa", locationId: at, hullConditionPct: 60, }); await addPilot(test.app, { id: "pa", name: "Pilot pa", assignedShipId: "ship-pa" }); await test.app.call("setPilotMode", { pilotId: "pa", mode: "independent" }); return test; } const tripsOf = (save: SaveDocument) => save.dispatches.filter((d) => d.pilotId === "pa"); const shipPa = (save: SaveDocument) => save.ships.find((s) => s.id === "ship-pa")!; const serviceLog = (save: SaveDocument) => save.logbook.flatMap((e) => (e.kind === "service" && e.shipId === "ship-pa" ? [e.phase] : [])); /** A second engine holding a copy of `test`'s save, at the same time. */ async function copyOf(test: TestApp): Promise { const { code } = await test.app.call("exportSave", {}); const copy = newTestApp(); copy.clock.now = test.clock.now; await copy.app.call("importSave", { code }); return copy; } describe("Independent pilots and booked service (Step 27.4, decision 0.5.154)", () => { it("takes the job that ends at the booked HQ, and the work starts on landing", async () => { const test = await bookedPilot(); const home = testContract({ id: "moon:home", originId: "moon", destinationId: "earth" }); const rich = testContract({ id: "moon:rich", originId: "moon", destinationId: "mars", payout: 20_000, }); await putBoard(test.app, [rich, home], test.clock.now, "moon"); await test.app.call("bookService", { shipId: "ship-pa", repair: true, upgradeId: null }); expect( (await test.app.call("getPlayerState", {})).ships.find((s) => s.id === "ship-pa")!.booking, ).toMatchObject({ status: "heading", hqId: "earth" }); test.clock.now += 2 * HOUR; await test.app.call("getPlayerState", {}); const save = await readSave(test.app); expect(tripsOf(save)[0]!.record.contract?.id).toBe("moon:home"); // Back to work afterward: wear and auto-repair may start the next round. expect(serviceLog(save).slice(0, 2)).toEqual(["started", "finished"]); expect(save.logbook.find((e) => e.kind === "repair" && e.shipId === "ship-pa")).toBeTruthy(); }); it("deadheads to the HQ when no job goes there", async () => { const test = await bookedPilot(); const away = testContract({ id: "moon:away", originId: "moon", destinationId: "mars" }); await putBoard(test.app, [away], test.clock.now, "moon"); await test.app.call("bookService", { shipId: "ship-pa", repair: true, upgradeId: null }); test.clock.now += 2 * HOUR; await test.app.call("getPlayerState", {}); const first = tripsOf(await readSave(test.app))[0]!; expect(first.record.contract).toBeNull(); expect(first.record.destinationId).toBe("earth"); }); it("still flies to booked service while the fleet is asleep, then waits", async () => { const test = await bookedPilot(); await putBoard(test.app, [], test.clock.now, "moon"); await edit(test.app, (save) => { // An automatic booking (not a manual decision), and the fleet has gone quiet. shipPa(save).booking = { repair: true, upgradeId: null, hqId: "earth", by: "auto", createdAt: test.clock.now, startedAt: null, waitingSince: null, }; save.company.lastManualDecisionAt = test.clock.now - 25 * HOUR; }); expect((await test.app.call("getPlayerState", {})).fleetSleep.asleep).toBe(true); test.clock.now += 12 * HOUR; await test.app.call("getPlayerState", {}); const save = await readSave(test.app); expect(tripsOf(save).map((d) => d.record.destinationId)).toEqual(["earth"]); expect(serviceLog(save)).toEqual(["started", "finished"]); expect(shipPa(save).locationId).toBe("earth"); }); it("works as usual when the booked HQ is out of reach", async () => { const test = await bookedPilot("earth"); // An HQ at Jupiter: a Courier can't get there from Earth in one leg. await edit(test.app, (save) => { save.bays.push({ id: "jupiter-bay", locationId: "jupiter-cloud-platform", createdAt: TEST_NOW, }); save.discoveredLocations.push({ locationId: "jupiter-cloud-platform", discoveredAt: TEST_NOW, }); }); const moon = testContract({ id: "earth:moon" }); await putBoard(test.app, [moon], test.clock.now, "earth"); const { ship } = await test.app.call("bookService", { shipId: "ship-pa", repair: true, upgradeId: null, hqId: "jupiter-cloud-platform", }); expect(ship.booking!.status).toBe("cant-reach"); test.clock.now += HOUR; await test.app.call("getPlayerState", {}); expect(tripsOf(await readSave(test.app))[0]!.record.contract?.id).toBe("earth:moon"); }); it("gives the same save whether the service trip is caught up at once or an hour at a time", async () => { const once = await bookedPilot(); await once.app.call("bookService", { shipId: "ship-pa", repair: true, upgradeId: null }); const pieces = await copyOf(once); once.clock.now += 8 * HOUR; await once.app.call("getPlayerState", {}); for (let hour = 0; hour < 8; hour++) { pieces.clock.now += HOUR; await pieces.app.call("getPlayerState", {}); } const a = await readSave(once.app); expect(serviceLog(a).slice(0, 2)).toEqual(["started", "finished"]); expect({ ...(await readSave(pieces.app)), lastTimeSeen: 0 }).toEqual({ ...a, lastTimeSeen: 0 }); }); });