fix(hud): restore 1.13c monster nameplate full names, race/desc subtext, elite/boss ranks & immunities (Closes #559)
This commit is contained in:
parent
7bd10903f6
commit
273aa6ffb8
|
|
@ -349,6 +349,10 @@ async function main() {
|
|||
for (const r of (await readTxt('SuperUniques.txt')).rows) {
|
||||
addKey(r['Name'])
|
||||
}
|
||||
for (const fn of ['UniquePrefix.txt', 'UniqueSuffix.txt', 'UniqueAppellation.txt']) {
|
||||
const t = await readTxt(fn)
|
||||
for (const r of t.rows) addKey(r['Name'])
|
||||
}
|
||||
for (const r of (await readTxt('Levels.txt')).rows) {
|
||||
addKey(r['LevelName'])
|
||||
addKey(r['LevelWarp'])
|
||||
|
|
|
|||
|
|
@ -11,7 +11,16 @@
|
|||
|
||||
import { SKILLS_BY_ID } from '../../common/data/skills-catalog.ts'
|
||||
import type { CharacterClassCode } from '../../common/units/classes.ts'
|
||||
import { monsterDisplayName } from '../../common/units/monsters-data.ts'
|
||||
import {
|
||||
CANONICAL_ELITE_MODIFIERS,
|
||||
FIXED_UNIQUE_BOSS_HCIDX_SET,
|
||||
applyMonsterUModResistances,
|
||||
getMonsterKindByHcIdx,
|
||||
getSuperUniqueByIndex,
|
||||
monsterDisplayName,
|
||||
rollUniqueMonsterNameKeys,
|
||||
type Difficulty,
|
||||
} from '../../common/units/monsters-data.ts'
|
||||
import { calculateManaCost } from '../../common/skills/skill-calc-engine.ts'
|
||||
import { onlineItemDataToUiInventoryItem } from '../../common/items/item-bridge.ts'
|
||||
import { ClickButtonId } from '../../netproto/index.ts'
|
||||
|
|
@ -30,7 +39,12 @@ import {
|
|||
type GridPlacement,
|
||||
type UiInventoryItem,
|
||||
} from '../ui/inventory.ts'
|
||||
import { GAMBLE_NPC_CLASS_IDS, type MonsterTargetHudInfo } from '../ui/world-panels.ts'
|
||||
import {
|
||||
GAMBLE_NPC_CLASS_IDS,
|
||||
MONSTER_UMOD_TBL_KEYS,
|
||||
lookupMonsterTblString,
|
||||
type MonsterTargetHudInfo,
|
||||
} from '../ui/world-panels.ts'
|
||||
import { requireTbl } from '../i18n/lang.ts'
|
||||
import { getTownNpcDescriptorByHcIdx, type TownNpcServiceDescriptor } from '../../common/units/npc-table.ts'
|
||||
import type { VendorPriceContext, VendorPriceFn, VendorRepairAllFn } from './vendor-price.ts'
|
||||
|
|
@ -1164,11 +1178,17 @@ export class HudModel implements HudCommandSink {
|
|||
return
|
||||
}
|
||||
const unit = this.world.getUnit('monster', this.hoveredMonsterId)
|
||||
if (!unit || unit.lifePct <= 0) {
|
||||
if (!unit || unit.lifePct <= 0 || unit.mode === 0 || unit.mode === 12) {
|
||||
hud.targetMonster = null
|
||||
return
|
||||
}
|
||||
hud.targetMonster = clientUnitToMonsterTargetInfo(unit)
|
||||
// D2Client.dll 0x6fb708a3..0x6fb708c4: skip town NPCs, non-killable units, and empty NameStr
|
||||
const kind = getMonsterKindByHcIdx(unit.classId, this.world.difficulty)
|
||||
if (kind && (kind.inTown || !kind.killable || !kind.nameKey)) {
|
||||
hud.targetMonster = null
|
||||
return
|
||||
}
|
||||
hud.targetMonster = clientUnitToMonsterTargetInfo(unit, this.world.difficulty)
|
||||
}
|
||||
|
||||
private emitCommand(cmd: ClientCommand): void {
|
||||
|
|
@ -1874,25 +1894,118 @@ export class HudModel implements HudCommandSink {
|
|||
}
|
||||
}
|
||||
|
||||
export function clientUnitToMonsterTargetInfo(unit: ClientUnit): MonsterTargetHudInfo {
|
||||
export function clientUnitToMonsterTargetInfo(
|
||||
unit: ClientUnit,
|
||||
difficulty: 0 | 1 | 2 | Difficulty = 0,
|
||||
): MonsterTargetHudInfo {
|
||||
const hpPct = Math.max(0, Math.min(128, unit.lifePct))
|
||||
const rank: MonsterTargetHudInfo['rank'] = unit.isSuperUnique
|
||||
const kind = getMonsterKindByHcIdx(unit.classId, difficulty)
|
||||
|
||||
const isSuperUnique = Boolean(unit.isSuperUnique || (unit.flags & 0x02) !== 0)
|
||||
const isUnique = Boolean(!isSuperUnique && (unit.isUnique || (unit.flags & 0x08) !== 0))
|
||||
const isChampion = Boolean(
|
||||
!isSuperUnique && !isUnique && (unit.isChampion || (unit.flags & 0x04) !== 0),
|
||||
)
|
||||
const isMinion = Boolean(
|
||||
!isSuperUnique && !isUnique && !isChampion && (unit.flags & 0x10) !== 0,
|
||||
)
|
||||
const isBoss = Boolean(kind?.boss)
|
||||
|
||||
const rank: MonsterTargetHudInfo['rank'] = isSuperUnique
|
||||
? 'superunique'
|
||||
: unit.isUnique
|
||||
: isUnique
|
||||
? 'unique'
|
||||
: unit.isChampion
|
||||
: isChampion
|
||||
? 'champion'
|
||||
: 'normal'
|
||||
const resolvedName =
|
||||
unit.name && unit.name.trim().length > 0
|
||||
? unit.name
|
||||
: monsterDisplayName(`mon_${unit.classId}`, 'ENG') || `Monster #${unit.classId}`
|
||||
: isMinion
|
||||
? 'minion'
|
||||
: isBoss
|
||||
? 'boss'
|
||||
: 'normal'
|
||||
|
||||
const su =
|
||||
isSuperUnique && unit.superUniqueId !== undefined
|
||||
? getSuperUniqueByIndex(unit.superUniqueId)
|
||||
: undefined
|
||||
|
||||
const explicitName = unit.name && unit.name.trim().length > 0 ? unit.name.trim() : undefined
|
||||
const nameKey = su?.nameKey ?? explicitName ?? kind?.nameKey
|
||||
|
||||
let resolvedName =
|
||||
explicitName ??
|
||||
(nameKey
|
||||
? (lookupMonsterTblString(nameKey, 'en') ??
|
||||
monsterDisplayName(nameKey, 'ENG') ??
|
||||
nameKey)
|
||||
: `Monster #${unit.classId}`)
|
||||
|
||||
if (
|
||||
!explicitName &&
|
||||
isUnique &&
|
||||
unit.nameSeed !== undefined &&
|
||||
!FIXED_UNIQUE_BOSS_HCIDX_SET.has(unit.classId)
|
||||
) {
|
||||
if (unit.bossMods?.includes(12)) {
|
||||
resolvedName = lookupMonsterTblString('Blood Raven', 'en') ?? 'Blood Raven'
|
||||
} else {
|
||||
const partKeys = rollUniqueMonsterNameKeys(unit.nameSeed)
|
||||
resolvedName = partKeys.map(k => lookupMonsterTblString(k, 'en') ?? k).join(' ')
|
||||
}
|
||||
}
|
||||
|
||||
// D2Client.dll 0x6facfedb & 0x6fb6fca0: Uber bosses (hcIdx 704..709) suppress MonUMod labels and immunities
|
||||
const isUberBoss = unit.classId >= 704 && unit.classId <= 709
|
||||
|
||||
const rawModIds: number[] = isUberBoss
|
||||
? []
|
||||
: unit.bossMods && unit.bossMods.length > 0
|
||||
? [...unit.bossMods]
|
||||
: su?.modifiers && su.modifiers.length > 0
|
||||
? [...su.modifiers]
|
||||
: []
|
||||
if (!isUberBoss && isChampion && (unit.flags & 0x40) !== 0 && !rawModIds.includes(36)) {
|
||||
rawModIds.unshift(36)
|
||||
}
|
||||
|
||||
const modifiers = rawModIds.map(
|
||||
(m) => CANONICAL_ELITE_MODIFIERS.find((e) => e.id === m)?.name ?? String(m),
|
||||
)
|
||||
const affixes = rawModIds.map((m) => {
|
||||
const key = String(m)
|
||||
const entry =
|
||||
MONSTER_UMOD_TBL_KEYS[key] ??
|
||||
CANONICAL_ELITE_MODIFIERS.find((e) => e.id === m)
|
||||
if (entry) {
|
||||
return lookupMonsterTblString(entry.tblKey, 'en') ?? entry.label
|
||||
}
|
||||
return `Mod #${m}`
|
||||
})
|
||||
|
||||
const descKey =
|
||||
!isUberBoss && kind?.descKey && kind.descKey.length > 0
|
||||
? kind.descKey
|
||||
: undefined
|
||||
|
||||
const resistances =
|
||||
!isUberBoss && kind?.resistances
|
||||
? applyMonsterUModResistances(kind.resistances, rawModIds)
|
||||
: undefined
|
||||
|
||||
return {
|
||||
name: resolvedName,
|
||||
hcIdx: unit.classId,
|
||||
...(unit.nameSeed !== undefined ? { nameSeed: unit.nameSeed } : {}),
|
||||
...(nameKey !== undefined ? { nameKey } : {}),
|
||||
...(descKey !== undefined ? { descKey } : {}),
|
||||
hp: hpPct,
|
||||
maxHp: 128,
|
||||
rank,
|
||||
affixes: unit.bossMods ? unit.bossMods.map((m) => `Mod #${m}`) : [],
|
||||
...(kind?.isUndead ? { isUndead: true } : {}),
|
||||
...(kind?.isDemon ? { isDemon: true } : {}),
|
||||
...(isBoss ? { isBoss: true } : {}),
|
||||
modifiers,
|
||||
affixes,
|
||||
...(resistances !== undefined ? { resistances } : {}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -54,7 +54,12 @@ import {
|
|||
} from '../../common/units/npc-table.ts'
|
||||
import type { VendorTabId } from '../../common/items/vendor-generator.ts'
|
||||
import { CANONICAL_CHI_TBL_DICT, CANONICAL_ENG_TBL_DICT } from '../../common/data/canonical-tbl-strings.ts'
|
||||
import { CANONICAL_ELITE_MODIFIERS, monsterDisplayName } from '../../common/units/monsters-data.ts'
|
||||
import {
|
||||
CANONICAL_ELITE_MODIFIERS,
|
||||
FIXED_UNIQUE_BOSS_HCIDX_SET,
|
||||
monsterDisplayName,
|
||||
rollUniqueMonsterNameKeys,
|
||||
} from '../../common/units/monsters-data.ts'
|
||||
import { getSocketableInfo, isJewelCode } from '../../common/items/gems-data.ts'
|
||||
import {
|
||||
transmuteCubeItems,
|
||||
|
|
@ -4475,6 +4480,8 @@ export function hitTestEscapeMenu(
|
|||
export interface MonsterTargetHudInfo {
|
||||
readonly name: string
|
||||
readonly nameKey?: string | undefined
|
||||
readonly hcIdx?: number | undefined
|
||||
readonly nameSeed?: number | undefined
|
||||
readonly hp: number
|
||||
readonly maxHp: number
|
||||
readonly rank?: 'normal' | 'champion' | 'unique' | 'superunique' | 'minion' | 'boss' | undefined
|
||||
|
|
@ -4561,7 +4568,8 @@ const CHAMPION_SUBTYPE_TBL_KEYS: Readonly<Record<string, string>> = {
|
|||
/**
|
||||
* Resolve localized monster title, 2-space lifebar padding (`" " + Name + " "`),
|
||||
* and canonical rank color (`white` Normal/Minion, `blue` Champion, `gold` Unique/SuperUnique/Boss)
|
||||
* per `D2Client.dll` `sub_6FACFDD0` (`0x6FACFDD0`) and `sub_6FB70850` (`0x6FB70850`).
|
||||
* per `D2Client.dll` `sub_6FACFDD0` (`0x6FACFDD0`), `sub_6FB70850` (`0x6FB70850`),
|
||||
* and `sub_6FAD0620` (`0x6FAD0620`).
|
||||
*/
|
||||
export function resolveMonsterTitleAndColor(
|
||||
target: MonsterTargetHudInfo,
|
||||
|
|
@ -4584,6 +4592,35 @@ export function resolveMonsterTitleAndColor(
|
|||
Boolean(target.isBoss)
|
||||
|
||||
if (isGoldRank) {
|
||||
if (
|
||||
target.rank === 'unique' &&
|
||||
target.nameSeed !== undefined &&
|
||||
!FIXED_UNIQUE_BOSS_HCIDX_SET.has(target.hcIdx ?? -1)
|
||||
) {
|
||||
const mods =
|
||||
target.modifiers && target.modifiers.length > 0
|
||||
? target.modifiers
|
||||
: (target.affixes ?? [])
|
||||
const hasBloodRavenUMod = mods.some(m => String(m).trim() === '12')
|
||||
if (hasBloodRavenUMod) {
|
||||
const brName = lookupMonsterTblString('Blood Raven', locale) ?? 'Blood Raven'
|
||||
return {
|
||||
displayName: brName,
|
||||
paddedTitle: ` ${brName} `,
|
||||
titleColor: 'gold',
|
||||
}
|
||||
}
|
||||
const partKeys = rollUniqueMonsterNameKeys(target.nameSeed)
|
||||
const rolledName = partKeys
|
||||
.map(k => lookupMonsterTblString(k, locale) ?? k)
|
||||
.join(' ')
|
||||
return {
|
||||
displayName: rolledName,
|
||||
paddedTitle: ` ${rolledName} `,
|
||||
titleColor: 'gold',
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
displayName: baseName,
|
||||
paddedTitle: ` ${baseName} `,
|
||||
|
|
@ -4720,7 +4757,7 @@ export const CANONICAL_IMMUNITY_DESCRIPTORS: readonly {
|
|||
* covering all 14 displayed `MonUMod.txt` IDs (`5..9, 17, 18, 24..30`),
|
||||
* canonical names, `.tbl` keys (`3205..3218`), and `MonUMod.txt` short names.
|
||||
*/
|
||||
const MONSTER_UMOD_TBL_KEYS: Readonly<Record<string, { readonly tblKey: string; readonly label: string }>> = {
|
||||
export const MONSTER_UMOD_TBL_KEYS: Readonly<Record<string, { readonly tblKey: string; readonly label: string }>> = {
|
||||
'5': { tblKey: 'uniquextrastrong', label: 'Extra Strong' },
|
||||
strong: { tblKey: 'uniquextrastrong', label: 'Extra Strong' },
|
||||
uniquextrastrong: { tblKey: 'uniquextrastrong', label: 'Extra Strong' },
|
||||
|
|
|
|||
|
|
@ -80,6 +80,8 @@ export class ClientUnit {
|
|||
isSuperUnique?: boolean | undefined
|
||||
superUniqueId?: number | undefined
|
||||
bossMods?: readonly number[] | undefined
|
||||
nameSeed?: number | undefined
|
||||
compVariants?: readonly number[] | undefined
|
||||
ownerType?: UnitTypeName | undefined
|
||||
ownerId?: number | undefined
|
||||
overheadText?: string | undefined
|
||||
|
|
@ -146,11 +148,13 @@ export class ClientUnit {
|
|||
this.warpId = snapshot.warpId ?? (this.unitType === 'tile' ? this.classId : undefined)
|
||||
this.objectState = snapshot.objectState
|
||||
this.interactType = snapshot.interactType
|
||||
this.isChampion = snapshot.isChampion
|
||||
this.isUnique = snapshot.isUnique
|
||||
this.isSuperUnique = snapshot.isSuperUnique
|
||||
this.isChampion = snapshot.isChampion ?? ((this.flags & 0x04) !== 0 ? true : undefined)
|
||||
this.isUnique = snapshot.isUnique ?? ((this.flags & 0x08) !== 0 ? true : undefined)
|
||||
this.isSuperUnique = snapshot.isSuperUnique ?? ((this.flags & 0x02) !== 0 ? true : undefined)
|
||||
this.superUniqueId = snapshot.superUniqueId
|
||||
this.bossMods = snapshot.bossMods
|
||||
this.nameSeed = snapshot.nameSeed
|
||||
this.compVariants = snapshot.compVariants
|
||||
this.ownerType = snapshot.ownerType !== undefined ? unitTypeFromId(snapshot.ownerType) : undefined
|
||||
this.ownerId = snapshot.ownerId
|
||||
if (snapshot.states) {
|
||||
|
|
@ -207,6 +211,9 @@ export class ClientUnit {
|
|||
}
|
||||
if (snapshot.flags !== undefined) {
|
||||
this.flags = snapshot.flags
|
||||
if ((snapshot.flags & 0x04) !== 0) this.isChampion = true
|
||||
if ((snapshot.flags & 0x08) !== 0) this.isUnique = true
|
||||
if ((snapshot.flags & 0x02) !== 0) this.isSuperUnique = true
|
||||
}
|
||||
if (snapshot.warpId !== undefined) {
|
||||
this.warpId = snapshot.warpId
|
||||
|
|
@ -234,6 +241,12 @@ export class ClientUnit {
|
|||
if (snapshot.bossMods !== undefined) {
|
||||
this.bossMods = snapshot.bossMods
|
||||
}
|
||||
if (snapshot.nameSeed !== undefined) {
|
||||
this.nameSeed = snapshot.nameSeed
|
||||
}
|
||||
if (snapshot.compVariants !== undefined) {
|
||||
this.compVariants = snapshot.compVariants
|
||||
}
|
||||
if (snapshot.ownerType !== undefined) {
|
||||
this.ownerType = unitTypeFromId(snapshot.ownerType)
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load Diff
|
|
@ -2104,4 +2104,229 @@ export const MONSTER_TABLE: ReadonlyMap<string, MonsterKind> & Readonly<Record<s
|
|||
},
|
||||
) as unknown as ReadonlyMap<string, MonsterKind> & Readonly<Record<string, MonsterKind>>
|
||||
|
||||
const cachedMonsterKindsByHcIdx = new Map<Difficulty, readonly MonsterKind[]>()
|
||||
let cachedSuperUniquesByIndex: readonly SuperUnique[] | null = null
|
||||
|
||||
function normalizeDifficulty(difficulty: Difficulty | number = 'normal'): Difficulty {
|
||||
if (typeof difficulty === 'number') {
|
||||
return difficulty === 2 ? 'hell' : difficulty === 1 ? 'nightmare' : 'normal'
|
||||
}
|
||||
return difficulty
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a canonical 1.13c `MonsterKind` by its 0-based `MonStats.txt` / `monstats.bin` `hcIdx` (`0..733`)
|
||||
* and difficulty (`'normal' | 'nightmare' | 'hell'` or `0 | 1 | 2`).
|
||||
*/
|
||||
export function getMonsterKindByHcIdx(
|
||||
hcIdx: number,
|
||||
difficulty: Difficulty | number = 'normal',
|
||||
): MonsterKind | undefined {
|
||||
if (!Number.isInteger(hcIdx) || hcIdx < 0) 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[] = []
|
||||
for (const [id, kind] of rawKinds) {
|
||||
const ov = diff === 'normal' ? CANONICAL_113C_MONSTER_OVERRIDES[id] : 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),
|
||||
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),
|
||||
experience: ov?.experience ?? (kind.experience > 0 ? kind.experience : 61),
|
||||
attack1: ov?.attack1 ?? (kind.attack1.maxDamage > 0 ? kind.attack1 : { minDamage: 51, maxDamage: 101, toHit: 101 }),
|
||||
attack2: ov?.attack2 ?? kind.attack2,
|
||||
}
|
||||
if (typeof (kind as any).getTreasureClass === 'function') {
|
||||
Object.defineProperty(enrichedKind, 'getTreasureClass', {
|
||||
value: (kind as any).getTreasureClass,
|
||||
enumerable: false,
|
||||
configurable: true,
|
||||
writable: true,
|
||||
})
|
||||
}
|
||||
arr.push(enrichedKind)
|
||||
}
|
||||
list = arr
|
||||
cachedMonsterKindsByHcIdx.set(diff, list)
|
||||
}
|
||||
return list[hcIdx]
|
||||
}
|
||||
|
||||
/**
|
||||
* Look up a canonical 1.13c `SuperUnique` by its 0-based `SuperUniques.txt` / `superuniques.bin` `wIdx` (`0..65`).
|
||||
*/
|
||||
export function getSuperUniqueByIndex(index: number): SuperUnique | undefined {
|
||||
if (!Number.isInteger(index) || index < 0) return undefined
|
||||
if (cachedSuperUniquesByIndex === null) {
|
||||
cachedSuperUniquesByIndex = readSuperUniques(
|
||||
parseTable(new TextEncoder().encode(RAW_SUPERUNIQUES.trim())),
|
||||
)
|
||||
}
|
||||
return cachedSuperUniquesByIndex[index]
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply 1.13c `MonUMod` resistance bonuses (`D2Game.dll` `0x6fccf610`) to a monster's base difficulty resistances.
|
||||
*/
|
||||
export function applyMonsterUModResistances(
|
||||
base: MonsterResistances,
|
||||
bossMods: readonly (number | string)[] | undefined,
|
||||
): MonsterResistances {
|
||||
if (!bossMods || bossMods.length === 0) return base
|
||||
|
||||
let physical = base.physical
|
||||
let magic = base.magic
|
||||
let fire = base.fire
|
||||
let lightning = base.lightning
|
||||
let cold = base.cold
|
||||
const poison = base.poison
|
||||
|
||||
const countImmunities = () =>
|
||||
(physical >= 100 ? 1 : 0) +
|
||||
(magic >= 100 ? 1 : 0) +
|
||||
(fire >= 100 ? 1 : 0) +
|
||||
(lightning >= 100 ? 1 : 0) +
|
||||
(cold >= 100 ? 1 : 0) +
|
||||
(poison >= 100 ? 1 : 0)
|
||||
|
||||
for (const rawMod of bossMods) {
|
||||
const key = String(rawMod).trim().toLowerCase()
|
||||
if (key === '8' || key === 'magicresistant') {
|
||||
if (countImmunities() < 2) fire += 40
|
||||
if (countImmunities() < 2) lightning += 40
|
||||
if (countImmunities() < 2) cold += 40
|
||||
} else if (key === '9' || key === 'fireenchant') {
|
||||
if (countImmunities() < 2 || fire >= 100) fire += 75
|
||||
} else if (key === '17' || key === 'lightenchant') {
|
||||
if (countImmunities() < 2 || lightning >= 100) lightning += 75
|
||||
} else if (key === '18' || key === 'coldenchant') {
|
||||
if (countImmunities() < 2 || cold >= 100) cold += 75
|
||||
} else if (key === '25' || key === 'manahit') {
|
||||
magic += 20
|
||||
} else if (key === '27' || key === 'spectralhit') {
|
||||
if (countImmunities() < 2 && fire < 75) fire += 20
|
||||
if (countImmunities() < 2 && lightning < 75) lightning += 20
|
||||
if (countImmunities() < 2 && cold < 75) cold += 20
|
||||
} else if (key === '28' || key === 'stoneskin') {
|
||||
physical += 50
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
physical,
|
||||
magic,
|
||||
fire,
|
||||
lightning,
|
||||
cold,
|
||||
poison,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical 1.13c `UniquePrefix.txt` / `uniqueprefix.bin` `.tbl` keys (53 records).
|
||||
* Ground truth: `D2Common.dll` `0x6fdaa810` (`MONSTER_ChooseRandomUniquePrefix`, ordinal `10249`).
|
||||
*/
|
||||
export const UNIQUE_MONSTER_PREFIX_KEYS = [
|
||||
'GloomUM', 'Gray', 'DireUM', 'Black', 'ShadowUM', 'Haze', 'Wind', 'StormUM', 'Warp', 'Night',
|
||||
'Moon', 'Star', 'Pit', 'Fire', 'Cold', 'Seethe', 'SharpUM', 'AshUM', 'Blade', 'SteelUM',
|
||||
'StoneUM', 'Rust', 'Mold', 'Blight', 'Plague', 'Rot', 'Ooze', 'Puke', 'Snot', 'Bile',
|
||||
'BloodUM', 'Pulse', 'Gut', 'Gore', 'FleshUM', 'BoneUM', 'SpineUM', 'Mind', 'SpiritUM', 'SoulUM',
|
||||
'Wrath', 'GriefUM', 'Foul', 'Vile', 'Sin', 'ChaosUM', 'DreadUM', 'DoomUM', 'BaneUM', 'DeathUM',
|
||||
'ViperUM', 'Dragon', 'Devil',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Canonical 1.13c `UniqueSuffix.txt` / `uniquesuffix.bin` `.tbl` keys (69 records).
|
||||
* Ground truth: `D2Common.dll` `0x6fdaa7d0` (`MONSTER_ChooseRandomUniqueSuffix`, ordinal `10852`).
|
||||
*/
|
||||
export const UNIQUE_MONSTER_SUFFIX_KEYS = [
|
||||
'touchUM', 'spellUM', 'feast', 'wound', 'grin', 'maim', 'hack', 'biteUM', 'rendUM', 'burn',
|
||||
'rip', 'kill', 'callUM', 'vex', 'jade', 'web', 'shieldUM', 'KillerUM', 'RazorUM', 'drinker',
|
||||
'shifter', 'crawler', 'dancer', 'bender', 'weaverUM', 'eater', 'widow', 'maggot', 'spawn', 'wight',
|
||||
'GrumbleUM', 'GrowlerUM', 'SnarlUM', 'wolf', 'crow', 'hawk', 'cloud', 'BangUM', 'head', 'skullUM',
|
||||
'browUM', 'eyeUM', 'maw', 'tongue', 'fangUM', 'hornUM', 'thorn', 'clawUM', 'fistUM', 'heartUM',
|
||||
'shankUM', 'skinUM', 'wingUM', 'pox', 'fester', 'blister', 'pus', 'SlimeUM', 'drool', 'froth',
|
||||
'sludge', 'venom', 'poison', 'break', 'shard', 'flame', 'maul', 'thirstUM', 'lust',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Canonical 1.13c `UniqueAppellation.txt` / `uniqueappellation.bin` `.tbl` keys (25 records).
|
||||
* Ground truth: `D2Common.dll` `0x6fdaa790` (`MONSTER_ChooseRandomUniqueAppellation`, ordinal `10272`).
|
||||
*/
|
||||
export const UNIQUE_MONSTER_APPELLATION_KEYS = [
|
||||
'the Hammer', 'the Axe', 'the Sharp', 'the Jagged', 'the Flayer',
|
||||
'the Slasher', 'the Impaler', 'the Hunter', 'the Slayer', 'the Mauler',
|
||||
'the Destroyer', 'theQuick', 'the Witch', 'the Mad', 'the Wraith',
|
||||
'the Shade', 'the Dead', 'the Unholy', 'the Howler', 'the Grim',
|
||||
'the Dark', 'the Tainted', 'the Unclean', 'the Hungry', 'the Cold',
|
||||
] as const
|
||||
|
||||
/**
|
||||
* `MonStats.txt` `hcIdx` values that bypass random Unique name rolling in `D2Client.dll` `0x6fad07cc..0x6fad08ec`
|
||||
* (`156` Andariel, `211` Duriel, `242` Mephisto, `243` Diablo, `256` Izual, `333` Diablo Clone, `544` Baal, `704..709` Uber Bosses).
|
||||
*/
|
||||
export const FIXED_UNIQUE_BOSS_HCIDX_SET: ReadonlySet<number> = new Set([
|
||||
156, 211, 242, 243, 256, 333, 544, 704, 705, 706, 707, 708, 709,
|
||||
])
|
||||
|
||||
/**
|
||||
* Deterministic 1.13c random Unique monster name `.tbl` key sequence from `nameSeed`.
|
||||
*
|
||||
* Ground truth:
|
||||
* - `D2Client.dll` `0x6fb5e5cb..0x6fb5e5d8`: stores `wNameSeed = nameSeed & 0xff`.
|
||||
* - `D2Common.dll` `0x6fd86740` (`SEED_InitLowSeed`, ordinal `10614`): `low = wNameSeed`, `high = 0x29a` (`666`).
|
||||
* - `D2Common.dll` `0x6fd51100` / `D2Client.dll` `0x6fab1330`: 64-bit MWC RNG `prod = 0x6ac690c5ULL * low + high`.
|
||||
* - `D2Client.dll` `0x6fad08ef..0x6fad095c`:
|
||||
* 1. `suffix1 = ChooseSuffix(pUnit)` (`roll(69)`, pushed first in `cdecl`)
|
||||
* 2. `prefix1 = ChoosePrefix(pUnit)` (`roll(53)`, pushed second in `cdecl`)
|
||||
* 3. `chance = SEED_RollLimitedRandomNumber(&pUnit->pSeed, 100)` (`roll(100)`)
|
||||
* 4. If `chance < 50`:
|
||||
* - `appellation = ChooseAppellation(pUnit)` (`roll(25)`, pushed first)
|
||||
* - `suffix2 = ChooseSuffix(pUnit)` (`roll(69)`, pushed second)
|
||||
* - `prefix2 = ChoosePrefix(pUnit)` (`roll(53)`, pushed third)
|
||||
* - Formats `string.tbl#1722` (`"%0 %1 %2"`): `[prefix2, suffix2, appellation]`.
|
||||
* 5. Else (`chance >= 50`):
|
||||
* - Formats `string.tbl#1721` (`"%0 %1"`): `[prefix1, suffix1]`.
|
||||
*/
|
||||
export function rollUniqueMonsterNameKeys(nameSeed: number): readonly string[] {
|
||||
let low = BigInt(nameSeed & 0xff)
|
||||
let high = 0x29an
|
||||
|
||||
const roll = (range: number): number => {
|
||||
if (range <= 0) return 0
|
||||
const prod = 0x6ac690c5n * low + high
|
||||
low = prod & 0xffffffffn
|
||||
high = (prod >> 32n) & 0xffffffffn
|
||||
if ((range & (range - 1)) === 0) {
|
||||
return Number(low) & (range - 1)
|
||||
}
|
||||
return Number(low % BigInt(range))
|
||||
}
|
||||
|
||||
const suffixIdx1 = roll(UNIQUE_MONSTER_SUFFIX_KEYS.length)
|
||||
const prefixIdx1 = roll(UNIQUE_MONSTER_PREFIX_KEYS.length)
|
||||
const chance = roll(100)
|
||||
|
||||
if (chance < 50) {
|
||||
const appellationIdx = roll(UNIQUE_MONSTER_APPELLATION_KEYS.length)
|
||||
const suffixIdx2 = roll(UNIQUE_MONSTER_SUFFIX_KEYS.length)
|
||||
const prefixIdx2 = roll(UNIQUE_MONSTER_PREFIX_KEYS.length)
|
||||
return [
|
||||
UNIQUE_MONSTER_PREFIX_KEYS[prefixIdx2]!,
|
||||
UNIQUE_MONSTER_SUFFIX_KEYS[suffixIdx2]!,
|
||||
UNIQUE_MONSTER_APPELLATION_KEYS[appellationIdx]!,
|
||||
]
|
||||
}
|
||||
|
||||
return [
|
||||
UNIQUE_MONSTER_PREFIX_KEYS[prefixIdx1]!,
|
||||
UNIQUE_MONSTER_SUFFIX_KEYS[suffixIdx1]!,
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,195 @@
|
|||
/**
|
||||
* Auto-generated from Diablo II v1.13c `monstats.bin` + `monstats2.bin`.
|
||||
*
|
||||
* Ground truth: `D2Client.dll` `0x6fb5e488..0x6fb5e4e3` and `0x6fab1260` (`D2Game.dll` `0x6fcacb35..0x6fcacb79`).
|
||||
* When `0xAC` (`ASSIGN_NPC`) has `hasAnimComp === 1`, the bitstream encodes 16 component variant indices
|
||||
* (`HD, TR, LG, RA, LA, RH, LH, SH, S1..S8`).
|
||||
* For each component `i` (`0..15`), let `count = monstats2.bin[monstats.bin[hcIdx].wMonStatsEx].nComponentVariants[i]`:
|
||||
* - If `count < 3` (`0`, `1`, or `2`), the bit width read from the `0xAC` stream is `1`.
|
||||
* - If `count >= 3`, the bit width is `bsr(count - 1) + 1` (`32 - Math.clz32(count - 1)`).
|
||||
*/
|
||||
|
||||
export const DEFAULT_MONSTER_COMP_BIT_WIDTHS: readonly number[] = [
|
||||
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
|
||||
]
|
||||
|
||||
/**
|
||||
* Sparse map of `hcIdx` -> 16-element component bit-width array for monsters where at least one
|
||||
* component has `nComponentVariants[i] >= 3` (`bitWidth > 1`).
|
||||
* All other `hcIdx` in `0..733` use `DEFAULT_MONSTER_COMP_BIT_WIDTHS` (16 bits total, 1 bit each).
|
||||
*/
|
||||
export const MONSTATS2_COMP_BIT_WIDTHS_BY_HCIDX: Readonly<Record<number, readonly number[]>> = {
|
||||
0: [3, 2, 2, 2, 2, 4, 1, 3, 4, 4, 1, 1, 1, 1, 1, 1],
|
||||
1: [3, 2, 2, 2, 2, 4, 1, 3, 4, 4, 1, 1, 1, 1, 1, 1],
|
||||
2: [3, 2, 2, 2, 2, 4, 1, 3, 4, 4, 1, 1, 1, 1, 1, 1],
|
||||
3: [3, 2, 2, 2, 2, 4, 1, 3, 4, 4, 1, 1, 1, 1, 1, 1],
|
||||
4: [3, 2, 2, 2, 2, 4, 1, 3, 4, 4, 1, 1, 1, 1, 1, 1],
|
||||
5: [2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
6: [2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
7: [2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
8: [2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
9: [2, 2, 2, 2, 2, 1, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
19: [1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
20: [1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
21: [1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
22: [1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
23: [1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
43: [2, 2, 2, 2, 2, 4, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
44: [2, 2, 2, 2, 2, 4, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
45: [2, 2, 2, 2, 2, 4, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
46: [2, 2, 2, 2, 2, 4, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
47: [2, 2, 2, 2, 2, 4, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
53: [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
54: [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
55: [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
56: [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
57: [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
83: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
84: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
85: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
86: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
91: [1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
92: [1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
93: [1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
94: [1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
95: [1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
127: [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
128: [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
129: [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
130: [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
160: [2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
161: [2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
162: [2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
163: [2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
164: [2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
165: [2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
166: [2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
167: [2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
168: [2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
169: [2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
170: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
171: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
172: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
173: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
174: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
195: [2, 2, 2, 1, 1, 1, 1, 1, 4, 1, 1, 1, 1, 1, 1, 1],
|
||||
196: [2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
235: [3, 3, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1],
|
||||
236: [3, 3, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1],
|
||||
237: [3, 3, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1],
|
||||
274: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
275: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
276: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
277: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
294: [2, 2, 2, 2, 2, 2, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1],
|
||||
296: [2, 2, 2, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
310: [2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
311: [2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 2, 1, 1, 1, 1, 1],
|
||||
312: [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1],
|
||||
360: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
361: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
362: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
363: [2, 1, 1, 2, 2, 4, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
364: [2, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1],
|
||||
373: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
374: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
375: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
376: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
379: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
380: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
381: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
382: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
383: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
384: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
385: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
386: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
387: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
388: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
389: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
390: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
391: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
394: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
395: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
453: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
454: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
455: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
456: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
457: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
458: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
459: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
460: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
522: [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
523: [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
535: [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
536: [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
558: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
560: [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
561: [1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
575: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
576: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
577: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
578: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
579: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
585: [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
586: [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
587: [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
588: [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
589: [1, 1, 1, 1, 1, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
609: [3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
610: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
611: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
612: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
613: [2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
614: [2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
615: [2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
616: [2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
617: [2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
620: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
621: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
622: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
623: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
624: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
625: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
626: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
627: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
628: [3, 2, 1, 2, 2, 1, 1, 1, 2, 2, 2, 2, 1, 1, 1, 1],
|
||||
642: [1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
643: [1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
644: [1, 1, 1, 1, 1, 2, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
648: [3, 2, 2, 2, 2, 4, 1, 3, 4, 4, 1, 1, 1, 1, 1, 1],
|
||||
649: [3, 2, 2, 2, 2, 4, 1, 3, 4, 4, 1, 1, 1, 1, 1, 1],
|
||||
654: [1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
655: [1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
671: [3, 3, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1],
|
||||
672: [3, 3, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1],
|
||||
682: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
683: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
684: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
686: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
687: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
699: [2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
700: [2, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
701: [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1],
|
||||
702: [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1],
|
||||
712: [1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
713: [2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1],
|
||||
718: [1, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
721: [2, 2, 2, 2, 2, 4, 1, 2, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
722: [2, 2, 2, 2, 2, 1, 2, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
723: [2, 2, 2, 2, 2, 2, 1, 1, 2, 2, 2, 1, 1, 1, 1, 1],
|
||||
725: [3, 2, 2, 2, 2, 4, 1, 3, 4, 4, 1, 1, 1, 1, 1, 1],
|
||||
726: [2, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
|
||||
727: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
728: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
729: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
730: [3, 2, 2, 2, 2, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1],
|
||||
}
|
||||
|
||||
export const MONSTATS_HCIDX_COUNT = 734
|
||||
|
||||
/**
|
||||
* Return the 16 component bit widths read by `0xAC` (`ASSIGN_NPC`) when `hasAnimComp === 1`.
|
||||
*/
|
||||
export function getMonsterAnimCompBitWidths(hcIdx: number): readonly number[] {
|
||||
return MONSTATS2_COMP_BIT_WIDTHS_BY_HCIDX[hcIdx] ?? DEFAULT_MONSTER_COMP_BIT_WIDTHS
|
||||
}
|
||||
|
|
@ -9,6 +9,7 @@ import { BitReader } from '../../core/bit-reader.ts'
|
|||
import { ByteReader } from '../../core/byte-reader.ts'
|
||||
import type { UnitType } from '../../domain/ids.ts'
|
||||
import type { ServerEvent } from '../../domain/server-event.ts'
|
||||
import { getMonsterAnimCompBitWidths } from './monstats2-comp-bits.ts'
|
||||
|
||||
function clampUnitType(raw: number): UnitType {
|
||||
return (raw >= 0 && raw <= 5 ? raw : 0) as UnitType
|
||||
|
|
@ -293,7 +294,15 @@ export function decode0x76UnitInSight(pkt: Uint8Array): ServerEvent {
|
|||
* - 4 bits: initial monster mode `nMode` (`D2Game.dll 0x6fcacac9`: `0=DT`, `8=SQ`, `9=KB`, `12=DD`,
|
||||
* or `1=NU` for all other alive modes; applied via `D2Common_11090` at `0x6fb5787c`/`0x6fafd345`)
|
||||
* - 1 bit: `hasAnimComp` (`0x6fb5e47f`)
|
||||
* - 1 bit: `hasMonsterFlags` (`0x6fb5e4fc`, followed by 5 boss flag bits when `hasAnimComp === 0`)
|
||||
* - If `hasAnimComp === 1`, 16 component variant values follow with bit widths from
|
||||
* `monstats2.bin[monstats.bin[hcIdx].wMonStatsEx].nComponentVariants[0..15]` (`0x6fab1260`).
|
||||
* - 1 bit: `hasMonsterFlags` (`0x6fb5e4fc`)
|
||||
* - If `hasMonsterFlags === 1`:
|
||||
* - 5 flag bits: `champion (0x04)`, `unique (0x08)`, `superunique (0x02)`, `minion (0x10)`, `ghostly (0x40)`
|
||||
* - If `superunique`: 16-bit `superUniqueId` (`0x6fb5e549`)
|
||||
* - Up to 9 `uint8` `bossMods` terminated by `0` (`0x6fb5e565..0x6fb5e585`)
|
||||
* - 16-bit `nameSeed` (`0x6fb5e594`)
|
||||
* - 1 bit: `hasOwner` (`0x6fb5e5ae`), followed by 32-bit `ownerId` (`0x6fb5e5c4`) when `1`.
|
||||
*/
|
||||
export function decode0xACAssignNpc(pkt: Uint8Array): ServerEvent {
|
||||
const r = new ByteReader(pkt)
|
||||
|
|
@ -307,22 +316,93 @@ export function decode0xACAssignNpc(pkt: Uint8Array): ServerEvent {
|
|||
|
||||
let rawMode: number | undefined
|
||||
let flags = 0
|
||||
let isChampion: boolean | undefined
|
||||
let isUnique: boolean | undefined
|
||||
let isSuperUnique: boolean | undefined
|
||||
let superUniqueId: number | undefined
|
||||
let bossMods: number[] | undefined
|
||||
let nameSeed: number | undefined
|
||||
let compVariants: number[] | undefined
|
||||
let ownerId: number | undefined
|
||||
|
||||
if (packetLen > 13 && pkt.byteLength >= 13) {
|
||||
const bitReader = new BitReader(pkt.subarray(13, Math.min(pkt.byteLength, packetLen)))
|
||||
if (bitReader.remainingBits >= 4) {
|
||||
rawMode = bitReader.readBits(4) // 4-bit initial monster mode (D2Client.dll 0x6fb5e462)
|
||||
if (bitReader.remainingBits >= 1) {
|
||||
const hasAnimComp = bitReader.readBit()
|
||||
// Note: if hasAnimComp === 1, 16 component values follow (widths depend on MonStats2).
|
||||
// If hasAnimComp === 0, we can directly read the monster flag bits at 0x6fb5e4fc:
|
||||
if (hasAnimComp === 0 && bitReader.remainingBits >= 1) {
|
||||
let canReadFlags = true
|
||||
if (hasAnimComp === 1) {
|
||||
const widths = getMonsterAnimCompBitWidths(classId)
|
||||
const comps: number[] = []
|
||||
for (let i = 0; i < 16; i++) {
|
||||
const w = widths[i] ?? 1
|
||||
if (bitReader.remainingBits < w) {
|
||||
canReadFlags = false
|
||||
break
|
||||
}
|
||||
comps.push(bitReader.readBits(w))
|
||||
}
|
||||
if (canReadFlags) {
|
||||
compVariants = comps
|
||||
}
|
||||
}
|
||||
|
||||
if (canReadFlags && bitReader.remainingBits >= 1) {
|
||||
const hasMonsterFlags = bitReader.readBit()
|
||||
let canReadOwner = hasMonsterFlags === 0
|
||||
if (hasMonsterFlags === 1 && bitReader.remainingBits >= 5) {
|
||||
if (bitReader.readBit()) flags |= 0x04 // champion
|
||||
if (bitReader.readBit()) flags |= 0x08 // unique
|
||||
if (bitReader.readBit()) flags |= 0x02 // superunique
|
||||
if (bitReader.readBit()) {
|
||||
flags |= 0x04 // champion
|
||||
isChampion = true
|
||||
}
|
||||
if (bitReader.readBit()) {
|
||||
flags |= 0x08 // unique
|
||||
isUnique = true
|
||||
}
|
||||
if (bitReader.readBit()) {
|
||||
flags |= 0x02 // superunique
|
||||
isSuperUnique = true
|
||||
}
|
||||
if (bitReader.readBit()) flags |= 0x10 // minion
|
||||
if (bitReader.readBit()) flags |= 0x40 // ghostly
|
||||
|
||||
let flagsComplete = true
|
||||
if (isSuperUnique) {
|
||||
if (bitReader.remainingBits >= 16) {
|
||||
superUniqueId = bitReader.readBits(16)
|
||||
} else {
|
||||
flagsComplete = false
|
||||
}
|
||||
}
|
||||
|
||||
if (flagsComplete) {
|
||||
const mods: number[] = []
|
||||
for (let i = 0; i < 9; i++) {
|
||||
if (bitReader.remainingBits < 8) {
|
||||
flagsComplete = false
|
||||
break
|
||||
}
|
||||
const mod = bitReader.readBits(8)
|
||||
if (mod === 0) break
|
||||
mods.push(mod)
|
||||
}
|
||||
if (mods.length > 0) {
|
||||
bossMods = mods
|
||||
}
|
||||
}
|
||||
|
||||
if (flagsComplete && bitReader.remainingBits >= 16) {
|
||||
nameSeed = bitReader.readBits(16)
|
||||
canReadOwner = true
|
||||
}
|
||||
}
|
||||
|
||||
if (canReadOwner && bitReader.remainingBits >= 1) {
|
||||
const hasOwner = bitReader.readBit()
|
||||
if (hasOwner === 1 && bitReader.remainingBits >= 32) {
|
||||
ownerId = bitReader.readBits(32) >>> 0
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -353,6 +433,14 @@ export function decode0xACAssignNpc(pkt: Uint8Array): ServerEvent {
|
|||
direction,
|
||||
lifePct,
|
||||
flags,
|
||||
...(isChampion !== undefined ? { isChampion } : {}),
|
||||
...(isUnique !== undefined ? { isUnique } : {}),
|
||||
...(isSuperUnique !== undefined ? { isSuperUnique } : {}),
|
||||
...(superUniqueId !== undefined ? { superUniqueId } : {}),
|
||||
...(bossMods !== undefined ? { bossMods } : {}),
|
||||
...(nameSeed !== undefined ? { nameSeed } : {}),
|
||||
...(compVariants !== undefined ? { compVariants } : {}),
|
||||
...(ownerId !== undefined ? { ownerId } : {}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -232,6 +232,8 @@ export interface UnitSnapshot {
|
|||
readonly isSuperUnique?: boolean | undefined
|
||||
readonly superUniqueId?: number | undefined
|
||||
readonly bossMods?: readonly number[] | undefined
|
||||
readonly nameSeed?: number | undefined
|
||||
readonly compVariants?: readonly number[] | undefined
|
||||
readonly ownerType?: UnitType | undefined
|
||||
readonly ownerId?: number | undefined
|
||||
readonly states?: readonly number[] | undefined
|
||||
|
|
|
|||
|
|
@ -226,6 +226,12 @@ export {
|
|||
D2gsS2cFramer,
|
||||
resolveS2cPacketSize,
|
||||
} from './d2gs/framing-s2c.ts'
|
||||
export {
|
||||
DEFAULT_MONSTER_COMP_BIT_WIDTHS,
|
||||
MONSTATS2_COMP_BIT_WIDTHS_BY_HCIDX,
|
||||
MONSTATS_HCIDX_COUNT,
|
||||
getMonsterAnimCompBitWidths,
|
||||
} from './d2gs/s2c/monstats2-comp-bits.ts'
|
||||
|
||||
import { ByteWriter } from './core/byte-writer.ts'
|
||||
import { resolveS2cPacketSize } from './d2gs/framing-s2c.ts'
|
||||
|
|
|
|||
|
|
@ -822,5 +822,138 @@ describe('Diablo II 1.13c Monster Lifebar & 2-Line Subtext Tooltip Parity', () =
|
|||
expect(modded.resistances?.lightning).toBe(20)
|
||||
expect(modded.resistances?.poison).toBe(75)
|
||||
})
|
||||
|
||||
it('maps online ClientUnit snapshots via clientUnitToMonsterTargetInfo with 1.13c nameKey, descKey, race, rank, modifiers, and immunities', async () => {
|
||||
const { ClientUnit } = await import('../src/client/world/client-unit.ts')
|
||||
const { clientUnitToMonsterTargetInfo } = await import('../src/client/ui-model/hud-model.ts')
|
||||
|
||||
// 1. Normal Skeleton (hcIdx = 0, Skeleton1): Undead
|
||||
const skelUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
id: 101,
|
||||
classId: 0,
|
||||
x: 5000,
|
||||
y: 5000,
|
||||
lifePct: 128,
|
||||
})
|
||||
const skelInfo = clientUnitToMonsterTargetInfo(skelUnit, 0)
|
||||
expect(skelInfo.name).toBe('Skeleton')
|
||||
expect(skelInfo.nameKey).toBe('Skeleton')
|
||||
expect(skelInfo.rank).toBe('normal')
|
||||
expect(skelInfo.isUndead).toBe(true)
|
||||
expect(buildMonsterHoverSubtext(skelInfo, 'en').line1).toBe('Undead')
|
||||
expect(buildMonsterHoverSubtext(skelInfo, 'zh').line1).toBe('不死的')
|
||||
|
||||
// 2. SuperUnique Bishibosh (hcIdx = 58 fallenshaman1, superUniqueId = 0, bossMods = [8, 9])
|
||||
const bishUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
id: 102,
|
||||
classId: 58,
|
||||
x: 5010,
|
||||
y: 5010,
|
||||
lifePct: 100,
|
||||
flags: 0x0a,
|
||||
isUnique: true,
|
||||
isSuperUnique: true,
|
||||
superUniqueId: 0,
|
||||
bossMods: [8, 9],
|
||||
})
|
||||
const bishInfo = clientUnitToMonsterTargetInfo(bishUnit, 0)
|
||||
expect(bishInfo.name).toBe('Bishibosh')
|
||||
expect(bishInfo.nameKey).toBe('Bishibosh')
|
||||
expect(bishInfo.rank).toBe('superunique')
|
||||
expect(bishInfo.isDemon).toBe(true)
|
||||
expect(resolveMonsterTitleAndColor(bishInfo, 'en').titleColor).toBe('gold')
|
||||
expect(resolveMonsterTitleAndColor(bishInfo, 'zh').displayName).toBe('畢須博須')
|
||||
const bishSubEn = buildMonsterHoverSubtext(bishInfo, 'en')
|
||||
expect(bishSubEn.line1).toBe('Demon Magic Resistant Fire Enchanted Raises Fallen')
|
||||
expect(bishSubEn.line2).toBe('Immune to Fire')
|
||||
|
||||
// 3. Ghostly Champion Fallen (hcIdx = 19 fallen1, flags = 0x44, bossMods = [36])
|
||||
const champUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
id: 103,
|
||||
classId: 19,
|
||||
x: 5020,
|
||||
y: 5020,
|
||||
lifePct: 128,
|
||||
flags: 0x44,
|
||||
bossMods: [36],
|
||||
})
|
||||
const champInfo = clientUnitToMonsterTargetInfo(champUnit, 0)
|
||||
expect(champInfo.rank).toBe('champion')
|
||||
expect(resolveMonsterTitleAndColor(champInfo, 'en')).toMatchObject({
|
||||
displayName: 'Ghostly Fallen',
|
||||
titleColor: 'blue',
|
||||
})
|
||||
|
||||
// 4. Minion Fallen (hcIdx = 19 fallen1, flags = 0x10)
|
||||
const minionUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
id: 104,
|
||||
classId: 19,
|
||||
x: 5030,
|
||||
y: 5030,
|
||||
lifePct: 128,
|
||||
flags: 0x10,
|
||||
})
|
||||
const minionInfo = clientUnitToMonsterTargetInfo(minionUnit, 0)
|
||||
expect(minionInfo.rank).toBe('minion')
|
||||
expect(buildMonsterHoverSubtext(minionInfo, 'en').line1).toBe('Demon Minion')
|
||||
|
||||
// 5. Act Boss Andariel (hcIdx = 156 andariel)
|
||||
const andarielUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
id: 105,
|
||||
classId: 156,
|
||||
x: 5040,
|
||||
y: 5040,
|
||||
lifePct: 128,
|
||||
})
|
||||
const andarielInfo = clientUnitToMonsterTargetInfo(andarielUnit, 0)
|
||||
expect(andarielInfo.rank).toBe('boss')
|
||||
expect(andarielInfo.isBoss).toBe(true)
|
||||
expect(andarielInfo.isDemon).toBe(true)
|
||||
expect(resolveMonsterTitleAndColor(andarielInfo, 'en').titleColor).toBe('gold')
|
||||
|
||||
// 6. Uber Diablo (hcIdx = 705 uberdiablo) suppresses modifiers and immunities (0x6facfedb & 0x6fb6fca0)
|
||||
const uberUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
id: 106,
|
||||
classId: 705,
|
||||
x: 5050,
|
||||
y: 5050,
|
||||
lifePct: 128,
|
||||
bossMods: [8, 9, 17, 18],
|
||||
})
|
||||
const uberInfo = clientUnitToMonsterTargetInfo(uberUnit, 2)
|
||||
expect(uberInfo.modifiers).toEqual([])
|
||||
expect(uberInfo.resistances).toBeUndefined()
|
||||
|
||||
// 7. Random Unique Fallen (hcIdx = 19, flags = 0x08, nameSeed = 0x1234 -> wNameSeed = 0x34)
|
||||
const randUniqueUnit = new ClientUnit({
|
||||
unitType: 1,
|
||||
id: 107,
|
||||
classId: 19,
|
||||
x: 5060,
|
||||
y: 5060,
|
||||
lifePct: 128,
|
||||
flags: 0x08,
|
||||
isUnique: true,
|
||||
nameSeed: 0x1234,
|
||||
bossMods: [5, 6],
|
||||
})
|
||||
const randUniqueInfo = clientUnitToMonsterTargetInfo(randUniqueUnit, 0)
|
||||
expect(randUniqueInfo.rank).toBe('unique')
|
||||
expect(randUniqueInfo.nameSeed).toBe(0x1234)
|
||||
const enTitle = resolveMonsterTitleAndColor(randUniqueInfo, 'en')
|
||||
const zhTitle = resolveMonsterTitleAndColor(randUniqueInfo, 'zh')
|
||||
expect(enTitle.titleColor).toBe('gold')
|
||||
expect(enTitle.displayName).not.toBe('Fallen')
|
||||
expect(enTitle.displayName.split(' ').length).toBeGreaterThanOrEqual(2)
|
||||
expect(zhTitle.titleColor).toBe('gold')
|
||||
expect(zhTitle.displayName).not.toBe('沉淪魔')
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
import { ByteWriter } from '../../src/netproto/core/byte-writer.ts'
|
||||
import {
|
||||
BitWriter,
|
||||
decodeD2gsS2cPacket,
|
||||
decodeD2gsServerPacket,
|
||||
getMonsterAnimCompBitWidths,
|
||||
S2C_PACKET_SIZES,
|
||||
} from '../../src/netproto/index.ts'
|
||||
|
||||
|
|
@ -183,6 +185,53 @@ describe('D2GS S->C 1.13c packet decoders (M1)', () => {
|
|||
expect(corpseEv.unit.lifePct).toBe(0)
|
||||
expect(corpseEv.unit.mode).toBe(12)
|
||||
}
|
||||
|
||||
// SuperUnique Bishibosh (classId=58 fallenshaman1, superUniqueId=0) with hasAnimComp=1, bossMods=[8, 9], nameSeed=0x1234, ownerId=0xdeadbeef
|
||||
const bw = new BitWriter()
|
||||
bw.writeBits(1, 4) // rawMode = 1 (MONMODE_NEUTRAL)
|
||||
bw.writeBit(1) // hasAnimComp = 1
|
||||
const widths = getMonsterAnimCompBitWidths(1) // hcIdx=1 (returned / Skeleton, has multi-bit component widths)
|
||||
for (let i = 0; i < 16; i++) {
|
||||
bw.writeBits(i % (1 << widths[i]!), widths[i]!)
|
||||
}
|
||||
bw.writeBit(1) // hasMonsterFlags = 1
|
||||
bw.writeBit(0) // champion = 0
|
||||
bw.writeBit(1) // unique = 1
|
||||
bw.writeBit(1) // superunique = 1
|
||||
bw.writeBit(0) // minion = 0
|
||||
bw.writeBit(0) // ghostly = 0
|
||||
bw.writeBits(0, 16) // superUniqueId = 0 (Bishibosh)
|
||||
bw.writeBits(8, 8) // mod 0 = 8 (Magic Resistant)
|
||||
bw.writeBits(9, 8) // mod 1 = 9 (Fire Enchanted)
|
||||
bw.writeBits(0, 8) // terminator = 0
|
||||
bw.writeBits(0x1234, 16) // nameSeed = 0x1234
|
||||
bw.writeBit(1) // hasOwner = 1
|
||||
bw.writeBits(0xdeadbeef, 32) // ownerId = 0xdeadbeef
|
||||
const bitBytes = bw.toUint8Array()
|
||||
const totalLen = 13 + bitBytes.byteLength
|
||||
const bossWriter = new ByteWriter(totalLen)
|
||||
bossWriter.u8(0xac)
|
||||
bossWriter.u32LE(777)
|
||||
bossWriter.u16LE(1)
|
||||
bossWriter.u16LE(5120)
|
||||
bossWriter.u16LE(5140)
|
||||
bossWriter.u8(96)
|
||||
bossWriter.u8(totalLen)
|
||||
bossWriter.bytes(bitBytes)
|
||||
|
||||
const bossEv = decodeD2gsServerPacket(bossWriter.toUint8Array())
|
||||
expect(bossEv.type).toBe('UnitAssign')
|
||||
if (bossEv.type === 'UnitAssign') {
|
||||
expect(bossEv.unit.id).toBe(777)
|
||||
expect(bossEv.unit.classId).toBe(1)
|
||||
expect(bossEv.unit.isUnique).toBe(true)
|
||||
expect(bossEv.unit.isSuperUnique).toBe(true)
|
||||
expect(bossEv.unit.superUniqueId).toBe(0)
|
||||
expect(bossEv.unit.bossMods).toEqual([8, 9])
|
||||
expect(bossEv.unit.nameSeed).toBe(0x1234)
|
||||
expect(bossEv.unit.ownerId).toBe(0xdeadbeef)
|
||||
expect(bossEv.unit.compVariants).toHaveLength(16)
|
||||
}
|
||||
})
|
||||
|
||||
it('maps 0x69/0x6A/0x6B state command index via D2Client.dll 0x6fb82e68 (MONSTER_CMD_TO_MODE)', () => {
|
||||
|
|
|
|||
Loading…
Reference in New Issue