176 lines
8.2 KiB
JavaScript
176 lines
8.2 KiB
JavaScript
/**
|
|
* Dev-only SHOP PANEL geometry probe (real browser, via cdp-firefox.mjs):
|
|
* boots the game, lands on the home world, forces the surface stage, opens
|
|
* the shop, clicks "Buy/Sell", waits for the slide-in, then reports the
|
|
* on-screen geometry of the planet HUD text + the SELL/BUY panels + the
|
|
* sub-bar — so the panel's top edge vs. the planet text is verifiable
|
|
* numerically. Also sets window.__SHOP_SHOT__ so a driver can screenshot
|
|
* the moment the numbers are ready.
|
|
*
|
|
* node dev/server.mjs 8126
|
|
* node dev/cdp-firefox.mjs http://127.0.0.1:8126/dev/shop-shot.html \
|
|
* 'return window.__SHOP_SHOT__;' 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_SHOT__ = { pass: false, error: String(msg), ...extra };
|
|
console.error('[shop-shot] FAIL:', msg);
|
|
};
|
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
|
const data = await ConfigLoader.load();
|
|
config.init(data);
|
|
// Await the theme web fonts (mirrors js/main.js) so canvas text draws with
|
|
// Ethnocentric/Centauri, not the fallback stack. Hard timeout: boot never
|
|
// hangs on a flaky font.
|
|
{ const fonts = globalThis.document?.fonts; if (fonts?.load) {
|
|
const fams = ['Ethnocentric', 'Centauri'];
|
|
await Promise.race([
|
|
Promise.allSettled(fams.map((f) => fonts.load(`16px "${f}"`).catch(() => {}))),
|
|
new Promise((r) => setTimeout(r, 2000)),
|
|
]);
|
|
} }
|
|
const gameConfig = createGameConfig();
|
|
gameConfig.scene = [GameScene, SurfaceScene];
|
|
const game = new Phaser.Game(gameConfig);
|
|
window.game = game;
|
|
|
|
const bounds = (o) => {
|
|
if (!o) return null;
|
|
const wb = o.worldBounds ?? (o.getBounds && o.getBounds());
|
|
if (wb && Number.isFinite(wb.x)) {
|
|
return { x: Math.round(wb.x), y: Math.round(wb.y), w: Math.round(wb.width), h: Math.round(wb.height) };
|
|
}
|
|
return { x: Math.round(o.x ?? 0), y: Math.round(o.y ?? 0) };
|
|
};
|
|
|
|
(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) throw new Error('GameScene never came up');
|
|
|
|
// Land on the home world + force the surface stage (skip the clip).
|
|
const target = gs.commsTargetFor(gs.planet);
|
|
gs.startLanding(target);
|
|
await sleep(300);
|
|
const ss = game.scene.getScene('SurfaceScene');
|
|
if (!ss) throw new Error('SurfaceScene never registered');
|
|
// Wait for the scene to finish loading (this build: RUNNING = 5) before
|
|
// driving it.
|
|
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 + ')');
|
|
await sleep(300);
|
|
|
|
// Give the hold a couple of goods so the SELL grid shows filled bays.
|
|
const goods = config.get('goods.goods', []);
|
|
gs.ship.cargo.add([goods[0].id, goods[1].id]);
|
|
|
|
// Open the shop the real way; if the shop clip is missing in this
|
|
// environment (headless video), build the view directly (same UI path).
|
|
ss.toggleShop();
|
|
await sleep(200);
|
|
if (!ss.shopView) ss.buildShopView();
|
|
const sv = ss.shopView;
|
|
if (!sv) throw new Error('no shopView');
|
|
|
|
// Click "Buy/Sell" (reveal the trade panel) and let the slide-in settle.
|
|
sv.setTab('trade');
|
|
await sleep(900);
|
|
|
|
// ---- HOVER TOOLTIP PROBE (name + price on hover, not over the icon) ----
|
|
const round = (v) => Math.round(v);
|
|
const worldTopLeft = (o) => o?.getTopLeft ? (() => { const v = o.getTopLeft(undefined, true); return { x: round(v.x), y: round(v.y) }; })() : null;
|
|
const tipState = (grid) => ({
|
|
visible: !!grid.tipVisible,
|
|
tipAlpha: grid.tip?.alpha ?? null,
|
|
name: grid.tipName?.text ?? null,
|
|
price: grid.tipPrice?.text ?? null,
|
|
nameFont: grid.tipName?.style?.fontFamily ?? grid.tipName?._style?.fontFamily ?? null,
|
|
tipTopLeft: worldTopLeft(grid.tipName),
|
|
tipPlateTopLeft: worldTopLeft(grid.tipPlate),
|
|
tipPos: grid.tip ? { x: round(grid.tip.x), y: round(grid.tip.y) } : null,
|
|
});
|
|
const cellWorldCenter = (grid, i) => {
|
|
const { x, y } = grid._cellXY(i);
|
|
const c = grid.children?.list?.find?.((o) => o === grid.plate) ?? null;
|
|
// grid local (x,y) → world: the grid container IS a pure translation (dev/cbox-test.html)
|
|
return { x: round(grid.x + x), y: round(grid.y + y) };
|
|
};
|
|
const toClient = (wx, wy) => {
|
|
const r = game.canvas.getBoundingClientRect();
|
|
return { cx: r.left + (wx / 1280) * r.width, cy: r.top + (wy / 720) * r.height };
|
|
};
|
|
const mouseMove = (wx, wy) => {
|
|
const { cx, cy } = toClient(wx, wy);
|
|
// This Giedi build's input listens for classic mouse events on the canvas
|
|
// (mousedown/up/move + mouseover/out) — NOT pointer events — so hover is
|
|
// driven with a real DOM mousemove (bubbles: true).
|
|
const el = game.canvas;
|
|
el.dispatchEvent(new MouseEvent('mousemove', { clientX: cx, clientY: cy, bubbles: true, view: window }));
|
|
return { cx: round(cx), cy: round(cy) };
|
|
};
|
|
|
|
const out = { pass: true, tooltip: null };
|
|
|
|
// 1) structure: the old per-cell name/price text is GONE from usable cells
|
|
const sellCell = sv.sellGrid.cells[0];
|
|
const struct = {
|
|
sellCellName: sellCell.name === undefined ? 'undefined' : 'present',
|
|
sellCellPrice: sellCell.price === undefined ? 'undefined' : 'present',
|
|
sellLabel: { text: sellCell.label?.text ?? null, alpha: sellCell.label?.alpha ?? null },
|
|
sellLockedLabel: { text: sv.sellGrid.cells[sv.sellGrid.capacity]?.label?.text ?? null, alpha: sv.sellGrid.cells[sv.sellGrid.capacity]?.label?.alpha ?? null },
|
|
sellIcon: { alpha: sellCell.icon?.alpha ?? null, scale: round((sellCell.icon?.scaleX ?? 0) * 100) / 100, pos: { x: sellCell.icon?.x, y: sellCell.icon?.y } },
|
|
titleFont: sv.sellGrid.title?.style?.fontFamily ?? sv.sellGrid.title?._style?.fontFamily ?? null,
|
|
buyQtyFont: sv.buyList.rows[0]?.objs?.qty?.style?.fontFamily ?? sv.buyList.rows[0]?.objs?.qty?._style?.fontFamily ?? null,
|
|
};
|
|
out.tooltip = { struct };
|
|
|
|
// 2) programmatic hover on a filled SELL bay (row 0 → tip flips BELOW)
|
|
const tipSellA = sv.sellGrid.hoverCell(0);
|
|
out.tooltip.sellHover = { ...tipState(sv.sellGrid), expectedName: goods[0].name, cell: cellWorldCenter(sv.sellGrid, 0) };
|
|
|
|
// 3) BUY side: the WIDE ROW carries name + description + price INLINE (no
|
|
// tooltip — the row is wide enough).
|
|
const row0 = sv.buyList.rows[0];
|
|
const rowNameEl = row0?.objs?.name;
|
|
out.tooltip.buyRow = {
|
|
name: rowNameEl?.text ?? null,
|
|
desc: row0?.objs?.desc?.text ?? null,
|
|
price: row0?.objs?.price?.text ?? null,
|
|
qty: row0?.objs?.qty?.text ?? null,
|
|
nameFont: rowNameEl?.style?.fontFamily ?? rowNameEl?._style?.fontFamily ?? null,
|
|
rowWorld: rowNameEl ? { x: round(sv.buyList.x + rowNameEl.x), y: round(sv.buyList.y + rowNameEl.y) } : null,
|
|
};
|
|
|
|
// 4) move the pointer OFF the panel → pointerout hides the tip
|
|
const off = toClient(Math.max(4, sv.buyList.x - 30), 360);
|
|
game.canvas.dispatchEvent(new MouseEvent('mousemove', { clientX: off.cx, clientY: off.cy, bubbles: true, view: window }));
|
|
await sleep(150);
|
|
out.tooltip.mouseOff = { state: tipState(sv.sellGrid) };
|
|
|
|
// 6) end with the SELL tooltip visible for the screenshot
|
|
sv.sellGrid.hoverCell(0);
|
|
out.tooltip.finalSell = tipState(sv.sellGrid);
|
|
|
|
out.screen = { w: ss.scale.width, h: ss.scale.height };
|
|
out.topY = sv.topY ?? null;
|
|
out.hudName = (() => { const t = ss.hudName; if (t?.getTopLeft) { const v = t.getTopLeft(undefined, true); return { x: round(v.x), y: round(v.y) }; } return null; })();
|
|
out.sellGrid = { x: round(sv.sellGrid.x), y: round(sv.sellGrid.y), w: sv.sellGrid.width, h: sv.sellGrid.height };
|
|
out.buyList = { x: round(sv.buyList.x), y: round(sv.buyList.y), w: sv.buyList.width, h: sv.buyList.height, rows: sv.buyList.rows.length, maxScroll: sv.buyList.maxScroll };
|
|
|
|
window.__SHOP_SHOT__ = out;
|
|
console.info('[shop-shot] OK:', JSON.stringify(out));
|
|
})().catch((e) => fail(String(e?.message || e)));
|