/** * Diablo II: Lord of Destruction v1.13c — Ground Item Rendering & Glint Sparkle Test Suite * * Requirements (Issue #389): * 1. Isometric depth sorting & entity queue integration. * 2. Adaptive item & gold icon silhouettes with bottom-center anchor and shadows. * 3. Authentic quality colors: normal, magic, rare, set, unique, craft, rune, gold. * 4. Periodic 4-point star glint & sparkle animation with sine easing. * 5. Parabolic bounce vertical translation. */ import { describe, expect, it } from 'vitest' import { drawGroundItem, GROUND_ITEM_QUALITY_COLORS, } from '../src/scene/act-scene.ts' import type { GroundItemEntity } from '../src/game/ground-items.ts' import type { SpriteRenderer } from '../src/render/renderer.ts' describe('Issue #389 — Ground Item Rendering & Glint Sparkle Ground Truth', () => { const createMockRenderer = () => { const drawnQuads: { x: number; y: number; w: number; h: number; color: readonly [number, number, number, number] }[] = [] const renderer = { drawSolid(x: number, y: number, w: number, h: number, color: readonly [number, number, number, number]) { drawnQuads.push({ x, y, w, h, color }) }, } as unknown as SpriteRenderer return { renderer, drawnQuads } } describe('1. GROUND_ITEM_QUALITY_COLORS Palette Parity', () => { it('provides distinct, authentic RGBA color tuples for all D2 quality tiers', () => { const requiredQualities = ['normal', 'low', 'superior', 'magic', 'rare', 'set', 'unique', 'craft', 'rune', 'gold'] for (const q of requiredQualities) { const col = GROUND_ITEM_QUALITY_COLORS[q] expect(col).toBeDefined() expect(col!.length).toBe(4) for (const c of col!) { expect(c).toBeGreaterThanOrEqual(0) expect(c).toBeLessThanOrEqual(1) } } // Magic is blue expect(GROUND_ITEM_QUALITY_COLORS.magic![2]).toBeGreaterThan(GROUND_ITEM_QUALITY_COLORS.magic![0]) // Rare is yellow (red & green high, blue low) expect(GROUND_ITEM_QUALITY_COLORS.rare![0]).toBeGreaterThan(0.8) expect(GROUND_ITEM_QUALITY_COLORS.rare![1]).toBeGreaterThan(0.8) // Set is green expect(GROUND_ITEM_QUALITY_COLORS.set![1]).toBeGreaterThan(GROUND_ITEM_QUALITY_COLORS.set![0]) // Unique is gold/tan expect(GROUND_ITEM_QUALITY_COLORS.unique![0]).toBeGreaterThan(0.7) }) }) describe('2. Gold Pile Rendering & Scaling', () => { it('draws small gold pile (< 100 gold) with shadow, body, and coin rim', () => { const { renderer, drawnQuads } = createMockRenderer() const goldItem: GroundItemEntity = { id: 'gold_small', item: { isGold: true }, name: '50 Gold', nameZh: '50 金币', quality: 'normal', isGold: true, amount: 50, invWidth: 1, invHeight: 1, dropTime: 0, x: 200, y: 150, cellX: 0, cellY: 0, sparklePhase: 0, } // Use a timestamp outside the sparkle window (e.g. 1000ms in a 2000ms cycle) drawGroundItem(renderer, goldItem, 1000) // At least 3 quads: shadow, body, highlight rim expect(drawnQuads.length).toBeGreaterThanOrEqual(3) const shadow = drawnQuads[0]! expect(shadow.w).toBe(12) // w + 2 = 10 + 2 = 12 expect(shadow.color[3]).toBeLessThan(0.5) // shadow alpha const body = drawnQuads[1]! expect(body.w).toBe(10) expect(body.h).toBe(8) // Gold color expect(body.color[0]).toBeGreaterThan(0.8) expect(body.color[1]).toBeGreaterThan(0.6) }) it('scales gold pile dimensions for large piles (>= 5000 gold)', () => { const { renderer, drawnQuads } = createMockRenderer() const largeGold: GroundItemEntity = { id: 'gold_large', item: { isGold: true }, name: '10000 Gold', nameZh: '10000 金币', quality: 'normal', isGold: true, amount: 10000, invWidth: 1, invHeight: 1, dropTime: 0, x: 300, y: 250, cellX: 0, cellY: 0, sparklePhase: 0, } drawGroundItem(renderer, largeGold, 1000) const body = drawnQuads[1]! expect(body.w).toBe(18) expect(body.h).toBe(13) }) }) describe('3. Equipment Item Rendering with Quality Framing & Bounce Offset', () => { it('draws equipment item with shadow, quality body, and 4 border edges', () => { const { renderer, drawnQuads } = createMockRenderer() const uniqueSword: GroundItemEntity = { id: 'unique_sword', item: { name: 'The Patriarch' }, name: 'The Patriarch', nameZh: '族长', quality: 'unique', isGold: false, amount: 1, invWidth: 1, invHeight: 3, dropTime: 0, x: 400, y: 350, cellX: 0, cellY: 0, sparklePhase: 0, } drawGroundItem(renderer, uniqueSword, 1000) // Shadow + body + 4 border edges = 6 quads expect(drawnQuads.length).toBeGreaterThanOrEqual(6) const body = drawnQuads[1]! const topBorder = drawnQuads[2]! // Unique color tint const uniqueColor = GROUND_ITEM_QUALITY_COLORS.unique! expect(topBorder.color[0]).toBeCloseTo(uniqueColor[0], 2) expect(topBorder.color[1]).toBeCloseTo(uniqueColor[1], 2) }) it('translates vertical rendering position upwards during parabolic bounce', () => { const { renderer: rGround, drawnQuads: quadsGround } = createMockRenderer() const { renderer: rAir, drawnQuads: quadsAir } = createMockRenderer() const restingItem: GroundItemEntity = { id: 'item_rest', item: {}, name: 'Helm', nameZh: '头盔', quality: 'magic', isGold: false, amount: 1, invWidth: 2, invHeight: 2, dropTime: 0, x: 100, y: 200, cellX: 0, cellY: 0, sparklePhase: 0, } const bouncingItem: GroundItemEntity = { ...restingItem, id: 'item_bounce', bounceState: { startTime: 1000, durationMs: 400, peakHeightPx: 30, }, } // Draw resting item at t = 1200 drawGroundItem(rGround, restingItem, 1200) // Draw bouncing item at apex (t = 1200, apex = 30px) drawGroundItem(rAir, bouncingItem, 1200) const groundBody = quadsGround[1]! const airBody = quadsAir[1]! // air body y should be exactly ground body y - 30 expect(airBody.y).toBe(groundBody.y - 30) }) }) describe('4. Glint & Sparkle 4-Point Star Animation', () => { it('spawns 4-point star rays, core, and quality aura during the active flash window', () => { const { renderer, drawnQuads } = createMockRenderer() const gem: GroundItemEntity = { id: 'gem_ruby', item: {}, name: 'Perfect Ruby', nameZh: '完美红宝石', quality: 'rare', isGold: false, amount: 1, invWidth: 1, invHeight: 1, dropTime: 0, x: 500, y: 400, cellX: 0, cellY: 0, sparklePhase: 0, } // At t = 160ms (middle of flash window 0..320ms, p = 0.5, intensity = 1.0) drawGroundItem(renderer, gem, 160) // Total quads: base item (6) + sparkle (horizontal ray, vertical ray, core, aura = 4) = 10 expect(drawnQuads.length).toBe(10) const horizontalRay = drawnQuads[6]! const verticalRay = drawnQuads[7]! const core = drawnQuads[8]! const aura = drawnQuads[9]! // Brilliant core color (near-white) expect(core.color[0]).toBeGreaterThan(0.9) expect(core.color[1]).toBeGreaterThan(0.9) expect(core.color[3]).toBeCloseTo(1.0, 1) // Rays are centered on sparkle point expect(horizontalRay.h).toBe(2) expect(verticalRay.w).toBe(2) expect(horizontalRay.w).toBeGreaterThan(10) expect(verticalRay.h).toBeGreaterThan(10) }) it('suppresses sparkle rendering during the dormant period of the cycle', () => { const { renderer, drawnQuads } = createMockRenderer() const item: GroundItemEntity = { id: 'item_dormant', item: {}, name: 'Ring', nameZh: '戒指', quality: 'unique', isGold: false, amount: 1, invWidth: 1, invHeight: 1, dropTime: 0, x: 200, y: 200, cellX: 0, cellY: 0, sparklePhase: 0, } // At t = 1000ms (far outside 0..320ms window) drawGroundItem(renderer, item, 1000) // Only base item quads (6), no sparkle quads expect(drawnQuads.length).toBe(6) }) }) })