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

78 lines
2.8 KiB
TypeScript

import type { PlayerState } from "@deadhead/schema";
import { formatLocalTime } from "../screens/DispatchSheet";
import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions";
import { Button, Icon, Panel } from "../ui";
/** A sleep threshold in words: "12 h", "24 h", "48 h", "3 days" (CR-024 §2). */
export function sleepLabel(hours: number): string {
return hours > 48 && hours % 24 === 0 ? `${hours / 24} days` : `${hours} h`;
}
/** The sleep time on the device, e.g. "Mar 4, 14:05". */
export function formatSleepTime(time: number): string {
const date = new Date(time).toLocaleDateString(undefined, { month: "short", day: "numeric" });
return `${date}, ${formatLocalTime(time)}`;
}
/**
* The fleet has gone quiet (decision 0.5.137) and it matters: some pilot is
* Independent, so there is work waiting on the player's orders.
*/
export function fleetQuiet(state: PlayerState): boolean {
return state.fleetSleep.asleep && state.pilots.some((p) => p.mode === "independent");
}
/** "Back to work": a manual decision that wakes the fleet (decision 0.5.138). */
function BackToWork() {
const wake = useGameAction("backToWork");
return (
<>
<Button size="sm" className="mt-3" disabled={wake.isPending} onClick={() => wake.mutate({})}>
<Icon name="send" className="size-4" />
Back to work
</Button>
{wake.isError && (
<p role="alert" className="mt-2 text-rose-300">
{errorMessage(wake.error)}
</p>
)}
</>
);
}
/** The "while you were away" banner: when the fleet went quiet, and why. */
export function SleepBanner() {
const state = useLoadedPlayer();
if (!fleetQuiet(state)) return null;
const { sleepsAt, afterHours } = state.fleetSleep;
return (
<Panel tone="amber" className="text-sm" aria-label="Fleet asleep">
<p className="flex items-start gap-2 text-amber-100">
<Icon name="rest" className="mt-0.5 size-4 shrink-0" />
Your fleet went quiet on {formatSleepTime(sleepsAt)} after {sleepLabel(afterHours)} without
orders.
</p>
<BackToWork />
</Panel>
);
}
/** The Bridge's status while the fleet sleeps. */
export function FleetAsleep() {
const state = useLoadedPlayer();
if (!fleetQuiet(state)) return null;
return (
<Panel tone="amber" className="text-sm" aria-label="Fleet asleep">
<p className="flex items-center gap-2 font-display font-semibold tracking-wide text-amber-100">
<Icon name="rest" className="size-4 shrink-0" />
Fleet asleep — waiting for your orders
</p>
<p className="mt-1 text-ink-300">
Independent pilots start no new jobs after {sleepLabel(state.fleetSleep.afterHours)} without
orders. Any decision wakes them.
</p>
<BackToWork />
</Panel>
);
}