import type { SimContent } from "./board"; import type { RealTime } from "./clock"; import type { DispatchRecord } from "./dispatch"; import { dutyAfterTrip, dutyLimit, needsRest, restEndsAt, tripGameHours } from "./duty"; import { splitLoanRepayment } from "./loan"; import type { SkillLevels } from "./pilot"; import { deliveryXp } from "./xp"; export interface Resolution { /** * Credits to add: payout minus pilot pay, minus the loan repayment. * Fuel was already charged at departure. */ credits: number; /** Credits that went to the lender out of this payout (decision 0.5.32). */ loanRepayment: number; pilotXp: number; /** Where the ship (or tutorial loaner) is now. */ shipLocationId: string; /** True when this delivery discovers the destination. */ discoversDestination: boolean; /** The pilot's duty hours after this trip: 0 if it ended in mandatory rest (CR-004). */ pilotDutyHours: number; /** Mandatory rest from the arrival time, or null (decision 0.5.45). */ pilotRestEndsAt: RealTime | null; } export type ResolveResult = { status: "in-flight" } | { status: "resolved"; resolution: Resolution }; export interface ResolveInput { record: DispatchRecord; /** The avatar is exempt from pilot pay (decision 0.5.12). */ pilotIsAvatar: boolean; discoveredIds: ReadonlySet; /** The company's loan balance before this resolution. */ loanBalance: number; /** The pilot's duty hours before this trip, and skills (Long Haul sets the limit). */ pilotDutyHours: number; pilotSkills: SkillLevels; now: RealTime; } /** * The pilot's duty after a trip (decisions 0.5.42–0.5.45): the trip's game * hours are added (tutorial runs add none); at or over the limit, rest starts * at the arrival time and duty resets, however late the trip is resolved. */ function dutyOutcome(input: ResolveInput, content: SimContent) { const { record, pilotDutyHours, pilotSkills } = input; const hours = tripGameHours(record.departureAt, record.arrivalAt, content.config); const duty = dutyAfterTrip(pilotDutyHours, hours, record.tutorial); if (!record.tutorial && needsRest(duty, dutyLimit(pilotSkills, content.config))) { return { pilotDutyHours: 0, pilotRestEndsAt: restEndsAt(record.arrivalAt, content.config) }; } return { pilotDutyHours: duty, pilotRestEndsAt: null }; } /** Resolves a dispatch once it has arrived. Same inputs always give the same result. */ export function resolveDispatch(input: ResolveInput, content: SimContent): ResolveResult { const { record, pilotIsAvatar, discoveredIds, loanBalance, now } = input; if (now < record.arrivalAt) return { status: "in-flight" }; const { contract } = record; const base = { shipLocationId: record.destinationId, ...dutyOutcome(input, content) }; if (contract === null) { // Deadheads earn nothing and only fly to discovered locations. return { status: "resolved", resolution: { ...base, credits: 0, loanRepayment: 0, pilotXp: 0, discoversDestination: false, }, }; } const pilotPay = record.tutorial || pilotIsAvatar ? 0 : content.config.pilotPayPerDelivery; // Tutorial contracts already store exactly the tutorial share as their payout. const { repayment, net } = splitLoanRepayment( contract.payout - pilotPay, loanBalance, content.config.loanRepaymentShare, ); return { status: "resolved", resolution: { ...base, credits: net, loanRepayment: repayment, pilotXp: deliveryXp(record.distanceMillionKm, contract.tier, content.config), discoversDestination: !discoveredIds.has(record.destinationId), }, }; }