243 lines
9.0 KiB
TypeScript
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! ---')
|