diff --git a/assets/images/originals/lava-planet-03.png b/assets/images/originals/lava-planet-03.png
new file mode 100644
index 0000000..4f2c4ff
Binary files /dev/null and b/assets/images/originals/lava-planet-03.png differ
diff --git a/assets/videos/lava-land-03.mp4 b/assets/videos/lava-land-03.mp4
new file mode 100644
index 0000000..1592b65
Binary files /dev/null and b/assets/videos/lava-land-03.mp4 differ
diff --git a/data/shopui.json b/data/shopui.json
index 65e8f9f..b80582a 100644
--- a/data/shopui.json
+++ b/data/shopui.json
@@ -141,9 +141,8 @@
"ease": "Cubic.Out"
},
"scout": {
- "_comment": "The TRADE SCOUT — the strategy panel that replaces the cargo grid when the player taps a BUY-list row (outside the stepper). Tap the same box again, the list dead space, or the panel itself to close it (the cargo grid returns) — EXCEPT the best-exit rows: tapping one offers the SET DESTINATION popup (setDestination below; the scene's ConfirmOverlay, same dialog as the map's). exitHint is the tap affordance on the section label. Data: js/economy/TradeScout.js (pure); UI: js/ui/shop/ScoutPanel.js. It FILLS the SELL grid box — only type/ink/colors live here.",
+ "_comment": "The TRADE SCOUT — the strategy panel that replaces the cargo grid when the player taps a BUY-list row (outside the stepper). Tap the same box again, the list dead space, or the panel itself to close it (the cargo grid returns) — EXCEPT the best-exit rows: tapping one offers the SET DESTINATION popup (the destination names the exit's WORLD — its planet of that class, the station, or the home world — not just the system; each exit carries objectId/objectName; setDestination below; the scene's ConfirmOverlay, same dialog as the map's). Data: js/economy/TradeScout.js (pure); UI: js/ui/shop/ScoutPanel.js. It FILLS the SELL grid box — only type/ink/colors live here.",
"topN": 3,
- "exitHint": "TAP A SYSTEM TO SET DESTINATION",
"setDestination": { "width": 520, "height": 176 },
"revealMs": 140,
"pad": 18,
@@ -197,7 +196,6 @@
"#c8d4e8",
"#d09058"
],
- "empty": "No charted exit yet — every CAPITAL buys everything. Chart a world that wants this good (or a capital) and it will rank here.",
- "footer": "TAP ANYWHERE TO RETURN TO CARGO"
+ "empty": "No charted exit yet — every CAPITAL buys everything. Chart a world that wants this good (or a capital) and it will rank here."
}
}
diff --git a/dev/saves.test.mjs b/dev/saves.test.mjs
index b75513d..826aa4d 100644
--- a/dev/saves.test.mjs
+++ b/dev/saves.test.mjs
@@ -41,6 +41,7 @@ const {
} = await import(file('save/SaveData.js'));
const { Galaxy } = await import(file('galaxy/Galaxy.js'));
const { Discovery } = await import(file('galaxy/Discovery.js'));
+const { CargoHold } = await import(file('economy/CargoHold.js'));
let failures = 0;
const check = (label, cond) => {
@@ -53,11 +54,14 @@ const SEED = 'SAVETEST';
const galaxy = Galaxy.create(SEED, { systemCount: 8 });
const system = galaxy.byId.get(galaxy.currentSystemId); // the starting system
-const fakeScene = () => ({
- registry: { map: new Map(), set(k, v) { this.map.set(k, v); }, get(k) { return this.map.get(k); } },
- galaxy,
- systemRecord: { name: system.name },
- ship: { x: 123.5, y: -77, rotation: 0.72, minerals: 37 },
+const fakeScene = () => {
+ const cargo = new CargoHold(4);
+ cargo.add(['frost-diamond', 'iron-ore']);
+ return {
+ registry: { map: new Map(), set(k, v) { this.map.set(k, v); }, get(k) { return this.map.get(k); } },
+ galaxy,
+ systemRecord: { name: system.name },
+ ship: { x: 123.5, y: -77, rotation: 0.72, minerals: 37, credits: 1250, cargo },
discovery: new Discovery(540),
depletedClusters: new Set([`${system.id}:c-depleted`]), // a mined-out field
tetherField: {
@@ -67,7 +71,8 @@ const fakeScene = () => ({
],
},
playTimeMs: 123456,
-});
+ };
+};
const makeRec = (over = {}) => ({
app: 'orbit',
@@ -210,6 +215,9 @@ const makeStorage = (fail = false) => {
check('capture: system identity carried', rec.currentSystemId === system.id && rec.systemName === system.name);
check('capture: ship carried', rec.ship.x === 123.5 && rec.ship.y === -77 && rec.ship.heading === 0.72);
check('capture: the hold is carried (ship.minerals)', rec.ship.minerals === 37);
+ check('capture: the credit ledger is carried (ship.credits)', rec.ship.credits === 1250);
+ check('capture: the trade hold is carried (ship.cargo, fill order)',
+ JSON.stringify(rec.ship.cargo) === JSON.stringify(['frost-diamond', 'iron-ore']));
check('capture: discovery carried', (rec.discovery.bySystem[system.id] ?? []).includes('pl:0'));
check('capture: tethers carried (both)', rec.tethers.length === 2 && rec.tethers[1].label === 'Outpost');
check('capture: playtime carried', rec.playTimeMs === 123456);
@@ -234,6 +242,9 @@ const makeStorage = (fail = false) => {
check('prepare: the live state is staged under the pending key',
staged !== null && staged.ship.x === 123.5 && staged.playTimeMs === 123456);
check('prepare: the hold rides along with the ship state', staged.ship.minerals === 37);
+ check('prepare: the credit ledger rides along (ship.credits)', staged.ship.credits === 1250);
+ check('prepare: the trade hold rides along (ship.cargo)',
+ JSON.stringify(staged.ship.cargo) === JSON.stringify(['frost-diamond', 'iron-ore']));
check('prepare: the comms ledger is restored (a Set, same ids)',
reg.get('commsPlayed') instanceof Set
&& reg.get('commsPlayed').has('oac-intro')
@@ -246,6 +257,9 @@ const makeStorage = (fail = false) => {
prepareLoad(regLegacy, makeRec()); // makeRec's ship has no minerals
check('prepare: a legacy record (no minerals) still loads, field absent',
regLegacy.get(PENDING_RESTORE_KEY).ship.minerals === undefined);
+ check('prepare: a legacy record (no credits/cargo) still loads, fields absent',
+ regLegacy.get(PENDING_RESTORE_KEY).ship.credits === undefined
+ && regLegacy.get(PENDING_RESTORE_KEY).ship.cargo === undefined);
check('prepare: a legacy record (no depletedClusters) stages an empty set',
regLegacy.get('depletedClusters') instanceof Set && regLegacy.get('depletedClusters').size === 0);
diff --git a/dev/scout-dest.mjs b/dev/scout-dest.mjs
index 1357483..5e3dfe0 100644
--- a/dev/scout-dest.mjs
+++ b/dev/scout-dest.mjs
@@ -8,8 +8,10 @@
* - a tapped exit row offers the exit to the SCENE (the panel does not
* close itself — propagation stops)
* - confirming SETS the destination on the (sleeping) GameScene:
- * destination.systemId = the exit, objectId null — and toasts it on
- * the surface (the game scene's toast would sit frozen while sleeping)
+ * destination.systemId = the exit, destination.objectId = the exit's
+ * WORLD (its planet of that class / station / home world — the
+ * route's final stop) — and toasts it on the surface (the game
+ * scene's toast would sit frozen while sleeping)
* - the LOCAL system exit is rejected with a note (no dialog, no
* destination change — pointing the ship at itself bounces)
* - CANCEL and ESC leave the destination untouched
@@ -159,8 +161,9 @@ window.game = game;
step('dialog-title', (await waitFor(() => dlg.title.text === 'SET DESTINATION')) && dlg.title.text === 'SET DESTINATION',
{ title: dlg.title.text });
const name = String(remoteExit.systemName ?? remoteExit.systemId).toUpperCase();
- step('dialog-body', dlg.bodyTexts[0].text === `DESTINATION — ${name}`,
- { body0: dlg.bodyTexts[0].text, want: `DESTINATION — ${name}` });
+ const obj = remoteExit.objectName ? String(remoteExit.objectName).toUpperCase() : null;
+ step('dialog-body', dlg.bodyTexts[0].text === `DESTINATION — ${obj ? `${obj} · ${name}` : name}`,
+ { body0: dlg.bodyTexts[0].text, want: `DESTINATION — ${obj ? `${obj} · ${name}` : name}` });
step('dialog-body-price', /SELLS \d+ CR\/UNIT/.test(dlg.bodyTexts[1].text ?? ''), { body1: dlg.bodyTexts[1].text });
// The scene staged the pending pick (exit + route hops) before showing.
@@ -171,8 +174,17 @@ window.game = game;
dlg.fireConfirm();
if (!(await dialogHidden())) throw new Error('dialog never closed after confirm');
step('confirmed-closed', dlg.state === 'hidden');
- step('destination-set', gs.destination?.systemId === remoteExit.systemId && gs.destination?.objectId === null,
- { destination: gs.destination });
+ step('destination-set', gs.destination?.systemId === remoteExit.systemId
+ && gs.destination?.objectId === (remoteExit.objectId ?? null),
+ { destination: gs.destination, wantObject: remoteExit.objectId ?? null });
+ if (remoteExit.objectId) {
+ // The destination must name a REAL object of the exit's world — a
+ // planet (discoveryId = its name) or the deep-space station.
+ const content = gs.galaxy.ensureContent(remoteExit.systemId);
+ const world = (content.planets ?? []).find((p) => p.name === remoteExit.objectId)
+ ?? (content.settlements ?? []).find((s) => s.id === remoteExit.objectId);
+ step('destination-names-a-world', !!world, { objectId: remoteExit.objectId, world: world?.name ?? world?.id });
+ }
step('surface-toast', /DESTINATION SET/.test(ss.noteG?.[1]?.text ?? ''), { note: ss.noteG?.[1]?.text });
// 8 — CANCEL: the same row again → popup; CANCEL leaves everything as-is.
diff --git a/dev/scout-overlap.html b/dev/scout-overlap.html
new file mode 100644
index 0000000..5cf21b1
--- /dev/null
+++ b/dev/scout-overlap.html
@@ -0,0 +1,32 @@
+
+
+
+
+ Orbit — dev scout overlap probe
+
+
+
+
+
+
+
+
+
+
diff --git a/dev/scout-overlap.mjs b/dev/scout-overlap.mjs
new file mode 100644
index 0000000..caf8b9b
--- /dev/null
+++ b/dev/scout-overlap.mjs
@@ -0,0 +1,128 @@
+/**
+ * Visual probe for a screenshot + numeric overlap report (headless brave,
+ * via dev/cdp-shot.mjs): boots the game, lands, opens the shop, opens the
+ * trade scout on a BUY row that has remote exits, and STOPS THERE (the
+ * scout panel is the shot's subject — no exit tapped, no dialog). It
+ * measures every text box in the panel (local coords) and reports any
+ * pairs whose bounds intersect, so text overlap is verifiable numerically
+ * and visually.
+ *
+ * /opt/brave.com/brave/brave --headless --disable-gpu --remote-debugging-port=9333 about:blank &
+ * python3 -m http.server 8129
+ * node dev/cdp-shot.mjs http://127.0.0.1:8129/dev/scout-overlap.html \
+ * /tmp/scout-overlap.png 'window.__SCOUT_OVERLAP__ && true' 90000
+ */
+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';
+import { SurfaceScene } from '../js/scenes/SurfaceScene.js';
+import { scoutGood } from '../js/economy/TradeScout.js';
+
+// A fixed galaxy seed (GameScene.ensureGalaxy honors it) so before/after
+// runs of this probe compare the SAME system names / world names.
+globalThis.__ORBIT_DEV_SEED = 'scout-overlap-probe';
+
+const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
+
+const data = await ConfigLoader.load();
+config.init(data);
+const gameConfig = createGameConfig();
+gameConfig.scene = [GameScene, SurfaceScene];
+const game = new Phaser.Game(gameConfig);
+window.game = game;
+
+(async () => {
+ let gs = null;
+ for (let i = 0; i < 300; i++) {
+ gs = game.scene?.getScene('GameScene') ?? null;
+ if (gs && gs.ship) break;
+ await sleep(100);
+ }
+ if (!gs || !gs.ship) { window.__SCOUT_OVERLAP__ = { pass: false, error: 'no GameScene' }; return; }
+
+ // A known ledger + cargo (something to sell), and the run has ENTERED
+ // every system — the scout then has remote exits to offer.
+ const goods = config.get('goods.goods', []);
+ gs.ship.credits = 10000;
+ gs.ship.cargo.add([goods[0].id]);
+ const sysId = gs.systemRecord.id;
+ for (const id of gs.galaxy.byId.keys()) gs.visitedSystems.add(id);
+
+ gs.startLanding(gs.commsTargetFor(gs.planet));
+ await sleep(300);
+ const ss = game.scene.getScene('SurfaceScene');
+ if (!ss) { window.__SCOUT_OVERLAP__ = { pass: false, error: 'no SurfaceScene' }; return; }
+ for (let i = 0; i < 120 && ss.sys.getStatus() !== 5; i++) await sleep(250);
+ ss.startSurface();
+ for (let i = 0; i < 40 && ss.phase !== 'surface'; i++) await sleep(100);
+ await sleep(900); // the planet HUD decode
+
+ ss.toggleShop();
+ await sleep(200);
+ if (!ss.shopView) ss.buildShopView();
+ const sv = ss.shopView;
+ if (!sv) { window.__SCOUT_OVERLAP__ = { pass: false, error: 'no shopView' }; return; }
+ sv.setTab('trade');
+ await sleep(900); // the slide-in
+
+ // Open the scout on a BUY row that has a REMOTE profitable exit — the
+ // fullest panel state (chips + 3 exit rows). Do NOT tap an exit row.
+ const visited = [...gs.visitedSystems];
+ let row = null, exits = null;
+ for (const r of sv.buyList.rows) {
+ const d = scoutGood(r.id, { galaxy: gs.galaxy, systemId: sysId, cls: sv.cls, buyUnit: r.price, visited, topN: 3 });
+ if (!d) continue;
+ const rem = d.exits.find((e) => e.systemId !== sysId && e.profit > 0);
+ if (rem) { row = r; exits = d.exits; break; }
+ }
+ if (!row || !exits) { window.__SCOUT_OVERLAP__ = { pass: false, error: 'no remote exit to open' }; return; }
+
+ sv._onRowTap(row);
+ await sleep(400);
+ const panel = sv._scout;
+ if (!panel) { window.__SCOUT_OVERLAP__ = { pass: false, error: 'no scout panel' }; return; }
+
+ // Collect every text box in the panel (container-local coords). In this
+ // Giedi build the panel's own `_content` array is the flat list of
+ // per-show objects (text/graphics/chips). Convert origin-anchored x/y to
+ // the true top-left (left = x - origin.x*width) so right-/center-aligned
+ // text (price, profit, footer) measures correctly.
+ const texts = [];
+ const isText = (o) => o && o.text != null && typeof o.width === 'number';
+ const take = (o) => {
+ if (isText(o)) {
+ const ox = o.originX ?? o.origin?.x ?? 0, oy = o.originY ?? o.origin?.y ?? 0;
+ texts.push({
+ text: String(o.text).slice(0, 60),
+ left: Math.round(o.x - ox * o.width), top: Math.round(o.y - oy * o.height),
+ w: Math.round(o.width), h: Math.round(o.height),
+ });
+ }
+ else for (const c of o?.list ?? o?.children?.list ?? []) take(c); // containers (chips)
+ };
+ for (const o of panel._content ?? []) take(o);
+ texts.sort((a, b) => (a.top - b.top) || (a.left - b.left));
+
+ // Pairwise overlap of the true text boxes (ignore sub-pixel kissing).
+ const overlaps = [];
+ for (let i = 0; i < texts.length; i++) {
+ for (let j = i + 1; j < texts.length; j++) {
+ const a = texts[i], b = texts[j];
+ const ix = Math.min(a.left + a.w, b.left + b.w) - Math.max(a.left, b.left);
+ const iy = Math.min(a.top + a.h, b.top + b.h) - Math.max(a.top, b.top);
+ if (ix > 2 && iy > 2) overlaps.push({ a: a.text, b: b.text, ix: Math.round(ix), iy: Math.round(iy) });
+ }
+ }
+
+ window.__SCOUT_OVERLAP__ = {
+ pass: overlaps.length === 0,
+ panel: { width: panel.width, height: panel.height, rows: panel._exitRows?.length },
+ good: row?.id,
+ exits: exits.map((e) => ({ sys: e.systemName, world: e.objectName, cls: e.cls, price: e.price, profit: e.profit, hops: e.hops })),
+ texts,
+ overlaps,
+ };
+ console.info('[scout-overlap] texts=' + texts.length + ' overlaps=' + overlaps.length);
+})().catch((err) => { window.__SCOUT_OVERLAP__ = { pass: false, error: String(err && err.message || err) }; });
diff --git a/dev/trade-scout.test.mjs b/dev/trade-scout.test.mjs
index 2eaa5f9..f62c0bb 100644
--- a/dev/trade-scout.test.mjs
+++ b/dev/trade-scout.test.mjs
@@ -115,6 +115,43 @@ const row = stock.find((s) => sellsHere(s.id, classOf(sysId).find((c) => c !== '
check('a charted CAPITAL always ranks as an exit', d.exits.some((e) => e.systemId === capId && g.economy.classBySystem.get(capId).has('capital')));
}
+// ---------------------------------------------------------------------------
+// 3b. Exit OBJECTS — each exit names the WORLD that pays the price
+// ---------------------------------------------------------------------------
+{
+ const visited = [...g.byId.keys()]; // everything charted — every exit class shows
+ let exitsChecked = 0, resolved = 0, unresolved = 0, bad = 0;
+ for (const good of goods) {
+ const d = scoutGood(good.id, { galaxy: g, systemId: sysId, cls, buyUnit: 1, visited });
+ for (const e of d.exits) {
+ exitsChecked += 1;
+ if (typeof e.objectId !== 'string' && e.objectId !== null) bad += 1;
+ if (typeof e.objectName !== 'string' && e.objectName !== null) bad += 1;
+ if (e.objectId == null) { unresolved += 1; continue; }
+ resolved += 1;
+ const content = g.ensureContent(e.systemId);
+ // The exit's WORLD must be a REAL object of the exit's class (the
+ // discovery id the route's final stop matches on: planets carry
+ // discoveryId = the planet NAME, stations the settlement id).
+ let ok = false;
+ if (e.cls === 'station') {
+ const st = (content.settlements ?? []).find((s) => s?.kind === 'deepSpaceStation');
+ ok = !!st && e.objectId === st.id && e.objectName === (st.name ?? st.id);
+ } else if (e.cls === 'capital') {
+ const p = (content.planets ?? []).find((x) => typeof x?.class === 'string' && x.class.startsWith('capital'));
+ ok = !!p && e.objectId === p.name && e.objectName === p.name;
+ } else {
+ const p = (content.planets ?? []).find((x) => x?.class === e.cls);
+ ok = !!p && e.objectId === p.name && e.objectName === p.name;
+ }
+ if (!ok) bad += 1;
+ }
+ }
+ check(`every exit carries objectId/objectName fields (${exitsChecked} exits, all systems charted)`, exitsChecked > 0 && bad === 0, { exitsChecked, bad });
+ check('most exits resolve a WORLD (not just the system)', resolved > 0 && resolved >= unresolved, { resolved, unresolved });
+ check('resolved objectIds are real objects of the exit class', bad === 0, { resolved, bad });
+}
+
// ---------------------------------------------------------------------------
// 4. Determinism — same inputs ⇒ identical report
// ---------------------------------------------------------------------------
diff --git a/js/economy/TradeScout.js b/js/economy/TradeScout.js
index 24eda62..31336d2 100644
--- a/js/economy/TradeScout.js
+++ b/js/economy/TradeScout.js
@@ -48,6 +48,41 @@ export function sellsHere(goodId, cls) {
return cls === 'capital' || E.wants(goodId, cls);
}
+/**
+ * The SPECIFIC OBJECT of a destination system that actually pays the exit
+ * class's price — the route's FINAL stop (the compass points at it, it
+ * wears the route pulse, and REACHED fires when the ship lands on it):
+ * · a planet class (terran/arid/rocky/gas/ice/lava) → a planet of that
+ * class (discovery id = the planet's NAME, unique in the system);
+ * · 'capital' → the capital world (class capital*);
+ * · 'station' → the deep-space station (a station shop IS the station —
+ * the only 'station'-class object in the system).
+ * The home world is special: it exists only in the STARTING system and is
+ * not in content.planets (the central body, discovery id 'home') — the
+ * home system's 'terran' class resolves to it. null when the content holds
+ * no such object (e.g. the capital pass replaced the world's class after
+ * the class roll) — the destination falls back to system level (objectId →
+ * null).
+ */
+function objectForClass(cls, content, isHome = false, homeCls = null) {
+ if (!content) return null;
+ if (isHome && homeCls && cls === homeCls) {
+ return { objectId: 'home', objectName: content.homeName ?? null };
+ }
+ const planets = Array.isArray(content.planets) ? content.planets : [];
+ if (cls === 'station') {
+ const st = (Array.isArray(content.settlements) ? content.settlements : [])
+ .find((s) => s?.kind === 'deepSpaceStation');
+ return st ? { objectId: st.id, objectName: st.name ?? st.id } : null;
+ }
+ const p = !cls
+ ? null
+ : cls === 'capital'
+ ? planets.find((x) => typeof x?.class === 'string' && x.class.startsWith('capital'))
+ : planets.find((x) => x?.class === cls);
+ return p ? { objectId: p.name, objectName: p.name } : null;
+}
+
/**
* The scout report for one good at one shop.
*
@@ -64,7 +99,7 @@ export function sellsHere(goodId, cls) {
* id:string, name:string, flavor:string, category:string|null, tier:number|null,
* localBuy:number, localSell:number|null,
* producer:Array<{cls,label,economy}>, demand:Array<{cls,label,economy}>,
- * exits:Array<{systemId,systemName,cls,clsLabel,price,profit,hops}>,
+ * exits:Array<{systemId,systemName,cls,clsLabel,price,profit,hops,objectId,objectName}>,
* bestExit:object|null, maxProfit:number, charted:number, topN:number
* } | null} — null only for an unknown good id.
*/
@@ -92,6 +127,8 @@ export function scoutGood(goodId, o = {}) {
if (systemId) visited.add(systemId); // we are here — it's charted by definition
const classBySystem = galaxy?.economy?.classBySystem ?? null;
+ const homeSystemId = galaxy?.homeSystemId ?? null;
+ const homeCls = config.get('planets.homePlanet', 'terran');
const exits = [];
for (const sysId of visited) {
const rec = galaxy?.byId?.get?.(sysId);
@@ -106,11 +143,18 @@ export function scoutGood(goodId, o = {}) {
}
if (!best) continue;
const hops = galaxy?.economy?.producerHops?.[goodId]?.[sysId];
+ // The WORLD this exit pays at — the destination names the planet (or
+ // the station / the home world), not just the system: setting it as
+ // the destination makes the route's final stop that object (see
+ // objectForClass; null → the system-level fallback).
+ const obj = objectForClass(best.cls, galaxy?.ensureContent?.(sysId) ?? null, sysId === homeSystemId, homeCls);
exits.push({
...best,
clsLabel: classInfo(best.cls).label,
profit: best.price - localBuy,
hops: Number.isFinite(hops) ? hops : null,
+ objectId: obj?.objectId ?? null,
+ objectName: obj?.objectName ?? null,
});
}
exits.sort((a, b) => (b.price - a.price) || String(a.systemName).localeCompare(String(b.systemName)));
diff --git a/js/scenes/GameScene.js b/js/scenes/GameScene.js
index 3365a40..f805bfa 100644
--- a/js/scenes/GameScene.js
+++ b/js/scenes/GameScene.js
@@ -2593,6 +2593,11 @@ export class GameScene extends Phaser.Scene {
y: arrival.y,
heading: arrival.heading,
minerals: rec.ship?.minerals ?? 0,
+ // The credit ledger + trade-goods hold ride the jump too (the ship
+ // keeps them — only the position + the system change hands). Without
+ // these the restore below would re-seed 0 credits and an empty hold.
+ credits: rec.ship?.credits ?? 0,
+ cargo: rec.ship?.cargo ?? [],
};
rec.tethers = []; // the destination's native set re-forms in create()
prepareLoad(this.registry, rec);
@@ -5037,11 +5042,12 @@ export class GameScene extends Phaser.Scene {
// Capacity stays the ship's stat (4, or an upgraded hold).
if (this.ship.cargo) this.ship.cargo.set(r.ship.cargo ?? []);
}
- // The corner readout (upper right) was built with the FRESH hold (0)
- // earlier in create() — push the restored value through it. No-op
+ // The corner readouts (upper right) were built with the FRESH hold (0)
+ // earlier in create() — push the restored values through them. No-op
// when the save held nothing (set() skips unchanged values — no
// count-up, no flash), so mineral-less saves stay quiet.
this.refreshMineralHud();
+ this.refreshCreditsHud(); // same seam — the ledger line was set to the fresh 0 at build
if (Array.isArray(r.tethers) && r.tethers.length > 0) {
for (const t of [...this.tetherField.tethers]) this.tetherField.remove(t.id);
for (const t of r.tethers) {
diff --git a/js/scenes/SurfaceScene.js b/js/scenes/SurfaceScene.js
index c4ef241..a6f595c 100644
--- a/js/scenes/SurfaceScene.js
+++ b/js/scenes/SurfaceScene.js
@@ -481,9 +481,11 @@ export class SurfaceScene extends Phaser.Scene {
* → ShopView): the small "SET DESTINATION" popup — the same confirm the
* map's SET DESTINATION uses (js/ui/ConfirmOverlay.js), centered over the
* shop screen. Confirming records the destination on the sleeping
- * GameScene (its stable seam — systemId + null object; the player flies
- * the route after take-off) and toasts it here on the surface (the game
- * scene's own toast would sit frozen on a sleeping scene).
+ * GameScene (its stable seam — the exit's system + its WORLD's object:
+ * the planet of the exit's class, the station, or the home world —
+ * null → the system-level fallback; the route's final stop is that
+ * object) and toasts it here on the surface (the game scene's own toast
+ * would sit frozen on a sleeping scene).
*/
askSetDestination(exit) {
if (!exit || typeof exit.systemId !== 'string') return;
@@ -499,6 +501,7 @@ export class SurfaceScene extends Phaser.Scene {
return;
}
const name = String(exit.systemName ?? exit.systemId).toUpperCase();
+ const obj = exit.objectName ? String(exit.objectName).toUpperCase() : null;
const cls = exit.clsLabel ? `${exit.clsLabel} · ` : '';
const sign = exit.profit >= 0 ? '+' : '−';
const plan = planRoute(gs.galaxy, gs.systemRecord?.id, exit.systemId);
@@ -506,7 +509,7 @@ export class SurfaceScene extends Phaser.Scene {
this._ensureSetDestDialog().show({
title: 'SET DESTINATION',
body: [
- `DESTINATION — ${name}`,
+ `DESTINATION — ${obj ? `${obj} · ${name}` : name}`,
`${cls}SELLS ${exit.price} CR/UNIT · ${sign}${Math.abs(exit.profit)}/UNIT`,
],
accent: toCss(themeColor('neon', 0x00e5ff)),
@@ -515,8 +518,9 @@ export class SurfaceScene extends Phaser.Scene {
const d = this._setDest;
this._setDest = null;
if (!d) return;
- gs.setDestination(d.exit.systemId, null); // the map's seam (objectId → null)
- const label = `DESTINATION SET — ${String(d.exit.systemName ?? d.exit.systemId).toUpperCase()}` + (d.hops == null ? '' : ` · ${d.hops} HOP(S)`);
+ gs.setDestination(d.exit.systemId, d.exit.objectId ?? null); // the map's seam (the exit's world, or null → system level)
+ const dest = String(d.exit.objectName ?? d.exit.systemName ?? d.exit.systemId).toUpperCase();
+ const label = `DESTINATION SET — ${dest}` + (d.hops == null ? '' : ` · ${d.hops} HOP(S)`);
this.consoleNote(label, { glyph: '◆', glyphColor: toCss(config.get('gates.route.compassColor', '#ff8c1a')) });
},
time: this.time.now,
diff --git a/js/ui/shop/ScoutPanel.js b/js/ui/shop/ScoutPanel.js
index e2c5681..c9a24b7 100644
--- a/js/ui/shop/ScoutPanel.js
+++ b/js/ui/shop/ScoutPanel.js
@@ -10,13 +10,11 @@
* │ The lifeblood… │
* │ ────────────────────────── │
* │ GROWN ON Gas Giant │
- * │ SIGHT ON Rocky · Ice … │
+ * │ SOUGHT ON Rocky · Ice … │
* │ ────────────────────────── │
- * │ BEST EXITS — CHARTED │
- * │ ① New Concord Gas 41 cr │ ← TAP A ROW → "SET DESTINATION" popup
+ * │ BEST EXITS │
+ * │ ① New Concord 41 cr │ ← TAP A ROW → "SET DESTINATION" popup
* │ ② … │ (the scene's ConfirmOverlay)
- * │ ────────────────────────── │
- * │ TAP ANYWHERE TO CLOSE │
* └────────────────────────────┘
*
* The DATA (which planets want the good, the top-N charted exits + profit)
@@ -218,14 +216,7 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
// --- BEST EXITS — the charted systems ranked by sell price ------------
this._divider(pad, y, W - pad * 2);
y += 20;
- label(y, `Best exits — charted (${d.exits.length ? 'top ' + d.exits.length : 'none yet'})`);
- // the tap affordance, on the section label's own line (right side) —
- // the rows are the actionable part of the report
- const hint = String(s.exitHint ?? '').toUpperCase();
- if (hint && d.exits.length > 0) {
- const ht = add(text(W - pad, y + 3, hint, { fontSize: '9px', color: sec.color ?? '#3d4c74', align: 'right' }));
- ht?.setOrigin?.(1, 0);
- }
+ label(y, 'Best Exits');
y += 24;
const er = s.exitRow ?? {};
@@ -243,10 +234,12 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
// rank medal
const rank = add(text(pad + 4, y + 8, String(i + 1), { fontSize: '15px', color: ranks[i] ?? '#c8d4e8', fontFamily: fontStack('header') }));
rank?.setOrigin?.(0, 0);
- // system name + its class
+ // system name + the WORLD it pays at (its name + hops — the class
+ // label is dropped: the world name is what the destination points at)
const nm2 = add(text(nameX, y + 4, e.systemName, { fontSize: `${er.fontSize ?? 13}px`, color: er.color ?? '#eaf6ff' }));
nm2?.setOrigin?.(0, 0);
- const cl = add(text(nameX, y + 20, `${e.clsLabel}${e.hops != null ? ` · ${e.hops} HOPS` : ''}`, { fontSize: '10px', color: '#7d92c4' }));
+ const parts = [e.objectName, e.hops != null ? `${e.hops} HOPS` : null].filter(Boolean);
+ const cl = add(text(nameX, y + 20, parts.join(' · '), { fontSize: '10px', color: '#7d92c4' }));
cl?.setOrigin?.(0, 0);
// price + profit (right block)
const pe = s.exitPrice ?? {};
@@ -269,12 +262,14 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
add(bar);
}
}
- // the row's TAP SURFACE — "SET DESTINATION" for this system. Built
- // during show() (AFTER the panel's tap layer was made in the
- // constructor), so it wins the pointer over "tap anywhere = close";
- // stopPropagation keeps that layer from swallowing the tap. The
- // handler is a plain function on the row record (dev/tests can fire
- // it without a real pointer — ScoutPanel._exitRows[i].onTap()).
+ // the row's TAP SURFACE — "SET DESTINATION" for this exit's WORLD
+ // (its planet — or station / the home world — the route's final
+ // stop). Built during show() (AFTER the panel's tap layer was made
+ // in the constructor), so it wins the pointer over "tap anywhere
+ // = close"; stopPropagation keeps that layer from swallowing the
+ // tap. The handler is a plain function on the row record
+ // (dev/tests can fire it without a real pointer —
+ // ScoutPanel._exitRows[i].onTap()).
const hit = this.scene.add?.graphics?.();
if (hit) {
const hx = pad - 6, hy = y - 4, hw = W - pad * 2 + 12, hh = rowH;
@@ -296,11 +291,9 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
});
}
- // --- footer hint -------------------------------------------------------
- const foot = add(text(W / 2, this.height - 14, s.footer ?? 'TAP ANYWHERE TO RETURN TO CARGO', { fontSize: '10px', color: '#3d4c74', align: 'center' }));
- foot?.setOrigin?.(0.5, 1);
- foot?.setWordWrap?.(true);
- foot?.setWordWrapWidth?.(W - pad * 2);
+ // (no footer hint — the shell's top-right "TAP TO CLOSE" already says
+ // it, and the line collided with the last exit row in a short panel;
+ // less text = less overlap in this box)
return this;
}