112 lines
4.4 KiB
TypeScript
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);
|
|
});
|
|
});
|