From c11d52f96fa7873428dbf71e7e5425bfa73db60f Mon Sep 17 00:00:00 2001 From: fireshaper Date: Sat, 3 Oct 2026 22:51:00 -0400 Subject: [PATCH] Step 10E.8: locations, map panels, title screen and dialogs Co-Authored-By: Claude Opus 5.5 --- apps/web/src/App.tsx | 2 +- apps/web/src/components/AwaySummary.tsx | 159 ++++++++++++------------ apps/web/src/components/DealModal.tsx | 82 ++++++------ apps/web/src/map/MapCanvas.tsx | 5 +- apps/web/src/screens/Locations.tsx | 109 +++++++++++----- apps/web/src/screens/MapScreen.tsx | 81 ++++++++---- apps/web/src/screens/StartScreen.tsx | 42 +++++-- memory-bank/progress.md | 1 + 8 files changed, 304 insertions(+), 177 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index fbdce8a..dc788e4 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -24,7 +24,7 @@ const MapScreen = lazy(() => import("./screens/MapScreen").then((m) => ({ defaul const SCREEN_COMPONENTS: Record<(typeof SCREENS)[number]["path"], ReactNode> = { "/": , "/map": ( - Loading map…

}> + Loading map…

}>
), diff --git a/apps/web/src/components/AwaySummary.tsx b/apps/web/src/components/AwaySummary.tsx index 108c044..e35166e 100644 --- a/apps/web/src/components/AwaySummary.tsx +++ b/apps/web/src/components/AwaySummary.tsx @@ -4,6 +4,7 @@ import { useNavigate } from "react-router"; import { formatCredits, locationName } from "../format"; import { formatLocalTime } from "../screens/DispatchSheet"; import { useGameAction, useLoadedPlayer } from "../state/actions"; +import { Button, Icon, Sheet } from "../ui"; /** "While you were away": deliveries resolved since the player last looked (Step 9.2). */ export function AwaySummary() { @@ -31,87 +32,91 @@ export function AwaySummary() { }; return ( -
-
-

- While you were away -

-
    - {summaries.map((s) => ( -
  • -
    {shipName(s.shipId, s.tutorial)}
    -
    + +
      + {summaries.map((s) => ( +
    • +
      + + {shipName(s.shipId, s.tutorial)} + + {locationName(s.originId)} → {locationName(s.destinationId)} + +
      +
      + = 0 ? "text-emerald-300" : "text-rose-300"}> + {s.credits >= 0 ? "+" : ""} + {formatCredits(s.credits)} + + +{s.pilotXp} XP +
      + {s.loanRepayment > 0 && ( +
      + {formatCredits(s.loanRepayment)} went to the lender
      -
      - = 0 ? "text-emerald-400" : "text-red-400"}> - {s.credits >= 0 ? "+" : ""} - {formatCredits(s.credits)} - - +{s.pilotXp} XP + )} + {s.scriptedId && ( +

      + { + content.scriptedTutorialContracts.find((c) => c.id === s.scriptedId) + ?.flavorMessage + } +

      + )} + {s.discoveredLocationId && ( +
      + + Discovered {locationName(s.discoveredLocationId)}!
      - {s.loanRepayment > 0 && ( -
      - {formatCredits(s.loanRepayment)} went to the lender -
      - )} - {s.scriptedId && ( -

      - { - content.scriptedTutorialContracts.find((c) => c.id === s.scriptedId) - ?.flavorMessage - } -

      - )} - {s.discoveredLocationId && ( -
      - Discovered {locationName(s.discoveredLocationId)}! -
      - )} - {s.restEndsAt !== null && ( -
      + )} + {s.restEndsAt !== null && ( +
      + + {state.pilots.find((p) => p.id === s.pilotId)?.name ?? "The pilot"} is resting until {formatLocalTime(s.restEndsAt)} -
      - )} -
    • - ))} -
    - {readyGoals.length > 0 && ( -
    -

    Goals ready to claim

    -
      - {readyGoals.map((goal) => ( -
    • - {goal.title} +{formatCredits(goal.reward)} -
    • - ))} -
    - -
    - )} - -
    -
+ +
+ )} + + ))} + + {readyGoals.length > 0 && ( +
+

+ + Goals ready to claim +

+
    + {readyGoals.map((goal) => ( +
  • + {goal.title}{" "} + + +{formatCredits(goal.reward)} + +
  • + ))} +
+ +
+ )} + + ); } diff --git a/apps/web/src/components/DealModal.tsx b/apps/web/src/components/DealModal.tsx index 921d370..38371b3 100644 --- a/apps/web/src/components/DealModal.tsx +++ b/apps/web/src/components/DealModal.tsx @@ -5,6 +5,7 @@ import { useApi } from "../api/context"; import { formatCredits } from "../format"; import { errorMessage, useLoadedPlayer } from "../state/actions"; import { PLAYER_STATE_KEY } from "../state/player"; +import { Art, Button, Sheet } from "../ui"; /** * The one-time deal (decision 0.5.30): shown when the server says it is @@ -30,46 +31,53 @@ export function DealModal() { const courier = content.shipModel("courier")!; return ( -
-
-

- An offer from Halvorsen -

-

+ +

+ +

Old Halvorsen is retiring. He'll sell you his Courier and his Earth bay. Pay him back out of every job.

-
-
You get
-
A {courier.name} and a bay at Earth
-
Loan
-
- {formatCredits(dealLoanAmount)} -
-
Repayment
-
- {Math.round(loanRepaymentShare * 100)}% of every payout until it's paid off -
-
- {accept.isError && ( -

- {errorMessage(accept.error)} -

- )} -
-
+
+
+ You get +
+
A {courier.name} and a bay at Earth
+
Loan
+
+ {formatCredits(dealLoanAmount)} +
+
+ Repayment +
+
+ {Math.round(loanRepaymentShare * 100)}% of every payout until it's paid off +
+
+ {accept.isError && ( +

+ {errorMessage(accept.error)} +

+ )} + + ); } diff --git a/apps/web/src/map/MapCanvas.tsx b/apps/web/src/map/MapCanvas.tsx index 722bd5c..db5b89e 100644 --- a/apps/web/src/map/MapCanvas.tsx +++ b/apps/web/src/map/MapCanvas.tsx @@ -157,7 +157,10 @@ export function MapCanvas({ className="relative h-full w-full touch-none overflow-hidden" > {loading && ( -

+

Loading map…

)} diff --git a/apps/web/src/screens/Locations.tsx b/apps/web/src/screens/Locations.tsx index 5aea0ae..c6fdf63 100644 --- a/apps/web/src/screens/Locations.tsx +++ b/apps/web/src/screens/Locations.tsx @@ -4,6 +4,7 @@ import { useClock } from "../clock/clock"; import { cargoName, formatCredits, formatMillionKm } from "../format"; import { distanceAt } from "../game"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; +import { Art, Button, buttonClass, Icon, Panel, Pill, Screen, ScreenHeader } from "../ui"; const TYPE_LABELS: Record = { "atmospheric-world": "Atmospheric world", @@ -24,72 +25,120 @@ export function Locations() { const lease = useGameAction("leaseBay"); return ( -
-

Locations

+ + {lease.isError && ( -

+

{errorMessage(lease.error)}

)} -
    +
      {content.locations.map((location) => { const discovered = state.locations.find((l) => l.id === location.id)?.discovered; if (!discovered) { return ( -
    • -
      {location.name}
      -
      Undiscovered — deliver here to unlock.
      -
    • + +
      + + +
      +
      +
      + {location.name} +
      +
      Undiscovered — deliver here to unlock.
      +
      +
      ); } const hasBay = state.bays.some((b) => b.locationId === location.id); const affordable = state.company.credits >= location.bayLeasePrice; return ( -
    • -
      - {location.name} - - {location.id === "earth" - ? "Home" - : `${formatMillionKm(distanceAt("earth", location.id, now))} from Earth`} - + +
      + +
      +
      + + {location.name} + + {hasBay && ( + + HQ + + )} +
      + + {location.id === "earth" + ? "Home" + : `${formatMillionKm(distanceAt("earth", location.id, now))} from Earth`} + +
      -
      -
      Type
      +
      +
      + Type +
      {TYPE_LABELS[location.type]}
      -
      Landing
      +
      + Landing +
      {LANDING_LABELS[location.landingRequirement]}
      -
      Produces
      +
      + Produces +
      {location.produces.map(cargoName).join(", ") || "—"}
      -
      Demands
      +
      + Demands +
      {location.demands.map(cargoName).join(", ") || "—"}
      + View contracts {hasBay ? ( -

      Your HQ is here

      +

      + + Your HQ is here +

      ) : ( <> -

      {HQ_EXPLANATION}

      - + )} -
    • + ); })}
    -
+ ); } diff --git a/apps/web/src/screens/MapScreen.tsx b/apps/web/src/screens/MapScreen.tsx index 6c2225e..bd008dd 100644 --- a/apps/web/src/screens/MapScreen.tsx +++ b/apps/web/src/screens/MapScreen.tsx @@ -12,6 +12,7 @@ import type { MapSelection } from "../map/hitTest"; import { MapCanvas } from "../map/MapCanvas"; import { modelsReachingFromEarth } from "../map/reach"; import { useLoadedPlayer } from "../state/actions"; +import { Art, buttonClass, Icon } from "../ui"; const TYPE_LABELS: Record = { "atmospheric-world": "Atmospheric world", @@ -35,7 +36,7 @@ export function MapScreen() { @@ -51,10 +52,12 @@ export function MapScreen() { function Panel({ title, + art, onClose, children, }: { title: string; + art?: ReactNode; onClose: () => void; children: ReactNode; }) { @@ -62,15 +65,25 @@ function Panel({
-
-

{title}

- +
+ {art} +
+
+

{title}

+ +
+
{children}
+
-
{children}
); } @@ -85,31 +98,46 @@ function LocationPanel({ locationId, onClose }: { locationId: string; onClose: ( const reachable = discovered ? [] : modelsReachingFromEarth(locationId, now); return ( - + + } + > {discovered ? ( <> -

+

{TYPE_LABELS[location.type]} ·{" "} {location.id === "earth" ? "Home" : `${formatMillionKm(distanceAt("earth", location.id, now))} from Earth`}

-

+

Produces {location.produces.map(cargoName).join(", ") || "nothing"} · demands{" "} {location.demands.map(cargoName).join(", ") || "nothing"}

+

+ {hasBay ? "Your HQ is here." : "No HQ here."} +

+ View contracts -

{hasBay ? "Your HQ is here." : "No HQ here."}

) : ( <> -

Undiscovered — deliver here to unlock.

-

+

+ + Undiscovered — deliver here to unlock. +

+

{reachable.length > 0 ? `Reachable with: ${reachable.map((m) => m.name).join(", ")}` : "No ship can reach it from Earth right now."} @@ -129,16 +157,27 @@ function FlightPanel({ dispatchId, onClose }: { dispatchId: string; onClose: () ? "Company loaner" : (state.ships.find((s) => s.id === dispatch.shipId)?.name ?? "Ship"); const pilotName = state.pilots.find((p) => p.id === dispatch.pilotId)?.name ?? "Unknown pilot"; + const shipSprite = dispatch.tutorial + ? "ship-company-courier" + : (content.shipModel(state.ships.find((s) => s.id === dispatch.shipId)?.modelId ?? "") + ?.sprite ?? "ship-courier"); return ( - -

+ } + > +

{locationName(dispatch.originId)} → {locationName(dispatch.destinationId)}

-

Pilot: {pilotName}

-

- Arrives in ( - {formatGameTime(realToGame(dispatch.arrivalAt, content.config))}) +

Pilot: {pilotName}

+

+ Arrives in{" "} + + + {" "} + ({formatGameTime(realToGame(dispatch.arrivalAt, content.config))})

); diff --git a/apps/web/src/screens/StartScreen.tsx b/apps/web/src/screens/StartScreen.tsx index 10c5401..124d043 100644 --- a/apps/web/src/screens/StartScreen.tsx +++ b/apps/web/src/screens/StartScreen.tsx @@ -2,6 +2,7 @@ import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useNavigate } from "react-router"; import { useApi } from "../api/context"; import { PLAYER_STATE_KEY } from "../state/player"; +import { Art, Button, Icon } from "../ui"; /** First launch: create a guest account (decision: guests first, accounts later). */ export function StartScreen() { @@ -19,25 +20,46 @@ export function StartScreen() { }); return ( -
-
-

DEADHEAD

-

Space Freight Tycoon

+
+ -

+ +

+

+ DEADHEAD +

+

+ Space Freight Tycoon +

+
+

Fly company runs between Earth and the Moon, save up for your own ship, and build a freight empire across the solar system.

- + {start.isError && ( -

+

Could not start a company. Check your connection and try again.

)} diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 98441cb..18c0a2c 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -186,3 +186,4 @@ Spec: `memory-bank/change-requests/CR-005-ui-overhaul.md`. New decisions recorde - [x] **Step 10E.5 — Contracts and dispatch.** Contracts uses `Screen`/`ScreenHeader` ("Freight market", or "Halvorsen Lunar Freight" in the tutorial). Location switcher: scrollable planet-art chips with cyan idle counts, amber glow when pressed. Contract cards (`Panel as="li"`, two columns from `sm`): route and large emerald payout, `RouteStrip`, cargo line, requirements in amber, expiry, and a Dispatch button; first contact cards are amber feature panels with a star pill. Board refresh countdown and the "nothing here" prompt (cyan panel, icon buttons) restyled. Tutorial board: intro panel with the loaner's art; suggested (amber, glowing) and scripted (fuchsia) feature cards with Pay/Time/XP stats and Fly it. Dispatch sheet on `Sheet` (route strip; each ship with its model art, the amber-glowing selection, problems as ✕ lines, numbers as stat cells). `PairOption.modelSprite` added. **`Sheet` now portals to `body`**: the screens' rise-in transform made the sheet's `fixed` overlay position inside the content column. Existing contracts, tutorial, dispatch, duty and board refresh tests pass unchanged. **Manual check:** scratchpad screenshots reviewed (final set in 10E.9). - [x] **Step 10E.6 — Fleet and Market.** Fleet: header eyebrow "N ships · M flying"; ship cards (two columns from `md`) with the model's art on a cyan glow (tilted in flight), an Idle/In flight pill, route or location and the pilot (amber "No pilot"), a cyan `Meter` with percent and arrival countdown, and full-width "Find work here" / "Deadhead (fly empty)" buttons; an empty fleet shows a dimmed Courier. Deadhead picker on `Sheet`, each destination with its planet art. Market ("Shipyard"): showroom cards (three columns from `md`) with large tilted ship art over a glow, price (amber when affordable), spec lines with icons and bars relative to the best model (range on a log scale, since it spans 5–1,000M km), and a full-width Buy. Delivery picker on `Sheet`, radio rows with planet art that glow when checked. Existing fleet and market tests pass unchanged. **Manual check:** scratchpad screenshots reviewed. - [x] **Step 10E.7 — Pilots.** Header eyebrow "N on the roster". Hire panel is an amber feature panel with an icon, styled select and button. Pilot cards (two columns from `md`): initials disc (amber glow for you, cyan for hires) with a level badge, "Level N", XP text and a decorative violet XP bar (not a `progressbar`: a resting pilot's card must have none), an In flight pill or location, the ship; skills on an inset panel as "Name n/max" with pips and a glowing + raise button; skill points in amber when available; duty as an amber `Meter` that turns rose from 85 %; rest as a sky panel with a moon icon; Rest now / confirmation and assign with kit buttons and selects. Existing pilots and duty tests pass unchanged. **Manual check:** scratchpad screenshots reviewed. +- [x] **Step 10E.8 — Locations, map panels, start screen and dialogs.** Locations ("Known space"): planet-art cards (two columns from `md`), emerald tone and an HQ pill where you have a bay, distance in cyan, specs on an inset `dl`, View contracts as a button, Lease bay as a full-width primary; undiscovered places dimmed and greyed with a padlock. Map: Reset view and the tap panels restyled (glass panel with the location's or ship's art, icon close button, View contracts as a button; a side card from `md`). Start screen is a title screen: gradient DEADHEAD wordmark, the Sun, Earth, Moon and a Courier around it, and a large Start button. "While you were away" and the deal are on `Sheet` ("Logbook" with big emerald credits and violet XP, discoveries with a star, goals panel with a success button; the deal as an amber "Incoming transmission" with the Courier's art and a large "Shake on it"). The last `slate-*` classes are gone from `apps/web/src`. Existing locations, map, onboarding, away and deal tests pass unchanged. **Manual check:** scratchpad screenshots reviewed.