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.location.in_hyperspace": "in hyperspace",
|
||||||
"game.inspector.ship_group.fleet.none": "—",
|
"game.inspector.ship_group.fleet.none": "—",
|
||||||
"game.inspector.ship_group.unidentified_no_data": "no data — only the radar blip is known",
|
"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;
|
} as const;
|
||||||
|
|
||||||
export default en;
|
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.location.in_hyperspace": "в гиперпространстве",
|
||||||
"game.inspector.ship_group.fleet.none": "—",
|
"game.inspector.ship_group.fleet.none": "—",
|
||||||
"game.inspector.ship_group.unidentified_no_data": "данных нет — известны только координаты",
|
"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;
|
export default ru;
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ dismiss from the IA section §6 land in Phase 35 polish.
|
|||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type {
|
import type {
|
||||||
|
ReportLocalShipGroup,
|
||||||
|
ReportOtherShipGroup,
|
||||||
ReportPlanet,
|
ReportPlanet,
|
||||||
ReportRoute,
|
ReportRoute,
|
||||||
ShipClassSummary,
|
ShipClassSummary,
|
||||||
@@ -27,6 +29,9 @@ dismiss from the IA section §6 land in Phase 35 polish.
|
|||||||
mapWidth: number;
|
mapWidth: number;
|
||||||
mapHeight: number;
|
mapHeight: number;
|
||||||
localPlayerDrive: number;
|
localPlayerDrive: number;
|
||||||
|
localShipGroups: ReportLocalShipGroup[];
|
||||||
|
otherShipGroups: ReportOtherShipGroup[];
|
||||||
|
localRace: string;
|
||||||
onMap: boolean;
|
onMap: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
};
|
};
|
||||||
@@ -38,6 +43,9 @@ dismiss from the IA section §6 land in Phase 35 polish.
|
|||||||
mapWidth,
|
mapWidth,
|
||||||
mapHeight,
|
mapHeight,
|
||||||
localPlayerDrive,
|
localPlayerDrive,
|
||||||
|
localShipGroups,
|
||||||
|
otherShipGroups,
|
||||||
|
localRace,
|
||||||
onMap,
|
onMap,
|
||||||
onClose,
|
onClose,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
@@ -66,6 +74,9 @@ dismiss from the IA section §6 land in Phase 35 polish.
|
|||||||
{mapWidth}
|
{mapWidth}
|
||||||
{mapHeight}
|
{mapHeight}
|
||||||
{localPlayerDrive}
|
{localPlayerDrive}
|
||||||
|
{localShipGroups}
|
||||||
|
{otherShipGroups}
|
||||||
|
{localRace}
|
||||||
/>
|
/>
|
||||||
</section>
|
</section>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ field with five buttons.
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getContext, tick } from "svelte";
|
import { getContext, tick } from "svelte";
|
||||||
import type {
|
import type {
|
||||||
|
ReportLocalShipGroup,
|
||||||
|
ReportOtherShipGroup,
|
||||||
ReportPlanet,
|
ReportPlanet,
|
||||||
ReportRoute,
|
ReportRoute,
|
||||||
ShipClassSummary,
|
ShipClassSummary,
|
||||||
@@ -30,6 +32,7 @@ field with five buttons.
|
|||||||
} from "$lib/util/entity-name";
|
} from "$lib/util/entity-name";
|
||||||
import CargoRoutes from "./planet/cargo-routes.svelte";
|
import CargoRoutes from "./planet/cargo-routes.svelte";
|
||||||
import Production from "./planet/production.svelte";
|
import Production from "./planet/production.svelte";
|
||||||
|
import ShipGroups from "./planet/ship-groups.svelte";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
planet: ReportPlanet;
|
planet: ReportPlanet;
|
||||||
@@ -39,6 +42,9 @@ field with five buttons.
|
|||||||
mapWidth: number;
|
mapWidth: number;
|
||||||
mapHeight: number;
|
mapHeight: number;
|
||||||
localPlayerDrive: number;
|
localPlayerDrive: number;
|
||||||
|
localShipGroups: ReportLocalShipGroup[];
|
||||||
|
otherShipGroups: ReportOtherShipGroup[];
|
||||||
|
localRace: string;
|
||||||
};
|
};
|
||||||
let {
|
let {
|
||||||
planet,
|
planet,
|
||||||
@@ -48,6 +54,9 @@ field with five buttons.
|
|||||||
mapWidth,
|
mapWidth,
|
||||||
mapHeight,
|
mapHeight,
|
||||||
localPlayerDrive,
|
localPlayerDrive,
|
||||||
|
localShipGroups,
|
||||||
|
otherShipGroups,
|
||||||
|
localRace,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const kindKeyMap: Record<ReportPlanet["kind"], TranslationKey> = {
|
const kindKeyMap: Record<ReportPlanet["kind"], TranslationKey> = {
|
||||||
@@ -223,6 +232,13 @@ field with five buttons.
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<ShipGroups
|
||||||
|
{planet}
|
||||||
|
{localShipGroups}
|
||||||
|
{otherShipGroups}
|
||||||
|
{localRace}
|
||||||
|
/>
|
||||||
|
|
||||||
<dl class="fields">
|
<dl class="fields">
|
||||||
{#if planet.kind === "other" && planet.owner !== null}
|
{#if planet.kind === "other" && planet.owner !== null}
|
||||||
<div class="field" data-testid="inspector-planet-field-owner">
|
<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(
|
const localPlayerDrive = $derived(
|
||||||
renderedReport?.report?.localPlayerDrive ?? 0,
|
renderedReport?.report?.localPlayerDrive ?? 0,
|
||||||
);
|
);
|
||||||
|
const localShipGroups = $derived(
|
||||||
|
renderedReport?.report?.localShipGroups ?? [],
|
||||||
|
);
|
||||||
|
const otherShipGroups = $derived(
|
||||||
|
renderedReport?.report?.otherShipGroups ?? [],
|
||||||
|
);
|
||||||
|
const localRace = $derived(renderedReport?.report?.race ?? "");
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section class="tool" data-testid="sidebar-tool-inspector">
|
<section class="tool" data-testid="sidebar-tool-inspector">
|
||||||
@@ -101,6 +108,9 @@ from the Phase 10 stub.
|
|||||||
{mapWidth}
|
{mapWidth}
|
||||||
{mapHeight}
|
{mapHeight}
|
||||||
{localPlayerDrive}
|
{localPlayerDrive}
|
||||||
|
{localShipGroups}
|
||||||
|
{otherShipGroups}
|
||||||
|
{localRace}
|
||||||
/>
|
/>
|
||||||
{:else if selectedShipGroup !== null}
|
{:else if selectedShipGroup !== null}
|
||||||
<ShipGroup selection={selectedShipGroup} planets={allPlanets} />
|
<ShipGroup selection={selectedShipGroup} planets={allPlanets} />
|
||||||
|
|||||||
@@ -4,22 +4,28 @@
|
|||||||
// incoming-trajectory lines) lives here.
|
// incoming-trajectory lines) lives here.
|
||||||
//
|
//
|
||||||
// Position rules:
|
// Position rules:
|
||||||
// - On-planet local / other groups (origin === null) — drawn next
|
// - On-planet local / other groups (origin === null, range === null)
|
||||||
// to the destination planet, slightly offset so the group has its
|
// are NOT rendered on the map. Stationed groups would otherwise
|
||||||
// own hit-target distinct from the planet pixel. Multiple groups
|
// pile up next to every populated planet and turn the canvas
|
||||||
// stationed at the same planet share the offset (Phase 19
|
// into noise; the planet inspector lists them instead
|
||||||
// limitation; a future phase fans them out or lists them in the
|
// (see `lib/inspectors/planet/ship-groups.svelte`).
|
||||||
// planet inspector).
|
|
||||||
// - In-hyperspace local / other groups (origin / range set) —
|
// - In-hyperspace local / other groups (origin / range set) —
|
||||||
// interpolated along the origin → destination line at `range`
|
// 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;
|
// - Incoming groups — origin and destination are always present;
|
||||||
// emit a dashed red trajectory line between the two and a
|
// emit a dashed red trajectory line from origin to a wrap-aware
|
||||||
// clickable point at the interpolated position (range = the
|
// destination plus a clickable point at the interpolated
|
||||||
// `distance` field).
|
// position (range = the `distance` field).
|
||||||
// - Unidentified groups — drawn at the absolute (x, y) the radar
|
// - Unidentified groups — drawn at the absolute (x, y) the radar
|
||||||
// reports.
|
// 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
|
// PrimitiveIDs are partitioned via large per-variant offsets so they
|
||||||
// never collide with planet ids (which run in `[0, planetCount)`).
|
// never collide with planet ids (which run in `[0, planetCount)`).
|
||||||
|
|
||||||
@@ -32,6 +38,7 @@ import type {
|
|||||||
ReportUnidentifiedShipGroup,
|
ReportUnidentifiedShipGroup,
|
||||||
} from "../api/game-state";
|
} from "../api/game-state";
|
||||||
import type { ShipGroupRef } from "../lib/selection.svelte";
|
import type { ShipGroupRef } from "../lib/selection.svelte";
|
||||||
|
import { torusShortestDelta } from "./math";
|
||||||
import type { LinePrim, PointPrim, PrimitiveID, Style } from "./world";
|
import type { LinePrim, PointPrim, PrimitiveID, Style } from "./world";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -49,12 +56,6 @@ export const SHIP_GROUP_ID_OFFSETS = {
|
|||||||
unidentified: 400_000_000,
|
unidentified: 400_000_000,
|
||||||
} as const;
|
} 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 = {
|
const STYLE_LOCAL_GROUP: Style = {
|
||||||
fillColor: 0xfff176,
|
fillColor: 0xfff176,
|
||||||
fillAlpha: 0.95,
|
fillAlpha: 0.95,
|
||||||
@@ -88,9 +89,9 @@ const STYLE_UNIDENTIFIED_GROUP: Style = {
|
|||||||
|
|
||||||
// Priority order inside `hit-test`: ship groups outrank planets so a
|
// Priority order inside `hit-test`: ship groups outrank planets so a
|
||||||
// hyperspace group landing on top of an unidentified planet is
|
// hyperspace group landing on top of an unidentified planet is
|
||||||
// selectable. On-planet groups stay below the planet so clicks on a
|
// selectable. The trajectory line itself is given the lowest priority
|
||||||
// planet still resolve to the planet itself (the offset gives the
|
// so a click on the dashed segment never "wins" over the clickable
|
||||||
// group its own un-overlapped hit area).
|
// point at the interpolated position.
|
||||||
const PRIORITY_LOCAL = 5;
|
const PRIORITY_LOCAL = 5;
|
||||||
const PRIORITY_OTHER = 5;
|
const PRIORITY_OTHER = 5;
|
||||||
const PRIORITY_INCOMING_POINT = 6;
|
const PRIORITY_INCOMING_POINT = 6;
|
||||||
@@ -109,10 +110,12 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
|||||||
for (const planet of report.planets) {
|
for (const planet of report.planets) {
|
||||||
planetIndex.set(planet.number, planet);
|
planetIndex.set(planet.number, planet);
|
||||||
}
|
}
|
||||||
|
const w = report.mapWidth;
|
||||||
|
const h = report.mapHeight;
|
||||||
|
|
||||||
for (let i = 0; i < report.localShipGroups.length; i++) {
|
for (let i = 0; i < report.localShipGroups.length; i++) {
|
||||||
const group = report.localShipGroups[i]!;
|
const group = report.localShipGroups[i]!;
|
||||||
const pos = computeGroupPosition(group, planetIndex);
|
const pos = computeInSpacePosition(group, planetIndex, w, h);
|
||||||
if (pos === null) continue;
|
if (pos === null) continue;
|
||||||
const id = SHIP_GROUP_ID_OFFSETS.local + i;
|
const id = SHIP_GROUP_ID_OFFSETS.local + i;
|
||||||
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_LOCAL, STYLE_LOCAL_GROUP));
|
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++) {
|
for (let i = 0; i < report.otherShipGroups.length; i++) {
|
||||||
const group = report.otherShipGroups[i]!;
|
const group = report.otherShipGroups[i]!;
|
||||||
const pos = computeGroupPosition(group, planetIndex);
|
const pos = computeInSpacePosition(group, planetIndex, w, h);
|
||||||
if (pos === null) continue;
|
if (pos === null) continue;
|
||||||
const id = SHIP_GROUP_ID_OFFSETS.other + i;
|
const id = SHIP_GROUP_ID_OFFSETS.other + i;
|
||||||
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_OTHER, STYLE_OTHER_GROUP));
|
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 origin = planetIndex.get(group.origin);
|
||||||
const destination = planetIndex.get(group.destination);
|
const destination = planetIndex.get(group.destination);
|
||||||
if (origin === undefined || destination === undefined) continue;
|
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;
|
const lineId = SHIP_GROUP_ID_OFFSETS.incomingLine + i;
|
||||||
primitives.push({
|
primitives.push({
|
||||||
kind: "line",
|
kind: "line",
|
||||||
@@ -142,16 +154,10 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
|||||||
hitSlopPx: 0,
|
hitSlopPx: 0,
|
||||||
x1: origin.x,
|
x1: origin.x,
|
||||||
y1: origin.y,
|
y1: origin.y,
|
||||||
x2: destination.x,
|
x2: destX,
|
||||||
y2: destination.y,
|
y2: destY,
|
||||||
});
|
});
|
||||||
const pos = interpolateAlongLine(
|
const pos = interpolateAlongLine(destX, destY, origin.x, origin.y, group.distance);
|
||||||
destination.x,
|
|
||||||
destination.y,
|
|
||||||
origin.x,
|
|
||||||
origin.y,
|
|
||||||
group.distance,
|
|
||||||
);
|
|
||||||
const pointId = SHIP_GROUP_ID_OFFSETS.incoming + i;
|
const pointId = SHIP_GROUP_ID_OFFSETS.incoming + i;
|
||||||
primitives.push(
|
primitives.push(
|
||||||
makePoint(
|
makePoint(
|
||||||
@@ -185,29 +191,34 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
|
|||||||
return { primitives, lookup };
|
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,
|
group: ReportLocalShipGroup | ReportOtherShipGroup,
|
||||||
planetIndex: Map<number, ReportPlanet>,
|
planetIndex: Map<number, ReportPlanet>,
|
||||||
|
mapWidth: number,
|
||||||
|
mapHeight: number,
|
||||||
): { x: number; y: number } | null {
|
): { x: number; y: number } | null {
|
||||||
|
if (group.origin === null || group.range === null) return null;
|
||||||
const destination = planetIndex.get(group.destination);
|
const destination = planetIndex.get(group.destination);
|
||||||
if (destination === undefined) return null;
|
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);
|
const origin = planetIndex.get(group.origin);
|
||||||
if (origin === undefined) return null;
|
if (origin === undefined) return null;
|
||||||
return interpolateAlongLine(
|
const dx = torusShortestDelta(destination.x, origin.x, mapWidth);
|
||||||
destination.x,
|
const dy = torusShortestDelta(destination.y, origin.y, mapHeight);
|
||||||
destination.y,
|
const total = Math.hypot(dx, dy);
|
||||||
origin.x,
|
if (total === 0 || group.range <= 0) {
|
||||||
origin.y,
|
return { x: destination.x, y: destination.y };
|
||||||
group.range,
|
}
|
||||||
);
|
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(
|
const inspectorLocalDrive = $derived(
|
||||||
renderedReport.report?.localPlayerDrive ?? 0,
|
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.
|
// Reveal the inspector whenever a new planet selection lands.
|
||||||
// Reading `selection.selected` once outside the effect keeps the
|
// Reading `selection.selected` once outside the effect keeps the
|
||||||
@@ -324,6 +331,9 @@ fresh.
|
|||||||
mapWidth={inspectorMapWidth}
|
mapWidth={inspectorMapWidth}
|
||||||
mapHeight={inspectorMapHeight}
|
mapHeight={inspectorMapHeight}
|
||||||
localPlayerDrive={inspectorLocalDrive}
|
localPlayerDrive={inspectorLocalDrive}
|
||||||
|
localShipGroups={inspectorLocalShipGroups}
|
||||||
|
otherShipGroups={inspectorOtherShipGroups}
|
||||||
|
localRace={inspectorLocalRace}
|
||||||
onMap={effectiveTool === "map"}
|
onMap={effectiveTool === "map"}
|
||||||
onClose={() => selection.clear()}
|
onClose={() => selection.clear()}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,31 +10,27 @@
|
|||||||
|
|
||||||
import { expect, test, type Page } from "@playwright/test";
|
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
|
// Seed an authenticated session through `/__debug/store` so the
|
||||||
// root layout's redirect-to-login guard passes. The synthetic flow
|
// root layout's redirect-to-login guard passes. The synthetic flow
|
||||||
// itself does not talk to the gateway, but the session check still
|
// 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> {
|
async function seedSession(page: Page): Promise<void> {
|
||||||
await page.goto("/__debug/store");
|
await page.goto("/__debug/store");
|
||||||
await expect(page.getByTestId("debug-store-ready")).toBeVisible();
|
await expect(page.getByTestId("debug-store-ready")).toBeVisible();
|
||||||
await page.waitForFunction(() => window.__galaxyDebug?.ready === true);
|
await page.waitForFunction(
|
||||||
|
() => (window as unknown as { __galaxyDebug?: { ready?: boolean } }).__galaxyDebug?.ready === true,
|
||||||
|
);
|
||||||
await page.evaluate(async () => {
|
await page.evaluate(async () => {
|
||||||
await window.__galaxyDebug!.loadSession();
|
const debug = (window as unknown as {
|
||||||
await window.__galaxyDebug!.setDeviceSessionId(
|
__galaxyDebug: {
|
||||||
"phase-19-synthetic-session",
|
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,
|
mapWidth: 1,
|
||||||
mapHeight: 1,
|
mapHeight: 1,
|
||||||
localPlayerDrive: 0,
|
localPlayerDrive: 0,
|
||||||
|
localShipGroups: [],
|
||||||
|
otherShipGroups: [],
|
||||||
|
localRace: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const section = ui.getByTestId("inspector-planet");
|
const section = ui.getByTestId("inspector-planet");
|
||||||
@@ -140,6 +143,9 @@ describe("planet inspector", () => {
|
|||||||
mapWidth: 1,
|
mapWidth: 1,
|
||||||
mapHeight: 1,
|
mapHeight: 1,
|
||||||
localPlayerDrive: 0,
|
localPlayerDrive: 0,
|
||||||
|
localShipGroups: [],
|
||||||
|
otherShipGroups: [],
|
||||||
|
localRace: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent(
|
expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent(
|
||||||
@@ -176,6 +182,9 @@ describe("planet inspector", () => {
|
|||||||
mapWidth: 1,
|
mapWidth: 1,
|
||||||
mapHeight: 1,
|
mapHeight: 1,
|
||||||
localPlayerDrive: 0,
|
localPlayerDrive: 0,
|
||||||
|
localShipGroups: [],
|
||||||
|
otherShipGroups: [],
|
||||||
|
localRace: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent(
|
expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent(
|
||||||
@@ -213,6 +222,9 @@ describe("planet inspector", () => {
|
|||||||
mapWidth: 1,
|
mapWidth: 1,
|
||||||
mapHeight: 1,
|
mapHeight: 1,
|
||||||
localPlayerDrive: 0,
|
localPlayerDrive: 0,
|
||||||
|
localShipGroups: [],
|
||||||
|
otherShipGroups: [],
|
||||||
|
localRace: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent(
|
expect(ui.getByTestId("inspector-planet-kind")).toHaveTextContent(
|
||||||
@@ -246,6 +258,9 @@ describe("planet inspector", () => {
|
|||||||
mapWidth: 1,
|
mapWidth: 1,
|
||||||
mapHeight: 1,
|
mapHeight: 1,
|
||||||
localPlayerDrive: 0,
|
localPlayerDrive: 0,
|
||||||
|
localShipGroups: [],
|
||||||
|
otherShipGroups: [],
|
||||||
|
localRace: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
expect(ui.queryByTestId("inspector-planet-rename-action")).toBeNull();
|
expect(ui.queryByTestId("inspector-planet-rename-action")).toBeNull();
|
||||||
@@ -283,6 +298,9 @@ describe("planet inspector", () => {
|
|||||||
mapWidth: 1,
|
mapWidth: 1,
|
||||||
mapHeight: 1,
|
mapHeight: 1,
|
||||||
localPlayerDrive: 0,
|
localPlayerDrive: 0,
|
||||||
|
localShipGroups: [],
|
||||||
|
otherShipGroups: [],
|
||||||
|
localRace: "",
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
@@ -351,6 +369,9 @@ describe("planet inspector", () => {
|
|||||||
mapWidth: 1,
|
mapWidth: 1,
|
||||||
mapHeight: 1,
|
mapHeight: 1,
|
||||||
localPlayerDrive: 0,
|
localPlayerDrive: 0,
|
||||||
|
localShipGroups: [],
|
||||||
|
otherShipGroups: [],
|
||||||
|
localRace: "",
|
||||||
},
|
},
|
||||||
context,
|
context,
|
||||||
});
|
});
|
||||||
@@ -386,6 +407,9 @@ describe("planet inspector", () => {
|
|||||||
mapWidth: 1,
|
mapWidth: 1,
|
||||||
mapHeight: 1,
|
mapHeight: 1,
|
||||||
localPlayerDrive: 0,
|
localPlayerDrive: 0,
|
||||||
|
localShipGroups: [],
|
||||||
|
otherShipGroups: [],
|
||||||
|
localRace: "",
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
// Empty production strings collapse to the localised "none"
|
// Empty production strings collapse to the localised "none"
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ function makeReport(overrides: Partial<GameReport> = {}): GameReport {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("reportToWorld — ship groups", () => {
|
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 home = planet({ number: 17, x: 100, y: 100, kind: "local" });
|
||||||
const { world, hitLookup } = reportToWorld(
|
const { world, hitLookup } = reportToWorld(
|
||||||
makeReport({
|
makeReport({
|
||||||
@@ -82,18 +82,13 @@ describe("reportToWorld — ship groups", () => {
|
|||||||
],
|
],
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
// 1 planet point + 1 ship-group point.
|
// Only the planet itself contributes a primitive; the on-planet
|
||||||
expect(world.primitives.length).toBe(2);
|
// group is intentionally invisible on the map. Phase 19's
|
||||||
const groupPrimId = SHIP_GROUP_ID_OFFSETS.local + 0;
|
// `lib/inspectors/planet/ship-groups.svelte` lists it inside the
|
||||||
const group = world.primitives.find((p) => p.id === groupPrimId);
|
// planet inspector instead.
|
||||||
expect(group).toBeDefined();
|
expect(world.primitives.length).toBe(1);
|
||||||
if (group?.kind !== "point") throw new Error("expected point");
|
expect(hitLookup.has(SHIP_GROUP_ID_OFFSETS.local + 0)).toBe(false);
|
||||||
// Off-planet rendering: not exactly on (100, 100).
|
expect(hitLookup.get(17)).toEqual({ kind: "planet", number: 17 });
|
||||||
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" },
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("in-hyperspace local group renders at the interpolated position", () => {
|
test("in-hyperspace local group renders at the interpolated position", () => {
|
||||||
@@ -130,6 +125,36 @@ describe("reportToWorld — ship groups", () => {
|
|||||||
expect(group.y).toBe(0);
|
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", () => {
|
test("incoming group emits one dashed line + one clickable point", () => {
|
||||||
const dest = planet({ number: 1, x: 0, y: 0 });
|
const dest = planet({ number: 1, x: 0, y: 0 });
|
||||||
const orig = planet({ number: 9, x: 100, y: 0 });
|
const orig = planet({ number: 9, x: 100, y: 0 });
|
||||||
|
|||||||
Reference in New Issue
Block a user