diff --git a/apps/web/src/components/GoalsCard.tsx b/apps/web/src/components/GoalsCard.tsx index df8521d..e64657d 100644 --- a/apps/web/src/components/GoalsCard.tsx +++ b/apps/web/src/components/GoalsCard.tsx @@ -15,8 +15,8 @@ export function GoalsCard() { return ( -

- +

+ Goals

    @@ -28,23 +28,21 @@ export function GoalsCard() {
  • - + {details.title} - + +{formatCredits(details.reward)} @@ -55,7 +53,7 @@ export function GoalsCard() { size="sm" disabled={claim.isPending} onClick={() => claim.mutate({ goalId: goal.id })} - className="shrink-0 animate-glow" + className="shrink-0" > Claim diff --git a/apps/web/src/components/IndependentPilots.tsx b/apps/web/src/components/IndependentPilots.tsx index af5d44e..6f1134e 100644 --- a/apps/web/src/components/IndependentPilots.tsx +++ b/apps/web/src/components/IndependentPilots.tsx @@ -27,9 +27,9 @@ export function DispatchOfficePanel() { Dispatch Office

    - Your hired pilots can run jobs on their own. Set them to Independent on{" "} - - Pilots + Your hired pilots can run jobs on their own. Set them to Independent in{" "} + + Crew .

    @@ -37,13 +37,15 @@ export function DispatchOfficePanel() { ); } return ( - -

    Dispatch Office

    + +

    Dispatch Office

    Lets your hired pilots run jobs on their own, so the company keeps earning while you're away.

    diff --git a/apps/web/src/components/NeedsYou.tsx b/apps/web/src/components/NeedsYou.tsx new file mode 100644 index 0000000..b0ec7b1 --- /dev/null +++ b/apps/web/src/components/NeedsYou.tsx @@ -0,0 +1,198 @@ +import { content } from "@deadhead/content"; +import type { PlayerState } from "@deadhead/schema"; +import { Link } from "react-router"; +import { formatClock, formatCredits, locationName } from "../format"; +import { useLoadedPlayer } from "../state/actions"; +import { Icon } from "../ui"; +import { readyShips } from "../game"; +import type { ScreenPath } from "../routes"; +import { Countdown } from "./Countdown"; +import { useRadioLater } from "./RadioCalls"; + +/** One thing waiting on the player, and the tab whose badge counts it (decision 0.5.168). */ +export type NeedsYouItem = + | { + kind: "radio"; + tab: ScreenPath; + count: 1; + dispatchId: string; + eventId: string; + pilotId: string; + endsAt: number; + } + | { kind: "ready"; tab: ScreenPath; count: 1; name: string; locationId: string | null } + | { kind: "skills"; tab: ScreenPath; count: number } + | { kind: "goal"; tab: ScreenPath; count: 1; goalId: string }; + +/** Everything waiting on the player, most urgent first. */ +export function needsYou(state: PlayerState): NeedsYouItem[] { + const items: NeedsYouItem[] = state.radioCalls.map((call) => ({ + kind: "radio", + tab: "/", + count: 1, + dispatchId: call.dispatchId, + eventId: call.eventId, + pilotId: call.pilotId, + endsAt: call.windowEndsAt, + })); + if (state.tutorial) { + // The company loaner isn't in the ships list. + if (!state.tutorial.runInFlight) { + items.push({ + kind: "ready", + tab: "/contracts", + count: 1, + name: "Company loaner", + locationId: state.tutorial.loanerLocationId, + }); + } + } else { + for (const ship of readyShips(state)) { + items.push({ + kind: "ready", + tab: "/contracts", + count: 1, + name: ship.name, + locationId: ship.locationId, + }); + } + } + const points = state.pilots.reduce((sum, p) => sum + p.skillPointsAvailable, 0); + if (points > 0) items.push({ kind: "skills", tab: "/fleet", count: points }); + for (const goal of state.goals.filter((g) => g.status === "ready")) { + items.push({ kind: "goal", tab: "/", count: 1, goalId: goal.id }); + } + return items; +} + +const ROW = "flex min-h-14 w-full items-center gap-3 py-2 text-left"; + +function Dot({ tone }: { tone: "problem" | "action" | "earned" | "ink" }) { + const fill = { + problem: "bg-problem", + action: "bg-action", + earned: "bg-earned", + ink: "bg-ink-300", + }[tone]; + return