orbit/dev/comms-click.mjs

259 lines
10 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 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;
};