e4dc0ce029
Wires pkg/calc/ship.go into the WASM Core boundary as seven thin
wrappers (DriveEffective, EmptyMass, WeaponsBlockMass, FullMass,
Speed, CargoCapacity, CarryingMass). The ship-class designer reads
Core through a new CORE_CONTEXT_KEY populated by the in-game layout
and renders a five-row preview pane (mass, full-load mass, max
speed, range at full load, cargo capacity) that updates reactively
on every form edit and on the player's localPlayer{Drive,Weapons,
Shields,Cargo} tech levels — three of which are now decoded from
the report's Player block alongside the existing localPlayerDrive.
CarryingMass is the seventh wrapper added to the original six-function
list so that "full-load mass" composes through pkg/calc/ functions
without putting math in TypeScript.
241 lines
6.6 KiB
TypeScript
241 lines
6.6 KiB
TypeScript
// 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,
|
||
STYLE_ROUTE_CAP,
|
||
STYLE_ROUTE_COL,
|
||
STYLE_ROUTE_EMP,
|
||
STYLE_ROUTE_MAT,
|
||
buildCargoRouteLines,
|
||
} from "../src/map/cargo-routes";
|
||
|
||
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,
|
||
};
|
||
}
|
||
|
||
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);
|
||
}
|
||
expect(styleByPriority.get(8)).toBe(STYLE_ROUTE_COL);
|
||
expect(styleByPriority.get(7)).toBe(STYLE_ROUTE_CAP);
|
||
expect(styleByPriority.get(6)).toBe(STYLE_ROUTE_MAT);
|
||
expect(styleByPriority.get(5)).toBe(STYLE_ROUTE_EMP);
|
||
});
|
||
|
||
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,
|
||
};
|
||
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);
|
||
});
|
||
});
|