diablo2-web/scripts/verify-automap.ts

724 lines
27 KiB
TypeScript
Raw 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.

/**
* Full-Spectrum 136-Level Isometric Automap & Minimap Verification (`scripts/verify-automap.ts`).
*
* Verifies:
* 1. All 8 baked automap DC6 sprite sheets (`MaxiMap`, `MaxiMapS`, `Act2Map`, `Act2MapS`,
* `Act4Map`, `Act4MapS`, `ExTnMap`, `ExTnMapS`) and manifest frame bounds.
* 2. All 136 level IDs (`1..136`, 365 baked scene variants in `samples/d2-packs`):
* - Non-empty automap cells (`floors + walls + objects + extras > 0`).
* - Every cell's `fSaved` (`0..3`) and `cellNo` within bounds of its target sheet.
* - Town pre-revealed `fSaved` extras for Lut Gholein (`40`), Pandemonium Fortress (`103`),
* and Harrogath (`109`).
* - `Minimap` (`reveal` + `draw`) execution in both Corner Minimap (`bMinimap=true`, `nDivisor=20`)
* and Full Automap (`bMinimap=false`, `nDivisor=10`) across all 136 levels (`1..136`),
* confirming isometric sprites (`spritesDrawn.total > 0`), 12-segment player crosshair
* (`crosshairsDrawn.player === 1`), and top-right HUD text (`hudText !== null`).
* 3. Headless Chrome CDP browser audit across Acts 1–5 towns, wildernesses, and dungeons
* (`LevelId = 1, 2, 8, 40, 47, 75, 103, 109`) in both Corner Minimap (`bMinimap=1`) and
* Full Automap (`bMinimap=0`) modes with zero console/page/WebGL errors.
*
* Usage:
* npx tsx scripts/verify-automap.ts
*/
import { spawn } from 'node:child_process'
import { readFile, stat } from 'node:fs/promises'
import { join } from 'node:path'
import { createServer } from 'vite'
import type { AutomapLayerData } from '../src/formats/automap-table.ts'
import type { AtlasFrame } from '../src/render/atlas.ts'
import type { AtlasHandle, SpriteRenderer } from '../src/render/renderer.ts'
import { AUTOMAP_UI_MANIFEST, type AutomapSpriteKey } from '../src/ui/baked-ui-meta.ts'
import { Minimap, type MinimapLevel, resolveAutomapSheetName } from '../src/ui/minimap.ts'
const PACK_DIR = process.argv[2] ?? 'samples/d2-packs'
let checks = 0
let failures = 0
function check(ok: boolean, message: string): void {
checks += 1
if (!ok) {
failures += 1
console.error(` FAIL ${message}`)
}
}
interface PackIndexEntry {
readonly act: number
readonly levelId: number
readonly slug: string
readonly label: string
readonly levelName: string
readonly path: string
readonly kind?: string
}
interface PackedSceneJson {
readonly act: number
readonly levelId: number
readonly levelName: string
readonly cellsX: number
readonly cellsY: number
readonly originX: number
readonly originY: number
readonly widthPx: number
readonly heightPx: number
readonly collision: {
readonly width: number
readonly height: number
readonly runs: readonly (readonly number[])[]
}
readonly spawn: readonly number[] | null
readonly entrances?: readonly { readonly x: number; readonly y: number; readonly toLevelId?: number }[]
readonly warps?: readonly { readonly x: number; readonly y: number; readonly toLevelId?: number }[]
readonly waypoints?: readonly { readonly x: number; readonly y: number; readonly waypointId?: number }[]
readonly automap?: AutomapLayerData
}
function createHeadlessMockRenderer(): SpriteRenderer {
return {
flush() {},
draw(_frame: AtlasFrame, _x: number, _y: number) {},
drawSolid(_x: number, _y: number, _w: number, _h: number) {},
drawSolidQuad(
_x0: number,
_y0: number,
_x1: number,
_y1: number,
_x2: number,
_y2: number,
_x3: number,
_y3: number,
) {},
} as unknown as SpriteRenderer
}
function registerSyntheticAtlases(minimap: Minimap): void {
const keys: readonly AutomapSpriteKey[] = [
'MaxiMap',
'MaxiMapS',
'Act2Map',
'Act2MapS',
'Act4Map',
'Act4MapS',
'ExTnMap',
'ExTnMapS',
]
for (const key of keys) {
const meta = minimap.getSheetMeta(key)
minimap.setAtlasHandle(key, {
texture: key as unknown as WebGLTexture,
width: meta.atlasWidth,
height: meta.atlasHeight,
} as AtlasHandle)
}
}
const sleep = (ms: number) => new Promise<void>(resolve => setTimeout(resolve, ms))
async function verifyAll136LevelsOffline(): Promise<Map<number, PackIndexEntry>> {
console.log('== 1. Verifying 8 Baked Automap DC6 Sprite Sheets ==')
const expectedSheets: readonly { key: AutomapSpriteKey; frames: number }[] = [
{ key: 'MaxiMap', frames: 1499 },
{ key: 'MaxiMapS', frames: 1499 },
{ key: 'Act2Map', frames: 40 },
{ key: 'Act2MapS', frames: 40 },
{ key: 'Act4Map', frames: 4 },
{ key: 'Act4MapS', frames: 4 },
{ key: 'ExTnMap', frames: 8 },
{ key: 'ExTnMapS', frames: 8 },
]
for (const spec of expectedSheets) {
const meta = AUTOMAP_UI_MANIFEST.atlases[spec.key]
check(
meta !== undefined && meta.frameCount === spec.frames && meta.frames.length === spec.frames,
`Sheet ${spec.key}: expected ${String(spec.frames)} frames, got ${String(meta?.frameCount)}`,
)
const assetStat = await stat(join('public', 'assets', 'ui', 'automap', `${spec.key}.png`)).catch(
() => null,
)
check(
assetStat !== null && assetStat.size > 0,
`Sheet PNG public/assets/ui/automap/${spec.key}.png exists (${String(assetStat?.size ?? 0)} bytes)`,
)
}
console.log('\n== 2. Verifying All 136 Level IDs (1..136) Across 365 Baked Scenes ==')
const indexRaw = await readFile(join(PACK_DIR, 'index.json'), 'utf8')
const index = JSON.parse(indexRaw) as { readonly levels: readonly PackIndexEntry[] }
const entriesByLevelId = new Map<number, PackIndexEntry[]>()
for (const entry of index.levels) {
const list = entriesByLevelId.get(entry.levelId) ?? []
list.push(entry)
entriesByLevelId.set(entry.levelId, list)
}
const representativeEntryByLevelId = new Map<number, PackIndexEntry>()
const renderer = createHeadlessMockRenderer()
const minimap = new Minimap()
registerSyntheticAtlases(minimap)
let totalScenesVerified = 0
let totalAutomapCellsVerified = 0
for (let levelId = 1; levelId <= 136; levelId += 1) {
const entries = entriesByLevelId.get(levelId) ?? []
check(entries.length > 0, `LevelId ${String(levelId)}: has at least 1 baked scene in index.json`)
if (entries.length === 0) continue
representativeEntryByLevelId.set(levelId, entries[0]!)
for (const entry of entries) {
const sceneRaw = await readFile(join(PACK_DIR, entry.path, 'scene.json'), 'utf8')
const scene = JSON.parse(sceneRaw) as PackedSceneJson
const automap = scene.automap
check(automap !== undefined, `${entry.path} (LevelId=${String(levelId)}): scene.automap is present`)
if (!automap) continue
const cellCount =
automap.floors.length + automap.walls.length + automap.objects.length + automap.extras.length
check(
cellCount > 0,
`${entry.path} (LevelId=${String(levelId)}): non-empty automap cells (${String(cellCount)})`,
)
totalAutomapCellsVerified += cellCount
totalScenesVerified += 1
let invalidCells = 0
for (const cell of [
...automap.floors,
...automap.walls,
...automap.objects,
...automap.extras,
]) {
const sheetFull = resolveAutomapSheetName(cell.fSaved ?? 0, false)
const sheetHalf = resolveAutomapSheetName(cell.fSaved ?? 0, true)
const metaFull = AUTOMAP_UI_MANIFEST.atlases[sheetFull]
const metaHalf = AUTOMAP_UI_MANIFEST.atlases[sheetHalf]
if (
!metaFull ||
!metaHalf ||
cell.cellNo < 0 ||
cell.cellNo >= metaFull.frames.length ||
cell.cellNo >= metaHalf.frames.length
) {
invalidCells += 1
}
}
check(
invalidCells === 0,
`${entry.path} (LevelId=${String(levelId)}): all ${String(cellCount)} cells have valid frame bounds`,
)
if (levelId === 40) {
check(
automap.extras.some(c => c.fSaved === 1),
`${entry.path} (Lut Gholein LevelId=40): contains fSaved=1 Act2Map town extras`,
)
} else if (levelId === 103) {
check(
automap.extras.some(c => c.fSaved === 2),
`${entry.path} (Pandemonium Fortress LevelId=103): contains fSaved=2 Act4Map town extras`,
)
} else if (levelId === 109) {
check(
automap.extras.some(c => c.fSaved === 3),
`${entry.path} (Harrogath LevelId=109): contains fSaved=3 ExTnMap town extras`,
)
}
}
// Exercise Minimap render pipeline in both Corner Minimap (bMinimap=true) and Full Automap (bMinimap=false)
// for the primary variant of each of the 136 levels.
const primaryEntry = entries[0]!
const primaryScene = JSON.parse(
await readFile(join(PACK_DIR, primaryEntry.path, 'scene.json'), 'utf8'),
) as PackedSceneJson
const automap = primaryScene.automap!
// Pick a reference cell from the level's automap so the camera is centered over real geometry
const refCell =
automap.floors[0] ?? automap.walls[0] ?? automap.objects[0] ?? automap.extras[0]!
const playerCellX = Math.max(
0,
Math.min(primaryScene.cellsX - 1, refCell.cellX ?? Math.floor(primaryScene.cellsX / 2)),
)
const playerCellY = Math.max(
0,
Math.min(primaryScene.cellsY - 1, refCell.cellY ?? Math.floor(primaryScene.cellsY / 2)),
)
const playerWorldX = primaryScene.originX + (playerCellX - playerCellY) * 80
const playerWorldY = primaryScene.originY + (playerCellX + playerCellY) * 40
const levelModel: MinimapLevel = {
key: `verify:${primaryEntry.path}`,
cellsX: primaryScene.cellsX,
cellsY: primaryScene.cellsY,
blocked: new Uint8Array(primaryScene.collision.width * primaryScene.collision.height),
gridWidth: primaryScene.collision.width,
originX: primaryScene.originX,
originY: primaryScene.originY,
automap,
levelId,
levelName: primaryScene.levelName,
difficulty: 'Normal',
markers: [
...(primaryScene.warps ?? []).map(w => ({
cellX: Math.floor(w.x / 5),
cellY: Math.floor(w.y / 5),
kind: 'exit' as const,
targetName: `Level ${String(w.toLevelId ?? 0)}`,
})),
],
}
minimap.reveal(levelModel, playerCellX, playerCellY, playerWorldX, playerWorldY, true)
// 1. Corner Minimap (bMinimap = true, nDivisor = 20)
minimap.mode = 'minimap'
minimap.draw(
renderer,
levelModel,
playerCellX,
playerCellY,
{ x: playerWorldX, y: playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{ playerWorldX, playerWorldY, levelId },
)
check(
minimap.lastRenderStats.divisor === 20 &&
minimap.lastRenderStats.spritesDrawn.total > 0 &&
minimap.lastRenderStats.crosshairsDrawn.player === 1 &&
minimap.lastRenderStats.hudText?.levelName === primaryScene.levelName,
`LevelId ${String(levelId)} (${primaryScene.levelName}): Corner Minimap (bMinimap=1, div=20) drew ${String(minimap.lastRenderStats.spritesDrawn.total)} sprites & player crosshair`,
)
// 2. Full Automap (bMinimap = false, nDivisor = 10)
minimap.mode = 'full'
minimap.draw(
renderer,
levelModel,
playerCellX,
playerCellY,
{ x: playerWorldX, y: playerWorldY, zoom: 1 },
{ width: 800, height: 600 },
{ playerWorldX, playerWorldY, levelId },
)
check(
minimap.lastRenderStats.divisor === 10 &&
minimap.lastRenderStats.spritesDrawn.total > 0 &&
minimap.lastRenderStats.crosshairsDrawn.player === 1 &&
minimap.lastRenderStats.hudText?.levelName === primaryScene.levelName,
`LevelId ${String(levelId)} (${primaryScene.levelName}): Full Automap (bMinimap=0, div=10) drew ${String(minimap.lastRenderStats.spritesDrawn.total)} sprites & player crosshair`,
)
}
console.log(
` Verified 136/136 level IDs (${String(totalScenesVerified)} scenes, ${String(totalAutomapCellsVerified)} total automap cells) in both Corner Minimap and Full Automap modes.`,
)
return representativeEntryByLevelId
}
async function verifyHeadlessBrowserCdp(
representativeEntryByLevelId: Map<number, PackIndexEntry>,
): Promise<void> {
console.log('\n== 3. Launching Headless Chrome CDP Verification (Acts 1–5 Towns, Wildernesses & Dungeons) ==')
const server = await createServer({
root: process.cwd(),
server: { port: 0, host: '127.0.0.1' },
})
await server.listen()
const address = server.httpServer?.address()
const port = typeof address === 'object' && address ? address.port : 5173
const baseUrl = `http://127.0.0.1:${port}`
console.log(` Vite dev server listening at ${baseUrl}`)
const debugPort = 9244
const chromeProc = spawn('/usr/bin/google-chrome', [
'--headless=new',
`--remote-debugging-port=${debugPort}`,
'--no-sandbox',
'--disable-dev-shm-usage',
'--enable-webgl',
'--ignore-gpu-blocklist',
'--use-gl=angle',
'--use-angle=swiftshader',
'--window-size=1280,840',
`${baseUrl}/acts.html?act=1`,
])
try {
let wsUrl: string | null = null
for (let i = 0; i < 60; i += 1) {
await sleep(300)
try {
const res = await fetch(`http://127.0.0.1:${debugPort}/json/list`)
const pages = (await res.json()) as {
type: string
url: string
webSocketDebuggerUrl?: string
}[]
const page = pages.find(p => p.type === 'page' && p.url.includes('acts.html'))
if (page?.webSocketDebuggerUrl) {
wsUrl = page.webSocketDebuggerUrl
break
}
} catch {
// Wait for Chrome CDP
}
}
if (!wsUrl) {
throw new Error('Failed to connect to Chrome CDP')
}
const ws = new WebSocket(wsUrl)
await new Promise<void>(resolve => {
ws.onopen = () => resolve()
})
const consoleErrors: string[] = []
const pageExceptions: string[] = []
let idCounter = 1
const sendCommand = <T = unknown>(
method: string,
params: Record<string, unknown> = {},
): Promise<T> =>
new Promise((resolve, reject) => {
const id = idCounter++
const handler = (event: MessageEvent) => {
const msg = JSON.parse(String(event.data))
if (msg.id === id) {
ws.removeEventListener('message', handler)
if (msg.error) reject(new Error(JSON.stringify(msg.error)))
else resolve(msg.result as T)
}
}
ws.addEventListener('message', handler)
ws.send(JSON.stringify({ id, method, params }))
})
ws.addEventListener('message', (event: MessageEvent) => {
try {
const msg = JSON.parse(String(event.data))
if (msg.method === 'Runtime.exceptionThrown') {
pageExceptions.push(JSON.stringify(msg.params))
} else if (msg.method === 'Runtime.consoleAPICalled' && msg.params?.type === 'error') {
const text = (msg.params.args ?? [])
.map((a: { value?: unknown; description?: string }) => a.value ?? a.description ?? '')
.join(' ')
if (!text.includes('favicon.ico')) {
consoleErrors.push(text)
}
}
} catch {
// ignore parse errors
}
})
const evalJs = async <T = unknown>(expression: string): Promise<T> => {
const res = await sendCommand<{ result: { value: T }; exceptionDetails?: unknown }>(
'Runtime.evaluate',
{
expression,
awaitPromise: true,
returnByValue: true,
},
)
if (res.exceptionDetails) {
throw new Error(`JS exception: ${JSON.stringify(res.exceptionDetails)}`)
}
return res.result.value
}
await sendCommand('Runtime.enable')
await sendCommand('Page.enable')
const auditLevelIds = [1, 2, 8, 40, 47, 75, 103, 109]
// Wait for initial Act 1 Town boot
let initialReady = false
for (let i = 0; i < 120; i += 1) {
await sleep(250)
try {
initialReady = await evalJs<boolean>(
`Boolean(window.__d2webActs?.ready && window.__d2webHud?.ready && window.__d2webActs?.automapAtlasesLoaded === 8 && (window.__d2webActs?.automapSpritesDrawn?.total ?? 0) > 0)`,
)
if (initialReady) break
} catch {
// Boot in progress
}
}
check(initialReady, 'Headless Chrome initial Act 1 Town scene and 8 automap atlases ready')
for (const levelId of auditLevelIds) {
const entry = representativeEntryByLevelId.get(levelId)
if (!entry) continue
const currentLevel = await evalJs<string>(`window.__d2webActs?.level ?? ''`)
if (currentLevel !== entry.levelName) {
await sleep(300) // Allow previous WARP_FADE_MS (250ms) fade-in to clear `travelling` guard
let transitioned = false
for (let i = 0; i < 80; i += 1) {
if (i % 5 === 0) {
await evalJs(`(() => {
if (window.__d2webActs?.level !== ${JSON.stringify(entry.levelName)}) {
window.__d2webHudInstance?.callbacks?.onWaypointTeleport(${String(entry.act)}, ${JSON.stringify(entry.slug)}, ${String(entry.levelId)});
}
})()`)
}
await sleep(150)
transitioned = await evalJs<boolean>(
`Boolean(window.__d2webActs?.level === ${JSON.stringify(entry.levelName)} && (window.__d2webActs?.automapSpritesDrawn?.total ?? 0) > 0)`,
)
if (transitioned) break
}
check(transitioned, `CDP in-engine transition to LevelId=${String(levelId)} (${entry.levelName})`)
}
// Ensure Corner Minimap mode (bMinimap=1, divisor=20)
await evalJs(`(() => {
const m = window.__d2webMinimap;
if (m) {
m.visible = true;
m.mode = 'minimap';
m.resetPan();
}
if (window.__d2webHudInstance) {
window.__d2webHudInstance.leftPanel = 'none';
window.__d2webHudInstance.rightPanel = 'none';
}
})()`)
for (let i = 0; i < 40; i += 1) {
await sleep(50)
const synced = await evalJs<boolean>(
`Boolean(window.__d2webActs?.automapMode === 'minimap' && window.__d2webActs?.automapDivisor === 20 && window.__d2webActs?.automapViewport?.width === 279 && (window.__d2webActs?.automapSpritesDrawn?.total ?? 0) > 0)`,
)
if (synced) break
}
const miniTelemetry = await evalJs<{
level: string
minimapVisible: boolean
automapMode: string
automapDivisor: number
automapAtlasesLoaded: number
automapCellCounts: { floors: number; walls: number; objects: number; extras: number; total: number }
automapRevealedRooms: number
automapSpritesDrawn: { total: number }
automapCrosshairsDrawn: { player: number; total: number }
automapHudText: { levelName: string; difficulty: string } | null
automapViewport: { width: number; height: number; x: number; y: number }
}>(`(() => {
const s = window.__d2webActs;
return {
level: s.level,
minimapVisible: s.minimapVisible,
automapMode: s.automapMode,
automapDivisor: s.automapDivisor,
automapAtlasesLoaded: s.automapAtlasesLoaded,
automapCellCounts: s.automapCellCounts,
automapRevealedRooms: s.automapRevealedRooms,
automapSpritesDrawn: s.automapSpritesDrawn,
automapCrosshairsDrawn: s.automapCrosshairsDrawn,
automapHudText: s.automapHudText,
automapViewport: s.automapViewport,
};
})()`)
check(
miniTelemetry.minimapVisible === true &&
miniTelemetry.automapMode === 'minimap' &&
miniTelemetry.automapDivisor === 20 &&
miniTelemetry.automapAtlasesLoaded === 8 &&
miniTelemetry.automapCellCounts.total > 0 &&
miniTelemetry.automapRevealedRooms > 0 &&
miniTelemetry.automapSpritesDrawn.total > 0 &&
miniTelemetry.automapCrosshairsDrawn.player === 1 &&
miniTelemetry.automapHudText !== null &&
miniTelemetry.automapViewport.width === 279 &&
miniTelemetry.automapViewport.height === 225,
`CDP LevelId=${String(levelId)} (${entry.levelName}): Corner Minimap (div=20, 279x225) rendered ${String(miniTelemetry.automapSpritesDrawn?.total)} sprites, ${String(miniTelemetry.automapCrosshairsDrawn?.total)} crosshairs, HUD="${miniTelemetry.automapHudText?.levelName ?? ''}"`,
)
// Toggle to Full Automap ('V') and test panel shift + arrow panning
await evalJs(`(() => {
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'v', bubbles: true }));
})()`)
for (let i = 0; i < 40; i += 1) {
await sleep(50)
const synced = await evalJs<boolean>(
`Boolean(window.__d2webActs?.automapMode === 'full' && window.__d2webActs?.automapDivisor === 10 && window.__d2webActs?.automapViewport?.width > 279 && (window.__d2webActs?.automapSpritesDrawn?.total ?? 0) > 0)`,
)
if (synced) break
}
const fullTelemetry = await evalJs<{
automapMode: string
automapDivisor: number
automapSpritesDrawn: { total: number }
automapCrosshairsDrawn: { player: number; total: number }
automapViewport: { width: number; height: number; centerShiftX: number; panX: number; panY: number }
}>(`(() => {
const s = window.__d2webActs;
return {
automapMode: s.automapMode,
automapDivisor: s.automapDivisor,
automapSpritesDrawn: s.automapSpritesDrawn,
automapCrosshairsDrawn: s.automapCrosshairsDrawn,
automapViewport: s.automapViewport,
};
})()`)
check(
fullTelemetry.automapMode === 'full' &&
fullTelemetry.automapDivisor === 10 &&
fullTelemetry.automapSpritesDrawn.total > 0 &&
fullTelemetry.automapCrosshairsDrawn.player === 1,
`CDP LevelId=${String(levelId)} (${entry.levelName}): Full Automap (div=10) rendered ${String(fullTelemetry.automapSpritesDrawn?.total)} sprites`,
)
console.log(
` ok CDP LevelId=${String(levelId)} (${entry.levelName}): minimap=${String(miniTelemetry.automapSpritesDrawn.total)} sprites, full=${String(fullTelemetry.automapSpritesDrawn.total)} sprites`,
)
}
// Interactive hotkeys & panel shift verification in Full Automap mode
const interactiveAudit = await evalJs<{
panAfterArrows: { panX: number; panY: number }
panAfterHome: { panX: number; panY: number }
fadeAfterF9: number
namesAfterF10: boolean
leftPanelShiftX: number
rightPanelShiftX: number
visibleAfterTabOff: boolean
visibleAfterTabOn: boolean
}>(`(async () => {
const waitFrames = () => new Promise(r => requestAnimationFrame(() => requestAnimationFrame(r)));
const m = window.__d2webMinimap;
const hud = window.__d2webHudInstance;
m.visible = true;
m.mode = 'full';
m.resetPan();
hud.leftPanel = 'none';
hud.rightPanel = 'none';
await waitFrames();
// Arrow key panning (+8px right, +8px down)
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowDown', bubbles: true }));
const panAfterArrows = { panX: m.panX, panY: m.panY };
// Home reset
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
const panAfterHome = { panX: m.panX, panY: m.panY };
// F9 fade cycle & F10 names toggle
const initialFade = m.fadeState;
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'F9', bubbles: true }));
const fadeAfterF9 = m.fadeState;
const initialNames = m.showNames;
window.dispatchEvent(new KeyboardEvent('keydown', { key: 'F10', bubbles: true }));
const namesAfterF10 = m.showNames !== initialNames;
// Left panel open -> +160px centerShiftX
hud.leftPanel = 'char';
hud.rightPanel = 'none';
await waitFrames();
const leftPanelShiftX = window.__d2webActs?.automapViewport?.centerShiftX ?? 0;
// Right panel open -> -160px centerShiftX
hud.leftPanel = 'none';
hud.rightPanel = 'inv';
await waitFrames();
const rightPanelShiftX = window.__d2webActs?.automapViewport?.centerShiftX ?? 0;
hud.rightPanel = 'none';
// Tab toggle off & on
m.toggle();
const visibleAfterTabOff = m.visible;
m.toggle();
const visibleAfterTabOn = m.visible;
void initialFade;
return {
panAfterArrows,
panAfterHome,
fadeAfterF9,
namesAfterF10,
leftPanelShiftX,
rightPanelShiftX,
visibleAfterTabOff,
visibleAfterTabOn,
};
})()`)
check(
interactiveAudit.panAfterArrows.panX === 8 && interactiveAudit.panAfterArrows.panY === 8,
`CDP Full Automap arrow panning: expected (8, 8), got (${String(interactiveAudit.panAfterArrows.panX)}, ${String(interactiveAudit.panAfterArrows.panY)})`,
)
check(
interactiveAudit.panAfterHome.panX === 0 && interactiveAudit.panAfterHome.panY === 0,
`CDP Full Automap Home pan reset: expected (0, 0), got (${String(interactiveAudit.panAfterHome.panX)}, ${String(interactiveAudit.panAfterHome.panY)})`,
)
check(
interactiveAudit.fadeAfterF9 === 1,
`CDP F9 fade cycle advanced fadeState to ${String(interactiveAudit.fadeAfterF9)}`,
)
check(interactiveAudit.namesAfterF10 === true, 'CDP F10 toggled showNames state')
check(
interactiveAudit.leftPanelShiftX === 160,
`CDP Full Automap left panel open shifted center by +160px (got ${String(interactiveAudit.leftPanelShiftX)})`,
)
check(
interactiveAudit.rightPanelShiftX === -160,
`CDP Full Automap right panel open shifted center by -160px (got ${String(interactiveAudit.rightPanelShiftX)})`,
)
check(
interactiveAudit.visibleAfterTabOff === false && interactiveAudit.visibleAfterTabOn === true,
'CDP Tab toggled automap visibility off and back on',
)
const unexpectedConsoleErrors = consoleErrors.filter(
msg => !msg.includes('monster art missing at draw time'),
)
if (unexpectedConsoleErrors.length > 0) {
for (const err of unexpectedConsoleErrors) {
console.error(' [CDP console.error]:', err)
}
}
check(
unexpectedConsoleErrors.length === 0,
`Zero unexpected console.error messages during CDP audit (got ${String(unexpectedConsoleErrors.length)})`,
)
check(
pageExceptions.length === 0,
`Zero uncaught page exceptions during CDP audit (got ${String(pageExceptions.length)})`,
)
ws.close()
} finally {
chromeProc.kill()
await server.close()
}
}
async function main(): Promise<void> {
const representativeEntryByLevelId = await verifyAll136LevelsOffline()
await verifyHeadlessBrowserCdp(representativeEntryByLevelId)
console.log(`\n${String(checks - failures)}/${String(checks)} checks passed`)
if (failures > 0) {
process.exit(1)
}
}
main().catch(err => {
console.error('verify-automap failed:', err)
process.exit(1)
})