diablo2-web/tests/common/hireling-table.test.ts

163 lines
6.6 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
HIRELING_RECORDS,
resolveHirelingAct,
resolveHirelingName,
resolveHirelingDesc,
resolveHirelingCandidate,
resolveHirelingCandidates,
formatHirelingCandidateLines,
} from '../../src/common/units/hireling-table.ts'
describe('hireling-table (authoritative Diablo II v1.13c Hireling.txt / D2Common parity)', () => {
it('loads all 120 compiled hireling records with valid attributes', () => {
expect(HIRELING_RECORDS).toHaveLength(120)
for (const r of HIRELING_RECORDS) {
expect([0, 100]).toContain(r.version)
expect([271, 338, 359, 561]).toContain(r.classId)
expect([1, 2, 3, 5]).toContain(r.act)
expect([1, 2, 3]).toContain(r.difficulty)
expect([150, 198, 252, 515]).toContain(r.seller)
expect(r.level).toBeGreaterThan(0)
expect(r.hireCost).toBeGreaterThan(0)
expect(r.hp).toBeGreaterThan(0)
expect(r.skills.length).toBeGreaterThan(0)
}
})
it('resolves act index from mercenary name ID matching D2Common_10988 (0x6fd7c9e0)', () => {
// Act 1 Rogue (merc01..merc34 / global 3411..3444) -> act 0
expect(resolveHirelingAct(3411, true)).toBe(0)
expect(resolveHirelingAct(3444, true)).toBe(0)
// Act 2 Desert Mercenary (merca201..merca221 / global 1019..1039) -> act 1
expect(resolveHirelingAct(1019, true)).toBe(1)
expect(resolveHirelingAct(1039, true)).toBe(1)
// Act 3 Iron Wolf (merca222..merca241 / global 1040..1059) -> act 2
expect(resolveHirelingAct(1040, true)).toBe(2)
expect(resolveHirelingAct(1059, true)).toBe(2)
expect(resolveHirelingAct(11021, true)).toBe(2) // Jarulf
expect(resolveHirelingAct(0x0421, true)).toBe(2) // 1057 Jarulf legacy
// Act 5 Barbarian (MercX101..MercX167 / global 10835..10901) -> act 4
expect(resolveHirelingAct(10835, true)).toBe(4)
expect(resolveHirelingAct(10901, true)).toBe(4)
})
it('resolves localized mercenary names in English and Chinese per D2Client.dll 0x6faf9783', () => {
// Act 1
expect(resolveHirelingName(3411, 'ENG')).toBe('Aliza')
expect(resolveHirelingName(3411, 'CHI')).toBe('阿里撒')
expect(resolveHirelingName(3444, 'ENG')).toBe('Ryann')
expect(resolveHirelingName(3444, 'CHI')).toBe('萊恩')
// Act 2
expect(resolveHirelingName(1019, 'ENG')).toBe('Hazade')
expect(resolveHirelingName(1019, 'CHI')).toBe('哈撒迪')
expect(resolveHirelingName(1039, 'ENG')).toBe('Vikhyat')
expect(resolveHirelingName(1039, 'CHI')).toBe('維克懷特')
// Act 3
expect(resolveHirelingName(1040, 'ENG')).toBe('Jelani')
expect(resolveHirelingName(1040, 'CHI')).toBe('喬安妮')
expect(resolveHirelingName(11021, 'ENG')).toBe('Jarulf')
expect(resolveHirelingName(0x0421, 'ENG')).toBe('Jarulf')
// Act 5
expect(resolveHirelingName(10835, 'ENG')).toBe('Varaya')
expect(resolveHirelingName(10835, 'CHI')).toBe('伐德哈卡')
expect(resolveHirelingName(10901, 'ENG')).toBe('Theodoric')
expect(resolveHirelingName(10901, 'CHI')).toBe('狄奧多理')
})
it('resolves ability descriptions matching D2Common_10091 (0x6fd7c9c0) in ENG and CHI', () => {
expect(resolveHirelingDesc(1, 'ENG')).toBe('Fire Arrow')
expect(resolveHirelingDesc(1, 'CHI')).toBe('火箭')
expect(resolveHirelingDesc(2, 'ENG')).toBe('Cold Arrow')
expect(resolveHirelingDesc(2, 'CHI')).toBe('冰箭')
expect(resolveHirelingDesc(3, 'ENG')).toBe('Combat')
expect(resolveHirelingDesc(3, 'CHI')).toBe('戰鬥')
expect(resolveHirelingDesc(4, 'ENG')).toBe('Defensive')
expect(resolveHirelingDesc(4, 'CHI')).toBe('防禦')
expect(resolveHirelingDesc(5, 'ENG')).toBe('Offensive')
expect(resolveHirelingDesc(5, 'CHI')).toBe('進攻')
expect(resolveHirelingDesc(6, 'ENG')).toBe('Fire')
expect(resolveHirelingDesc(6, 'CHI')).toBe('火')
expect(resolveHirelingDesc(7, 'ENG')).toBe('Cold')
expect(resolveHirelingDesc(7, 'CHI')).toBe('冷')
expect(resolveHirelingDesc(8, 'ENG')).toBe('Lightning')
expect(resolveHirelingDesc(8, 'CHI')).toBe('閃電')
expect(resolveHirelingDesc(0, 'ENG')).toBe('')
expect(resolveHirelingDesc(0, 'CHI')).toBe('')
})
it('formats candidate lines per D2Client.dll 0x6faf9440', () => {
const eng = formatHirelingCandidateLines('Aliza', 8, 90, 55, 175, 'Fire Arrow', 'ENG')
expect(eng.line1).toBe('Aliza - Lvl: 8 Life: 90 Def: 55 Cost: 175')
expect(eng.line2).toBe(' Fire Arrow')
const chi = formatHirelingCandidateLines('阿里撒', 8, 90, 55, 175, '火箭', 'CHI')
expect(chi.line1).toBe('阿里撒 - 等級: 8 生命: 90 防禦: 55 花費: 175')
expect(chi.line2).toBe(' 火箭')
const blankSpecial = formatHirelingCandidateLines('Varaya', 30, 320, 200, 9200, '', 'ENG')
expect(blankSpecial.line2).toBe(' ')
})
it('computes exact 64-bit LCG stats matching D2Common_10458 across difficulties', () => {
// Act 1 Rogue (Normal, seed 0x1234, playerLevel 10)
const c1 = resolveHirelingCandidate({ nameId: 3411, seed: 0x1234 }, 10, 0, 0, true, 'ENG')
expect(c1.name).toBe('Aliza')
expect(c1.actIndex).toBe(0)
expect(c1.classId).toBe(271)
expect(c1.level).toBe(8)
expect(c1.hp).toBe(90)
expect(c1.defense).toBe(55)
expect(c1.strength).toBe(41)
expect(c1.dexterity).toBe(55)
expect(c1.cost).toBe(175)
expect(c1.hireDesc).toBe('Fire Arrow')
// Act 2 Desert Mercenary (Nightmare, seed 0x5678, playerLevel 50)
const c2 = resolveHirelingCandidate({ nameId: 1019, seed: 0x5678 }, 50, 1, 1, true, 'ENG')
expect(c2.name).toBe('Hazade')
expect(c2.actIndex).toBe(1)
expect(c2.classId).toBe(338)
expect(c2.level).toBeGreaterThanOrEqual(45)
expect(c2.cost).toBeGreaterThan(5000)
expect(['Combat', 'Defensive', 'Offensive']).toContain(c2.hireDesc)
// Act 5 Barbarian (Normal, seed 0x9abc, playerLevel 35)
const c5 = resolveHirelingCandidate({ nameId: 10835, seed: 0x9abc }, 35, 0, 4, true, 'ENG')
expect(c5.name).toBe('Varaya')
expect(c5.actIndex).toBe(4)
expect(c5.classId).toBe(561)
expect(c5.level).toBeGreaterThanOrEqual(30)
expect(c5.cost).toBeGreaterThanOrEqual(9000)
expect(c5.hireDesc).toBe('')
})
it('resolveHirelingCandidates maps candidate list cleanly', () => {
const raw = [
{ nameId: 3411, seed: 0x1111 },
{ nameId: 3412, seed: 0x2222 },
{ nameId: 3413, seed: 0x3333 },
]
const res = resolveHirelingCandidates(raw, 12, 0, 0, true, 'CHI')
expect(res).toHaveLength(3)
expect(res[0]!.nameId).toBe(3411)
expect(res[1]!.nameId).toBe(3412)
expect(res[2]!.nameId).toBe(3413)
expect(res[0]!.line1).toContain('等級:')
})
})