f7109af55c
Two follow-up fixes after the initial Phase 19 landing:
1. The IncomingGroup dashed trajectory was drawn between raw
(x1, y1) and (x2, y2) world coordinates. On torus wrap mode
this took the long way around when origin and destination
sat near opposite seams. The line now picks endpoints via
`torusShortestDelta` so the segment crosses the seam when
that's the shorter visual path. The interpolated clickable
point follows the same unwrapped vector. The same helper
fixes the in-hyperspace position for local / foreign groups.
2. On-planet local and foreign groups previously rendered as
small offset points next to every populated planet, which
turned the canvas into noise as soon as a player held more
than a handful of planets. The map no longer renders any
in-orbit group; the planet inspector grows a compact
"stationed ship groups" subsection
(`lib/inspectors/planet/ship-groups.svelte`) that lists
each in-orbit group as a row of `<race> · <class> · <count>
ships · <mass>`. Race attribution: LocalGroup → the player's
race, OtherGroup on a foreign-owned planet → the planet's
owner, OtherGroup elsewhere → "foreign" placeholder. Rows
are non-interactive in Phase 19; Phase 21+ will deep-link
into the ship-groups table view with a (planet, race) filter.
Tests:
- `state-binding-groups.test.ts` swaps the on-planet rendering
expectation for the new "no map primitive" rule, and adds a
regression that asserts the incoming line crosses the torus
seam via `torusShortestDelta`.
- new `inspector-planet-ship-groups.test.ts` covers row
composition, the destination-mismatch filter, the
in-hyperspace exclusion, the foreign-planet owner fallback,
and the empty-state collapse.
- `inspector-planet.test.ts` and `inspector-ship-group.spec.ts`
pick up the new prop chain (`localShipGroups`,
`otherShipGroups`, `localRace`).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
138 lines
4.3 KiB
Svelte
138 lines
4.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 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 localShipGroups = $derived(
|
|
renderedReport?.report?.localShipGroups ?? [],
|
|
);
|
|
const otherShipGroups = $derived(
|
|
renderedReport?.report?.otherShipGroups ?? [],
|
|
);
|
|
const localRace = $derived(renderedReport?.report?.race ?? "");
|
|
</script>
|
|
|
|
<section class="tool" data-testid="sidebar-tool-inspector">
|
|
{#if selectedPlanet !== null}
|
|
<Planet
|
|
planet={selectedPlanet}
|
|
{localShipClass}
|
|
{routes}
|
|
planets={allPlanets}
|
|
{mapWidth}
|
|
{mapHeight}
|
|
{localPlayerDrive}
|
|
{localShipGroups}
|
|
{otherShipGroups}
|
|
{localRace}
|
|
/>
|
|
{:else if selectedShipGroup !== null}
|
|
<ShipGroup selection={selectedShipGroup} planets={allPlanets} />
|
|
{: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: #888;
|
|
}
|
|
</style>
|