import { content } from "@deadhead/content"; import type { Contract } from "@deadhead/schema"; import { screen, within } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { afterEach, describe, expect, it, vi } from "vitest"; import { useClock } from "../src/clock/clock"; import { layoutScene } from "../src/map/layout"; import { reachersByOrigin } from "../src/map/reach"; import { buildScene } from "../src/map/scene"; import { fakeApi, renderApp, samplePlayerState, shipExtras } from "./render"; const NOW = Date.parse("2100-01-01T00:00:00Z"); const DAY = 24 * 3_600_000; const view = { width: 1280, height: 720 }; const camera = { zoom: 1.5, panX: 40, panY: -25 }; /** Date words the fixed map has no use for (CR-015 §2). */ const DATED = /\bnow\b|right now|other dates|on some dates|closest|farthest|opposition/i; describe("the fixed map (Step 15.8, decision 0.5.93)", () => { it("keeps every body in the same place on screen as the clock advances", () => { const state = samplePlayerState(); const at = (t: number) => layoutScene(buildScene(state, t), view, camera).locations.map((l) => ({ id: l.id, center: l.center, })); const first = at(NOW); // Real minutes, game years: before CR-015 the planets moved over these. for (const t of [NOW + 60_000, NOW + 30 * DAY, NOW + 400 * DAY]) { expect(at(t)).toEqual(first); } }); it("draws an orbit ring for every body but those riding with a parent (Moon, Io, Europa)", () => { const scene = buildScene(samplePlayerState(), NOW); const riders = ["moon", "io-sulfur-works", "europa-ice-station"]; for (const loc of scene.locations) { if (riders.includes(loc.id)) expect(loc.orbitRadius, loc.id).toBeNull(); else expect(loc.orbitRadius, loc.id).toBeGreaterThan(0); } }); }); describe("reach wording without dates (Step 15.8)", () => { afterEach(() => vi.useRealTimers()); it("says only where each ship can fly from, in the Market", async () => { renderApp( fakeApi({ getPlayerState: () => samplePlayerState({ bays: [{ id: "b", locationId: "earth" }] }), }), "/market", ); const clipper = (await screen.findByRole("button", { name: "Buy Clipper" })).closest("li")!; const reach = within(clipper).getByLabelText("Reach").textContent ?? ""; expect(reach).toBe( "Reach From Earth: Moon, Mars, Venus Cloud City · By hopping: Ceres Station", ); expect(reach).not.toMatch(DATED); }); it("names who can reach an undiscovered place, per base, on the map", () => { const lines = reachersByOrigin( samplePlayerState({ bays: [{ id: "b", locationId: "earth" }] }), "mars", ); expect(lines).toEqual([ { originId: "earth", reachers: ["Clipper", "Freighter", "Bulk Hauler"] }, ]); }); it("gives an out-of-range pair its reach without a date", async () => { vi.useFakeTimers({ toFake: ["Date", "setInterval", "clearInterval"] }); vi.setSystemTime(NOW); useClock.setState({ offsetMs: 0, synced: true, now: NOW }); const marsJob: Contract = { id: "c-mars", originId: "earth", destinationId: "mars", cargoTypeId: "standard", goodId: "consumer-goods", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: 0, massTonnes: 5, payout: 50_000, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: NOW, expiresAt: NOW + 3_600_000, firstContact: false, }; const state = samplePlayerState({ tutorial: null, ships: [ { id: "s1", name: "Dusty Mule", modelId: "courier", status: "idle", locationId: "earth", dispatchId: null, lastArrivalAt: 0, pilotId: "avatar", ...shipExtras("courier"), }, ], pilots: [{ ...samplePlayerState().pilots[0]!, assignedShipId: "s1" }], locations: content.locations.map((l) => ({ id: l.id, discovered: true })), }); renderApp( fakeApi({ getPlayerState: () => state, getContractBoard: () => ({ contracts: [marsJob], periodEndsAt: NOW + 3_600_000 }), }), "/contracts", ); await userEvent.click(await screen.findByRole("button", { name: "Dispatch" })); const problems = within(screen.getByRole("dialog")).getByLabelText("Problems"); expect(problems).toHaveTextContent(/Out of range \(Courier reaches \d+M km from Earth\)/); expect(problems.textContent).not.toMatch(DATED); }); });