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 3 authentic DC6 graphic tiers based on gold amount', () => { // Tier 0: small pile (1 - 49 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) // Tier 1: medium pile (50 - 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) // Tier 2: large pile (500+ 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) const huge = resolveGroundItemSpriteRect({ isGold: true, amount: 10000, code: 'gld' }) expect(huge).toEqual(BAKED_UI_MANIFEST.flippyRects['flpgld_2']) expect(huge!.w).toBe(32) expect(huge!.h).toBe(20) }) 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]! // Tier 2 large pile (500+): 32x20 expect(goldSprite.options.width).toBe(32) expect(goldSprite.options.height).toBe(20) expect(goldSprite.x).toBe(Math.round(400 - 32 / 2)) expect(goldSprite.y).toBe(350 - 20) }) }) 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) }) }) describe('4. Full-Spectrum 100% Drop Table Flippy DC6 Coverage & Anti-Color-Box Parity (Issue #423)', () => { it('resolves valid non-empty flippyRects for 100% of weapons, armor, and misc bases in embedded drop tables', async () => { const { getEmbeddedDropTables } = await import('../src/game/embedded-drop-tables.ts') const tables = getEmbeddedDropTables() const allBases = [ ...tables.weapons.all, ...tables.armor.all, ...tables.misc.all, ] expect(allBases.length).toBeGreaterThanOrEqual(500) for (const base of allBases) { const sr = resolveGroundItemSpriteRect( { code: base.code, name: base.name, flippyFile: (base as any).flippyfile, invFile: (base as any).invfile }, BAKED_UI_MANIFEST.flippyRects, BAKED_UI_MANIFEST.codeToFlippyFile, ) expect(sr, `Expected flippy sprite rect for base ${base.code} (${base.name})`).not.toBeNull() expect(sr!.w).toBeGreaterThan(0) expect(sr!.h).toBeGreaterThan(0) } }) it('never draws procedural colored boxes even when itemsAtlas is undefined', () => { 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 groundSword: GroundItemEntity = { id: 'no_atlas_sword', 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, undefined) expect(drawnSprites.length).toBe(1) expect(drawnQuads.length).toBe(1) // Only the drop shadow expect(drawnQuads[0]!.color[0]).toBe(0) expect(drawnQuads[0]!.color[1]).toBe(0) expect(drawnQuads[0]!.color[2]).toBe(0) }) }) })