259 lines
10 KiB
JavaScript
259 lines
10 KiB
JavaScript
/**
|
||
* Dev: real-input test for the CommsPanel interaction paths:
|
||
* 1. click an object → panel opens, ship does NOT fly (a planet
|
||
* click is not a fly-here — it is the
|
||
* landing-request window, not a move)
|
||
* 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 → panel opens, ship stays put --
|
||
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 does NOT fly',
|
||
s.commsPanel.isOpen && s.ship.target === null && s.commsPanel._name === s.planet.discoveryName.toUpperCase(),
|
||
{ shipTarget: s.ship.target, panelName: s.commsPanel._name, planetName: s.planet.discoveryName },
|
||
);
|
||
|
||
// 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. For a planet target the seam is
|
||
// startLanding() — this harness registers ONLY GameScene (no
|
||
// SurfaceScene), so stub it and assert the seam fired with the
|
||
// planet's payload (the landing-click.mjs harness runs the real one).
|
||
let landingArgs = null;
|
||
const origLanding = s.startLanding;
|
||
s.startLanding = (t) => { landingArgs = t; };
|
||
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);
|
||
s.startLanding = origLanding;
|
||
step(
|
||
'click REQUEST LANDING (enabled) → seam fires + closes',
|
||
!s.commsPanel.isOpen &&
|
||
landingArgs?.name === s.planet.discoveryName &&
|
||
landingArgs?.isPlanet === true,
|
||
{ landingArgs: landingArgs && { name: landingArgs.name, isPlanet: landingArgs.isPlanet, frame: landingArgs.frame } },
|
||
);
|
||
|
||
out.allPass = out.steps.every((x) => x.pass);
|
||
return out;
|
||
};
|