import { content } from "@deadhead/content"; import type { SaveDocument } from "@deadhead/schema"; import { boardPeriodIndex, canLandAt, emergencyFuel, isInShop, permanentFailure, shipStats, tripFuel, type AwayProblem, type RealTime, } from "@deadhead/sim"; import { acceptedContractIds } from "./boards"; import { discoveredIds, shipState } from "./state"; import { canReachBooking } from "./service"; /** * What needs the player now (decision 0.5.140's last highlight), from the * state: idle crewed ships that can't fly anywhere on the fuel aboard (or need * emergency fuel), Manual ships whose board this period has no job they could * take, credits below the pilots' spending floor, and booked service waiting * for funds or out of reach (CR-027). */ export function awayProblems(save: SaveDocument, now: RealTime): AwayProblem[] { if (save.player.tutorialActive) return []; const discovered = discoveredIds(save); const accepted = acceptedContractIds(save); const period = boardPeriodIndex(now, content.config); const problems: AwayProblem[] = []; // Booked service stuck (CR-027 ยง4): waiting for funds, or the HQ out of reach. for (const ship of save.ships) { const booking = ship.booking; if (!booking || booking.startedAt !== null) continue; if (booking.waitingSince !== null) { problems.push({ kind: "service-waiting-funds", shipId: ship.id, hqId: booking.hqId }); } else if (!canReachBooking(save, ship)) { problems.push({ kind: "service-cant-reach", shipId: ship.id, hqId: booking.hqId }); } } for (const ship of save.ships) { const pilot = save.pilots.find((p) => p.assignedShipId === ship.id); if (ship.status !== "idle" || !ship.locationId || !pilot || isInShop(ship, now)) continue; const at = { shipId: ship.id, locationId: ship.locationId }; const state = shipState(ship); if ( emergencyFuel( { ship: state, credits: save.company.credits, discoveredIds: discovered }, content, ) ) { problems.push({ kind: "emergency-fuel", ...at }); continue; } const model = shipStats(state, content); const here = content.location(ship.locationId)!; const legs = content.locations .filter((next) => next.id !== here.id && discovered.has(next.id) && canLandAt(model, next)) .map((next) => tripFuel(model, here, next, content.config)); if (legs.length > 0 && Math.min(...legs) > ship.fuelFu) { problems.push({ kind: "low-fuel", ...at }); continue; } // Only the player sends a Manual pilot; an Independent one waits or heads home. if (pilot.mode !== "manual") continue; const board = save.contractBoards.find( (b) => b.kind === "location" && b.locationId === ship.locationId && b.periodIndex === period, ); if (!board) continue; const member = { modelId: ship.modelId, skills: pilot.skills, upgrades: ship.upgrades, fuelFu: ship.fuelFu, }; const open = board.contracts.filter((c) => !accepted.has(c.id) && c.expiresAt > now); if (!open.some((c) => permanentFailure(c, member, discovered, content) === null)) { problems.push({ kind: "no-jobs", ...at }); } } const { credits, npcSpendingFloor, dispatchOffice } = save.company; if ( dispatchOffice && credits < npcSpendingFloor && save.pilots.some((p) => p.mode === "independent") ) { problems.push({ kind: "below-floor", credits, floor: npcSpendingFloor }); } return problems; }