164 lines
7.1 KiB
TypeScript
164 lines
7.1 KiB
TypeScript
import { describe, expect, test } from 'vitest'
|
|
import {
|
|
readSuperUniques,
|
|
CANONICAL_SUPERUNIQUES_TABLE,
|
|
CANONICAL_SUPER_UNIQUE_LANDMARKS,
|
|
CANONICAL_SUPER_UNIQUES_BY_LEVEL,
|
|
} from '../src/game/monsters.ts'
|
|
import {
|
|
MONSTER_ART_MAP,
|
|
resolveMonsterArtSpec,
|
|
} from '../src/game/monster-mapping.ts'
|
|
import { CANONICAL_LANDMARK_ENTITIES } from '../src/game/wilderness.ts'
|
|
import { MpqArchive } from '../src/mpq/archive.ts'
|
|
import { fileSource } from '../src/mpq/file-source.ts'
|
|
import { MountedArchives } from '../src/mpq/mount.ts'
|
|
import { parseTable } from '../src/game/acts.ts'
|
|
|
|
describe('SuperUniques Fidelity & Art Packaging (Issue #115)', () => {
|
|
const d2DataDir = '/usr/local/google/home/taodao/d2-data'
|
|
|
|
test('CANONICAL_SUPER_UNIQUE_LANDMARKS contains valid and corrected minion IDs', () => {
|
|
// 10 previously mistyped minions must match correct IDs
|
|
const landmarkMap = new Map(CANONICAL_SUPER_UNIQUE_LANDMARKS.map(l => [l.id, l]))
|
|
|
|
expect(landmarkMap.get('Beetleburst')?.minionMonsterId).toBe('scarab2')
|
|
expect(landmarkMap.get('Dark Elder')?.minionMonsterId).toBe('zombie5')
|
|
expect(landmarkMap.get('Bloodwitch the Wild')?.minionMonsterId).toBe('pantherwoman1')
|
|
expect(landmarkMap.get('Sszark the Burning')?.minionMonsterId).toBe('arach4')
|
|
expect(landmarkMap.get('Witch Doctor Endugu')?.minionMonsterId).toBe('fetish4')
|
|
expect(landmarkMap.get('Battlemaid Sarina')?.minionMonsterId).toBe('corruptrogue5')
|
|
expect(landmarkMap.get('Dac Farren')?.minionMonsterId).toBe('imp3')
|
|
expect(landmarkMap.get('Shenk the Overseer')?.minionMonsterId).toBe('minion1')
|
|
expect(landmarkMap.get('Frozenstein')?.minionMonsterId).toBe('snowyeti4')
|
|
expect(landmarkMap.get('Nihlathak')?.minionMonsterId).toBe('minion6')
|
|
})
|
|
|
|
test('CANONICAL_LANDMARK_ENTITIES contains aligned boss and minion IDs', () => {
|
|
const entityMap = new Map(CANONICAL_LANDMARK_ENTITIES.map(e => [e.id, e]))
|
|
|
|
expect(entityMap.get('Beetleburst')?.monsterId).toBe('scarab2')
|
|
expect(entityMap.get('Beetleburst')?.minionMonsterId).toBe('scarab2')
|
|
|
|
expect(entityMap.get('Dark Elder')?.monsterId).toBe('zombie5')
|
|
expect(entityMap.get('Dark Elder')?.minionMonsterId).toBe('zombie5')
|
|
|
|
expect(entityMap.get('Bloodwitch the Wild')?.monsterId).toBe('pantherwoman1')
|
|
expect(entityMap.get('Bloodwitch the Wild')?.minionMonsterId).toBe('pantherwoman1')
|
|
|
|
expect(entityMap.get('Sszark the Burning')?.monsterId).toBe('arach4')
|
|
expect(entityMap.get('Sszark the Burning')?.minionMonsterId).toBe('arach4')
|
|
|
|
expect(entityMap.get('Witch Doctor Endugu')?.monsterId).toBe('fetishshaman4')
|
|
expect(entityMap.get('Witch Doctor Endugu')?.minionMonsterId).toBe('fetish4')
|
|
|
|
expect(entityMap.get('Battlemaid Sarina')?.monsterId).toBe('corruptrogue5')
|
|
expect(entityMap.get('Battlemaid Sarina')?.minionMonsterId).toBe('corruptrogue5')
|
|
|
|
expect(entityMap.get('Dac Farren')?.monsterId).toBe('imp3')
|
|
expect(entityMap.get('Dac Farren')?.minionMonsterId).toBe('imp3')
|
|
|
|
expect(entityMap.get('Shenk the Overseer')?.minionMonsterId).toBe('minion1')
|
|
|
|
expect(entityMap.get('Frozenstein')?.monsterId).toBe('snowyeti4')
|
|
expect(entityMap.get('Frozenstein')?.minionMonsterId).toBe('snowyeti4')
|
|
|
|
expect(entityMap.get('Nihlathak')?.minionMonsterId).toBe('minion6')
|
|
})
|
|
|
|
test('CANONICAL_SUPERUNIQUES_TABLE parses with corrected Class entries', () => {
|
|
const parsed = readSuperUniques(CANONICAL_SUPERUNIQUES_TABLE)
|
|
const map = new Map(parsed.map(p => [p.id, p]))
|
|
|
|
expect(map.get('Beetleburst')?.monsterId).toBe('scarab2')
|
|
expect(map.get('Dark Elder')?.monsterId).toBe('zombie5')
|
|
expect(map.get('Bloodwitch the Wild')?.monsterId).toBe('pantherwoman1')
|
|
expect(map.get('Sszark the Burning')?.monsterId).toBe('arach4')
|
|
expect(map.get('Witch Doctor Endugu')?.monsterId).toBe('fetishshaman4')
|
|
expect(map.get('Battlemaid Sarina')?.monsterId).toBe('corruptrogue5')
|
|
expect(map.get('Dac Farren')?.monsterId).toBe('imp3')
|
|
expect(map.get('Frozenstein')?.monsterId).toBe('snowyeti4')
|
|
})
|
|
|
|
test('MONSTER_ART_MAP correctly maps critical SuperUnique boss tokens for each Act', () => {
|
|
// Act 1: Griswold (GZ), The Smith (5P)
|
|
expect(MONSTER_ART_MAP.griswold?.token).toBe('GZ')
|
|
expect(MONSTER_ART_MAP.smith?.token).toBe('5P')
|
|
|
|
// Act 2: Radament (RD), Coldworm (MQ), The Summoner (SU)
|
|
expect(MONSTER_ART_MAP.radament?.token).toBe('RD')
|
|
expect(MONSTER_ART_MAP.maggotqueen1?.token).toBe('MQ')
|
|
expect(MONSTER_ART_MAP.summoner?.token).toBe('SU')
|
|
|
|
// Act 3: High Council members (HP)
|
|
expect(MONSTER_ART_MAP.councilmember1?.token).toBe('HP')
|
|
expect(MONSTER_ART_MAP.councilmember2?.token).toBe('HP')
|
|
expect(MONSTER_ART_MAP.councilmember3?.token).toBe('HP')
|
|
|
|
// Act 4: Hephasto the Armorer (5P)
|
|
expect(MONSTER_ART_MAP.hephasto?.token).toBe('5P')
|
|
|
|
// Act 5: Nihlathak (XU), Baal Subject 5 (43)
|
|
expect(MONSTER_ART_MAP.nihlathakboss?.token).toBe('XU')
|
|
expect(MONSTER_ART_MAP.baalminion1?.token).toBe('43')
|
|
})
|
|
|
|
test('All 66 SuperUniques in game data verify boss class and minion resolution', async () => {
|
|
const archives = new MountedArchives()
|
|
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
|
|
try {
|
|
const archive = await MpqArchive.open(await fileSource(`${d2DataDir}/${name}`))
|
|
archives.add(name, archive)
|
|
} catch {
|
|
// If data dir unavailable in test runner, skip archive loading portion
|
|
return
|
|
}
|
|
}
|
|
|
|
const suData = await archives.read('data\\global\\excel\\SuperUniques.txt')
|
|
const suTable = parseTable(suData)
|
|
const superUniques = readSuperUniques(suTable)
|
|
|
|
expect(superUniques.length).toBe(66)
|
|
|
|
for (const su of superUniques) {
|
|
expect(su.id).toBeTruthy()
|
|
expect(su.monsterId).toBeTruthy()
|
|
|
|
// Boss class must resolve to an art spec or return null for dummy 'xx' codes gracefully
|
|
const bossArt = resolveMonsterArtSpec(su.monsterId)
|
|
if (su.monsterId === 'minion1') {
|
|
// minion1 has Code="xx" in MonStats.txt and resolveMonsterArtSpec returns null
|
|
expect(bossArt).toBeNull()
|
|
} else {
|
|
expect(bossArt).not.toBeNull()
|
|
expect(bossArt?.token).toBeTruthy()
|
|
expect(bossArt?.token.toLowerCase()).not.toBe('xx')
|
|
}
|
|
}
|
|
})
|
|
|
|
test('Dummy code xx monsters resolve to null and do not throw in resolveMonsterArtSpec', () => {
|
|
// minion1..minion11 have Code="xx" in MonStats
|
|
for (let i = 1; i <= 11; i += 1) {
|
|
const art = resolveMonsterArtSpec(`minion${i}`)
|
|
expect(art).toBeNull()
|
|
}
|
|
// Unknown or dummy
|
|
expect(resolveMonsterArtSpec('xx')).toBeNull()
|
|
expect(resolveMonsterArtSpec('')).toBeNull()
|
|
})
|
|
|
|
test('CANONICAL_SUPER_UNIQUES_BY_LEVEL builds without undefined errors', () => {
|
|
expect(Object.keys(CANONICAL_SUPER_UNIQUES_BY_LEVEL).length).toBeGreaterThan(0)
|
|
for (const [lvl, specOrSpecs] of Object.entries(CANONICAL_SUPER_UNIQUES_BY_LEVEL)) {
|
|
const specs = Array.isArray(specOrSpecs) ? specOrSpecs : [specOrSpecs]
|
|
for (const s of specs) {
|
|
expect(s.id).toBeTruthy()
|
|
expect(s.monsterId).toBeTruthy()
|
|
expect(s.minionMonsterId).toBeTruthy()
|
|
}
|
|
}
|
|
})
|
|
})
|