/** * Dev: real-input test for the CommsPanel interaction paths: * 1. click an object → panel opens + ship retargeted * 2. click CANCEL → closes * 3. rep ≤ −4, click REQUEST LANDING → inert (panel stays open) * 4. click outside → closes * 5. ESC → closes * 6. rep > −4, click REQUEST LANDING → action seam fires + closes * * Real DOM pointer events hit the canvas (Phaser's input manager), so this * exercises the full path: scene handler → worldObjectAt → openCommsPanel, * and the button race-guard (closedByButtonAt) for clicks inside. * * python3 -m http.server 8080 * node dev/wdshot.mjs http://localhost:8080/dev/comms-click.html \ * 'return window.__CLICK_TEST__();' 90000 9000 */ 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 data = await ConfigLoader.load(); config.init(data); // The game measures its UI text in the web font — wait for it before the // scenes build any text (same contract as js/main.js). const __fonts = globalThis.document?.fonts; if (__fonts && typeof __fonts.load === 'function') { const fams = ['header', 'body'] .map((k) => config.get(`theme.fonts.${k}.family`)) .filter((f) => typeof f === 'string' && f.length > 0); if (fams.length > 0) { await Promise.race([ Promise.allSettled(fams.map((f) => __fonts.load(`16px "${f}"`).catch(() => {}))), new Promise((r) => setTimeout(r, 2000)), ]); } } // 'VESTRA's starting system has settled + unsettled worlds. globalThis.__ORBIT_DEV_SEED = 'VESTRA'; const gameConfig = createGameConfig(); gameConfig.scene = [GameScene]; // GameScene boots first if (typeof Phaser !== 'undefined') Phaser.NoAudioContext = true; const game = new Phaser.Game(gameConfig); window.game = game; /* ---- helpers ---------------------------------------------------------- */ const scene = () => { const g = window.game; if (!g) return null; const s = g.scene.getScene('GameScene'); return s && s.sys?.isActive && s.commsPanel ? s : null; }; const sleep = (ms) => new Promise((r) => setTimeout(r, ms)); /** Pump the (rAF-frozen) loop until `ok()` or the budget elapses. */ async function pump(s, ok, ms = 4000) { const t0 = Date.now(); while (!ok() && Date.now() - t0 < ms) { s.game.loop.step(performance.now()); await sleep(0); } return ok(); } async function ready() { const t0 = Date.now(); for (;;) { const s = scene(); if (s && s.ship) return s; if (Date.now() - t0 > 45000) throw new Error('game never became ready'); await sleep(50); } } /** A real mouse click at viewport (client) coordinates (v4's input * manager listens for classic mouse events on the canvas). A leading * mousemove refreshes the pointer so the mousedown's world coords are * current (Phaser caches them per frame). * NOTE: camera placement must use cam.setScroll() — plain .scrollX * assignment does not stick — and the scene's follow must be off first. */ function clickAtClient(clientX, clientY) { const canvas = document.querySelector('canvas'); const mk = (type, buttons) => new MouseEvent(type, { bubbles: true, composed: true, cancelable: true, view: window, clientX, clientY, button: 0, buttons, }); canvas.dispatchEvent(mk('mousemove', 0)); // The pointer's world coords are recomputed per frame, not on the event: // a step between the move and the down refreshes them against the // camera as it stands (the handler reads pointer.worldX on the down). window.game.loop.step(performance.now()); canvas.dispatchEvent(mk('mousedown', 1)); canvas.dispatchEvent(mk('mouseup', 0)); } /** ESC (Phaser's keyboard plugin reads the legacy keyCode). */ function pressEscape() { const ev = new KeyboardEvent('keydown', { key: 'Escape', code: 'Escape', bubbles: true, composed: true, cancelable: true, }); Object.defineProperty(ev, 'keyCode', { get: () => 27 }); Object.defineProperty(ev, 'which', { get: () => 27 }); window.dispatchEvent(ev); } /** World → client coords through the camera + the CSS-scaled canvas rect. */ function worldToClient(s, wx, wy) { const cam = s.cameras.main; const rect = document.querySelector('canvas').getBoundingClientRect(); const scaleX = rect.width / s.scale.width; const scaleY = rect.height / s.scale.height; return { clientX: rect.left + (wx - cam.scrollX) * scaleX, clientY: rect.top + (wy - cam.scrollY) * scaleY, }; } /* ---- the test --------------------------------------------------------- */ window.__CLICK_TEST__ = async () => { const out = { steps: [] }; const s = await ready(); const step = (name, pass, extra = {}) => out.steps.push({ name, pass, ...extra }); const cam = s.cameras.main; s.cameraFollowShip = false; s.hideHint?.(); s.ship.stop(); // A settled system planet (home is PINNED at +20 — the disabled-state // test needs a place whose standing we can set). const settledPlanet = () => { for (const p of s.systemPlanets) { const rec = (s.systemContent.planets ?? []).find((r) => r.name === p.discoveryName); if (rec && (s.systemContent.settlements ?? []).some((x) => x.anchor?.type === 'planet' && x.anchor?.ordinal === rec.ordinal)) return p; } return null; }; const planet = settledPlanet(); if (!planet) return { steps, allPass: false, note: 'no settled planet in this system' }; const frame = (wx, wy, sx, sy) => { cam.setScroll(wx - sx, wy - sy); // setScroll — .scrollX= does not stick s.cameraFollowShip = false; }; // ---- 1. Real click ON the home world → fly + panel opens ------------- const hx = 0, hy = 0, hsx = 640, hsy = 300; s.ship.x = 700; s.ship.y = 650; frame(hx, hy, hsx, hsy); await pump(s, () => !s.commsPanel.isOpen, 1000); const c1 = worldToClient(s, hx, hy); clickAtClient(c1.clientX, c1.clientY); await pump(s, () => s.commsPanel.isOpen && s.commsPanel.alpha >= 0.999 && s.commsPanel.nameDec === null, 4000); step( 'click object → panel opens + ship retargeted', s.commsPanel.isOpen && s.ship.target !== null && s.commsPanel.lastTarget?.name === s.planet.discoveryName.toUpperCase(), { shipTarget: s.ship.target, panelName: s.commsPanel._name }, ); // Button centers in panel-local y: btn1 = +23.5, btn2 = +76.5. const btn1 = () => worldToClient(s, s.commsPanel.x, s.commsPanel.y + 23.5); const btn2 = () => worldToClient(s, s.commsPanel.x, s.commsPanel.y + 76.5); // ---- 2. Click CANCEL → closes ---------------------------------------- const b2 = btn2(); clickAtClient(b2.clientX, b2.clientY); await pump(s, () => !s.commsPanel.isOpen, 2500); step('click CANCEL → closes', !s.commsPanel.isOpen); // ---- 3. Standing ≤ −4 → REQUEST LANDING is inert --------------------- const t = s.commsTargetFor(planet); s.reputation.set(t.key, -8); frame(planet.x, planet.y, 640, 300); await pump(s, () => !s.commsPanel.isOpen, 500); s.openCommsPanel(planet, { worldX: planet.x, worldY: planet.y, x: 640, y: 300 }); await pump(s, () => s.commsPanel.isOpen && s.commsPanel.alpha >= 0.999, 4000); step('standing −8 → REQUEST LANDING disabled', s.commsPanel.btn1.disabled, { rep: s.reputation.standingFor(t.key) }); const b3 = btn1(); clickAtClient(b3.clientX, b3.clientY); await sleep(300); // let the input pass settle step('click disabled REQUEST LANDING → stays open', s.commsPanel.isOpen); s.commsPanel.close(); await pump(s, () => s.commsPanel.state === 'closed', 2000); // ---- 4. Click OUTSIDE (open space corner) → closes -------------------- s.commsPanel.close(); await pump(s, () => s.commsPanel.state === 'closed', 2000); frame(hx, hy, 640, 300); // back to the home framing (step 3 moved it) s.openCommsPanel(s.planet, { worldX: hx, worldY: hy, x: 640, y: 300 }); await pump(s, () => s.commsPanel.isOpen, 1500); const corners = [ { x: -580, y: -240 }, { x: 580, y: -240 }, { x: -580, y: 460 }, { x: 580, y: 460 }, ]; const corner = corners.find((p) => s.worldObjectAt(p.x, p.y) === null) ?? null; if (corner) { const cc = worldToClient(s, corner.x, corner.y); clickAtClient(cc.clientX, cc.clientY); await pump(s, () => !s.commsPanel.isOpen, 2500); } step('click outside → closes', !s.commsPanel.isOpen, { corner }); // ---- 5. ESC closes ---------------------------------------------------- s.openCommsPanel(s.planet, { worldX: hx, worldY: hy, x: 640, y: 300 }); await pump(s, () => s.commsPanel.isOpen, 1500); pressEscape(); await pump(s, () => !s.commsPanel.isOpen, 2500); step('ESC → closes', !s.commsPanel.isOpen); // ---- 6. Enabled REQUEST LANDING fires the seam + closes --------------- // (Home is pinned +20 — canLand is true; the seam logs the action.) let logged = ''; const orig = console.info; console.info = (...a) => { logged += ' ' + a.join(' '); orig(...a); }; s.openCommsPanel(s.planet, { worldX: hx, worldY: hy, x: 640, y: 300 }); await pump(s, () => s.commsPanel.isOpen && s.commsPanel.alpha >= 0.999, 4000); const b6 = btn1(); clickAtClient(b6.clientX, b6.clientY); await pump(s, () => !s.commsPanel.isOpen, 2500); console.info = orig; step( 'click REQUEST LANDING (enabled) → seam fires + closes', !s.commsPanel.isOpen && logged.includes('request-landing'), { logged: logged.trim() }, ); out.allPass = out.steps.every((x) => x.pass); return out; };