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) }; }