ui/phase-13: planet inspector — read-only
Plumbs the map → inspector pathway: a click on a planet selects it through the new SelectionStore, the sidebar Inspector tab swaps its empty-state copy for a per-kind read-only field set, and a mobile-only bottom-sheet mirrors the same content over the map. Field projection in api/game-state.ts now surfaces every documented planet field.
This commit is contained in:
@@ -8,10 +8,16 @@ the existing renderer instance alive). Empty-planet reports render
|
||||
the empty world without errors — the regression test in
|
||||
`tests/e2e/game-shell-map.spec.ts` covers this.
|
||||
|
||||
Phase 9 owns the renderer's hit-test and pan/zoom semantics; Phase 13
|
||||
will plug map clicks into the inspector. Phase 29 wires the wrap-mode
|
||||
toggle on top of the per-game `wrapMode` preference the store
|
||||
already manages.
|
||||
Phase 9 owns the renderer's hit-test and pan/zoom semantics. Phase 13
|
||||
plugs map clicks into the inspector by translating the renderer's
|
||||
`clicked` event into a hit-test, looking the planet up by id in the
|
||||
report, and calling `SelectionStore.selectPlanet`. The selection
|
||||
store, set in the layout, drives both the desktop sidebar inspector
|
||||
tab and the mobile bottom-sheet — the map view itself does not need
|
||||
to know which surface is showing the result.
|
||||
|
||||
Phase 29 wires the wrap-mode toggle on top of the per-game `wrapMode`
|
||||
preference the store already manages.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy, onMount, untrack } from "svelte";
|
||||
@@ -26,8 +32,13 @@ already manages.
|
||||
GAME_STATE_CONTEXT_KEY,
|
||||
type GameStateStore,
|
||||
} from "$lib/game-state.svelte";
|
||||
import {
|
||||
SELECTION_CONTEXT_KEY,
|
||||
type SelectionStore,
|
||||
} from "$lib/selection.svelte";
|
||||
|
||||
const store = getContext<GameStateStore | undefined>(GAME_STATE_CONTEXT_KEY);
|
||||
const selection = getContext<SelectionStore | undefined>(SELECTION_CONTEXT_KEY);
|
||||
|
||||
let canvasEl: HTMLCanvasElement | null = $state(null);
|
||||
let containerEl: HTMLDivElement | null = $state(null);
|
||||
@@ -37,6 +48,7 @@ already manages.
|
||||
let mountedTurn: number | null = null;
|
||||
let mountedGameId: string | null = null;
|
||||
let onResize: (() => void) | null = null;
|
||||
let detachClick: (() => void) | null = null;
|
||||
let mounted = false;
|
||||
|
||||
$effect(() => {
|
||||
@@ -70,6 +82,10 @@ already manages.
|
||||
mode: "torus" | "no-wrap",
|
||||
): Promise<void> {
|
||||
if (canvasEl === null || containerEl === null) return;
|
||||
if (detachClick !== null) {
|
||||
detachClick();
|
||||
detachClick = null;
|
||||
}
|
||||
if (handle !== null) {
|
||||
handle.dispose();
|
||||
handle = null;
|
||||
@@ -92,6 +108,7 @@ already manages.
|
||||
);
|
||||
handle.viewport.setZoom(minScale * 1.05, true);
|
||||
if (mode === "no-wrap") handle.setMode("no-wrap");
|
||||
detachClick = handle.onClick(handleMapClick);
|
||||
mountedTurn = report.turn;
|
||||
mountedGameId = store?.gameId ?? "";
|
||||
mountError = null;
|
||||
@@ -100,6 +117,25 @@ already manages.
|
||||
}
|
||||
}
|
||||
|
||||
// handleMapClick translates a renderer click into a planet
|
||||
// selection. A click that misses every primitive (empty space) is
|
||||
// a deliberate no-op: the selection rule for Phase 13 is that
|
||||
// only the explicit close button on the mobile sheet clears the
|
||||
// current selection.
|
||||
function handleMapClick(cursorPx: { x: number; y: number }): void {
|
||||
if (handle === null || store?.report === undefined || store.report === null) {
|
||||
return;
|
||||
}
|
||||
if (selection === undefined) return;
|
||||
const hit = handle.hitAt(cursorPx);
|
||||
if (hit === null) return;
|
||||
if (hit.primitive.kind !== "point") return;
|
||||
const planetId = hit.primitive.id;
|
||||
const planet = store.report.planets.find((p) => p.number === planetId);
|
||||
if (planet === undefined) return;
|
||||
selection.selectPlanet(planet.number);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
mounted = true;
|
||||
onResize = (): void => {
|
||||
@@ -115,6 +151,10 @@ already manages.
|
||||
window.removeEventListener("resize", onResize);
|
||||
onResize = null;
|
||||
}
|
||||
if (detachClick !== null) {
|
||||
detachClick();
|
||||
detachClick = null;
|
||||
}
|
||||
if (handle !== null) {
|
||||
handle.dispose();
|
||||
handle = null;
|
||||
|
||||
Reference in New Issue
Block a user