import { content } from "@deadhead/content"; import { describe, expect, it } from "vitest"; import { MS_PER_HOUR, createDispatchRecord, createRepositionRecord, dutyLimit, resolveDispatch, restEndsAt, tripGameHours, validateDispatch, validateReposition, type Contract, type DispatchProposal, type DispatchRecord, type PilotState, type ResolveInput, type ShipState, } from "../src/index"; const now = Date.parse("2100-01-01T00:30:00Z"); const contract: Contract = { id: "c1", originId: "earth", destinationId: "moon", cargoTypeId: "standard", goodId: "consumer-goods", shipper: "Pacific Orbital Logistics", priority: false, deliveryWindowGameHours: null, tier: 0, massTonnes: 10, payout: 2000, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: Date.parse("2100-01-01T00:00:00Z"), expiresAt: Date.parse("2100-01-01T01:00:00Z"), firstContact: false, }; const ship: ShipState = { id: "s1", modelId: "courier", locationId: "earth", inFlight: false }; const pilot: PilotState = { id: "p1", isAvatar: false, assignedShipId: "s1", inFlight: false, skills: {}, restEndsAt: null, }; const proposal: DispatchProposal = { ship, pilot, contract, tutorial: false, credits: 10_000, now }; const limit = dutyLimit({}, content.config); const resolveAt = (record: DispatchRecord, dutyBefore: number, at = record.arrivalAt) => { const input: ResolveInput = { record, pilotIsAvatar: false, discoveredIds: new Set(["earth", "moon"]), loanBalance: 0, pilotDutyHours: dutyBefore, pilotSkills: {}, now: at, }; const result = resolveDispatch(input, content); if (result.status !== "resolved") throw new Error("not resolved"); return result.resolution; }; describe("duty in validation (Step 10D.3)", () => { it("refuses a resting pilot with pilot-resting, before other pilot checks", () => { const resting = { ...pilot, restEndsAt: now + 60_000, inFlight: true, assignedShipId: null }; expect(validateDispatch({ ...proposal, pilot: resting }, content)).toEqual({ ok: false, reason: "pilot-resting", }); expect( validateReposition( { ship, pilot: { ...pilot, restEndsAt: now + 1 }, destinationId: "moon", discoveredIds: new Set(["earth", "moon"]), credits: 10_000, now, }, content, ), ).toEqual({ ok: false, reason: "pilot-resting" }); }); it("lets a pilot whose rest has ended fly", () => { expect( validateDispatch({ ...proposal, pilot: { ...pilot, restEndsAt: now } }, content), ).toEqual({ ok: true, }); }); }); describe("duty in resolution (Step 10D.3)", () => { const record = createDispatchRecord({ ...proposal, id: "d1", worldSeed: 7 }, content); const hours = tripGameHours(record.departureAt, record.arrivalAt, content.config); it("adds the trip's hours to duty when under the limit", () => { expect(resolveAt(record, 1)).toMatchObject({ pilotDutyHours: 1 + hours, pilotRestEndsAt: null, }); }); it("starts rest at the arrival time when duty reaches the limit, even resolved hours later", () => { const late = record.arrivalAt + 5 * MS_PER_HOUR; expect(resolveAt(record, limit - hours / 2, late)).toMatchObject({ pilotDutyHours: 0, pilotRestEndsAt: restEndsAt(record.arrivalAt, content.config), }); expect(restEndsAt(record.arrivalAt, content.config)).toBe( record.arrivalAt + (content.config.restGameHours * MS_PER_HOUR) / content.config.timeCompression, ); }); it("counts deadheads but not tutorial runs", () => { const deadhead = createRepositionRecord( { id: "r1", ship, pilot, destinationId: "moon", now, worldSeed: 7 }, content, ); const deadheadHours = tripGameHours(deadhead.departureAt, deadhead.arrivalAt, content.config); expect(resolveAt(deadhead, 2).pilotDutyHours).toBeCloseTo(2 + deadheadHours); const tutorial = createDispatchRecord( { ...proposal, tutorial: true, pilot: { ...pilot, isAvatar: true }, id: "t1", worldSeed: 7 }, content, ); expect(resolveAt(tutorial, limit - 0.1)).toMatchObject({ pilotDutyHours: limit - 0.1, pilotRestEndsAt: null, }); }); });