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[]; dispatches: { record: Record; resolution: Record | 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); }); });