148 lines
5.5 KiB
TypeScript
148 lines
5.5 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
||
import { buildAwaySummary, flightNet, type AwayProblem, type LogbookEntry } from "../src/index";
|
||
|
||
const FROM = 1_000_000;
|
||
const HOUR = 3_600_000;
|
||
let n = 0;
|
||
|
||
const flight = (pilotId: string, payout: number, over: Partial<LogbookEntry> = {}): LogbookEntry =>
|
||
({
|
||
id: `f${n++}`,
|
||
at: FROM + 60_000 + n,
|
||
pilotId,
|
||
shipId: `ship-${pilotId}`,
|
||
kind: "flight",
|
||
dispatchId: `d${n}`,
|
||
deadhead: false,
|
||
tutorial: false,
|
||
independent: true,
|
||
originId: "earth",
|
||
destinationId: "moon",
|
||
goodId: "consumer-goods",
|
||
massTonnes: 6,
|
||
shipper: "Pacific Orbital Logistics",
|
||
payout,
|
||
pilotPay: Math.round(payout / 5),
|
||
fuelCost: 100,
|
||
loanRepayment: 0,
|
||
xp: 10,
|
||
scriptedId: null,
|
||
...over,
|
||
}) as LogbookEntry;
|
||
|
||
const other = (entry: Partial<LogbookEntry> & { kind: LogbookEntry["kind"] }): LogbookEntry =>
|
||
({ id: `e${n++}`, at: FROM + 30_000 + n, pilotId: "p0", shipId: null, ...entry }) as LogbookEntry;
|
||
|
||
const pilots = (count: number) =>
|
||
Array.from({ length: count }, (_, i) => ({ id: `p${i}`, name: `Pilot ${i}`, now: "At Earth" }));
|
||
|
||
const summarise = (
|
||
entries: LogbookEntry[],
|
||
extra: { pilots?: number; problems?: AwayProblem[] } = {},
|
||
) =>
|
||
buildAwaySummary({
|
||
from: FROM,
|
||
to: FROM + 10 * HOUR,
|
||
entries,
|
||
pilots: pilots(extra.pilots ?? 3),
|
||
problems: extra.problems ?? [],
|
||
});
|
||
|
||
describe("the away summary builder (Step 25.2, decision 0.5.140)", () => {
|
||
it("shows 5 pilot lines by net profit and sums the other 45 into one line", () => {
|
||
// Pilot i earns (i + 1) × 1,000 per job, two jobs each.
|
||
const entries = pilots(50).flatMap((p, i) => [
|
||
flight(p.id, (i + 1) * 1_000),
|
||
flight(p.id, (i + 1) * 1_000),
|
||
]);
|
||
const summary = summarise(entries, { pilots: 50 });
|
||
expect(summary.pilots.map((l) => l.pilotId)).toEqual(["p49", "p48", "p47", "p46", "p45"]);
|
||
expect(summary.pilots[0]).toMatchObject({ deliveries: 2, now: "At Earth", leveledUp: false });
|
||
const net = (id: string) =>
|
||
entries
|
||
.filter((e) => e.pilotId === id && e.kind === "flight")
|
||
.reduce((sum, e) => sum + flightNet(e as never), 0);
|
||
expect(summary.pilots[0]!.net).toBe(net("p49"));
|
||
const rest = pilots(45).map((p) => net(p.id));
|
||
expect(summary.morePilots).toEqual({ count: 45, net: rest.reduce((a, b) => a + b, 0) });
|
||
expect(summary.idlePilots).toBe(0);
|
||
expect(summary.direct).toBeNull();
|
||
});
|
||
|
||
it("puts highlights in priority order, at most 3, with the rest counted", () => {
|
||
const problem: AwayProblem = { kind: "low-fuel", shipId: "s1", locationId: "io-sulfur-works" };
|
||
const all = [
|
||
flight("p0", 8_420, { scriptedId: "unmarked-crates" }),
|
||
flight("p1", 2_000),
|
||
other({ kind: "goal-ready", goalId: "first-hire" }),
|
||
other({ kind: "level-up", level: 4, skillPoints: 1 }),
|
||
other({ kind: "discovery", locationId: "mars" }),
|
||
];
|
||
const summary = summarise(all, { problems: [problem] });
|
||
expect(summary.highlights.map((h) => h.kind)).toEqual(["discovery", "level-up", "goal-ready"]);
|
||
expect(summary.moreHighlights).toBe(3);
|
||
|
||
const fewer = summarise(all.slice(0, 2), { problems: [problem] });
|
||
expect(fewer.highlights).toEqual([
|
||
{ kind: "scripted", scriptedId: "unmarked-crates" },
|
||
{
|
||
kind: "best-job",
|
||
goodId: "consumer-goods",
|
||
destinationId: "moon",
|
||
payout: 8_420,
|
||
pilotId: "p0",
|
||
},
|
||
{ kind: "problem", problem },
|
||
]);
|
||
expect(fewer.moreHighlights).toBe(0);
|
||
});
|
||
|
||
it("adds up the totals from the entries", () => {
|
||
const entries = [
|
||
flight("p0", 3_000, { loanRepayment: 300 }),
|
||
flight("p1", 5_000),
|
||
flight("p1", 0, { deadhead: true, goodId: null, pilotPay: 0, fuelCost: 80 }),
|
||
other({ kind: "discovery", locationId: "mars" }),
|
||
other({ kind: "refuel", locationId: "earth", boughtFu: 10, cost: 90, source: "pilot" }),
|
||
];
|
||
const summary = summarise(entries);
|
||
const flights = entries.filter((e) => e.kind === "flight");
|
||
expect(summary.totals).toEqual({
|
||
net: flights.reduce((sum, e) => sum + flightNet(e as never), 0),
|
||
deliveries: 2,
|
||
discoveries: 1,
|
||
loanRepaid: 300,
|
||
});
|
||
// Fuel bought counts when it's used, not when it's bought (decision 0.5.142).
|
||
expect(summary.totals.net).toBe(3_000 - 600 - 100 - 300 + (5_000 - 1_000 - 100) - 80);
|
||
// Pilot 2 flew nothing.
|
||
expect(summary.idlePilots).toBe(1);
|
||
expect(summary.pilots.map((l) => [l.pilotId, l.deliveries])).toEqual([
|
||
["p1", 1],
|
||
["p0", 1],
|
||
]);
|
||
});
|
||
|
||
it("lists a short absence's events directly: 3 or fewer", () => {
|
||
const two = [flight("p0", 410), other({ kind: "level-up", level: 2, skillPoints: 1 })];
|
||
expect(summarise(two).direct!.map((e) => e.kind)).toEqual(["level-up", "flight"]);
|
||
const four = [flight("p0", 1), flight("p0", 2), flight("p1", 3), flight("p1", 4)];
|
||
expect(summarise(four).direct).toBeNull();
|
||
// Refuels and rests aren't events.
|
||
const quiet = [
|
||
flight("p0", 1),
|
||
other({ kind: "rest", until: FROM + HOUR }),
|
||
other({ kind: "refuel", locationId: "earth", boughtFu: 1, cost: 5, source: "bought" }),
|
||
];
|
||
expect(summarise(quiet).direct!.map((e) => e.kind)).toEqual(["flight"]);
|
||
});
|
||
|
||
it("covers only the absence, and says how long it was", () => {
|
||
const before = flight("p0", 9_999, { at: FROM });
|
||
const during = flight("p0", 1_000);
|
||
const summary = summarise([before, during]);
|
||
expect(summary.totals.deliveries).toBe(1);
|
||
expect(summary.durationMs).toBe(10 * HOUR);
|
||
});
|
||
});
|