164 lines
5.8 KiB
TypeScript
164 lines
5.8 KiB
TypeScript
import { content } from "@deadhead/content";
|
|
import {
|
|
dispatchResponseSchema,
|
|
errorResponseSchema,
|
|
shipResponseSchema,
|
|
type PlayerState,
|
|
} from "@deadhead/schema";
|
|
import {
|
|
effectiveStats,
|
|
MS_PER_HOUR,
|
|
realToGame,
|
|
routeDistance,
|
|
travelTimeGameHours,
|
|
upgradeCost,
|
|
} from "@deadhead/sim";
|
|
import { describe, expect, it } from "vitest";
|
|
import { setCredits, useTestDatabase } from "./db";
|
|
import {
|
|
call,
|
|
getState,
|
|
insertDispatch,
|
|
putBoard,
|
|
signUpOwner,
|
|
testContract,
|
|
useTestApp,
|
|
type Guest,
|
|
} from "./game";
|
|
|
|
const database = useTestDatabase();
|
|
const testApp = useTestApp(database);
|
|
|
|
const errorCode = (response: { json(): unknown }) =>
|
|
errorResponseSchema.parse(response.json()).error.code;
|
|
const freighterModel = content.shipModel("freighter")!;
|
|
const FAR_FUTURE = Date.parse("2200-01-01T00:00:00Z");
|
|
|
|
/** An owner whose avatar now crews a newly bought Freighter at the Earth HQ. */
|
|
async function ownerWithFreighter() {
|
|
const { app } = testApp();
|
|
const owner = await signUpOwner(app, database().db);
|
|
await setCredits(database().db, owner.playerId, freighterModel.price + 50_000);
|
|
const bought = await call(app, owner, "POST", "/api/ships", {
|
|
modelId: "freighter",
|
|
bayId: owner.earthBayId,
|
|
});
|
|
const freighter = shipResponseSchema.parse(bought.json()).ship;
|
|
await call(app, owner, "POST", `/api/pilots/${owner.avatarId}/unassign`, {});
|
|
await call(app, owner, "POST", `/api/pilots/${owner.avatarId}/assign`, {
|
|
shipId: freighter.id,
|
|
});
|
|
return { owner, freighter };
|
|
}
|
|
|
|
const upgrade = (owner: Guest, shipId: string, upgradeId: string) =>
|
|
call(testApp().app, owner, "POST", `/api/ships/${shipId}/upgrades`, { upgradeId });
|
|
const shipIn = (state: PlayerState, id: string) => state.ships.find((s) => s.id === id)!;
|
|
|
|
describe("starting an upgrade (Step 10I.5)", () => {
|
|
it("charges, raises the level, keeps the ship in the shop, then flies it faster", async () => {
|
|
const { app, clock } = testApp();
|
|
const { owner, freighter } = await ownerWithFreighter();
|
|
const before = (await getState(app, owner)).company.credits;
|
|
|
|
const response = await upgrade(owner, freighter.id, "engines");
|
|
expect(response.statusCode).toBe(200);
|
|
const until = clock.now + 10 * 60_000;
|
|
expect(shipResponseSchema.parse(response.json()).ship).toMatchObject({
|
|
upgrades: { engines: 1 },
|
|
inShopUntil: until,
|
|
shop: { upgradeId: "engines", level: 1, until },
|
|
});
|
|
const state = await getState(app, owner);
|
|
expect(state.company.credits).toBe(before - upgradeCost(freighterModel, 1, content.config));
|
|
expect(shipIn(state, freighter.id).stats.cruiseSpeedKmPerGameHour).toBeCloseTo(
|
|
freighterModel.cruiseSpeedKmPerGameHour * 1.1,
|
|
);
|
|
|
|
const job = testContract({ massTonnes: 60, expiresAt: FAR_FUTURE });
|
|
await putBoard(database().db, owner.playerId, [job], clock.now);
|
|
const dispatch = () =>
|
|
call(app, owner, "POST", "/api/dispatches", {
|
|
kind: "contract",
|
|
contractId: job.id,
|
|
shipId: freighter.id,
|
|
pilotId: owner.avatarId,
|
|
});
|
|
|
|
const refused = await dispatch();
|
|
expect(refused.statusCode).toBe(409);
|
|
expect(errorCode(refused)).toBe("ship-in-shop");
|
|
|
|
clock.now = until;
|
|
const flown = await dispatch();
|
|
expect(flown.statusCode).toBe(200);
|
|
const { dispatch: trip } = dispatchResponseSchema.parse(flown.json());
|
|
const distance = routeDistance(
|
|
content.location("earth")!,
|
|
content.location("moon")!,
|
|
realToGame(until, content.config),
|
|
);
|
|
const hours = travelTimeGameHours(
|
|
effectiveStats(freighterModel, { engines: 1 }, content),
|
|
distance,
|
|
job.massTonnes,
|
|
content.config,
|
|
);
|
|
expect(trip.arrivalAt - trip.departureAt).toBeCloseTo(
|
|
(hours * MS_PER_HOUR) / content.config.timeCompression,
|
|
-1,
|
|
);
|
|
});
|
|
|
|
it("refuses away from an HQ with not-at-hq", async () => {
|
|
const { app, clock } = testApp();
|
|
const owner = await signUpOwner(app, database().db);
|
|
await setCredits(database().db, owner.playerId, 50_000);
|
|
// Fly the Courier to the Moon, where the player has no bay.
|
|
const record = await insertDispatch(database().db, {
|
|
playerId: owner.playerId,
|
|
shipId: owner.shipId,
|
|
pilotId: owner.avatarId,
|
|
departAt: clock.now,
|
|
contract: testContract(),
|
|
});
|
|
clock.now = record.arrivalAt;
|
|
const credits = (await getState(app, owner)).company.credits;
|
|
const response = await upgrade(owner, owner.shipId, "cargo-bay");
|
|
expect(response.statusCode).toBe(409);
|
|
expect(errorCode(response)).toBe("not-at-hq");
|
|
expect((await getState(app, owner)).company.credits).toBe(credits);
|
|
});
|
|
|
|
it("refuses an unknown upgrade with not-found", async () => {
|
|
const owner = await signUpOwner(testApp().app, database().db);
|
|
expect((await upgrade(owner, owner.shipId, "warp-drive")).statusCode).toBe(404);
|
|
});
|
|
|
|
it("makes exactly one of two simultaneous upgrades the player can afford only one of", async () => {
|
|
const { app } = testApp();
|
|
const owner = await signUpOwner(app, database().db);
|
|
const courier = content.shipModel("courier")!;
|
|
await setCredits(database().db, owner.playerId, courier.price);
|
|
const second = shipResponseSchema.parse(
|
|
(
|
|
await call(app, owner, "POST", "/api/ships", {
|
|
modelId: "courier",
|
|
bayId: owner.earthBayId,
|
|
})
|
|
).json(),
|
|
).ship;
|
|
const cost = upgradeCost(courier, 1, content.config);
|
|
await setCredits(database().db, owner.playerId, cost + cost / 2);
|
|
|
|
const responses = await Promise.all([
|
|
upgrade(owner, owner.shipId, "engines"),
|
|
upgrade(owner, second.id, "engines"),
|
|
]);
|
|
expect(responses.map((r) => r.statusCode).sort()).toEqual([200, 409]);
|
|
const state = await getState(app, owner);
|
|
expect(state.company.credits).toBe(cost / 2);
|
|
expect(state.ships.filter((s) => s.upgrades.engines === 1)).toHaveLength(1);
|
|
});
|
|
});
|