ui/phase-19: torus-aware incoming track + on-planet groups in inspector
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>
This commit is contained in:
@@ -275,6 +275,12 @@ const en = {
|
||||
"game.inspector.ship_group.location.in_hyperspace": "in hyperspace",
|
||||
"game.inspector.ship_group.fleet.none": "—",
|
||||
"game.inspector.ship_group.unidentified_no_data": "no data — only the radar blip is known",
|
||||
|
||||
"game.inspector.planet.ship_groups.title": "stationed ship groups",
|
||||
"game.inspector.planet.ship_groups.row.count": "{count} ships",
|
||||
"game.inspector.planet.ship_groups.row.mass": "mass {mass}",
|
||||
"game.inspector.planet.ship_groups.race.unknown": "unknown",
|
||||
"game.inspector.planet.ship_groups.race.foreign": "foreign",
|
||||
} as const;
|
||||
|
||||
export default en;
|
||||
|
||||
@@ -276,6 +276,12 @@ const ru: Record<keyof typeof en, string> = {
|
||||
"game.inspector.ship_group.location.in_hyperspace": "в гиперпространстве",
|
||||
"game.inspector.ship_group.fleet.none": "—",
|
||||
"game.inspector.ship_group.unidentified_no_data": "данных нет — известны только координаты",
|
||||
|
||||
"game.inspector.planet.ship_groups.title": "корабли на орбите",
|
||||
"game.inspector.planet.ship_groups.row.count": "{count} кораблей",
|
||||
"game.inspector.planet.ship_groups.row.mass": "масса {mass}",
|
||||
"game.inspector.planet.ship_groups.race.unknown": "неизвестно",
|
||||
"game.inspector.planet.ship_groups.race.foreign": "чужие",
|
||||
};
|
||||
|
||||
export default ru;
|
||||
|
||||
@@ -12,6 +12,8 @@ dismiss from the IA section §6 land in Phase 35 polish.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type {
|
||||
ReportLocalShipGroup,
|
||||
ReportOtherShipGroup,
|
||||
ReportPlanet,
|
||||
ReportRoute,
|
||||
ShipClassSummary,
|
||||
@@ -27,6 +29,9 @@ dismiss from the IA section §6 land in Phase 35 polish.
|
||||
mapWidth: number;
|
||||
mapHeight: number;
|
||||
localPlayerDrive: number;
|
||||
localShipGroups: ReportLocalShipGroup[];
|
||||
otherShipGroups: ReportOtherShipGroup[];
|
||||
localRace: string;
|
||||
onMap: boolean;
|
||||
onClose: () => void;
|
||||
};
|
||||
@@ -38,6 +43,9 @@ dismiss from the IA section §6 land in Phase 35 polish.
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
localPlayerDrive,
|
||||
localShipGroups,
|
||||
otherShipGroups,
|
||||
localRace,
|
||||
onMap,
|
||||
onClose,
|
||||
}: Props = $props();
|
||||
@@ -66,6 +74,9 @@ dismiss from the IA section §6 land in Phase 35 polish.
|
||||
{mapWidth}
|
||||
{mapHeight}
|
||||
{localPlayerDrive}
|
||||
{localShipGroups}
|
||||
{otherShipGroups}
|
||||
{localRace}
|
||||
/>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
@@ -15,6 +15,8 @@ field with five buttons.
|
||||
<script lang="ts">
|
||||
import { getContext, tick } from "svelte";
|
||||
import type {
|
||||
ReportLocalShipGroup,
|
||||
ReportOtherShipGroup,
|
||||
ReportPlanet,
|
||||
ReportRoute,
|
||||
ShipClassSummary,
|
||||
@@ -30,6 +32,7 @@ field with five buttons.
|
||||
} from "$lib/util/entity-name";
|
||||
import CargoRoutes from "./planet/cargo-routes.svelte";
|
||||
import Production from "./planet/production.svelte";
|
||||
import ShipGroups from "./planet/ship-groups.svelte";
|
||||
|
||||
type Props = {
|
||||
planet: ReportPlanet;
|
||||
@@ -39,6 +42,9 @@ field with five buttons.
|
||||
mapWidth: number;
|
||||
mapHeight: number;
|
||||
localPlayerDrive: number;
|
||||
localShipGroups: ReportLocalShipGroup[];
|
||||
otherShipGroups: ReportOtherShipGroup[];
|
||||
localRace: string;
|
||||
};
|
||||
let {
|
||||
planet,
|
||||
@@ -48,6 +54,9 @@ field with five buttons.
|
||||
mapWidth,
|
||||
mapHeight,
|
||||
localPlayerDrive,
|
||||
localShipGroups,
|
||||
otherShipGroups,
|
||||
localRace,
|
||||
}: Props = $props();
|
||||
|
||||
const kindKeyMap: Record<ReportPlanet["kind"], TranslationKey> = {
|
||||
@@ -223,6 +232,13 @@ field with five buttons.
|
||||
/>
|
||||
{/if}
|
||||
|
||||
<ShipGroups
|
||||
{planet}
|
||||
{localShipGroups}
|
||||
{otherShipGroups}
|
||||
{localRace}
|
||||
/>
|
||||
|
||||
<dl class="fields">
|
||||
{#if planet.kind === "other" && planet.owner !== null}
|
||||
<div class="field" data-testid="inspector-planet-field-owner">
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
<!--
|
||||
Phase 19 read-only "ship groups stationed here" subsection of the
|
||||
planet inspector. Phase 19 originally rendered every in-orbit group
|
||||
as an offset point near its planet on the map, which crowded the
|
||||
canvas to the point of unreadability. The map now hides on-planet
|
||||
groups and the planet inspector lists them instead — one row per
|
||||
group, showing its race, class, ship count, and mass.
|
||||
|
||||
Race attribution is best-effort:
|
||||
- LocalGroup → the player's own race (`localRace` prop).
|
||||
- OtherGroup on an `other`-kind planet → the planet's owner.
|
||||
- OtherGroup elsewhere → "foreign" placeholder; the engine's
|
||||
typed contract does not carry per-group ownership outside
|
||||
battle rosters.
|
||||
|
||||
Rows are intentionally non-interactive in Phase 19. Phase 21+ will
|
||||
deliver a full ship-groups table view; clicking a row will then
|
||||
deep-link into that table with a `(planet, race)` filter pre-applied.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type {
|
||||
ReportLocalShipGroup,
|
||||
ReportOtherShipGroup,
|
||||
ReportPlanet,
|
||||
} from "../../../api/game-state";
|
||||
import { i18n } from "$lib/i18n/index.svelte";
|
||||
|
||||
type Props = {
|
||||
planet: ReportPlanet;
|
||||
localShipGroups: ReportLocalShipGroup[];
|
||||
otherShipGroups: ReportOtherShipGroup[];
|
||||
localRace: string;
|
||||
};
|
||||
let { planet, localShipGroups, otherShipGroups, localRace }: Props = $props();
|
||||
|
||||
interface StationedRow {
|
||||
key: string;
|
||||
race: string;
|
||||
class: string;
|
||||
count: number;
|
||||
mass: number;
|
||||
}
|
||||
|
||||
const stationedRows: StationedRow[] = $derived.by(() => {
|
||||
const rows: StationedRow[] = [];
|
||||
for (const g of localShipGroups) {
|
||||
if (g.destination !== planet.number) continue;
|
||||
if (g.origin !== null || g.range !== null) continue;
|
||||
rows.push({
|
||||
key: `local:${g.id}`,
|
||||
race: localRace || i18n.t("game.inspector.planet.ship_groups.race.unknown"),
|
||||
class: g.class,
|
||||
count: g.count,
|
||||
mass: g.mass,
|
||||
});
|
||||
}
|
||||
const foreignRace =
|
||||
planet.owner ??
|
||||
i18n.t("game.inspector.planet.ship_groups.race.foreign");
|
||||
for (let i = 0; i < otherShipGroups.length; i++) {
|
||||
const g = otherShipGroups[i]!;
|
||||
if (g.destination !== planet.number) continue;
|
||||
if (g.origin !== null || g.range !== null) continue;
|
||||
rows.push({
|
||||
key: `other:${i}`,
|
||||
race: foreignRace,
|
||||
class: g.class,
|
||||
count: g.count,
|
||||
mass: g.mass,
|
||||
});
|
||||
}
|
||||
return rows;
|
||||
});
|
||||
|
||||
function formatNumber(value: number): string {
|
||||
return value.toLocaleString(undefined, { maximumFractionDigits: 2 });
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if stationedRows.length > 0}
|
||||
<section class="ship-groups" data-testid="inspector-planet-ship-groups">
|
||||
<h4>{i18n.t("game.inspector.planet.ship_groups.title")}</h4>
|
||||
<ul class="rows">
|
||||
{#each stationedRows as row (row.key)}
|
||||
<li class="row" data-testid="inspector-planet-ship-groups-row">
|
||||
<span class="race" data-testid="inspector-planet-ship-groups-race">
|
||||
{row.race}
|
||||
</span>
|
||||
<span class="class">{row.class}</span>
|
||||
<span class="count">
|
||||
{i18n.t("game.inspector.planet.ship_groups.row.count", {
|
||||
count: String(row.count),
|
||||
})}
|
||||
</span>
|
||||
<span class="mass">
|
||||
{i18n.t("game.inspector.planet.ship_groups.row.mass", {
|
||||
mass: formatNumber(row.mass),
|
||||
})}
|
||||
</span>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
</section>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.ship-groups {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
}
|
||||
h4 {
|
||||
margin: 0;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
color: #aab;
|
||||
}
|
||||
.rows {
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.2rem;
|
||||
}
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr auto auto;
|
||||
gap: 0.5rem;
|
||||
font-size: 0.85rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.race {
|
||||
font-weight: 600;
|
||||
}
|
||||
.class {
|
||||
color: #cdd;
|
||||
}
|
||||
.count,
|
||||
.mass {
|
||||
color: #aab;
|
||||
}
|
||||
</style>
|
||||
@@ -89,6 +89,13 @@ from the Phase 10 stub.
|
||||
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">
|
||||
@@ -101,6 +108,9 @@ from the Phase 10 stub.
|
||||
{mapWidth}
|
||||
{mapHeight}
|
||||
{localPlayerDrive}
|
||||
{localShipGroups}
|
||||
{otherShipGroups}
|
||||
{localRace}
|
||||
/>
|
||||
{:else if selectedShipGroup !== null}
|
||||
<ShipGroup selection={selectedShipGroup} planets={allPlanets} />
|
||||
|
||||
Reference in New Issue
Block a user