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 => ({ 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 & { 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); // Four flights: not a short absence, so every highlight counts. const fewer = summarise([...all.slice(0, 2), flight("p1", 10), flight("p1", 20)], { 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"]); // What the list shows isn't a highlight too, and isn't counted as "more". const listed = summarise( [flight("p0", 500), other({ kind: "discovery", locationId: "mars" })], { problems: [{ kind: "below-floor", credits: 10, floor: 500 }] }, ); expect(listed.highlights.map((h) => h.kind)).toEqual(["problem"]); expect(listed.moreHighlights).toBe(0); }); 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); }); }); describe("level-ups in the highlights", () => { it("shows one per pilot, at the highest level reached", () => { const summary = summarise([ other({ kind: "level-up", pilotId: "p0", level: 2, skillPoints: 2 }), other({ kind: "level-up", pilotId: "p0", level: 3, skillPoints: 3 }), other({ kind: "level-up", pilotId: "p1", level: 1, skillPoints: 1 }), flight("p2", 0, { goodId: null, deadhead: true }), ]); expect(summary.highlights).toEqual([ { kind: "level-up", pilotId: "p0", level: 3, skillPoints: 3 }, { kind: "level-up", pilotId: "p1", level: 1, skillPoints: 1 }, ]); }); });