112 lines
4.5 KiB
TypeScript
112 lines
4.5 KiB
TypeScript
import { content } from "@deadhead/content";
|
||
import type { SaveDocument } from "@deadhead/schema";
|
||
import { boardPeriodIndex, effectiveStats, resolveDispatch, type RealTime } from "@deadhead/sim";
|
||
import { discoveredIds } from "./state";
|
||
|
||
/**
|
||
* Resolves every in-flight dispatch that has arrived by `now`, in arrival
|
||
* order (Steps 6.1, 7.2): credits (net of pilot pay and loan repayment), Auto
|
||
* top-up (CR-015), XP,
|
||
* duty and rest, ship and pilot locations, tutorial progress and discovery.
|
||
* A resolved dispatch is never resolved again, so running this twice in a
|
||
* row changes nothing the second time. Returns how many were resolved.
|
||
*/
|
||
export function resolveArrivals(save: SaveDocument, now: RealTime): number {
|
||
const due = save.dispatches
|
||
.filter((d) => d.status === "in-flight" && d.arrivalAt <= now)
|
||
.sort((a, b) => a.arrivalAt - b.arrivalAt);
|
||
if (due.length === 0) return 0;
|
||
|
||
const discovered = discoveredIds(save);
|
||
let resolved = 0;
|
||
for (const dispatch of due) {
|
||
const pilot = save.pilots.find((p) => p.id === dispatch.pilotId);
|
||
if (!pilot) throw new Error(`Dispatch ${dispatch.id} has no pilot`);
|
||
const ship = dispatch.shipId ? save.ships.find((s) => s.id === dispatch.shipId) : undefined;
|
||
const model = ship ? content.shipModel(ship.modelId) : undefined;
|
||
const result = resolveDispatch(
|
||
{
|
||
record: dispatch.record,
|
||
pilotIsAvatar: pilot.isAvatar,
|
||
discoveredIds: discovered,
|
||
// Each arrival repays against the balance left by the previous one.
|
||
loanBalance: save.company.loanBalance,
|
||
pilotDutyHours: pilot.dutyHours,
|
||
pilotSkills: pilot.skills,
|
||
now,
|
||
fuel:
|
||
ship && model
|
||
? {
|
||
tank: { fuelFu: ship.fuelFu, avgPrice: ship.fuelAvgPrice },
|
||
capacityFu: effectiveStats(model, ship.upgrades, content).tankCapacityFu,
|
||
refuelPolicy: ship.refuelPolicy,
|
||
credits: save.company.credits,
|
||
}
|
||
: undefined,
|
||
},
|
||
content,
|
||
);
|
||
if (result.status !== "resolved") continue;
|
||
const r = result.resolution;
|
||
resolved++;
|
||
|
||
Object.assign(dispatch, { status: "resolved", resolution: r, resolvedAt: now });
|
||
save.company.credits += r.credits;
|
||
save.company.loanBalance -= r.loanRepayment;
|
||
// Auto top-up on landing (decision 0.5.97).
|
||
if (r.topUp && ship) {
|
||
save.company.credits -= r.topUp.cost;
|
||
ship.fuelFu = r.topUp.tank.fuelFu;
|
||
ship.fuelAvgPrice = r.topUp.tank.avgPrice;
|
||
}
|
||
|
||
pilot.xp += r.pilotXp;
|
||
pilot.status = "idle";
|
||
// Duty and mandatory rest from the arrival time (decisions 0.5.42–0.5.45).
|
||
pilot.dutyHours = r.pilotDutyHours;
|
||
if (r.pilotRestEndsAt !== null) pilot.restEndsAt = r.pilotRestEndsAt;
|
||
// Only unassigned pilots store a location (decision 0.5.27): the tutorial avatar.
|
||
if (pilot.assignedShipId === null) pilot.locationId = r.shipLocationId;
|
||
|
||
if (ship) {
|
||
ship.status = "idle";
|
||
ship.locationId = r.shipLocationId;
|
||
ship.lastArrivalAt = Math.max(ship.lastArrivalAt, dispatch.arrivalAt);
|
||
}
|
||
if (dispatch.tutorial) {
|
||
save.player.loanerLocationId = r.shipLocationId;
|
||
save.player.tutorialRunsCompleted++;
|
||
} else if (dispatch.record.contract) {
|
||
save.stats.contractDeliveries++;
|
||
if (dispatch.record.contract.priority) save.stats.priorityDeliveries++;
|
||
}
|
||
if (r.discoversDestination && !discovered.has(dispatch.record.destinationId)) {
|
||
save.discoveredLocations.push({
|
||
locationId: dispatch.record.destinationId,
|
||
discoveredAt: now,
|
||
});
|
||
discovered.add(dispatch.record.destinationId);
|
||
}
|
||
}
|
||
return resolved;
|
||
}
|
||
|
||
/**
|
||
* Keeps the save small (CR-014 §2, Step 7.3): drops boards and accepted
|
||
* contracts older than the board retention, and resolved dispatches the player
|
||
* has seen that are older than the dispatch retention. In-flight dispatches,
|
||
* unseen summaries and the goal counters are never touched.
|
||
*/
|
||
export function pruneSave(save: SaveDocument, now: RealTime): void {
|
||
const cutoffPeriod = boardPeriodIndex(now, content.config) - content.config.boardRetentionPeriods;
|
||
save.contractBoards = save.contractBoards.filter((b) => b.periodIndex >= cutoffPeriod);
|
||
save.acceptedContracts = save.acceptedContracts.filter((a) => a.periodIndex >= cutoffPeriod);
|
||
const cutoff = now - content.config.savedDispatchRetentionRealHours * 3_600_000;
|
||
save.dispatches = save.dispatches.filter(
|
||
(d) =>
|
||
d.status === "in-flight" ||
|
||
d.acknowledgedAt === null ||
|
||
(d.resolvedAt ?? d.arrivalAt) >= cutoff,
|
||
);
|
||
}
|