/** * Challenger M8 Stress Test Suite: Pointer Hit-Testing, Coordinate Translation, * Quick Transfer, and HUD Interception under Widescreen Docking. * * Implements empirical challenger verification following the solution-stress-testing * methodology (generator, oracle, property invariants, edge-case probing). */ import { describe, it, expect, beforeEach } from 'vitest' import { HudManager, computeDockingLayout, computeHudLayout, clientToLogical, logicalToClient, CHAR_PANEL_ORIGIN, INV_PANEL_ORIGIN, INV_GRID_ORIGIN, SKILL_PANEL_ORIGIN, } from '../src/ui/hud-manager.ts' import { InventoryPanel, EQUIP_SLOTS_LAYOUT, INV_CLOSE_BTN_BOUNDS, type EquipSlotId, type UiInventoryItem, } from '../src/ui/inventory.ts' import { CHAR_CLOSE_BTN_BOUNDS } from '../src/ui/character-sheet.ts' import { STASH_GRID_ORIGIN, STASH_PANEL_ORIGIN } from '../src/ui/world-panels.ts' class MockWindow extends EventTarget { innerWidth = 1920 innerHeight = 1080 devicePixelRatio = 1 } if (typeof (globalThis as any).window === 'undefined') { ;(globalThis as any).window = new MockWindow() } if (typeof (globalThis as any).HTMLInputElement === 'undefined') { ;(globalThis as any).HTMLInputElement = class HTMLInputElement {} } if (typeof (globalThis as any).HTMLSelectElement === 'undefined') { ;(globalThis as any).HTMLSelectElement = class HTMLSelectElement {} } class MockCanvas extends EventTarget { clientWidth: number clientHeight: number width: number height: number style = { pointerEvents: 'none' } constructor(width: number, height: number) { super() this.clientWidth = width this.clientHeight = height this.width = width this.height = height } getBoundingClientRect() { return { left: 0, top: 0, width: this.clientWidth, height: this.clientHeight } } getContext() { return null } } class SyntheticMouseEvent extends Event { readonly clientX: number readonly clientY: number readonly button: number readonly shiftKey: boolean constructor( type: string, init: { clientX: number; clientY: number; button?: number; shiftKey?: boolean }, ) { super(type, { cancelable: true, bubbles: true }) this.clientX = init.clientX this.clientY = init.clientY this.button = init.button ?? 0 this.shiftKey = init.shiftKey ?? false } } function createDummyItem( id: string, code: string, invWidth: number, invHeight: number, nameZh: string, allowedSlots: readonly EquipSlotId[] = [], ): UiInventoryItem { return { id, code, invFile: code, name: nameZh, nameZh, baseNameZh: nameZh, quality: 'normal', invWidth, invHeight, allowedSlots, stats: [], } } const WIDESCREEN_RESOLUTIONS = [ { name: '1280x720 (16:9)', width: 1280, height: 720 }, { name: '1920x1080 (16:9 Full HD)', width: 1920, height: 1080 }, { name: '2560x1440 (16:9 QHD)', width: 2560, height: 1440 }, { name: '3440x1440 (21:9 Ultrawide)', width: 3440, height: 1440 }, ] as const describe('Challenger M8: Pointer Hit-Testing & Coordinate Translation under Widescreen Docking', () => { describe('1. Pointer Hit-Testing Round-Trip Accuracy Across Widescreen Resolutions', () => { for (const res of WIDESCREEN_RESOLUTIONS) { describe(`Resolution ${res.name}`, () => { let canvas: MockCanvas let hud: HudManager beforeEach(() => { const win = new MockWindow() win.innerWidth = res.width win.innerHeight = res.height ;(globalThis as any).window = win canvas = new MockCanvas(res.width, res.height) hud = new HudManager(canvas as unknown as HTMLCanvasElement, { onToggleAutomap: () => {}, onWaypointTeleport: () => {}, }) hud['bindEvents']() hud.rightPanel = 'inv' hud.inventory.visible = true }) it(`tests Inventory Grid (10x4) hit-testing at all 40 cells with dynamic offset (rightDockX + 40 + col * 29)`, () => { const docking = hud.getDockingLayout() expect(docking.isWidescreen).toBe(true) const deltaRight = docking.marginW for (let col = 0; col < INV_GRID_ORIGIN.cols; col++) { for (let row = 0; row < INV_GRID_ORIGIN.rows; row++) { // Formula specified by user request: rightDockX + 40 + col * 29 const dynamicLogicalX = docking.rightDockX + 40 + col * INV_GRID_ORIGIN.cellPx const dynamicLogicalY = INV_GRID_ORIGIN.y + 14.5 + row * INV_GRID_ORIGIN.cellPx // Convert logical to client coordinates const clientPos = logicalToClient(dynamicLogicalX, dynamicLogicalY, canvas.getBoundingClientRect()) // Test 1: Mathematical round-trip translation const pt = clientToLogical(clientPos.x, clientPos.y, canvas.getBoundingClientRect()) const translatedRightX = pt.x - deltaRight const translatedLogicalY = pt.y const hitCol = Math.floor((translatedRightX - INV_GRID_ORIGIN.x) / INV_GRID_ORIGIN.cellPx) const hitRow = Math.floor((translatedLogicalY - INV_GRID_ORIGIN.y) / INV_GRID_ORIGIN.cellPx) // Assert translated rightX hits exact corresponding col and row with 0 pixel drift expect(hitCol).toBe(col) expect(hitRow).toBe(row) const cellBaseX = INV_GRID_ORIGIN.x + col * INV_GRID_ORIGIN.cellPx const offsetInCell = translatedRightX - cellBaseX // Since rightDockX = 400 + marginW and INV_GRID_ORIGIN.x = 418: // translatedRightX - INV_GRID_ORIGIN.x = (400 + 40 + col*29) - 418 = 22 + col*29 // So offsetInCell must be exactly 22px, with zero drift expect(offsetInCell).toBeCloseTo(22, 5) // Test 2: Empirical click round-trip (pick up and put back item at this cell) const testItem = createDummyItem(`item-${col}-${row}`, 'rin', 1, 1, `TestRing-${col}-${row}`) hud.inventory.gridItems = [{ item: testItem, col, row }] hud.inventory.cursorItem = null canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: clientPos.x, clientY: clientPos.y, button: 0, shiftKey: false, }), ) // Item must have been picked up from exact cell into cursorItem expect(hud.inventory.cursorItem).not.toBeNull() expect((hud.inventory.cursorItem as UiInventoryItem | null)?.id).toBe(`item-${col}-${row}`) expect(hud.inventory.gridItems).toHaveLength(0) // Click again at same cell to drop it back canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: clientPos.x, clientY: clientPos.y, button: 0, shiftKey: false, }), ) expect(hud.inventory.cursorItem).toBeNull() expect(hud.inventory.gridItems).toHaveLength(1) expect(hud.inventory.gridItems[0]!.col).toBe(col) expect(hud.inventory.gridItems[0]!.row).toBe(row) } } }) it('tests Equipment Slots (all 10 slots) bounding box hit-testing accuracy', () => { const docking = hud.getDockingLayout() const deltaRight = docking.marginW const slotIds: EquipSlotId[] = [ 'helm', 'amulet', 'weapon1', 'armor', 'weapon2', 'gloves', 'ring1', 'belt', 'ring2', 'boots', ] for (const slotId of slotIds) { const rect = EQUIP_SLOTS_LAYOUT[slotId] const canonicalCenterX = rect.x + rect.w / 2 const canonicalCenterY = rect.y + rect.h / 2 // Visual position in widescreen right dock const visualX = canonicalCenterX + deltaRight const visualY = canonicalCenterY const clientPos = logicalToClient(visualX, visualY, canvas.getBoundingClientRect()) // Mathematical translation check const pt = clientToLogical(clientPos.x, clientPos.y, canvas.getBoundingClientRect()) const rightX = pt.x - deltaRight expect(rightX).toBeGreaterThanOrEqual(rect.x) expect(rightX).toBeLessThanOrEqual(rect.x + rect.w) expect(pt.y).toBeGreaterThanOrEqual(rect.y) expect(pt.y).toBeLessThanOrEqual(rect.y + rect.h) // Empirical click interaction check const eqItem = createDummyItem(`eq-${slotId}`, 'cap', 2, 2, `Item-${slotId}`, [slotId]) hud.inventory.equipped[slotId] = eqItem hud.inventory.cursorItem = null canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: clientPos.x, clientY: clientPos.y, button: 0, shiftKey: false, }), ) // Item unequipped to cursor expect((hud.inventory.cursorItem as UiInventoryItem | null)?.id).toBe(`eq-${slotId}`) expect(hud.inventory.equipped[slotId]).toBeUndefined() // Click slot again to re-equip canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: clientPos.x, clientY: clientPos.y, button: 0, shiftKey: false, }), ) expect(hud.inventory.cursorItem).toBeNull() expect(hud.inventory.equipped[slotId]?.id).toBe(`eq-${slotId}`) } }) it('tests Character Sheet close button (left bezel) and Inventory close button (right bezel)', () => { hud.leftPanel = 'char' hud.charSheet.visible = true hud.rightPanel = 'inv' hud.inventory.visible = true const docking = hud.getDockingLayout() const deltaLeft = -docking.marginW const deltaRight = docking.marginW // 1. Character Sheet Close Button const charCloseCenterLogicalX = CHAR_CLOSE_BTN_BOUNDS.x + CHAR_CLOSE_BTN_BOUNDS.w / 2 + deltaLeft const charCloseCenterLogicalY = CHAR_CLOSE_BTN_BOUNDS.y + CHAR_CLOSE_BTN_BOUNDS.h / 2 const charClientPos = logicalToClient(charCloseCenterLogicalX, charCloseCenterLogicalY, canvas.getBoundingClientRect()) const charPt = clientToLogical(charClientPos.x, charClientPos.y, canvas.getBoundingClientRect()) const translatedLeftX = charPt.x - deltaLeft expect(translatedLeftX).toBeGreaterThanOrEqual(CHAR_CLOSE_BTN_BOUNDS.x) expect(translatedLeftX).toBeLessThanOrEqual(CHAR_CLOSE_BTN_BOUNDS.x + CHAR_CLOSE_BTN_BOUNDS.w) expect(charPt.y).toBeGreaterThanOrEqual(CHAR_CLOSE_BTN_BOUNDS.y) expect(charPt.y).toBeLessThanOrEqual(CHAR_CLOSE_BTN_BOUNDS.y + CHAR_CLOSE_BTN_BOUNDS.h) canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: charClientPos.x, clientY: charClientPos.y, button: 0, }), ) expect(hud.leftPanel).toBe('none') expect(hud.charSheet.visible).toBe(false) // 2. Inventory Close Button const invCloseCenterLogicalX = INV_CLOSE_BTN_BOUNDS.x + INV_CLOSE_BTN_BOUNDS.w / 2 + deltaRight const invCloseCenterLogicalY = INV_CLOSE_BTN_BOUNDS.y + INV_CLOSE_BTN_BOUNDS.h / 2 const invClientPos = logicalToClient(invCloseCenterLogicalX, invCloseCenterLogicalY, canvas.getBoundingClientRect()) const invPt = clientToLogical(invClientPos.x, invClientPos.y, canvas.getBoundingClientRect()) const translatedRightX = invPt.x - deltaRight expect(translatedRightX).toBeGreaterThanOrEqual(INV_CLOSE_BTN_BOUNDS.x) expect(translatedRightX).toBeLessThanOrEqual(INV_CLOSE_BTN_BOUNDS.x + INV_CLOSE_BTN_BOUNDS.w) expect(invPt.y).toBeGreaterThanOrEqual(INV_CLOSE_BTN_BOUNDS.y) expect(invPt.y).toBeLessThanOrEqual(INV_CLOSE_BTN_BOUNDS.y + INV_CLOSE_BTN_BOUNDS.h) canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: invClientPos.x, clientY: invClientPos.y, button: 0, }), ) expect(hud.rightPanel).toBe('none') expect(hud.inventory.visible).toBe(false) }) }) } }) describe('2. Central Vision Corridor Passthrough Stress Testing (1920x1080, marginW = 133.33)', () => { let canvas: MockCanvas let hud: HudManager beforeEach(() => { const win = new MockWindow() win.innerWidth = 1920 win.innerHeight = 1080 ;(globalThis as any).window = win canvas = new MockCanvas(1920, 1080) hud = new HudManager(canvas as unknown as HTMLCanvasElement, { onToggleAutomap: () => {}, onWaypointTeleport: () => {}, }) hud['bindEvents']() }) const panelPairs: { left: 'char' | 'stash' | 'quest'; right: 'inv' | 'skill' }[] = [ { left: 'char', right: 'inv' }, { left: 'stash', right: 'inv' }, { left: 'quest', right: 'skill' }, ] for (const pair of panelPairs) { describe(`Dual Panels: '${pair.left}' + '${pair.right}'`, () => { beforeEach(() => { hud.leftPanel = pair.left if (pair.left === 'char') hud.charSheet.visible = true hud.rightPanel = pair.right if (pair.right === 'inv') hud.inventory.visible = true if (pair.right === 'skill') hud.skillTree.visible = true }) it('probes points strictly inside central corridor (400 - marginW, 400 + marginW): returns false', () => { const docking = hud.getDockingLayout() const marginW = docking.marginW // 133.333... const leftBound = 400 - marginW const rightBound = 400 + marginW // Dense sweep of points across corridor width const testXs = [ leftBound + 0.5, leftBound + 5, leftBound + 20, 300, 350, 400, // Screen midpoint 450, 500, rightBound - 20, rightBound - 5, rightBound - 0.5, ] // Sweep across heights in normal gameplay viewport (above bottom control bar) const testYs = [50, 150, 250, 300, 350, 450, 490] for (const x of testXs) { for (const y of testYs) { expect( hud.isPointInterceptedByHud(x, y), `Expected isPointInterceptedByHud(${x}, ${y}) to be false in central corridor`, ).toBe(false) } } }) it('probes points inside left panel [-marginW, 400 - marginW]: returns true', () => { const docking = hud.getDockingLayout() const marginW = docking.marginW const leftPanelRightEdge = 400 - marginW const testXs = [ -marginW, -marginW + 5, -marginW + 50, 0, 50, 100, 200, leftPanelRightEdge - 10, leftPanelRightEdge - 1, leftPanelRightEdge, ] for (const x of testXs) { expect( hud.isPointInterceptedByHud(x, 300), `Expected isPointInterceptedByHud(${x}, 300) to be true on left panel`, ).toBe(true) } }) it('probes points inside right panel [400 + marginW, 800 + marginW]: returns true', () => { const docking = hud.getDockingLayout() const marginW = docking.marginW const rightPanelLeftEdge = 400 + marginW const rightPanelRightEdge = 800 + marginW const testXs = [ rightPanelLeftEdge, rightPanelLeftEdge + 1, rightPanelLeftEdge + 10, 600, 700, 800, rightPanelRightEdge - 20, rightPanelRightEdge - 1, rightPanelRightEdge, ] for (const x of testXs) { expect( hud.isPointInterceptedByHud(x, 300), `Expected isPointInterceptedByHud(${x}, 300) to be true on right panel`, ).toBe(true) } }) it('probes points out of horizontal bounds (< -marginW or > 800 + marginW): returns false', () => { const docking = hud.getDockingLayout() const marginW = docking.marginW expect(hud.isPointInterceptedByHud(-marginW - 1, 300)).toBe(false) expect(hud.isPointInterceptedByHud(-marginW - 100, 300)).toBe(false) expect(hud.isPointInterceptedByHud(800 + marginW + 1, 300)).toBe(false) expect(hud.isPointInterceptedByHud(800 + marginW + 200, 300)).toBe(false) }) }) } }) describe('3. Quick Transfer Mechanics Under Widescreen Docking (1920x1080)', () => { let canvas: MockCanvas let hud: HudManager beforeEach(() => { const win = new MockWindow() win.innerWidth = 1920 win.innerHeight = 1080 ;(globalThis as any).window = win canvas = new MockCanvas(1920, 1080) hud = new HudManager(canvas as unknown as HTMLCanvasElement, { onToggleAutomap: () => {}, onWaypointTeleport: () => {}, }) hud['bindEvents']() hud.leftPanel = 'stash' hud.rightPanel = 'inv' hud.inventory.visible = true hud.inventory.gridItems = [] hud.worldPanels.stashItems = [] }) it('transfers multiple varied-size items cleanly from Inventory to Stash via Shift+Click with 0 loss and 0 dupes', () => { const docking = hud.getDockingLayout() const deltaRight = docking.marginW // Setup 5 varied-dimension items in Inventory grid const itemsToTest = [ createDummyItem('item-1x1', 'rin', 1, 1, '乔丹之石'), createDummyItem('item-1x2', 'amu', 1, 2, '马拉的万花筒'), createDummyItem('item-2x2', 'cap', 2, 2, '军帽 (Shako)'), createDummyItem('item-2x3', 'crs', 2, 3, '水晶剑 (Crystal Sword)'), createDummyItem('item-2x4', 'gpa', 2, 4, '无限 (Infinity)'), ] hud.inventory.gridItems = [ { item: itemsToTest[0]!, col: 0, row: 0 }, { item: itemsToTest[1]!, col: 1, row: 0 }, { item: itemsToTest[2]!, col: 2, row: 0 }, { item: itemsToTest[3]!, col: 4, row: 0 }, { item: itemsToTest[4]!, col: 6, row: 0 }, ] expect(hud.inventory.gridItems).toHaveLength(5) expect(hud.worldPanels.stashItems).toHaveLength(0) // Shift+Click each item in Inventory grid for (const targetItem of itemsToTest) { const placement = hud.inventory.gridItems.find(p => p.item.id === targetItem.id) expect(placement).toBeDefined() const visualLogicalX = INV_GRID_ORIGIN.x + (placement!.col + 0.5) * INV_GRID_ORIGIN.cellPx + deltaRight const visualLogicalY = INV_GRID_ORIGIN.y + (placement!.row + 0.5) * INV_GRID_ORIGIN.cellPx const clientPos = logicalToClient(visualLogicalX, visualLogicalY, canvas.getBoundingClientRect()) canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: clientPos.x, clientY: clientPos.y, button: 0, shiftKey: true, }), ) } // Assertions: 0 item duplication, 0 item loss expect(hud.inventory.gridItems).toHaveLength(0) expect(hud.worldPanels.stashItems).toHaveLength(5) const stashItemIds = hud.worldPanels.stashItems.map(p => p.item.id) expect(new Set(stashItemIds).size).toBe(5) for (const expected of itemsToTest) { expect(stashItemIds).toContain(expected.id) } // Assert zero coordinate translation errors: all items properly placed in 6x8 stash grid for (const p of hud.worldPanels.stashItems) { expect(p.col).toBeGreaterThanOrEqual(0) expect(p.col + p.item.invWidth).toBeLessThanOrEqual(STASH_GRID_ORIGIN.cols) expect(p.row).toBeGreaterThanOrEqual(0) expect(p.row + p.item.invHeight).toBeLessThanOrEqual(STASH_GRID_ORIGIN.rows) } // Assert no overlapping items in Stash for (let i = 0; i < hud.worldPanels.stashItems.length; i++) { for (let j = i + 1; j < hud.worldPanels.stashItems.length; j++) { const a = hud.worldPanels.stashItems[i]! const b = hud.worldPanels.stashItems[j]! const xOverlap = a.col < b.col + b.item.invWidth && a.col + a.item.invWidth > b.col const yOverlap = a.row < b.row + b.item.invHeight && a.row + a.item.invHeight > b.row expect(xOverlap && yOverlap).toBe(false) } } }) it('transfers multiple items cleanly from Stash to Inventory via Shift+Click with 0 loss and 0 dupes', () => { const docking = hud.getDockingLayout() const deltaLeft = -docking.marginW const itemsInStash = [ createDummyItem('stash-1x1', 'rin', 1, 1, '布尔凯索婚戒'), createDummyItem('stash-1x2', 'tbk', 1, 2, '回城之书'), createDummyItem('stash-2x2', 'box', 2, 2, '赫拉迪克方块'), createDummyItem('stash-2x3', 'crs', 2, 3, '橡树之心'), ] hud.worldPanels.stashItems = [ { item: itemsInStash[0]!, col: 0, row: 0 }, { item: itemsInStash[1]!, col: 1, row: 0 }, { item: itemsInStash[2]!, col: 2, row: 0 }, { item: itemsInStash[3]!, col: 4, row: 0 }, ] expect(hud.worldPanels.stashItems).toHaveLength(4) expect(hud.inventory.gridItems).toHaveLength(0) for (const targetItem of itemsInStash) { const placement = hud.worldPanels.stashItems.find(p => p.item.id === targetItem.id) expect(placement).toBeDefined() const visualLogicalX = STASH_GRID_ORIGIN.x + (placement!.col + 0.5) * STASH_GRID_ORIGIN.cellPx + deltaLeft const visualLogicalY = STASH_GRID_ORIGIN.y + (placement!.row + 0.5) * STASH_GRID_ORIGIN.cellPx const clientPos = logicalToClient(visualLogicalX, visualLogicalY, canvas.getBoundingClientRect()) canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: clientPos.x, clientY: clientPos.y, button: 0, shiftKey: true, }), ) } // Assertions: 0 item duplication, 0 item loss expect(hud.worldPanels.stashItems).toHaveLength(0) expect(hud.inventory.gridItems).toHaveLength(4) const invItemIds = hud.inventory.gridItems.map(p => p.item.id) expect(new Set(invItemIds).size).toBe(4) for (const expected of itemsInStash) { expect(invItemIds).toContain(expected.id) } // Assert all items legally positioned in 10x4 inventory grid without overlap for (const p of hud.inventory.gridItems) { expect(p.col).toBeGreaterThanOrEqual(0) expect(p.col + p.item.invWidth).toBeLessThanOrEqual(INV_GRID_ORIGIN.cols) expect(p.row).toBeGreaterThanOrEqual(0) expect(p.row + p.item.invHeight).toBeLessThanOrEqual(INV_GRID_ORIGIN.rows) } }) it('handles destination full gracefully without item loss or state corruption', () => { const docking = hud.getDockingLayout() const deltaRight = docking.marginW // Completely pack stash 6x8 grid with 48 1x1 items for (let col = 0; col < STASH_GRID_ORIGIN.cols; col++) { for (let row = 0; row < STASH_GRID_ORIGIN.rows; row++) { hud.worldPanels.stashItems.push({ item: createDummyItem(`fill-${col}-${row}`, 'rin', 1, 1, 'Filler'), col, row, }) } } expect(hud.worldPanels.stashItems).toHaveLength(48) // Place 1 item in inventory const extraItem = createDummyItem('overflow-ring', 'rin', 1, 1, '溢出之戒') hud.inventory.gridItems = [{ item: extraItem, col: 0, row: 0 }] const visualLogicalX = INV_GRID_ORIGIN.x + 14.5 + deltaRight const visualLogicalY = INV_GRID_ORIGIN.y + 14.5 const clientPos = logicalToClient(visualLogicalX, visualLogicalY, canvas.getBoundingClientRect()) canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: clientPos.x, clientY: clientPos.y, button: 0, shiftKey: true, }), ) // Transfer must fail gracefully: item stays in inventory, stash remains full expect(hud.worldPanels.stashItems).toHaveLength(48) expect(hud.inventory.gridItems).toHaveLength(1) expect(hud.inventory.gridItems[0]!.item.id).toBe('overflow-ring') }) it('transfers items cleanly from Inventory to Stash and Stash to Inventory via Right-Click (button: 2)', () => { const docking = hud.getDockingLayout() const deltaRight = docking.marginW const deltaLeft = -docking.marginW const rightClickItem = createDummyItem('rc-item', 'rin', 1, 1, '右键转移戒指') hud.inventory.gridItems = [{ item: rightClickItem, col: 0, row: 0 }] expect(hud.worldPanels.stashItems).toHaveLength(0) // Right click item in Inventory (button: 2) const visualX = INV_GRID_ORIGIN.x + 14.5 + deltaRight const visualY = INV_GRID_ORIGIN.y + 14.5 const clientPos = logicalToClient(visualX, visualY, canvas.getBoundingClientRect()) canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: clientPos.x, clientY: clientPos.y, button: 2, shiftKey: false, }), ) expect(hud.inventory.gridItems).toHaveLength(0) expect(hud.worldPanels.stashItems).toHaveLength(1) expect(hud.worldPanels.stashItems[0]!.item.id).toBe('rc-item') // Right click item back in Stash (button: 2) const stashP = hud.worldPanels.stashItems[0]! const stashVisualX = STASH_GRID_ORIGIN.x + (stashP.col + 0.5) * STASH_GRID_ORIGIN.cellPx + deltaLeft const stashVisualY = STASH_GRID_ORIGIN.y + (stashP.row + 0.5) * STASH_GRID_ORIGIN.cellPx const stashClientPos = logicalToClient(stashVisualX, stashVisualY, canvas.getBoundingClientRect()) canvas.dispatchEvent( new SyntheticMouseEvent('mousedown', { clientX: stashClientPos.x, clientY: stashClientPos.y, button: 2, shiftKey: false, }), ) expect(hud.worldPanels.stashItems).toHaveLength(0) expect(hud.inventory.gridItems).toHaveLength(1) expect(hud.inventory.gridItems[0]!.item.id).toBe('rc-item') }) }) describe('4. Tooltip Anchoring Compensation Across Resolutions', () => { for (const res of WIDESCREEN_RESOLUTIONS) { describe(`Resolution ${res.name}`, () => { let canvas: MockCanvas let hud: HudManager beforeEach(() => { const win = new MockWindow() win.innerWidth = res.width win.innerHeight = res.height ;(globalThis as any).window = win canvas = new MockCanvas(res.width, res.height) hud = new HudManager(canvas as unknown as HTMLCanvasElement, { onToggleAutomap: () => {}, onWaypointTeleport: () => {}, }) hud['bindEvents']() hud.leftPanel = 'stash' hud.rightPanel = 'inv' hud.inventory.visible = true }) it('asserts hovered item tooltip on Inventory item has visual anchor properly compensated by +deltaRight', () => { const docking = hud.getDockingLayout() const deltaRight = docking.marginW const item = createDummyItem('inv-tooltip-test', 'cap', 2, 2, '军帽') hud.inventory.gridItems = [{ item, col: 3, row: 1 }] // Canonical center anchor for (col: 3, row: 1, w: 2, h: 2): // x = INV_GRID_ORIGIN.x + (col + w/2) * cellPx = 418 + (3 + 1) * 29 = 418 + 116 = 534 // y = INV_GRID_ORIGIN.y + row * cellPx = 316 + 29 = 345 const canonicalAnchorX = INV_GRID_ORIGIN.x + (3 + 2 / 2) * INV_GRID_ORIGIN.cellPx const canonicalAnchorY = INV_GRID_ORIGIN.y + 1 * INV_GRID_ORIGIN.cellPx const expectedVisualAnchorX = canonicalAnchorX + deltaRight // Mouse is over this item at visual position const visualMouseLogicalX = canonicalAnchorX + deltaRight const visualMouseLogicalY = canonicalAnchorY + 15 const clientPos = logicalToClient(visualMouseLogicalX, visualMouseLogicalY, canvas.getBoundingClientRect()) ;(globalThis as any).window.dispatchEvent( new SyntheticMouseEvent('mousemove', { clientX: clientPos.x, clientY: clientPos.y, }), ) expect(hud.inventory.hoveredItem).not.toBeNull() expect(hud.inventory.hoveredItem?.item.id).toBe('inv-tooltip-test') // Assert visual anchor x is compensated by deltaRight expect(hud.inventory.hoveredItem?.x).toBeCloseTo(expectedVisualAnchorX, 2) expect(hud.inventory.hoveredItem?.y).toBeCloseTo(canonicalAnchorY, 2) }) it('asserts hovered item tooltip on Stash item has visual anchor properly compensated by +deltaLeft', () => { const docking = hud.getDockingLayout() const deltaLeft = -docking.marginW const item = createDummyItem('stash-tooltip-test', 'amu', 1, 1, '马拉万花筒') hud.worldPanels.stashItems = [{ item, col: 2, row: 3 }] // Canonical center anchor for (col: 2, row: 3, w: 1, h: 1): // x = STASH_GRID_ORIGIN.x + (col + w/2) * cellPx = 154 + (2 + 0.5) * 29 = 154 + 72.5 = 226.5 // y = STASH_GRID_ORIGIN.y + row * cellPx = 142 + 3 * 29 = 142 + 87 = 229 const canonicalAnchorX = STASH_GRID_ORIGIN.x + (2 + 0.5) * STASH_GRID_ORIGIN.cellPx const canonicalAnchorY = STASH_GRID_ORIGIN.y + 3 * STASH_GRID_ORIGIN.cellPx const expectedVisualAnchorX = canonicalAnchorX + deltaLeft const visualMouseLogicalX = canonicalAnchorX + deltaLeft const visualMouseLogicalY = canonicalAnchorY + 14.5 const clientPos = logicalToClient(visualMouseLogicalX, visualMouseLogicalY, canvas.getBoundingClientRect()) ;(globalThis as any).window.dispatchEvent( new SyntheticMouseEvent('mousemove', { clientX: clientPos.x, clientY: clientPos.y, }), ) expect(hud.inventory.hoveredItem).not.toBeNull() expect(hud.inventory.hoveredItem?.item.id).toBe('stash-tooltip-test') // Assert visual anchor x is compensated by deltaLeft expect(hud.inventory.hoveredItem?.x).toBeCloseTo(expectedVisualAnchorX, 2) expect(hud.inventory.hoveredItem?.y).toBeCloseTo(canonicalAnchorY, 2) }) it('asserts hovered item tooltip on Equipment slot has visual anchor compensated by +deltaRight', () => { const docking = hud.getDockingLayout() const deltaRight = docking.marginW const helmItem = createDummyItem('eq-helm-tooltip', 'cap', 2, 2, '暗金头盔', ['helm']) hud.inventory.equipped.helm = helmItem const rect = EQUIP_SLOTS_LAYOUT.helm const canonicalAnchorX = rect.x + rect.w / 2 const canonicalAnchorY = rect.y const expectedVisualAnchorX = canonicalAnchorX + deltaRight const visualMouseLogicalX = canonicalAnchorX + deltaRight const visualMouseLogicalY = canonicalAnchorY + rect.h / 2 const clientPos = logicalToClient(visualMouseLogicalX, visualMouseLogicalY, canvas.getBoundingClientRect()) ;(globalThis as any).window.dispatchEvent( new SyntheticMouseEvent('mousemove', { clientX: clientPos.x, clientY: clientPos.y, }), ) expect(hud.inventory.hoveredItem).not.toBeNull() expect(hud.inventory.hoveredItem?.item.id).toBe('eq-helm-tooltip') expect(hud.inventory.hoveredItem?.x).toBeCloseTo(expectedVisualAnchorX, 2) expect(hud.inventory.hoveredItem?.y).toBeCloseTo(canonicalAnchorY, 2) }) }) } }) })