Name scout exits by their world, not just the system
- TradeScout now resolves each exit's specific object (planet of that class, deep-space station, or home world) and exposes objectId/objectName so the destination points at the route's final stop rather than a bare system. - SurfaceScene dialog and toast show "WORLD · SYSTEM" and pass the object id to GameScene.setDestination; null falls back to system level. - ScoutPanel exit rows display the world name + hops instead of the class label, drop the per-section tap hint and footer (they collided with the last row in a short panel), and rename "SIGHT ON" to "SOUGHT ON". - Save/restore now carries ship.credits and ship.cargo across jumps; GameScene restores both and refreshes the credits HUD. Saves tests cover capture, prepare, and legacy records without these fields. - trade-scout tests assert every exit has objectId/objectName and that resolved ids are real objects of the exit class; scout-dest test verifies the confirmed destination names a real world. - Add dev/scout-overlap probe (html + mjs) to screenshot the scout panel and numerically report any overlapping text boxes. - Ship lava-planet-03 art and lava-land-03 video assets.
This commit is contained in:
parent
7549c91df1
commit
b6523808fa
Binary file not shown.
|
After Width: | Height: | Size: 2.6 MiB |
Binary file not shown.
|
|
@ -141,9 +141,8 @@
|
||||||
"ease": "Cubic.Out"
|
"ease": "Cubic.Out"
|
||||||
},
|
},
|
||||||
"scout": {
|
"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,
|
"topN": 3,
|
||||||
"exitHint": "TAP A SYSTEM TO SET DESTINATION",
|
|
||||||
"setDestination": { "width": 520, "height": 176 },
|
"setDestination": { "width": 520, "height": 176 },
|
||||||
"revealMs": 140,
|
"revealMs": 140,
|
||||||
"pad": 18,
|
"pad": 18,
|
||||||
|
|
@ -197,7 +196,6 @@
|
||||||
"#c8d4e8",
|
"#c8d4e8",
|
||||||
"#d09058"
|
"#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.",
|
"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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ const {
|
||||||
} = await import(file('save/SaveData.js'));
|
} = await import(file('save/SaveData.js'));
|
||||||
const { Galaxy } = await import(file('galaxy/Galaxy.js'));
|
const { Galaxy } = await import(file('galaxy/Galaxy.js'));
|
||||||
const { Discovery } = await import(file('galaxy/Discovery.js'));
|
const { Discovery } = await import(file('galaxy/Discovery.js'));
|
||||||
|
const { CargoHold } = await import(file('economy/CargoHold.js'));
|
||||||
|
|
||||||
let failures = 0;
|
let failures = 0;
|
||||||
const check = (label, cond) => {
|
const check = (label, cond) => {
|
||||||
|
|
@ -53,11 +54,14 @@ const SEED = 'SAVETEST';
|
||||||
const galaxy = Galaxy.create(SEED, { systemCount: 8 });
|
const galaxy = Galaxy.create(SEED, { systemCount: 8 });
|
||||||
const system = galaxy.byId.get(galaxy.currentSystemId); // the starting system
|
const system = galaxy.byId.get(galaxy.currentSystemId); // the starting system
|
||||||
|
|
||||||
const fakeScene = () => ({
|
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); } },
|
registry: { map: new Map(), set(k, v) { this.map.set(k, v); }, get(k) { return this.map.get(k); } },
|
||||||
galaxy,
|
galaxy,
|
||||||
systemRecord: { name: system.name },
|
systemRecord: { name: system.name },
|
||||||
ship: { x: 123.5, y: -77, rotation: 0.72, minerals: 37 },
|
ship: { x: 123.5, y: -77, rotation: 0.72, minerals: 37, credits: 1250, cargo },
|
||||||
discovery: new Discovery(540),
|
discovery: new Discovery(540),
|
||||||
depletedClusters: new Set([`${system.id}:c-depleted`]), // a mined-out field
|
depletedClusters: new Set([`${system.id}:c-depleted`]), // a mined-out field
|
||||||
tetherField: {
|
tetherField: {
|
||||||
|
|
@ -67,7 +71,8 @@ const fakeScene = () => ({
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
playTimeMs: 123456,
|
playTimeMs: 123456,
|
||||||
});
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const makeRec = (over = {}) => ({
|
const makeRec = (over = {}) => ({
|
||||||
app: 'orbit',
|
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: 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: 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 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: 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: tethers carried (both)', rec.tethers.length === 2 && rec.tethers[1].label === 'Outpost');
|
||||||
check('capture: playtime carried', rec.playTimeMs === 123456);
|
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',
|
check('prepare: the live state is staged under the pending key',
|
||||||
staged !== null && staged.ship.x === 123.5 && staged.playTimeMs === 123456);
|
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 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)',
|
check('prepare: the comms ledger is restored (a Set, same ids)',
|
||||||
reg.get('commsPlayed') instanceof Set
|
reg.get('commsPlayed') instanceof Set
|
||||||
&& reg.get('commsPlayed').has('oac-intro')
|
&& reg.get('commsPlayed').has('oac-intro')
|
||||||
|
|
@ -246,6 +257,9 @@ const makeStorage = (fail = false) => {
|
||||||
prepareLoad(regLegacy, makeRec()); // makeRec's ship has no minerals
|
prepareLoad(regLegacy, makeRec()); // makeRec's ship has no minerals
|
||||||
check('prepare: a legacy record (no minerals) still loads, field absent',
|
check('prepare: a legacy record (no minerals) still loads, field absent',
|
||||||
regLegacy.get(PENDING_RESTORE_KEY).ship.minerals === undefined);
|
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',
|
check('prepare: a legacy record (no depletedClusters) stages an empty set',
|
||||||
regLegacy.get('depletedClusters') instanceof Set && regLegacy.get('depletedClusters').size === 0);
|
regLegacy.get('depletedClusters') instanceof Set && regLegacy.get('depletedClusters').size === 0);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,10 @@
|
||||||
* - a tapped exit row offers the exit to the SCENE (the panel does not
|
* - a tapped exit row offers the exit to the SCENE (the panel does not
|
||||||
* close itself — propagation stops)
|
* close itself — propagation stops)
|
||||||
* - confirming SETS the destination on the (sleeping) GameScene:
|
* - confirming SETS the destination on the (sleeping) GameScene:
|
||||||
* destination.systemId = the exit, objectId null — and toasts it on
|
* destination.systemId = the exit, destination.objectId = the exit's
|
||||||
* the surface (the game scene's toast would sit frozen while sleeping)
|
* 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
|
* - the LOCAL system exit is rejected with a note (no dialog, no
|
||||||
* destination change — pointing the ship at itself bounces)
|
* destination change — pointing the ship at itself bounces)
|
||||||
* - CANCEL and ESC leave the destination untouched
|
* - 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',
|
step('dialog-title', (await waitFor(() => dlg.title.text === 'SET DESTINATION')) && dlg.title.text === 'SET DESTINATION',
|
||||||
{ title: dlg.title.text });
|
{ title: dlg.title.text });
|
||||||
const name = String(remoteExit.systemName ?? remoteExit.systemId).toUpperCase();
|
const name = String(remoteExit.systemName ?? remoteExit.systemId).toUpperCase();
|
||||||
step('dialog-body', dlg.bodyTexts[0].text === `DESTINATION — ${name}`,
|
const obj = remoteExit.objectName ? String(remoteExit.objectName).toUpperCase() : null;
|
||||||
{ body0: dlg.bodyTexts[0].text, want: `DESTINATION — ${name}` });
|
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 });
|
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.
|
// The scene staged the pending pick (exit + route hops) before showing.
|
||||||
|
|
@ -171,8 +174,17 @@ window.game = game;
|
||||||
dlg.fireConfirm();
|
dlg.fireConfirm();
|
||||||
if (!(await dialogHidden())) throw new Error('dialog never closed after confirm');
|
if (!(await dialogHidden())) throw new Error('dialog never closed after confirm');
|
||||||
step('confirmed-closed', dlg.state === 'hidden');
|
step('confirmed-closed', dlg.state === 'hidden');
|
||||||
step('destination-set', gs.destination?.systemId === remoteExit.systemId && gs.destination?.objectId === null,
|
step('destination-set', gs.destination?.systemId === remoteExit.systemId
|
||||||
{ destination: gs.destination });
|
&& 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 });
|
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.
|
// 8 — CANCEL: the same row again → popup; CANCEL leaves everything as-is.
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<title>Orbit — dev scout overlap probe</title>
|
||||||
|
<!-- This page lives in /dev, but the game's relative asset paths are rooted
|
||||||
|
at the project root — resolve them against it. ("../" keeps this
|
||||||
|
working even if the project is served from a subdirectory.) -->
|
||||||
|
<base href="../" />
|
||||||
|
<style>
|
||||||
|
/* Same typefaces as index.html (the probe page doesn't include it), so
|
||||||
|
screenshots show the real Ethnocentric/Centauri canvas text. */
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Ethnocentric';
|
||||||
|
src: url('assets/fonts/Ethnocentric-Regular.otf') format('opentype');
|
||||||
|
font-weight: normal; font-style: normal; font-display: swap;
|
||||||
|
}
|
||||||
|
@font-face {
|
||||||
|
font-family: 'Centauri';
|
||||||
|
src: url('assets/fonts/FontsFree-Net-Centauri.ttf') format('truetype');
|
||||||
|
font-weight: normal; font-style: normal; font-display: swap;
|
||||||
|
}
|
||||||
|
html, body { margin: 0; height: 100%; background: #04060d; overflow: hidden; }
|
||||||
|
#game { width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; }
|
||||||
|
</style>
|
||||||
|
<script src="lib/phaser.min.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="game"></div>
|
||||||
|
<script type="module" src="dev/scout-overlap.mjs"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
|
@ -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) }; });
|
||||||
|
|
@ -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')));
|
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
|
// 4. Determinism — same inputs ⇒ identical report
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -48,6 +48,41 @@ export function sellsHere(goodId, cls) {
|
||||||
return cls === 'capital' || E.wants(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.
|
* 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,
|
* id:string, name:string, flavor:string, category:string|null, tier:number|null,
|
||||||
* localBuy:number, localSell:number|null,
|
* localBuy:number, localSell:number|null,
|
||||||
* producer:Array<{cls,label,economy}>, demand:Array<{cls,label,economy}>,
|
* 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
|
* bestExit:object|null, maxProfit:number, charted:number, topN:number
|
||||||
* } | null} — null only for an unknown good id.
|
* } | 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
|
if (systemId) visited.add(systemId); // we are here — it's charted by definition
|
||||||
|
|
||||||
const classBySystem = galaxy?.economy?.classBySystem ?? null;
|
const classBySystem = galaxy?.economy?.classBySystem ?? null;
|
||||||
|
const homeSystemId = galaxy?.homeSystemId ?? null;
|
||||||
|
const homeCls = config.get('planets.homePlanet', 'terran');
|
||||||
const exits = [];
|
const exits = [];
|
||||||
for (const sysId of visited) {
|
for (const sysId of visited) {
|
||||||
const rec = galaxy?.byId?.get?.(sysId);
|
const rec = galaxy?.byId?.get?.(sysId);
|
||||||
|
|
@ -106,11 +143,18 @@ export function scoutGood(goodId, o = {}) {
|
||||||
}
|
}
|
||||||
if (!best) continue;
|
if (!best) continue;
|
||||||
const hops = galaxy?.economy?.producerHops?.[goodId]?.[sysId];
|
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({
|
exits.push({
|
||||||
...best,
|
...best,
|
||||||
clsLabel: classInfo(best.cls).label,
|
clsLabel: classInfo(best.cls).label,
|
||||||
profit: best.price - localBuy,
|
profit: best.price - localBuy,
|
||||||
hops: Number.isFinite(hops) ? hops : null,
|
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)));
|
exits.sort((a, b) => (b.price - a.price) || String(a.systemName).localeCompare(String(b.systemName)));
|
||||||
|
|
|
||||||
|
|
@ -2593,6 +2593,11 @@ export class GameScene extends Phaser.Scene {
|
||||||
y: arrival.y,
|
y: arrival.y,
|
||||||
heading: arrival.heading,
|
heading: arrival.heading,
|
||||||
minerals: rec.ship?.minerals ?? 0,
|
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()
|
rec.tethers = []; // the destination's native set re-forms in create()
|
||||||
prepareLoad(this.registry, rec);
|
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).
|
// Capacity stays the ship's stat (4, or an upgraded hold).
|
||||||
if (this.ship.cargo) this.ship.cargo.set(r.ship.cargo ?? []);
|
if (this.ship.cargo) this.ship.cargo.set(r.ship.cargo ?? []);
|
||||||
}
|
}
|
||||||
// The corner readout (upper right) was built with the FRESH hold (0)
|
// The corner readouts (upper right) were built with the FRESH hold (0)
|
||||||
// earlier in create() — push the restored value through it. No-op
|
// earlier in create() — push the restored values through them. No-op
|
||||||
// when the save held nothing (set() skips unchanged values — no
|
// when the save held nothing (set() skips unchanged values — no
|
||||||
// count-up, no flash), so mineral-less saves stay quiet.
|
// count-up, no flash), so mineral-less saves stay quiet.
|
||||||
this.refreshMineralHud();
|
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) {
|
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 [...this.tetherField.tethers]) this.tetherField.remove(t.id);
|
||||||
for (const t of r.tethers) {
|
for (const t of r.tethers) {
|
||||||
|
|
|
||||||
|
|
@ -481,9 +481,11 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
* → ShopView): the small "SET DESTINATION" popup — the same confirm the
|
* → ShopView): the small "SET DESTINATION" popup — the same confirm the
|
||||||
* map's SET DESTINATION uses (js/ui/ConfirmOverlay.js), centered over the
|
* map's SET DESTINATION uses (js/ui/ConfirmOverlay.js), centered over the
|
||||||
* shop screen. Confirming records the destination on the sleeping
|
* shop screen. Confirming records the destination on the sleeping
|
||||||
* GameScene (its stable seam — systemId + null object; the player flies
|
* GameScene (its stable seam — the exit's system + its WORLD's object:
|
||||||
* the route after take-off) and toasts it here on the surface (the game
|
* the planet of the exit's class, the station, or the home world —
|
||||||
* scene's own toast would sit frozen on a sleeping scene).
|
* 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) {
|
askSetDestination(exit) {
|
||||||
if (!exit || typeof exit.systemId !== 'string') return;
|
if (!exit || typeof exit.systemId !== 'string') return;
|
||||||
|
|
@ -499,6 +501,7 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const name = String(exit.systemName ?? exit.systemId).toUpperCase();
|
const name = String(exit.systemName ?? exit.systemId).toUpperCase();
|
||||||
|
const obj = exit.objectName ? String(exit.objectName).toUpperCase() : null;
|
||||||
const cls = exit.clsLabel ? `${exit.clsLabel} · ` : '';
|
const cls = exit.clsLabel ? `${exit.clsLabel} · ` : '';
|
||||||
const sign = exit.profit >= 0 ? '+' : '−';
|
const sign = exit.profit >= 0 ? '+' : '−';
|
||||||
const plan = planRoute(gs.galaxy, gs.systemRecord?.id, exit.systemId);
|
const plan = planRoute(gs.galaxy, gs.systemRecord?.id, exit.systemId);
|
||||||
|
|
@ -506,7 +509,7 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
this._ensureSetDestDialog().show({
|
this._ensureSetDestDialog().show({
|
||||||
title: 'SET DESTINATION',
|
title: 'SET DESTINATION',
|
||||||
body: [
|
body: [
|
||||||
`DESTINATION — ${name}`,
|
`DESTINATION — ${obj ? `${obj} · ${name}` : name}`,
|
||||||
`${cls}SELLS ${exit.price} CR/UNIT · ${sign}${Math.abs(exit.profit)}/UNIT`,
|
`${cls}SELLS ${exit.price} CR/UNIT · ${sign}${Math.abs(exit.profit)}/UNIT`,
|
||||||
],
|
],
|
||||||
accent: toCss(themeColor('neon', 0x00e5ff)),
|
accent: toCss(themeColor('neon', 0x00e5ff)),
|
||||||
|
|
@ -515,8 +518,9 @@ export class SurfaceScene extends Phaser.Scene {
|
||||||
const d = this._setDest;
|
const d = this._setDest;
|
||||||
this._setDest = null;
|
this._setDest = null;
|
||||||
if (!d) return;
|
if (!d) return;
|
||||||
gs.setDestination(d.exit.systemId, null); // the map's seam (objectId → null)
|
gs.setDestination(d.exit.systemId, d.exit.objectId ?? null); // the map's seam (the exit's world, or null → system level)
|
||||||
const label = `DESTINATION SET — ${String(d.exit.systemName ?? d.exit.systemId).toUpperCase()}` + (d.hops == null ? '' : ` · ${d.hops} HOP(S)`);
|
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')) });
|
this.consoleNote(label, { glyph: '◆', glyphColor: toCss(config.get('gates.route.compassColor', '#ff8c1a')) });
|
||||||
},
|
},
|
||||||
time: this.time.now,
|
time: this.time.now,
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,11 @@
|
||||||
* │ The lifeblood… │
|
* │ The lifeblood… │
|
||||||
* │ ────────────────────────── │
|
* │ ────────────────────────── │
|
||||||
* │ GROWN ON Gas Giant │
|
* │ GROWN ON Gas Giant │
|
||||||
* │ SIGHT ON Rocky · Ice … │
|
* │ SOUGHT ON Rocky · Ice … │
|
||||||
* │ ────────────────────────── │
|
* │ ────────────────────────── │
|
||||||
* │ BEST EXITS — CHARTED │
|
* │ BEST EXITS │
|
||||||
* │ ① New Concord Gas 41 cr │ ← TAP A ROW → "SET DESTINATION" popup
|
* │ ① New Concord 41 cr │ ← TAP A ROW → "SET DESTINATION" popup
|
||||||
* │ ② … │ (the scene's ConfirmOverlay)
|
* │ ② … │ (the scene's ConfirmOverlay)
|
||||||
* │ ────────────────────────── │
|
|
||||||
* │ TAP ANYWHERE TO CLOSE │
|
|
||||||
* └────────────────────────────┘
|
* └────────────────────────────┘
|
||||||
*
|
*
|
||||||
* The DATA (which planets want the good, the top-N charted exits + profit)
|
* 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 ------------
|
// --- BEST EXITS — the charted systems ranked by sell price ------------
|
||||||
this._divider(pad, y, W - pad * 2);
|
this._divider(pad, y, W - pad * 2);
|
||||||
y += 20;
|
y += 20;
|
||||||
label(y, `Best exits — charted (${d.exits.length ? 'top ' + d.exits.length : 'none yet'})`);
|
label(y, 'Best Exits');
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
y += 24;
|
y += 24;
|
||||||
|
|
||||||
const er = s.exitRow ?? {};
|
const er = s.exitRow ?? {};
|
||||||
|
|
@ -243,10 +234,12 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
|
||||||
// rank medal
|
// rank medal
|
||||||
const rank = add(text(pad + 4, y + 8, String(i + 1), { fontSize: '15px', color: ranks[i] ?? '#c8d4e8', fontFamily: fontStack('header') }));
|
const rank = add(text(pad + 4, y + 8, String(i + 1), { fontSize: '15px', color: ranks[i] ?? '#c8d4e8', fontFamily: fontStack('header') }));
|
||||||
rank?.setOrigin?.(0, 0);
|
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' }));
|
const nm2 = add(text(nameX, y + 4, e.systemName, { fontSize: `${er.fontSize ?? 13}px`, color: er.color ?? '#eaf6ff' }));
|
||||||
nm2?.setOrigin?.(0, 0);
|
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);
|
cl?.setOrigin?.(0, 0);
|
||||||
// price + profit (right block)
|
// price + profit (right block)
|
||||||
const pe = s.exitPrice ?? {};
|
const pe = s.exitPrice ?? {};
|
||||||
|
|
@ -269,12 +262,14 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
|
||||||
add(bar);
|
add(bar);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// the row's TAP SURFACE — "SET DESTINATION" for this system. Built
|
// the row's TAP SURFACE — "SET DESTINATION" for this exit's WORLD
|
||||||
// during show() (AFTER the panel's tap layer was made in the
|
// (its planet — or station / the home world — the route's final
|
||||||
// constructor), so it wins the pointer over "tap anywhere = close";
|
// stop). Built during show() (AFTER the panel's tap layer was made
|
||||||
// stopPropagation keeps that layer from swallowing the tap. The
|
// in the constructor), so it wins the pointer over "tap anywhere
|
||||||
// handler is a plain function on the row record (dev/tests can fire
|
// = close"; stopPropagation keeps that layer from swallowing the
|
||||||
// it without a real pointer — ScoutPanel._exitRows[i].onTap()).
|
// 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?.();
|
const hit = this.scene.add?.graphics?.();
|
||||||
if (hit) {
|
if (hit) {
|
||||||
const hx = pad - 6, hy = y - 4, hw = W - pad * 2 + 12, hh = rowH;
|
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 -------------------------------------------------------
|
// (no footer hint — the shell's top-right "TAP TO CLOSE" already says
|
||||||
const foot = add(text(W / 2, this.height - 14, s.footer ?? 'TAP ANYWHERE TO RETURN TO CARGO', { fontSize: '10px', color: '#3d4c74', align: 'center' }));
|
// it, and the line collided with the last exit row in a short panel;
|
||||||
foot?.setOrigin?.(0.5, 1);
|
// less text = less overlap in this box)
|
||||||
foot?.setWordWrap?.(true);
|
|
||||||
foot?.setWordWrapWidth?.(W - pad * 2);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue