226 lines
8.5 KiB
TypeScript
226 lines
8.5 KiB
TypeScript
import { content } from "@deadhead/content";
|
|
import { emergencyFuel, fuelPriceAt, tripFuel } from "@deadhead/sim";
|
|
import { describe, expect, it } from "vitest";
|
|
import { fullSave } from "../../schema/test/save-fixture";
|
|
import { shipState } from "../src/game/state";
|
|
import { encodeSave } from "../src/index";
|
|
import {
|
|
call,
|
|
edit,
|
|
getState,
|
|
putBoard,
|
|
readSave,
|
|
setCredits,
|
|
signUp,
|
|
signUpOwner,
|
|
testContract,
|
|
useTestApp,
|
|
} from "./support";
|
|
|
|
const testApp = useTestApp();
|
|
const tank = (id: string) => content.shipModel(id)!.tankCapacityFu;
|
|
const price = (id: string) => fuelPriceAt(content.location(id)!, content.config);
|
|
|
|
describe("fuel tanks in the engine (Step 15.4)", () => {
|
|
it("delivers a Market ship with a full tank, bought at the bay's price, on Manual (CR-018)", async () => {
|
|
const { app } = testApp();
|
|
const owner = await signUpOwner(app);
|
|
await setCredits(app, content.shipModel("freighter")!.price);
|
|
const { ship } = await app.call("buyShip", { modelId: "freighter", bayId: owner.earthBayId });
|
|
expect(ship).toMatchObject({
|
|
fuelFu: tank("freighter"),
|
|
fuelAvgPrice: price("earth"),
|
|
refuelPolicy: "manual",
|
|
});
|
|
// The tank was included in the price.
|
|
expect((await getState(app)).company.credits).toBe(0);
|
|
});
|
|
|
|
it("hands over the deal's ships full, at Earth's price, on Manual (CR-018, decisions 0.5.111, 0.5.112)", async () => {
|
|
const { app } = testApp();
|
|
await signUp(app);
|
|
await edit(app, (save) => {
|
|
save.player.tutorialRunsCompleted = content.config.tutorialRunsBeforeDeal;
|
|
save.player.loanerLocationId = "earth";
|
|
});
|
|
const state = await app.call("acceptDeal", {});
|
|
for (const ship of state.ships) {
|
|
expect(ship.fuelFu, ship.modelId).toBeCloseTo(tank(ship.modelId));
|
|
expect(ship.fuelAvgPrice).toBe(price("earth"));
|
|
expect(ship.refuelPolicy).toBe("manual");
|
|
}
|
|
});
|
|
|
|
it("burns the leg from the tank and, switched to Auto, tops up on landing at the local price", async () => {
|
|
const { app, clock } = testApp();
|
|
const owner = await signUpOwner(app);
|
|
await setCredits(app, 10_000);
|
|
await app.call("setRefuelPolicy", { shipId: owner.shipId, policy: "auto" });
|
|
const contract = testContract();
|
|
await putBoard(app, [contract]);
|
|
const { dispatch } = await app.call("createDispatch", {
|
|
kind: "contract",
|
|
contractId: contract.id,
|
|
shipId: owner.shipId,
|
|
pilotId: owner.avatarId,
|
|
});
|
|
expect((await getState(app)).company.credits).toBe(10_000);
|
|
|
|
clock.now = dispatch.arrivalAt;
|
|
const state = await getState(app);
|
|
const [summary] = state.summaries;
|
|
const bought = summary!.topUp!;
|
|
expect(bought.locationId).toBe("moon");
|
|
expect(bought.boughtFu).toBeCloseTo(dispatch.fuelBurnedFu);
|
|
expect(bought.cost).toBe(Math.round(dispatch.fuelBurnedFu * price("moon")));
|
|
expect(state.company.credits).toBe(10_000 + summary!.credits - bought.cost);
|
|
expect(state.ships[0]!.fuelFu).toBeCloseTo(tank("courier"));
|
|
});
|
|
|
|
it("leaves the tank as it landed, and spends nothing on fuel, on Manual (the default)", async () => {
|
|
const { app, clock } = testApp();
|
|
const owner = await signUpOwner(app);
|
|
await setCredits(app, 10_000);
|
|
expect((await readSave(app)).ships.find((s) => s.id === owner.shipId)!.refuelPolicy).toBe(
|
|
"manual",
|
|
);
|
|
const contract = testContract();
|
|
await putBoard(app, [contract]);
|
|
const { dispatch } = await app.call("createDispatch", {
|
|
kind: "contract",
|
|
contractId: contract.id,
|
|
shipId: owner.shipId,
|
|
pilotId: owner.avatarId,
|
|
});
|
|
clock.now = dispatch.arrivalAt;
|
|
const state = await getState(app);
|
|
expect(state.summaries[0]!.topUp).toBeNull();
|
|
expect(state.company.credits).toBe(10_000 + state.summaries[0]!.credits);
|
|
const saved = (await readSave(app)).ships.find((s) => s.id === owner.shipId)!;
|
|
expect(saved.fuelFu).toBeCloseTo(tank("courier") - dispatch.fuelBurnedFu);
|
|
});
|
|
});
|
|
|
|
/** The fixture as a version-1 save, before fuel tanks (CR-015). */
|
|
function versionOneSave() {
|
|
const old = JSON.parse(JSON.stringify(fullSave())) as {
|
|
saveVersion: number;
|
|
ships: Record<string, unknown>[];
|
|
dispatches: { record: Record<string, unknown>; resolution: Record<string, unknown> | null }[];
|
|
};
|
|
old.saveVersion = 1;
|
|
for (const ship of old.ships) {
|
|
delete ship.fuelFu;
|
|
delete ship.fuelAvgPrice;
|
|
delete ship.refuelPolicy;
|
|
}
|
|
for (const dispatch of old.dispatches) {
|
|
delete dispatch.record.fuelBurnedFu;
|
|
if (dispatch.resolution) delete dispatch.resolution.topUp;
|
|
}
|
|
return old;
|
|
}
|
|
|
|
describe("fuel commands (Step 15.6)", () => {
|
|
it("opens an older save with every ship full, on Manual (CR-018)", async () => {
|
|
const { app } = testApp();
|
|
const { state } = await app.call("importSave", {
|
|
code: await encodeSave(versionOneSave() as never),
|
|
});
|
|
for (const ship of state.ships) {
|
|
expect(ship.fuelFu, ship.name).toBeCloseTo(ship.stats.tankCapacityFu);
|
|
expect(ship.refuelPolicy).toBe("manual");
|
|
}
|
|
});
|
|
|
|
it("refuels where the ship is, charging the local price, and keeps the average", async () => {
|
|
const { app } = testApp();
|
|
const owner = await signUpOwner(app);
|
|
await setCredits(app, 1_000);
|
|
await edit(app, (save) => {
|
|
Object.assign(
|
|
save.ships.find((s) => s.id === owner.shipId)!,
|
|
{
|
|
fuelFu: 10,
|
|
fuelAvgPrice: 11,
|
|
},
|
|
);
|
|
});
|
|
const { ship } = await app.call("refuelShip", { shipId: owner.shipId, amountFu: 20 });
|
|
expect(ship.fuelFu).toBeCloseTo(30);
|
|
// 10 fu bought at 11 cr/fu, then 20 fu at Earth's price.
|
|
expect(ship.fuelAvgPrice).toBeCloseTo((10 * 11 + 20 * price("earth")) / 30);
|
|
expect((await getState(app)).company.credits).toBe(1_000 - Math.round(20 * price("earth")));
|
|
|
|
const filled = await app.call("refuelShip", { shipId: owner.shipId });
|
|
expect(filled.ship.fuelFu).toBeCloseTo(tank("courier"));
|
|
await expect(app.call("refuelShip", { shipId: owner.shipId })).rejects.toMatchObject({
|
|
code: "tank-full",
|
|
});
|
|
});
|
|
|
|
it("refuses to refuel without the credits, changing nothing", async () => {
|
|
const { app } = testApp();
|
|
const owner = await signUpOwner(app);
|
|
await setCredits(app, 0);
|
|
await edit(app, (save) => {
|
|
save.ships.find((s) => s.id === owner.shipId)!.fuelFu = 1;
|
|
});
|
|
const before = await readSave(app);
|
|
const response = await call(app, owner, "POST", `/api/ships/${owner.shipId}/refuel`, {});
|
|
expect(response.statusCode).toBe(409);
|
|
expect(response.json()).toMatchObject({ error: { code: "insufficient-funds" } });
|
|
expect(await readSave(app)).toEqual(before);
|
|
});
|
|
|
|
it("switches a ship between Auto top-up and Manual", async () => {
|
|
const { app } = testApp();
|
|
const owner = await signUpOwner(app);
|
|
const manual = await app.call("setRefuelPolicy", { shipId: owner.shipId, policy: "manual" });
|
|
expect(manual.ship.refuelPolicy).toBe("manual");
|
|
const auto = await app.call("setRefuelPolicy", { shipId: owner.shipId, policy: "auto" });
|
|
expect(auto.ship.refuelPolicy).toBe("auto");
|
|
});
|
|
|
|
it("refuses emergency fuel when it isn't needed", async () => {
|
|
const { app } = testApp();
|
|
const owner = await signUpOwner(app);
|
|
await expect(app.call("orderEmergencyFuel", { shipId: owner.shipId })).rejects.toMatchObject({
|
|
code: "emergency-fuel-unavailable",
|
|
});
|
|
});
|
|
|
|
it("adds emergency fuel and exactly its cost to the loan", async () => {
|
|
const { app } = testApp();
|
|
const owner = await signUpOwner(app);
|
|
await setCredits(app, 0);
|
|
await edit(app, (save) => {
|
|
save.ships.find((s) => s.id === owner.shipId)!.fuelFu = 1;
|
|
save.company.loanBalance = 5_000;
|
|
});
|
|
const save = await readSave(app);
|
|
const offer = emergencyFuel(
|
|
{
|
|
ship: shipState(save.ships.find((s) => s.id === owner.shipId)!),
|
|
credits: 0,
|
|
discoveredIds: new Set(save.discoveredLocations.map((d) => d.locationId)),
|
|
},
|
|
content,
|
|
)!;
|
|
const needed = tripFuel(
|
|
content.shipModel("courier")!,
|
|
content.location("earth")!,
|
|
content.location("moon")!,
|
|
content.config,
|
|
);
|
|
expect(offer.amountFu).toBeCloseTo(needed - 1);
|
|
expect(offer.cost).toBe(Math.round((needed - 1) * price("earth") * 2.5));
|
|
|
|
const { ship } = await app.call("orderEmergencyFuel", { shipId: owner.shipId });
|
|
expect(ship.fuelFu).toBeCloseTo(needed);
|
|
const state = await getState(app);
|
|
expect(state.company.loanBalance).toBe(5_000 + offer.cost);
|
|
expect(state.company.credits).toBe(0);
|
|
});
|
|
});
|