680ebac919
* 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>
170 lines
5.3 KiB
Svelte
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>
|