fix(units): align getMonsterKindByHcIdx with MONSTER_HCIDX_TO_ID for hcIdx 0..733 (Closes #713)
This commit is contained in:
parent
948a586432
commit
25fdf81aa2
|
|
@ -2069,6 +2069,7 @@ function getCanonicalMonsterKinds(): Map<string, MonsterKind> {
|
|||
const rawKinds = readMonsterKinds(parseTable(new TextEncoder().encode(RAW_MONSTATS)))
|
||||
const enriched = new Map<string, MonsterKind>()
|
||||
for (const [id, kind] of rawKinds) {
|
||||
if (id.toLowerCase() === 'expansion') continue
|
||||
const ov = CANONICAL_113C_MONSTER_OVERRIDES[id]
|
||||
const canonicalSpeed = CANONICAL_113C_VELOCITIES_BY_ID[id.toLowerCase()]
|
||||
const canonicalVel = canonicalSpeed && canonicalSpeed[0] > 0 ? canonicalSpeed[0] : undefined
|
||||
|
|
@ -2135,7 +2136,7 @@ export function getMonsterKindByClassId(classId: number): MonsterKind | undefine
|
|||
return getCanonicalMonsterKinds().get(id)
|
||||
}
|
||||
|
||||
const cachedMonsterKindsByHcIdx = new Map<Difficulty, readonly MonsterKind[]>()
|
||||
const cachedMonsterKindsByHcIdx = new Map<Difficulty, readonly (MonsterKind | undefined)[]>()
|
||||
let cachedSuperUniquesByIndex: readonly SuperUnique[] | null = null
|
||||
|
||||
function normalizeDifficulty(difficulty: Difficulty | number = 'normal'): Difficulty {
|
||||
|
|
@ -2154,18 +2155,24 @@ export function getMonsterKindByHcIdx(
|
|||
difficulty: Difficulty | number = 'normal',
|
||||
): MonsterKind | undefined {
|
||||
if (!Number.isInteger(hcIdx) || hcIdx < 0) return undefined
|
||||
const targetId = MONSTER_HCIDX_TO_ID[hcIdx]
|
||||
if (!targetId) return undefined
|
||||
const diff = normalizeDifficulty(difficulty)
|
||||
let list = cachedMonsterKindsByHcIdx.get(diff)
|
||||
if (!list) {
|
||||
const rawKinds = readMonsterKinds(parseTable(new TextEncoder().encode(RAW_MONSTATS)), diff)
|
||||
const arr: MonsterKind[] = []
|
||||
const byId = new Map<string, MonsterKind>()
|
||||
for (const [id, kind] of rawKinds) {
|
||||
if (id.toLowerCase() === 'expansion') continue
|
||||
const ov = diff === 'normal' ? CANONICAL_113C_MONSTER_OVERRIDES[id] : undefined
|
||||
const canonicalSpeed = CANONICAL_113C_VELOCITIES_BY_ID[id.toLowerCase()]
|
||||
const canonicalVel = canonicalSpeed && canonicalSpeed[0] > 0 ? canonicalSpeed[0] : undefined
|
||||
const canonicalRun = canonicalSpeed && canonicalSpeed[1] > 0 ? canonicalSpeed[1] : undefined
|
||||
const enrichedKind: MonsterKind = {
|
||||
...kind,
|
||||
enabled: kind.killable || !kind.inTown,
|
||||
velocity: ov?.velocity ?? (kind.velocity > 0 ? kind.velocity : 5),
|
||||
runVelocity: ov?.runVelocity ?? (kind.runVelocity > 0 ? kind.runVelocity : 5),
|
||||
velocity: ov?.velocity ?? canonicalVel ?? (kind.velocity > 0 ? kind.velocity : 5),
|
||||
runVelocity: ov?.runVelocity ?? canonicalRun ?? (kind.runVelocity > 0 ? kind.runVelocity : 5),
|
||||
minHp: ov?.minHp ?? (kind.minHp > 0 ? kind.minHp : 21),
|
||||
maxHp: ov?.maxHp ?? (kind.maxHp > 0 ? kind.maxHp : 61),
|
||||
armour: ov?.armour ?? (kind.armour > 0 ? kind.armour : 84),
|
||||
|
|
@ -2181,7 +2188,12 @@ export function getMonsterKindByHcIdx(
|
|||
writable: true,
|
||||
})
|
||||
}
|
||||
arr.push(enrichedKind)
|
||||
byId.set(id, enrichedKind)
|
||||
}
|
||||
const arr: (MonsterKind | undefined)[] = []
|
||||
for (const [idxStr, mappedId] of Object.entries(MONSTER_HCIDX_TO_ID)) {
|
||||
const idx = Number(idxStr)
|
||||
arr[idx] = byId.get(mappedId) ?? byId.get(mappedId.toLowerCase())
|
||||
}
|
||||
list = arr
|
||||
cachedMonsterKindsByHcIdx.set(diff, list)
|
||||
|
|
|
|||
|
|
@ -916,7 +916,7 @@ describe('Diablo II 1.13c Monster Lifebar & 2-Line Subtext Tooltip Parity', () =
|
|||
expect(andarielInfo.isDemon).toBe(true)
|
||||
expect(resolveMonsterTitleAndColor(andarielInfo, 'en').titleColor).toBe('gold')
|
||||
|
||||
// 6. Uber Diablo (hcIdx = 705 uberdiablo) suppresses modifiers and immunities (0x6facfedb & 0x6fb6fca0)
|
||||
// 6. Uber Diablo (hcIdx = 705 uberdiablo) and Uber Mephisto (hcIdx = 704 ubermephisto) suppress modifiers and immunities (0x6facfedb & 0x6fb6fca0)
|
||||
const uberUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
id: 106,
|
||||
|
|
@ -927,9 +927,45 @@ describe('Diablo II 1.13c Monster Lifebar & 2-Line Subtext Tooltip Parity', () =
|
|||
bossMods: [8, 9, 17, 18],
|
||||
})
|
||||
const uberInfo = clientUnitToMonsterTargetInfo(uberUnit, 2)
|
||||
expect(uberInfo.name).toBe('Diablo')
|
||||
expect(uberInfo.nameKey).toBe('Diablo')
|
||||
expect(uberInfo.isBoss).toBe(true)
|
||||
expect(uberInfo.isDemon).toBe(true)
|
||||
expect(uberInfo.modifiers).toEqual([])
|
||||
expect(uberInfo.resistances).toBeUndefined()
|
||||
|
||||
const uberMephUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
id: 108,
|
||||
classId: 704,
|
||||
x: 5055,
|
||||
y: 5055,
|
||||
lifePct: 128,
|
||||
bossMods: [8, 9],
|
||||
})
|
||||
const uberMephInfo = clientUnitToMonsterTargetInfo(uberMephUnit, 2)
|
||||
expect(uberMephInfo.name).toBe('Mephisto')
|
||||
expect(uberMephInfo.nameKey).toBe('Mephisto')
|
||||
expect(uberMephInfo.isBoss).toBe(true)
|
||||
expect(uberMephInfo.isDemon).toBe(true)
|
||||
expect(uberMephInfo.modifiers).toEqual([])
|
||||
expect(uberMephInfo.resistances).toBeUndefined()
|
||||
|
||||
// Baal (hcIdx = 544 baalcrab) resolves NameStr 'Baal Crab' -> "Baal", not baalthrone
|
||||
const baalUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
id: 109,
|
||||
classId: 544,
|
||||
x: 5058,
|
||||
y: 5058,
|
||||
lifePct: 128,
|
||||
})
|
||||
const baalInfo = clientUnitToMonsterTargetInfo(baalUnit, 2)
|
||||
expect(baalInfo.nameKey).toBe('Baal Crab')
|
||||
expect(baalInfo.name).toBe('Baal')
|
||||
expect(baalInfo.isBoss).toBe(true)
|
||||
expect(baalInfo.isDemon).toBe(true)
|
||||
|
||||
// 7. Random Unique Fallen (hcIdx = 19, flags = 0x08, nameSeed = 0x1234 -> wNameSeed = 0x34)
|
||||
const randUniqueUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
|
|
@ -954,6 +990,92 @@ describe('Diablo II 1.13c Monster Lifebar & 2-Line Subtext Tooltip Parity', () =
|
|||
expect(zhTitle.titleColor).toBe('gold')
|
||||
expect(zhTitle.displayName).not.toBe('沉淪魔')
|
||||
})
|
||||
|
||||
it('Issue #713: getMonsterKindByHcIdx aligns 0..733 with MONSTER_HCIDX_TO_ID and 1.13c MonStats.txt with 0 discrepancies across all difficulties', async () => {
|
||||
const fs = await import('node:fs')
|
||||
const { cell } = await import('../src/common/world/acts.ts')
|
||||
const { MONSTER_HCIDX_TO_ID } = await import('../src/common/units/monster-mapping.ts')
|
||||
const { getMonsterKindByHcIdx, getMonsterKindByClassId, MONSTER_TABLE } = await import(
|
||||
'../src/common/units/monsters-data.ts'
|
||||
)
|
||||
|
||||
// 1. Canonical boundary checks for Expansion shift & duplicate cr_lancer8
|
||||
expect(getMonsterKindByHcIdx(409, 'normal')?.id).toBe('hephasto')
|
||||
expect(getMonsterKindByHcIdx(410, 'normal')?.id).toBe('wakeofdestruction')
|
||||
expect(getMonsterKindByHcIdx(543, 'normal')?.id).toBe('baalthrone')
|
||||
expect(getMonsterKindByHcIdx(544, 'normal')?.id).toBe('baalcrab')
|
||||
expect(getMonsterKindByHcIdx(617, 'normal')?.id).toBe('cr_lancer8')
|
||||
expect(getMonsterKindByHcIdx(703, 'normal')?.id).toBe('mummy6')
|
||||
expect(getMonsterKindByHcIdx(704, 'normal')?.id).toBe('ubermephisto')
|
||||
expect(getMonsterKindByHcIdx(705, 'normal')?.id).toBe('uberdiablo')
|
||||
expect(getMonsterKindByHcIdx(723, 'normal')?.id).toBe('cr_lancer8')
|
||||
expect(getMonsterKindByHcIdx(733, 'normal')?.id).toBe('willowisp8')
|
||||
|
||||
// Out-of-range and non-integer inputs return undefined; Expansion separator never leaks
|
||||
expect(getMonsterKindByHcIdx(-1, 'normal')).toBeUndefined()
|
||||
expect(getMonsterKindByHcIdx(734, 'normal')).toBeUndefined()
|
||||
expect(getMonsterKindByHcIdx(1.5, 'normal')).toBeUndefined()
|
||||
expect(getMonsterKindByHcIdx(Number.NaN, 'normal')).toBeUndefined()
|
||||
expect(MONSTER_TABLE.get('Expansion')).toBeUndefined()
|
||||
|
||||
// 2. Full 0..733 spectrum parity across all difficulties ('normal'|'nightmare'|'hell' and 0|1|2)
|
||||
const rawTable = parseTable(new TextEncoder().encode(RAW_MONSTATS))
|
||||
const nonExpansionRows = rawTable.rows.filter(r => {
|
||||
const id = cell(rawTable, r, 'Id').trim()
|
||||
return id !== '' && id.toLowerCase() !== 'expansion'
|
||||
})
|
||||
expect(nonExpansionRows).toHaveLength(734)
|
||||
|
||||
for (let hcIdx = 0; hcIdx <= 733; hcIdx += 1) {
|
||||
const expectedId = MONSTER_HCIDX_TO_ID[hcIdx]
|
||||
expect(expectedId, `MONSTER_HCIDX_TO_ID[${hcIdx}] must be defined`).toBeDefined()
|
||||
|
||||
const row = nonExpansionRows[hcIdx]!
|
||||
expect(cell(rawTable, row, 'Id').trim()).toBe(expectedId)
|
||||
|
||||
const byClassId = getMonsterKindByClassId(hcIdx)
|
||||
expect(byClassId?.id).toBe(expectedId)
|
||||
|
||||
for (const [diffStr, diffNum] of [
|
||||
['normal', 0],
|
||||
['nightmare', 1],
|
||||
['hell', 2],
|
||||
] as const) {
|
||||
const kindStr = getMonsterKindByHcIdx(hcIdx, diffStr)
|
||||
const kindNum = getMonsterKindByHcIdx(hcIdx, diffNum)
|
||||
expect(kindStr?.id, `hcIdx=${hcIdx} diff=${diffStr}`).toBe(expectedId)
|
||||
expect(kindNum?.id, `hcIdx=${hcIdx} diff=${diffNum}`).toBe(expectedId)
|
||||
expect(kindStr?.nameKey).toBe(cell(rawTable, row, 'NameStr').trim())
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Direct row-by-row comparison against Patch_D2.mpq MonStats.txt when local MPQ is present
|
||||
const mpqPath = fs.existsSync('samples/d2/Patch_D2.mpq')
|
||||
? 'samples/d2/Patch_D2.mpq'
|
||||
: fs.existsSync('/usr/local/google/home/taodao/d2-data/Patch_D2.mpq')
|
||||
? '/usr/local/google/home/taodao/d2-data/Patch_D2.mpq'
|
||||
: null
|
||||
if (mpqPath) {
|
||||
const { MpqArchive } = await import('../src/common/mpq/archive.ts')
|
||||
const { fileSource } = await import('../src/baker/io/file-source.ts')
|
||||
const archive = await MpqArchive.open(await fileSource(mpqPath))
|
||||
const monstatsBytes = await archive.read(archive.find('data\\global\\excel\\MonStats.txt')!)
|
||||
const mpqTable = parseTable(monstatsBytes)
|
||||
const mpqRows = mpqTable.rows.filter(r => {
|
||||
const id = cell(mpqTable, r, 'Id').trim()
|
||||
return id !== '' && id.toLowerCase() !== 'expansion'
|
||||
})
|
||||
expect(mpqRows).toHaveLength(734)
|
||||
for (let hcIdx = 0; hcIdx <= 733; hcIdx += 1) {
|
||||
const mpqRow = mpqRows[hcIdx]!
|
||||
expect(Number(cell(mpqTable, mpqRow, 'hcIdx').trim())).toBe(hcIdx)
|
||||
const mpqId = cell(mpqTable, mpqRow, 'Id').trim()
|
||||
const kind = getMonsterKindByHcIdx(hcIdx, 'hell')
|
||||
expect(kind?.id).toBe(mpqId)
|
||||
expect(kind?.nameKey).toBe(cell(mpqTable, mpqRow, 'NameStr').trim())
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue