Add set-destination from trade scout, shop HUD, and green sell selection
- Trade scout best-exit rows now open a "SET DESTINATION" confirm dialog (SurfaceScene.askSetDestination + ConfirmOverlay), confirming sets the GameScene destination; local-system exits are rejected with a note. - Surface scene builds a corner credits/minerals readout over the shop clip, refreshed after trades and torn down on leave. - Sell-bay selection restyled: green stroke + tinted fill + upper-left checkmark badge (data/shopui.json slot config), replacing the cyan ring. - Add lava/capital-neutral music, videos, and landing data entries; remove tresle.png in favor of capital-neutral-surface.png. - New dev probes: scout-dest, shop-hud, shop-select-shot, map-dest-shot, cdp-cast; extend shop-ui.test.mjs for selection visuals and exit taps.
This commit is contained in:
parent
141cbdb156
commit
7549c91df1
Binary file not shown.
|
After Width: | Height: | Size: 2.8 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -67,13 +67,13 @@
|
|||
{ "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" },
|
||||
{ "land": "ice-land-02.mp4", "surface": "ice-surface-02.mp4", "takeoff": "ice-takeoff-02.mp4", "shop": "ice-shop-02.mp4" },
|
||||
{ "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" },
|
||||
{ "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" },
|
||||
{ "land": "lava-land-01.mp4", "surface": "lava-surface-01.mp4", "takeoff": "lava-takeoff-01.mp4", "shop": "ice-shop-01.mp4" },
|
||||
{ "land": "lava-land-02.mp4", "surface": "lava-surface-02.mp4", "takeoff": "lava-takeoff-02.mp4", "shop": "ice-shop-02.mp4" },
|
||||
{ "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" },
|
||||
{ "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" },
|
||||
{ "land": "lava-land-02.mp4", "surface": "lava-surface-02.mp4", "takeoff": "lava-takeoff-02.mp4", "shop": "ice-shop-02.mp4" },
|
||||
{ "land": "ice-land-01.mp4", "surface": "lava-surface-01.mp4", "takeoff": "lava-takeoff-01.mp4", "shop": "ice-shop-01.mp4" },
|
||||
{ "land": "ice-land-02.mp4", "surface": "lava-surface-02.mp4", "takeoff": "lava-takeoff-02.mp4", "shop": "ice-shop-02.mp4" },
|
||||
{ "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" },
|
||||
{ "land": "capital-land-neutral.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" },
|
||||
{ "land": "capital-land-neutral.mp4", "surface": "capital-surface-neutral.mp4", "takeoff": "capital-takeoff-neutral.mp4", "shop": "ice-shop-01.mp4" },
|
||||
{ "land": "lava-land-02.mp4", "surface": "lava-surface-02.mp4", "takeoff": "lava-takeoff-02.mp4", "shop": "ice-shop-02.mp4" },
|
||||
{ "land": "ice-land-03.mp4", "surface": "ice-surface-03.mp4", "takeoff": "ice-takeoff-03.mp4", "shop": "ice-shop-03.mp4" },
|
||||
{ "land": "ice-land-01.mp4", "surface": "ice-surface-01.mp4", "takeoff": "ice-takeoff-01.mp4", "shop": "ice-shop-01.mp4" }
|
||||
|
|
|
|||
|
|
@ -23,7 +23,14 @@
|
|||
"8": "assets/music/rocky-03.mp3",
|
||||
"9": "assets/music/ice-01.mp3",
|
||||
"10": "assets/music/ice-02.mp3",
|
||||
"11": "assets/music/ice-03.mp3"
|
||||
"11": "assets/music/ice-03.mp3",
|
||||
"12": "assets/music/lava-01.mp3",
|
||||
"13": "assets/music/lava-02.mp3",
|
||||
"14": "assets/music/lava-03.mp3",
|
||||
"15": "assets/music/lava-01.mp3",
|
||||
"16": "assets/music/lava-02.mp3",
|
||||
"17": "assets/music/lava-03.mp3",
|
||||
"18": "assets/music/capital-neutral-01.mp3"
|
||||
},
|
||||
"_stationFrames_comment": "stationFrames = the loop that plays on a deep-space station's surface — keyed by the spacestations.png VARIANT frame (the same key its landing/takeoff clips use, data/landing.json → stationVideos), so a station hums the track matching the art it is drawn with. Empty for now (a station surface is silent) until the station tracks are authored — add one line per variant when they land.",
|
||||
"stationFrames": {}
|
||||
|
|
|
|||
|
|
@ -29,10 +29,19 @@
|
|||
"centerW": 16
|
||||
},
|
||||
"slot": {
|
||||
"_comment": "A single SELL bay cell. `bg` is the cell plate, `selected` the SELL-check highlight (a bay marked to sell), `lockAlpha` dims the locked/empty cells. `iconScale` sizes the good icon, which fills the cell (the name + price live in the hover tooltip, not over the icon). `name`/`price` colors feed the tooltip's name/price lines. Colors follow the theme (cyan selection, green sell, magenta = bad).",
|
||||
"_comment": "A single SELL bay cell. `bg` is the cell plate, `selected` the SELL-check highlight (a bay marked to sell), `selBgColor`/`selBgAlpha` the slightly lifted green-tinted fill a checked bay gets, and `check` the green CHECKMARK badge in the checked bay's upper-left corner (a second tap reverts all of it). `lockAlpha` dims the locked/empty cells. `iconScale` sizes the good icon, which fills the cell (the name + price live in the hover tooltip, not over the icon). `name`/`price` colors feed the tooltip's name/price lines. Colors follow the theme (green = queued/sell, cyan = info, magenta = bad).",
|
||||
"bgColor": "#0e1728",
|
||||
"border": "#1d2e4c",
|
||||
"selected": "#00e5ff",
|
||||
"selected": "#7ce8a4",
|
||||
"selBgColor": "#152a22",
|
||||
"selBgAlpha": 0.95,
|
||||
"check": {
|
||||
"size": 11,
|
||||
"inset": 7,
|
||||
"color": "#7ce8a4",
|
||||
"badgeBg": "#0a1410",
|
||||
"badgeAlpha": 0.85
|
||||
},
|
||||
"lockAlpha": 0.3,
|
||||
"iconScale": 0.62,
|
||||
"name": {
|
||||
|
|
@ -132,8 +141,10 @@
|
|||
"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). 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 (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.",
|
||||
"topN": 3,
|
||||
"exitHint": "TAP A SYSTEM TO SET DESTINATION",
|
||||
"setDestination": { "width": 520, "height": 176 },
|
||||
"revealMs": 140,
|
||||
"pad": 18,
|
||||
"title": {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
// Dev-only: CDP screencast while the scout-dest dialog is open — checks
|
||||
// whether ANY live frame paints the dialog plate (vs. the stale-capture
|
||||
// hypothesis). Usage:
|
||||
// node dev/cdp-cast.mjs <url> <dir> '<readyExpr>' [ms]
|
||||
import { mkdir, writeFile } from 'node:fs/promises';
|
||||
|
||||
const [url, dir, readyExpr, ms] = process.argv.slice(2);
|
||||
const CDP = process.env.CDP_PORT ? `127.0.0.1:${CDP_PORT}` : '127.0.0.1:9333';
|
||||
const dur = Number(ms ?? 4000);
|
||||
|
||||
let target = await (await fetch(`http://${CDP}/json/list`)).json()
|
||||
.then((ts) => ts.find((t) => t.type === 'page'));
|
||||
if (!target) {
|
||||
target = await (await fetch(`http://${CDP}/json/new?${encodeURIComponent(url)}`, { method: 'PUT' })).json();
|
||||
}
|
||||
const ws = new WebSocket(target.webSocketDebuggerUrl);
|
||||
let id = 0;
|
||||
const pending = new Map();
|
||||
const send = (method, params = {}) => new Promise((res, rej) => {
|
||||
const mid = ++id;
|
||||
pending.set(mid, { res, rej });
|
||||
ws.send(JSON.stringify({ id: mid, method, params }));
|
||||
});
|
||||
const frames = [];
|
||||
await new Promise((res, rej) => { ws.onopen = res; ws.onerror = (e) => rej(new Error('ws')); });
|
||||
ws.onmessage = (m) => {
|
||||
const msg = JSON.parse(m.data);
|
||||
if (msg.id && pending.has(msg.id)) {
|
||||
const { res, rej } = pending.get(msg.id);
|
||||
pending.delete(msg.id);
|
||||
msg.error ? rej(new Error(msg.error.message)) : res(msg.result);
|
||||
} else if (msg.method === 'Page.screencastFrame') {
|
||||
frames.push(msg.params.data);
|
||||
send('Page.screencastFrameAck', { sessionId: msg.params.sessionId });
|
||||
}
|
||||
};
|
||||
|
||||
await send('Page.enable');
|
||||
await send('Runtime.enable');
|
||||
await send('Network.enable');
|
||||
await send('Network.setCacheDisabled', { cacheDisabled: true });
|
||||
await send('Page.navigate', { url: 'about:blank' });
|
||||
await new Promise((r) => setTimeout(r, 300));
|
||||
await send('Page.navigate', { url });
|
||||
|
||||
const evaluate = async (expr) => {
|
||||
const r = await send('Runtime.evaluate', { expression: expr, returnByValue: true, awaitPromise: true });
|
||||
if (r.exceptionDetails) throw new Error('page: ' + (r.exceptionDetails.exception?.description ?? r.exceptionDetails.text));
|
||||
return r.result?.value;
|
||||
};
|
||||
|
||||
const deadline = Date.now() + 120000;
|
||||
while (Date.now() < deadline) {
|
||||
const v = await evaluate(readyExpr).catch(() => null);
|
||||
if (v) break;
|
||||
await new Promise((r) => setTimeout(r, 200));
|
||||
}
|
||||
|
||||
await send('Page.startScreencast', { format: 'png', everyNthFrame: 1 });
|
||||
await new Promise((r) => setTimeout(r, dur));
|
||||
await send('Page.stopScreencast');
|
||||
|
||||
await mkdir(dir, { recursive: true });
|
||||
let i = 0;
|
||||
for (const data of frames) {
|
||||
await writeFile(`${dir}/frame-${String(i).padStart(3, '0')}.png`, Buffer.from(data, 'base64'));
|
||||
i++;
|
||||
}
|
||||
console.log(`captured ${i} frames in ${dir}`);
|
||||
ws.close();
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev map set-destination shot</title>
|
||||
<!-- This page lives in /dev, but the game's relative asset paths are rooted
|
||||
at the project root — resolve them against it. -->
|
||||
<base href="../" />
|
||||
<style>
|
||||
@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/map-dest-shot.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
import Phaser from '../js/vendor/phaser.js';
|
||||
import { config } from '../js/config/Config.js';
|
||||
import { ConfigLoader } from '../js/config/ConfigLoader.js';
|
||||
import { createGameConfig } from '../js/config/GameConfig.js';
|
||||
import { GameScene } from '../js/scenes/GameScene.js';
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const data = await ConfigLoader.load();
|
||||
config.init(data);
|
||||
const gameConfig = createGameConfig();
|
||||
gameConfig.scene = [GameScene];
|
||||
const game = new Phaser.Game(gameConfig);
|
||||
window.game = game;
|
||||
window.__STEP__ = 'booting';
|
||||
|
||||
(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.__SHOT_READY__ = { error: 'no GameScene' }; return; }
|
||||
window.__STEP__ = 'gs ok, mapWindow=' + !!gs.mapWindow;
|
||||
if (!gs.mapWindow) { window.__SHOT_READY__ = { error: 'no mapWindow' }; return; }
|
||||
gs.mapWindow.open();
|
||||
window.__STEP__ = 'opened';
|
||||
await sleep(600);
|
||||
try {
|
||||
if (typeof gs.mapWindow.setMode === 'function') gs.mapWindow.setMode('system');
|
||||
else gs.mapWindow._mode = 'system';
|
||||
window.__STEP__ = 'mode set';
|
||||
} catch (e) { window.__SHOT_READY__ = { error: 'setMode: ' + e.message }; return; }
|
||||
await sleep(1500);
|
||||
try {
|
||||
gs.mapWindow._askObject({ label: 'XANTHOS', id: 'xantos', x: 0, y: 0, r: 4 });
|
||||
window.__STEP__ = 'asked, dlg=' + gs.mapWindow.dialog.state;
|
||||
} catch (e) { window.__SHOT_READY__ = { error: 'ask: ' + e.message }; return; }
|
||||
const dlg = gs.mapWindow.dialog;
|
||||
for (let i = 0; i < 80 && dlg.state !== 'shown'; i++) await sleep(100);
|
||||
window.__SHOT_READY__ = { ok: dlg.state === 'shown', state: dlg.state };
|
||||
})().catch((err) => { window.__SHOT_READY__ = { error: String(err && err.message || err) }; });
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev scout set-destination shot</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-dest-shot.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,97 @@
|
|||
/**
|
||||
* Visual probe for a screenshot (headless brave, via dev/cdp-shot.mjs):
|
||||
* boots the game, lands, opens the shop, opens the trade scout on a BUY
|
||||
* row, and taps a REMOTE best-exit row — the "SET DESTINATION" popup sits
|
||||
* open over the shop screen (the shot's subject). The exit rows are
|
||||
* tappable (hand cursor) and the section label carries the hint.
|
||||
*
|
||||
* /opt/brave.com/brave/brave --headless --disable-gpu --remote-debugging-port=9333 about:blank &
|
||||
* python3 -m http.server 8128
|
||||
* node dev/cdp-shot.mjs http://127.0.0.1:8128/dev/scout-dest-shot.html \
|
||||
* /tmp/scout-dest.png 'window.__SHOT_READY__ && 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';
|
||||
|
||||
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.__SHOT_READY__ = { 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.__SHOT_READY__ = { 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.__SHOT_READY__ = { error: 'no shopView' }; return; }
|
||||
sv.setTab('trade');
|
||||
await sleep(900); // the slide-in
|
||||
|
||||
// Pick a BUY row with a REMOTE profitable exit, open its scout, and tap
|
||||
// that exit row — the SET DESTINATION popup opens and STAYS open (the
|
||||
// shot's subject).
|
||||
const visited = [...gs.visitedSystems];
|
||||
let row = null, exit = null, best = -Infinity;
|
||||
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 && rem.profit > best) { row = r; exit = rem; best = rem.profit; }
|
||||
}
|
||||
if (!row || !exit) { window.__SHOT_READY__ = { error: 'no remote exit to offer' }; return; }
|
||||
|
||||
sv._onRowTap(row);
|
||||
await sleep(300);
|
||||
const i = sv._scout._exitRows.findIndex((x) => x.exit.systemId === exit.systemId);
|
||||
if (i < 0) { window.__SHOT_READY__ = { error: 'no exit row for the picked exit' }; return; }
|
||||
sv._scout._exitRows[i].onTap({ stopPropagation() {} });
|
||||
|
||||
// Wait for the open animation to land (title decoded, buttons up).
|
||||
const dlg = ss._setDestDialog;
|
||||
if (!dlg) { window.__SHOT_READY__ = { error: 'no dialog' }; return; }
|
||||
const t0 = Date.now();
|
||||
while (dlg.state !== 'shown' && Date.now() - t0 < 5000) await sleep(50);
|
||||
await sleep(700); // the title decode + button flicker settle
|
||||
|
||||
window.__SHOT_READY__ = {
|
||||
state: dlg.state,
|
||||
title: dlg.title.text,
|
||||
body: dlg.bodyTexts.map((t) => t.text),
|
||||
exit: { systemId: exit.systemId, systemName: exit.systemName, price: exit.price, profit: exit.profit },
|
||||
hops: ss._setDest?.hops,
|
||||
};
|
||||
})().catch((err) => { window.__SHOT_READY__ = { error: String(err && err.message || err) }; });
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev scout set-destination test</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-dest.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,202 @@
|
|||
/**
|
||||
* Dev-only TRADE SCOUT → SET DESTINATION test: boots the game, lands on the
|
||||
* home world, opens the buy/sell screen, taps a BUY row's box (the trade
|
||||
* scout), then taps a BEST-EXIT row and drives the "SET DESTINATION" popup
|
||||
* end-to-end (ScoutPanel → ShopView → SurfaceScene.askSetDestination →
|
||||
* ConfirmOverlay → GameScene.setDestination):
|
||||
*
|
||||
* - 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)
|
||||
* - 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
|
||||
*
|
||||
* python3 -m http.server 8125
|
||||
* node dev/cdp-firefox.mjs http://127.0.0.1:8125/dev/scout-dest.html \
|
||||
* 'return window.__SCOUT_DEST_TEST__;' 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';
|
||||
import { planRoute } from '../js/galaxy/Route.js';
|
||||
|
||||
const fail = (msg, extra = {}) => {
|
||||
window.__SCOUT_DEST_TEST__ = { pass: false, error: String(msg), ...extra };
|
||||
console.error('[scout-dest] FAIL:', msg, extra);
|
||||
};
|
||||
const step = (name, cond, extra = {}) => {
|
||||
if (!cond) throw new Error(`${name}: ${JSON.stringify(extra).slice(0, 400)}`);
|
||||
window.__STEPS__ = (window.__STEPS__ ?? []).concat(name);
|
||||
};
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const waitFor = async (fn, timeoutMs = 4000) => {
|
||||
const t0 = Date.now();
|
||||
for (;;) {
|
||||
if (fn()) return true;
|
||||
if (Date.now() - t0 > timeoutMs) return false;
|
||||
await sleep(50);
|
||||
}
|
||||
};
|
||||
|
||||
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 () => {
|
||||
// 1 — the flight scene is live (v4 boots asynchronously).
|
||||
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) throw new Error('GameScene never came up');
|
||||
step('boot', true);
|
||||
|
||||
// 2 — a known ledger + cargo (something to sell).
|
||||
const A = config.get('goods.goods', [])[0].id;
|
||||
gs.ship.credits = 10000;
|
||||
gs.ship.minerals = 37;
|
||||
gs.ship.cargo.add([A]);
|
||||
step('ledger-seeded', true);
|
||||
|
||||
// 3 — the surface stage on the home world (skip the clip).
|
||||
gs.startLanding(gs.commsTargetFor(gs.planet));
|
||||
await sleep(300);
|
||||
const ss = game.scene.getScene('SurfaceScene');
|
||||
if (!ss) throw new Error('SurfaceScene never registered');
|
||||
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);
|
||||
if (ss.phase !== 'surface') throw new Error('surface stage never reached (phase=' + ss.phase + ')');
|
||||
step('surface-stage', true);
|
||||
|
||||
// 4 — the run has ENTERED (charted) every system, so the scout has
|
||||
// remote exits to offer (visitedSystems is the scout's "charted" set).
|
||||
const sysId = gs.systemRecord.id;
|
||||
for (const id of gs.galaxy.byId.keys()) gs.visitedSystems.add(id);
|
||||
step('charted-everywhere', gs.visitedSystems.size > 1, { visited: gs.visitedSystems.size });
|
||||
|
||||
// 5 — open the shop the real way; headless (no shop clip) falls back to
|
||||
// the direct build (same UI path).
|
||||
ss.toggleShop();
|
||||
await sleep(200);
|
||||
if (!ss.shopView) ss.buildShopView();
|
||||
if (!ss.shopView) throw new Error('no shopView');
|
||||
const sv = ss.shopView;
|
||||
sv.setTab('trade');
|
||||
await sleep(600); // the slide-in
|
||||
step('shop-open', true, { rows: sv.buyList.rows.length });
|
||||
|
||||
// The scout's data, computed the same way the panel does — so we can
|
||||
// pick a good with a REMOTE profitable exit and one the LOCAL system
|
||||
// buys (its exit row is the reject case).
|
||||
const visited = [...gs.visitedSystems];
|
||||
const reportFor = (row) => scoutGood(row.id, {
|
||||
galaxy: gs.galaxy, systemId: sysId, cls: sv.cls,
|
||||
buyUnit: row.price, visited, topN: 3,
|
||||
});
|
||||
const rows = sv.buyList.rows;
|
||||
let remoteRow = null, remoteExit = null, bestProfit = -Infinity;
|
||||
let localRow = null, localExit = null;
|
||||
for (const row of rows) {
|
||||
const d = reportFor(row);
|
||||
if (!d) continue;
|
||||
const rem = d.exits.find((e) => e.systemId !== sysId && e.profit > 0);
|
||||
if (rem && rem.profit > bestProfit) { remoteRow = row; remoteExit = rem; bestProfit = rem.profit; }
|
||||
const loc = d.exits.find((e) => e.systemId === sysId);
|
||||
if (loc && !localRow) { localRow = row; localExit = loc; }
|
||||
}
|
||||
step('scout-data', !!remoteRow && !!remoteExit, { remote: remoteExit?.systemId, profit: bestProfit });
|
||||
|
||||
const tapRow = (scout, exit) => {
|
||||
const i = scout._exitRows.findIndex((r) => r.exit.systemId === exit.systemId);
|
||||
if (i < 0) throw new Error('no exit row for ' + exit.systemId);
|
||||
scout._exitRows[i].onTap({ stopPropagation() {} });
|
||||
};
|
||||
const dialogShown = async () => waitFor(() => ss._setDestDialog && ss._setDestDialog.state === 'shown');
|
||||
const dialogHidden = async () => waitFor(() => ss._setDestDialog && ss._setDestDialog.state === 'hidden');
|
||||
|
||||
// 6 — REJECT: the LOCAL system's exit row → a note, NO dialog, no
|
||||
// destination change (pointing the ship at itself bounces).
|
||||
if (localRow) {
|
||||
sv._onRowTap(localRow);
|
||||
step('local-scout-open', sv._scoutOpen === localRow.id);
|
||||
tapRow(sv._scout, localExit);
|
||||
await sleep(150);
|
||||
step('local-rejected', ss._setDestDialog == null, { dialog: ss._setDestDialog && ss._setDestDialog.state });
|
||||
step('local-note', /ALREADY AT THAT SYSTEM/.test(ss.noteG?.[1]?.text ?? ''), { note: ss.noteG?.[1]?.text });
|
||||
step('local-dest-untouched', gs.destination == null);
|
||||
sv._onRowTap(null); // the scout closes, cargo grid back
|
||||
} else {
|
||||
// Fallback (no local-buying good in stock): the scene API rejects it.
|
||||
ss.askSetDestination({ systemId: sysId, systemName: 'LOCAL', price: 1, profit: 0, clsLabel: 'X' });
|
||||
await sleep(150);
|
||||
step('local-rejected', ss._setDestDialog == null);
|
||||
step('local-note', /ALREADY AT THAT SYSTEM/.test(ss.noteG?.[1]?.text ?? ''), { note: ss.noteG?.[1]?.text });
|
||||
}
|
||||
|
||||
// 7 — CONFIRM: a REMOTE exit row → the popup; CONFIRM sets the
|
||||
// destination on the GameScene and toasts it on the surface.
|
||||
sv._onRowTap(remoteRow);
|
||||
step('remote-scout-open', sv._scoutOpen === remoteRow.id && sv._scout.report.id === remoteRow.id);
|
||||
step('exit-rows-built', sv._scout._exitRows.length === sv._scout.report.exits.length,
|
||||
{ rows: sv._scout._exitRows.length, exits: sv._scout.report.exits.length });
|
||||
tapRow(sv._scout, remoteExit);
|
||||
if (!(await dialogShown())) throw new Error('SET DESTINATION dialog never opened');
|
||||
const dlg = ss._setDestDialog;
|
||||
step('dialog-open', dlg.isOpen, { state: dlg.state });
|
||||
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}` });
|
||||
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.
|
||||
const wantHops = planRoute(gs.galaxy, sysId, remoteExit.systemId)?.hops ?? null;
|
||||
step('pending-staged', ss._setDest?.exit?.systemId === remoteExit.systemId, { setDest: !!ss._setDest });
|
||||
if (wantHops != null) step('hops-computed', ss._setDest?.hops === wantHops, { got: ss._setDest?.hops, want: wantHops });
|
||||
|
||||
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('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.
|
||||
tapRow(sv._scout, remoteExit);
|
||||
if (!(await dialogShown())) throw new Error('dialog never reopened');
|
||||
ss._setDestDialog.cancel();
|
||||
if (!(await dialogHidden())) throw new Error('dialog never closed after cancel');
|
||||
step('cancel-kept', gs.destination?.systemId === remoteExit.systemId, { destination: gs.destination });
|
||||
|
||||
// 9 — ESC: the same row a third time → popup; ESC cancels (the esc
|
||||
// chain: … → set-destination → save → …) without touching the rest.
|
||||
tapRow(sv._scout, remoteExit);
|
||||
if (!(await dialogShown())) throw new Error('dialog never reopened (esc case)');
|
||||
const saveWas = ss.savePanel ?? null;
|
||||
ss.escAction();
|
||||
if (!(await dialogHidden())) throw new Error('ESC did not close the dialog');
|
||||
step('esc-cancelled', gs.destination?.systemId === remoteExit.systemId, { destination: gs.destination });
|
||||
step('esc-scope', ss.savePanel === saveWas, { savePanel: ss.savePanel, was: saveWas });
|
||||
|
||||
// 10 — teardown is clean.
|
||||
ss.leaveShop();
|
||||
await sleep(150);
|
||||
step('leave-shop', ss.shopView === null && ss.shopMode === false, { view: !!ss.shopView, mode: ss.shopMode });
|
||||
|
||||
window.__SCOUT_DEST_TEST__ = { pass: true, steps: window.__STEPS__ ?? [] };
|
||||
console.info('[scout-dest] PASS:', (window.__STEPS__ ?? []).join(' → '));
|
||||
})().catch((err) => fail(String(err && err.message || err)));
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev shop hud shot</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/shop-hud-shot.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
/**
|
||||
* Visual probe for a screenshot (headless brave, via dev/cdp-shot.mjs): boots the game, lands, opens the shop, reveals the
|
||||
* buy/sell panel — then sets window.__SHOT_READY__ so the driver captures
|
||||
* the moment the trade screen (and its upper-right credits/minerals
|
||||
* readout) is on screen.
|
||||
*
|
||||
* /opt/brave.com/brave/brave --headless --disable-gpu --remote-debugging-port=9333 about:blank &
|
||||
* node dev/server.mjs 8127
|
||||
* node dev/cdp-shot.mjs http://127.0.0.1:8127/dev/shop-hud-shot.html \
|
||||
* /tmp/shop-hud.png 'window.__SHOT_READY__ && (r = window.__SHOT_READY__, r) && 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';
|
||||
|
||||
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.__SHOT_READY__ = { error: 'no GameScene' }; return; }
|
||||
|
||||
// A known ledger + cargo so the SELL grid has filled bays.
|
||||
const goods = config.get('goods.goods', []);
|
||||
gs.ship.credits = 10000;
|
||||
gs.ship.minerals = 37;
|
||||
gs.ship.cargo.add([goods[0].id, goods[1].id]);
|
||||
|
||||
gs.startLanding(gs.commsTargetFor(gs.planet));
|
||||
await sleep(300);
|
||||
const ss = game.scene.getScene('SurfaceScene');
|
||||
if (!ss) { window.__SHOT_READY__ = { 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.__SHOT_READY__ = { error: 'no shopView' }; return; }
|
||||
sv.setTab('trade');
|
||||
await sleep(900); // the slide-in + the HUD count-up
|
||||
|
||||
window.__SHOT_READY__ = {
|
||||
hud: !!ss.shopHud,
|
||||
credits: ss.shopHud?.creditsHud?.label?.text ?? null,
|
||||
minerals: ss.shopHud?.mineralHud?.label?.text ?? null,
|
||||
creditsPos: ss.shopHud ? { x: Math.round(ss.shopHud.creditsHud.label.x), y: Math.round(ss.shopHud.creditsHud.label.y) } : null,
|
||||
mineralsPos: ss.shopHud ? { x: Math.round(ss.shopHud.mineralHud.label.x), y: Math.round(ss.shopHud.mineralHud.label.y) } : null,
|
||||
};
|
||||
})().catch((err) => { window.__SHOT_READY__ = { error: String(err && err.message || err) }; });
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev shop hud test</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/shop-hud.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,159 @@
|
|||
/**
|
||||
* Dev-only SHOP HUD test: boots the game, lands on the home world, forces
|
||||
* the surface stage, opens the buy/sell screen, and checks the corner
|
||||
* readout (SurfaceScene.buildShopHud) — the main game's own credits +
|
||||
* minerals pair, rebuilt over the shop clip in the upper right:
|
||||
*
|
||||
* - the HUD is built with the shop view (credits line + minerals line
|
||||
* + hold bar, upper right, the GameScene recipe)
|
||||
* - the credits line reads the ship's ledger and sits at the corner
|
||||
* margin; the minerals line stacks below it (topOffset recipe)
|
||||
* - a confirmed trade moves the credits readout (count-up settles on
|
||||
* the new ledger)
|
||||
* - leaving the shop tears the readout down with the shop view
|
||||
*
|
||||
* python3 -m http.server 8125
|
||||
* node dev/cdp-firefox.mjs http://127.0.0.1:8125/dev/shop-hud.html \
|
||||
* 'return window.__SHOP_HUD_TEST__;' 60000
|
||||
*/
|
||||
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';
|
||||
|
||||
const fail = (msg, extra = {}) => {
|
||||
window.__SHOP_HUD_TEST__ = { pass: false, error: String(msg), ...extra };
|
||||
console.error('[shop-hud] FAIL:', msg, extra);
|
||||
};
|
||||
const step = (name, cond, extra = {}) => {
|
||||
if (!cond) throw new Error(`${name}: ${JSON.stringify(extra).slice(0, 300)}`);
|
||||
window.__STEPS__ = (window.__STEPS__ ?? []).concat(name);
|
||||
};
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
const waitFor = async (fn, timeoutMs = 3000) => {
|
||||
const t0 = Date.now();
|
||||
for (;;) {
|
||||
if (fn()) return true;
|
||||
if (Date.now() - t0 > timeoutMs) return false;
|
||||
await sleep(50);
|
||||
}
|
||||
};
|
||||
|
||||
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 () => {
|
||||
// 1 — the flight scene is live (v4 boots asynchronously).
|
||||
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) throw new Error('GameScene never came up');
|
||||
step('boot', true, { frame: gs.planet.sheetFrame });
|
||||
|
||||
// 2 — a known ledger + cargo (something to sell).
|
||||
const goods = config.get('goods.goods', []);
|
||||
const A = goods[0].id;
|
||||
gs.ship.credits = 10000;
|
||||
gs.ship.minerals = 37;
|
||||
gs.ship.cargo.add([A]);
|
||||
step('ledger-seeded', gs.ship.credits === 10000 && gs.ship.minerals === 37);
|
||||
|
||||
// 3 — the real entry point + the surface stage (skip the clip).
|
||||
gs.startLanding(gs.commsTargetFor(gs.planet));
|
||||
await sleep(300);
|
||||
const ss = game.scene.getScene('SurfaceScene');
|
||||
if (!ss) throw new Error('SurfaceScene never registered');
|
||||
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);
|
||||
if (ss.phase !== 'surface') throw new Error('surface stage never reached (phase=' + ss.phase + ')');
|
||||
step('surface-stage', true);
|
||||
|
||||
// 4 — open the shop the real way; headless (no shop clip) falls back to
|
||||
// the direct build (same UI path — the HUD is part of buildShopView).
|
||||
ss.toggleShop();
|
||||
await sleep(200);
|
||||
if (!ss.shopView) ss.buildShopView();
|
||||
if (!ss.shopView) throw new Error('no shopView');
|
||||
step('shop-open', true, { shopMode: ss.shopMode });
|
||||
|
||||
// 5 — THE HUD: the corner readout is up with the shop view.
|
||||
const hud = ss.shopHud;
|
||||
step('hud-built', !!hud && !!hud.creditsHud && !!hud.mineralHud,
|
||||
{ hud: !!hud, credits: !!hud?.creditsHud, minerals: !!hud?.mineralHud });
|
||||
|
||||
const W = game.scale.width; // 1280
|
||||
const padC = Number(config.get('creditshud.pad', 16));
|
||||
const padM = Number(config.get('mineralhud.pad', 16));
|
||||
|
||||
// The CREDITS line: the ship's ledger, right-anchored at the corner margin
|
||||
// (origin 1,0 — x is the RIGHT edge, y the top). The count-up is a ~420 ms
|
||||
// tween — poll until it settles on the seeded ledger.
|
||||
const cl = hud.creditsHud.label;
|
||||
const wantC = `CREDITS ${gs.ship.credits.toLocaleString('en-US')}`;
|
||||
await waitFor(() => cl.text === wantC);
|
||||
step('credits-line',
|
||||
cl.text === wantC
|
||||
&& Math.abs(cl.x - (W - padC)) < 1 && Math.abs(cl.y - padC) < 1,
|
||||
{ text: cl.text, x: cl.x, y: cl.y, right: W - padC });
|
||||
|
||||
// The MINERALS line: stacks BELOW the credits line (topOffset recipe),
|
||||
// right-anchored the same way — n / CAP like the main game's corner.
|
||||
const ml = hud.mineralHud.label;
|
||||
const expectM = `MINERALS ${gs.ship.minerals} / ${gs.ship.stats.mineralStorage}`;
|
||||
step('minerals-line',
|
||||
ml.text === expectM
|
||||
&& Math.abs(ml.x - (W - padM)) < 1,
|
||||
{ text: ml.text, want: expectM, x: ml.x, y: ml.y });
|
||||
step('minerals-below-credits', ml.y > cl.y, { min: ml.y, cred: cl.y });
|
||||
|
||||
// The hold BAR: below its label, inside the screen.
|
||||
const mh = hud.mineralHud;
|
||||
step('minerals-bar',
|
||||
Number.isFinite(mh.barX) && Number.isFinite(mh.barY)
|
||||
&& mh.barY > ml.y && mh.barX + mh.barW <= W,
|
||||
{ barX: mh.barX, barY: mh.barY, barW: mh.barW });
|
||||
|
||||
// Rendered ABOVE the shop clip (the video is depth 10; the HUD is 30).
|
||||
step('hud-above-clip', (cl.depth ?? 0) >= 30, { depth: cl.depth });
|
||||
|
||||
// 6 — A TRADE: sell the bay, confirm → the credits line moves (the
|
||||
// count-up settles on the new ledger; the minerals line is untouched).
|
||||
const sv = ss.shopView;
|
||||
sv.setTab('trade');
|
||||
await sleep(600); // the slide-in
|
||||
sv.sellGrid.toggleBay(0);
|
||||
const pv = sv.preview();
|
||||
const expectCredits = gs.ship.credits + pv.sellTotal;
|
||||
step('trade-confirmed', sv.confirm() === true, { sell: pv.sellTotal });
|
||||
step('ledger-moved', gs.ship.credits === expectCredits,
|
||||
{ credits: gs.ship.credits, want: expectCredits });
|
||||
// The count-up is a ~420 ms tween — poll until it settles on the new ledger.
|
||||
const wantC2 = `CREDITS ${expectCredits.toLocaleString('en-US')}`;
|
||||
await waitFor(() => cl.text === wantC2);
|
||||
step('hud-follows-trade', cl.text === wantC2,
|
||||
{ text: cl.text, want: expectCredits });
|
||||
step('minerals-untouched', ml.text === expectM, { text: ml.text });
|
||||
|
||||
// 7 — LEAVE SHOP: the readout dies with the shop view.
|
||||
ss.leaveShop();
|
||||
await sleep(100);
|
||||
step('hud-torn-down', ss.shopHud === null && ss.shopView === null,
|
||||
{ hud: ss.shopHud, view: ss.shopView });
|
||||
const kids = ss.sys.displayList?.getChildren?.() ?? [];
|
||||
step('labels-destroyed',
|
||||
!kids.includes(cl) && !kids.includes(ml),
|
||||
{ clIn: kids.includes(cl), mlIn: kids.includes(ml) });
|
||||
|
||||
window.__SHOP_HUD_TEST__ = { pass: true, steps: window.__STEPS__ ?? [] };
|
||||
console.info('[shop-hud] PASS:', (window.__STEPS__ ?? []).join(' → '));
|
||||
})().catch((err) => fail(String(err && err.message || err)));
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Orbit — dev shop select shot</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/shop-select-shot.mjs"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,99 @@
|
|||
/**
|
||||
* Visual probe for a screenshot (headless brave, via dev/cdp-shot.mjs):
|
||||
* boots the game, lands, opens the shop, reveals the buy/sell panel, and
|
||||
* CHECKS ONE CARGO BAY — bay 0 marked to sell (green stroke + tinted fill
|
||||
* + the green checkmark in its upper-left corner), bay 1 left unchecked so
|
||||
* the shot shows both states side by side. On ready it sets
|
||||
* window.__SHOT_READY__ with the checked state + a record of what the
|
||||
* checked bay drew (border/fill colors + the corner-check strokes).
|
||||
*
|
||||
* /opt/brave.com/brave/brave --headless --disable-gpu --remote-debugging-port=9333 about:blank &
|
||||
* python3 -m http.server 8128
|
||||
* node dev/cdp-shot.mjs http://127.0.0.1:8128/dev/shop-select-shot.html \
|
||||
* /tmp/shop-select.png 'window.__SHOT_READY__ && 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';
|
||||
|
||||
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.__SHOT_READY__ = { error: 'no GameScene' }; return; }
|
||||
|
||||
// A known ledger + cargo: TWO goods → bay 0 checked, bay 1 left unchecked
|
||||
// so the shot shows the selection look next to an idle bay.
|
||||
const goods = config.get('goods.goods', []);
|
||||
gs.ship.credits = 10000;
|
||||
gs.ship.cargo.add([goods[0].id, goods[1].id]);
|
||||
|
||||
gs.startLanding(gs.commsTargetFor(gs.planet));
|
||||
await sleep(300);
|
||||
const ss = game.scene.getScene('SurfaceScene');
|
||||
if (!ss) { window.__SHOT_READY__ = { 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.__SHOT_READY__ = { error: 'no shopView' }; return; }
|
||||
sv.setTab('trade');
|
||||
await sleep(900); // the slide-in
|
||||
|
||||
// CHECK bay 0 (queue one unit for sale) — the selection look is what
|
||||
// this shot is for.
|
||||
sv.sellGrid.toggleBay(0);
|
||||
await sleep(300);
|
||||
|
||||
// Diagnostics: record what the (re)render of the CHECKED state draws on
|
||||
// the bay's plate (fill + border) and its sel graphics (the corner
|
||||
// check). Un-check first, then re-check, so the LAST recorded calls are
|
||||
// the checked look.
|
||||
let diag = null;
|
||||
const cell = sv.sellGrid.cells[0];
|
||||
if (cell?.plate && cell?.sel) {
|
||||
diag = { plate: [], sel: [] };
|
||||
const rec = (o, k, sink) => {
|
||||
const orig = o[k];
|
||||
o[k] = (...a) => {
|
||||
sink.push({ k, a: a.map((v) => (typeof v === 'number' ? Math.round(v) : v)) });
|
||||
return orig.apply(o, a); // record AND keep drawing
|
||||
};
|
||||
};
|
||||
for (const k of ['fillStyle', 'lineStyle']) { rec(cell.plate, k, diag.plate); rec(cell.sel, k, diag.sel); }
|
||||
rec(cell.sel, 'lineBetween', diag.sel);
|
||||
rec(cell.sel, 'fillCircle', diag.sel);
|
||||
sv.sellGrid.toggleBay(0); // un-check (idle render)
|
||||
sv.sellGrid.toggleBay(0); // re-check → the checked render (what we want)
|
||||
}
|
||||
|
||||
window.__SHOT_READY__ = {
|
||||
checked: sv.sellGrid.getSellMap(),
|
||||
diag,
|
||||
slot: {
|
||||
selected: config.get('shopui.slot.selected'),
|
||||
selBgColor: config.get('shopui.slot.selBgColor'),
|
||||
check: config.get('shopui.slot.check'),
|
||||
},
|
||||
};
|
||||
})().catch((err) => { window.__SHOT_READY__ = { error: String(err && err.message || err) }; });
|
||||
|
|
@ -318,6 +318,68 @@ console.log('\n== INTERACTION — icons bound per good + taps carry a hitAreaCal
|
|||
}
|
||||
console.log('INTERACTION: OK');
|
||||
|
||||
// ===========================================================================
|
||||
console.log('\n== SELECT VISUAL — checked bay: green stroke + tint + corner check; uncheck reverts ==');
|
||||
{
|
||||
const ship = makeShip(10000);
|
||||
ship.cargo.add([A]);
|
||||
const { view } = makeView(ship);
|
||||
const grid = view.sellGrid;
|
||||
const cell = grid.cells[0];
|
||||
const slotCfg = config.get('shopui.slot', {});
|
||||
const hexInt = (h) => {
|
||||
const m = String(h).trim().match(/^#?([0-9a-f]{6})$/i);
|
||||
return m ? parseInt(m[1], 16) : null;
|
||||
};
|
||||
const GREEN = hexInt(slotCfg.selected ?? '#7ce8a4');
|
||||
const BG = hexInt(slotCfg.bgColor ?? '#0e1728');
|
||||
const BORDER = hexInt(slotCfg.border ?? '#1d2e4c');
|
||||
const SELBG = hexInt(slotCfg.selBgColor ?? BG);
|
||||
assert.ok(GREEN != null && BG != null && SELBG != null, 'the slot config carries the selection colors');
|
||||
assert.notEqual(SELBG, BG, 'the selected fill is a distinct (tinted) shade');
|
||||
|
||||
// Record the draw calls on the cell's plate + check graphics.
|
||||
const wrap = (o) => {
|
||||
const calls = { fill: [], line: [], between: [] };
|
||||
if (!o) return calls;
|
||||
if (typeof o.fillStyle === 'function') o.fillStyle = (...a) => { calls.fill.push(a); return o; };
|
||||
if (typeof o.lineStyle === 'function') o.lineStyle = (...a) => { calls.line.push(a); return o; };
|
||||
if (typeof o.lineBetween === 'function') o.lineBetween = (...a) => { calls.between.push(a); return o; };
|
||||
return calls;
|
||||
};
|
||||
|
||||
// CHECK the bay → GREEN stroke + tinted fill on the plate, and the
|
||||
// corner check (green strokes in the upper-left) on the cell's sel graphics.
|
||||
const plate = wrap(cell.plate);
|
||||
const sel = wrap(cell.sel);
|
||||
grid.toggleBay(0);
|
||||
assert.deepEqual(grid.getSellMap(), { [A]: 1 }, 'bay 0 is checked (1 unit queued)');
|
||||
assert.ok(plate.line.length >= 1, 'the plate was re-styled on the check');
|
||||
assert.equal(plate.line[0][1], GREEN, `checked bay strokes the SELECTION GREEN (got 0x${(plate.line[0]?.[1] ?? 0).toString(16)}, want 0x${GREEN.toString(16)})`);
|
||||
assert.ok(plate.line[0][0] >= 2, `… a heavier stroke than the idle border (width ${plate.line[0][0]})`);
|
||||
assert.equal(plate.fill[0][0], SELBG, `… and the fill is the selected tint (got 0x${(plate.fill[0]?.[0] ?? 0).toString(16)}, want 0x${SELBG.toString(16)})`);
|
||||
assert.ok(sel.between.length >= 2, `the corner check is drawn (≥ 2 strokes, got ${sel.between.length})`);
|
||||
assert.ok(sel.line.some((l) => l[1] === GREEN), `… in the selection green (line styles: ${JSON.stringify(sel.line)})`);
|
||||
// The check sits in the cell's UPPER-LEFT quadrant (up + left of centre).
|
||||
const { x, y } = grid._cellXY(0);
|
||||
for (const b of sel.between) {
|
||||
assert.ok(b[0] < x && b[1] < y && b[2] < x && b[3] < y,
|
||||
`check stroke (${b.map((v) => Math.round(v)).join(',')}) is up-left of the cell centre (${x},${y})`);
|
||||
}
|
||||
|
||||
// UNCHECK the bay → the plate reverts to the idle border/fill and the
|
||||
// check is cleared (nothing new drawn on the sel graphics).
|
||||
const plate2 = wrap(cell.plate);
|
||||
const sel2 = wrap(cell.sel);
|
||||
grid.toggleBay(0);
|
||||
assert.deepEqual(grid.getSellMap(), {}, 'bay 0 is checked off again');
|
||||
assert.equal(plate2.line[0][1], BORDER, `unchecked bay strokes the IDLE border (got 0x${(plate2.line[0]?.[1] ?? 0).toString(16)}, want 0x${BORDER.toString(16)})`);
|
||||
assert.equal(plate2.line[0][0], 1, `… the thin idle border again (width ${plate2.line[0][0]})`);
|
||||
assert.equal(plate2.fill[0][0], BG, `… and the plain idle fill (got 0x${(plate2.fill[0]?.[0] ?? 0).toString(16)}, want 0x${BG.toString(16)})`);
|
||||
assert.equal(sel2.between.length, 0, 'the corner check is gone (cleared, no new strokes)');
|
||||
}
|
||||
console.log('SELECT VISUAL: OK');
|
||||
|
||||
// ===========================================================================
|
||||
console.log('\n== HOVER TOOLTIP — name + price on hover, NOT over the icon ==');
|
||||
{
|
||||
|
|
@ -758,4 +820,45 @@ console.log('\n== TRADE SCOUT — tap a BUY row: the cargo grid swaps for the re
|
|||
}
|
||||
console.log('TRADE SCOUT: OK');
|
||||
|
||||
console.log('\n== SET DESTINATION — tap a best-exit row on the scout: the scene is offered the exit ==');
|
||||
{
|
||||
const ship = makeShip(10000);
|
||||
ship.cargo.add([A]);
|
||||
const { view, scene } = makeView(ship);
|
||||
const calls = [];
|
||||
scene.askSetDestination = (exit) => { calls.push(exit); };
|
||||
|
||||
const row = view.buyList.rows[0];
|
||||
view._onRowTap(row);
|
||||
assert.equal(view._scoutOpen, row.id, 'scout open on the row');
|
||||
const d = view._scout.report;
|
||||
assert.ok(d.exits.length > 0, 'the report has charted exits to tap');
|
||||
|
||||
// One tappable record per exit row, in rank order.
|
||||
assert.equal(view._scout._exitRows.length, d.exits.length, 'one tappable record per exit row');
|
||||
assert.ok(view._scout._exitRows.every((r, i) => r.exit.systemId === d.exits[i].systemId), '… rows keep the report\'s rank order');
|
||||
|
||||
// TAP ROW 1 → the scene is offered THAT exit, and the panel does NOT close.
|
||||
const best = d.exits[0];
|
||||
let stopped = false;
|
||||
view._scout._exitRows[0].onTap({ stopPropagation() { stopped = true; } });
|
||||
assert.equal(calls.length, 1, 'the scene was offered the exit');
|
||||
assert.equal(calls[0].systemId, best.systemId, '… for the tapped (best) system');
|
||||
assert.equal(stopped, true, '… propagation stopped (the panel tap-layer is not fired)');
|
||||
assert.equal(view._scoutOpen, row.id, '… and the scout stays open (no close)');
|
||||
|
||||
// TAP ROW 2 (when there is one) → the scene is offered THAT row\'s exit.
|
||||
if (d.exits.length > 1) {
|
||||
view._scout._exitRows[1].onTap({});
|
||||
assert.equal(calls.length, 2, 'second row tap → second offer');
|
||||
assert.equal(calls[1].systemId, d.exits[1].systemId, '… for that row\'s system');
|
||||
}
|
||||
|
||||
// Re-showing the report rebuilds the rows (no stale tap surfaces).
|
||||
view._onRowTap(null); // close
|
||||
view._onRowTap(row); // reopen → show() runs again
|
||||
assert.equal(view._scout._exitRows.length, view._scout.report.exits.length, 'rows rebuilt for the fresh report');
|
||||
}
|
||||
console.log('SET DESTINATION (panel → scene): OK');
|
||||
|
||||
console.log('\nAll shop-UI checks passed \u2714');
|
||||
|
|
|
|||
|
|
@ -11,7 +11,11 @@ import { SavePanel } from '../ui/SavePanel.js';
|
|||
import { SaveManager } from '../save/SaveManager.js';
|
||||
import { BuildWindow } from '../ui/BuildWindow.js';
|
||||
import { QuestWindow } from '../ui/QuestWindow.js';
|
||||
import { ConfirmOverlay } from '../ui/ConfirmOverlay.js';
|
||||
import { planRoute } from '../galaxy/Route.js';
|
||||
import { ShopView } from '../ui/shop/ShopView.js';
|
||||
import { CreditsHud } from '../ui/CreditsHud.js';
|
||||
import { MineralHud } from '../ui/MineralHud.js';
|
||||
import * as Economy from '../economy/Economy.js';
|
||||
import { buildDefs } from '../research/ResearchModel.js';
|
||||
|
||||
|
|
@ -104,8 +108,12 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
this.shopVideo = null; // the SHOP clip (created on first press, then paused/resumed)
|
||||
this.shopMode = false; // true while the shop clip is on screen (SHOP toggles it)
|
||||
this.shopView = null; // the buy/sell UI over the clip (js/ui/shop/ShopView.js); null when out of the shop
|
||||
this.shopHud = null; // the buy/sell screen's corner readout (upper right): this scene's CreditsHud + MineralHud pair
|
||||
this.noteG = null; // console-note glyphs (consoleNote)
|
||||
this.buildWindow = null; // the BUILD console (deck slot) — a view over gameScene.buildState
|
||||
this._setDestDialog = null; // the shop's "SET DESTINATION" confirm (ConfirmOverlay, container-hosted)
|
||||
this._setDestHost = null; // the centered container hosting that dialog (SavePanel pattern)
|
||||
this._setDest = null; // the pending { exit, hops } behind that dialog
|
||||
}
|
||||
|
||||
/** Pick this world's clips (data/landing.json) and queue them. */
|
||||
|
|
@ -357,6 +365,9 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
* Buyback state resets with it), and destroyed by leaveShop(). It reads the
|
||||
* paused GameScene for the ledger (ship.credits / ship.cargo) + the economy
|
||||
* (galaxy, this system) and the shop's economic class (this world's nature).
|
||||
* The screen also carries the main game's corner readout (upper right —
|
||||
* buildShopHud), so the player sees the same credits/minerals lines they
|
||||
* have in flight while trading.
|
||||
*/
|
||||
buildShopView() {
|
||||
if (this.shopView) return this.shopView; // already up this visit
|
||||
|
|
@ -373,9 +384,43 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
anchor: this.shopAnchor(),
|
||||
topY: this.shopTopY(),
|
||||
});
|
||||
this.buildShopHud(); // the upper-right ledger (credits + minerals), this scene
|
||||
return this.shopView;
|
||||
}
|
||||
|
||||
/**
|
||||
* The buy/sell screen's CORNER READOUT — the main game's own pair
|
||||
* (js/ui/CreditsHud.js + js/ui/MineralHud.js) built in THIS scene, so it
|
||||
* renders over the shop clip: CREDITS on top, MINERALS + the hold bar
|
||||
* stacked below (upper right, the same recipe as GameScene's corner —
|
||||
* same data/*.json config, same count-up). It reads the very ship the
|
||||
* shop trades against and dies with the shop view (leaveShop/shutdown).
|
||||
*/
|
||||
buildShopHud() {
|
||||
if (this.shopHud) return this.shopHud;
|
||||
const ship = this.gameScene?.ship;
|
||||
if (!ship) return null;
|
||||
const creditsHud = new CreditsHud(this);
|
||||
creditsHud.set(ship.credits);
|
||||
const mineralHud = new MineralHud(this, { topOffset: creditsHud.height + creditsHud.gap });
|
||||
mineralHud.set(ship.minerals, ship.stats?.mineralStorage);
|
||||
this.shopHud = { creditsHud, mineralHud };
|
||||
return this.shopHud;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirror the ship's ledger into the corner readout (after a trade —
|
||||
* ShopView.refreshHud — or any other change while the shop is up).
|
||||
* No-op when the shop (and its HUD) is not on screen.
|
||||
*/
|
||||
refreshShopHud() {
|
||||
const hud = this.shopHud;
|
||||
const ship = this.gameScene?.ship;
|
||||
if (!hud || !ship) return;
|
||||
hud.creditsHud?.set?.(ship.credits);
|
||||
hud.mineralHud?.set?.(ship.minerals, ship.stats?.mineralStorage);
|
||||
}
|
||||
|
||||
/**
|
||||
* The trade panel's top edge — just under the surface HUD's planet text
|
||||
* (the name + the type/tether sub-line), with a little padding. The HUD is
|
||||
|
|
@ -420,6 +465,96 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
if (!this.shopView) return;
|
||||
this.shopView.destroy?.();
|
||||
this.shopView = null;
|
||||
this.destroyShopHud(); // the corner readout belongs to the shop visit
|
||||
}
|
||||
|
||||
/** Tear the corner readout down (leaving the shop / scene shutdown). */
|
||||
destroyShopHud() {
|
||||
if (!this.shopHud) return;
|
||||
this.shopHud.creditsHud?.destroy?.();
|
||||
this.shopHud.mineralHud?.destroy?.();
|
||||
this.shopHud = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* A BEST-EXIT row on the trade scout was tapped (ScoutPanel → onExitTap
|
||||
* → 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).
|
||||
*/
|
||||
askSetDestination(exit) {
|
||||
if (!exit || typeof exit.systemId !== 'string') return;
|
||||
const gs = this.gameScene;
|
||||
if (!gs || typeof gs.setDestination !== 'function') {
|
||||
this.consoleNote('NO NAV LINK — SET IT FROM THE MAP', { glyph: '✕', glyphColor: toCss(themeColor('neon2', 0xff9b9b)) });
|
||||
return;
|
||||
}
|
||||
// The local system is always in the exit list (charted, 0 hops) —
|
||||
// pointing the ship at itself just bounces back, so say so instead.
|
||||
if (gs.systemRecord?.id === exit.systemId) {
|
||||
this.consoleNote('ALREADY AT THAT SYSTEM', { glyph: '✕', glyphColor: toCss(themeColor('neon2', 0xff9b9b)) });
|
||||
return;
|
||||
}
|
||||
const name = String(exit.systemName ?? exit.systemId).toUpperCase();
|
||||
const cls = exit.clsLabel ? `${exit.clsLabel} · ` : '';
|
||||
const sign = exit.profit >= 0 ? '+' : '−';
|
||||
const plan = planRoute(gs.galaxy, gs.systemRecord?.id, exit.systemId);
|
||||
this._setDest = { exit, hops: plan?.hops ?? null };
|
||||
this._ensureSetDestDialog().show({
|
||||
title: 'SET DESTINATION',
|
||||
body: [
|
||||
`DESTINATION — ${name}`,
|
||||
`${cls}SELLS ${exit.price} CR/UNIT · ${sign}${Math.abs(exit.profit)}/UNIT`,
|
||||
],
|
||||
accent: toCss(themeColor('neon', 0x00e5ff)),
|
||||
confirmLabel: 'SET DESTINATION',
|
||||
onConfirm: () => {
|
||||
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)`);
|
||||
this.consoleNote(label, { glyph: '◆', glyphColor: toCss(config.get('gates.route.compassColor', '#ff8c1a')) });
|
||||
},
|
||||
time: this.time.now,
|
||||
});
|
||||
}
|
||||
|
||||
/** The shared confirm dialog (built once, sized like the map's). */
|
||||
_ensureSetDestDialog() {
|
||||
if (this._setDestDialog) return this._setDestDialog;
|
||||
const ds = config.get('shopui.scout.setDestination', {});
|
||||
const w = Number(ds?.width) > 0 ? Number(ds.width) : 520;
|
||||
const h = Number(ds?.height) > 0 ? Number(ds.height) : 176;
|
||||
// Host it the way SavePanel/MapWindow host their confirm dialogs:
|
||||
// `new ConfirmOverlay(scene, 0, 0, …)` inside a centered container
|
||||
// (`new Container` + `add.existing` — not `scene.add.container`, which
|
||||
// this v4 build doesn't put on the display list).
|
||||
// Note: in the headless-Brave probe the plate Graphics doesn't paint in
|
||||
// the SurfaceScene context (the same happens to SavePanel's own confirm
|
||||
// there); the dialog renders correctly in a real browser — see
|
||||
// dev/map-dest-shot.* (the identical component over the map, painted
|
||||
// fully in headless).
|
||||
this._setDestHost = new Phaser.GameObjects.Container(
|
||||
this, this.scale.width / 2, this.scale.height / 2,
|
||||
);
|
||||
this.add.existing(this._setDestHost);
|
||||
// Above EVERYTHING in the surface scene — the shop's panels live in
|
||||
// ShopView's root container at depth 100 (js/ui/shop/ShopView.js),
|
||||
// above the deck (50), console note (60), SavePanel (70), MapWindow
|
||||
// (80). 110 puts the dialog + its scrim over all of it.
|
||||
this._setDestHost.setDepth(110);
|
||||
this._setDestDialog = new ConfirmOverlay(
|
||||
this,
|
||||
0, 0,
|
||||
w, h,
|
||||
{ areaW: this.scale.width, areaH: this.scale.height },
|
||||
);
|
||||
this._setDestHost.add(this._setDestDialog);
|
||||
return this._setDestDialog;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -855,7 +990,7 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
this.questWindow?.refresh();
|
||||
}
|
||||
|
||||
/** ESC: build console → quests console → confirm dialog → save pop-up → sub-bar. */
|
||||
/** ESC: build console → quests console → set-destination → confirm dialog → save pop-up → sub-bar. */
|
||||
escAction() {
|
||||
if (this.buildWindow?.isOpen) {
|
||||
this.buildWindow.close();
|
||||
|
|
@ -865,6 +1000,10 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
this.questWindow.close();
|
||||
return;
|
||||
}
|
||||
if (this._setDestDialog?.isOpen) {
|
||||
this._setDestDialog.cancel();
|
||||
return;
|
||||
}
|
||||
if (this.savePanel) {
|
||||
if (this.savePanel.confirm.isOpen) {
|
||||
this.savePanel.confirm.cancel();
|
||||
|
|
@ -1106,6 +1245,7 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
this.actionBar?.update(time, delta);
|
||||
this.menuSubBar?.update(time, delta);
|
||||
this.savePanel?.update(time);
|
||||
this._setDestDialog?.update(time); // the SET DESTINATION confirm
|
||||
this.updateHud(time); // the upper-left name/type decode (surface stage only)
|
||||
|
||||
// The build console: tick the single in-progress build (one at a
|
||||
|
|
@ -1144,6 +1284,12 @@ export class SurfaceScene extends Phaser.Scene {
|
|||
this.noteG = null;
|
||||
}
|
||||
this.destroyHud();
|
||||
this.destroyShopHud();
|
||||
this._setDestDialog?.destroy?.();
|
||||
this._setDestDialog = null;
|
||||
this._setDestHost?.destroy?.();
|
||||
this._setDestHost = null;
|
||||
this._setDest = null;
|
||||
this.buildWindow?.destroy();
|
||||
this.buildWindow = null;
|
||||
this.actionBar?.destroy();
|
||||
|
|
|
|||
|
|
@ -13,8 +13,8 @@
|
|||
* │ SIGHT ON Rocky · Ice … │
|
||||
* │ ────────────────────────── │
|
||||
* │ BEST EXITS — CHARTED │
|
||||
* │ ① New Concord Gas 41 cr │
|
||||
* │ ② … │
|
||||
* │ ① New Concord Gas 41 cr │ ← TAP A ROW → "SET DESTINATION" popup
|
||||
* │ ② … │ (the scene's ConfirmOverlay)
|
||||
* │ ────────────────────────── │
|
||||
* │ TAP ANYWHERE TO CLOSE │
|
||||
* └────────────────────────────┘
|
||||
|
|
@ -22,7 +22,9 @@
|
|||
* The DATA (which planets want the good, the top-N charted exits + profit)
|
||||
* is computed by js/economy/TradeScout.js (pure). This component only
|
||||
* renders that report in the panel's w×h box and reports taps back to the
|
||||
* ShopView (tap anywhere = close — the ShopView restores the cargo grid).
|
||||
* ShopView (tap anywhere = close — the ShopView restores the cargo grid;
|
||||
* tap a BEST-EXIT row = onExitTap(exit) — the scene offers to SET it as
|
||||
* the destination).
|
||||
*
|
||||
* Headless-safe: the Phaser rendering is best-effort (guarded); the layout
|
||||
* math is fixed offsets so a stub scene still constructs.
|
||||
|
|
@ -35,8 +37,10 @@ import { GOODS_TEXTURE_KEY, goodFrame } from '../../economy/Economy.js';
|
|||
export class ScoutPanel extends Phaser.GameObjects.Container {
|
||||
/**
|
||||
* @param {object} scene — the SurfaceScene (or a headless fake).
|
||||
* @param {object} o — { width, height, onClose } (ShopView sizes it to the
|
||||
* SELL grid's box; onClose fires on any tap of the panel).
|
||||
* @param {object} o — { width, height, onClose, onExitTap } (ShopView sizes
|
||||
* it to the SELL grid's box; onClose fires on any tap of the panel;
|
||||
* onExitTap(exit) fires when a BEST-EXIT row is tapped — the scene
|
||||
* offers the "Set Destination" popup for that system).
|
||||
*/
|
||||
constructor(scene, o = {}) {
|
||||
super(scene, 0, 0);
|
||||
|
|
@ -46,10 +50,12 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
|
|||
this.height = Math.max(240, Number(o.height) || 480);
|
||||
this.pad = s.pad ?? 18;
|
||||
this.onClose = typeof o.onClose === 'function' ? o.onClose : null;
|
||||
this.onExitTap = typeof o.onExitTap === 'function' ? o.onExitTap : null;
|
||||
|
||||
this._objs = []; // shell objects (plate, title, tap layer)
|
||||
this._content = []; // per-show content (rebuilt by show())
|
||||
this._data = null; // the last scoutGood() report
|
||||
this._exitRows = []; // per-show best-exit rows { exit, hit, onTap }
|
||||
|
||||
this._buildShell();
|
||||
this._buildTapLayer(); // LAST — the tap rect sits on top
|
||||
|
|
@ -121,6 +127,7 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
|
|||
if (!d || typeof d.id !== 'string') return this;
|
||||
this._data = d;
|
||||
this._destroyContent();
|
||||
this._exitRows = [];
|
||||
const s = config.section('shopui.scout', {});
|
||||
const W = this.width;
|
||||
const pad = this.pad;
|
||||
|
|
@ -212,6 +219,13 @@ export class ScoutPanel extends Phaser.GameObjects.Container {
|
|||
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);
|
||||
}
|
||||
y += 24;
|
||||
|
||||
const er = s.exitRow ?? {};
|
||||
|
|
@ -255,6 +269,29 @@ 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()).
|
||||
const hit = this.scene.add?.graphics?.();
|
||||
if (hit) {
|
||||
const hx = pad - 6, hy = y - 4, hw = W - pad * 2 + 12, hh = rowH;
|
||||
const onTap = (p) => { p?.stopPropagation?.(); this.onExitTap?.(e); };
|
||||
if (typeof hit.setInteractive === 'function' && typeof hit.on === 'function') {
|
||||
const Rect = Phaser.Geom?.Rectangle;
|
||||
const area = Rect ? new Rect(hx, hy, hw, hh) : null;
|
||||
hit.setInteractive({
|
||||
useHandCursor: true,
|
||||
hitArea: area,
|
||||
hitAreaCallback: (pp, px, py) => (area ? Rect.Contains(pp, px, py) : true),
|
||||
});
|
||||
hit.on('pointerdown', onTap);
|
||||
}
|
||||
this._exitRows.push({ exit: e, hit, onTap });
|
||||
add(hit);
|
||||
}
|
||||
y += rowH;
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -494,11 +494,14 @@ export class ShopView extends Phaser.GameObjects.Container {
|
|||
this._syncPreview();
|
||||
|
||||
// The scout panel: sized to the SELL grid's box (it replaces the cargo
|
||||
// area), added LAST so it paints above the grid while open.
|
||||
// area), added LAST so it paints above the grid while open. A BEST-EXIT
|
||||
// row's tap goes to the scene (the "SET DESTINATION" popup —
|
||||
// SurfaceScene.askSetDestination); anything else closes the panel.
|
||||
const scout = new ScoutPanel(this.scene, {
|
||||
width: sellGrid.width,
|
||||
height: sellGrid.height,
|
||||
onClose: () => this._closeScout(),
|
||||
onExitTap: (exit) => this.scene?.askSetDestination?.(exit),
|
||||
});
|
||||
scout.setPosition?.(leftX, this.topY);
|
||||
this._scout = scout;
|
||||
|
|
@ -674,7 +677,12 @@ export class ShopView extends Phaser.GameObjects.Container {
|
|||
}
|
||||
|
||||
refreshHud() {
|
||||
// The GAME scene's HUD (the main-flight corner readout) — and the
|
||||
// SURFACE scene's HUD (this screen's own upper-right pair, which is the
|
||||
// one the player actually sees over the shop clip — the GameScene is
|
||||
// sleeping beneath us, so its HUD update is belt-and-braces).
|
||||
this.gameScene?.refreshCreditsHud?.();
|
||||
this.scene?.refreshShopHud?.();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -7,8 +7,8 @@
|
|||
* │ │icon││icon│ │ filled bay → good icon
|
||||
* │ └────┘└────┘ │ empty bay → dim plate
|
||||
* │ ┌────┐┌────┐ │ locked bay → "locked" label (the hold's upgrade room)
|
||||
* │ │ ││lock│ │ checked → cyan selection ring (1 unit queued)
|
||||
* │ └────┘└────┘ │
|
||||
* │ │ ✓ ││lock│ │ checked → GREEN stroke + tinted fill + a green ✓ in
|
||||
* │ └────┘└────┘ │ the upper-left corner (1 unit queued)
|
||||
* └────────────┘
|
||||
*
|
||||
* A BAY is the unit (not a good): each filled bay holds 1 unit of its good
|
||||
|
|
@ -16,7 +16,10 @@
|
|||
* live (filled with a good, or empty); the rest are LOCKED — the hold's
|
||||
* upgrade room (the ship can't carry there yet). Hovering a cell reveals
|
||||
* the good's name + sell price in a shared tooltip (icon-only cells — no
|
||||
* text over the icon).
|
||||
* text over the icon). A CHECKED bay is marked three ways (all revert
|
||||
* together when it's checked off again): the green border stroke, a
|
||||
* slightly lifted green-tinted fill, and the green checkmark badge in the
|
||||
* upper-left corner (data/shopui.json → slot: selected, selBg, check).
|
||||
*
|
||||
* (The BUY side is no longer a grid — the shop's stock is now a scrollable
|
||||
* list of wide rows: js/ui/shop/BuyList.js. ShopView composes the two side
|
||||
|
|
@ -158,7 +161,8 @@ export class TradeGrid extends Phaser.GameObjects.Container {
|
|||
const plate = this.scene.add?.graphics?.();
|
||||
if (plate) { cell.plate = plate; cell.objects.push(plate); }
|
||||
|
||||
// selection ring (sell) — cyan highlight when the bay is checked
|
||||
// corner check (sell) — the green checkmark badge when the bay is
|
||||
// queued (added AFTER the icon so it paints over it — see _buildCells)
|
||||
if (this.side === 'sell') {
|
||||
const sel = this.scene.add?.graphics?.();
|
||||
if (sel) { cell.sel = sel; cell.objects.push(sel); }
|
||||
|
|
@ -190,9 +194,9 @@ export class TradeGrid extends Phaser.GameObjects.Container {
|
|||
|
||||
this.cells.push(cell);
|
||||
this.add?.(cell.plate);
|
||||
if (cell.sel) this.add?.(cell.sel);
|
||||
if (cell.icon) this.add?.(cell.icon);
|
||||
if (cell.label) this.add?.(cell.label);
|
||||
if (cell.sel) this.add?.(cell.sel); // the corner check — LAST, above the icon
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -333,17 +337,29 @@ export class TradeGrid extends Phaser.GameObjects.Container {
|
|||
|
||||
const usable = !locked && !!goodId;
|
||||
|
||||
// plate border (selected sell bays get the cyan ring)
|
||||
// plate — base fill + border. A QUEUED bay (checked to sell) is marked
|
||||
// here: the GREEN stroke + a slightly lifted green-tinted fill (the
|
||||
// corner check is drawn below); unchecking re-renders the idle look.
|
||||
const selected = usable && this.sellSel.has(i);
|
||||
if (cell.plate && typeof cell.plate.clear === 'function') {
|
||||
const selected = this.sellSel.has(i);
|
||||
cell.plate.clear?.();
|
||||
if (typeof cell.plate.fillStyle === 'function') {
|
||||
cell.plate.fillStyle(_hex(sl.bgColor ?? '#0e1728'), usable ? 0.92 : (sl.lockAlpha ?? 0.3));
|
||||
cell.plate.lineStyle?.(selected ? 2 : 1, _hex(selected ? (sl.selected ?? '#00e5ff') : (sl.border ?? '#1d2e4c')), 255);
|
||||
cell.plate.fillStyle(
|
||||
_hex(selected ? (sl.selBgColor ?? sl.bgColor ?? '#0e1728') : (sl.bgColor ?? '#0e1728')),
|
||||
usable ? (selected ? (sl.selBgAlpha ?? 0.95) : 0.92) : (sl.lockAlpha ?? 0.3));
|
||||
cell.plate.lineStyle?.(selected ? 2 : 1, _hex(selected ? (sl.selected ?? '#7ce8a4') : (sl.border ?? '#1d2e4c')), 255);
|
||||
}
|
||||
cell.plate.fillRect?.(x - this.slot / 2, y - this.slot / 2, this.slot, this.slot);
|
||||
}
|
||||
|
||||
// the corner check (upper-left) — the selected bay's green checkmark
|
||||
// badge, drawn on cell.sel (added after the icon → above it); cleared
|
||||
// when the bay is checked off again (the render is pure state).
|
||||
if (cell.sel && typeof cell.sel.clear === 'function') {
|
||||
cell.sel.clear?.();
|
||||
if (selected) this._drawCheck(cell.sel, x, y, sl);
|
||||
}
|
||||
|
||||
// icon — the cell's only content for a usable bay (the name+price live
|
||||
// in the hover tooltip). Centered.
|
||||
if (cell.icon) {
|
||||
|
|
@ -370,6 +386,31 @@ export class TradeGrid extends Phaser.GameObjects.Container {
|
|||
return list.find((g) => g.id === id)?.name ?? id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The checked bay's corner mark (UPPER-LEFT): a small dark badge so the
|
||||
* green check reads over any icon corner, then the green check itself
|
||||
* (two strokes — short arm down to the vertex, long arm up). Sized from
|
||||
* data/shopui.json → slot.check; drawn on the cell's `sel` graphics.
|
||||
* Headless-safe: a fake scene without fillCircle just skips the badge.
|
||||
*/
|
||||
_drawCheck(g, x, y, sl) {
|
||||
if (!g || typeof g.fillStyle !== 'function' || typeof g.lineStyle !== 'function' || typeof g.lineBetween !== 'function') return;
|
||||
const chk = sl.check ?? {};
|
||||
const r = Math.max(6, Number(chk.size) || 11); // the badge radius
|
||||
const inset = Math.max(4, Number(chk.inset) || 7); // the badge's corner inset
|
||||
const cx = x - this.slot / 2 + inset + r; // badge centre (upper-left)
|
||||
const cy = y - this.slot / 2 + inset + r;
|
||||
if (chk.badgeBg != null && typeof g.fillCircle === 'function') {
|
||||
g.fillStyle(_hex(chk.badgeBg), chk.badgeAlpha != null ? Number(chk.badgeAlpha) : 0.85);
|
||||
g.fillCircle(cx, cy, r);
|
||||
}
|
||||
// the check — sized to the badge, green (the selection color)
|
||||
const s = r * 0.62;
|
||||
g.lineStyle?.(Math.max(2, Math.round(r * 0.3)), _hex(chk.color ?? sl.selected ?? '#7ce8a4'), 1);
|
||||
g.lineBetween(cx - s * 0.9, cy + s * 0.05, cx - s * 0.25, cy + s * 0.62);
|
||||
g.lineBetween(cx - s * 0.25, cy + s * 0.62, cx + s * 0.95, cy - s * 0.55);
|
||||
}
|
||||
|
||||
// --- HOVER TOOLTIP (name + price, revealed per cell) ------------------
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue