import { describe, expect, it, vi } from 'vitest' import { D2Rng } from '../src/game/d2-rng.ts' import { createDroppedItem, shouldSpawnIdentifiedByDefault, } from '../src/game/drop-pipeline.ts' import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts' import { GroundItemManager, isGroundSourceUnidentified, resolveGroundItemMetadata, } from '../src/game/ground-items.ts' import { calculateTransactionCost, TransactionType, } from '../src/game/item-cost.ts' import { ItemFlag, ItemQuality, type Item } from '../src/game/items.ts' import { VendorNpcId } from '../src/game/npc-table.ts' import { generateGambleInventory, generateVendorStoreItem, resolveGambledItem, } from '../src/game/vendor-generator.ts' import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts' import type { D2ColorCode, D2FontName, D2FontRenderer } from '../src/ui/font.ts' import { formatGroundItemLabelText, getGroundItemQualityColor, GROUND_LABEL_QUALITY_COLORS, } from '../src/ui/ground-labels.ts' import { EQUIP_SLOTS_LAYOUT, identifyUiItem, INV_GRID_ORIGIN, InventoryPanel, isUiItemUnidentified, itemToUiInventoryItem, resolveItemSpriteRect, type UiInventoryItem, } from '../src/ui/inventory.ts' import { WorldPanelsHud } from '../src/ui/world-panels.ts' function createMockTooltipCanvas() { const fontCalls: Array<{ text: string; color?: D2ColorCode | undefined; font?: D2FontName | undefined }> = [] const fillStyles: string[] = [] const ctx = { save: vi.fn(), restore: vi.fn(), drawImage: vi.fn(), fillRect: vi.fn(function (this: { fillStyle: string }) { fillStyles.push(this.fillStyle) }), strokeRect: vi.fn(), beginPath: vi.fn(), moveTo: vi.fn(), lineTo: vi.fn(), stroke: vi.fn(), fillText: vi.fn(), fillStyle: '', strokeStyle: '', lineWidth: 1, } as unknown as CanvasRenderingContext2D const font = { loaded: true, measureText: vi.fn((text: string) => text.length * 8), drawText: vi.fn( ( _ctx: CanvasRenderingContext2D, text: string, _x: number, _y: number, opts?: { font?: D2FontName; color?: D2ColorCode }, ) => { fontCalls.push({ text, color: opts?.color, font: opts?.font }) }, ), } as unknown as D2FontRenderer return { ctx, font, fontCalls, fillStyles } } describe('Diablo II v1.13c Unidentified Equipment Drops & Identification System', () => { const dropTables = getEmbeddedDropTables() const shakoBase = dropTables.getBase('uap')! const harlequinDef = dropTables.uniques.getEnabledByCode('uap')[0]! function makeDroppedShako(qualityTier: 'normal' | 'magic' | 'rare' | 'unique', seed = 101): Item { const rng = new D2Rng(seed) return createDroppedItem(shakoBase, qualityTier, { uniqueItem: qualityTier === 'unique' ? harlequinDef : undefined, itemTypes: dropTables.itemTypes, ilvl: 85, dwInitSeed: seed, itemRng: rng, }) } describe('1. Drop Pipeline Unidentified Spawn Rules (D2Game.dll / Items.cpp)', () => { it('spawns low, normal, superior, and quest items as identified, and magic/rare/set/unique/crafted items as unidentified', () => { expect(shakoBase).toBeDefined() expect(harlequinDef).toBeDefined() expect(shouldSpawnIdentifiedByDefault(shakoBase, 'low')).toBe(true) expect(shouldSpawnIdentifiedByDefault(shakoBase, 'normal')).toBe(true) expect(shouldSpawnIdentifiedByDefault(shakoBase, 'superior')).toBe(true) expect(shouldSpawnIdentifiedByDefault(shakoBase, 'magic')).toBe(false) expect(shouldSpawnIdentifiedByDefault(shakoBase, 'rare')).toBe(false) expect(shouldSpawnIdentifiedByDefault(shakoBase, 'set')).toBe(false) expect(shouldSpawnIdentifiedByDefault(shakoBase, 'unique')).toBe(false) expect(shouldSpawnIdentifiedByDefault(shakoBase, 'crafted' as any)).toBe(false) // Quest items (base.quest > 0) always spawn identified regardless of quality tier const cubeBase = dropTables.getBase('box')! expect(cubeBase.quest).toBeGreaterThan(0) expect(shouldSpawnIdentifiedByDefault(cubeBase, 'unique')).toBe(true) // Verify concrete createDroppedItem outputs const normalItem = makeDroppedShako('normal', 1) expect(normalItem.identified).toBe(true) expect(normalItem.flags?.identified).toBe(true) expect((normalItem.rawFlags! & ItemFlag.IDENTIFIED) !== 0).toBe(true) const magicItem = makeDroppedShako('magic', 2) expect(magicItem.identified).toBe(false) expect(magicItem.flags?.identified).toBe(false) expect((magicItem.rawFlags! & ItemFlag.IDENTIFIED) !== 0).toBe(false) const rareItem = makeDroppedShako('rare', 3) expect(rareItem.identified).toBe(false) expect(rareItem.flags?.identified).toBe(false) expect((rareItem.rawFlags! & ItemFlag.IDENTIFIED) !== 0).toBe(false) const uniqueShako = makeDroppedShako('unique', 4) expect(uniqueShako.quality).toBe(ItemQuality.UNIQUE) expect(uniqueShako.name).toBe('Harlequin Crest') expect(uniqueShako.identified).toBe(false) expect(uniqueShako.flags?.identified).toBe(false) expect((uniqueShako.rawFlags! & ItemFlag.IDENTIFIED) !== 0).toBe(false) }) }) describe('2. Vendor Store, Gambling Reveal & Unidentified Transaction Cost', () => { it('spawns vendor trade items and resolved gamble items as pre-identified', () => { const rng = new D2Rng(42) const capBase = dropTables.getBase('cap')! const vendorMagic = generateVendorStoreItem({ base: capBase, vendorId: VendorNpcId.CHARSI, qualityTier: 'magic', cLvl: 25, nItemLevel: 30, difficulty: 0, permStore: false, tables: dropTables, rng, }) expect(vendorMagic).not.toBeNull() expect(vendorMagic!.identified).toBe(true) expect(vendorMagic!.flags?.identified).toBe(true) expect((vendorMagic!.rawFlags! & ItemFlag.IDENTIFIED) !== 0).toBe(true) const gambleGrid = generateGambleInventory({ vendorId: VendorNpcId.GHEED, cLvl: 30, tables: dropTables, rng, }) const mystery = gambleGrid.list()[0]!.item expect(mystery.isGambleMystery).toBe(true) expect(mystery.identified).toBe(false) const gambled = resolveGambledItem({ mysteryItem: mystery, cLvl: 30, tables: dropTables, rng, }) expect(gambled.identified).toBe(true) expect(gambled.flags?.identified).toBe(true) expect(gambled.isGambleMystery).toBe(false) }) it('discounts sell price of unidentified magic/unique items until identified (0x6FD9B1C0)', () => { const uniqueShako = makeDroppedShako('unique', 7) expect(uniqueShako.identified).toBe(false) const unidSellCost = calculateTransactionCost({ item: uniqueShako, transactionType: TransactionType.SELL, vendorId: VendorNpcId.CHARSI, difficulty: 2, tables: dropTables, }) const idShako: Item = { ...uniqueShako, identified: true, flags: { ...uniqueShako.flags!, identified: true }, rawFlags: uniqueShako.rawFlags! | ItemFlag.IDENTIFIED, } const idSellCost = calculateTransactionCost({ item: idShako, transactionType: TransactionType.SELL, vendorId: VendorNpcId.CHARSI, difficulty: 2, tables: dropTables, }) expect(unidSellCost).toBeGreaterThan(0) expect(idSellCost).toBeGreaterThan(unidSellCost) }) }) describe('3. Ground Item Metadata & Alt Label Masking', () => { it('masks unidentified ground item labels to base item name while preserving quality color and underlying secret name', () => { const uniqueShako = makeDroppedShako('unique', 11) expect(isGroundSourceUnidentified(uniqueShako)).toBe(true) const meta = resolveGroundItemMetadata(uniqueShako) expect(meta.quality).toBe('unique') expect(meta.name).toBe('Shako') expect(meta.nameZh).toContain('军帽') expect(meta.nameZh).not.toContain('Harlequin Crest') expect(meta.nameZh).not.toContain('谐角之冠') const manager = new GroundItemManager() const groundEntity = manager.add(uniqueShako, 10, 10, 320, 240) expect(getGroundItemQualityColor(groundEntity)).toBe(GROUND_LABEL_QUALITY_COLORS.unique) const labelText = formatGroundItemLabelText(groundEntity) expect(labelText).toContain('军帽') expect(labelText).not.toContain('Harlequin Crest') expect(labelText).not.toContain('谐角之冠') // Underlying item's secret identity is untouched so picking it up and identifying it works expect(uniqueShako.name).toBe('Harlequin Crest') }) }) describe('4. UI Item Bridge, Sprite/Dye Masking, Tooltip Masking & Equipping Gate', () => { it('masks unique dye transform and custom unique invFile while unidentified and restores them upon identification', () => { const uniqueShako = makeDroppedShako('unique', 19) const uiShako = itemToUiInventoryItem(uniqueShako, dropTables) expect(isUiItemUnidentified(uiShako)).toBe(true) expect(uiShako.invtransform).toBeTruthy() expect(uiShako.baseInvFile).toBe('invcap') const unidSprite = resolveItemSpriteRect(uiShako) const baseCapSprite = BAKED_UI_MANIFEST.itemRects['invcap'] const dyedCapSprite = BAKED_UI_MANIFEST.itemRects[`invcap_${uiShako.invtransform}`] expect(unidSprite).toEqual(baseCapSprite) if (dyedCapSprite) { expect(unidSprite).not.toEqual(dyedCapSprite) } // Identify and verify dyed sprite is now resolved identifyUiItem(uiShako) expect(isUiItemUnidentified(uiShako)).toBe(false) if (dyedCapSprite) { expect(resolveItemSpriteRect(uiShako)).toEqual(dyedCapSprite) } }) it('renders red cell backdrop in InventoryPanel.draw and hides secret name/affixes in drawItemTooltip while unidentified', () => { const uniqueShako = makeDroppedShako('unique', 23) const uiShako = itemToUiInventoryItem(uniqueShako, dropTables) const inv = new InventoryPanel() inv.visible = true inv.gridItems = [{ item: uiShako, col: 0, row: 0 }] const { ctx, font, fontCalls, fillStyles } = createMockTooltipCanvas() inv.draw( ctx, { borderRightImg: null, invSheetImg: null, invTab0Img: null, invTab1Img: null, buySellBtnImg: null, itemsAtlasImg: null, itemRects: BAKED_UI_MANIFEST.itemRects, }, font, ) expect(fillStyles).toContain('rgba(72, 18, 18, 0.50)') // Hover tooltip while unidentified const wp = new WorldPanelsHud() fontCalls.length = 0 wp.drawItemTooltip(ctx, { item: uiShako, x: 450, y: 300 }, font, false) const texts = fontCalls.map(c => c.text) expect(texts[0]).toContain('军帽') expect(texts.some(t => t.includes('Harlequin Crest') || t.includes('谐角之冠'))).toBe(false) expect(texts).toContain('未辨识 (Unidentified)') expect(texts).toContain('无法使用 (Cannot be Equipped)') // Hover tooltip after identification identifyUiItem(uiShako) fontCalls.length = 0 wp.drawItemTooltip(ctx, { item: uiShako, x: 450, y: 300 }, font, false) const identifiedTexts = fontCalls.map(c => c.text) expect(identifiedTexts[0]).toContain('Harlequin Crest') expect(identifiedTexts).not.toContain('未辨识 (Unidentified)') expect(identifiedTexts).not.toContain('无法使用 (Cannot be Equipped)') expect(identifiedTexts.length).toBeGreaterThan(texts.length) }) }) describe('5. End-to-End Identification Flows: Scroll (isc), Tome (ibk), and Deckard Cain', () => { it('identifies an item using a Scroll of Identify (isc), consumes the scroll, and unlocks equipping', () => { const inv = new InventoryPanel() inv.visible = true delete inv.equipped.helm const uniqueShako = itemToUiInventoryItem(makeDroppedShako('unique', 31), dropTables) const iscBase = dropTables.getBase('isc')! const scrollItem = itemToUiInventoryItem( createDroppedItem(iscBase, 'normal', { itemTypes: dropTables.itemTypes, ilvl: 1, dwInitSeed: 32, itemRng: new D2Rng(32), }), dropTables, ) inv.gridItems = [ { item: uniqueShako, col: 0, row: 0 }, { item: scrollItem, col: 2, row: 0 }, ] // Right-click Scroll of Identify at (2, 0) via handleClick(button=2) to enter Identify Cursor Mode const scrollX = INV_GRID_ORIGIN.x + 2 * INV_GRID_ORIGIN.cellPx + 5 const scrollY = INV_GRID_ORIGIN.y + 5 expect(inv.handleClick(scrollX, scrollY, undefined, 2)).toBe(true) expect(inv.isIdentifyMode()).toBe(true) // Left-click the unidentified Shako at (0, 0) via handleClick(button=0) to identify it const shakoX = INV_GRID_ORIGIN.x + 5 const shakoY = INV_GRID_ORIGIN.y + 5 expect(inv.handleClick(shakoX, shakoY, undefined, 0)).toBe(true) expect(inv.isIdentifyMode()).toBe(false) expect(isUiItemUnidentified(uniqueShako)).toBe(false) expect((uniqueShako.rawItem!.rawFlags! & ItemFlag.IDENTIFIED) !== 0).toBe(true) // Scroll was consumed and removed from inventory expect(inv.gridItems.some(p => p.item.id === scrollItem.id)).toBe(false) // Now pick up the identified Shako and equip it into the helm slot expect(inv.clickGridCell(0, 0)).toBe(true) expect(inv.cursorItem?.id).toBe(uniqueShako.id) expect(inv.clickEquipSlot('helm')).toBe(true) expect((inv.equipped.helm as UiInventoryItem | undefined)?.id).toBe(uniqueShako.id) expect(inv.cursorItem).toBeNull() }) it('identifies an item using a Tome of Identify (ibk) and decrements tome quantity', () => { const inv = new InventoryPanel() inv.visible = true const rinBase = dropTables.getBase('rin')! const ibkBase = dropTables.getBase('ibk')! const rareRing = itemToUiInventoryItem( createDroppedItem(rinBase, 'rare', { itemTypes: dropTables.itemTypes, ilvl: 60, dwInitSeed: 41, itemRng: new D2Rng(41), }), dropTables, ) const tomeItem = itemToUiInventoryItem( { ...createDroppedItem(ibkBase, 'normal', { itemTypes: dropTables.itemTypes, ilvl: 1, dwInitSeed: 42, itemRng: new D2Rng(42), }), quantity: 5, stack: 5, }, dropTables, ) inv.gridItems = [ { item: rareRing, col: 0, row: 0 }, { item: tomeItem, col: 1, row: 0 }, ] const tomeX = INV_GRID_ORIGIN.x + 1 * INV_GRID_ORIGIN.cellPx + 5 const tomeY = INV_GRID_ORIGIN.y + 5 expect(inv.handleClick(tomeX, tomeY, undefined, 2)).toBe(true) expect(inv.isIdentifyMode()).toBe(true) const ringX = INV_GRID_ORIGIN.x + 5 const ringY = INV_GRID_ORIGIN.y + 5 expect(inv.handleClick(ringX, ringY, undefined, 0)).toBe(true) expect(isUiItemUnidentified(rareRing)).toBe(false) expect(tomeItem.quantity).toBe(4) expect(inv.gridItems.some(p => p.item.id === tomeItem.id)).toBe(true) }) it('identifies all unidentified items across Inventory, Stash, and Horadric Cube via Deckard Cain', () => { const inv = new InventoryPanel() const wp = new WorldPanelsHud() const rinBase = dropTables.getBase('rin')! const amuBase = dropTables.getBase('amu')! const bagUnid = itemToUiInventoryItem(makeDroppedShako('unique', 51), dropTables) const stashUnid = itemToUiInventoryItem( createDroppedItem(rinBase, 'rare', { itemTypes: dropTables.itemTypes, ilvl: 60, dwInitSeed: 52, itemRng: new D2Rng(52), }), dropTables, ) const cubeUnid = itemToUiInventoryItem( createDroppedItem(amuBase, 'magic', { itemTypes: dropTables.itemTypes, ilvl: 50, dwInitSeed: 53, itemRng: new D2Rng(53), }), dropTables, ) inv.gridItems = [{ item: bagUnid, col: 0, row: 0 }] wp.stashItems = [{ item: stashUnid, col: 0, row: 0 }] wp.cubeItems = [{ item: cubeUnid, col: 0, row: 0 }] expect(isUiItemUnidentified(bagUnid)).toBe(true) expect(isUiItemUnidentified(stashUnid)).toBe(true) expect(isUiItemUnidentified(cubeUnid)).toBe(true) const count = wp.identifyAllUiItems(inv) expect(count).toBe(3) expect(isUiItemUnidentified(bagUnid)).toBe(false) expect(isUiItemUnidentified(stashUnid)).toBe(false) expect(isUiItemUnidentified(cubeUnid)).toBe(false) expect(wp.identifyAllUiItems(inv)).toBe(0) }) }) })