diablo2-web/tests/issue-506-equipment-restric...

624 lines
21 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import {
BARBARIAN_ONE_OR_TWO_HANDED_SWORD_CODES,
CANONICAL_CLASS_SPECIFIC_ITEM_CODES,
CANONICAL_ONE_HANDED_WEAPON_CODES,
CANONICAL_SHIELD_AND_OFFHAND_CODES,
EQUIP_SLOTS_LAYOUT,
INV_GRID_ORIGIN,
InventoryPanel,
STRICT_TWO_HANDED_WEAPON_CODES,
canPlayerUseUiItem,
classifyHandSlotItem,
getEffectiveItemReqClass,
itemToUiInventoryItem,
type UiInventoryItem,
} from '../src/ui/inventory.ts'
import { WorldPanelsHud } from '../src/ui/world-panels.ts'
import { D2FontRenderer } from '../src/ui/font.ts'
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts'
import { CLASS_STARTER_ATTRS, createStarterProfileForClass } from '../src/game/class-starter-profiles.ts'
import type { CharacterClassCode } from '../src/game/classes.ts'
function createMockCtx() {
const fillRectCalls: Array<{ x: number; y: number; w: number; h: number; fillStyle: string }> = []
let currentFillStyle = ''
const ctx = {
save: () => {},
restore: () => {},
fillRect: (x: number, y: number, w: number, h: number) => {
fillRectCalls.push({ x, y, w, h, fillStyle: currentFillStyle })
},
strokeRect: () => {},
drawImage: () => {},
beginPath: () => {},
moveTo: () => {},
lineTo: () => {},
stroke: () => {},
fillText: () => {},
measureText: (t: string) => ({ width: t.length * 8 }),
get fillStyle() {
return currentFillStyle
},
set fillStyle(val: string) {
currentFillStyle = val
},
strokeStyle: '',
lineWidth: 1,
font: '',
textAlign: 'left',
textBaseline: 'alphabetic',
} as unknown as CanvasRenderingContext2D
return { ctx, fillRectCalls }
}
function captureTooltipLines(
hud: WorldPanelsHud,
item: UiInventoryItem,
): Array<{ text: string; color: string; x: number; y: number }> {
const { ctx } = createMockCtx()
const font = new D2FontRenderer()
const drawnLines: Array<{ text: string; color: string; x: number; y: number }> = []
font.drawText = (_c, text, x, y, opts) => {
drawnLines.push({ text, color: opts?.color ?? 'white', x, y })
}
hud.drawItemTooltip(ctx, { item, x: 400, y: 300 }, font)
return drawnLines
}
describe('Issue #506: Diablo II v1.13c Character Equipment Restrictions & Red Requirement Display', () => {
it('verifies canonical 1.13c weapon, shield, and class-specific item code sets from MPQ data tables', () => {
const tables = getEmbeddedDropTables()
expect(BARBARIAN_ONE_OR_TWO_HANDED_SWORD_CODES.size).toBe(18)
expect(BARBARIAN_ONE_OR_TWO_HANDED_SWORD_CODES.has('2hs')).toBe(true)
expect(BARBARIAN_ONE_OR_TWO_HANDED_SWORD_CODES.has('7gd')).toBe(true) // Colossus Blade
expect(STRICT_TWO_HANDED_WEAPON_CODES.size).toBe(119)
expect(STRICT_TWO_HANDED_WEAPON_CODES.has('7m7')).toBe(true) // Ogre Maul (IK Maul)
expect(STRICT_TWO_HANDED_WEAPON_CODES.has('6lw')).toBe(true) // Hydra Bow
expect(STRICT_TWO_HANDED_WEAPON_CODES.has('6bs')).toBe(true) // Archon Staff
expect(CANONICAL_ONE_HANDED_WEAPON_CODES.has('crs')).toBe(true) // Crystal Sword
expect(CANONICAL_ONE_HANDED_WEAPON_CODES.has('7cr')).toBe(true) // Phase Blade
expect(CANONICAL_ONE_HANDED_WEAPON_CODES.has('7wa')).toBe(true) // Berserker Axe
expect(CANONICAL_SHIELD_AND_OFFHAND_CODES.has('uit')).toBe(true) // Monarch
expect(CANONICAL_SHIELD_AND_OFFHAND_CODES.has('paf')).toBe(true) // Vortex Shield
expect(CANONICAL_SHIELD_AND_OFFHAND_CODES.has('nea')).toBe(true) // Succubus Skull
expect(CANONICAL_CLASS_SPECIFIC_ITEM_CODES['oba']).toBe('sor')
expect(CANONICAL_CLASS_SPECIFIC_ITEM_CODES['baa']).toBe('bar')
expect(CANONICAL_CLASS_SPECIFIC_ITEM_CODES['paa']).toBe('pal')
expect(CANONICAL_CLASS_SPECIFIC_ITEM_CODES['ama']).toBe('ama')
expect(CANONICAL_CLASS_SPECIFIC_ITEM_CODES['nea']).toBe('nec')
expect(CANONICAL_CLASS_SPECIFIC_ITEM_CODES['dra']).toBe('dru')
expect(CANONICAL_CLASS_SPECIFIC_ITEM_CODES['ktr']).toBe('ass')
// Verify WeaponBase in embedded drop tables parses oneOrTwoHanded
const colossusBlade = tables.weapons.get('7gd')
expect(colossusBlade?.twoHanded).toBe(true)
expect(colossusBlade?.oneOrTwoHanded).toBe(true)
const ogreMaul = tables.weapons.get('7m7')
expect(ogreMaul?.twoHanded).toBe(true)
expect(ogreMaul?.oneOrTwoHanded).toBe(false)
})
it('Requirement 1: blocks equipping items exceeding character level, strength, dexterity, or class requirements', () => {
const inv = new InventoryPanel()
;(inv as unknown as { equipped: Record<string, unknown> }).equipped = {}
inv.setPlayerContext({
level: 40,
str: 80,
dex: 50,
classCode: 'sor',
})
// 1. Exceeds level requirement (`reqLevel: 62 > 40`)
const shako: UiInventoryItem = {
id: 'test-shako',
code: 'uap',
name: 'Harlequin Crest',
nameZh: '谐角之冠',
baseNameZh: '军帽',
quality: 'unique',
invFile: 'invcap',
invWidth: 2,
invHeight: 2,
allowedSlots: ['helm'],
reqLevel: 62,
reqStr: 50,
stats: [],
identified: true,
}
inv.cursorItem = shako
expect(inv.canEquipItemInSlot(shako, 'helm')).toBe(false)
expect(inv.clickEquipSlot('helm')).toBe(false)
expect(inv.equipped.helm).toBeUndefined()
expect(inv.cursorItem).toBe(shako)
// 2. Exceeds strength requirement (`reqStr: 156 > 80`)
const monarch: UiInventoryItem = {
id: 'test-monarch',
code: 'uit',
name: 'Stormshield',
nameZh: '暴风之盾',
baseNameZh: '统治者大盾',
quality: 'unique',
invFile: 'invkts',
invWidth: 2,
invHeight: 3,
allowedSlots: ['weapon2'],
reqLevel: 30,
reqStr: 156,
stats: [],
identified: true,
}
inv.cursorItem = monarch
expect(inv.canEquipItemInSlot(monarch, 'weapon2')).toBe(false)
expect(inv.clickEquipSlot('weapon2')).toBe(false)
expect(inv.equipped.weapon2).toBeUndefined()
// 3. Exceeds dexterity requirement (`reqDex: 136 > 50`)
const phaseBlade: UiInventoryItem = {
id: 'test-pb',
code: '7cr',
name: 'Phase Blade',
nameZh: '幻化之刃',
baseNameZh: '幻化之刃',
quality: 'normal',
invFile: 'invpbl',
invWidth: 2,
invHeight: 3,
allowedSlots: ['weapon1', 'weapon2'],
reqLevel: 30,
reqStr: 25,
reqDex: 136,
stats: [],
identified: true,
}
inv.cursorItem = phaseBlade
expect(inv.canEquipItemInSlot(phaseBlade, 'weapon1')).toBe(false)
expect(inv.clickEquipSlot('weapon1')).toBe(false)
expect(inv.equipped.weapon1).toBeUndefined()
// 4. Wrong character class (`reqClass: 'bar'` or canonical class code `baa` while player is `'sor'`)
const arreatsFace: UiInventoryItem = {
id: 'test-arreat',
code: 'baa',
name: "Arreat's Face",
nameZh: '亚瑞特的面容',
baseNameZh: '杀手防护面甲',
quality: 'unique',
invFile: 'invba2',
invWidth: 2,
invHeight: 2,
allowedSlots: ['helm'],
reqLevel: 20,
reqStr: 50,
stats: [],
identified: true,
}
expect(getEffectiveItemReqClass(arreatsFace)).toBe('bar')
inv.cursorItem = arreatsFace
expect(inv.canEquipItemInSlot(arreatsFace, 'helm')).toBe(false)
expect(inv.clickEquipSlot('helm')).toBe(false)
expect(inv.equipped.helm).toBeUndefined()
// Raise player stats and switch class to Barbarian: now Arreat's Face can be equipped!
inv.setPlayerContext({
level: 85,
str: 180,
dex: 150,
classCode: 'bar',
})
expect(inv.canEquipItemInSlot(arreatsFace, 'helm')).toBe(true)
expect(inv.clickEquipSlot('helm')).toBe(true)
expect(inv.equipped.helm?.id).toBe('test-arreat')
expect(inv.cursorItem).toBeNull()
})
it('Requirement 1 UI: renders unmet level, strength, dexterity, and class requirements in RED in tooltips and grid backdrops', () => {
const inv = new InventoryPanel()
const worldPanels = new WorldPanelsHud()
worldPanels.activePlayerInventory = inv
// Sorceress with low level, str, and dex inspecting a Barbarian-only item with high reqs
inv.setPlayerContext({
level: 25,
str: 40,
dex: 30,
classCode: 'sor',
})
const barbSword: UiInventoryItem = {
id: 'test-barb-item',
code: 'baa',
name: 'Slayer Guard',
nameZh: '杀手防护面甲',
baseNameZh: '杀手防护面甲',
quality: 'rare',
invFile: 'invba2',
invWidth: 2,
invHeight: 2,
allowedSlots: ['helm'],
reqLevel: 60,
reqStr: 118,
reqDex: 75,
stats: [{ text: '+2 野蛮人技能等级', color: 'blue' }],
identified: true,
}
// Check ZH tooltip lines
worldPanels.lang = 'CHI'
const zhLines = captureTooltipLines(worldPanels, barbSword)
const zhClassLine = zhLines.find(l => l.text === '(限野蛮人使用)')
const zhLevelLine = zhLines.find(l => l.text === '需要等级: 60')
const zhStrLine = zhLines.find(l => l.text === '需要力量: 118')
const zhDexLine = zhLines.find(l => l.text === '需要敏捷: 75')
expect(zhClassLine?.color).toBe('red')
expect(zhLevelLine?.color).toBe('red')
expect(zhStrLine?.color).toBe('red')
expect(zhDexLine?.color).toBe('red')
// Check EN tooltip lines
worldPanels.lang = 'ENG'
const enLines = captureTooltipLines(worldPanels, barbSword)
expect(enLines.find(l => l.text === '(Barbarian Only)')?.color).toBe('red')
expect(enLines.find(l => l.text === 'Required Level: 60')?.color).toBe('red')
expect(enLines.find(l => l.text === 'Required Strength: 118')?.color).toBe('red')
expect(enLines.find(l => l.text === 'Required Dexterity: 75')?.color).toBe('red')
// Check Inventory 10x4 grid backdrop is RED (`rgba(72, 18, 18, 0.50)`) when unmet
inv.visible = true
inv.gridItems = [{ item: barbSword, col: 0, row: 0 }]
const { ctx, fillRectCalls } = createMockCtx()
inv.draw(
ctx,
{
borderRightImg: null,
invSheetImg: null,
invTab0Img: null,
invTab1Img: null,
buySellBtnImg: null,
itemsAtlasImg: null,
itemRects: {},
},
new D2FontRenderer(),
)
const cellRect = fillRectCalls.find(
c => c.x === INV_GRID_ORIGIN.x + 1 && c.y === INV_GRID_ORIGIN.y + 1,
)
expect(cellRect?.fillStyle).toBe('rgba(72, 18, 18, 0.50)')
// Now update playerContext so all requirements are met
inv.setPlayerContext({
level: 85,
str: 150,
dex: 100,
classCode: 'bar',
})
worldPanels.lang = 'CHI'
const metLines = captureTooltipLines(worldPanels, barbSword)
expect(metLines.find(l => l.text === '(限野蛮人使用)')?.color).toBe('white')
expect(metLines.find(l => l.text === '需要等级: 60')?.color).toBe('white')
expect(metLines.find(l => l.text === '需要力量: 118')?.color).toBe('white')
expect(metLines.find(l => l.text === '需要敏捷: 75')?.color).toBe('white')
})
it('Requirement 2 (Default Non-Barbarian): allows 1H weapon + shield OR 1 two-handed weapon, blocks dual-wielding and 2H + shield', () => {
const inv = new InventoryPanel()
;(inv as unknown as { equipped: Record<string, unknown> }).equipped = {}
inv.setPlayerContext({
level: 85,
str: 200,
dex: 150,
classCode: 'pal', // Paladin (non-Barbarian)
})
const oneHandSword1: UiInventoryItem = {
id: '1h-sword-1',
code: '7cr', // Phase Blade (1H)
name: 'Grief Phase Blade',
nameZh: '悔恨之幻化之刃',
baseNameZh: '幻化之刃',
quality: 'unique',
invFile: 'invpbl',
invWidth: 2,
invHeight: 3,
allowedSlots: ['weapon1', 'weapon2'],
reqLevel: 59,
reqStr: 25,
reqDex: 136,
stats: [],
identified: true,
}
const oneHandSword2: UiInventoryItem = {
...oneHandSword1,
id: '1h-sword-2',
name: 'Azurewrath',
}
const shield: UiInventoryItem = {
id: 'pal-shield',
code: 'paa', // Herald of Zakarum (Paladin Shield)
name: 'Herald of Zakarum',
nameZh: '撒卡兰姆使者',
baseNameZh: '饰金盾牌',
quality: 'unique',
invFile: 'invpa2',
invWidth: 2,
invHeight: 4,
allowedSlots: ['weapon2'],
reqLevel: 42,
reqStr: 89,
reqClass: 'pal',
stats: [],
identified: true,
}
const twoHandedSword: UiInventoryItem = {
id: '2h-sword',
code: '7gd', // Colossus Blade (1or2handed=1)
name: 'The Grandfather',
nameZh: '祖父',
baseNameZh: '巨神之刃',
quality: 'unique',
invFile: 'invgsd',
invWidth: 2,
invHeight: 4,
allowedSlots: ['weapon1', 'weapon2'],
reqLevel: 81,
reqStr: 189,
reqDex: 110,
stats: [],
identified: true,
}
const strictTwoHandedPolearm: UiInventoryItem = {
id: '2h-polearm',
code: '7pa', // Cryptic Axe (strict 2H)
name: 'Tomb Reaver',
nameZh: '盗墓者',
baseNameZh: '神秘之斧',
quality: 'unique',
invFile: 'invpax',
invWidth: 2,
invHeight: 4,
allowedSlots: ['weapon1', 'weapon2'],
reqLevel: 84,
reqStr: 165,
reqDex: 103,
stats: [],
identified: true,
}
expect(classifyHandSlotItem(oneHandSword1)).toBe('one_handed_weapon')
expect(classifyHandSlotItem(shield)).toBe('offhand')
expect(classifyHandSlotItem(twoHandedSword)).toBe('barbarian_1or2h_weapon')
expect(classifyHandSlotItem(strictTwoHandedPolearm)).toBe('strict_two_handed_weapon')
// 1. Equip 1H weapon in weapon1 -> allowed
inv.cursorItem = oneHandSword1
expect(inv.clickEquipSlot('weapon1')).toBe(true)
expect(inv.equipped.weapon1?.id).toBe('1h-sword-1')
expect(inv.cursorItem).toBeNull()
// 2. Try to dual-wield second 1H weapon in weapon2 -> BLOCKED for non-Barbarian!
inv.cursorItem = oneHandSword2
expect(inv.canEquipItemInSlot(oneHandSword2, 'weapon2')).toBe(false)
expect(inv.clickEquipSlot('weapon2')).toBe(false)
expect(inv.equipped.weapon2).toBeUndefined()
expect(inv.cursorItem?.id).toBe('1h-sword-2')
// 3. Equip shield in weapon2 alongside 1H weapon in weapon1 -> ALLOWED!
inv.cursorItem = shield
expect(inv.canEquipItemInSlot(shield, 'weapon2')).toBe(true)
expect(inv.clickEquipSlot('weapon2')).toBe(true)
expect(inv.equipped.weapon2?.id).toBe('pal-shield')
expect(inv.cursorItem).toBeNull()
// 4. While shield is in weapon2, trying to put a 2H sword or 2H polearm into weapon1 -> BLOCKED!
inv.cursorItem = twoHandedSword
expect(inv.canEquipItemInSlot(twoHandedSword, 'weapon1')).toBe(false)
expect(inv.clickEquipSlot('weapon1')).toBe(false)
expect(inv.equipped.weapon1?.id).toBe('1h-sword-1')
inv.cursorItem = strictTwoHandedPolearm
expect(inv.canEquipItemInSlot(strictTwoHandedPolearm, 'weapon1')).toBe(false)
expect(inv.clickEquipSlot('weapon1')).toBe(false)
// 5. Unequip shield from weapon2; now weapon2 is empty, so 2H weapon CAN be swapped into weapon1!
inv.cursorItem = null
expect(inv.clickEquipSlot('weapon2')).toBe(true)
expect(inv.equipped.weapon2).toBeUndefined()
inv.cursorItem = strictTwoHandedPolearm
expect(inv.clickEquipSlot('weapon1')).toBe(true)
expect(inv.equipped.weapon1?.id).toBe('2h-polearm')
expect(inv.cursorItem?.id).toBe('1h-sword-1')
// 6. While 2H polearm is in weapon1, trying to equip shield or 1H weapon in weapon2 -> BLOCKED!
inv.cursorItem = shield
expect(inv.canEquipItemInSlot(shield, 'weapon2')).toBe(false)
expect(inv.clickEquipSlot('weapon2')).toBe(false)
expect(inv.equipped.weapon2).toBeUndefined()
})
it('Requirement 2 (Barbarian 1.13c): allows dual-wielding 1H weapons and 1or2handed two-handed swords, and 2H sword + shield, while blocking strict 2H dual-wielding', () => {
const inv = new InventoryPanel()
;(inv as unknown as { equipped: Record<string, unknown> }).equipped = {}
inv.setPlayerContext({
level: 85,
str: 235,
dex: 150,
classCode: 'bar',
})
const phaseBlade: UiInventoryItem = {
id: 'bar-1h-pb',
code: '7cr', // 1H sword
name: 'Phase Blade',
nameZh: '幻化之刃',
baseNameZh: '幻化之刃',
quality: 'normal',
invFile: 'invpbl',
invWidth: 2,
invHeight: 3,
allowedSlots: ['weapon1', 'weapon2'],
reqLevel: 54,
reqStr: 25,
reqDex: 136,
stats: [],
identified: true,
}
const colossusBlade1: UiInventoryItem = {
id: 'bar-2h-cb1',
code: '7gd', // Colossus Blade (`1or2handed = 1`)
name: 'Colossus Blade I',
nameZh: '巨神之刃 I',
baseNameZh: '巨神之刃',
quality: 'unique',
invFile: 'invgsd',
invWidth: 2,
invHeight: 4,
allowedSlots: ['weapon1', 'weapon2'],
reqLevel: 81,
reqStr: 189,
reqDex: 110,
stats: [],
identified: true,
}
const colossusBlade2: UiInventoryItem = {
...colossusBlade1,
id: 'bar-2h-cb2',
name: 'Colossus Blade II',
}
const stormshield: UiInventoryItem = {
id: 'bar-shield',
code: 'uit',
name: 'Stormshield',
nameZh: '暴风之盾',
baseNameZh: '统治者大盾',
quality: 'unique',
invFile: 'invkts',
invWidth: 2,
invHeight: 3,
allowedSlots: ['weapon2'],
reqLevel: 73,
reqStr: 156,
stats: [],
identified: true,
}
const ikMaul: UiInventoryItem = {
id: 'bar-ik-maul',
code: '7m7', // Ogre Maul (`2handed = 1, 1or2handed = 0`)
name: "Immortal King's Stone Crusher",
nameZh: '不朽之王的碎魂者',
baseNameZh: '食人魔之槌',
quality: 'set',
invFile: 'invgom',
invWidth: 2,
invHeight: 4,
allowedSlots: ['weapon1', 'weapon2'],
reqLevel: 76,
reqStr: 225,
stats: [],
identified: true,
}
// 1. Barbarian can dual-wield 1H weapon + 2H sword (`1or2handed=1`)
inv.cursorItem = phaseBlade
expect(inv.clickEquipSlot('weapon1')).toBe(true)
inv.cursorItem = colossusBlade1
expect(inv.canEquipItemInSlot(colossusBlade1, 'weapon2')).toBe(true)
expect(inv.clickEquipSlot('weapon2')).toBe(true)
expect(inv.equipped.weapon1?.id).toBe('bar-1h-pb')
expect(inv.equipped.weapon2?.id).toBe('bar-2h-cb1')
// 2. Barbarian can dual-wield two 2H swords (`Colossus Blade I` + `Colossus Blade II`)
inv.cursorItem = colossusBlade2
expect(inv.canEquipItemInSlot(colossusBlade2, 'weapon1')).toBe(true)
expect(inv.clickEquipSlot('weapon1')).toBe(true)
expect(inv.equipped.weapon1?.id).toBe('bar-2h-cb2')
expect(inv.equipped.weapon2?.id).toBe('bar-2h-cb1')
// 3. Barbarian can wield 2H sword in weapon1 + Stormshield in weapon2
inv.cursorItem = stormshield
expect(inv.canEquipItemInSlot(stormshield, 'weapon2')).toBe(true)
expect(inv.clickEquipSlot('weapon2')).toBe(true)
expect(inv.equipped.weapon1?.id).toBe('bar-2h-cb2')
expect(inv.equipped.weapon2?.id).toBe('bar-shield')
// 4. Barbarian CANNOT equip strict 2H Ogre Maul while Stormshield is in weapon2
inv.cursorItem = ikMaul
expect(inv.canEquipItemInSlot(ikMaul, 'weapon1')).toBe(false)
expect(inv.clickEquipSlot('weapon1')).toBe(false)
// 5. After removing Stormshield from weapon2, Barbarian CAN equip strict 2H Ogre Maul in weapon1,
// and while Ogre Maul is equipped, Barbarian CANNOT equip a 1H sword or 2H sword or shield in weapon2!
inv.cursorItem = null
expect(inv.clickEquipSlot('weapon2')).toBe(true) // unequip shield
inv.cursorItem = ikMaul
expect(inv.clickEquipSlot('weapon1')).toBe(true)
expect(inv.equipped.weapon1?.id).toBe('bar-ik-maul')
inv.cursorItem = phaseBlade
expect(inv.canEquipItemInSlot(phaseBlade, 'weapon2')).toBe(false)
expect(inv.clickEquipSlot('weapon2')).toBe(false)
inv.cursorItem = colossusBlade1
expect(inv.canEquipItemInSlot(colossusBlade1, 'weapon2')).toBe(false)
inv.cursorItem = stormshield
expect(inv.canEquipItemInSlot(stormshield, 'weapon2')).toBe(false)
})
it('verifies all 7 starter classes meet requirements for their starter equipped sets', () => {
const classes: CharacterClassCode[] = ['ama', 'sor', 'nec', 'pal', 'bar', 'dru', 'ass']
for (const classCode of classes) {
const attrs = CLASS_STARTER_ATTRS[classCode]
const profile = createStarterProfileForClass(classCode)
const ctx = {
level: attrs.level,
str: attrs.str,
dex: attrs.dex,
classCode,
}
for (const [slotId, item] of Object.entries(profile.equipped)) {
if (!item) continue
expect(
canPlayerUseUiItem(item, ctx),
`Starter ${classCode} item ${item.id} in slot ${slotId} should meet requirements`,
).toBe(true)
}
}
})
it('populates twoHanded and oneOrTwoHanded via itemToUiInventoryItem', () => {
const tables = getEmbeddedDropTables()
const cbBase = tables.weapons.get('7gd')!
const uiCb = itemToUiInventoryItem(
{
name: 'Colossus Blade',
base: cbBase,
quality: 2,
level: 85,
identified: true,
stats: {},
prefix: null,
suffix: null,
invWidth: 2,
invHeight: 4,
stack: 1,
value: 1000,
},
tables,
)
expect(uiCb.twoHanded).toBe(true)
expect(uiCb.oneOrTwoHanded).toBe(true)
expect(classifyHandSlotItem(uiCb)).toBe('barbarian_1or2h_weapon')
})
})