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>
248 lines
7.1 KiB
TypeScript
248 lines
7.1 KiB
TypeScript
// Vitest coverage for the Phase 19 ship-group → World binding. The
|
|
// `reportToWorld` function now blends planet and ship-group
|
|
// primitives in one pass and returns a hitLookup map keyed by the
|
|
// primitive id; these tests assert that each ship-group variant
|
|
// (own on-planet, own in-hyperspace, foreign in-hyperspace,
|
|
// incoming, unidentified) shows up with the expected position,
|
|
// style, priority, and lookup entry.
|
|
|
|
import "@testing-library/jest-dom/vitest";
|
|
import { describe, expect, test } from "vitest";
|
|
|
|
import type {
|
|
GameReport,
|
|
ReportPlanet,
|
|
} from "../src/api/game-state";
|
|
import { reportToWorld } from "../src/map/state-binding";
|
|
import { SHIP_GROUP_ID_OFFSETS } from "../src/map/ship-groups";
|
|
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";
|
|
|
|
function planet(overrides: Partial<ReportPlanet>): ReportPlanet {
|
|
return {
|
|
number: 0,
|
|
name: "",
|
|
x: 0,
|
|
y: 0,
|
|
kind: "local",
|
|
owner: null,
|
|
size: null,
|
|
resources: null,
|
|
industryStockpile: null,
|
|
materialsStockpile: null,
|
|
industry: null,
|
|
population: null,
|
|
colonists: null,
|
|
production: null,
|
|
freeIndustry: null,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function makeReport(overrides: Partial<GameReport> = {}): GameReport {
|
|
return {
|
|
turn: 1,
|
|
mapWidth: 1000,
|
|
mapHeight: 1000,
|
|
planetCount: 0,
|
|
planets: [],
|
|
race: "Earthlings",
|
|
localShipClass: [],
|
|
routes: [],
|
|
localPlayerDrive: 0,
|
|
localPlayerWeapons: 0,
|
|
localPlayerShields: 0,
|
|
localPlayerCargo: 0,
|
|
...EMPTY_SHIP_GROUPS,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("reportToWorld — ship groups", () => {
|
|
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({
|
|
planets: [home],
|
|
localShipGroups: [
|
|
{
|
|
id: "uuid-local-1",
|
|
count: 2,
|
|
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,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
// 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", () => {
|
|
const dest = planet({ number: 1, x: 0, y: 0 });
|
|
const orig = planet({ number: 2, x: 100, y: 0 });
|
|
const { world } = reportToWorld(
|
|
makeReport({
|
|
planets: [dest, orig],
|
|
localShipGroups: [
|
|
{
|
|
id: "uuid-local-fly",
|
|
count: 1,
|
|
class: "Cruiser",
|
|
tech: { drive: 10, weapons: 0, shields: 0, cargo: 0 },
|
|
cargo: "NONE",
|
|
load: 0,
|
|
destination: 1,
|
|
origin: 2,
|
|
range: 25,
|
|
speed: 0,
|
|
mass: 50,
|
|
state: "In_Space",
|
|
fleet: null,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
const groupPrimId = SHIP_GROUP_ID_OFFSETS.local + 0;
|
|
const group = world.primitives.find((p) => p.id === groupPrimId);
|
|
if (group?.kind !== "point") throw new Error("expected point");
|
|
// dest=(0,0), orig=(100,0), range=25 → 25 units toward orig from
|
|
// dest along the segment of length 100 → (25, 0).
|
|
expect(group.x).toBe(25);
|
|
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 });
|
|
const { world, hitLookup } = reportToWorld(
|
|
makeReport({
|
|
planets: [dest, orig],
|
|
incomingShipGroups: [
|
|
{
|
|
origin: 9,
|
|
destination: 1,
|
|
distance: 40,
|
|
speed: 20,
|
|
mass: 4,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
const lineId = SHIP_GROUP_ID_OFFSETS.incomingLine + 0;
|
|
const pointId = SHIP_GROUP_ID_OFFSETS.incoming + 0;
|
|
const line = world.primitives.find((p) => p.id === lineId);
|
|
if (line?.kind !== "line") throw new Error("expected line for incoming");
|
|
expect(line.x1).toBe(100); // origin
|
|
expect(line.x2).toBe(0); // destination
|
|
expect(line.style.strokeDashPx).toBeGreaterThan(0);
|
|
const point = world.primitives.find((p) => p.id === pointId);
|
|
if (point?.kind !== "point") throw new Error("expected point for incoming");
|
|
expect(point.x).toBe(40); // distance=40 from dest along line of len 100
|
|
expect(point.y).toBe(0);
|
|
// Hit lookup is registered only for the clickable point, not
|
|
// the dashed trajectory line.
|
|
expect(hitLookup.get(pointId)).toEqual({
|
|
kind: "shipGroup",
|
|
ref: { variant: "incoming", index: 0 },
|
|
});
|
|
expect(hitLookup.has(lineId)).toBe(false);
|
|
});
|
|
|
|
test("unidentified group renders at its absolute coordinates", () => {
|
|
const { world, hitLookup } = reportToWorld(
|
|
makeReport({
|
|
unidentifiedShipGroups: [{ x: 555, y: 222 }],
|
|
}),
|
|
);
|
|
const id = SHIP_GROUP_ID_OFFSETS.unidentified + 0;
|
|
const point = world.primitives.find((p) => p.id === id);
|
|
if (point?.kind !== "point") throw new Error("expected point");
|
|
expect(point.x).toBe(555);
|
|
expect(point.y).toBe(222);
|
|
expect(hitLookup.get(id)).toEqual({
|
|
kind: "shipGroup",
|
|
ref: { variant: "unidentified", index: 0 },
|
|
});
|
|
});
|
|
|
|
test("group whose destination is missing from the report is dropped", () => {
|
|
const { world } = reportToWorld(
|
|
makeReport({
|
|
planets: [],
|
|
localShipGroups: [
|
|
{
|
|
id: "uuid-orphan",
|
|
count: 1,
|
|
class: "Drone",
|
|
tech: { drive: 1, weapons: 0, shields: 0, cargo: 0 },
|
|
cargo: "NONE",
|
|
load: 0,
|
|
destination: 999, // not in planets
|
|
origin: null,
|
|
range: null,
|
|
speed: 0,
|
|
mass: 1,
|
|
state: "In_Orbit",
|
|
fleet: null,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
// Only the (empty) planet list contributes — no group primitive.
|
|
expect(world.primitives.length).toBe(0);
|
|
});
|
|
|
|
test("planet hitLookup entries are registered alongside ship groups", () => {
|
|
const { hitLookup } = reportToWorld(
|
|
makeReport({
|
|
planets: [planet({ number: 42, x: 0, y: 0, kind: "local" })],
|
|
}),
|
|
);
|
|
expect(hitLookup.get(42)).toEqual({ kind: "planet", number: 42 });
|
|
});
|
|
});
|