import { content } from "@deadhead/content"; import { describe, expect, it } from "vitest"; import { createDispatchRecord, createRepositionRecord, createRng, pilotShare, resolveDispatch, type Contract, 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: 400, requiredSkill: null, requiredLongHaulLevel: 0, offeredAt: now, expiresAt: now + 3_600_000, 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 base = createDispatchRecord( { id: "d1", ship, pilot, contract, tutorial: false, now, worldSeed: 7 }, content, ); /** The trip with a chosen payout and fuel cost. */ const trip = (payout: number, fuelCost: number, extra: Partial = {}) => ({ ...base, contract: { ...contract, payout }, fuelCost, ...extra, }); const resolve = (record: DispatchRecord, more: Partial = {}) => { const result = resolveDispatch( { record, pilotIsAvatar: false, discoveredIds: new Set(["earth", "moon"]), loanBalance: 0, pilotDutyHours: 0, pilotSkills: {}, now: record.arrivalAt, ...more, }, content, ); if (result.status !== "resolved") throw new Error("not resolved"); return result.resolution; }; describe("profit-share pilot pay (Step 10J.2)", () => { it("pays a hired pilot a quarter of payout minus fuel, rounded like other credits", () => { // 0.25 × (400 − 150) = 62.5 → 63 (Math.round, as payouts and fuel). expect(pilotShare(400, 150, content.config)).toBe(63); expect(resolve(trip(400, 150))).toMatchObject({ pilotPay: 63, credits: 400 - 63 }); }); it("pays nothing when fuel costs as much as the payout or more", () => { expect(resolve(trip(400, 400)).pilotPay).toBe(0); expect(resolve(trip(400, 900))).toMatchObject({ pilotPay: 0, credits: 400 }); }); it("pays the avatar, deadheads and tutorial runs nothing", () => { expect(resolve(trip(400, 150), { pilotIsAvatar: true }).pilotPay).toBe(0); expect(resolve(trip(400, 150, { tutorial: true })).pilotPay).toBe(0); const deadhead = createRepositionRecord( { id: "r1", ship, pilot, destinationId: "moon", now, worldSeed: 7 }, content, ); expect(resolve(deadhead)).toMatchObject({ pilotPay: 0, credits: 0 }); }); it("takes the loan repayment from what is left after pilot pay", () => { const resolution = resolve(trip(400, 150), { loanBalance: 5000 }); const afterPay = 400 - 63; expect(resolution).toMatchObject({ pilotPay: 63, loanRepayment: Math.floor(afterPay * content.config.loanRepaymentShare), }); }); it("always splits the payout exactly into credits, pilot pay and repayment", () => { const rng = createRng(10); for (let i = 0; i < 500; i++) { const payout = rng.int(0, 50_000); const fuel = rng.int(0, 20_000); const r = resolve(trip(payout, fuel), { loanBalance: rng.int(0, 6000), pilotIsAvatar: rng.next() < 0.3, }); expect(r.credits + r.pilotPay + r.loanRepayment).toBe(payout); expect(r.pilotPay).toBeGreaterThanOrEqual(0); } }); });