166 lines
6.6 KiB
TypeScript
166 lines
6.6 KiB
TypeScript
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<TestApp> {
|
|
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, empty, then waits", async () => {
|
|
const test = await bookedPilot();
|
|
// A paid job home is on the board, but asleep the fleet earns nothing (CR-024).
|
|
const home = testContract({ id: "moon:home", originId: "moon", destinationId: "earth" });
|
|
await putBoard(test.app, [home], 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(tripsOf(save)[0]!.record.contract).toBeNull();
|
|
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 });
|
|
});
|
|
});
|