import { describe, it, expect, vi } from 'vitest' import { existsSync, readFileSync } from 'node:fs' import { join } from 'node:path' import { SkillTreePanel, SKILL_PANEL_ORIGIN } from '../src/ui/skill-tree-panel.ts' import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts' import type { D2FontRenderer } from '../src/ui/font.ts' describe('Issue #377: Authentic D2 v1.13c Skill Tree Tabs & Skill Points Banner', () => { it('manifest contains skillTabs and skillPoints image references', () => { expect(BAKED_UI_MANIFEST.images.skillTabs).toBe('/ui/skill-tree-tabs.png') expect(BAKED_UI_MANIFEST.images.skillPoints).toBe('/ui/skillpoints.png') const tabsPath = join(process.cwd(), 'public', 'ui', 'skill-tree-tabs.png') const spPath = join(process.cwd(), 'public', 'ui', 'skillpoints.png') expect(existsSync(tabsPath), 'skill-tree-tabs.png must exist in public/ui').toBe(true) expect(existsSync(spPath), 'skillpoints.png must exist in public/ui').toBe(true) }) it('skill-tree-tabs.png has authentic 192x432 strip dimensions (3 states x 64x432)', () => { const tabsPath = join(process.cwd(), 'public', 'ui', 'skill-tree-tabs.png') const buf = readFileSync(tabsPath) // PNG width and height at bytes 16..24 const width = buf.readUInt32BE(16) const height = buf.readUInt32BE(20) expect(width).toBe(192) expect(height).toBe(432) }) it('skillpoints.png has authentic 135x23 dimensions', () => { const spPath = join(process.cwd(), 'public', 'ui', 'skillpoints.png') const buf = readFileSync(spPath) const width = buf.readUInt32BE(16) const height = buf.readUInt32BE(20) expect(width).toBe(135) expect(height).toBe(23) }) it('draws authentic 64x432 tab column slice according to activeTab', () => { const panel = new SkillTreePanel() panel.visible = true const mockCtx = { save: vi.fn(), restore: vi.fn(), drawImage: vi.fn(), fillRect: vi.fn(), strokeRect: vi.fn(), fillStyle: '', strokeStyle: '', lineWidth: 1, globalAlpha: 1, } as unknown as CanvasRenderingContext2D const mockFont = { drawText: vi.fn(), measureText: vi.fn(() => 50), } as unknown as D2FontRenderer const mockTabsImg = { complete: true, naturalWidth: 192, naturalHeight: 432, } as unknown as HTMLImageElement const mockSpImg = { complete: true, naturalWidth: 135, naturalHeight: 23, } as unknown as HTMLImageElement // 1. Tab 2 (Cold Spells, Top Tab): slice sx = 0 panel.activeTab = 2 panel.draw(mockCtx, { borderRightImg: null, skillPointsImg: mockSpImg, buySellBtnImg: null, skillTabsImg: mockTabsImg, }, mockFont) expect(mockCtx.drawImage).toHaveBeenCalledWith( mockTabsImg, 0, // sx = 0 0, 64, 432, SKILL_PANEL_ORIGIN.x + 256, SKILL_PANEL_ORIGIN.y, 64, 432, ) // 2. Tab 1 (Lightning Spells, Middle Tab): slice sx = 64 vi.clearAllMocks() panel.activeTab = 1 panel.draw(mockCtx, { borderRightImg: null, skillPointsImg: mockSpImg, buySellBtnImg: null, skillTabsImg: mockTabsImg, }, mockFont) expect(mockCtx.drawImage).toHaveBeenCalledWith( mockTabsImg, 64, // sx = 64 0, 64, 432, SKILL_PANEL_ORIGIN.x + 256, SKILL_PANEL_ORIGIN.y, 64, 432, ) // 3. Tab 0 (Fire Spells, Bottom Tab): slice sx = 128 vi.clearAllMocks() panel.activeTab = 0 panel.draw(mockCtx, { borderRightImg: null, skillPointsImg: mockSpImg, buySellBtnImg: null, skillTabsImg: mockTabsImg, }, mockFont) expect(mockCtx.drawImage).toHaveBeenCalledWith( mockTabsImg, 128, // sx = 128 0, 64, 432, SKILL_PANEL_ORIGIN.x + 256, SKILL_PANEL_ORIGIN.y, 64, 432, ) }) it('draws authentic skillpoints.png banner when unspentSkillPoints > 0', () => { const panel = new SkillTreePanel() panel.visible = true panel.unspentSkillPoints = 5 const mockCtx = { save: vi.fn(), restore: vi.fn(), drawImage: vi.fn(), fillRect: vi.fn(), strokeRect: vi.fn(), fillStyle: '', strokeStyle: '', lineWidth: 1, globalAlpha: 1, } as unknown as CanvasRenderingContext2D const mockFont = { drawText: vi.fn(), measureText: vi.fn(() => 50), } as unknown as D2FontRenderer const mockSpImg = { complete: true, naturalWidth: 135, naturalHeight: 23, } as unknown as HTMLImageElement panel.draw(mockCtx, { borderRightImg: null, skillPointsImg: mockSpImg, buySellBtnImg: null, }, mockFont) // Verify skillpoints banner drawn at (ox + 60, oy + 395) expect(mockCtx.drawImage).toHaveBeenCalledWith( mockSpImg, SKILL_PANEL_ORIGIN.x + Math.round((256 - 135) / 2), SKILL_PANEL_ORIGIN.y + 395, 135, 23, ) // Verify when unspentSkillPoints = 0, banner is NOT drawn vi.clearAllMocks() panel.unspentSkillPoints = 0 panel.draw(mockCtx, { borderRightImg: null, skillPointsImg: mockSpImg, buySellBtnImg: null, }, mockFont) expect(mockCtx.drawImage).not.toHaveBeenCalledWith( mockSpImg, expect.anything(), expect.anything(), expect.anything(), expect.anything(), ) }) })