108 lines
3.6 KiB
TypeScript
108 lines
3.6 KiB
TypeScript
import { content } from "@deadhead/content";
|
||
import type {
|
||
Company,
|
||
DispatchOffice,
|
||
Pilot,
|
||
PilotMode,
|
||
PilotOrders,
|
||
SaveDocument,
|
||
} from "@deadhead/schema";
|
||
import { defaultOrders, type RealTime } from "@deadhead/sim";
|
||
import { fail } from "./errors";
|
||
import {
|
||
companyView,
|
||
hasBayAt,
|
||
requirePilot,
|
||
requireTutorialOver,
|
||
spend,
|
||
toApiPilot,
|
||
} from "./state";
|
||
|
||
/** A new pilot's orders (CR-020 §2): anything goes, home at `homeLocationId`. */
|
||
export function newPilotOrders(homeLocationId: string): PilotOrders {
|
||
const orders = defaultOrders(homeLocationId, content);
|
||
return { ...orders, cargoClassIds: [...orders.cargoClassIds], destinationIds: null };
|
||
}
|
||
|
||
export function dispatchOfficeView(save: SaveDocument): DispatchOffice {
|
||
return { owned: save.company.dispatchOffice, spendingFloor: save.company.npcSpendingFloor };
|
||
}
|
||
|
||
/** The one-time Dispatch Office (decision 0.5.119). */
|
||
export function buyDispatchOffice(save: SaveDocument): {
|
||
company: Company;
|
||
dispatchOffice: DispatchOffice;
|
||
} {
|
||
requireTutorialOver(save, "buying a Dispatch Office");
|
||
if (save.company.dispatchOffice) {
|
||
throw fail("dispatch-office-owned", "You already have a Dispatch Office.");
|
||
}
|
||
spend(save, content.config.independentPilots.dispatchOfficePrice, "a Dispatch Office");
|
||
save.company.dispatchOffice = true;
|
||
return { company: companyView(save), dispatchOffice: dispatchOfficeView(save) };
|
||
}
|
||
|
||
/**
|
||
* Manual or Independent (decisions 0.5.118–0.5.120). Independent needs the
|
||
* Dispatch Office and is never the avatar. Switching to Manual mid-trip takes
|
||
* effect when the trip ends: an Independent pilot only acts when idle.
|
||
*/
|
||
export function setPilotMode(
|
||
save: SaveDocument,
|
||
input: { pilotId: string; mode: PilotMode },
|
||
now: RealTime,
|
||
): Pilot {
|
||
const pilot = requirePilot(save, input.pilotId);
|
||
if (input.mode === "independent") {
|
||
if (pilot.isAvatar) {
|
||
throw fail(
|
||
"avatar-not-independent",
|
||
"You fly your own jobs: only hired pilots can go Independent.",
|
||
);
|
||
}
|
||
if (!save.company.dispatchOffice) {
|
||
throw fail("dispatch-office-required", "Buy a Dispatch Office first.");
|
||
}
|
||
}
|
||
pilot.mode = input.mode;
|
||
return toApiPilot(save, pilot, now);
|
||
}
|
||
|
||
/** A pilot's orders (decision 0.5.120), checked against content and the player's bays. */
|
||
export function setPilotOrders(
|
||
save: SaveDocument,
|
||
input: { pilotId: string; orders: PilotOrders },
|
||
now: RealTime,
|
||
): Pilot {
|
||
const pilot = requirePilot(save, input.pilotId);
|
||
const { orders } = input;
|
||
for (const id of orders.destinationIds ?? []) {
|
||
if (!content.location(id)) throw fail("unknown-location", `No location "${id}".`);
|
||
}
|
||
for (const id of orders.cargoClassIds) {
|
||
if (!content.cargoTypes.some((c) => c.id === id)) {
|
||
throw fail("unknown-cargo-class", `No cargo class "${id}".`);
|
||
}
|
||
}
|
||
if (!hasBayAt(save, orders.homeLocationId)) {
|
||
throw fail("no-bay-at-location", "A pilot's home must be one of your bays.");
|
||
}
|
||
pilot.orders = {
|
||
destinationIds: orders.destinationIds ? [...new Set(orders.destinationIds)] : null,
|
||
cargoClassIds: [...new Set(orders.cargoClassIds)],
|
||
takePriority: orders.takePriority,
|
||
minProfit: orders.minProfit,
|
||
homeLocationId: orders.homeLocationId,
|
||
};
|
||
return toApiPilot(save, pilot, now);
|
||
}
|
||
|
||
/** The credits Independent pilots never spend the company below (decision 0.5.123). */
|
||
export function setSpendingFloor(
|
||
save: SaveDocument,
|
||
credits: number,
|
||
): { company: Company; dispatchOffice: DispatchOffice } {
|
||
save.company.npcSpendingFloor = credits;
|
||
return { company: companyView(save), dispatchOffice: dispatchOfficeView(save) };
|
||
}
|