127 lines
4.0 KiB
TypeScript
127 lines
4.0 KiB
TypeScript
import { content, type Location, type ShipModel } from "@deadhead/content";
|
|
import type { Ship } from "@deadhead/schema";
|
|
import {
|
|
checkRoute,
|
|
effectiveStats,
|
|
MS_PER_HOUR,
|
|
routeDistance,
|
|
travelTimeGameHours,
|
|
type EffectiveStats,
|
|
type ShipState,
|
|
} from "@deadhead/sim";
|
|
|
|
/** Client-side predictions with the same sim the server uses (rule 0.1.6). */
|
|
|
|
export function location(id: string): Location {
|
|
const found = content.location(id);
|
|
if (!found) throw new Error(`Unknown location ${id}`);
|
|
return found;
|
|
}
|
|
|
|
export function shipModel(id: string): ShipModel {
|
|
const found = content.shipModel(id);
|
|
if (!found) throw new Error(`Unknown ship model ${id}`);
|
|
return found;
|
|
}
|
|
|
|
/** Route distance in million km; fixed (decision 0.5.93). */
|
|
export function distanceAt(originId: string, destinationId: string): number {
|
|
return routeDistance(location(originId), location(destinationId));
|
|
}
|
|
|
|
/** Trip duration in real milliseconds for a ship carrying `massTonnes`. */
|
|
export function tripRealMs(
|
|
model: ShipModel,
|
|
originId: string,
|
|
destinationId: string,
|
|
massTonnes: number,
|
|
) {
|
|
const hours = travelTimeGameHours(
|
|
model,
|
|
distanceAt(originId, destinationId),
|
|
massTonnes,
|
|
content.config,
|
|
);
|
|
return (hours * MS_PER_HOUR) / content.config.timeCompression;
|
|
}
|
|
|
|
/** A ship's effective stats: its model plus its upgrades (decision 0.5.63). */
|
|
export function shipStatsOf(ship: Pick<Ship, "modelId" | "upgrades">): EffectiveStats {
|
|
return effectiveStats(shipModel(ship.modelId), ship.upgrades, content);
|
|
}
|
|
|
|
/** The sim's view of an owned ship: location, flight, upgrades and shop time (CR-009). */
|
|
export function simShip(ship: Ship): ShipState {
|
|
return {
|
|
id: ship.id,
|
|
modelId: ship.modelId,
|
|
locationId: ship.locationId ?? "",
|
|
inFlight: ship.status === "in-flight",
|
|
upgrades: ship.upgrades,
|
|
inShopUntil: ship.inShopUntil,
|
|
};
|
|
}
|
|
|
|
const legKey = (fromId: string, toId: string) => `${fromId}>${toId}`;
|
|
const legCache = new Map<string, Set<string>>();
|
|
|
|
/** Legs a model can fly (the map is fixed, decision 0.5.93). */
|
|
function legsInRange(model: ShipModel): Set<string> {
|
|
let legs = legCache.get(model.id);
|
|
if (!legs) {
|
|
legs = new Set();
|
|
for (const from of content.locations) {
|
|
for (const to of content.locations) {
|
|
if (from.id !== to.id && checkRoute(model, from, to).ok) legs.add(legKey(from.id, to.id));
|
|
}
|
|
}
|
|
legCache.set(model.id, legs);
|
|
}
|
|
return legs;
|
|
}
|
|
|
|
export interface ShipReach {
|
|
/** Per origin, the locations in range right now. */
|
|
now: { originId: string; destinationIds: string[] }[];
|
|
/** Reachable directly from an origin, but not right now. */
|
|
otherDates: string[];
|
|
/** Reachable only through other locations. */
|
|
byHopping: string[];
|
|
}
|
|
|
|
/**
|
|
* Where a base model can fly from `originIds` (Market, Step 10L.7): legs in
|
|
* range from each origin, and places it can reach only by hopping. On the fixed
|
|
* map (decision 0.5.93) `otherDates` is always empty; it goes with its wording
|
|
* in Step 15.8.
|
|
*/
|
|
export function shipReach(model: ShipModel, originIds: string[]): ShipReach {
|
|
const ever = legsInRange(model);
|
|
const ids = content.locations.map((l) => l.id);
|
|
const origins = new Set(originIds);
|
|
const nowLegs = originIds.map((originId) => ({
|
|
originId,
|
|
destinationIds: ids.filter(
|
|
(to) => to !== originId && checkRoute(model, location(originId), location(to)).ok,
|
|
),
|
|
}));
|
|
const inRangeNow = new Set(nowLegs.flatMap((l) => l.destinationIds));
|
|
const direct = new Set(ids.filter((to) => originIds.some((o) => ever.has(legKey(o, to)))));
|
|
const reached = new Set(originIds);
|
|
const queue = [...originIds];
|
|
while (queue.length > 0) {
|
|
const from = queue.shift()!;
|
|
for (const to of ids) {
|
|
if (!reached.has(to) && ever.has(legKey(from, to))) {
|
|
reached.add(to);
|
|
queue.push(to);
|
|
}
|
|
}
|
|
}
|
|
return {
|
|
now: nowLegs,
|
|
otherDates: ids.filter((id) => direct.has(id) && !inRangeNow.has(id) && !origins.has(id)),
|
|
byHopping: ids.filter((id) => reached.has(id) && !direct.has(id) && !origins.has(id)),
|
|
};
|
|
}
|