diff --git a/assets/images/originals/rocky-surface-02.png b/assets/images/originals/rocky-surface-02.png
new file mode 100644
index 0000000..5596cee
Binary files /dev/null and b/assets/images/originals/rocky-surface-02.png differ
diff --git a/dev/map-click.html b/dev/map-click.html
new file mode 100644
index 0000000..d66d35a
--- /dev/null
+++ b/dev/map-click.html
@@ -0,0 +1,17 @@
+
+
+
+
+
+ Orbit — Map click (dev test)
+
+
+
+
+
+
+
+
diff --git a/dev/map-click.mjs b/dev/map-click.mjs
new file mode 100644
index 0000000..cb2aa8e
--- /dev/null
+++ b/dev/map-click.mjs
@@ -0,0 +1,225 @@
+/**
+ * Dev-only: repro + regression check for the MAP console's object click.
+ *
+ * Scenario under test:
+ * 1. open the map console (deck MAP button)
+ * 2. hover a discovered object on the chart → the tooltip
+ * (name + "TAP TO PLOT COURSE") must be part of the window
+ * 3. click it → autopilot (ship.target set)
+ * 4. close the map → NOTHING of the tooltip may linger on screen
+ *
+ * The known bug: ttPlate/ttName/ttSub/ttHint are created with
+ * scene.add() but never added to the MapWindow container (only the
+ * EMPTY ttCont is) — so they paint at depth 5, BEHIND the window
+ * (depth 80), and survive close() (nothing ever hides/clears them).
+ *
+ * node dev/server.mjs 8091
+ * node dev/shot-firefox.mjs \
+ * "http://127.0.0.1:8091/dev/map-click.html" \
+ * "window.__MAPCLICK ? window.__MAPCLICK.ready : null" \
+ * /tmp/map-click.png 30000
+ */
+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 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');
+}
+
+/** Walk up the container parents to the object's top-level display-list ancestor. */
+function topLevel(obj) {
+ let top = obj;
+ let c = obj.parentContainer; // Phaser 4: container children track their container here
+ while (c) { top = c; c = c.parentContainer; }
+ return top;
+}
+/** Effective (chained) visibility down to the top-level object. */
+function effVisible(obj) {
+ let v = true;
+ let o = obj;
+ while (o) {
+ v = v && o.visible !== false;
+ o = o.parentContainer;
+ }
+ return v;
+}
+
+try {
+ const s = await waitScene();
+ await sleep(800);
+
+ // Discover the central body + first planet + first station so the
+ // chart (and _hits) has something to point at.
+ const sysId = s.systemRecord.id;
+ let known = s.discovery.bySystem.get(sysId);
+ if (!known) { known = new Set(); s.discovery.bySystem.set(sysId, known); }
+ const push = (obj) => { if (obj && !known.has(obj.id)) s.discovery.check(sysId, obj.x, obj.y, [obj]); };
+ if (s.isHomeSystem && s.planet) push({ id: 'home', x: 0, y: 0, radius: s.planet.radius });
+ if (s.systemPlanets[0]) push({ id: s.systemPlanets[0].discoveryId, x: s.systemPlanets[0].x, y: s.systemPlanets[0].y, radius: s.systemPlanets[0].radius });
+ if (s.systemStations[0]) push({ id: s.systemStations[0].discoveryId, x: s.systemStations[0].x, y: s.systemStations[0].y, radius: s.systemStations[0].bound ?? 60 });
+
+ // Open the console, wait for the reveal + first paint.
+ s.deckAction('map');
+ await sleep(1800);
+ const win = s.mapWindow;
+ if (!win.isOpen) throw new Error('map window did not open');
+
+ // Force a fresh snapshot so _hits is populated.
+ const snap = win._snapOf();
+ if (snap && win._fp !== win._fpOf(snap)) win._applySnap(snap);
+ if (!win._hits || win._hits.length === 0) throw new Error('no hits on the chart');
+
+ const hit = win._hits[0];
+ const sx = win.geo.mapX + hit.x;
+ const sy = win.geo.mapY + hit.y;
+
+ const dl = s.sys.displayList.getChildren();
+ const iWin = dl.indexOf(win);
+ const iMapImg = dl.indexOf(win.mapImg);
+
+ const describe = (o, label) => {
+ const top = topLevel(o);
+ const iTop = dl.indexOf(top);
+ return {
+ label,
+ visible: o.visible,
+ effVisible: effVisible(o),
+ inWindow: top === win,
+ topInDisplayList: top === win ? 'the map window' : (iTop >= 0 ? `${top.type || top.constructor.name} @ dl[${iTop}] (win=${iWin})` : 'NOT in display list'),
+ paintsAboveMap: top === win || (iTop > iWin),
+ };
+ };
+
+ // ---- 1: hover the object ------------------------------------------------
+ win._setHover({ x: sx, y: sy });
+ await sleep(120);
+
+ const before = {
+ ttPlate: describe(win.ttPlate, 'ttPlate'),
+ ttName: describe(win.ttName, 'ttName'),
+ ttHint: describe(win.ttHint, 'ttHint'),
+ ttCont: describe(win.ttCont, 'ttCont'),
+ hoverG: describe(win.hoverG, 'hoverG'),
+ hover: !!win._hover,
+ hoverId: win._hover?.id ?? null,
+ ttNameText: win.ttName.text,
+ mapImg: { inDL: iMapImg >= 0, interactive: !!(win.mapImg.input && win.mapImg.input.enabled) },
+ };
+
+ // ---- 2: click the object (through the real handler) --------------------
+ const targetBefore = s.ship.target;
+ win.mapImg.emit('pointerdown', { x: sx, y: sy });
+ await sleep(120);
+ const click = {
+ targetWasSet: s.ship.target !== null && s.ship.target !== targetBefore,
+ target: s.ship.target ? { x: Math.round(s.ship.target.x), y: Math.round(s.ship.target.y) } : null,
+ flash: !!win._flash,
+ };
+
+ // a click on EMPTY plate must not retarget
+ s.ship.target = null;
+ win.mapImg.emit('pointerdown', { x: win.geo.mapX + 4, y: win.geo.mapY + 4 });
+ await sleep(120);
+ const missClick = { targetStayedNull: s.ship.target === null };
+
+ // ?keepOpen=1 — stop here (map still open, tooltip up) so a screenshot
+ // harness can capture the chart + tooltip on screen.
+ const keepOpen = new URLSearchParams(location.search).has('keepOpen');
+ let after = null, reopen = null, after2 = null;
+ if (keepOpen) {
+ const pass = click.targetWasSet && missClick.targetStayedNull;
+ window.__MAPCLICK = {
+ ready: true, pass, keepOpen: true, before, click, missClick, errors,
+ };
+ console.log('MAPCLICK (keepOpen) ready');
+ } else {
+ // ---- 3: close the map, then inspect what lingers ------------------------
+ win.close();
+ await sleep(700); // close tween is 150 ms
+
+ after = {
+ openState: win.openState,
+ ttPlate: describe(win.ttPlate, 'ttPlate'),
+ ttName: describe(win.ttName, 'ttName'),
+ ttHint: describe(win.ttHint, 'ttHint'),
+ ttCont: describe(win.ttCont, 'ttCont'),
+ hoverG: describe(win.hoverG, 'hoverG'),
+ };
+
+ // ---- 4: re-open, hover, close again (no leftovers, no crash) -----------
+ win.open();
+ await sleep(1800);
+ win._setHover({ x: sx, y: sy });
+ await sleep(120);
+ reopen = { open: win.isOpen, hoverVisible: effVisible(win.ttName) };
+ win.close();
+ await sleep(700);
+ after2 = {
+ openState: win.openState,
+ ttContHidden: !effVisible(win.ttCont),
+ ttPlateHidden: !effVisible(win.ttPlate),
+ ttNameHidden: !effVisible(win.ttName),
+ ttHintHidden: !effVisible(win.ttHint),
+ };
+ }
+
+ const results = [];
+ const check = (label, cond) => results.push({ label, pass: !!cond });
+
+ check('tooltip (ttPlate) belongs to the map window (paints with it)',
+ topLevel(win.ttPlate) === win);
+ check('tooltip name text belongs to the map window', topLevel(win.ttName) === win);
+ check('tooltip hint ("TAP TO PLOT COURSE") belongs to the map window', topLevel(win.ttHint) === win);
+ check('while open: tooltip effectively visible on the chart',
+ before.ttPlate.effVisible && before.ttName.effVisible);
+ check('while open: tooltip paints ABOVE the map surface (inside the window)',
+ before.ttPlate.paintsAboveMap && before.ttName.paintsAboveMap);
+ check('clicking a discovered object sets the ship target (autopilot)',
+ click.targetWasSet);
+ check('clicking empty plate does NOT retarget', missClick.targetStayedNull);
+ if (!keepOpen) {
+ check('after close: tooltip plate hidden (no lingering button)',
+ !after.ttPlate.effVisible);
+ check('after close: tooltip name hidden', !after.ttName.effVisible);
+ check('after close: tooltip hint hidden', !after.ttHint.effVisible);
+ check('after close: hover ring cleared',
+ !after.hoverG.effVisible || win.hoverG.commandBuffer.length === 0);
+ check('re-open: hover tooltip visible on the chart again', reopen.open && reopen.hoverVisible);
+ check('second close: tooltip fully hidden (no leftovers)',
+ after2.ttContHidden && after2.ttPlateHidden && after2.ttNameHidden && after2.ttHintHidden);
+ }
+ check('no console errors', errors.length === 0);
+
+ const pass = results.every((r) => r.pass);
+ window.__MAPCLICK = { ready: true, pass, results, before, click, missClick, after, reopen, after2, errors };
+ console.log(pass ? 'MAPCLICK PASS' : 'MAPCLICK FAIL');
+ console.log(JSON.stringify({ results, before, click, after }, null, 1));
+} catch (err) {
+ const results = [{ label: `THREW: ${err.message}`, pass: false }];
+ window.__MAPCLICK = { ready: true, pass: false, results, errors };
+ console.error('MAPCLICK FAIL (threw)', err);
+}
diff --git a/js/ui/MapWindow.js b/js/ui/MapWindow.js
index 6d222b1..7125e41 100644
--- a/js/ui/MapWindow.js
+++ b/js/ui/MapWindow.js
@@ -1348,8 +1348,14 @@ export class MapWindow extends Phaser.GameObjects.Container {
this.hoverG = s.graphics().setScrollFactor(0).setDepth(4);
this.add(this.hoverG);
- // tooltip (name + kind + "tap to plot course")
- this.ttPlate = s.graphics().setScrollFactor(0).setDepth(5);
+ // tooltip (name + kind + "tap to plot course") — ALL children of
+ // ttCont, which is inside the window container. (They used to be
+ // bare scene objects at depth 5: painted BEHIND the map window
+ // (depth 80) and never hidden on close — a ghost "tap to plot
+ // course" button left on the world after the map closed.) Inside
+ // the container they paint with the window, fade with its close
+ // tween, and hide with ttCont.
+ this.ttPlate = s.graphics().setScrollFactor(0);
this.ttName = s
.text(0, 0, '', {
fontFamily: HEADER,
@@ -1358,8 +1364,7 @@ export class MapWindow extends Phaser.GameObjects.Container {
fontStyle: 'bold',
letterSpacing: 1.5,
})
- .setScrollFactor(0)
- .setDepth(5);
+ .setScrollFactor(0);
this.ttSub = s
.text(0, 0, '', {
fontFamily: BODY,
@@ -1367,8 +1372,7 @@ export class MapWindow extends Phaser.GameObjects.Container {
color: toCss(C.dim),
letterSpacing: 1.5,
})
- .setScrollFactor(0)
- .setDepth(5);
+ .setScrollFactor(0);
this.ttHint = s
.text(0, 0, 'TAP TO PLOT COURSE', {
fontFamily: BODY,
@@ -1376,9 +1380,9 @@ export class MapWindow extends Phaser.GameObjects.Container {
color: toCss(C.amber),
letterSpacing: 2,
})
- .setScrollFactor(0)
- .setDepth(5);
+ .setScrollFactor(0);
this.ttCont = new Phaser.GameObjects.Container(this.scene, 0, 0);
+ this.ttCont.add([this.ttPlate, this.ttName, this.ttSub, this.ttHint]);
this.ttCont.setVisible(false).setDepth(5);
this.add(this.ttCont);