deadhead-space-freight-tycoon/apps/web/test/fixed-map.test.tsx

122 lines
4.4 KiB
TypeScript

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 the Moon, which rides with Earth", () => {
const scene = buildScene(samplePlayerState(), NOW);
for (const loc of scene.locations) {
if (loc.id === "moon") expect(loc.orbitRadius).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 · 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);
});
});