diablo2-web/tests/gems-runes-parity.test.ts

288 lines
9.9 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
formatItemTooltip,
GEM_INFO,
RUNE_INFO,
getSocketableInfo,
isGemCode,
isRuneCode,
} from '../src/game/item-tooltip.ts'
import type { Item } from '../src/game/items.ts'
describe('Diablo II 1.13c Gems & Runes Authentic Display Parity', () => {
describe('1. Perfect Skull (skz) Screenshot Parity', () => {
const perfectSkullItem: Item = {
name: 'Perfect Skull',
code: 'skz',
base: { id: 'skz', name: 'Perfect Skull', kind: 'misc' } as any,
prefix: null,
suffix: null,
level: 18,
ilvl: 18,
stats: {},
invWidth: 1,
invHeight: 1,
stack: 1,
value: 1000,
}
it('1.1 English format strictly matches screenshot structure and text', () => {
const tt = formatItemTooltip(perfectSkullItem, 'en')
// Title & Quality
expect(tt.title).toBe('Perfect Skull')
expect(tt.subTitle).toBeUndefined()
expect(tt.quality).toBe('Gem')
expect(tt.qualityColor).toBe('#ffffff')
expect(tt.baseType).toBe('Gem')
expect(tt.isSocketable).toBe(true)
// 6 Lines total: Instruction + 4 slots + Required Level
expect(tt.lines).toHaveLength(6)
// Line 0: Instruction
expect(tt.lines[0]).toEqual({
text: 'Can be Inserted into Socketed Items',
color: 'instruction',
})
// Lines 1..4: The 4 distinct slots in pure white
expect(tt.lines[1]).toEqual({
text: 'Weapons: 4% Life Stolen Per Hit, 3% Mana Stolen Per Hit',
color: 'white',
})
expect(tt.lines[2]).toEqual({
text: 'Armor: Regenerate Mana 19%, Replenish Life +5',
color: 'white',
})
expect(tt.lines[3]).toEqual({
text: 'Helms: Regenerate Mana 19%, Replenish Life +5',
color: 'white',
})
expect(tt.lines[4]).toEqual({
text: 'Shields: Attacker Takes Damage of 20',
color: 'white',
})
// Line 5: Required Level in Red
expect(tt.lines[5]).toEqual({
text: 'Required Level: 18',
color: 'red',
})
expect(tt.reqLevel).toBe(18)
})
it('1.2 Simplified Chinese format has authentic terminology and red required level', () => {
const tt = formatItemTooltip(perfectSkullItem, 'zh')
expect(tt.title).toBe('完美的骷髅')
expect(tt.subTitle).toBeUndefined()
expect(tt.quality).toBe('宝石')
expect(tt.qualityColor).toBe('#ffffff')
expect(tt.baseType).toBe('宝石')
expect(tt.isSocketable).toBe(true)
expect(tt.lines).toHaveLength(6)
expect(tt.lines[0]?.text).toBe('可镶嵌进有凹槽的装备')
expect(tt.lines[1]).toEqual({
text: '武器: 每次命中偷取 4% 生命, 每次命中偷取 3% 法力',
color: 'white',
})
expect(tt.lines[2]).toEqual({
text: '防具: 法力恢复速度 19%, 生命恢复 +5',
color: 'white',
})
expect(tt.lines[3]).toEqual({
text: '头盔: 法力恢复速度 19%, 生命恢复 +5',
color: 'white',
})
expect(tt.lines[4]).toEqual({
text: '盾牌: 攻击者受到伤害 20',
color: 'white',
})
expect(tt.lines[5]).toEqual({
text: '需要等级: 18',
color: 'red',
})
})
it('1.3 Bilingual format includes both English and Chinese names', () => {
const tt = formatItemTooltip(perfectSkullItem, 'bilingual')
expect(tt.title).toBe('Perfect Skull (完美的骷髅)')
expect(tt.lines[0]?.text).toBe('可镶嵌进有凹槽的装备')
expect(tt.lines[5]?.text).toBe('需要等级: 18')
})
})
describe('2. Full Spectrum 35 Gems Verification', () => {
it('2.1 Exactly 35 gems exist across 7 types and 5 tiers', () => {
const codes = Object.keys(GEM_INFO)
expect(codes).toHaveLength(35)
})
it('2.2 Every gem formats correctly with 4 distinct slots and red reqLevel', () => {
for (const [code, info] of Object.entries(GEM_INFO)) {
const item: Item = {
name: info.nameEn,
code,
base: { id: code, name: info.nameEn, kind: 'misc' } as any,
prefix: null,
suffix: null,
level: info.reqLevel,
ilvl: info.reqLevel,
stats: {},
invWidth: 1,
invHeight: 1,
stack: 1,
value: 100,
}
const ttEn = formatItemTooltip(item, 'en')
expect(ttEn.title).toBe(info.nameEn)
expect(ttEn.qualityColor).toBe('#ffffff')
expect(ttEn.isSocketable).toBe(true)
expect(ttEn.lines[0]?.text).toBe('Can be Inserted into Socketed Items')
expect(ttEn.lines[1]?.text).toBe(`Weapons: ${info.weaponsEn}`)
expect(ttEn.lines[2]?.text).toBe(`Armor: ${info.armorEn}`)
expect(ttEn.lines[3]?.text).toBe(`Helms: ${info.helmsEn}`)
expect(ttEn.lines[4]?.text).toBe(`Shields: ${info.shieldsEn}`)
expect(ttEn.lines[5]?.text).toBe(`Required Level: ${info.reqLevel}`)
expect(ttEn.lines[5]?.color).toBe('red')
const ttZh = formatItemTooltip(item, 'zh')
expect(ttZh.title).toBe(info.nameZh)
expect(ttZh.lines[0]?.text).toBe('可镶嵌进有凹槽的装备')
expect(ttZh.lines[1]?.text).toBe(`武器: ${info.weaponsZh}`)
expect(ttZh.lines[2]?.text).toBe(`防具: ${info.armorZh}`)
expect(ttZh.lines[3]?.text).toBe(`头盔: ${info.helmsZh}`)
expect(ttZh.lines[4]?.text).toBe(`盾牌: ${info.shieldsZh}`)
expect(ttZh.lines[5]?.text).toBe(`需要等级: ${info.reqLevel}`)
}
})
})
describe('3. Full Spectrum 33 Runes Verification', () => {
it('3.1 Exactly 33 runes exist (r01..r33)', () => {
const runeKeys = Object.keys(RUNE_INFO)
expect(runeKeys).toHaveLength(33)
for (let i = 1; i <= 33; i++) {
const key = `r${String(i).padStart(2, '0')}`
expect(RUNE_INFO[key]).toBeDefined()
expect(RUNE_INFO[key]?.num).toBe(i)
}
})
it('3.2 Hel Rune (r15) reqLevel=0 suppresses Required Level line', () => {
const helItem: Item = {
name: 'Hel Rune',
code: 'r15',
base: { id: 'r15', name: 'Hel Rune', kind: 'misc' } as any,
prefix: null,
suffix: null,
level: 1,
ilvl: 1,
stats: {},
invWidth: 1,
invHeight: 1,
stack: 1,
value: 100,
}
const tt = formatItemTooltip(helItem, 'en')
expect(tt.title).toBe('Hel Rune')
expect(tt.reqLevel).toBeUndefined()
// Exactly 5 lines: instruction + 4 slots (no Required Level line)
expect(tt.lines).toHaveLength(5)
expect(tt.lines.some(l => l.text.includes('Required Level'))).toBe(false)
})
it('3.3 Ber Rune (r30) formats with orange title and correct 4 slots', () => {
const berItem: Item = {
name: 'Ber Rune',
code: 'r30',
base: { id: 'r30', name: 'Ber Rune', kind: 'misc' } as any,
prefix: null,
suffix: null,
level: 63,
ilvl: 63,
stats: {},
invWidth: 1,
invHeight: 1,
stack: 1,
value: 100,
}
const ttEn = formatItemTooltip(berItem, 'en')
expect(ttEn.title).toBe('Ber Rune')
expect(ttEn.subTitle).toBe('Rune (30)')
expect(ttEn.qualityColor).toBe('#ff8c00')
expect(ttEn.lines[1]?.text).toBe('Weapons: 20% Chance of Crushing Blow')
expect(ttEn.lines[2]?.text).toBe('Armor: Damage Reduced by 8%')
expect(ttEn.lines[3]?.text).toBe('Helms: Damage Reduced by 8%')
expect(ttEn.lines[4]?.text).toBe('Shields: Damage Reduced by 8%')
expect(ttEn.lines[5]?.text).toBe('Required Level: 63')
expect(ttEn.lines[5]?.color).toBe('red')
const ttZh = formatItemTooltip(berItem, 'zh')
expect(ttZh.title).toBe('贝符文')
expect(ttZh.subTitle).toBe('符文 (30)')
expect(ttZh.qualityColor).toBe('#ff8c00')
expect(ttZh.lines[1]?.text).toBe('武器: 20% 几率造成压碎打击')
expect(ttZh.lines[2]?.text).toBe('防具: 伤害减少 8%')
expect(ttZh.lines[3]?.text).toBe('头盔: 伤害减少 8%')
expect(ttZh.lines[4]?.text).toBe('盾牌: 伤害减少 8%')
expect(ttZh.lines[5]?.text).toBe('需要等级: 63')
})
it('3.4 Jah Rune (r31) correctly differentiates Armor/Helms (+5% max life) vs Shields (+50 life)', () => {
const jahItem: Item = {
name: 'Jah Rune',
code: 'r31',
base: { id: 'r31', name: 'Jah Rune', kind: 'misc' } as any,
prefix: null,
suffix: null,
level: 65,
ilvl: 65,
stats: {},
invWidth: 1,
invHeight: 1,
stack: 1,
value: 100,
}
const tt = formatItemTooltip(jahItem, 'en')
expect(tt.lines[1]?.text).toBe("Weapons: Ignore Target's Defense")
expect(tt.lines[2]?.text).toBe('Armor: Increase Maximum Life 5%')
expect(tt.lines[3]?.text).toBe('Helms: Increase Maximum Life 5%')
expect(tt.lines[4]?.text).toBe('Shields: +50 to Life')
})
})
describe('4. Helper functions parity', () => {
it('4.1 isGemCode identifies all gems and rejects non-gems', () => {
expect(isGemCode('skz')).toBe(true)
expect(isGemCode('gcv')).toBe(true)
expect(isGemCode('gpw')).toBe(true)
expect(isGemCode('r01')).toBe(false)
expect(isGemCode('hax')).toBe(false)
expect(isGemCode(undefined)).toBe(false)
})
it('4.2 isRuneCode identifies all runes and rejects non-runes', () => {
expect(isRuneCode('r01')).toBe(true)
expect(isRuneCode('r33')).toBe(true)
expect(isRuneCode('skz')).toBe(false)
expect(isRuneCode('r00')).toBe(false)
expect(isRuneCode('r34')).toBe(false)
expect(isRuneCode(undefined)).toBe(false)
})
it('4.3 getSocketableInfo returns valid info for both gems and runes', () => {
const skz = getSocketableInfo('skz')
expect(skz?.nameEn).toBe('Perfect Skull')
const r30 = getSocketableInfo('r30')
expect(r30?.nameEn).toBe('Ber Rune')
expect(getSocketableInfo('invalid')).toBeUndefined()
})
})
})