deadhead-space-freight-tycoon/packages/engine/src/game/away.ts

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