/** * 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, destination.objectId = the exit's * WORLD (its planet of that class / station / home world — the * route's final stop) — and toasts it on the surface (the game * scene's toast would sit frozen while sleeping) * - the LOCAL system exit is rejected with a note (no dialog, no * destination change — pointing the ship at itself bounces) * - CANCEL and ESC leave the destination untouched * * 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); // 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 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(); const obj = remoteExit.objectName ? String(remoteExit.objectName).toUpperCase() : null; step('dialog-body', dlg.bodyTexts[0].text === `DESTINATION — ${obj ? `${obj} · ${name}` : name}`, { body0: dlg.bodyTexts[0].text, want: `DESTINATION — ${obj ? `${obj} · ${name}` : name}` }); step('dialog-body-price', /SELLS \d+ CR\/UNIT/.test(dlg.bodyTexts[1].text ?? ''), { body1: dlg.bodyTexts[1].text }); // The scene staged the pending pick (exit + route hops) before showing. 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 === (remoteExit.objectId ?? null), { destination: gs.destination, wantObject: remoteExit.objectId ?? null }); if (remoteExit.objectId) { // The destination must name a REAL object of the exit's world — a // planet (discoveryId = its name) or the deep-space station. const content = gs.galaxy.ensureContent(remoteExit.systemId); const world = (content.planets ?? []).find((p) => p.name === remoteExit.objectId) ?? (content.settlements ?? []).find((s) => s.id === remoteExit.objectId); step('destination-names-a-world', !!world, { objectId: remoteExit.objectId, world: world?.name ?? world?.id }); } step('surface-toast', /DESTINATION SET/.test(ss.noteG?.[1]?.text ?? ''), { note: ss.noteG?.[1]?.text }); // 8 — CANCEL: the same row again → popup; CANCEL leaves everything as-is. 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)));