81 lines
2.9 KiB
TypeScript
81 lines
2.9 KiB
TypeScript
import { content } from "@deadhead/content";
|
|
import type { Contract, PlayerState } from "@deadhead/schema";
|
|
import { contractTripGameHours, MS_PER_HOUR } from "@deadhead/sim";
|
|
import { shipStatsOf } from "../game";
|
|
import { useState } from "react";
|
|
import { cargoName, formatDuration } from "../format";
|
|
import { Pill, type Tone } from "../ui";
|
|
|
|
/** The good's name as shown on a contract: a Priority good may have its own name (CR-007). */
|
|
export function goodName(contract: Pick<Contract, "goodId" | "priority">): string {
|
|
const good = content.good(contract.goodId);
|
|
if (!good) return contract.goodId;
|
|
return contract.priority && good.priorityName ? good.priorityName : good.name;
|
|
}
|
|
|
|
/** A delivery window in game hours, e.g. "5h" or "2.5h". */
|
|
export function formatWindow(hours: number): string {
|
|
const rounded = hours < 10 ? Math.round(hours * 10) / 10 : Math.round(hours);
|
|
return `${rounded}h`;
|
|
}
|
|
|
|
/** Real time for a span of game hours, e.g. "5m 12s". */
|
|
export function realTimeOf(gameHours: number): string {
|
|
return formatDuration((gameHours * MS_PER_HOUR) / content.config.timeCompression);
|
|
}
|
|
|
|
const CLASS_TONES: Record<string, Tone> = {
|
|
heavy: "cyan",
|
|
"high-value": "emerald",
|
|
dangerous: "rose",
|
|
};
|
|
|
|
/** "HIGH-VALUE · T1": the cargo class, with the tier when it has one. */
|
|
export function ClassChip({ contract }: { contract: Pick<Contract, "cargoTypeId" | "tier"> }) {
|
|
return (
|
|
<Pill tone={CLASS_TONES[contract.cargoTypeId] ?? "default"}>
|
|
{cargoName(contract.cargoTypeId)}
|
|
{contract.tier > 0 && ` · T${contract.tier}`}
|
|
</Pill>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* "PRIORITY · within 5h" (game hours). Tap to switch to real time; hovering
|
|
* shows both.
|
|
*/
|
|
export function PriorityChip({ windowHours }: { windowHours: number }) {
|
|
const [real, setReal] = useState(false);
|
|
return (
|
|
<button
|
|
type="button"
|
|
onClick={() => setReal(!real)}
|
|
title={`Within ${formatWindow(windowHours)} game time, about ${realTimeOf(windowHours)} real time`}
|
|
className="rounded-full"
|
|
>
|
|
<Pill tone="amber" icon="bolt">
|
|
Priority · within{" "}
|
|
<span className="normal-case">
|
|
{real ? realTimeOf(windowHours) : formatWindow(windowHours)}
|
|
</span>
|
|
</Pill>
|
|
</button>
|
|
);
|
|
}
|
|
|
|
/**
|
|
* True when a Priority contract has idle ships at its origin and none of them
|
|
* can make the window (CR-007). Display only; the sim decides at dispatch.
|
|
*/
|
|
export function tooSlowForAll(state: PlayerState, contract: Contract, now: number): boolean {
|
|
if (!contract.priority || contract.deliveryWindowGameHours === null) return false;
|
|
const idle = state.ships.filter((s) => s.status === "idle" && s.locationId === contract.originId);
|
|
if (idle.length === 0) return false;
|
|
// Effective stats: an engines upgrade can make the window (CR-009).
|
|
return idle.every(
|
|
(ship) =>
|
|
contractTripGameHours(shipStatsOf(ship), contract, now, content) >
|
|
contract.deliveryWindowGameHours!,
|
|
);
|
|
}
|