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; }