199 lines
5.4 KiB
TypeScript
199 lines
5.4 KiB
TypeScript
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(),
|
|
)
|
|
})
|
|
})
|