/* Measures click→destination accuracy in the live game. * usage: node tools/cdp_move_accuracy.mjs [port] */ const port = process.argv[2] || '9370'; const targets = await (await fetch(`http://127.0.0.1:${port}/json`)).json(); const page = targets.find(t => t.type === 'page'); const ws = new WebSocket(page.webSocketDebuggerUrl); let id = 0; const pending = new Map(); const send = (m, p = {}) => new Promise((res, rej) => { const i = ++id; pending.set(i, { res, rej }); ws.send(JSON.stringify({ id: i, method: m, params: p })); }); ws.onmessage = ev => { const msg = JSON.parse(ev.data); if (msg.id && pending.has(msg.id)) { const { res, rej } = pending.get(msg.id); pending.delete(msg.id); msg.error ? rej(new Error(msg.error.message)) : res(msg.result); } }; await new Promise(r => { ws.onopen = r; }); const ev = async expr => (await send('Runtime.evaluate', { expression: expr, returnByValue: true })).result.value; for (let i = 0; i < 90; i++) { const s = await ev(`(document.getElementById('status')||{textContent:''}).textContent`); if (/DONE|DRIVE_ERROR/.test(s)) break; await new Promise(r => setTimeout(r, 1000)); } /* make sure the camera is live (RAF or pump renders) */ console.log('env:', await ev(`(() => { const D = document.getElementById('g').contentWindow.D2; return JSON.stringify({ cam: [+D.render.cam.x.toFixed(1), +D.render.cam.y.toFixed(1)], player: [+D.game.player.x.toFixed(1), +D.game.player.y.toFixed(1)] }); })()`)); const runCase = await send('Runtime.evaluate', { awaitPromise: true, returnByValue: true, expression: `(async () => { const w = document.getElementById('g').contentWindow; const D = w.D2, cv = w.document.getElementById('game-canvas'); const r = cv.getBoundingClientRect(); const TW = 64, TH = 32; const toScreen = (wx, wy) => { const cam = D.render.cam, z = cam.zoom || 1; const dx = wx - cam.x, dy = wy - cam.y; return { x: r.left + cv.clientWidth / 2 + (dx - dy) * TW / 2 * z, y: r.top + cv.clientHeight / 2 + (dx + dy) * TH / 2 * z }; }; const sleep = ms => new Promise(r2 => setTimeout(r2, ms)); const results = []; const cases = [[2, 1], [-3, 2], [4, -2], [-2, -3]]; for (const [ox, oy] of cases) { const p = D.game.player; const want = { x: Math.floor(p.x + ox) + .5, y: Math.floor(p.y + oy) + .5 }; if (!D.world || !D.game.world.isWalkable(Math.floor(want.x), Math.floor(want.y))) { results.push({ ox, oy, skipped: 'not walkable' }); continue; } const sp = toScreen(want.x, want.y); /* settle camera right before measuring */ D.render.frame(D.game, 1 / 60); const o = { bubbles: true, button: 0, clientX: sp.x, clientY: sp.y }; cv.dispatchEvent(new w.MouseEvent('mousemove', o)); cv.dispatchEvent(new w.MouseEvent('mousedown', o)); w.dispatchEvent(new w.MouseEvent('mouseup', { bubbles: true, button: 0 })); await sleep(3500); const q = D.game.player; results.push({ ox, oy, want: [+want.x.toFixed(2), +want.y.toFixed(2)], got: [+q.x.toFixed(2), +q.y.toFixed(2)], err: +Math.hypot(q.x - want.x, q.y - want.y).toFixed(2), hoverTaken: !!(D.game.hoverEntity) }); } return JSON.stringify(results); })()`, }); console.log('accuracy:', runCase.result ? runCase.result.value : JSON.stringify(runCase)); ws.close();