diablo2-web/scripts/forensic-audit-m3.ts

243 lines
9.0 KiB
TypeScript

import {
WorldPanelsHud,
ACT_WAYPOINTS,
ACT_QUESTS,
ALL_QUESTS,
findWaypointBySlug,
} from '../src/ui/world-panels.ts'
import {
InventoryPanel,
resolveItemSpriteRect,
INV_GRID_ORIGIN,
EQUIP_SLOTS_LAYOUT,
STARTER_EQUIPPED_GEAR,
STARTER_BAG_ITEMS,
} from '../src/ui/inventory.ts'
import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts'
console.log('--- STARTING ADVERSARIAL BOUNDARY AUDIT ---')
// 1. Act Tab Switching & Act 4 Socket Clamping
const wp = new WorldPanelsHud()
// Default state
console.assert(wp.selectedActTab === 1, 'Initial act tab should be 1')
console.assert(wp.selectedQuestIdx === 0, 'Initial quest index should be 0')
// Test clicking Act tabs at boundary X coordinates
// ox = 80, oy = 60. Tab Y: oy + 6 .. oy + 42 = 66 .. 102.
// Tab 1: 88 .. 146
// Tab 2: 149 .. 207
// Tab 3: 210 .. 268
// Tab 4: 271 .. 329
// Tab 5: 332 .. 390
const callbacks = {
onClose: () => {},
onWaypointTeleport: (act: number, slug: string, levelId: number) => {
state.dispatchedWp = { act, slug, levelId }
},
}
const state = {
dispatchedWp: null as { act: number; slug: string; levelId: number } | null,
cubeOpened: false,
tpCasted: false,
}
const getDispatchedWp = () => state.dispatchedWp
const isCubeOpened = () => state.cubeOpened
const isTpCasted = () => state.tpCasted
// Select quest 5 in Act 1
wp.handleLeftDockClick('quest', 80 + 28 + 2 * 92 + 10, 60 + 64 + 1 * 100 + 10, callbacks)
console.assert(wp.selectedQuestIdx === 5, `Expected quest index 5, got ${wp.selectedQuestIdx}`)
// Switch to Act 4 (tab 4: x = 80 + 8 + 3 * 61 + 10 = 281, y = 75)
wp.handleLeftDockClick('quest', 281, 75, callbacks)
console.assert(wp.selectedActTab === 4, `Expected Act 4, got ${wp.selectedActTab}`)
// BOUNDARY CHECK: selectedQuestIdx MUST clamp to 0 because Act 4 only has 3 quests (0, 1, 2)
console.assert(
wp.selectedQuestIdx === 0,
`Act 4 clamping failed! selectedQuestIdx should be 0, got ${wp.selectedQuestIdx}`
)
// In Act 4, click on empty socket 3 (row 1, col 0: x = 80 + 28 + 10 = 118, y = 60 + 64 + 100 + 10 = 234)
// This socket does not exist in Act 4 (only 3 quests exist: indices 0, 1, 2)
wp.handleLeftDockClick('quest', 118, 234, callbacks)
console.assert(
wp.selectedQuestIdx === 0,
`Act 4 socket 3 should be unassigned/unclickable, but selectedQuestIdx changed to ${wp.selectedQuestIdx}`
)
// In Act 4, click on empty socket 4 (row 1, col 1: x = 80 + 28 + 92 + 10 = 210, y = 234)
wp.handleLeftDockClick('quest', 210, 234, callbacks)
console.assert(
wp.selectedQuestIdx === 0,
`Act 4 socket 4 should be unassigned/unclickable, but selectedQuestIdx changed to ${wp.selectedQuestIdx}`
)
// In Act 4, click on empty socket 5 (row 1, col 2: x = 80 + 28 + 184 + 10 = 302, y = 234)
wp.handleLeftDockClick('quest', 302, 234, callbacks)
console.assert(
wp.selectedQuestIdx === 0,
`Act 4 socket 5 should be unassigned/unclickable, but selectedQuestIdx changed to ${wp.selectedQuestIdx}`
)
// Select valid quest 2 in Act 4 (row 0, col 2: x = 302, y = 134)
wp.handleLeftDockClick('quest', 302, 134, callbacks)
console.assert(
wp.selectedQuestIdx === 2,
`Act 4 quest 2 selection failed, got ${wp.selectedQuestIdx}`
)
// Extreme out-of-bounds clicks for act tabs
// Far left (< 88): should clamp to Act 1
wp.handleLeftDockClick('quest', 50, 75, callbacks)
console.assert(wp.selectedActTab === 1, `Expected Act 1 on left clamp, got ${wp.selectedActTab}`)
// Far right (395): should clamp to Act 5
wp.handleLeftDockClick('quest', 395, 75, callbacks)
console.assert(wp.selectedActTab === 5, `Expected Act 5 on right clamp, got ${wp.selectedActTab}`)
// Beyond left panel boundary (> 400): should return false without state change
const handledBeyond = wp.handleLeftDockClick('quest', 450, 75, callbacks)
console.assert(handledBeyond === false, 'Click beyond 400 should return false')
console.assert(wp.selectedActTab === 5, 'Act tab should not change on click beyond 400')
// 2. Waypoint Menu Act switching & clicks across all 5 acts
for (let act = 1; act <= 5; act++) {
wp.selectedActTab = act
const list = ACT_WAYPOINTS[act]!
const expectedCount = act === 4 ? 3 : 9
console.assert(
list.length === expectedCount,
`Act ${act} should have ${expectedCount} waypoints, got ${list.length}`
)
// Click each waypoint
for (let i = 0; i < list.length; i++) {
state.dispatchedWp = null
const wy = 60 + 62 + i * 36 + 10
const clicked = wp.handleLeftDockClick('waypoint', 80 + 50, wy, callbacks)
console.assert(clicked === true, `Waypoint click failed for Act ${act} index ${i}`)
console.assert(getDispatchedWp() !== null, `No waypoint dispatched for Act ${act} index ${i}`)
console.assert(
getDispatchedWp()?.act === act,
`Expected act ${act}, got ${getDispatchedWp()?.act}`
)
console.assert(
getDispatchedWp()?.levelId === list[i]!.levelId,
`Expected levelId ${list[i]!.levelId}, got ${getDispatchedWp()?.levelId}`
)
console.assert(
getDispatchedWp()?.slug === list[i]!.slug,
`Expected slug ${list[i]!.slug}, got ${getDispatchedWp()?.slug}`
)
}
// In Act 4, clicking rows 3..8 should NOT dispatch any waypoint
if (act === 4) {
for (let i = 3; i < 9; i++) {
state.dispatchedWp = null
const wy = 60 + 62 + i * 36 + 10
wp.handleLeftDockClick('waypoint', 80 + 50, wy, callbacks)
console.assert(
getDispatchedWp() === null,
`Act 4 row ${i} should be empty, but dispatched waypoint!`
)
}
}
}
// 3. Waypoint slug lookups
console.assert(
findWaypointBySlug('halls-of-pain')?.levelId === 123,
'Halls of Pain levelId should be 123'
)
console.assert(
findWaypointBySlug('the-pandemonium-fortress')?.levelId === 103,
'The Pandemonium Fortress levelId should be 103'
)
console.assert(
findWaypointBySlug('nonexistent') === undefined,
'Nonexistent slug should return undefined'
)
// 4. Inventory item sprite resolution: Adversarial inputs
const itemRects = BAKED_UI_MANIFEST.itemRects
console.assert(
resolveItemSpriteRect({}, itemRects) !== null,
'Empty item should resolve fallback rect'
)
console.assert(
resolveItemSpriteRect({ invFile: 'NON_EXISTENT_DC6' }, itemRects) !== null,
'Invalid invFile should fallback'
)
console.assert(
resolveItemSpriteRect({ width: 0, height: 0 }, itemRects) !== null,
'Zero dimensions should fallback'
)
console.assert(
resolveItemSpriteRect({ invWidth: -5, invHeight: 100 }, itemRects) !== null,
'Negative/weird dimensions should fallback'
)
// 5. Inventory panel drag, drop, swap, out of bounds
const inv = new InventoryPanel()
inv.visible = true
// Out of bounds click in grid
console.assert(inv.cursorItem === null, 'Cursor should start null')
const beforeCount = inv.gridItems.length
// Pick up Horadric Cube at (0, 0)
inv.handleClick(INV_GRID_ORIGIN.x + 10, INV_GRID_ORIGIN.y + 10, {
onOpenCube: () => {},
onCastTownPortal: () => {},
})
// Cube right-click or left-click without cursor opens cube if code is box
// But handleClick with hit?.item.code === 'box' triggers onOpenCube
state.cubeOpened = false
inv.handleClick(INV_GRID_ORIGIN.x + 10, INV_GRID_ORIGIN.y + 10, {
onOpenCube: () => { state.cubeOpened = true },
onCastTownPortal: () => {},
})
console.assert(isCubeOpened() === true, 'Clicking cube without cursorItem should open cube')
// Click on normal item to pick up (Tome of Town Portal at col 2, row 0: x = origin + 2 * 29 + 10)
state.tpCasted = false
inv.handleClick(INV_GRID_ORIGIN.x + 2 * 29 + 10, INV_GRID_ORIGIN.y + 10, {
onOpenCube: () => {},
onCastTownPortal: () => { state.tpCasted = true },
})
console.assert(isTpCasted() === true, 'Clicking tbk should trigger town portal')
// Directly pick up an item via clickGridCell
inv.clickGridCell(3, 0) // Hellfire torch
console.assert(inv.cursorItem?.code === 'cm2', 'Hellfire torch should be on cursor')
// Tooltip hover check: when cursorItem is held, hoveredItem should still be set by mouseMove,
// but HudManager suppresses tooltip drawing via `!this.inventory.cursorItem`
inv.handleMouseMove(INV_GRID_ORIGIN.x + 10, INV_GRID_ORIGIN.y + 10)
console.assert(inv.hoveredItem !== null, 'Hovered item detected in inventory')
// Place torch in empty space (col 6, row 0)
const placedEmpty = inv.clickGridCell(6, 0)
console.assert(placedEmpty === true, 'Placing torch in empty space should succeed')
console.assert(inv.cursorItem === null, 'Cursor item should now be null')
// Now pick up torch again from (6, 0) and swap with Call to Arms at (8, 0)
inv.clickGridCell(6, 0)
console.assert(inv.cursorItem?.code === 'cm2', 'Picked up torch again')
const swapped = inv.clickGridCell(8, 0) // Overlaps with Call to Arms (crs)
console.assert(swapped === true, 'Swapping torch with Call to Arms should succeed')
console.assert(inv.cursorItem?.code === 'crs', 'Cursor item should now be Call to Arms')
// Weapon swap I <-> II
console.assert(inv.weaponSwapSet === 0, 'Initial weapon swap set is 0')
inv.toggleWeaponSwap()
console.assert(inv.weaponSwapSet === 1, 'Weapon swap set is now 1')
inv.toggleWeaponSwap()
console.assert(inv.weaponSwapSet === 0, 'Weapon swap set is now 0')
console.log('--- ALL ADVERSARIAL BOUNDARY AUDIT ASSERTIONS PASSED! ---')