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:
Ilia Denisov
2026-05-10 15:08:41 +02:00
parent d63fe44618
commit f7109af55c
12 changed files with 511 additions and 77 deletions
@@ -0,0 +1,175 @@
// Vitest coverage for the Phase 19 follow-up "stationed ship groups"
// subsection of the planet inspector. Phase 19 originally rendered
// every in-orbit group as a small offset point on the map; the
// resulting visual noise pushed the listing into this subsection
// (`lib/inspectors/planet/ship-groups.svelte`) instead.
import "@testing-library/jest-dom/vitest";
import { render } from "@testing-library/svelte";
import { beforeEach, describe, expect, test } from "vitest";
import { i18n } from "../src/lib/i18n/index.svelte";
import type {
ReportLocalShipGroup,
ReportOtherShipGroup,
ReportPlanet,
} from "../src/api/game-state";
import ShipGroups from "../src/lib/inspectors/planet/ship-groups.svelte";
beforeEach(() => {
i18n.resetForTests("en");
});
const HOME_PLANET: ReportPlanet = {
number: 17,
name: "Castle",
x: 100,
y: 100,
kind: "local",
owner: null,
size: 1000,
resources: 10,
industryStockpile: 0,
materialsStockpile: 0,
industry: 1000,
population: 1000,
colonists: 100,
production: "Capital",
freeIndustry: 1000,
};
const FOREIGN_PLANET: ReportPlanet = {
...HOME_PLANET,
number: 99,
name: "Outpost",
kind: "other",
owner: "Klingons",
};
function localGroup(
overrides: Partial<ReportLocalShipGroup> = {},
): ReportLocalShipGroup {
return {
id: "uuid-1",
count: 1,
class: "Frontier",
tech: { drive: 5, weapons: 0, shields: 0, cargo: 1 },
cargo: "NONE",
load: 0,
destination: 17,
origin: null,
range: null,
speed: 0,
mass: 12,
state: "In_Orbit",
fleet: null,
...overrides,
};
}
function otherGroup(
overrides: Partial<ReportOtherShipGroup> = {},
): ReportOtherShipGroup {
return {
count: 3,
class: "Bird-of-Prey",
tech: { drive: 6, weapons: 4, shields: 3, cargo: 0 },
cargo: "NONE",
load: 0,
destination: 99,
origin: null,
range: null,
speed: 0,
mass: 25,
...overrides,
};
}
describe("planet inspector — stationed ship groups", () => {
test("renders one row per in-orbit local group with the player's race", () => {
const ui = render(ShipGroups, {
props: {
planet: HOME_PLANET,
localShipGroups: [
localGroup({ id: "g1", count: 2, class: "Frontier", mass: 24 }),
localGroup({ id: "g2", count: 7, class: "Furgon", mass: 173.25 }),
],
otherShipGroups: [],
localRace: "Earthlings",
},
});
const rows = ui.getAllByTestId("inspector-planet-ship-groups-row");
expect(rows.length).toBe(2);
expect(rows[0]).toHaveTextContent("Earthlings");
expect(rows[0]).toHaveTextContent("Frontier");
expect(rows[0]).toHaveTextContent("2");
expect(rows[0]).toHaveTextContent("24");
expect(rows[1]).toHaveTextContent("Furgon");
expect(rows[1]).toHaveTextContent("173.25");
});
test("filters out groups stationed on a different planet", () => {
const ui = render(ShipGroups, {
props: {
planet: HOME_PLANET,
localShipGroups: [
localGroup({ id: "g1", destination: 17 }),
localGroup({ id: "g2", destination: 99 }),
],
otherShipGroups: [],
localRace: "Earthlings",
},
});
expect(ui.getAllByTestId("inspector-planet-ship-groups-row").length).toBe(
1,
);
});
test("excludes in-hyperspace groups even when destination matches", () => {
const ui = render(ShipGroups, {
props: {
planet: HOME_PLANET,
localShipGroups: [
localGroup({ id: "stationed", destination: 17 }),
localGroup({
id: "fleeing",
destination: 17,
origin: 99,
range: 5,
}),
],
otherShipGroups: [],
localRace: "Earthlings",
},
});
expect(ui.getAllByTestId("inspector-planet-ship-groups-row").length).toBe(
1,
);
});
test("foreign-planet visitors fall back to the planet owner's race", () => {
const ui = render(ShipGroups, {
props: {
planet: FOREIGN_PLANET,
localShipGroups: [],
otherShipGroups: [otherGroup({ destination: 99 })],
localRace: "Earthlings",
},
});
const row = ui.getByTestId("inspector-planet-ship-groups-row");
expect(row).toHaveTextContent("Klingons");
expect(row).toHaveTextContent("Bird-of-Prey");
});
test("subsection collapses entirely when nothing is stationed", () => {
const ui = render(ShipGroups, {
props: {
planet: HOME_PLANET,
localShipGroups: [],
otherShipGroups: [],
localRace: "Earthlings",
},
});
expect(ui.queryByTestId("inspector-planet-ship-groups")).toBeNull();
});
});