/** * Empirical Challenger Stress Test Suite for Milestone M6: * Normal Attack Icon Resolution, Dual Slot Binding Permutations, and Hotkey Switching. * * Ground Truth: Diablo II v1.13c Skill System Parity (Issue #151) */ import { describe, expect, it, beforeEach } from 'vitest' import { readFileSync, existsSync, statSync } from 'node:fs' import { join } from 'node:path' import { DEFAULT_SORCERESS_SKILLS, SkillHotkeysHud, resolveSkillIcon, getAvailableSkillsForSide, LEFT_SKILL_BOUNDS, RIGHT_SKILL_BOUNDS, type HotkeySkillEntry, } from '../src/ui/hotkeys.ts' describe('Empirical Challenger Stress Suite: M6 Normal Attack & Dual Slot Bindings', () => { const rootDir = process.cwd() // ========================================================================= // 1. Icon Resolution & Asset Existence // ========================================================================= describe('1. Icon Resolution & Disk Asset Integrity', () => { it('verifies resolveSkillIcon(0) resolves to authentic Attack sword icon metadata', () => { const mapping = resolveSkillIcon(0) expect(mapping).toBeDefined() expect(mapping.skillId).toBe(0) expect(mapping.name).toBe('Attack') expect(mapping.iconPath).toBe('/skills/icon_0.png') expect(mapping.isGeneric).toBe(true) expect(mapping.dc6Path).toBe('data/global/ui/SPELLS/Skillicon.DC6') expect(mapping.dc6Source).toBe('data/global/ui/SPELLS/Skillicon.DC6') expect(mapping.frameIndex).toBe(2) expect(mapping.atlasRect).toEqual({ x: 96, y: 0, width: 48, height: 48, w: 48, h: 48, }) }) it('verifies resolveSkillIcon handles numeric variations consistently', () => { const zeroPos = resolveSkillIcon(+0) const zeroNeg = resolveSkillIcon(-0) const zeroNum = resolveSkillIcon(Number(0)) expect(zeroPos).toEqual(zeroNeg) expect(zeroPos).toEqual(zeroNum) expect(zeroPos.skillId).toBe(0) expect(zeroPos.name).toBe('Attack') // Contrast with non-zero skills const skill47 = resolveSkillIcon(47) expect(skill47.skillId).toBe(47) expect(skill47.isGeneric).toBe(false) expect(skill47.iconPath).toBe('/skills/icon_47.png') }) const targetPngs = [ { path: join(rootDir, 'public', 'skills', 'icon_0.png'), name: 'icon_0.png (skills)', expectedWidth: 48, expectedHeight: 48, }, { path: join(rootDir, 'public', 'skills', 'icon_0_active.png'), name: 'icon_0_active.png (skills)', expectedWidth: 48, expectedHeight: 48, }, { path: join(rootDir, 'public', 'ui', 'icon_0.png'), name: 'icon_0.png (ui)', expectedWidth: 48, expectedHeight: 48, }, ] for (const file of targetPngs) { it(`verifies ${file.name} exists on disk and is a valid 48x48 PNG with authentic headers`, () => { expect(existsSync(file.path)).toBe(true) const stat = statSync(file.path) expect(stat.size).toBeGreaterThan(100) // valid png is larger than dummy stub expect(stat.size).toBeLessThan(1024 * 1024) // sanity upper bound const buf = readFileSync(file.path) // 1. PNG 8-byte Magic Signature (\x89PNG\r\n\x1a\n) const magic = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a] for (let i = 0; i < 8; i++) { expect(buf[i]).toBe(magic[i]) } // 2. IHDR Chunk Header const ihdrLength = buf.readUInt32BE(8) expect(ihdrLength).toBe(13) // standard IHDR length const ihdrType = buf.toString('ascii', 12, 16) expect(ihdrType).toBe('IHDR') // 3. Exact 48x48 Dimensions const width = buf.readUInt32BE(16) const height = buf.readUInt32BE(20) expect(width).toBe(file.expectedWidth) expect(height).toBe(file.expectedHeight) // 4. Bit depth and valid color type const bitDepth = buf[24] expect([8, 16]).toContain(bitDepth) const colorType = buf[25] expect([0, 2, 3, 4, 6]).toContain(colorType) // 5. IEND Chunk at file end const endChunkType = buf.toString('ascii', buf.length - 8, buf.length - 4) expect(endChunkType).toBe('IEND') }) } }) // ========================================================================= // 2. Slot Binding Permutations & Invariance // ========================================================================= describe('2. Slot Binding Permutations & Invariance', () => { let hud: SkillHotkeysHud beforeEach(() => { hud = new SkillHotkeysHud() }) it('verifies getAvailableSkillsForSide includes Skill 0 (Attack) in both left and right palettes', () => { const leftSkills = getAvailableSkillsForSide('left') const rightSkills = getAvailableSkillsForSide('right') const leftAttack = leftSkills.find((s) => s.skillId === 0) const rightAttack = rightSkills.find((s) => s.skillId === 0) expect(leftAttack).toBeDefined() expect(rightAttack).toBeDefined() expect(leftAttack!.name).toBe('Attack') expect(leftAttack!.nameZh).toBe('普通攻击') expect(leftAttack!.leftUsable).toBe(true) expect(leftAttack!.rightUsable).toBe(true) expect(rightAttack!.name).toBe('Attack') expect(rightAttack!.nameZh).toBe('普通攻击') expect(rightAttack!.leftUsable).toBe(true) expect(rightAttack!.rightUsable).toBe(true) }) it('assigns Attack to left slot and right slot simultaneously (Dual Attack)', () => { hud.assignSkill('left', 0) hud.assignSkill('right', 0) expect(hud.leftSkillId).toBe(0) expect(hud.rightSkillId).toBe(0) // Test setDualSkill convenience method hud.assignSkill('left', 47) hud.assignSkill('right', 64) expect(hud.leftSkillId).toBe(47) expect(hud.rightSkillId).toBe(64) hud.setDualSkill(0) expect(hud.leftSkillId).toBe(0) expect(hud.rightSkillId).toBe(0) }) it('tests asymmetric permutations: Attack left / Spells right, and Spells left / Attack right', () => { const availableLeft = hud.getAvailableSkillsForSide('left').map((s) => s.skillId) const availableRight = hud.getAvailableSkillsForSide('right').map((s) => s.skillId) // Permutation A: Attack on Left, cycle every available Right skill hud.assignSkill('left', 0) for (const rSkill of availableRight) { hud.assignSkill('right', rSkill) expect(hud.leftSkillId).toBe(0) expect(hud.rightSkillId).toBe(rSkill) } // Permutation B: Attack on Right, cycle every available Left skill hud.assignSkill('right', 0) for (const lSkill of availableLeft) { hud.assignSkill('left', lSkill) expect(hud.leftSkillId).toBe(lSkill) expect(hud.rightSkillId).toBe(0) } }) it('rigorously verifies slot state orthogonality across full Cartesian product of skills', () => { const leftSkills = hud.getAvailableSkillsForSide('left').map((s) => s.skillId) const rightSkills = hud.getAvailableSkillsForSide('right').map((s) => s.skillId) for (const l of leftSkills) { for (const r of rightSkills) { hud.assignSkill('left', l) expect(hud.leftSkillId).toBe(l) hud.assignSkill('right', r) // Crucial check: assigning to right slot MUST NOT mutate left slot expect(hud.leftSkillId).toBe(l) expect(hud.rightSkillId).toBe(r) // Re-assign left slot: assigning left MUST NOT mutate right slot hud.assignSkill('left', 0) expect(hud.leftSkillId).toBe(0) expect(hud.rightSkillId).toBe(r) } } }) it('stress tests 1000 randomized slot mutations against an independent reference oracle', () => { const leftCandidates = hud.getAvailableSkillsForSide('left').map((s) => s.skillId) const rightCandidates = hud.getAvailableSkillsForSide('right').map((s) => s.skillId) let oracleLeft = hud.leftSkillId let oracleRight = hud.rightSkillId // Pseudo-random deterministic LCG generator let seed = 1337 function nextRand(): number { seed = (seed * 1664525 + 1013904223) >>> 0 return seed / 4294967296 } for (let i = 0; i < 1000; i++) { const action = Math.floor(nextRand() * 3) if (action === 0) { // Mutate left const idx = Math.floor(nextRand() * leftCandidates.length) const chosen = leftCandidates[idx] hud.assignSkill('left', chosen) oracleLeft = chosen } else if (action === 1) { // Mutate right const idx = Math.floor(nextRand() * rightCandidates.length) const chosen = rightCandidates[idx] hud.assignSkill('right', chosen) oracleRight = chosen } else { // Dual assign (frequently attack = 0) const chosen = nextRand() > 0.5 ? 0 : leftCandidates[Math.floor(nextRand() * leftCandidates.length)] hud.setDualSkill(chosen) oracleLeft = chosen oracleRight = chosen } expect(hud.leftSkillId).toBe(oracleLeft) expect(hud.rightSkillId).toBe(oracleRight) } }) }) // ========================================================================= // 3. Hotkey Assignment, Rapid Switching, & Speedbar Selection // ========================================================================= describe('3. Hotkey Assignment & Rapid Switching Stress', () => { let hud: SkillHotkeysHud beforeEach(() => { hud = new SkillHotkeysHud() }) it('binds F1 through F8 to Attack (0) and verifies instant key activation', () => { const keys = ['F1', 'F2', 'F3', 'F4', 'F5', 'F6', 'F7', 'F8'] for (const key of keys) { // Set F-key to Attack for right slot hud.setHotkey(key, 0, 'right') expect(hud.getHotkey(key)).toBe(0) // Set right skill to something else first hud.assignSkill('right', 64) expect(hud.rightSkillId).toBe(64) // Press hotkey const handled = hud.handleKeyDown(key) expect(handled).toBe(true) expect(hud.rightSkillId).toBe(0) } // Also test left slot binding for (const key of keys) { hud.bindHotkey(key, 'left', 0) hud.assignSkill('left', 47) expect(hud.leftSkillId).toBe(47) const handled = hud.handleKeyDown(key) expect(handled).toBe(true) expect(hud.leftSkillId).toBe(0) } }) it('verifies slot state invariance during hotkey trigger', () => { // Bind F1 to right Attack (0) and F2 to left Attack (0) hud.bindHotkey('F1', 'right', 0) hud.bindHotkey('F2', 'left', 0) hud.assignSkill('left', 47) hud.assignSkill('right', 64) // Triggering F1 (right attack) must not alter left skill hud.handleKeyDown('F1') expect(hud.rightSkillId).toBe(0) expect(hud.leftSkillId).toBe(47) // Triggering F2 (left attack) must not alter right skill hud.handleKeyDown('F2') expect(hud.leftSkillId).toBe(0) expect(hud.rightSkillId).toBe(0) }) it('asserts leftUsable: true and rightUsable: true remain immutable on Skill 0', () => { const attackDef = DEFAULT_SORCERESS_SKILLS.find((s) => s.skillId === 0) expect(attackDef).toBeDefined() expect(attackDef!.leftUsable).toBe(true) expect(attackDef!.rightUsable).toBe(true) // Perform a flurry of mutations, speedbar interactions, and bindings for (let i = 0; i < 200; i++) { hud.openSpeedbar('left') hud.handleClick(LEFT_SKILL_BOUNDS.x, LEFT_SKILL_BOUNDS.y) hud.openSpeedbar('right') hud.handleClick(RIGHT_SKILL_BOUNDS.x, RIGHT_SKILL_BOUNDS.y) hud.assignSkill('left', 0) hud.assignSkill('right', 0) hud.bindHotkey('F1', 'left', 0) hud.handleKeyDown('F1') } // Check DEFAULT_SORCERESS_SKILLS const attackDefAfter = DEFAULT_SORCERESS_SKILLS.find((s) => s.skillId === 0) expect(attackDefAfter!.leftUsable).toBe(true) expect(attackDefAfter!.rightUsable).toBe(true) // Check hud.availableSkills const hudAttack = hud.availableSkills.find((s) => s.skillId === 0) expect(hudAttack!.leftUsable).toBe(true) expect(hudAttack!.rightUsable).toBe(true) }) it('stress tests rapid hotkey switching with 5000 key events simulating hyper APM', () => { // Configure F1..F8 layout const config: Array<{ key: string; side: 'left' | 'right'; skillId: number }> = [ { key: 'F1', side: 'right', skillId: 0 }, // Attack { key: 'F2', side: 'right', skillId: 64 }, // Frozen Orb { key: 'F3', side: 'right', skillId: 59 }, // Blizzard { key: 'F4', side: 'right', skillId: 54 }, // Teleport { key: 'F5', side: 'left', skillId: 0 }, // Attack { key: 'F6', side: 'left', skillId: 47 }, // Fire Ball { key: 'F7', side: 'left', skillId: 36 }, // Fire Bolt { key: 'F8', side: 'right', skillId: 40 }, // Frozen Armor ] for (const item of config) { hud.bindHotkey(item.key, item.side, item.skillId) } let expectedLeft = hud.leftSkillId let expectedRight = hud.rightSkillId let seed = 424242 function randInt(max: number): number { seed = (seed * 1103515245 + 12345) & 0x7fffffff return seed % max } for (let step = 0; step < 5000; step++) { const choice = config[randInt(config.length)] const res = hud.triggerFunctionKey(choice.key) expect(res).not.toBeNull() expect(res!.skillId).toBe(choice.skillId) expect(res!.side).toBe(choice.side) if (choice.side === 'left') { expectedLeft = choice.skillId } else { expectedRight = choice.skillId } expect(hud.leftSkillId).toBe(expectedLeft) expect(hud.rightSkillId).toBe(expectedRight) } }) it('stress tests speedbar popup selection for Normal Attack', () => { // 1. Left Speedbar hud.openSpeedbar('left') expect(hud.selectionOpen).toBe('left') const leftGrid = hud.getSpeedbarSkills('left') const leftAttack = leftGrid.find((cell) => cell.skill.skillId === 0) expect(leftAttack).toBeDefined() // Click within Attack cell bounds const leftClicked = hud.handleClick(leftAttack!.x + 16, leftAttack!.y + 16) expect(leftClicked).toBe(true) expect(hud.leftSkillId).toBe(0) expect(hud.selectionOpen).toBeNull() // closed automatically // 2. Right Speedbar hud.openSpeedbar('right') expect(hud.selectionOpen).toBe('right') const rightGrid = hud.getSpeedbarSkills('right') const rightAttack = rightGrid.find((cell) => cell.skill.skillId === 0) expect(rightAttack).toBeDefined() // Click within Attack cell bounds const rightClicked = hud.handleClick(rightAttack!.x + 16, rightAttack!.y + 16) expect(rightClicked).toBe(true) expect(hud.rightSkillId).toBe(0) expect(hud.selectionOpen).toBeNull() }) it('tests speedbar interactive hover and in-palette hotkey rebinding', () => { hud.openSpeedbar('right') const rightGrid = hud.getSpeedbarSkills('right') const attackCell = rightGrid.find((cell) => cell.skill.skillId === 0)! // Move mouse over Attack in speedbar hud.handleMouseMove(attackCell.x + 10, attackCell.y + 10) expect(hud.hoverSpeedbarSkillId).toBe(0) expect(hud.hoveredPopupSkill).toEqual({ side: 'right', skillId: 0 }) // Press F5 while hovering -> rebinds F5 to Attack on right const boundRes = hud.triggerFunctionKey('F5') expect(boundRes).toEqual({ side: 'right', skillId: 0 }) expect(hud.getHotkey('F5')).toBe(0) // Close speedbar hud.handleClick(10, 10) expect(hud.selectionOpen).toBeNull() // Reset right skill to Blizzard (59) hud.assignSkill('right', 59) expect(hud.rightSkillId).toBe(59) // Trigger F5 hud.handleKeyDown('F5') expect(hud.rightSkillId).toBe(0) }) }) })