Files
galaxy-game/ui/frontend/tests/map-cargo-routes.test.ts
T
Ilia Denisov f6e4a4f6bd
Tests · UI / test (push) Waiting to run
Tests · UI / test (pull_request) Successful in 2m45s
feat(ui): map canvas follows light/dark theme; fix invisible gear control
The map view now selects a DARK_THEME or LIGHT_THEME palette from the
resolved app theme and threads it through every primitive builder, so
the canvas, planets, ship groups, cargo routes, battle/bombing markers,
fog, reach + selection rings, pending-Send tracks, and the pick overlay
all switch with the rest of the chrome. A theme flip remounts the
renderer preserving the camera — Pixi bakes the background at init and
every primitive bakes its colour at build, so a live re-tint is not
possible on the same instance.

This also fixes the reported bug: the gear-popover trigger and the
loading overlay hardcoded a dark navy background, so in light theme the
gear was invisible (dark icon on dark chip) until hover flipped it to a
white chip. Both now use the --color-surface-overlay token and read
correctly in both themes.

The light palette mirrors the dark one role-for-role, darkened /
saturated for contrast on a light background while keeping the incoming,
battle, and bombing accents vivid. The values are a first pass meant to
be refined during the F8 manual-QA loop.

Removes the now-dead "Phase 35" references from the code and lifts the
map-recoloring prohibition from the design-system / renderer docs; the
battle scene stays a fixed-palette data-viz surface.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 08:49:37 +02:00

