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

198 lines
6.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { content } from "@deadhead/content";
import type { SaveDocument } from "@deadhead/schema";
import {
boardPeriodIndex,
effectiveStats,
pilotLevel,
resolveDispatch,
skillPointsAvailable,
type RealTime,
type Resolution,
} from "@deadhead/sim";
import { logEntry, pruneLogbook } from "./logbook";
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,
event: dispatch.eventApplied,
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;
}
const levelBefore = pilotLevel(pilot.xp, content.config);
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++;
}
const discovers = r.discoversDestination && !discovered.has(dispatch.record.destinationId);
if (discovers) {
save.discoveredLocations.push({
locationId: dispatch.record.destinationId,
discoveredAt: now,
});
discovered.add(dispatch.record.destinationId);
}
logArrival(save, dispatch, r, now, discovers, levelBefore, pilot);
}
return resolved;
}
/** One arrival in the logbook (decision 0.5.141): the flight and what came with it. */
function logArrival(
save: SaveDocument,
dispatch: SaveDocument["dispatches"][number],
r: Resolution,
now: RealTime,
discovers: boolean,
levelBefore: number,
pilot: SaveDocument["pilots"][number],
): void {
const { record } = dispatch;
const contract = record.contract;
const who = { pilotId: pilot.id, shipId: dispatch.shipId };
logEntry(save, `${dispatch.id}:flight`, {
at: now,
...who,
kind: "flight",
dispatchId: dispatch.id,
deadhead: contract === null,
tutorial: dispatch.tutorial,
independent: dispatch.independent,
originId: record.originId,
destinationId: record.destinationId,
goodId: contract?.goodId ?? null,
massTonnes: contract?.massTonnes ?? null,
shipper: contract?.shipper ?? null,
payout: Math.max(0, r.credits + r.pilotPay + r.loanRepayment),
pilotPay: r.pilotPay,
fuelCost: record.fuelCost,
loanRepayment: r.loanRepayment,
xp: r.pilotXp,
scriptedId: contract?.scriptedId ?? null,
});
if (r.topUp && r.topUp.boughtFu > 0) {
logEntry(save, `${dispatch.id}:top-up`, {
at: now,
...who,
kind: "refuel",
locationId: r.topUp.locationId,
boughtFu: r.topUp.boughtFu,
cost: r.topUp.cost,
source: "top-up",
});
}
if (r.pilotRestEndsAt !== null) {
logEntry(save, `${dispatch.id}:rest`, {
at: now,
...who,
kind: "rest",
until: r.pilotRestEndsAt,
});
}
if (discovers) {
logEntry(save, `${dispatch.id}:discovery`, {
at: now,
...who,
kind: "discovery",
locationId: record.destinationId,
});
}
const level = pilotLevel(pilot.xp, content.config);
if (level > levelBefore) {
logEntry(save, `${dispatch.id}:level`, {
at: now,
...who,
kind: "level-up",
level,
skillPoints: skillPointsAvailable(level, pilot.skillPointsSpent, content.config),
});
}
}
/**
* 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;
pruneLogbook(save, now);
save.dispatches = save.dispatches.filter(
(d) =>
d.status === "in-flight" ||
d.acknowledgedAt === null ||
(d.resolvedAt ?? d.arrivalAt) >= cutoff,
);
}