Merge branch 'issue-115'
This commit is contained in:
commit
afc653dbc8
|
|
@ -15,7 +15,12 @@ import { MountedArchives } from '../src/mpq/mount.ts'
|
|||
import { decodePl2 } from '../src/formats/pl2.ts'
|
||||
import { loadCharacterSheet } from '../src/game/character.ts'
|
||||
import { compositeMonsterAnimation } from '../src/game/monster-art.ts'
|
||||
import { ACT_MONSTER_SPECS } from '../src/game/monster-mapping.ts'
|
||||
import { ACT_MONSTER_SPECS, resolveMonsterArtSpec } from '../src/game/monster-mapping.ts'
|
||||
import {
|
||||
CANONICAL_SUPER_UNIQUE_LANDMARKS,
|
||||
CANONICAL_SUPERUNIQUES_TABLE,
|
||||
readSuperUniques,
|
||||
} from '../src/game/monsters.ts'
|
||||
import type { SpriteSheet } from '../src/formats/sprite.ts'
|
||||
import { encodeIndexedPng } from './png.ts'
|
||||
|
||||
|
|
@ -165,6 +170,34 @@ export async function bakeEntities(archiveDir = 'samples/d2', outDir = 'samples/
|
|||
for (let act = 1; act <= 5; act += 1) {
|
||||
const specs = ACT_MONSTER_SPECS[act] ?? []
|
||||
for (const spec of specs) {
|
||||
if (!spec.token || spec.token.toLowerCase() === 'xx') continue
|
||||
const key = `${spec.token.toLowerCase()}-${spec.weapon.toLowerCase()}`
|
||||
if (!uniqueSpecs.has(key)) {
|
||||
uniqueSpecs.set(key, spec)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Include SuperUnique landmark bosses and minions
|
||||
const superUniques = readSuperUniques(CANONICAL_SUPERUNIQUES_TABLE)
|
||||
const landmarkMinionIds = new Set<string>()
|
||||
for (const landmark of CANONICAL_SUPER_UNIQUE_LANDMARKS) {
|
||||
if (landmark.minionMonsterId) {
|
||||
landmarkMinionIds.add(landmark.minionMonsterId)
|
||||
}
|
||||
}
|
||||
|
||||
const allSuperUniqueMonsterIds = new Set<string>()
|
||||
for (const su of superUniques) {
|
||||
allSuperUniqueMonsterIds.add(su.monsterId)
|
||||
}
|
||||
for (const minionId of landmarkMinionIds) {
|
||||
allSuperUniqueMonsterIds.add(minionId)
|
||||
}
|
||||
|
||||
for (const monsterId of allSuperUniqueMonsterIds) {
|
||||
const spec = resolveMonsterArtSpec(monsterId)
|
||||
if (spec && spec.token && spec.token.toLowerCase() !== 'xx') {
|
||||
const key = `${spec.token.toLowerCase()}-${spec.weapon.toLowerCase()}`
|
||||
if (!uniqueSpecs.has(key)) {
|
||||
uniqueSpecs.set(key, spec)
|
||||
|
|
@ -193,6 +226,7 @@ export async function bakeEntities(archiveDir = 'samples/d2', outDir = 'samples/
|
|||
cursor += 1
|
||||
if (idx >= entries.length) return
|
||||
const [key, spec] = entries[idx]!
|
||||
if (!spec.token || spec.token.toLowerCase() === 'xx') continue
|
||||
try {
|
||||
const [walk, stand] = await Promise.all([
|
||||
compositeMonsterAnimation(archives, spec.token, 'wl', spec.weapon),
|
||||
|
|
|
|||
|
|
@ -1252,26 +1252,26 @@ export const CANONICAL_SUPER_UNIQUE_LANDMARKS: readonly SuperUniqueLandmarkMappi
|
|||
{ id: 'Blood Raven', levelIds: [17], landmark: 'Act 1 - Graveyard', minionMonsterId: 'zombie1' },
|
||||
// Act 2
|
||||
{ id: 'Creeping Feature', levelIds: [41, 55, 59], landmark: 'Act 2 - Desert Tomb 1', minionMonsterId: 'mummy1' },
|
||||
{ id: 'Beetleburst', levelIds: [43], landmark: 'Act 2 - Desert Oasis 1', minionMonsterId: 'beetle2' },
|
||||
{ id: 'Dark Elder', levelIds: [44], landmark: 'Act 2 - Desert Ruins Elder', minionMonsterId: 'plaguebearer1' },
|
||||
{ id: 'Bloodwitch the Wild', levelIds: [45, 60], landmark: 'Act 2 - Desert Tomb 2', minionMonsterId: 'huntress2' },
|
||||
{ id: 'Beetleburst', levelIds: [43], landmark: 'Act 2 - Desert Oasis 1', minionMonsterId: 'scarab2' },
|
||||
{ id: 'Dark Elder', levelIds: [44], landmark: 'Act 2 - Desert Ruins Elder', minionMonsterId: 'zombie5' },
|
||||
{ id: 'Bloodwitch the Wild', levelIds: [45, 60], landmark: 'Act 2 - Desert Tomb 2', minionMonsterId: 'pantherwoman1' },
|
||||
{ id: 'Fangskin', levelIds: [45, 61], landmark: 'Act 2 - Desert Valley Ruin 2', minionMonsterId: 'clawviper3' },
|
||||
// Act 3
|
||||
{ id: 'Sszark the Burning', levelIds: [76, 84], landmark: 'Act 3 - Spider Cavern', minionMonsterId: 'spider2' },
|
||||
{ id: 'Sszark the Burning', levelIds: [76, 84], landmark: 'Act 3 - Spider Cavern', minionMonsterId: 'arach4' },
|
||||
{ id: 'Stormtree', levelIds: [78], landmark: 'Act 3 - Flayer Jungle Entrance', minionMonsterId: 'thornhulk3' },
|
||||
{ id: 'Witch Doctor Endugu', levelIds: [78, 88, 91], landmark: 'Act 3 - Flayer Dungeon', minionMonsterId: 'flayer4' },
|
||||
{ id: 'Battlemaid Sarina', levelIds: [80, 94], landmark: 'Act 3 - Burbs Temple', minionMonsterId: 'corruptrogue8' },
|
||||
{ id: 'Witch Doctor Endugu', levelIds: [78, 88, 91], landmark: 'Act 3 - Flayer Dungeon', minionMonsterId: 'fetish4' },
|
||||
{ id: 'Battlemaid Sarina', levelIds: [80, 94], landmark: 'Act 3 - Burbs Temple', minionMonsterId: 'corruptrogue5' },
|
||||
// Act 4
|
||||
{ id: 'Izual', levelIds: [105], landmark: 'Act 4 - Plains of Despair', minionMonsterId: 'izual' },
|
||||
{ id: 'Hephasto the Armorer', levelIds: [106, 107], landmark: 'Act 4 - Hellforge', minionMonsterId: 'hephasto' },
|
||||
// Act 5
|
||||
{ id: 'Dac Farren', levelIds: [110], landmark: 'Act 5 - Barricade Prison 1', minionMonsterId: 'demonimp1' },
|
||||
{ id: 'Shenk the Overseer', levelIds: [110], landmark: 'Act 5 - Barricade Building', minionMonsterId: 'enslaved1' },
|
||||
{ id: 'Dac Farren', levelIds: [110], landmark: 'Act 5 - Barricade Prison 1', minionMonsterId: 'imp3' },
|
||||
{ id: 'Shenk the Overseer', levelIds: [110], landmark: 'Act 5 - Barricade Building', minionMonsterId: 'minion1' },
|
||||
{ id: 'Eyeback the Unleashed', levelIds: [111], landmark: 'Act 5 - Barricade Hell Portal N', minionMonsterId: 'bloodlord2' },
|
||||
{ id: 'Thresh Socket', levelIds: [112], landmark: 'Act 5 - Barricade Snow Unique', minionMonsterId: 'siegebeast1' },
|
||||
{ id: 'Frozenstein', levelIds: [114], landmark: 'Act 5 - Frozen River', minionMonsterId: 'yeti2' },
|
||||
{ id: 'Frozenstein', levelIds: [114], landmark: 'Act 5 - Frozen River', minionMonsterId: 'snowyeti4' },
|
||||
{ id: 'Bonesaw Breaker', levelIds: [115], landmark: 'Act 5 - Glacial Trail', minionMonsterId: 'reanimatedhorde2' },
|
||||
{ id: 'Nihlathak', levelIds: [124], landmark: 'Act 5 - Halls of Vaught', minionMonsterId: 'iceprowler2' },
|
||||
{ id: 'Nihlathak', levelIds: [124], landmark: 'Act 5 - Halls of Vaught', minionMonsterId: 'minion6' },
|
||||
]
|
||||
|
||||
/** Canonical SuperUniques TSV extracted from game data for landmark bosses. */
|
||||
|
|
@ -1282,21 +1282,21 @@ Treehead WoodFist\tTreehead WoodFist\tbrute1\t5\t6\t0\t3\t4\t0\t0\t0\t0\tAct 1 S
|
|||
The Countess\tThe Countess\tcorruptrogue5\t9\t0\t0\t4\t6\t0\t0\t0\t0\tCountess\tCountess (N)\tCountess (H)
|
||||
Blood Raven\tBlood Raven\tcorruptrogue1\t9\t6\t0\t6\t8\t0\t0\t0\t0\tBlood Raven\tBlood Raven (N)\tBlood Raven (H)
|
||||
Creeping Feature\tCreeping Feature\tmummy1\t5\t18\t0\t4\t6\t0\t0\t0\t0\tAct 2 Super A\tAct 2 (N) Super A\tAct 2 (H) Super A
|
||||
Beetleburst\tBeetleburst\tbeetle2\t8\t0\t0\t4\t6\t0\t0\t0\t0\tAct 2 Super A\tAct 2 (N) Super A\tAct 2 (H) Super A
|
||||
Dark Elder\tDark Elder\tplaguebearer1\t6\t8\t0\t4\t6\t0\t0\t0\t0\tAct 2 Super B\tAct 2 (N) Super B\tAct 2 (H) Super B
|
||||
Bloodwitch the Wild\tBloodwitch the Wild\thuntress2\t7\t5\t0\t4\t6\t0\t0\t0\t0\tAct 2 Super B\tAct 2 (N) Super B\tAct 2 (H) Super B
|
||||
Beetleburst\tBeetleburst\tscarab2\t8\t0\t0\t4\t6\t0\t0\t0\t0\tAct 2 Super A\tAct 2 (N) Super A\tAct 2 (H) Super A
|
||||
Dark Elder\tDark Elder\tzombie5\t6\t8\t0\t4\t6\t0\t0\t0\t0\tAct 2 Super B\tAct 2 (N) Super B\tAct 2 (H) Super B
|
||||
Bloodwitch the Wild\tBloodwitch the Wild\tpantherwoman1\t7\t5\t0\t4\t6\t0\t0\t0\t0\tAct 2 Super B\tAct 2 (N) Super B\tAct 2 (H) Super B
|
||||
Fangskin\tFangskin\tclawviper3\t17\t6\t0\t4\t6\t0\t0\t0\t0\tAct 2 Super C\tAct 2 (N) Super C\tAct 2 (H) Super C
|
||||
Sszark the Burning\tSszark the Burning\tspider2\t5\t7\t0\t4\t6\t0\t0\t0\t0\tAct 3 Super A\tAct 3 (N) Super A\tAct 3 (H) Super A
|
||||
Sszark the Burning\tSszark the Burning\tarach4\t5\t7\t0\t4\t6\t0\t0\t0\t0\tAct 3 Super A\tAct 3 (N) Super A\tAct 3 (H) Super A
|
||||
Stormtree\tStormtree\tthornhulk3\t17\t6\t0\t4\t6\t0\t0\t0\t0\tAct 3 Super B\tAct 3 (N) Super B\tAct 3 (H) Super B
|
||||
Witch Doctor Endugu\tWitch Doctor Endugu\tflayershaman4\t8\t9\t0\t4\t6\t0\t0\t0\t0\tAct 3 Super B\tAct 3 (N) Super B\tAct 3 (H) Super B
|
||||
Battlemaid Sarina\tBattlemaid Sarina\tcorruptrogue8\t6\t27\t0\t4\t6\t0\t0\t0\t0\tAct 3 Super C\tAct 3 (N) Super C\tAct 3 (H) Super C
|
||||
Witch Doctor Endugu\tWitch Doctor Endugu\tfetishshaman4\t8\t9\t0\t4\t6\t0\t0\t0\t0\tAct 3 Super B\tAct 3 (N) Super B\tAct 3 (H) Super B
|
||||
Battlemaid Sarina\tBattlemaid Sarina\tcorruptrogue5\t6\t27\t0\t4\t6\t0\t0\t0\t0\tAct 3 Super C\tAct 3 (N) Super C\tAct 3 (H) Super C
|
||||
Izual\tIzual\tizual\t18\t0\t0\t0\t0\t0\t0\t0\t0\tIzual\tIzual (N)\tIzual (H)
|
||||
Hephasto the Armorer\tHephasto the Armorer\thephasto\t27\t30\t0\t0\t0\t0\t0\t0\t0\tHephasto\tHephasto (N)\tHephasto (H)
|
||||
Dac Farren\tDac Farren\tdemonimp1\t18\t0\t0\t4\t6\t0\t0\t0\t0\tAct 5 Super A\tAct 5 (N) Super A\tAct 5 (H) Super A
|
||||
Dac Farren\tDac Farren\timp3\t18\t0\t0\t4\t6\t0\t0\t0\t0\tAct 5 Super A\tAct 5 (N) Super A\tAct 5 (H) Super A
|
||||
Shenk the Overseer\tShenk the Overseer\toverseer2\t5\t0\t0\t15\t20\t0\t0\t0\t0\tShenk\tShenk (N)\tShenk (H)
|
||||
Eyeback the Unleashed\tEyeback the Unleashed\tbloodlord2\t6\t5\t0\t3\t5\t0\t0\t0\t0\tAct 5 Super A\tAct 5 (N) Super A\tAct 5 (H) Super A
|
||||
Thresh Socket\tThresh Socket\tsiegebeast1\t7\t0\t0\t2\t4\t0\t0\t0\t0\tAct 5 Super B\tAct 5 (N) Super B\tAct 5 (H) Super B
|
||||
Frozenstein\tFrozenstein\tyeti2\t18\t25\t0\t4\t6\t0\t0\t0\t0\tAct 5 Super B\tAct 5 (N) Super B\tAct 5 (H) Super B
|
||||
Frozenstein\tFrozenstein\tsnowyeti4\t18\t25\t0\t4\t6\t0\t0\t0\t0\tAct 5 Super B\tAct 5 (N) Super B\tAct 5 (H) Super B
|
||||
Bonesaw Breaker\tBonesaw Breaker\treanimatedhorde2\t5\t8\t0\t4\t6\t0\t0\t0\t0\tAct 5 Super C\tAct 5 (N) Super C\tAct 5 (H) Super C
|
||||
Nihlathak\tNihlathak\tnihlathakboss\t18\t0\t0\t6\t8\t0\t0\t0\t0\tNihlathak\tNihlathak (N)\tNihlathak (H)`
|
||||
|
||||
|
|
|
|||
|
|
@ -6635,12 +6635,12 @@ export const CANONICAL_LANDMARK_ENTITIES: readonly LandmarkEntityDef[] = [
|
|||
role: 'superunique',
|
||||
act: 2,
|
||||
levelIds: [45, 58, 60],
|
||||
monsterId: 'huntress2',
|
||||
monsterId: 'pantherwoman1',
|
||||
presetMatch: /Desert Tomb 2|Claw Viper|Valley Ruin 1/i,
|
||||
defaultLandmark: 'Act 2 - Desert Tomb 2',
|
||||
modifiers: ['cursed', 'strong'],
|
||||
minionCount: [4, 6],
|
||||
minionMonsterId: 'huntress2',
|
||||
minionMonsterId: 'pantherwoman1',
|
||||
objectId: 76,
|
||||
defaultRelativePos: { x: 0.48, y: 0.52 },
|
||||
},
|
||||
|
|
@ -6650,12 +6650,12 @@ export const CANONICAL_LANDMARK_ENTITIES: readonly LandmarkEntityDef[] = [
|
|||
role: 'superunique',
|
||||
act: 2,
|
||||
levelIds: [43],
|
||||
monsterId: 'beetle2',
|
||||
monsterId: 'scarab2',
|
||||
presetMatch: /Desert Oasis|Desert Lair/i,
|
||||
defaultLandmark: 'Act 2 - Desert Oasis 1',
|
||||
modifiers: ['magicresistant'],
|
||||
minionCount: [4, 6],
|
||||
minionMonsterId: 'beetle2',
|
||||
minionMonsterId: 'scarab2',
|
||||
objectId: 77,
|
||||
defaultRelativePos: { x: 0.52, y: 0.48 },
|
||||
},
|
||||
|
|
@ -6665,12 +6665,12 @@ export const CANONICAL_LANDMARK_ENTITIES: readonly LandmarkEntityDef[] = [
|
|||
role: 'superunique',
|
||||
act: 2,
|
||||
levelIds: [44],
|
||||
monsterId: 'plaguebearer1',
|
||||
monsterId: 'zombie5',
|
||||
presetMatch: /Desert Ruins Elder/i,
|
||||
defaultLandmark: 'Act 2 - Desert Ruins Elder',
|
||||
modifiers: ['fast', 'magicresistant'],
|
||||
minionCount: [4, 6],
|
||||
minionMonsterId: 'plaguebearer1',
|
||||
minionMonsterId: 'zombie5',
|
||||
objectId: 78,
|
||||
defaultRelativePos: { x: 0.5, y: 0.5 },
|
||||
},
|
||||
|
|
@ -6711,12 +6711,12 @@ export const CANONICAL_LANDMARK_ENTITIES: readonly LandmarkEntityDef[] = [
|
|||
role: 'superunique',
|
||||
act: 3,
|
||||
levelIds: [76, 84],
|
||||
monsterId: 'spider2',
|
||||
monsterId: 'arach4',
|
||||
presetMatch: /Spider Cavern|Spider/i,
|
||||
defaultLandmark: 'Act 3 - Spider Cavern',
|
||||
modifiers: ['strong', 'cursed'],
|
||||
minionCount: [4, 6],
|
||||
minionMonsterId: 'spider2',
|
||||
minionMonsterId: 'arach4',
|
||||
objectId: 81,
|
||||
defaultRelativePos: { x: 0.48, y: 0.52 },
|
||||
},
|
||||
|
|
@ -6726,12 +6726,12 @@ export const CANONICAL_LANDMARK_ENTITIES: readonly LandmarkEntityDef[] = [
|
|||
role: 'superunique',
|
||||
act: 3,
|
||||
levelIds: [78, 88, 91],
|
||||
monsterId: 'flayershaman4',
|
||||
monsterId: 'fetishshaman4',
|
||||
presetMatch: /Flayer Dungeon/i,
|
||||
defaultLandmark: 'Act 3 - Flayer Dungeon',
|
||||
modifiers: ['magicresistant', 'fireenchant'],
|
||||
minionCount: [4, 6],
|
||||
minionMonsterId: 'flayer4',
|
||||
minionMonsterId: 'fetish4',
|
||||
objectId: 82,
|
||||
defaultRelativePos: { x: 0.52, y: 0.48 },
|
||||
},
|
||||
|
|
@ -6741,12 +6741,12 @@ export const CANONICAL_LANDMARK_ENTITIES: readonly LandmarkEntityDef[] = [
|
|||
role: 'superunique',
|
||||
act: 3,
|
||||
levelIds: [80, 94],
|
||||
monsterId: 'corruptrogue8',
|
||||
monsterId: 'corruptrogue5',
|
||||
presetMatch: /Burbs Temple|Ruined Temple|MetroTemple/i,
|
||||
defaultLandmark: 'Act 3 - Burbs Temple',
|
||||
modifiers: ['fast', 'spectralhit'],
|
||||
minionCount: [4, 6],
|
||||
minionMonsterId: 'corruptrogue8',
|
||||
minionMonsterId: 'corruptrogue5',
|
||||
objectId: 83,
|
||||
defaultRelativePos: { x: 0.5, y: 0.5 },
|
||||
},
|
||||
|
|
@ -6803,12 +6803,12 @@ export const CANONICAL_LANDMARK_ENTITIES: readonly LandmarkEntityDef[] = [
|
|||
role: 'superunique',
|
||||
act: 5,
|
||||
levelIds: [110],
|
||||
monsterId: 'demonimp1',
|
||||
monsterId: 'imp3',
|
||||
presetMatch: /Barricade Prison 1|Barricade Ruins/i,
|
||||
defaultLandmark: 'Act 5 - Barricade Prison 1',
|
||||
modifiers: ['coldenchant'],
|
||||
minionCount: [4, 6],
|
||||
minionMonsterId: 'demonimp1',
|
||||
minionMonsterId: 'imp3',
|
||||
objectId: 87,
|
||||
defaultRelativePos: { x: 0.35, y: 0.5 },
|
||||
},
|
||||
|
|
@ -6823,7 +6823,7 @@ export const CANONICAL_LANDMARK_ENTITIES: readonly LandmarkEntityDef[] = [
|
|||
defaultLandmark: 'Act 5 - Barricade Building',
|
||||
modifiers: ['strong'],
|
||||
minionCount: [15, 20],
|
||||
minionMonsterId: 'enslaved1',
|
||||
minionMonsterId: 'minion1',
|
||||
objectId: 88,
|
||||
defaultRelativePos: { x: 0.75, y: 0.5 },
|
||||
},
|
||||
|
|
@ -6878,12 +6878,12 @@ export const CANONICAL_LANDMARK_ENTITIES: readonly LandmarkEntityDef[] = [
|
|||
role: 'superunique',
|
||||
act: 5,
|
||||
levelIds: [114],
|
||||
monsterId: 'yeti2',
|
||||
monsterId: 'snowyeti4',
|
||||
presetMatch: /Frozen River|Anya/i,
|
||||
defaultLandmark: 'Act 5 - Frozen River',
|
||||
modifiers: ['coldenchant', 'manahit'],
|
||||
minionCount: [4, 6],
|
||||
minionMonsterId: 'yeti2',
|
||||
minionMonsterId: 'snowyeti4',
|
||||
objectId: 92,
|
||||
defaultRelativePos: { x: 0.5, y: 0.5 },
|
||||
},
|
||||
|
|
@ -6898,7 +6898,7 @@ export const CANONICAL_LANDMARK_ENTITIES: readonly LandmarkEntityDef[] = [
|
|||
defaultLandmark: 'Act 5 - Halls of Vaught',
|
||||
modifiers: ['coldenchant'],
|
||||
minionCount: [6, 8],
|
||||
minionMonsterId: 'iceprowler2',
|
||||
minionMonsterId: 'minion6',
|
||||
objectId: 93,
|
||||
defaultRelativePos: { x: 0.5, y: 0.5 },
|
||||
},
|
||||
|
|
|
|||
|
|
@ -70,10 +70,10 @@ describe('no synthetic monster fallback', () => {
|
|||
const tables = { levels: levelsTxt, monstats: monstatsTxt, monlvl: monlvlTxt }
|
||||
|
||||
const plan = planLevelMonsters(tables, 110, 240 * 48, 11042, 3.5, 'normal')
|
||||
// overseer2 is Shenk's boss id, and enslaved1 is his minion id, neither in m1..m3 (siegebeast1, demonimp1, enflayer1)
|
||||
// overseer2 is Shenk's boss id, and minion1 is his minion id, neither in m1..m3 (siegebeast1, demonimp1, enflayer1)
|
||||
expect(plan.types).toContain('overseer2')
|
||||
expect(plan.types).toContain('enslaved1')
|
||||
expect(plan.types).toContain('demonimp1')
|
||||
expect(plan.types).toContain('minion1')
|
||||
expect(plan.types).toContain('imp3')
|
||||
})
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,163 @@
|
|||
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()
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
@ -371,6 +371,7 @@ describe("Wilderness SuperUniques & Landmark Entities (Issue #57)", () => {
|
|||
].join("\n"))
|
||||
const monstatsTxt = mockTable([
|
||||
"Id\tBaseId\tNameStr\tLevel\tMinGrp\tMaxGrp\tVelocity\trun\tAC\tExp\tminHP\tmaxHP\tA1MinD\tA1MaxD\tA1TH\tenabled\tissend\tisSpawn\tisMelee",
|
||||
"imp1\timp1\tDemon Imp\t35\t3\t6\t8\t12\t5\t15\t8\t14\t1\t3\t20\t1\t1\t1\t1",
|
||||
"demonimp1\tdemonimp1\tDemon Imp\t35\t3\t6\t8\t12\t5\t15\t8\t14\t1\t3\t20\t1\t1\t1\t1",
|
||||
"overseer1\toverseer1\tOverseer\t35\t1\t2\t7\t10\t10\t35\t16\t26\t3\t6\t30\t1\t1\t1\t1",
|
||||
"siegebeast1\tsiegebeast1\tSiege Beast\t35\t1\t2\t6\t8\t15\t50\t20\t35\t4\t8\t30\t1\t1\t1\t1",
|
||||
|
|
|
|||
Loading…
Reference in New Issue