feat(ui): Phase 29 map visibility toggles
Tests · Go / test (push) Successful in 2m31s
Tests · UI / test (push) Failing after 8m7s

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:
Ilia Denisov
2026-05-19 21:33:53 +02:00
parent 65c0fbb87d
commit 2bd1b54936
32 changed files with 3046 additions and 63 deletions
+51 -3
View File
@@ -31,7 +31,6 @@
import type {
GameReport,
ReportIncomingShipGroup,
ReportLocalShipGroup,
ReportOtherShipGroup,
ReportPlanet,
@@ -107,14 +106,51 @@ const PRIORITY_INCOMING_POINT = 6;
const PRIORITY_INCOMING_LINE = 0;
const PRIORITY_UNIDENTIFIED = 4;
/**
* ShipGroupCategory tags every emitted primitive with the toggleable
* surface it belongs to. The Phase 29 hide-set machinery in
* `lib/active-view/map.svelte` looks these up via `categories` to
* decide whether to hide the primitive when the matching `MapToggles`
* flag is `false`.
*/
export type ShipGroupCategory =
| "hyperspaceGroup"
| "incomingGroup"
| "unidentifiedGroup";
export interface ShipGroupPrimitives {
primitives: (PointPrim | LinePrim)[];
lookup: Map<PrimitiveID, ShipGroupRef>;
categories: Map<PrimitiveID, ShipGroupCategory>;
/**
* planetDependents maps a planet number to the set of primitive
* ids that should hide together with that planet. In Phase 29 the
* hide-by-id machinery cascades planet visibility onto in-space
* and incoming groups flying *to* the planet (their points + the
* trajectory / track lines). Unidentified groups have no planet
* anchor and therefore contribute nothing here.
*/
planetDependents: Map<number, Set<PrimitiveID>>;
}
function addDependent(
planetDependents: Map<number, Set<PrimitiveID>>,
planetNumber: number,
primitiveId: PrimitiveID,
): void {
let set = planetDependents.get(planetNumber);
if (set === undefined) {
set = new Set();
planetDependents.set(planetNumber, set);
}
set.add(primitiveId);
}
export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives {
const primitives: (PointPrim | LinePrim)[] = [];
const lookup = new Map<PrimitiveID, ShipGroupRef>();
const categories = new Map<PrimitiveID, ShipGroupCategory>();
const planetDependents = new Map<number, Set<PrimitiveID>>();
const planetIndex = new Map<number, ReportPlanet>();
for (const planet of report.planets) {
planetIndex.set(planet.number, planet);
@@ -129,6 +165,8 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
const id = SHIP_GROUP_ID_OFFSETS.local + i;
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_LOCAL, STYLE_LOCAL_GROUP));
lookup.set(id, { variant: "local", id: group.id });
categories.set(id, "hyperspaceGroup");
addDependent(planetDependents, group.destination, id);
// Yellow dashed track from the origin planet to the destination
// planet. The colour matches the in-space group point so the
// player can read both as one entity at a glance. Wrap-aware
@@ -140,9 +178,10 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
if (origin !== undefined && destination !== undefined) {
const dx = torusShortestDelta(origin.x, destination.x, w);
const dy = torusShortestDelta(origin.y, destination.y, h);
const lineId = SHIP_GROUP_ID_OFFSETS.localLine + i;
primitives.push({
kind: "line",
id: SHIP_GROUP_ID_OFFSETS.localLine + i,
id: lineId,
priority: PRIORITY_LOCAL_LINE,
style: STYLE_LOCAL_INSPACE_LINE,
hitSlopPx: 0,
@@ -151,6 +190,8 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
x2: origin.x + dx,
y2: origin.y + dy,
});
categories.set(lineId, "hyperspaceGroup");
addDependent(planetDependents, group.destination, lineId);
}
}
@@ -161,6 +202,8 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
const id = SHIP_GROUP_ID_OFFSETS.other + i;
primitives.push(makePoint(id, pos.x, pos.y, PRIORITY_OTHER, STYLE_OTHER_GROUP));
lookup.set(id, { variant: "other", index: i });
categories.set(id, "hyperspaceGroup");
addDependent(planetDependents, group.destination, id);
}
for (let i = 0; i < report.incomingShipGroups.length; i++) {
@@ -189,6 +232,8 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
x2: destX,
y2: destY,
});
categories.set(lineId, "incomingGroup");
addDependent(planetDependents, group.destination, lineId);
const pos = interpolateAlongLine(destX, destY, origin.x, origin.y, group.distance);
const pointId = SHIP_GROUP_ID_OFFSETS.incoming + i;
primitives.push(
@@ -202,6 +247,8 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
),
);
lookup.set(pointId, { variant: "incoming", index: i });
categories.set(pointId, "incomingGroup");
addDependent(planetDependents, group.destination, pointId);
}
for (let i = 0; i < report.unidentifiedShipGroups.length; i++) {
@@ -218,9 +265,10 @@ export function shipGroupsToPrimitives(report: GameReport): ShipGroupPrimitives
),
);
lookup.set(id, { variant: "unidentified", index: i });
categories.set(id, "unidentifiedGroup");
}
return { primitives, lookup };
return { primitives, lookup, categories, planetDependents };
}
/**