deadhead-space-freight-tycoon/packages/engine/test/ported/veteran.test.ts

112 lines
4.4 KiB
TypeScript

// Ported from apps/server/test (CR-014 Step 14.5): now runs on the engine.
import { content } from "@deadhead/content";
import {
dispatchResponseSchema,
errorResponseSchema,
playerStateSchema,
type PlayerState,
} from "@deadhead/schema";
import { pilotShare } from "@deadhead/sim";
import { describe, expect, it } from "vitest";
import {
useTestApp,
signUp,
call,
getState,
setCredits,
testContract,
putBoard,
flyTutorialRun,
type Guest,
} from "../support";
const testApp = useTestApp();
const errorCode = (response: { json(): unknown }) =>
errorResponseSchema.parse(response.json()).error.code;
const veteranOf = (state: PlayerState) => state.pilots.find((p) => p.isVeteran)!;
const avatarOf = (state: PlayerState) => state.pilots.find((p) => p.isAvatar)!;
const FAR_FUTURE = Date.parse("2200-01-01T00:00:00Z");
/** A guest who flies the tutorial runs and accepts the deal. */
async function dealt(): Promise<{ guest: Guest; state: PlayerState }> {
const guest = await signUp(testApp().app);
for (let run = 0; run < content.config.tutorialRunsBeforeDeal; run++) {
await flyTutorialRun(testApp(), guest);
}
const response = await call(testApp().app, guest, "POST", "/api/deal/accept", {});
expect(response.statusCode).toBe(200);
return { guest, state: playerStateSchema.parse(response.json()) };
}
describe("the veteran comes with the deal (Step 10J.4)", () => {
it("adds the veteran crewing the Clipper at Earth, level 2 with Expensive Cargo 1 (CR-013)", async () => {
const { state } = await dealt();
expect(state.pilots).toHaveLength(2);
const [courier, clipper] = state.ships;
expect(clipper!.modelId).toBe("clipper");
expect(avatarOf(state)).toMatchObject({ assignedShipId: courier!.id, locationId: "earth" });
const spentPoints = 1;
expect(veteranOf(state)).toMatchObject({
name: content.veteranPilot.name,
isAvatar: false,
isVeteran: true,
level: 2,
skills: { "expensive-cargo": 1 },
skillPointsAvailable: 2 * content.config.skillPointsPerLevel - spentPoints,
assignedShipId: clipper!.id,
locationId: "earth",
status: "idle",
});
});
it("creates no veteran on a refused or repeated accept", async () => {
const guest = await signUp(testApp().app);
const early = await call(testApp().app, guest, "POST", "/api/deal/accept", {});
expect(errorCode(early)).toBe("deal-not-available");
expect((await getState(testApp().app, guest)).pilots).toHaveLength(1);
const { guest: owner } = await dealt();
const again = await call(testApp().app, owner, "POST", "/api/deal/accept", {});
expect(again.statusCode).toBe(409);
const pilots = (await getState(testApp().app, owner)).pilots;
expect(pilots.filter((p) => p.isVeteran)).toHaveLength(1);
});
it("dispatches the Courier and the Clipper at once, paying the veteran a profit share", async () => {
const { app, clock } = testApp();
const { guest, state } = await dealt();
const [courier, clipper] = state.ships;
const veteran = veteranOf(state);
await setCredits(testApp().app, 10_000);
const forCourier = testContract({ payout: 2000, expiresAt: FAR_FUTURE });
const forClipper = testContract({ payout: 2000, expiresAt: FAR_FUTURE });
await putBoard(testApp().app, [forCourier, forClipper], clock.now);
const send = (contractId: string, shipId: string, pilotId: string) =>
call(app, guest, "POST", "/api/dispatches", {
kind: "contract",
contractId,
shipId,
pilotId,
});
const avatarRun = await send(forCourier.id, courier!.id, guest.avatarId);
const veteranRun = await send(forClipper.id, clipper!.id, veteran.id);
expect(avatarRun.statusCode).toBe(200);
expect(veteranRun.statusCode).toBe(200);
expect((await getState(app, guest)).activeDispatches).toHaveLength(2);
const { dispatch } = dispatchResponseSchema.parse(veteranRun.json());
clock.now = Math.max(
dispatch.arrivalAt,
dispatchResponseSchema.parse(avatarRun.json()).dispatch.arrivalAt,
);
const after = await getState(app, guest);
const summary = after.summaries.find((s) => s.dispatchId === dispatch.id)!;
const pay = pilotShare(forClipper.payout, dispatch.fuelCost, content.config);
expect(pay).toBeGreaterThan(0);
expect(summary.pilotPay).toBe(pay);
expect(summary.credits + summary.loanRepayment).toBe(forClipper.payout - pay);
});
});