import { describe, it, expect } from 'vitest' import { resolveGroundItemSpriteRect } from '../src/ui/inventory.ts' import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts' import { drawGroundItem } from '../src/scene/act-scene.ts' import type { GroundItemEntity } from '../src/game/ground-items.ts' import type { SpriteRenderer } from '../src/render/renderer.ts' import { GameEngine } from '../src/game/engine.ts' describe('Authentic Diablo II v1.13c Ground Items Flippy Parity', () => { describe('1. Flippy File Resolution Parity', () => { it('resolves authentic flippy ground sprites for weapons and armor', () => { const sword = resolveGroundItemSpriteRect({ code: 'ssd', name: 'Short Sword' }) expect(sword).toBeDefined() expect(sword).not.toBeNull() expect(sword).toEqual(BAKED_UI_MANIFEST.flippyRects['flpssd']) expect(sword!.w).toBe(27) expect(sword!.h).toBe(9) const axe = resolveGroundItemSpriteRect({ code: 'hax', name: 'Hand Axe' }) expect(axe).toEqual(BAKED_UI_MANIFEST.flippyRects['flphax']) expect(axe!.w).toBe(9) expect(axe!.h).toBe(13) const cap = resolveGroundItemSpriteRect({ code: 'cap', name: 'Cap' }) expect(cap).toEqual(BAKED_UI_MANIFEST.flippyRects['flpcap']) expect(cap!.w).toBe(11) expect(cap!.h).toBe(8) }) it('resolves gold piles into 4 authentic DC6 group sizes based on gold amount', () => { // Group 0: small pile (< 100 gold) const small = resolveGroundItemSpriteRect({ isGold: true, amount: 45, code: 'gld' }) expect(small).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_0']) expect(small!.w).toBe(24) expect(small!.h).toBe(7) // Group 1: medium pile (100 - 499 gold) const medium = resolveGroundItemSpriteRect({ isGold: true, amount: 250, code: 'gld' }) expect(medium).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_1']) expect(medium!.w).toBe(26) expect(medium!.h).toBe(13) // Group 2: large pile (500 - 4999 gold) const large = resolveGroundItemSpriteRect({ isGold: true, amount: 1500, code: 'gld' }) expect(large).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_2']) expect(large!.w).toBe(32) expect(large!.h).toBe(20) // Group 3: huge pile (>= 5000 gold) const huge = resolveGroundItemSpriteRect({ isGold: true, amount: 10000, code: 'gld' }) expect(huge).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_3']) expect(huge!.w).toBe(46) expect(huge!.h).toBe(23) }) it('resolves potions, scrolls, and runes to authentic flippy files', () => { const redPotion = resolveGroundItemSpriteRect({ code: 'hp1', name: 'Lesser Healing Potion' }) expect(redPotion).toEqual(BAKED_UI_MANIFEST.flippyRects['flprps']) const bluePotion = resolveGroundItemSpriteRect({ code: 'mp1', name: 'Lesser Mana Potion' }) expect(bluePotion).toEqual(BAKED_UI_MANIFEST.flippyRects['flpbps']) const rune = resolveGroundItemSpriteRect({ code: 'r01', name: 'El Rune' }) expect(rune).toEqual(BAKED_UI_MANIFEST.flippyRects['flprun']) expect(rune!.w).toBe(4) expect(rune!.h).toBe(4) const tpScroll = resolveGroundItemSpriteRect({ code: 'tsc', name: 'Town Portal Scroll' }) expect(tpScroll).toEqual(BAKED_UI_MANIFEST.flippyRects['flpbsc']) }) it('resolves unique items and charms to authentic ground art', () => { const anni = resolveGroundItemSpriteRect({ code: 'cm1', name: 'Annihilus' }) expect(anni).toEqual(BAKED_UI_MANIFEST.flippyRects['flpmss']) const torch = resolveGroundItemSpriteRect({ code: 'cm2', name: 'Hellfire Torch' }) expect(torch).toEqual(BAKED_UI_MANIFEST.flippyRects['flptrch']) const cube = resolveGroundItemSpriteRect({ code: 'box', name: 'Horadric Cube' }) expect(cube).toEqual(BAKED_UI_MANIFEST.flippyRects['flpbox']) }) it('resolves color-transformed items to dyed ground flippy sprites', () => { const shako = resolveGroundItemSpriteRect({ code: 'uap', invFile: 'invcap', invtransform: 'cgrn', name: 'Harlequin Crest', }) expect(shako).toEqual(BAKED_UI_MANIFEST.flippyRects['flpcap_cgrn']) }) }) describe('2. Ground Item Rendering with Atlas Parity', () => { it('renders authentic Blizzard flippy sprite with 1:1 isometric dimensions and ground shadow', () => { const drawnQuads: { x: number; y: number; w: number; h: number; color: readonly [number, number, number, number] }[] = [] const drawnSprites: { frame: any; x: number; y: number; options: any }[] = [] const renderer = { drawSolid(x: number, y: number, w: number, h: number, color: readonly [number, number, number, number]) { drawnQuads.push({ x, y, w, h, color }) }, draw(frame: any, x: number, y: number, options: any) { drawnSprites.push({ frame, x, y, options }) }, } as unknown as SpriteRenderer const mockAtlas = { texture: {} as any, width: 1024, height: 2145 } as any const groundSword: GroundItemEntity = { id: 'ground_sword_test', item: { name: 'Short Sword', code: 'ssd' }, name: 'Short Sword', nameZh: '短剑', quality: 'magic', isGold: false, amount: 1, invWidth: 1, invHeight: 3, dropTime: 0, x: 300, y: 250, cellX: 0, cellY: 0, sparklePhase: 0, } drawGroundItem(renderer, groundSword, 1000, mockAtlas) // Verified: Exactly 1 sprite drawn via renderer.draw using flpssd rect expect(drawnSprites.length).toBe(1) const sprite = drawnSprites[0]! expect(sprite.options.atlas).toBe(mockAtlas) expect(sprite.options.width).toBe(27) expect(sprite.options.height).toBe(9) expect(sprite.x).toBe(Math.round(300 - 27 / 2)) expect(sprite.y).toBe(250 - 9) // Ground contact shadow is drawn at item.y - 2 expect(drawnQuads.length).toBe(1) const shadow = drawnQuads[0]! expect(shadow.y).toBe(250 - 2) expect(shadow.color[3]).toBeLessThanOrEqual(0.4) // Subtle shadow alpha // Zero blue bounding boxes const blueBox = drawnQuads.find(q => q.color[2] > 0.8 && q.w > 10 && q.h > 10) expect(blueBox).toBeUndefined() // Zero white sparkle star quads const whiteGlint = drawnQuads.find(q => q.color[0] === 1.0 && q.color[1] === 1.0 && q.color[2] === 1.0) expect(whiteGlint).toBeUndefined() }) it('renders authentic flpgld sprite for dropped gold when atlas is present', () => { const drawnQuads: any[] = [] const drawnSprites: any[] = [] const renderer = { drawSolid(x: number, y: number, w: number, h: number, color: any) { drawnQuads.push({ x, y, w, h, color }) }, draw(frame: any, x: number, y: number, options: any) { drawnSprites.push({ frame, x, y, options }) }, } as unknown as SpriteRenderer const mockAtlas = { texture: {} as any, width: 1024, height: 2145 } as any const groundGold: GroundItemEntity = { id: 'gold_huge_test', item: { isGold: true }, name: '5000 Gold', nameZh: '5000 金币', quality: 'normal', isGold: true, amount: 5000, invWidth: 1, invHeight: 1, dropTime: 0, x: 400, y: 350, cellX: 0, cellY: 0, sparklePhase: 0, } drawGroundItem(renderer, groundGold, 1000, mockAtlas) expect(drawnSprites.length).toBe(1) const goldSprite = drawnSprites[0]! // Group 3 huge pile: 46x23 expect(goldSprite.options.width).toBe(46) expect(goldSprite.options.height).toBe(23) expect(goldSprite.x).toBe(Math.round(400 - 46 / 2)) expect(goldSprite.y).toBe(350 - 23) }) }) describe('3. Clean Pickup Lifecycle (Zero Remaining Dots / Artifacts)', () => { it('completely cleans groundItems on engine pickup tick without leaving orphaned entities', () => { const engine = new GameEngine( { widthPx: 1000, heightPx: 1000, overlap: () => 0 }, { spawn: { x: 100, y: 100 }, stats: [], xpTable: [], skills: [], npcDefs: [], questDefs: [], combatOptions: { playerSpeed: 4, playerReach: 50, playerCooldownTicks: 10, playerDamage: 5, playerManaPerAttack: 1, respawnTicks: 100, }, talkRadius: 50, pickupRadius: 30, inventoryCols: 10, inventoryRows: 4, }, ) engine.world.player.x = 100 engine.world.player.y = 100 const droppedItem = { id: 'test_drop_item', code: 'hax', name: 'Hand Axe', invWidth: 1, invHeight: 3, } // Drop item near player engine.dropItem(droppedItem, 105, 105, 5, 5) expect(engine.groundItems.count).toBe(1) expect(engine.ground.length).toBe(1) // Tick with pickup intent engine.tick({ pickingUp: true, movement: { x: 0, y: 0 }, attacking: false, talking: false, digits: [], saving: false, loading: false, }) // Verified: Cleanly removed from both collections expect(engine.groundItems.count).toBe(0) expect(engine.ground.length).toBe(0) expect(engine.bag.contents.some(p => (p.item as any).id === 'test_drop_item')).toBe(true) }) }) })