91 lines
3.4 KiB
TypeScript
91 lines
3.4 KiB
TypeScript
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;
|
|
}
|