189 lines
5.5 KiB
TypeScript
189 lines
5.5 KiB
TypeScript
import { content } from "@deadhead/content";
|
|
import type { LogbookResponse, LogEntry, Pilot, PlayerState, Ship } from "@deadhead/schema";
|
|
import { effectiveStats } from "@deadhead/sim";
|
|
|
|
// Plain data builders shared by component tests and the Playwright tests (no Vitest imports).
|
|
|
|
/**
|
|
* The CR-009 fields of a ship with no upgrades and not in the shop: its model's
|
|
* stats as effective stats. Spread into Ship fixtures, then override as needed.
|
|
*/
|
|
export function shipExtras(
|
|
modelId: string,
|
|
upgrades: Record<string, number> = {},
|
|
): Pick<
|
|
Ship,
|
|
| "upgrades"
|
|
| "stats"
|
|
| "inShopUntil"
|
|
| "shop"
|
|
| "fuelFu"
|
|
| "fuelAvgPrice"
|
|
| "refuelPolicy"
|
|
| "hullConditionPct"
|
|
| "repairing"
|
|
> {
|
|
const model = content.shipModel(modelId);
|
|
if (!model) throw new Error(`Unknown ship model ${modelId}`);
|
|
const s = effectiveStats(model, upgrades, content);
|
|
return {
|
|
upgrades,
|
|
stats: {
|
|
cruiseSpeedKmPerGameHour: s.cruiseSpeedKmPerGameHour,
|
|
cargoCapacityTonnes: s.cargoCapacityTonnes,
|
|
tankCapacityFu: s.tankCapacityFu,
|
|
burnFuPerMillionKm: s.burnFuPerMillionKm,
|
|
overheadGameHours: s.overheadGameHours,
|
|
handlingGameHoursPerTonne: s.handlingGameHoursPerTonne,
|
|
},
|
|
inShopUntil: null,
|
|
shop: null,
|
|
// A full tank bought at Earth (CR-015).
|
|
fuelFu: s.tankCapacityFu,
|
|
fuelAvgPrice: 16,
|
|
refuelPolicy: "auto",
|
|
// A whole hull, not in for repair (CR-026).
|
|
hullConditionPct: 100,
|
|
repairing: false,
|
|
};
|
|
}
|
|
|
|
/** CR-021 and CR-023 fields of a Manual pilot with default orders, home at Earth, not in the shop. Spread into Pilot fixtures. */
|
|
export const pilotExtras: Pick<Pilot, "mode" | "orders" | "restingInShopUntil"> = {
|
|
mode: "manual",
|
|
restingInShopUntil: null,
|
|
orders: {
|
|
destinationIds: null,
|
|
cargoClassIds: content.cargoTypes.map((c) => c.id),
|
|
takePriority: true,
|
|
minProfit: 0,
|
|
homeLocationId: "earth",
|
|
},
|
|
};
|
|
|
|
/** A brand-new tutorial player, as guest sign-up returns it. */
|
|
export function samplePlayerState(overrides: Partial<PlayerState> = {}): PlayerState {
|
|
return {
|
|
playerId: "player-1",
|
|
company: { id: "co1", name: "Deadhead Freight", credits: 0, loanBalance: 0 },
|
|
ships: [],
|
|
pilots: [
|
|
{
|
|
id: "avatar",
|
|
name: "Captain",
|
|
isAvatar: true,
|
|
isVeteran: false,
|
|
xp: 0,
|
|
level: 0,
|
|
skills: {},
|
|
skillPointsAvailable: 0,
|
|
assignedShipId: null,
|
|
status: "idle",
|
|
locationId: "earth",
|
|
dutyHours: 0,
|
|
dutyLimit: 12,
|
|
resting: false,
|
|
restEndsAt: null,
|
|
...pilotExtras,
|
|
},
|
|
],
|
|
bays: [],
|
|
locations: [
|
|
{ id: "earth", discovered: true },
|
|
{ id: "moon", discovered: true },
|
|
{ id: "mars", discovered: false },
|
|
{ id: "ceres-station", discovered: false },
|
|
{ id: "jupiter-cloud-platform", discovered: false },
|
|
],
|
|
tutorial: {
|
|
loanerLocationId: "earth",
|
|
runInFlight: false,
|
|
runsCompleted: 0,
|
|
dealAvailable: false,
|
|
},
|
|
activeDispatches: [],
|
|
summaries: [],
|
|
goals: [],
|
|
dispatchOffice: { owned: false, spendingFloor: 500 },
|
|
// Awake: a day after the usual test time (CR-024).
|
|
radioCalls: [],
|
|
nextRadioCallAt: null,
|
|
fleetSleep: { afterHours: 24, sleepsAt: Date.parse("2100-01-02T00:00:00Z"), asleep: false },
|
|
welcomeSeen: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* The logbook a state's unseen summaries and finished upgrades stand for
|
|
* (CR-025): what the engine's `getLogbook` would return. `fakeApi` answers
|
|
* `getLogbook` with it unless a test gives its own.
|
|
*/
|
|
export function logbookFrom(state: PlayerState): LogbookResponse {
|
|
const entries: LogEntry[] = [];
|
|
for (const s of state.summaries) {
|
|
const who = { at: s.resolvedAt, pilotId: s.pilotId, shipId: s.shipId };
|
|
entries.push({
|
|
id: `${s.dispatchId}:flight`,
|
|
...who,
|
|
kind: "flight",
|
|
dispatchId: s.dispatchId,
|
|
deadhead: s.deadhead,
|
|
tutorial: s.tutorial,
|
|
independent: s.independent,
|
|
originId: s.originId,
|
|
destinationId: s.destinationId,
|
|
goodId: s.goodId,
|
|
massTonnes: s.massTonnes,
|
|
shipper: s.shipper,
|
|
payout: Math.max(0, s.credits + s.pilotPay + s.loanRepayment),
|
|
pilotPay: s.pilotPay,
|
|
fuelCost: s.fuelCost,
|
|
loanRepayment: s.loanRepayment,
|
|
xp: s.pilotXp,
|
|
scriptedId: s.scriptedId,
|
|
});
|
|
if (s.restEndsAt !== null) {
|
|
entries.push({ id: `${s.dispatchId}:rest`, ...who, kind: "rest", until: s.restEndsAt });
|
|
}
|
|
if (s.discoveredLocationId) {
|
|
entries.push({
|
|
id: `${s.dispatchId}:discovery`,
|
|
...who,
|
|
kind: "discovery",
|
|
locationId: s.discoveredLocationId,
|
|
});
|
|
}
|
|
}
|
|
for (const ship of state.ships) {
|
|
if (!ship.shop || ship.shop.until > Date.now()) continue;
|
|
entries.push({
|
|
id: `${ship.id}:upgrade`,
|
|
at: ship.shop.until,
|
|
pilotId: ship.pilotId,
|
|
shipId: ship.id,
|
|
kind: "upgrade",
|
|
upgradeId: ship.shop.upgradeId,
|
|
level: ship.shop.level,
|
|
});
|
|
}
|
|
for (const goal of state.goals) {
|
|
if (goal.status !== "ready") continue;
|
|
entries.push({
|
|
id: `goal:${goal.id}`,
|
|
at: Date.now(),
|
|
pilotId: null,
|
|
shipId: null,
|
|
kind: "goal-ready",
|
|
goalId: goal.id,
|
|
});
|
|
}
|
|
const from = Math.min(Date.now(), ...entries.map((e) => e.at)) - 60_000;
|
|
return {
|
|
entries: entries.sort((a, b) => a.at - b.at),
|
|
absence: { from, to: Math.max(Date.now(), ...entries.map((e) => e.at)) },
|
|
lastAbsence: null,
|
|
problems: [],
|
|
};
|
|
}
|