feat(ui): Phase 29 map visibility toggles
Adds the gear-icon popover on the map view with per-game persistence of every category toggle plus the wrap-mode radio. Hide-by-id and visibility-fog facilities land on the renderer so every flip applies within one frame without a Pixi remount; the wrap-mode toggle keeps its existing remount + camera-preserve path. A new server-side turn force-resets every flag to defaults so a hidden category never makes the player miss the next turn's news. Also fixes the FligthDistance → FlightDistance typo in pkg/calc/race.go (plus the single Go caller); the TS side keeps duplicating the formula until a race-level WASM bridge lands. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,311 @@
|
||||
// Phase 29 coverage for the categories + planetDependents maps that
|
||||
// `reportToWorld` now returns. The map view consumes both to feed
|
||||
// `RendererHandle.setHiddenPrimitiveIds`: categories drive the
|
||||
// per-category toggle, planetDependents drive the cascade (planet
|
||||
// hidden → markers + in-space + incoming groups flying to it hide
|
||||
// together).
|
||||
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
import type {
|
||||
GameReport,
|
||||
ReportBattle,
|
||||
ReportBombing,
|
||||
ReportIncomingShipGroup,
|
||||
ReportLocalShipGroup,
|
||||
ReportOtherShipGroup,
|
||||
ReportPlanet,
|
||||
ReportUnidentifiedShipGroup,
|
||||
} from "../src/api/game-state";
|
||||
import { BATTLE_MARKER_ID_PREFIX, BOMBING_MARKER_ID_PREFIX } from "../src/map/battle-markers";
|
||||
import { SHIP_GROUP_ID_OFFSETS } from "../src/map/ship-groups";
|
||||
import { reportToWorld } from "../src/map/state-binding";
|
||||
import { EMPTY_SHIP_GROUPS } from "./helpers/empty-ship-groups";
|
||||
|
||||
function makeReport(overrides: Partial<GameReport> = {}): GameReport {
|
||||
return {
|
||||
turn: 1,
|
||||
mapWidth: 4000,
|
||||
mapHeight: 4000,
|
||||
planetCount: 0,
|
||||
planets: [],
|
||||
race: "",
|
||||
localShipClass: [],
|
||||
routes: [],
|
||||
localPlayerDrive: 0,
|
||||
localPlayerWeapons: 0,
|
||||
localPlayerShields: 0,
|
||||
localPlayerCargo: 0,
|
||||
...EMPTY_SHIP_GROUPS,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
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 makeLocalShipGroup(
|
||||
overrides: Partial<ReportLocalShipGroup>,
|
||||
): ReportLocalShipGroup {
|
||||
return {
|
||||
id: "00000000-0000-0000-0000-000000000000",
|
||||
count: 1,
|
||||
class: "Scout",
|
||||
tech: { drive: 1, weapons: 0, shields: 0, cargo: 0 },
|
||||
cargo: "NONE",
|
||||
load: 0,
|
||||
destination: 0,
|
||||
origin: null,
|
||||
range: null,
|
||||
speed: 1,
|
||||
mass: 0,
|
||||
state: "InOrbit",
|
||||
fleet: null,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeOtherShipGroup(
|
||||
overrides: Partial<ReportOtherShipGroup>,
|
||||
): ReportOtherShipGroup {
|
||||
return {
|
||||
count: 1,
|
||||
class: "Cruiser",
|
||||
tech: { drive: 1, weapons: 0, shields: 0, cargo: 0 },
|
||||
cargo: "NONE",
|
||||
load: 0,
|
||||
destination: 0,
|
||||
origin: null,
|
||||
range: null,
|
||||
speed: 1,
|
||||
mass: 0,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeIncoming(
|
||||
overrides: Partial<ReportIncomingShipGroup>,
|
||||
): ReportIncomingShipGroup {
|
||||
return {
|
||||
origin: 0,
|
||||
destination: 0,
|
||||
distance: 0,
|
||||
speed: 1,
|
||||
mass: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeUnidentified(
|
||||
overrides: Partial<ReportUnidentifiedShipGroup>,
|
||||
): ReportUnidentifiedShipGroup {
|
||||
return { x: 0, y: 0, ...overrides };
|
||||
}
|
||||
|
||||
function makeBattle(overrides: Partial<ReportBattle>): ReportBattle {
|
||||
return {
|
||||
id: "battle",
|
||||
planet: 0,
|
||||
shots: 1,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function makeBombing(overrides: Partial<ReportBombing>): ReportBombing {
|
||||
return {
|
||||
planetNumber: 0,
|
||||
planet: "",
|
||||
owner: "",
|
||||
attacker: "",
|
||||
production: "",
|
||||
industry: 0,
|
||||
population: 0,
|
||||
colonists: 0,
|
||||
industryStockpile: 0,
|
||||
materialsStockpile: 0,
|
||||
attackPower: 0,
|
||||
wiped: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("reportToWorld — categories", () => {
|
||||
test("planet primitives carry their kind-flavoured category", () => {
|
||||
const { categories } = reportToWorld(
|
||||
makeReport({
|
||||
planets: [
|
||||
makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
|
||||
makePlanet({ number: 2, kind: "other", x: 200, y: 200 }),
|
||||
makePlanet({ number: 3, kind: "uninhabited", x: 300, y: 300 }),
|
||||
makePlanet({ number: 4, kind: "unidentified", x: 400, y: 400 }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(categories.get(1)).toBe("planet-local");
|
||||
expect(categories.get(2)).toBe("planet-foreign");
|
||||
expect(categories.get(3)).toBe("planet-uninhabited");
|
||||
expect(categories.get(4)).toBe("planet-unidentified");
|
||||
});
|
||||
|
||||
test("ship-group sub-builder tags every primitive id", () => {
|
||||
const localGroupPrimId = SHIP_GROUP_ID_OFFSETS.local + 0;
|
||||
const localLineId = SHIP_GROUP_ID_OFFSETS.localLine + 0;
|
||||
const otherId = SHIP_GROUP_ID_OFFSETS.other + 0;
|
||||
const incomingPointId = SHIP_GROUP_ID_OFFSETS.incoming + 0;
|
||||
const incomingLineId = SHIP_GROUP_ID_OFFSETS.incomingLine + 0;
|
||||
const unidentifiedId = SHIP_GROUP_ID_OFFSETS.unidentified + 0;
|
||||
const { categories } = reportToWorld(
|
||||
makeReport({
|
||||
planets: [
|
||||
makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
|
||||
makePlanet({ number: 2, kind: "other", x: 200, y: 200 }),
|
||||
],
|
||||
localShipGroups: [
|
||||
makeLocalShipGroup({ origin: 1, range: 10, destination: 2 }),
|
||||
],
|
||||
otherShipGroups: [
|
||||
makeOtherShipGroup({ origin: 1, range: 10, destination: 2 }),
|
||||
],
|
||||
incomingShipGroups: [
|
||||
makeIncoming({ origin: 1, destination: 2, distance: 5 }),
|
||||
],
|
||||
unidentifiedShipGroups: [makeUnidentified({ x: 500, y: 500 })],
|
||||
}),
|
||||
);
|
||||
expect(categories.get(localGroupPrimId)).toBe("hyperspaceGroup");
|
||||
expect(categories.get(localLineId)).toBe("hyperspaceGroup");
|
||||
expect(categories.get(otherId)).toBe("hyperspaceGroup");
|
||||
expect(categories.get(incomingPointId)).toBe("incomingGroup");
|
||||
expect(categories.get(incomingLineId)).toBe("incomingGroup");
|
||||
expect(categories.get(unidentifiedId)).toBe("unidentifiedGroup");
|
||||
});
|
||||
|
||||
test("battle markers and bombing markers each carry their own category", () => {
|
||||
const { categories } = reportToWorld(
|
||||
makeReport({
|
||||
planets: [
|
||||
makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
|
||||
makePlanet({ number: 2, kind: "other", x: 200, y: 200 }),
|
||||
],
|
||||
battles: [makeBattle({ id: "b1", planet: 2 })],
|
||||
bombings: [makeBombing({ planetNumber: 2 })],
|
||||
}),
|
||||
);
|
||||
// Battle marker emits two LinePrims at `BATTLE_MARKER_ID_PREFIX | (i << 4) | (A|B)`.
|
||||
const battleA = BATTLE_MARKER_ID_PREFIX | (0 << 4) | 0;
|
||||
const battleB = BATTLE_MARKER_ID_PREFIX | (0 << 4) | 1;
|
||||
expect(categories.get(battleA)).toBe("battleMarker");
|
||||
expect(categories.get(battleB)).toBe("battleMarker");
|
||||
const bombingId = BOMBING_MARKER_ID_PREFIX | 0;
|
||||
expect(categories.get(bombingId)).toBe("bombingMarker");
|
||||
});
|
||||
});
|
||||
|
||||
describe("reportToWorld — planetDependents", () => {
|
||||
test("every planet seeds its own dependents entry with its own id", () => {
|
||||
const { planetDependents } = reportToWorld(
|
||||
makeReport({
|
||||
planets: [
|
||||
makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
|
||||
makePlanet({ number: 7, kind: "other", x: 200, y: 200 }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
expect(planetDependents.get(1)?.has(1)).toBe(true);
|
||||
expect(planetDependents.get(7)?.has(7)).toBe(true);
|
||||
});
|
||||
|
||||
test("battle / bombing markers cascade onto their anchor planet", () => {
|
||||
const { planetDependents } = reportToWorld(
|
||||
makeReport({
|
||||
planets: [
|
||||
makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
|
||||
makePlanet({ number: 2, kind: "other", x: 200, y: 200 }),
|
||||
],
|
||||
battles: [makeBattle({ planet: 2 })],
|
||||
bombings: [makeBombing({ planetNumber: 2 })],
|
||||
}),
|
||||
);
|
||||
const battleA = BATTLE_MARKER_ID_PREFIX | (0 << 4) | 0;
|
||||
const battleB = BATTLE_MARKER_ID_PREFIX | (0 << 4) | 1;
|
||||
const bombingId = BOMBING_MARKER_ID_PREFIX | 0;
|
||||
const deps = planetDependents.get(2) ?? new Set();
|
||||
expect(deps.has(2)).toBe(true);
|
||||
expect(deps.has(battleA)).toBe(true);
|
||||
expect(deps.has(battleB)).toBe(true);
|
||||
expect(deps.has(bombingId)).toBe(true);
|
||||
});
|
||||
|
||||
test("in-space groups cascade onto their destination planet", () => {
|
||||
const { planetDependents } = reportToWorld(
|
||||
makeReport({
|
||||
planets: [
|
||||
makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
|
||||
makePlanet({ number: 2, kind: "other", x: 200, y: 200 }),
|
||||
],
|
||||
localShipGroups: [
|
||||
makeLocalShipGroup({ origin: 1, range: 10, destination: 2 }),
|
||||
],
|
||||
otherShipGroups: [
|
||||
makeOtherShipGroup({ origin: 1, range: 10, destination: 2 }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
const localPointId = SHIP_GROUP_ID_OFFSETS.local + 0;
|
||||
const localLineId = SHIP_GROUP_ID_OFFSETS.localLine + 0;
|
||||
const otherId = SHIP_GROUP_ID_OFFSETS.other + 0;
|
||||
const deps = planetDependents.get(2) ?? new Set();
|
||||
expect(deps.has(localPointId)).toBe(true);
|
||||
expect(deps.has(localLineId)).toBe(true);
|
||||
expect(deps.has(otherId)).toBe(true);
|
||||
});
|
||||
|
||||
test("incoming groups cascade onto their destination planet", () => {
|
||||
const { planetDependents } = reportToWorld(
|
||||
makeReport({
|
||||
planets: [
|
||||
makePlanet({ number: 1, kind: "local", x: 100, y: 100 }),
|
||||
makePlanet({ number: 2, kind: "other", x: 200, y: 200 }),
|
||||
],
|
||||
incomingShipGroups: [
|
||||
makeIncoming({ origin: 1, destination: 2, distance: 5 }),
|
||||
],
|
||||
}),
|
||||
);
|
||||
const incomingPointId = SHIP_GROUP_ID_OFFSETS.incoming + 0;
|
||||
const incomingLineId = SHIP_GROUP_ID_OFFSETS.incomingLine + 0;
|
||||
const deps = planetDependents.get(2) ?? new Set();
|
||||
expect(deps.has(incomingPointId)).toBe(true);
|
||||
expect(deps.has(incomingLineId)).toBe(true);
|
||||
});
|
||||
|
||||
test("unidentified groups do not contribute to any planet's dependents", () => {
|
||||
const { planetDependents } = reportToWorld(
|
||||
makeReport({
|
||||
planets: [makePlanet({ number: 1, kind: "local", x: 100, y: 100 })],
|
||||
unidentifiedShipGroups: [makeUnidentified({ x: 500, y: 500 })],
|
||||
}),
|
||||
);
|
||||
// Only the local planet seeds its own entry; no other entries.
|
||||
expect(planetDependents.size).toBe(1);
|
||||
expect(planetDependents.get(1)?.size).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user