diff --git a/assets/videos/ss-takeoff-03.mp4 b/assets/videos/ss-takeoff-03.mp4 new file mode 100644 index 0000000..ed04d6b Binary files /dev/null and b/assets/videos/ss-takeoff-03.mp4 differ diff --git a/data/map.json b/data/map.json index 7e2324a..73f088a 100644 --- a/data/map.json +++ b/data/map.json @@ -172,11 +172,19 @@ "void": { "speed": 0.2, "amp": 0.3 } }, "edges": { - "_comment": "The jump-lane web — three reads of the same thin line. used = a lane the run TRAVELED (bright glow pass + the flow packet); frontier = exactly one end charted (the next step); unexplored = the faint maze. width = plate-px at 1× (0.75–1.25× with zoom).", + "_comment": "The jump-lane web — three reads of the same thin line. used = a lane the run TRAVELED (bright glow pass + the flow packet); frontier = exactly one end charted (the next step); unexplored = the faint maze. route = a lane on the active SET DESTINATION path (the \"where am I going\" line, data/gates.json → route.compassColor orange) — drawn above used/frontier so the course reads clearly. width = plate-px at 1× (0.75–1.25× with zoom).", + "route": { "color": "#ff8c1a", "width": 2.4, "alpha": 0.95 }, "used": { "color": "#8df3ff", "width": 1.8, "alpha": 0.95 }, "frontier": { "color": "#3fb9d8", "width": 1.1, "alpha": 0.62 }, "unexplored": { "color": "#2a4f70", "width": 0.8, "alpha": 0.3 } }, + "destination": { + "_comment": "The DESTINATION star's ring on the GALAXY tab — the route's final stop, circled so the target is unmistakable. color = data/gates.json → route.compassColor (orange); width/radiusMul = the ring's plate-px width and its radius as a multiple of the star's glow radius; alpha = the ring's opacity. Shown only while a destination is set (snapshot.destinationId).", + "color": "#ff8c1a", + "width": 2.5, + "radiusMul": 2.1, + "alpha": 0.9 + }, "hull": { "_comment": "The CHARTED REGION — the convex hull of the visited systems, inflated by `padding` (a fraction of the plate width at 1× zoom — stable under zoom). A soft fill + a subtle double outline (the discovered-area shading). FACTIONS (planned): per-faction territories reuse this exact pass, colored per faction.", "enabled": true, diff --git a/dev/exit-gate-shot.html b/dev/exit-gate-shot.html new file mode 100644 index 0000000..21d925a --- /dev/null +++ b/dev/exit-gate-shot.html @@ -0,0 +1,17 @@ + + + + + + Orbit — Exit gate shot (dev) + + + + +
+ + + diff --git a/dev/exit-gate-shot.mjs b/dev/exit-gate-shot.mjs new file mode 100644 index 0000000..0c2a941 --- /dev/null +++ b/dev/exit-gate-shot.mjs @@ -0,0 +1,130 @@ +/** + * Dev-only: boot GameScene, set a DESTINATION (a far star), and open the + * MAP console's CURRENT SYSTEM tab so a headless screenshot shows: + * · the ROUTE EXIT GATE circled in orange (the gate the player should + * path out of the system from to reach the destination), + * · an orange line from the ship to that gate ("path out of the system"). + * + * node dev/server.mjs 8084 + * node dev/shot-firefox.mjs \ + * "http://127.0.0.1:8084/dev/exit-gate-shot.html" \ + * "window.__EXIT_GATE ? window.__EXIT_GATE.ready : null" \ + * EXIT_GATE.png 40000 + * + * The report (top-left) lists console errors + the exit-gate state. + */ +import Phaser from '../js/vendor/phaser.js'; +import { config } from '../js/config/Config.js'; +import { ConfigLoader } from '../js/config/ConfigLoader.js'; +import { createGameConfig } from '../js/config/GameConfig.js'; +import { GameScene } from '../js/scenes/GameScene.js'; + +const data = await ConfigLoader.load(); +config.init(data); + +const errors = []; +const origErr = console.error.bind(console); +console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); }; +window.addEventListener('error', (e) => errors.push(String(e.message))); +window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`)); + +const gameConfig = createGameConfig(); +gameConfig.scene = [GameScene]; +const game = new Phaser.Game(gameConfig); +window.game = game; + +const report = document.createElement('pre'); +report.id = 'report'; +report.style.cssText = 'position:fixed;left:10px;top:10px;z-index:9999;max-width:70%;margin:0;padding:8px 12px;font:13px/1.5 monospace;color:#eaf6ff;background:rgba(6,20,16,0.92);border:1px solid #1b3a5a;white-space:pre-wrap;'; +document.body.appendChild(report); +const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); }; +setReport('booting…'); + +const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); + +async function waitScene() { + for (let i = 0; i < 100; i++) { + const s = game.scene.getScene('GameScene'); + if (s && s.ship && s.mapWindow) return s; + await sleep(100); + } + throw new Error('GameScene never booted'); +} + +try { + const s = await waitScene(); + await sleep(800); + + // Pick a FAR destination (a multi-hop route, so the exit gate is the + // route's next gate in the CURRENT system). + const sysId = s.systemRecord.id; + const records = s.galaxy.records; + const cur = records.find((r) => r.id === sysId); + const others = records.filter((r) => r.id !== sysId); + others.sort((a, b) => { + const da = Math.hypot(a.x - cur.x, a.y - cur.y); + const db = Math.hypot(b.x - cur.x, b.y - cur.y); + return db - da; + }); + const dest = others[2] ?? others[0]; + if (!dest) throw new Error('no destination candidate'); + + const destContent = s.galaxy.contentOf(dest.id); + const destPlanet = (destContent.planets ?? [])[0]; + const destObjId = destPlanet?.name ?? null; + + // Set the destination. + s.setDestination(dest.id, destObjId); + await sleep(400); + + // Discover the exit gate (the route's next gate in the CURRENT system) + // so it's drawn on the chart. + const exitGate = s.routeNextGate(); + if (exitGate) { + let known = s.discovery.bySystem.get(sysId); + if (!known) { known = new Set(); s.discovery.bySystem.set(sysId, known); } + s.discovery.check(sysId, exitGate.x, exitGate.y, [{ id: exitGate.discoveryId, x: exitGate.x, y: exitGate.y, radius: 100 }]); + } + await sleep(300); + + // Open the MAP console's CURRENT SYSTEM tab (the default mode). + s.mapWindow._switchMode('currentSystem'); + s.mapWindow.open(); + await sleep(1600); + + // Collect the state for the report. + const chartSnap = s.mapChartSnapshot(); + const exitInfo = chartSnap?.routeExitGate; + const exitObj = (chartSnap?.objects ?? []).find((o) => o?.id === exitInfo?.id); + + window.__EXIT_GATE = { + ready: true, + errors, + destSystem: dest.id, + destName: dest.name, + exitGateId: exitInfo?.id, + exitGateName: exitInfo?.name, + exitGateDiscovered: exitObj?.discovered, + exitGateX: exitInfo?.x, + exitGateY: exitInfo?.y, + shipX: chartSnap?.ship?.x, + shipY: chartSnap?.ship?.y, + destinationId: chartSnap?.destinationId, + }; + + const lines = [ + `DESTINATION: ${dest.name} (${dest.id})`, + `EXIT GATE: ${exitInfo?.name ?? 'null'} (${exitInfo?.id ?? 'null'})`, + `EXIT GATE DISCOVERED: ${exitObj?.discovered}`, + `EXIT GATE POS: (${exitInfo?.x?.toFixed(0)}, ${exitInfo?.y?.toFixed(0)})`, + `SHIP POS: (${chartSnap?.ship?.x?.toFixed(0)}, ${chartSnap?.ship?.y?.toFixed(0)})`, + `DESTINATION IN CURRENT SYS: ${chartSnap?.destinationId ?? 'no (route exit gate shown)'}`, + `ERRORS: ${errors.length ? errors.join(' | ') : 'none'}`, + ]; + setReport(lines); + console.log('EXIT GATE READY', JSON.stringify(window.__EXIT_GATE, null, 2)); +} catch (e) { + const lines = ['FATAL: ' + (e?.message ?? e), ...errors]; + setReport(lines); + window.__EXIT_GATE = { ready: true, errors: [...errors, String(e?.message ?? e)] }; +} diff --git a/dev/galaxy-map.test.mjs b/dev/galaxy-map.test.mjs index edd10eb..f852817 100644 --- a/dev/galaxy-map.test.mjs +++ b/dev/galaxy-map.test.mjs @@ -126,6 +126,28 @@ const b = others[1]; const vb = snap2.systems.find((s) => s.id === e.b).visited; return va !== vb; }).length); + + // ROUTE + DESTINATION: the active SET DESTINATION path (its lanes marked + // `route: true`) and the destination star (`isDestination: true`). + const routeLane = edgeKey(homeId, a.id); + const snapR = buildGalaxySnapshot({ + galaxy, + visited: [homeId], + used: [], + live: new Set(), + currentSystemId: homeId, + route: [routeLane], + destinationId: a.id, + }); + check('snapR: destinationId carried', snapR.destinationId === a.id); + check('snapR: destination star flagged', snapR.systems.find((s) => s.id === a.id)?.isDestination === true); + check('snapR: non-destination stars unflagged', snapR.systems.find((s) => s.id === b.id)?.isDestination === false); + check('snapR: route lane flagged', snapR.edges.find((e) => e.key === routeLane)?.route === true); + check('snapR: non-route lanes unflagged', snapR.edges.every((e) => e.route === (e.key === routeLane))); + // No route / no destination → everything unflagged (the default). + const snapNR = buildGalaxySnapshot({ galaxy, visited: [homeId], currentSystemId: homeId }); + check('snapNR: no destination → all unflagged', snapNR.destinationId === null && snapNR.systems.every((s) => s.isDestination === false)); + check('snapNR: no route → no route edges', snapNR.edges.every((e) => e.route === false)); } // --------------------------------------------------------------------------- diff --git a/dev/route-shot.html b/dev/route-shot.html new file mode 100644 index 0000000..2a61ff6 --- /dev/null +++ b/dev/route-shot.html @@ -0,0 +1,17 @@ + + + + + + Orbit — Route shot (dev) + + + + +
+ + + diff --git a/dev/route-shot.mjs b/dev/route-shot.mjs new file mode 100644 index 0000000..10cc10d --- /dev/null +++ b/dev/route-shot.mjs @@ -0,0 +1,154 @@ +/** + * Dev-only: boot GameScene, set a DESTINATION (a far star's first planet), + * and open the MAP console's GALAXY tab so a headless screenshot shows: + * · the route (the active SET DESTINATION path) in ORANGE on the lanes, + * · the destination star circled in orange, + * · the compass's route gate (orange, full-sized). + * + * node dev/server.mjs 8083 # static server + * node dev/shot-firefox.mjs \ + * "http://127.0.0.1:8083/dev/route-shot.html" \ + * "window.__ROUTE_SHOT ? window.__ROUTE_SHOT.ready : null" \ + * ROUTE_SHOT.png 40000 + * + * The report (top-left) lists console errors + the route state. + */ +import Phaser from '../js/vendor/phaser.js'; +import { config } from '../js/config/Config.js'; +import { ConfigLoader } from '../js/config/ConfigLoader.js'; +import { createGameConfig } from '../js/config/GameConfig.js'; +import { GameScene } from '../js/scenes/GameScene.js'; + +const data = await ConfigLoader.load(); +config.init(data); + +// Capture console errors + uncaught exceptions for the report. +const errors = []; +const origErr = console.error.bind(console); +console.error = (...a) => { errors.push(a.map(String).join(' ')); origErr(...a); }; +window.addEventListener('error', (e) => errors.push(String(e.message))); +window.addEventListener('unhandledrejection', (e) => errors.push(`rejection: ${e.reason}`)); + +const gameConfig = createGameConfig(); +gameConfig.scene = [GameScene]; +const game = new Phaser.Game(gameConfig); +window.game = game; + +// The report lives OUTSIDE the canvas — a DOM
 the screenshot can
+// always read (headless canvases don't have to cooperate).
+const report = document.createElement('pre');
+report.id = 'report';
+report.style.cssText = 'position:fixed;left:10px;top:10px;z-index:9999;max-width:70%;margin:0;padding:8px 12px;font:13px/1.5 monospace;color:#eaf6ff;background:rgba(6,20,16,0.92);border:1px solid #1b3a5a;white-space:pre-wrap;';
+document.body.appendChild(report);
+const setReport = (lines) => { report.textContent = (Array.isArray(lines) ? lines : [lines]).join('\n'); };
+setReport('booting…');
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+async function waitScene() {
+  for (let i = 0; i < 100; i++) {
+    const s = game.scene.getScene('GameScene');
+    if (s && s.ship && s.mapWindow) return s;
+    await sleep(100);
+  }
+  throw new Error('GameScene never booted');
+}
+
+try {
+  const s = await waitScene();
+  await sleep(800); // let the boot settle
+
+  // Pick a FAR destination: the furthest star from the current system
+  // (a multi-hop route, so the orange path is visible on several lanes).
+  const sysId = s.systemRecord.id;
+  const records = s.galaxy.records;
+  const cur = records.find((r) => r.id === sysId);
+  const others = records.filter((r) => r.id !== sysId);
+  others.sort((a, b) => {
+    const da = Math.hypot(a.x - cur.x, a.y - cur.y);
+    const db = Math.hypot(b.x - cur.x, b.y - cur.y);
+    return db - da; // farthest first
+  });
+  // Use the 3rd-furthest (leave some margin so the route is a real path,
+  // not a single hop).
+  const dest = others[2] ?? others[0];
+  if (!dest) throw new Error('no destination candidate');
+
+  // Discover the destination's first planet (so it has an objectId).
+  const destContent = s.galaxy.contentOf(dest.id);
+  const destPlanet = (destContent.planets ?? [])[0];
+  const destObjId = destPlanet?.name ?? null;
+
+  // Set the destination (the route is derived from it).
+  s.setDestination(dest.id, destObjId);
+  await sleep(400);
+
+  // Open the MAP console's GALAXY tab (the route + destination ring are
+  // drawn there). Use the window's own open() (reveal tween + state).
+  s.mapWindow._switchMode('galaxy');
+  s.mapWindow.open();
+  await sleep(1600); // let the reveal tween + ripple settle + the route paint
+
+  // Collect the route state for the report.
+  const plan = s.routePlan();
+  const routeKeys = s.routeEdgeKeys();
+  const snap = s.galaxySnapshot();
+  const destStar = snap?.systems?.find((x) => x.isDestination);
+  const routeEdges = (snap?.edges ?? []).filter((e) => e.route);
+
+  // --- Now switch to the SYSTEM tab and open the destination system's chart.
+  // --- Discover the destination object first (so it's drawn on the chart),
+  // --- then the destination ring should appear around it.
+  const destSysId = dest.id;
+  if (destPlanet) {
+    // Discover the destination planet (so it's drawn on the chart).
+    let known = s.discovery.bySystem.get(destSysId);
+    if (!known) { known = new Set(); s.discovery.bySystem.set(destSysId, known); }
+    s.discovery.check(destSysId, destPlanet.x, destPlanet.y, [{ id: destPlanet.name, x: destPlanet.x, y: destPlanet.y, radius: 100 }]);
+  }
+  await sleep(300);
+
+  // Switch to the SYSTEM tab and open the destination system's chart.
+  s.mapWindow._switchMode('system');
+  s.mapWindow._openSystem(destSysId);
+  await sleep(1200); // let the chart repaint
+
+  // Collect the chart state for the report.
+  const chartSnap = s.systemChartSnapshotFor(destSysId);
+  const destInChart = chartSnap?.objects?.find((o) => o.id === chartSnap.destinationId);
+
+  window.__ROUTE_SHOT = {
+    ready: true,
+    errors,
+    destSystem: dest.id,
+    destName: dest.name,
+    destObjId,
+    plan: { path: plan?.path, hops: plan?.hops },
+    routeKeys,
+    destStar: destStar ? { id: destStar.id, name: destStar.name } : null,
+    routeEdgeCount: routeEdges.length,
+    destinationId: snap?.destinationId,
+    chartDestinationId: chartSnap?.destinationId,
+    chartDestDiscovered: destInChart?.discovered,
+    chartDestName: destInChart?.name,
+  };
+
+  const lines = [
+    `DESTINATION: ${dest.name} (${dest.id})`,
+    `OBJECT: ${destObjId}`,
+    `ROUTE: ${plan?.path?.join(' → ') ?? 'null'}`,
+    `HOPS: ${plan?.hops}`,
+    `ROUTE EDGES: ${routeEdges.length}`,
+    `DEST STAR: ${destStar ? `${destStar.name} (${destStar.id})` : 'null'}`,
+    `CHART DEST: ${chartSnap?.destinationId ?? 'null'}`,
+    `CHART DEST DISCOVERED: ${destInChart?.discovered}`,
+    `CHART DEST NAME: ${destInChart?.name}`,
+    `ERRORS: ${errors.length ? errors.join(' | ') : 'none'}`,
+  ];
+  setReport(lines);
+  console.log('ROUTE SHOT READY', JSON.stringify(window.__ROUTE_SHOT, null, 2));
+} catch (e) {
+  const lines = ['FATAL: ' + (e?.message ?? e), ...errors];
+  setReport(lines);
+  window.__ROUTE_SHOT = { ready: true, errors: [...errors, String(e?.message ?? e)] };
+}
diff --git a/docs/PROJECT_NOTES.md b/docs/PROJECT_NOTES.md
index bdc6dd2..21dc8fe 100644
--- a/docs/PROJECT_NOTES.md
+++ b/docs/PROJECT_NOTES.md
@@ -495,6 +495,39 @@ of storing a progress counter) makes the follow/detour rules automatic:
 **Tuning** — `data/gates.json → route` (the orange `compassColor`, the
 `typeLabel` / `destTypeLabel`, the three toasts with their
 `{dest}`/`{hops}` placeholders).
+
+**Visualization** — the active route is drawn on the MAP console:
+- **GALAXY tab** — the route's lanes are drawn in **ORANGE**
+  (`data/map.json → galaxy.edges.route`, color = `route.compassColor`)
+  above the used/frontier/unexplored reads, and the DESTINATION STAR is
+  circled in orange (`data/map.json → galaxy.destination`) — the
+  "where am I going" path + target across the whole map. The route's
+  edge keys are computed by `GameScene.routeEdgeKeys()` (the consecutive
+  system pairs of `planRoute`'s path) and passed to
+  `buildGalaxySnapshot` (which marks `route: true` on those edges and
+  `isDestination: true` on the destination star). `GalaxyView._redrawEdges`
+  draws the route lanes (three-pass glow, like traveled) and
+  `GalaxyView._drawMarkers` draws the destination ring (a breathing
+  orange circle, shown only while a destination is set).
+- **CURRENT SYSTEM tab** — two cases:
+  - **Destination is IN this system** — the destination object is circled
+    in orange (the route's final stop). `GameScene.mapChartSnapshot`
+    sets `destinationId` on the snapshot; `MapWindow.paintChart` draws the
+    ring (step 8b) over the object (shown only when the object is
+    discovered — a ring over a ghost would be noise).
+  - **Destination is OUTSIDE this system** — the ROUTE EXIT GATE (the jump
+    gate the player should path out of the system from, `routeNextGate`) is
+    circled in orange + a dashed orange line is drawn from the ship to it
+    ("path out of the system"). `GameScene.mapChartSnapshot` sets
+    `routeExitGate` on the snapshot (the gate's id/x/y/name); `MapWindow.
+    paintChart` draws the ring + line (step 8c). The gate may be
+    undiscovered (the player hasn't seen it yet) — the ring + line still
+    show WHERE to go, which is the point of the route.
+- **SYSTEM tab** — the destination object is circled in orange when
+  viewing the destination system's chart. `GameScene.systemChartSnapshotFor`
+  passes `destinationId` to `systemChartSnapshot` (which carries it on
+  the snapshot); `MapWindow.paintChart` draws the ring (same step 8b).
+
 **Tests** — `dev/route.test.mjs` (already-there short-circuit, path
 validity over real gate edges, `next` is a gate neighbour, determinism +
 tree symmetry (a→b reversed === b→a), strong connectivity across sampled
diff --git a/js/galaxy/GalaxyChart.js b/js/galaxy/GalaxyChart.js
index 4297141..998f0ca 100644
--- a/js/galaxy/GalaxyChart.js
+++ b/js/galaxy/GalaxyChart.js
@@ -49,14 +49,21 @@ export function edgeKey(a, b) {
  *   (`"${from}>${to}"`, GameScene.activatedGates) — the lanes a JUMP
  *   can currently be confirmed from the current system
  * @param {string|null} [o.currentSystemId]
+ * @param {Iterable} [o.route] edge keys (edgeKey) on the active
+ *   route to the destination (GameScene.routeEdgeKeys) — the lanes the
+ *   player will fly; marked `route: true` so the plate draws them orange.
+ * @param {string|null} [o.destinationId] the destination SYSTEM id — its
+ *   star is marked `isDestination: true` so the plate circles it.
  * @returns {{name:string, seed:string, homeSystemId:string|null,
- *   currentSystemId:string|null, systems:Array, edges:Array,
+ *   currentSystemId:string|null, destinationId:string|null,
+ *   systems:Array, edges:Array,
  *   stats:{systems:number, visited:number, lanes:number, lanesUsed:number}}}
  */
-export function buildGalaxySnapshot({ galaxy, visited = null, used = null, live = null, currentSystemId = null } = {}) {
+export function buildGalaxySnapshot({ galaxy, visited = null, used = null, live = null, currentSystemId = null, route = null, destinationId = null } = {}) {
   const visitedSet = new Set(visited ?? []);
   const usedSet = new Set(used ?? []);
   const liveSet = new Set(live ?? []);
+  const routeSet = new Set(route ?? []);
   const network = galaxy?.jumpNetwork ?? null;
   const homeId = galaxy?.homeSystemId ?? null;
 
@@ -72,6 +79,9 @@ export function buildGalaxySnapshot({ galaxy, visited = null, used = null, live
       visited: visitedSet.has(s.id),
       isHome: s.id === homeId,
       isCurrent: s.id === currentSystemId,
+      // The DESTINATION star — the route's final stop. The plate circles
+      // it (GalaxyView) so the "where am I going" target is unmistakable.
+      isDestination: s.id === destinationId,
       gates: network?.gates?.get?.(s.id)?.length ?? 0,
       // FACTIONS (planned — not yet implemented): the system's faction
       // id + the player's relation tier (Neutral / Friendly / Aligned /
@@ -100,6 +110,10 @@ export function buildGalaxySnapshot({ galaxy, visited = null, used = null, live
         b: hi,
         key,
         used,
+        // ROUTE — a lane on the active route to the destination: the
+        // plate's "where am I going" path, drawn in the route's orange
+        // (data/gates.json → route.compassColor), above used/frontier.
+        route: routeSet.has(key),
         // FRONTIER — a lane leaving the charted region (exactly one end
         // visited): the "next step" of the maze, drawn brighter than the
         // unexplored web.
@@ -124,6 +138,7 @@ export function buildGalaxySnapshot({ galaxy, visited = null, used = null, live
     seed: galaxy?.seed ?? '',
     homeSystemId: homeId,
     currentSystemId,
+    destinationId,
     systems,
     edges,
     stats,
diff --git a/js/galaxy/SystemChart.js b/js/galaxy/SystemChart.js
index e8e50f8..2d6f9d1 100644
--- a/js/galaxy/SystemChart.js
+++ b/js/galaxy/SystemChart.js
@@ -159,9 +159,14 @@ export function resourceStats(discovery, systemId, content) {
  *   absent → nothing discovered (the safe default for a REMOTE system —
  *   the stats share the same footing)
  * @param {object} [o.discovery] — the Discovery state (for the stats)
+ * @param {string|null} [o.destinationId] — the DESTINATION object's id, if
+ *   this system is the destination and the destination names an object
+ *   (GameScene.systemChartSnapshotFor passes it). The chart circles it in
+ *   the route's orange (data/gates.json → route.compassColor) so the final
+ *   stop is unmistakable. Null/absent → no destination ring.
  * @returns {?{systemId:string, systemName:string, isHome:false,
  *             central:object, objects:object[], tethers:number[],
- *             ship:null, stats:{nav:object, res:object}}}
+ *             ship:null, destinationId:string|null, stats:{nav:object, res:object}}}
  *   (null when the inputs are missing)
  */
 export function systemChartSnapshot(systemId, content, o = {}) {
@@ -258,6 +263,9 @@ export function systemChartSnapshot(systemId, content, o = {}) {
     objects,
     tethers: [], // the player's tethers live in the CURRENT system only
     ship: null, // the ship lives in the CURRENT system only
+    // The DESTINATION object (if this system is the destination and the
+    // destination names one) — the chart circles it (MapWindow.paintChart).
+    destinationId: typeof o.destinationId === 'string' ? o.destinationId : null,
     stats: {
       nav: navDiscoveryStats(o.discovery, systemId, content),
       res: resourceStats(o.discovery, systemId, content),
diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js
index 45ad46c..3adc34a 100644
--- a/js/scenes/GameScene.js
+++ b/js/scenes/GameScene.js
@@ -2426,6 +2426,11 @@ export class GameScene extends Phaser.Scene {
       used: this.usedGates,
       live: this.activatedGates,
       currentSystemId: this.systemRecord.id,
+      // The active route (data/gates.json → route): its lanes drawn orange
+      // and the destination star circled (js/ui/GalaxyView.js). Absent when
+      // no destination is set (the route is derived — routePlan is null).
+      route: this.routeEdgeKeys(),
+      destinationId: this.destination?.systemId ?? null,
     });
   }
 
@@ -2545,6 +2550,33 @@ export class GameScene extends Phaser.Scene {
       objects,
       tethers,
       ship: this.ship ? { x: this.ship.x, y: this.ship.y, heading: this.ship.rotation } : null,
+      // The DESTINATION object (if the player is in the destination system
+      // and the destination names one) — the chart circles it in the route's
+      // orange (data/gates.json → route.compassColor) so the final stop is
+      // unmistakable. Null otherwise (no destination, or the destination is
+      // in another system — then the route is shown on the GALAXY tab).
+      destinationId: this.destination?.objectId && this.destination.systemId === sysId
+        ? this.destination.objectId
+        : null,
+      // The ROUTE EXIT GATE — the jump gate in THIS system that leads toward
+      // the destination (routeNextGate). Shown when the destination is OUTSIDE
+      // the current system (the player needs to leave this system to reach
+      // it): the chart circles it in orange + draws an orange line from the
+      // ship to it ("path out of the system"). Null when the destination is
+      // in this system (then destinationId is set instead) or there is no
+      // active route.
+      routeExitGate: (() => {
+        const d = this.destination;
+        if (!d?.systemId || d.systemId === sysId) return null;
+        const gate = this.routeNextGate();
+        if (!gate) return null;
+        return {
+          id: gate.discoveryId,
+          x: gate.x,
+          y: gate.y,
+          name: gate.discoveryName ?? 'Gate',
+        };
+      })(),
       stats: {
         nav: navDiscoveryStats(disc, sysId, content),
         res: resourceStats(disc, sysId, content),
@@ -2569,6 +2601,11 @@ export class GameScene extends Phaser.Scene {
     return systemChartSnapshot(systemId, this.galaxy.contentOf(systemId), {
       discovery: this.discovery,
       isDiscovered: (id) => this.discovery?.isDiscovered(systemId, id),
+      // The DESTINATION object (if this is the destination system and the
+      // destination names one) — the chart circles it in the route's orange.
+      destinationId: this.destination?.objectId && this.destination.systemId === systemId
+        ? this.destination.objectId
+        : null,
     });
   }
 
@@ -2640,6 +2677,21 @@ export class GameScene extends Phaser.Scene {
     return planRoute(this.galaxy, this.systemRecord.id, this.destination.systemId);
   }
 
+  /**
+   * The lanes on the active route — the `edgeKey`s of the segments of the
+   * planRoute path (consecutive system pairs). The galaxy plate uses this
+   * to draw the route in orange (data/gates.json → route.compassColor):
+   * the "where am I going" path across the map. Empty when no destination.
+   * @returns {Array} edge keys (js/galaxy/GalaxyChart.js → edgeKey)
+   */
+  routeEdgeKeys() {
+    const plan = this.routePlan();
+    if (!plan?.path || plan.path.length < 2) return [];
+    const out = [];
+    for (let i = 0; i < plan.path.length - 1; i++) out.push(edgeKey(plan.path[i], plan.path[i + 1]));
+    return out;
+  }
+
   /**
    * The route's NEXT POINT — the jump gate in the CURRENT system that
    * leads toward the destination (its gate.to === the route's next
diff --git a/js/ui/GalaxyView.js b/js/ui/GalaxyView.js
index cd308a3..3d34c60 100644
--- a/js/ui/GalaxyView.js
+++ b/js/ui/GalaxyView.js
@@ -180,6 +180,7 @@ export class GalaxyView {
       zoom: config.get('map.galaxy.zoom', {}),
       reveal: config.get('map.galaxy.reveal', {}),
       dialog: config.get('map.galaxy.dialog', {}),
+      destination: config.get('map.galaxy.destination', {}),
     };
 
     this._snap = null;
@@ -587,12 +588,15 @@ export class GalaxyView {
       const seg = clipLineToRect(a.lx, a.ly, b.lx, b.ly, this._plateRect);
       if (!seg) continue;
       const [ax, ay, bx, by] = seg;
-      const st = e.used ? ec.used : e.frontier ? ec.frontier : ec.unexplored;
+      // ROUTE lanes (the active SET DESTINATION path) read first — the
+      // "where am I going" line in orange (data/map.json → galaxy.edges.route,
+      // color = data/gates.json → route.compassColor) — above used/frontier.
+      const st = e.route ? ec.route : e.used ? ec.used : e.frontier ? ec.frontier : ec.unexplored;
       const color = toColor(st.color);
       const alpha = st.alpha ?? 0.5;
       const width = Math.max(0.6, (st.width ?? 1) * (0.75 + 0.25 * k));
-      if (e.used) {
-        // the TRAVELED lane — three passes: halo, body, bright core
+      if (e.route || e.used) {
+        // the ROUTE / TRAVELED lane — three passes: halo, body, bright core
         g.lineStyle(width + 5, color, alpha * 0.14);
         g.lineBetween(ax, ay, bx, by);
         g.lineStyle(width + 1.5, color, alpha * 0.42);
@@ -907,6 +911,26 @@ export class GalaxyView {
       this._strokeClippedRing(g, cur.lx, cur.ly, dot * (1.6 + 2.2 * u), 1.6, toColor(C.neon), 0.85 * (1 - u));
       this._strokeClippedRing(g, cur.lx, cur.ly, dot * 1.35, 1, toColor(C.neon), 0.55);
     }
+    // The DESTINATION star — the route's final stop. A solid orange ring
+    // (data/map.json → galaxy.destination, color = data/gates.json →
+    // route.compassColor) so the "where am I going" target is unmistakable.
+    // Shown only while a destination is set (snapshot.destinationId); the
+    // star may still be undiscovered (the target is the thing to find).
+    const destId = this._snap.destinationId;
+    if (destId) {
+      const d = this._starById.get(destId);
+      if (d) {
+        const dc = this._cfg.destination ?? {};
+        const color = toColor(dc.color ?? '#ff8c1a');
+        const base = Math.max(12, dot * (dc.radiusMul ?? 2.1));
+        const w = Math.max(1.5, Number(dc.width) || 2);
+        const a = Number(dc.alpha) ?? 0.9;
+        // a gentle breathing pulse so it reads as "active target"
+        const breathe = 0.5 + 0.5 * Math.sin(time / 700);
+        this._strokeClippedRing(g, d.lx, d.ly, base + breathe * 4, w, color, a * (0.7 + 0.3 * breathe));
+        this._strokeClippedRing(g, d.lx, d.ly, base + 5 + breathe * 4, 1, color, a * 0.35);
+      }
+    }
   }
 
   /** Reveal ripple out of the home system (the galaxy resolves). */
diff --git a/js/ui/MapWindow.js b/js/ui/MapWindow.js
index 7f5add7..24f0a38 100644
--- a/js/ui/MapWindow.js
+++ b/js/ui/MapWindow.js
@@ -751,6 +751,80 @@ function paintChart(ctx, w, h, dpr, snap, view) {
   // 8 — the tether union boundary (over the fog, sharp)
   drawTetherArcs(ctx, snap.tethers ?? [], tf);
 
+  // 8b — the DESTINATION object's ring (the route's final stop): a solid
+  //      orange circle (data/gates.json → route.compassColor) over the
+  //      object so the "where am I going" target is unmistakable. Shown
+  //      only when the snapshot names one (destinationId) and that object
+  //      is drawn (discovered) — the undiscovered stays hidden, so a
+  //      ring over a ghost would be noise.
+  if (snap.destinationId) {
+    const dest = (snap.objects ?? []).find((o) => o?.id === snap.destinationId);
+    if (dest?.discovered) {
+      const x = tf.toX(dest.x);
+      const y = tf.toY(dest.y);
+      const rpx = Math.max(6, (dest.radius ?? 60) * tf.scale);
+      const color = toCss(config.get('gates.route.compassColor', '#ff8c1a'));
+      ctx.save();
+      ctx.strokeStyle = color;
+      ctx.globalAlpha = 0.9;
+      ctx.lineWidth = 2;
+      ctx.beginPath();
+      ctx.arc(x, y, rpx + 8, 0, Math.PI * 2);
+      ctx.stroke();
+      ctx.globalAlpha = 0.35;
+      ctx.lineWidth = 1;
+      ctx.beginPath();
+      ctx.arc(x, y, rpx + 14, 0, Math.PI * 2);
+      ctx.stroke();
+      ctx.restore();
+    }
+  }
+
+  // 8c — the ROUTE EXIT GATE (the jump gate the player should path out of
+  //      this system from to reach the destination): an orange circle over
+  //      the gate + an orange line from the ship to it ("path out of the
+  //      system"). Shown when the destination is OUTSIDE the current system
+  //      and the snapshot names an exit gate (routeExitGate). The gate may
+  //      be undiscovered (the player hasn't seen it yet) — the ring + line
+  //      still show WHERE to go, which is the point of the route.
+  if (snap.routeExitGate) {
+    const ge = snap.routeExitGate;
+    const gx = tf.toX(ge.x);
+    const gy = tf.toY(ge.y);
+    const color = toCss(config.get('gates.route.compassColor', '#ff8c1a'));
+    // the gate's drawn radius (look it up in the objects list for its size)
+    const gateObj = (snap.objects ?? []).find((o) => o?.id === ge.id);
+    const rpx = gateObj ? Math.max(6, (gateObj.radius ?? 96) * tf.scale) : Math.max(8, 96 * tf.scale);
+    ctx.save();
+    // the orange line from the ship to the gate (the "path")
+    if (snap.ship) {
+      const sx = tf.toX(snap.ship.x);
+      const sy = tf.toY(snap.ship.y);
+      ctx.strokeStyle = color;
+      ctx.globalAlpha = 0.55;
+      ctx.lineWidth = 1.5;
+      ctx.setLineDash([6, 4]);
+      ctx.beginPath();
+      ctx.moveTo(sx, sy);
+      ctx.lineTo(gx, gy);
+      ctx.stroke();
+      ctx.setLineDash([]);
+    }
+    // the orange circle over the gate (the "target")
+    ctx.strokeStyle = color;
+    ctx.globalAlpha = 0.9;
+    ctx.lineWidth = 2;
+    ctx.beginPath();
+    ctx.arc(gx, gy, rpx + 10, 0, Math.PI * 2);
+    ctx.stroke();
+    ctx.globalAlpha = 0.35;
+    ctx.lineWidth = 1;
+    ctx.beginPath();
+    ctx.arc(gx, gy, rpx + 16, 0, Math.PI * 2);
+    ctx.stroke();
+    ctx.restore();
+  }
+
   // 9 — chrome + vignette
   drawChrome(ctx, w, h, bounds, tf);
   drawVignette(ctx, w, h);