98 lines
3.6 KiB
TypeScript
98 lines
3.6 KiB
TypeScript
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<string>;
|
||
/** 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),
|
||
},
|
||
};
|
||
}
|