deadhead-space-freight-tycoon/apps/web/src/components/Goods.tsx

78 lines
2.8 KiB
TypeScript

import { content } from "@deadhead/content";
import type { Contract, PlayerState } from "@deadhead/schema";
import { contractTripGameHours, MS_PER_HOUR } from "@deadhead/sim";
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 {real ? realTimeOf(windowHours) : formatWindow(windowHours)}
</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;
return idle.every((ship) => {
const model = content.shipModel(ship.modelId);
return (
!model ||
contractTripGameHours(model, contract, now, content) > contract.deliveryWindowGameHours!
);
});
}