ui: plan 01-27 done #1
@@ -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} />
|
||||
|
||||
@@ -4,22 +4,28 @@
|
||||
// incoming-trajectory lines) lives here.
|
||||
//
|
||||
// Position rules:
|
||||
// - On-planet local / other groups (origin === null) — drawn next
|
||||
// to the destination planet, slightly offset so the group has its
|
||||
// own hit-target distinct from the planet pixel. Multiple groups
|
||||
// stationed at the same planet share the offset (Phase 19
|
||||
// limitation; a future phase fans them out or lists them in the
|
||||
// planet inspector).
|
||||
// - On-planet local / other groups (origin === null, range === null)
|
||||
// are NOT rendered on the map. Stationed groups would otherwise
|
||||
// pile up next to every populated planet and turn the canvas
|
||||
// into noise; the planet inspector lists them instead
|
||||
// (see `lib/inspectors/planet/ship-groups.svelte`).
|
||||
// - In-hyperspace local / other groups (origin / range set) —
|
||||
// interpolated along the origin → destination line at `range`
|
||||
// world units from the destination.
|
||||
// world units from the destination. The line is the wrap-aware
|
||||
// shortest path on a torus.
|
||||
// - Incoming groups — origin and destination are always present;
|
||||
// emit a dashed red trajectory line between the two and a
|
||||
// clickable point at the interpolated position (range = the
|
||||
// `distance` field).
|
||||
// emit a dashed red trajectory line from origin to a wrap-aware
|
||||
// destination plus a clickable point at the interpolated
|
||||
// position (range = the `distance` field).
|
||||
// - Unidentified groups — drawn at the absolute (x, y) the radar
|
||||
// reports.
|
||||
//
|
||||
// Torus-shortest deltas come from `map/math.torusShortestDelta`. The
|
||||
// canonical Go-side equivalent is `pkg/calc.ShortestDelta`; the TS
|
||||
// helper duplicates the formula because the renderer's hot path
|
||||
// avoids the WASM boundary cost. Both implementations agree on the
|
||||
// half-circumference tie-break.
|
||||
//
|
||||
// PrimitiveIDs are partitioned via large per-variant offsets so they
|
||||
// never collide with planet ids (which run in `[0, planetCount)`).
|
||||
|
||||
@@ -32,6 +38,7 @@ import type {
|
||||
ReportUnidentifiedShipGroup,
|
||||
} from "../api/game-state";
|
||||
import type { ShipGroupRef } from "../lib/selection.svelte";
|
||||
import { torusShortestDelta } from "./math";
|
||||
import type { LinePrim, PointPrim, PrimitiveID, Style } from "./world";
|
||||
|
||||
/**
|
||||
@@ -49,12 +56,6 @@ export const SHIP_GROUP_ID_OFFSETS = {
|
||||
unidentified: 400_000_000,
|
||||
} as const;
|
||||
|
||||
/** ON_PLANET_OFFSET is the (dx, dy) world-unit shift applied to a
|
||||
* group point that sits on a planet, so the group has a distinct
|
||||
* click target from the planet itself. The offset is small enough
|
||||
* that the visual association with the planet stays clear. */
|
||||
const ON_PLANET_OFFSET = { dx: 6, dy: -6 };
|
||||
|
||||
const STYLE_LOCAL_GROUP: Style = {
|
||||
fillColor: 0xfff176,
|
||||
fillAlpha: 0.95,
|
||||
@@ -88,9 +89,9 @@ const STYLE_UNIDENTIFIED_GROUP: Style = {
|
||||
|
||||
// Priority order inside `hit-test`: ship groups outrank planets so a
|
||||
// hyperspace group landing on top of an unidentified planet is
|
||||
// selectable. On-planet groups stay below the planet so clicks on a
|
||||
// planet still resolve to the planet itself (the offset gives the
|
||||
// group its own un-overlapped hit area).
|
||||
// selectable. The trajectory line itself is given the lowest priority
|
||||
// so a click on the dashed segment never "wins" over the clickable
|
||||
// point at the interpolated position.
|
||||
const PRIORITY_LOCAL = 5;
|
||||
const PRIORITY_OTHER = 5;
|
||||
const PRIORITY_INCOMING_POINT = 6;
|
||||
@@ -109,10 +110,12 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
for (const planet of report.planets) {
|
||||
planetIndex.set(planet.number, planet);
|
||||
}
|
||||
const w = report.mapWidth;
|
||||
const h = report.mapHeight;
|
||||
|
||||
for (let i = 0; i < report.localShipGroups.length; i++) {
|
||||
const group = report.localShipGroups[i]!;
|
||||
const pos = computeGroupPosition(group, planetIndex);
|
||||
const pos = computeInSpacePosition(group, planetIndex, w, h);
|
||||
if (pos === null) continue;
|
||||
const id = SHIP_GROUP_ID_OFFSETS.local + i;
|
||||
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_LOCAL, STYLE_LOCAL_GROUP));
|
||||
@@ -121,7 +124,7 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
|
||||
for (let i = 0; i < report.otherShipGroups.length; i++) {
|
||||
const group = report.otherShipGroups[i]!;
|
||||
const pos = computeGroupPosition(group, planetIndex);
|
||||
const pos = computeInSpacePosition(group, planetIndex, w, h);
|
||||
if (pos === null) continue;
|
||||
const id = SHIP_GROUP_ID_OFFSETS.other + i;
|
||||
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_OTHER, STYLE_OTHER_GROUP));
|
||||
@@ -133,6 +136,15 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
const origin = planetIndex.get(group.origin);
|
||||
const destination = planetIndex.get(group.destination);
|
||||
if (origin === undefined || destination === undefined) continue;
|
||||
// Unwrap the destination relative to origin so the line crosses
|
||||
// the torus seam when that is the shorter path. Renderer-side
|
||||
// we draw the segment in a single tile; in torus mode PixiJS
|
||||
// repeats the world so the line still appears continuous on
|
||||
// the visible side of the seam.
|
||||
const dx = torusShortestDelta(origin.x, destination.x, w);
|
||||
const dy = torusShortestDelta(origin.y, destination.y, h);
|
||||
const destX = origin.x + dx;
|
||||
const destY = origin.y + dy;
|
||||
const lineId = SHIP_GROUP_ID_OFFSETS.incomingLine + i;
|
||||
primitives.push({
|
||||
kind: "line",
|
||||
@@ -142,16 +154,10 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
hitSlopPx: 0,
|
||||
x1: origin.x,
|
||||
y1: origin.y,
|
||||
x2: destination.x,
|
||||
y2: destination.y,
|
||||
x2: destX,
|
||||
y2: destY,
|
||||
});
|
||||
const pos = interpolateAlongLine(
|
||||
destination.x,
|
||||
destination.y,
|
||||
origin.x,
|
||||
origin.y,
|
||||
group.distance,
|
||||
);
|
||||
const pos = interpolateAlongLine(destX, destY, origin.x, origin.y, group.distance);
|
||||
const pointId = SHIP_GROUP_ID_OFFSETS.incoming + i;
|
||||
primitives.push(
|
||||
makePoint(
|
||||
@@ -185,29 +191,34 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
||||
return { primitives, lookup };
|
||||
}
|
||||
|
||||
function computeGroupPosition(
|
||||
/**
|
||||
* computeInSpacePosition returns the renderer-side (x, y) of a local
|
||||
* or foreign group that is currently in hyperspace. On-planet groups
|
||||
* (origin === null || range === null) are intentionally skipped so the
|
||||
* map does not pile dozens of primitives onto every populated planet
|
||||
* — the planet inspector lists them instead. Returns null when either
|
||||
* the group is on-planet, or the origin / destination planets are
|
||||
* not visible to the local player.
|
||||
*/
|
||||
function computeInSpacePosition(
|
||||
group: ReportLocalShipGroup | ReportOtherShipGroup,
|
||||
planetIndex: Map<number, ReportPlanet>,
|
||||
mapWidth: number,
|
||||
mapHeight: number,
|
||||
): { x: number; y: number } | null {
|
||||
if (group.origin === null || group.range === null) return null;
|
||||
const destination = planetIndex.get(group.destination);
|
||||
if (destination === undefined) return null;
|
||||
if (group.origin === null || group.range === null) {
|
||||
// Stationed on the destination planet; offset slightly so the
|
||||
// group is distinct from the planet's own hit target.
|
||||
return {
|
||||
x: destination.x + ON_PLANET_OFFSET.dx,
|
||||
y: destination.y + ON_PLANET_OFFSET.dy,
|
||||
};
|
||||
}
|
||||
const origin = planetIndex.get(group.origin);
|
||||
if (origin === undefined) return null;
|
||||
return interpolateAlongLine(
|
||||
destination.x,
|
||||
destination.y,
|
||||
origin.x,
|
||||
origin.y,
|
||||
group.range,
|
||||
);
|
||||
const dx = torusShortestDelta(destination.x, origin.x, mapWidth);
|
||||
const dy = torusShortestDelta(destination.y, origin.y, mapHeight);
|
||||
const total = Math.hypot(dx, dy);
|
||||
if (total === 0 || group.range <= 0) {
|
||||
return { x: destination.x, y: destination.y };
|
||||
}
|
||||
const t = Math.min(1, group.range / total);
|
||||
return { x: destination.x + t * dx, y: destination.y + t * dy };
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -180,6 +180,13 @@ fresh.
|
||||
const inspectorLocalDrive = $derived(
|
||||
renderedReport.report?.localPlayerDrive ?? 0,
|
||||
);
|
||||
const inspectorLocalShipGroups = $derived(
|
||||
renderedReport.report?.localShipGroups ?? [],
|
||||
);
|
||||
const inspectorOtherShipGroups = $derived(
|
||||
renderedReport.report?.otherShipGroups ?? [],
|
||||
);
|
||||
const inspectorLocalRace = $derived(renderedReport.report?.race ?? "");
|
||||
|
||||
// Reveal the inspector whenever a new planet selection lands.
|
||||
// Reading `selection.selected` once outside the effect keeps the
|
||||
@@ -324,6 +331,9 @@ fresh.
|
||||
mapWidth={inspectorMapWidth}
|
||||
mapHeight={inspectorMapHeight}
|
||||
localPlayerDrive={inspectorLocalDrive}
|
||||
localShipGroups={inspectorLocalShipGroups}
|
||||
otherShipGroups={inspectorOtherShipGroups}
|
||||
localRace={inspectorLocalRace}
|
||||
onMap={effectiveTool === "map"}
|
||||
onClose={() => selection.clear()}
|
||||
/>
|
||||
|
||||
@@ -10,31 +10,27 @@
|
||||
|
||||
import { expect, test, type Page } from "@playwright/test";
|
||||
|
||||
interface DebugSurface {
|
||||
ready: true;
|
||||
loadSession(): Promise<unknown>;
|
||||
setDeviceSessionId(id: string): Promise<void>;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
__galaxyDebug?: DebugSurface;
|
||||
}
|
||||
}
|
||||
|
||||
// Seed an authenticated session through `/__debug/store` so the
|
||||
// root layout's redirect-to-login guard passes. The synthetic flow
|
||||
// itself does not talk to the gateway, but the session check still
|
||||
// runs at every navigation.
|
||||
// runs at every navigation. The full `__galaxyDebug` shape is
|
||||
// declared globally in `tests/e2e/storage-keypair-persistence.spec.ts`;
|
||||
// here we only need `loadSession` + `setDeviceSessionId`.
|
||||
async function seedSession(page: Page): Promise<void> {
|
||||
await page.goto("/__debug/store");
|
||||
await expect(page.getByTestId("debug-store-ready")).toBeVisible();
|
||||
await page.waitForFunction(() => window.__galaxyDebug?.ready === true);
|
||||
await page.evaluate(async () => {
|
||||
await window.__galaxyDebug!.loadSession();
|
||||
await window.__galaxyDebug!.setDeviceSessionId(
|
||||
"phase-19-synthetic-session",
|
||||
await page.waitForFunction(
|
||||
() => (window as unknown as { __galaxyDebug?: { ready?: boolean } }).__galaxyDebug?.ready === true,
|
||||
);
|
||||
await page.evaluate(async () => {
|
||||
const debug = (window as unknown as {
|
||||
__galaxyDebug: {
|
||||
loadSession(): Promise<unknown>;
|
||||
setDeviceSessionId(id: string): Promise<void>;
|
||||
};
|
||||
}).__galaxyDebug;
|
||||
await debug.loadSession();
|
||||
await debug.setDeviceSessionId("phase-19-synthetic-session");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -70,6 +70,9 @@ describe("planet inspector", () => {
|
||||
mapWidth: 1,
|
||||
mapHeight: 1,
|
||||
localPlayerDrive: 0,
|
||||
localShipGroups: [],
|
||||
otherShipGroups: [],
|
||||
localRace: "",
|
||||
},
|
||||
});
|
||||
const section = ui.getByTestId("inspector-planet");
|
||||
@@ -140,6 +143,9 @@ describe("planet inspector", () => {
|
||||
mapWidth: 1,
|
||||
mapHeight: 1,
|
||||
localPlayerDrive: 0,
|
||||
localShipGroups: [],
|
||||
otherShipGroups: [],
|
||||
localRace: "",
|
||||
},
|
||||
});
|
||||
expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent(
|
||||
@@ -176,6 +182,9 @@ describe("planet inspector", () => {
|
||||
mapWidth: 1,
|
||||
mapHeight: 1,
|
||||
localPlayerDrive: 0,
|
||||
localShipGroups: [],
|
||||
otherShipGroups: [],
|
||||
localRace: "",
|
||||
},
|
||||
});
|
||||
expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent(
|
||||
@@ -213,6 +222,9 @@ describe("planet inspector", () => {
|
||||
mapWidth: 1,
|
||||
mapHeight: 1,
|
||||
localPlayerDrive: 0,
|
||||
localShipGroups: [],
|
||||
otherShipGroups: [],
|
||||
localRace: "",
|
||||
},
|
||||
});
|
||||
expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent(
|
||||
@@ -246,6 +258,9 @@ describe("planet inspector", () => {
|
||||
mapWidth: 1,
|
||||
mapHeight: 1,
|
||||
localPlayerDrive: 0,
|
||||
localShipGroups: [],
|
||||
otherShipGroups: [],
|
||||
localRace: "",
|
||||
},
|
||||
});
|
||||
expect(ui.queryByTestId("inspector-planet-rename-action")).toBeNull();
|
||||
@@ -283,6 +298,9 @@ describe("planet inspector", () => {
|
||||
mapWidth: 1,
|
||||
mapHeight: 1,
|
||||
localPlayerDrive: 0,
|
||||
localShipGroups: [],
|
||||
otherShipGroups: [],
|
||||
localRace: "",
|
||||
},
|
||||
context,
|
||||
});
|
||||
@@ -351,6 +369,9 @@ describe("planet inspector", () => {
|
||||
mapWidth: 1,
|
||||
mapHeight: 1,
|
||||
localPlayerDrive: 0,
|
||||
localShipGroups: [],
|
||||
otherShipGroups: [],
|
||||
localRace: "",
|
||||
},
|
||||
context,
|
||||
});
|
||||
@@ -386,6 +407,9 @@ describe("planet inspector", () => {
|
||||
mapWidth: 1,
|
||||
mapHeight: 1,
|
||||
localPlayerDrive: 0,
|
||||
localShipGroups: [],
|
||||
otherShipGroups: [],
|
||||
localRace: "",
|
||||
},
|
||||
});
|
||||
// Empty production strings collapse to the localised "none"
|
||||
|
||||
@@ -58,7 +58,7 @@ function makeReport(overrides: Partial<GameReport> = {}): GameReport {
|
||||
}
|
||||
|
||||
describe("reportToWorld — ship groups", () => {
|
||||
test("on-planet local group renders a clickable point near the planet", () => {
|
||||
test("on-planet local group is NOT rendered on the map (planet inspector hosts it)", () => {
|
||||
const home = planet({ number: 17, x: 100, y: 100, kind: "local" });
|
||||
const { world, hitLookup } = reportToWorld(
|
||||
makeReport({
|
||||
@@ -82,18 +82,13 @@ describe("reportToWorld — ship groups", () => {
|
||||
],
|
||||
}),
|
||||
);
|
||||
// 1 planet point + 1 ship-group point.
|
||||
expect(world.primitives.length).toBe(2);
|
||||
const groupPrimId = SHIP_GROUP_ID_OFFSETS.local + 0;
|
||||
const group = world.primitives.find((p) => p.id === groupPrimId);
|
||||
expect(group).toBeDefined();
|
||||
if (group?.kind !== "point") throw new Error("expected point");
|
||||
// Off-planet rendering: not exactly on (100, 100).
|
||||
expect(group.x === home.x && group.y === home.y).toBe(false);
|
||||
expect(hitLookup.get(groupPrimId)).toEqual({
|
||||
kind: "shipGroup",
|
||||
ref: { variant: "local", id: "uuid-local-1" },
|
||||
});
|
||||
// Only the planet itself contributes a primitive; the on-planet
|
||||
// group is intentionally invisible on the map. Phase 19's
|
||||
// `lib/inspectors/planet/ship-groups.svelte` lists it inside the
|
||||
// planet inspector instead.
|
||||
expect(world.primitives.length).toBe(1);
|
||||
expect(hitLookup.has(SHIP_GROUP_ID_OFFSETS.local + 0)).toBe(false);
|
||||
expect(hitLookup.get(17)).toEqual({ kind: "planet", number: 17 });
|
||||
});
|
||||
|
||||
test("in-hyperspace local group renders at the interpolated position", () => {
|
||||
@@ -130,6 +125,36 @@ describe("reportToWorld — ship groups", () => {
|
||||
expect(group.y).toBe(0);
|
||||
});
|
||||
|
||||
test("incoming-group line crosses the torus seam via the shortest path", () => {
|
||||
const dest = planet({ number: 1, x: 5, y: 50 });
|
||||
const orig = planet({ number: 9, x: 95, y: 50 });
|
||||
const { world } = reportToWorld(
|
||||
makeReport({
|
||||
mapWidth: 100,
|
||||
mapHeight: 100,
|
||||
planets: [dest, orig],
|
||||
incomingShipGroups: [
|
||||
{
|
||||
origin: 9,
|
||||
destination: 1,
|
||||
distance: 5,
|
||||
speed: 5,
|
||||
mass: 1,
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const line = world.primitives.find(
|
||||
(p) => p.id === SHIP_GROUP_ID_OFFSETS.incomingLine + 0,
|
||||
);
|
||||
if (line?.kind !== "line") throw new Error("expected line");
|
||||
// Origin (95) → unwrapped destination at 105 (origin.x + (-10) is
|
||||
// the no-wrap path). The shortest delta from 95 to 5 on width 100
|
||||
// is +10, so we expect line.x2 = 95 + 10 = 105.
|
||||
expect(line.x1).toBe(95);
|
||||
expect(line.x2).toBe(105);
|
||||
});
|
||||
|
||||
test("incoming group emits one dashed line + one clickable point", () => {
|
||||
const dest = planet({ number: 1, x: 0, y: 0 });
|
||||
const orig = planet({ number: 9, x: 100, y: 0 });
|
||||
|
||||
Reference in New Issue
Block a user