256 lines
7.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Pure-function coverage for `map/cargo-routes.ts.buildCargoRouteLines`.
// The renderer turns each `ReportRouteEntry` into one shaft plus two
// arrowhead wings; the tests assert geometry on a flat fixture, on a
// torus seam-crossing fixture, and the per-load-type style/priority
// mapping. Pixi-free — the helper is a pure projection of the report.
import { describe, expect, test } from "vitest";
import type {
GameReport,
ReportPlanet,
ReportRouteEntry,
} from "../src/api/game-state";
import {
ROUTE_LINE_ID_PREFIX,
buildCargoRouteLines,
} from "../src/map/cargo-routes";
import { DARK_THEME, LIGHT_THEME } from "../src/map/world";
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";
function makePlanet(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(
planets: ReportPlanet[],
source: number,
entries: ReportRouteEntry[],
mapWidth = 1000,
mapHeight = 1000,
): GameReport {
return {
turn: 1,
mapWidth,
mapHeight,
planetCount: planets.length,
planets,
race: "Earthlings",
localShipClass: [],
routes: [{ sourcePlanetNumber: source, entries }],
localPlayerDrive: 1,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
...EMPTY_SHIP_GROUPS,
};
}
describe("buildCargoRouteLines", () => {
test("emits one shaft + two wings per route entry", () => {
const report = makeReport(
[
makePlanet({ number: 1, x: 100, y: 100 }),
makePlanet({ number: 2, x: 300, y: 100 }),
],
1,
[{ loadType: "COL", destinationPlanetNumber: 2 }],
);
const lines = buildCargoRouteLines(report);
expect(lines.length).toBe(3);
expect(lines.every((l) => l.kind === "line")).toBe(true);
});
test("shaft endpoints follow the no-wrap straight line", () => {
const report = makeReport(
[
makePlanet({ number: 1, x: 100, y: 100 }),
makePlanet({ number: 2, x: 300, y: 100 }),
],
1,
[{ loadType: "COL", destinationPlanetNumber: 2 }],
);
const [shaft] = buildCargoRouteLines(report);
expect(shaft).toBeDefined();
if (shaft === undefined) return;
expect(shaft.x1).toBe(100);
expect(shaft.y1).toBe(100);
expect(shaft.x2).toBe(300);
expect(shaft.y2).toBe(100);
});
test("shaft uses the torus-shortest delta on the seam", () => {
// Source at x=950, dest at x=50 in a world 1000 wide. The
// shorter wrap is +100 (right past x=1000 to x=1050), not
// 900 (left to x=50).
const report = makeReport(
[
makePlanet({ number: 1, x: 950, y: 500 }),
makePlanet({ number: 2, x: 50, y: 500 }),
],
1,
[{ loadType: "MAT", destinationPlanetNumber: 2 }],
1000,
1000,
);
const [shaft] = buildCargoRouteLines(report);
expect(shaft).toBeDefined();
if (shaft === undefined) return;
expect(shaft.x1).toBe(950);
expect(shaft.x2).toBe(1050); // 950 + 100
expect(shaft.y2).toBe(500);
});
test("each load type maps to the documented style and priority", () => {
const report = makeReport(
[
makePlanet({ number: 1, x: 100, y: 100 }),
makePlanet({ number: 2, x: 200, y: 100 }),
makePlanet({ number: 3, x: 300, y: 100 }),
makePlanet({ number: 4, x: 400, y: 100 }),
makePlanet({ number: 5, x: 500, y: 100 }),
],
1,
[
{ loadType: "COL", destinationPlanetNumber: 2 },
{ loadType: "CAP", destinationPlanetNumber: 3 },
{ loadType: "MAT", destinationPlanetNumber: 4 },
{ loadType: "EMP", destinationPlanetNumber: 5 },
],
);
const lines = buildCargoRouteLines(report);
expect(lines.length).toBe(12);
const styleByPriority = new Map<number, typeof lines[number]["style"]>();
for (const line of lines) {
const existing = styleByPriority.get(line.priority);
if (existing === undefined) styleByPriority.set(line.priority, line.style);
else expect(existing).toBe(line.style);
}
// Default (dark) palette colours, one per load type.
expect(styleByPriority.get(8)?.strokeColor).toBe(DARK_THEME.routeCol);
expect(styleByPriority.get(7)?.strokeColor).toBe(DARK_THEME.routeCap);
expect(styleByPriority.get(6)?.strokeColor).toBe(DARK_THEME.routeMat);
expect(styleByPriority.get(5)?.strokeColor).toBe(DARK_THEME.routeEmp);
});
test("uses the supplied palette's stroke colours", () => {
const report = makeReport(
[
makePlanet({ number: 1, x: 100, y: 100 }),
makePlanet({ number: 2, x: 200, y: 100 }),
],
1,
[{ loadType: "COL", destinationPlanetNumber: 2 }],
);
const [shaft] = buildCargoRouteLines(report, undefined, LIGHT_THEME);
expect(shaft.style.strokeColor).toBe(LIGHT_THEME.routeCol);
expect(LIGHT_THEME.routeCol).not.toBe(DARK_THEME.routeCol);
});
test("line ids carry the ROUTE_LINE_ID_PREFIX high bit", () => {
const report = makeReport(
[
makePlanet({ number: 1, x: 100, y: 100 }),
makePlanet({ number: 2, x: 200, y: 100 }),
],
1,
[{ loadType: "COL", destinationPlanetNumber: 2 }],
);
const lines = buildCargoRouteLines(report);
for (const line of lines) {
expect((line.id & ROUTE_LINE_ID_PREFIX) !== 0).toBe(true);
}
// Three distinct ids — one per segment.
const ids = new Set(lines.map((l) => l.id));
expect(ids.size).toBe(3);
});
test("skips routes whose source or destination is missing", () => {
const report = makeReport(
[makePlanet({ number: 1, x: 100, y: 100 })],
1,
[
{ loadType: "COL", destinationPlanetNumber: 999 }, // unknown dest
],
);
expect(buildCargoRouteLines(report).length).toBe(0);
});
test("skips zero-length routes (source == destination coords)", () => {
const report = makeReport(
[
makePlanet({ number: 1, x: 100, y: 100 }),
makePlanet({ number: 2, x: 100, y: 100 }),
],
1,
[{ loadType: "COL", destinationPlanetNumber: 2 }],
);
expect(buildCargoRouteLines(report).length).toBe(0);
});
test("returns an empty array when no routes are configured", () => {
const report: GameReport = {
turn: 1,
mapWidth: 1000,
mapHeight: 1000,
planetCount: 1,
planets: [makePlanet({ number: 1, x: 100, y: 100 })],
race: "Earthlings",
localShipClass: [],
routes: [],
localPlayerDrive: 1,
localPlayerWeapons: 0,
localPlayerShields: 0,
localPlayerCargo: 0,
...EMPTY_SHIP_GROUPS,
};
expect(buildCargoRouteLines(report)).toEqual([]);
});
test("arrowhead wings symmetric around the shaft direction", () => {
const report = makeReport(
[
makePlanet({ number: 1, x: 0, y: 0 }),
makePlanet({ number: 2, x: 100, y: 0 }),
],
1,
[{ loadType: "COL", destinationPlanetNumber: 2 }],
);
const [shaft, leftWing, rightWing] = buildCargoRouteLines(report);
expect(shaft).toBeDefined();
expect(leftWing).toBeDefined();
expect(rightWing).toBeDefined();
if (
shaft === undefined ||
leftWing === undefined ||
rightWing === undefined
)
return;
// Both wings start at the head.
expect(leftWing.x1).toBe(shaft.x2);
expect(leftWing.y1).toBe(shaft.y2);
expect(rightWing.x1).toBe(shaft.x2);
expect(rightWing.y1).toBe(shaft.y2);
// And land symmetrically around the y axis (shaft along +x).
expect(leftWing.y2 + rightWing.y2).toBeCloseTo(0);
expect(leftWing.x2).toBeCloseTo(rightWing.x2);
});
});