orbit/dev/shop-hud.mjs

164 lines
6.9 KiB
JavaScript

/**
* 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 });
// The probe drives the trade panel on the home world — lift the TRADE gate
// (the 'Special Delivery' capital-landing witness) so Buy/Sell is live.
gs.story.capitalLanded = true;
// 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)));