import { content } from "@deadhead/content"; import type { Contract, Pilot, Ship } from "@deadhead/schema"; import { contractTripGameHours, effectiveStats } from "@deadhead/sim"; import { screen, within } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { useClock } from "../src/clock/clock"; import { formatGameHours } from "../src/format"; import { jobGameHours } from "../src/game"; import { pilotExtras } from "./fixtures"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const job = (over: Partial = {}): Contract => ({ id: "j1", originId: "earth", destinationId: "moon", cargoTypeId: "standard", goodId: "consumer-goods", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: 0, massTonnes: 12, payout: 1_500, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: NOW, expiresAt: NOW + 30 * 60_000, firstContact: false, ...over, }); const ship = (id: string, modelId: string, locationId: string, pilotId: string | null): Ship => ({ id, modelId, name: id, status: "idle", locationId, dispatchId: null, pilotId, lastArrivalAt: 0, ...shipExtras(modelId), }); const pilot = (id: string, shipId: string): Pilot => ({ ...samplePlayerState().pilots[0]!, id, name: id, isAvatar: id === "avatar", assignedShipId: shipId, locationId: null, ...pilotExtras, }); const hoursFor = (modelId: string, contract: Contract) => contractTripGameHours( effectiveStats(content.shipModel(modelId)!, {}, content), contract, content, ); describe("job length on the board", () => { beforeEach(() => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); }); afterEach(() => vi.useRealTimers()); it("shows the game hours beside the weight and distance", async () => { const contract = job(); const state = samplePlayerState({ tutorial: null, bays: [{ id: "b", locationId: "earth" }], ships: [ship("s1", "courier", "earth", "avatar")], pilots: [pilot("avatar", "s1")], }); renderApp( fakeApi({ getPlayerState: () => state, getContractBoard: () => ({ contracts: [contract], periodEndsAt: NOW + 30 * 60_000 }), }), "/contracts?location=earth", ); const hours = formatGameHours(hoursFor("courier", contract)); const time = await screen.findByLabelText(`Takes ${hours} of game time`); const line = time.parentElement!; expect(line).toHaveTextContent(`Pacific Orbital Logistics · 12 t · 0.38M km · ${hours}`); expect(within(line).getByText(/12 t/)).toBeInTheDocument(); }); it("times the job for the fastest ship that could take it, preferring one at the origin", () => { const contract = job(); const ships = [ ship("slow-here", "freighter", "earth", "p1"), ship("fast-away", "clipper", "moon", "p2"), ]; const pilots = [pilot("p1", "slow-here"), pilot("p2", "fast-away")]; expect(jobGameHours({ ships, pilots }, contract)).toBeCloseTo(hoursFor("freighter", contract)); // Nobody at Earth: the fastest anywhere. const away = ships.map((s) => ({ ...s, locationId: "moon" })); expect(jobGameHours({ ships: away, pilots }, contract)).toBeCloseTo( Math.min(hoursFor("freighter", contract), hoursFor("clipper", contract)), ); // No crewed ship that could take it: no time shown. expect(jobGameHours({ ships, pilots: [] }, contract)).toBeNull(); }); });