Files
galaxy-game/ui/frontend/src/lib/sidebar/inspector-tab.svelte
T
Ilia Denisov 680ebac919
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Failing after 5m16s
feat(ui): F8-12 — map polish (zoom invariance, labels, selection, soft radius) (#55)
* Honest pixel-space sizing for `pointRadiusPx` / `strokeWidthPx`: the
  renderer divides by the current camera scale on every
  `viewport.zoomed` so thin lines / small markers stay the same on-screen
  size at any zoom.
* Known-size planets switch to `pointRadiusWorld`, softened against the
  reference scale by `PLANET_SIZE_ZOOM_ALPHA = 0.33`; unidentified
  planets pin to a 3-px disc.
* New planet label layer renders a two-line `name / #N` legend under
  each planet (`#N` only for unidentified or when the new `planetNames`
  toggle is off). Selection now paints an inverse-fill frame around the
  selected planet's label plus an outline on the disc; the old
  selection-ring primitive is retired.
* Bombing markers swap the separate CirclePrim for a planet-outline
  overlay (damaged / wiped colour); the report deep-link moves to a
  "view bombing report" link in the planet inspector.
* Docs + tests follow: `renderer.md` reflects the new sizing contract +
  label / outline layers, vitest covers the sizing math, label
  formatting, and the new toggle, and the map-toggles e2e adds a
  persistence case for `planetNames`.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-27 23:51:16 +02:00

170 lines
5.3 KiB
Svelte

<!--
Inspector sidebar tool. Reads the per-game `SelectionStore` and the
`GameStateStore` from context (both set by the in-game shell layout).
When a planet selection resolves to a live `ReportPlanet` in the
current report, the tab swaps the empty-state copy for the read-
only planet inspector. A selection that points at a planet missing
from the current report (e.g. visibility lost between turns) falls
back to the empty state instead of holding stale data.
Phase 19 widens the dispatch: a `kind === "shipGroup"` selection
resolves against the matching report array and mounts the read-only
ship-group inspector. Unresolvable refs (e.g. the chosen index has
fallen out of the new turn's report) cleanly collapse to the empty
state — same fallback as a stale planet selection.
The empty-state copy still matches the IA section verbatim — `select
an object on the map` — so the no-selection experience is unchanged
from the Phase 10 stub.
-->
<script lang="ts">
import { getContext } from "svelte";
import { i18n } from "$lib/i18n/index.svelte";
import {
SELECTION_CONTEXT_KEY,
type SelectionStore,
} from "$lib/selection.svelte";
import {
RENDERED_REPORT_CONTEXT_KEY,
type RenderedReportSource,
} from "$lib/rendered-report.svelte";
import Planet from "$lib/inspectors/planet.svelte";
import ShipGroup, {
type ShipGroupSelection,
} from "$lib/inspectors/ship-group.svelte";
const renderedReport = getContext<RenderedReportSource | undefined>(
RENDERED_REPORT_CONTEXT_KEY,
);
const selection = getContext<SelectionStore | undefined>(
SELECTION_CONTEXT_KEY,
);
const selectedPlanet = $derived.by(() => {
const sel = selection?.selected;
if (sel === undefined || sel === null || sel.kind !== "planet") return null;
const report = renderedReport?.report;
if (report === undefined || report === null) return null;
return report.planets.find((p) => p.number === sel.id) ?? null;
});
const selectedShipGroup: ShipGroupSelection | null = $derived.by(() => {
const sel = selection?.selected;
if (sel === undefined || sel === null || sel.kind !== "shipGroup") {
return null;
}
const report = renderedReport?.report;
if (report === undefined || report === null) return null;
const ref = sel.ref;
switch (ref.variant) {
case "local": {
const group = report.localShipGroups.find((g) => g.id === ref.id);
if (group === undefined) return null;
return { variant: "local", group };
}
case "other": {
const group = report.otherShipGroups[ref.index];
if (group === undefined) return null;
return { variant: "other", group };
}
case "incoming": {
const group = report.incomingShipGroups[ref.index];
if (group === undefined) return null;
return { variant: "incoming", group };
}
case "unidentified": {
const group = report.unidentifiedShipGroups[ref.index];
if (group === undefined) return null;
return { variant: "unidentified", group };
}
}
});
const localShipClass = $derived(
renderedReport?.report?.localShipClass ?? [],
);
const localScience = $derived(renderedReport?.report?.localScience ?? []);
const allPlanets = $derived(renderedReport?.report?.planets ?? []);
const routes = $derived(renderedReport?.report?.routes ?? []);
const mapWidth = $derived(renderedReport?.report?.mapWidth ?? 1);
const mapHeight = $derived(renderedReport?.report?.mapHeight ?? 1);
const localPlayerDrive = $derived(
renderedReport?.report?.localPlayerDrive ?? 0,
);
const localPlayerWeapons = $derived(
renderedReport?.report?.localPlayerWeapons ?? 0,
);
const localPlayerShields = $derived(
renderedReport?.report?.localPlayerShields ?? 0,
);
const localPlayerCargo = $derived(
renderedReport?.report?.localPlayerCargo ?? 0,
);
const localShipGroups = $derived(
renderedReport?.report?.localShipGroups ?? [],
);
const otherShipGroups = $derived(
renderedReport?.report?.otherShipGroups ?? [],
);
const localFleets = $derived(renderedReport?.report?.localFleets ?? []);
const otherRaces = $derived(renderedReport?.report?.otherRaces ?? []);
const localRace = $derived(renderedReport?.report?.race ?? "");
const bombings = $derived(renderedReport?.report?.bombings ?? []);
const selectedPlanetBombing = $derived(
selectedPlanet === null
? null
: (bombings.find((b) => b.planetNumber === selectedPlanet.number) ?? null),
);
</script>
<section class="tool" data-testid="sidebar-tool-inspector">
{#if selectedPlanet !== null}
<Planet
planet={selectedPlanet}
{localShipClass}
{localScience}
{routes}
planets={allPlanets}
{mapWidth}
{mapHeight}
{localPlayerDrive}
{localShipGroups}
{otherShipGroups}
{localRace}
bombing={selectedPlanetBombing}
/>
{:else if selectedShipGroup !== null}
<ShipGroup
selection={selectedShipGroup}
planets={allPlanets}
{localShipClass}
{localFleets}
{otherRaces}
{mapWidth}
{mapHeight}
{localPlayerDrive}
{localPlayerWeapons}
{localPlayerShields}
{localPlayerCargo}
/>
{:else}
<h3>{i18n.t("game.sidebar.tab.inspector")}</h3>
<p>{i18n.t("game.sidebar.empty.inspector")}</p>
{/if}
</section>
<style>
.tool {
font-family: system-ui, sans-serif;
}
.tool > h3 {
margin: 0 0 0.5rem;
padding: 1rem 1rem 0;
font-size: 1rem;
}
.tool > p {
margin: 0;
padding: 0 1rem 1rem;
color: var(--color-text-muted);
}
</style>