From 29290de4786b3b3ede60e5d23f764b08d3a22248 Mon Sep 17 00:00:00 2001 From: fireshaper Date: Sat, 3 Oct 2026 07:06:55 -0400 Subject: [PATCH] Step 10.1: add PixiJS system map Co-Authored-By: Claude Opus 5.5 --- DECISIONS.md | 10 ++ apps/web/package.json | 1 + apps/web/src/App.tsx | 10 +- apps/web/src/layout/Layout.tsx | 2 +- apps/web/src/map/MapCanvas.tsx | 149 +++++++++++++++++++++++++++ apps/web/src/map/camera.ts | 58 +++++++++++ apps/web/src/map/projection.ts | 53 ++++++++++ apps/web/src/map/scene.ts | 36 +++++++ apps/web/src/routes.ts | 7 +- apps/web/src/screens/MapScreen.tsx | 21 ++++ apps/web/test/map-canvas-double.tsx | 6 ++ apps/web/test/map-projection.test.ts | 74 +++++++++++++ apps/web/test/setup.ts | 6 +- memory-bank/progress.md | 6 ++ pnpm-lock.yaml | 78 ++++++++++++++ 15 files changed, 511 insertions(+), 6 deletions(-) create mode 100644 apps/web/src/map/MapCanvas.tsx create mode 100644 apps/web/src/map/camera.ts create mode 100644 apps/web/src/map/projection.ts create mode 100644 apps/web/src/map/scene.ts create mode 100644 apps/web/src/screens/MapScreen.tsx create mode 100644 apps/web/test/map-canvas-double.tsx create mode 100644 apps/web/test/map-projection.test.ts diff --git a/DECISIONS.md b/DECISIONS.md index 45dc514..923857a 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -453,3 +453,13 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Two levels of network error:** if the first player-state load fails there is nothing to show, so the app shows an error with Retry. If a later refresh fails, the last state stays on screen under an "Offline — showing last update" banner with Retry, which serves every screen (Phase 11 builds on this for offline mode). Contract boards have their own error and Retry. - **Market and Locations have no empty state:** they list static content, which always exists. Their error handling is the app-level one. - **The placeholder screen is gone;** every route has a real screen. + +## Step 10.1 + +- **PixiJS v8 used directly** (an `Application` created in an effect), not `@pixi/react`: the map is one imperative canvas redrawn each frame from player state and the server clock, which needs no React reconciliation. The effect copes with StrictMode double-mounting. +- **Radial scale is a square root of the distance from the Sun,** normalised so 1,000M km is the map edge. Order is preserved, and Jupiter (~780M km) sits about 2.3× Earth's map radius instead of 5×. Directions from the Sun are unchanged. +- **The Moon is drawn a fixed small offset from Earth** in its true direction; at map scale it would sit on top of Earth. +- **All map logic is plain, testable functions** (projection, camera, scene; hit-testing in Step 10.3). jsdom has no WebGL, so component tests replace `MapCanvas` with a test double (`test/setup.ts`). +- **Camera state (zoom, pan) is a Zustand store**, per the tech stack; zoom is clamped to 0.5–40×, and "Reset view" restores it. +- **The Map screen is lazy-loaded,** so PixiJS (~285 KB) downloads only when the map is opened. +- **Seven tabs:** a Map tab is added after HQ. To fit 375 px, the Contracts tab is labelled "Jobs" and Market "Shop"; screen titles are unchanged. diff --git a/apps/web/package.json b/apps/web/package.json index 83f6451..642e29b 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -14,6 +14,7 @@ "@deadhead/schema": "workspace:*", "@deadhead/sim": "workspace:*", "@tanstack/react-query": "^5.104.1", + "pixi.js": "^8.22.0", "react": "^19.3.0", "react-dom": "^19.3.0", "react-router": "^8.4.0", diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index b82dc9e..89dc96d 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -1,4 +1,4 @@ -import { useEffect, type ReactNode } from "react"; +import { lazy, Suspense, useEffect, type ReactNode } from "react"; import { Route, Routes } from "react-router"; import { useApi } from "./api/context"; import { startClockTicker, syncClock } from "./clock/clock"; @@ -15,8 +15,16 @@ import { Pilots } from "./screens/Pilots"; import { StartScreen } from "./screens/StartScreen"; import { usePlayerState, useRefetchOnArrival } from "./state/player"; +// The map pulls in PixiJS; load it only when the Map tab is opened. +const MapScreen = lazy(() => import("./screens/MapScreen").then((m) => ({ default: m.MapScreen }))); + const SCREEN_COMPONENTS: Record<(typeof SCREENS)[number]["path"], ReactNode> = { "/": , + "/map": ( + Loading map…

}> + +
+ ), "/contracts": , "/fleet": , "/pilots": , diff --git a/apps/web/src/layout/Layout.tsx b/apps/web/src/layout/Layout.tsx index a21e1cc..1b3acc4 100644 --- a/apps/web/src/layout/Layout.tsx +++ b/apps/web/src/layout/Layout.tsx @@ -32,7 +32,7 @@ export function Layout({ credits = "—", clock = , banner }: Layou