deadhead-space-freight-tycoon/packages/engine/test/shop-rest.test.ts

162 lines
6.3 KiB
TypeScript

import { content } from "@deadhead/content";
import { MS_PER_MINUTE, upgradeShopMs } from "@deadhead/sim";
import { describe, expect, it } from "vitest";
import { turnaroundMs } from "../src/game/independent";
import {
addPilot,
addShip,
edit,
newTestApp,
readSave,
setCredits,
signUpOwner,
type TestApp,
} from "./support";
const SHOP_MS = upgradeShopMs(1, content.config);
const LIMIT = content.config.dutyLimitGameHours;
const pilotIn = async (test: TestApp, id: string) =>
(await test.app.call("getPlayerState", {})).pilots.find((p) => p.id === id)!;
/** An owner with a Courier at Earth crewed by `p1`, one trip short of the duty limit. */
async function tiredPilot(test: TestApp) {
await signUpOwner(test.app);
await setCredits(test.app, 100_000);
await addShip(test.app, { id: "f1", modelId: "courier", name: "Wren" });
await addPilot(test.app, { id: "p1", name: "Ada", assignedShipId: "f1", dutyHours: LIMIT - 0.5 });
}
const upgrade = (test: TestApp) =>
test.app.call("startUpgrade", { shipId: "f1", upgradeId: "engines" });
describe("shop time counts as rest (Step 23.2, decision 0.5.135)", () => {
it("brings a pilot near the duty limit out of the shop with no duty, ready to fly", async () => {
const test = newTestApp();
await tiredPilot(test);
const startedAt = test.clock.now;
await upgrade(test);
expect((await pilotIn(test, "p1")).restingInShopUntil).toBe(startedAt + SHOP_MS);
test.clock.now = startedAt + SHOP_MS;
const pilot = await pilotIn(test, "p1");
expect(pilot.dutyHours).toBe(0);
expect(pilot.resting).toBe(false);
expect(pilot.restingInShopUntil).toBeNull();
const { dispatch } = await test.app.call("createDispatch", {
kind: "reposition",
shipId: "f1",
pilotId: "p1",
destinationId: "moon",
});
expect(dispatch.status).toBe("in-flight");
});
it("keeps the duty of a pilot who left the ship and flew another one", async () => {
const test = newTestApp();
await tiredPilot(test);
await addShip(test.app, { id: "f2", modelId: "courier", name: "Kite" });
// Fresh enough that the Moon trip adds duty without forcing a rest.
await edit(test.app, (save) => {
save.pilots.find((p) => p.id === "p1")!.dutyHours = 2;
});
const startedAt = test.clock.now;
await upgrade(test);
await test.app.call("unassignPilot", { pilotId: "p1" });
expect((await pilotIn(test, "p1")).restingInShopUntil).toBeNull();
await test.app.call("assignPilot", { pilotId: "p1", shipId: "f2" });
await test.app.call("createDispatch", {
kind: "reposition",
shipId: "f2",
pilotId: "p1",
destinationId: "moon",
});
test.clock.now = startedAt + SHOP_MS + 60 * MS_PER_MINUTE;
const pilot = await pilotIn(test, "p1");
// At the Moon with the trip added to their duty, not reset by the shop.
expect(pilot.status).toBe("idle");
expect(pilot.dutyHours).toBeGreaterThan(2);
expect((await readSave(test.app)).ships.find((s) => s.id === "f1")!.shopPilotId).toBeNull();
});
it("doesn't reset a pilot who left the ship and came back during the upgrade", async () => {
const test = newTestApp();
await tiredPilot(test);
const startedAt = test.clock.now;
await upgrade(test);
await test.app.call("unassignPilot", { pilotId: "p1" });
await test.app.call("assignPilot", { pilotId: "p1", shipId: "f1" });
test.clock.now = startedAt + SHOP_MS;
expect((await pilotIn(test, "p1")).dutyHours).toBe(LIMIT - 0.5);
});
it("frees a pilot whose rest would have ended after the upgrade, and leaves a shorter rest alone", async () => {
const test = newTestApp();
await tiredPilot(test);
const startedAt = test.clock.now;
await addShip(test.app, { id: "f2", modelId: "courier", name: "Kite" });
await addPilot(test.app, { id: "p2", name: "Bo", assignedShipId: "f2" });
await edit(test.app, (save) => {
save.pilots.find((p) => p.id === "p1")!.restEndsAt = startedAt + 3 * SHOP_MS;
save.pilots.find((p) => p.id === "p2")!.restEndsAt = startedAt + SHOP_MS / 2;
});
await upgrade(test);
await test.app.call("startUpgrade", { shipId: "f2", upgradeId: "engines" });
// The short rest runs its course alongside the upgrade.
test.clock.now = startedAt + SHOP_MS / 2;
expect((await pilotIn(test, "p2")).resting).toBe(false);
expect((await pilotIn(test, "p1")).resting).toBe(true);
test.clock.now = startedAt + SHOP_MS;
const p1 = await pilotIn(test, "p1");
expect(p1.resting).toBe(false);
expect(p1.dutyHours).toBe(0);
});
it("brings an Independent pilot out rested, flying after the turnaround", async () => {
const test = newTestApp();
await tiredPilot(test);
await test.app.call("buyDispatchOffice", {});
await edit(test.app, (save) => {
save.pilots.find((p) => p.id === "p1")!.restEndsAt = test.clock.now + 3 * SHOP_MS;
});
const startedAt = test.clock.now;
await upgrade(test);
await test.app.call("setPilotMode", { pilotId: "p1", mode: "independent" });
test.clock.now = startedAt + SHOP_MS;
await test.app.call("getPlayerState", {});
const save = await readSave(test.app);
const p1 = save.pilots.find((p) => p.id === "p1")!;
expect(p1.dutyHours).toBe(0);
expect(p1.restEndsAt).toBe(startedAt + SHOP_MS);
expect(p1.nextDecisionAt).toBe(startedAt + SHOP_MS + turnaroundMs());
});
it("gives the same save caught up at once as in live play", async () => {
const once = newTestApp();
await tiredPilot(once);
await once.app.call("buyDispatchOffice", {});
await upgrade(once);
await once.app.call("setPilotMode", { pilotId: "p1", mode: "independent" });
const { code } = await once.app.call("exportSave", {});
const live = newTestApp();
live.clock.now = once.clock.now;
await live.app.call("importSave", { code });
const end = once.clock.now + 3 * 60 * MS_PER_MINUTE;
once.clock.now = end;
await once.app.call("getPlayerState", {});
while (live.clock.now < end) {
live.clock.now = Math.min(end, live.clock.now + 30_000);
await live.app.call("getPlayerState", {});
}
const a = await readSave(once.app);
const b = await readSave(live.app);
expect(a.dispatches.some((d) => d.independent)).toBe(true);
expect({ ...b, lastTimeSeen: 0 }).toEqual({ ...a, lastTimeSeen: 0 });
});
});