From 45a21a8684dc33b7bcfe07464e201e92352d9ddd Mon Sep 17 00:00:00 2001 From: fireshaper Date: Sat, 3 Oct 2026 22:47:22 -0400 Subject: [PATCH] Step 10E.7: pilot cards with skills, XP and duty Co-Authored-By: Claude Opus 5.5 --- apps/web/src/screens/Pilots.tsx | 212 +++++++++++++++++++------------- memory-bank/progress.md | 1 + 2 files changed, 130 insertions(+), 83 deletions(-) diff --git a/apps/web/src/screens/Pilots.tsx b/apps/web/src/screens/Pilots.tsx index d16fd6b..0f6ee7c 100644 --- a/apps/web/src/screens/Pilots.tsx +++ b/apps/web/src/screens/Pilots.tsx @@ -11,6 +11,7 @@ import { shipModelName, } from "../format"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; +import { Button, Icon, Meter, Panel, Pill, Screen, ScreenHeader } from "../ui"; /** XP needed for the pilot's current and next level, or null at max level. */ export function levelProgress(xp: number, level: number) { @@ -25,20 +26,20 @@ export function levelProgress(xp: number, level: number) { export function Pilots() { const state = useLoadedPlayer(); return ( -
-

Pilots

+ + {!state.pilots.some((p) => !p.isAvatar) && ( -

+

No hired pilots yet. Hire one at a bay to crew another ship.

)} -
    +
      {state.pilots.map((pilot) => ( ))}
    -
+ ); } @@ -50,18 +51,23 @@ function HirePilot() { const hasBay = state.bays.some((b) => b.locationId === bay); return ( -
+ {state.bays.length === 0 ? ( -

Lease a bay to hire pilots there.

+

Lease a bay to hire pilots there.

) : ( -
+
+ + + - +
)} {hire.isError && ( -

+

{errorMessage(hire.error)}

)} -
+
); } @@ -96,23 +101,60 @@ function PilotCard({ pilot }: { pilot: Pilot }) { const ship = state.ships.find((s) => s.id === pilot.assignedShipId); const lockedToLoaner = pilot.isAvatar && state.tutorial !== null; + const initials = pilot.name + .split(" ") + .map((w) => w[0]) + .join("") + .slice(0, 2); + return ( -
  • -
    - - {pilot.name} - {pilot.isAvatar && (you)} + +
    + - Level {pilot.level} +
    +
    + + {pilot.name} + {pilot.isAvatar && (you)} + + + Level {pilot.level} + +
    +
    + {progress + ? `${progress.xp} / ${progress.needed} XP to next level` + : `${pilot.xp} XP · max level`} +
    +
    -
    - {progress - ? `${progress.xp} / ${progress.needed} XP to next level` - : `${pilot.xp} XP · max level`} -
    -
    - {pilot.status === "in-flight" ? "In flight" : `At ${locationName(pilot.locationId ?? "")}`} +
    + {pilot.status === "in-flight" ? ( + + In flight + + ) : ( + `At ${locationName(pilot.locationId ?? "")}` + )} {" · "} {lockedToLoaner ? "Flying the company loaner" @@ -121,19 +163,31 @@ function PilotCard({ pilot }: { pilot: Pilot }) { : "No ship"}
    -
    -
    +
    +
    0 ? "text-amber-300" : "text-ink-400"}`} + > Skill points:{" "} {pilot.skillPointsAvailable}
    -
      +
        {content.skills.map((skill) => { const level = pilot.skills[skill.id] ?? 0; const canRaise = pilot.skillPointsAvailable > 0 && level < skill.maxLevel; return (
      • - - {skill.name} {level}/{skill.maxLevel} + + + {skill.name} {level}/{skill.maxLevel} + + {canRaise && ( )}
      • @@ -151,7 +205,7 @@ function PilotCard({ pilot }: { pilot: Pilot }) { })}
      {spend.isError && ( -

      +

      {errorMessage(spend.error)}

      )} @@ -162,7 +216,7 @@ function PilotCard({ pilot }: { pilot: Pilot }) { {!lockedToLoaner && pilot.assignedShipId === null && pilot.status === "idle" && ( )} - + ); } @@ -179,75 +233,68 @@ function Duty({ pilot, canRest }: { pilot: Pilot; canRest: boolean }) { if (pilot.resting && pilot.restEndsAt !== null) { return ( -
      - Resting · wakes in +
      + + + Resting · wakes in +
      ); } const used = Math.min(pilot.dutyHours, pilot.dutyLimit); return ( -
      -
      +
      +
      Duty - + {formatGameHours(pilot.dutyHours)} of {formatGameHours(pilot.dutyLimit)}
      -
      -
      -
      + = 0.85 ? "rose" : "amber"} + size="sm" + className="mt-1" + /> {canRest && pilot.status === "idle" && !confirming && ( - + )} {confirming && (
      -

      +

      {pilot.name} rests for {formatDuration(restRealMs())} and can't fly until then. Rest always lasts at least one board refresh, so they wake to new contracts.

      - - + +
      )} {rest.isError && ( -

      +

      {errorMessage(rest.error)}

      )} @@ -266,18 +313,18 @@ function AssignShip({ pilot }: { pilot: Pilot }) { if (ships.length === 0) { return ( -

      +

      No uncrewed ships at {locationName(pilot.locationId ?? "")}.

      ); } return ( -
      +
      - + {assign.isError && ( -

      +

      {errorMessage(assign.error)}

      )} diff --git a/memory-bank/progress.md b/memory-bank/progress.md index 217c8b5..98441cb 100644 --- a/memory-bank/progress.md +++ b/memory-bank/progress.md @@ -185,3 +185,4 @@ Spec: `memory-bank/change-requests/CR-005-ui-overhaul.md`. New decisions recorde - [x] **Step 10E.4 — HQ dashboard.** `ScreenHeader` with the company name; icon stat tiles (2 × 2 on phones, 4 across from `md`); two columns from `md`. New `components/RouteStrip` (origin art → dashed line → destination art; with `progress` the line fills and the ship's art rides along; `aria-hidden`, place names drawn with CSS `content` so they never duplicate DOM text). "Next arrival" shows the strip, the route and a large countdown. Tutorial panel: run pips and a "Fly a company run" link to Jobs. Pilot rest with an icon; `GoalsCard` rows with check discs, reward amounts and a glowing Claim; `LoanCard` with an emerald `Meter` and kit input/button. Backdrop overlay lightened (0.5→0.82). Existing HQ, goals, loan and rest tests pass unchanged. **Manual check:** scratchpad screenshots reviewed at desktop and phone width (final set in 10E.9). - [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.