diablo2-web/scripts/verify-vendor-ui.ts

228 lines
7.4 KiB
TypeScript

import { spawn, type ChildProcess } from 'node:child_process'
import { existsSync, mkdirSync, writeFileSync } from 'node:fs'
import { join, resolve } from 'node:path'
import { createServer, type ViteDevServer } from 'vite'
import { ensurePacksAvailable } from './verify-ground-combat-browser.ts'
const sleep = (ms: number) => new Promise<void>(r => setTimeout(r, ms))
const CHROME_CANDIDATES = [
'/usr/bin/google-chrome',
'/usr/local/google/home/taodao/.cache/ms-playwright/chromium-1228/chrome-linux64/chrome',
'/root/.cache/ms-playwright/chromium-1223/chrome-linux64/chrome',
]
function resolveChromeBinary(): string {
for (const bin of CHROME_CANDIDATES) {
if (existsSync(bin)) return bin
}
throw new Error('No accessible Chromium binary found')
}
async function main() {
const outputDir =
process.env.VENDOR_AUDIT_OUT ??
'/usr/local/google/home/taodao/.gemini/jetski/brain/ba5784fe-f165-4cff-aa7e-c3cfdf22c420'
mkdirSync(outputDir, { recursive: true })
ensurePacksAvailable()
const port = 5419
const viteServer: ViteDevServer = await createServer({
root: resolve('.'),
logLevel: 'silent',
server: { host: '127.0.0.1', port, strictPort: false },
})
await viteServer.listen()
const actualPort = (viteServer.httpServer?.address() as any)?.port ?? port
const targetUrl = `http://127.0.0.1:${actualPort}/acts.html?act=1&level=1-act-1-town-1`
const chromeBin = resolveChromeBinary()
const debugPort = 9519
const chromeProc: ChildProcess = spawn(
chromeBin,
[
'--headless=new',
'--no-sandbox',
'--disable-dev-shm-usage',
'--window-size=1280,840',
'--enable-webgl',
'--ignore-gpu-blocklist',
'--use-gl=angle',
'--use-angle=swiftshader',
'--no-proxy-server',
`--remote-debugging-port=${debugPort}`,
targetUrl,
],
{ stdio: ['ignore', 'ignore', 'pipe'] },
)
let ws: WebSocket | null = null
try {
let wsDebuggerUrl: string | null = null
for (let i = 0; i < 80; i++) {
await sleep(150)
try {
const res = await fetch(`http://127.0.0.1:${debugPort}/json`)
const pages = (await res.json()) as any[]
const page = pages.find(p => p.type === 'page' && p.url.includes('acts.html'))
if (page?.webSocketDebuggerUrl) {
wsDebuggerUrl = page.webSocketDebuggerUrl
break
}
} catch {}
}
if (!wsDebuggerUrl) throw new Error('Failed to attach to Chrome CDP')
ws = new WebSocket(wsDebuggerUrl)
await new Promise((res, rej) => {
ws!.onopen = () => res(true)
ws!.onerror = rej
})
let cmdId = 1
const pending = new Map<number, (res: any) => void>()
ws.onmessage = (event: MessageEvent) => {
const msg = JSON.parse(String(event.data))
if (msg.id && pending.has(msg.id)) {
pending.get(msg.id)!(msg)
pending.delete(msg.id)
}
}
const cdp = <T = any>(method: string, params: Record<string, unknown> = {}): Promise<T> =>
new Promise((resolve, reject) => {
const id = cmdId++
pending.set(id, (msg: any) => {
if (msg.error) reject(new Error(JSON.stringify(msg.error)))
else resolve(msg.result as T)
})
ws!.send(JSON.stringify({ id, method, params }))
})
const evalJs = async <T = any>(expression: string): Promise<T> => {
const res = await cdp<{ result: { value: T }; exceptionDetails?: any }>('Runtime.evaluate', {
expression,
awaitPromise: true,
returnByValue: true,
})
if (res.exceptionDetails) {
throw new Error(`Eval error: ${JSON.stringify(res.exceptionDetails)}`)
}
return res.result.value
}
const takeScreenshot = async (name: string): Promise<string> => {
const shot = await cdp<{ data: string }>('Page.captureScreenshot', { format: 'png' })
const filePath = join(outputDir, `${name}.png`)
writeFileSync(filePath, Buffer.from(shot.data, 'base64'))
console.log(`Saved screenshot: ${filePath}`)
return filePath
}
await cdp('Runtime.enable')
await cdp('Page.enable')
for (let i = 0; i < 100; i++) {
const ready = await evalJs<boolean>(`Boolean(window.__d2webAct?.ready && window.__d2webHud?.ready)`)
if (ready) break
await sleep(200)
}
// 1. Open Charsi NPC Menu
const menuInfo = await evalJs(`(() => {
const hud = window.__d2webHudInstance;
hud.openNpcMenuByName('Charsi', 400, 220);
hud.render();
return {
npcName: hud.worldPanels.npcMenu?.npcName,
options: hud.worldPanels.npcMenu?.options?.map(o => o.labelZh),
};
})()`)
console.log('NPC Menu:', JSON.stringify(menuInfo))
await sleep(250)
await takeScreenshot('vendor_npc_menu_charsi')
// 2. Open Charsi Trade/Repair 4-tab 10x10 Vendor UI
const charsiInfo = await evalJs(`(() => {
const hud = window.__d2webHudInstance;
hud.charSheet.attrs.level = 24;
hud.openVendorSession('Charsi', 'trade', 0);
const wp = hud.worldPanels;
wp.activeVendorTab = 'armor';
const armorItems = wp.vendorTabPlacements.armor;
const docking = hud.getDockingLayout();
const deltaLeft = -docking.marginW;
if (armorItems.length > 0) {
const first = armorItems[0];
wp.hoveredVendorItem = {
item: first.item,
x: 80 + 17 + first.col * 29 + 14,
y: 60 + 64 + first.row * 29 + 14,
col: first.col,
row: first.row,
};
hud.inventory.hoveredItem = {
item: first.item,
x: wp.hoveredVendorItem.x + deltaLeft,
y: wp.hoveredVendorItem.y,
};
}
hud.render();
return {
leftPanel: hud.leftPanel,
rightPanel: hud.rightPanel,
armorCount: wp.vendorTabPlacements.armor.length,
weapons1Count: wp.vendorTabPlacements.weapons1.length,
miscCount: wp.vendorTabPlacements.misc.length,
firstItem: armorItems[0] ? { nameZh: armorItems[0].item.nameZh, buyCost: armorItems[0].item.buyCost } : null,
};
})()`)
console.log('Charsi Trade UI:', JSON.stringify(charsiInfo))
await sleep(250)
await takeScreenshot('vendor_trade_charsi')
// 3. Open Gheed Gamble UI
const gheedInfo = await evalJs(`(() => {
const hud = window.__d2webHudInstance;
hud.charSheet.attrs.level = 60;
hud.openVendorSession('Gheed', 'gamble', 1);
const wp = hud.worldPanels;
const gambleItems = wp.gamblePlacements;
const docking = hud.getDockingLayout();
const deltaLeft = -docking.marginW;
if (gambleItems.length > 0) {
const first = gambleItems[0];
wp.hoveredVendorItem = {
item: first.item,
x: 80 + 17 + first.col * 29 + 14,
y: 60 + 64 + first.row * 29 + 14,
col: first.col,
row: first.row,
};
hud.inventory.hoveredItem = {
item: first.item,
x: wp.hoveredVendorItem.x + deltaLeft,
y: wp.hoveredVendorItem.y,
};
}
hud.render();
return {
gambleCount: gambleItems.length,
firstGamble: gambleItems[0] ? { nameZh: gambleItems[0].item.nameZh, buyCost: gambleItems[0].item.buyCost } : null,
};
})()`)
console.log('Gheed Gamble UI:', JSON.stringify(gheedInfo))
await sleep(250)
await takeScreenshot('vendor_gamble_gheed')
} finally {
ws?.close()
chromeProc.kill('SIGKILL')
await viteServer.close()
}
}
main().catch(err => {
console.error(err)
process.exit(1)
})