diff --git a/src/baker/packers/pack-canonical-tbl.ts b/src/baker/packers/pack-canonical-tbl.ts index b309bf3..f4e784b 100644 --- a/src/baker/packers/pack-canonical-tbl.ts +++ b/src/baker/packers/pack-canonical-tbl.ts @@ -350,6 +350,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']) diff --git a/src/client/ui-model/hud-model.ts b/src/client/ui-model/hud-model.ts index f4a9a17..5de0535 100644 --- a/src/client/ui-model/hud-model.ts +++ b/src/client/ui-model/hud-model.ts @@ -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' @@ -1231,11 +1245,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 { @@ -1943,25 +1963,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 } : {}), } } diff --git a/src/client/ui/world-panels.ts b/src/client/ui/world-panels.ts index f4161f8..5b4b70e 100644 --- a/src/client/ui/world-panels.ts +++ b/src/client/ui/world-panels.ts @@ -55,7 +55,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, @@ -5085,6 +5090,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 @@ -5171,7 +5178,8 @@ const CHAMPION_SUBTYPE_TBL_KEYS: Readonly> = { /** * 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, @@ -5194,6 +5202,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} `, @@ -5330,7 +5367,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> = { +export const MONSTER_UMOD_TBL_KEYS: Readonly> = { '5': { tblKey: 'uniquextrastrong', label: 'Extra Strong' }, strong: { tblKey: 'uniquextrastrong', label: 'Extra Strong' }, uniquextrastrong: { tblKey: 'uniquextrastrong', label: 'Extra Strong' }, diff --git a/src/client/world/client-unit.ts b/src/client/world/client-unit.ts index edd8e0a..ef7e239 100644 --- a/src/client/world/client-unit.ts +++ b/src/client/world/client-unit.ts @@ -84,6 +84,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 @@ -150,11 +152,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) { @@ -211,6 +215,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 @@ -238,6 +245,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) } diff --git a/src/common/data/canonical-tbl-strings.ts b/src/common/data/canonical-tbl-strings.ts index e10b206..a9fddd3 100644 --- a/src/common/data/canonical-tbl-strings.ts +++ b/src/common/data/canonical-tbl-strings.ts @@ -268,6 +268,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Arreat Plateau": "Arreat Plateau", "Arreat's Face": "Arreat's Face", "Artificer's": "Artisan's", + "AshUM": "Ash", "AssOnly": "(Assassin Only)", "Assailant": "Assailant", "Assamic": "Lunar", @@ -302,6 +303,8 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Balrog": "Balrog", "Bane": "Bane", "Bane Ash": "Bane Ash", + "BaneUM": "Bane", + "BangUM": "Bang", "Banished": "The Banished", "BarOnly": "(Barbarian Only)", "Baranar's Star": "Baranar's Star", @@ -321,9 +324,11 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Berserker's Hauberk": "Berserker's Hauberk", "Berserker's Headgear": "Berserker's Headgear", "Beryl": "Beryl", + "Bile": "Bile", "Bing Sz Wang": "Bing Sz Wang", "Bishibosh": "Bishibosh", "Bitter": "Bitter", + "Black": "Black", "Black Hades": "Black Hades", "Black Marsh": "Black Marsh", "BlackArcher": "Black Archer", @@ -339,6 +344,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Blackleach Blade": "Blackleach Blade", "Blackoak Shield": "Blackoak Shield", "Blacktongue": "Blacktongue", + "Blade": "Blade", "Blade Creeper": "Blade Sentinel", "Blade of Ali Baba": "Blade of Ali Baba", "Bladebone": "Bladebone", @@ -349,6 +355,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Blaze Ripper": "Blaze Ripper", "Blazing": "Blazing", "Blessed": "Blessed", + "Blight": "Blight", "Blighting": "Blighting", "Blinkbats Form": "Blinkbat's Form", "Blood": "Blood", @@ -371,6 +378,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "BloodHook": "Blood Hook", "BloodHookNest": "Blood Hook Nest", "BloodLord": "Blood Lord", + "BloodUM": "Blood", "BloodWing": "Blood Wing", "BloodWingNest": "Blood Wing Nest", "Bloodfist": "Bloodfist", @@ -389,6 +397,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Bone": "Bone", "BoneArcher": "Bone Archer", "BoneMage": "Bone Mage", + "BoneUM": "Bone", "BoneWarrior": "Bone Warrior", "Boneash": "Bone Ash", "Bonebreak": "Bonebreaker", @@ -472,6 +481,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Chance Guards": "Chance Guards", "Chaos": "Chaos", "Chaos Sanctum": "The Chaos Sanctuary", + "ChaosUM": "Chaos", "Charged": "Charged", "Charged Bolt Sentry": "Charged Bolt Sentry", "Charsi": "Charsi", @@ -500,6 +510,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Cloudcrack": "Cloudcrack", "Cobalt": "Cobalt", "Coif of Glory": "Coif of Glory", + "Cold": "Ice", "Cold Plains": "Cold Plains", "Coldcrow": "Coldcrow", "Coldkill": "Coldkill", @@ -582,6 +593,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Death's Hand": "Death's Hand", "Death's Touch": "Death's Touch", "DeathClan": "Death Clan", + "DeathUM": "Death", "Deathbit": "Deathbit", "Deathcleaver": "Death Cleaver", "Deaths's Web": "Death's Web", @@ -598,6 +610,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Den of Evil": "Den of Evil", "Dense": "Dense", "DesertWing": "Desert Wing", + "Devil": "Devil", "Devilkin": "Devilkin", "DevilkinShaman": "Devilkin Shaman", "Devious": "Devious", @@ -609,6 +622,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Diamond": "Diamond", "Dimoaks Hew": "Dimoak's Hew", "Dire": "Dire", + "DireUM": "Dire", "Disfigured": "Disfigured", "Disused Fane": "Disused Fane", "Disused Reliquary": "Disused Reliquary", @@ -618,14 +632,17 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Doom": "Doom", "DoomApe": "Doom Ape", "DoomKnight": "Doom Knight", + "DoomUM": "Doom", "Doombringer": "Doombringer", "Doomspittle": "Doomslinger", "Dopplezon": "Decoy", "Dracul's Grasp": "Dracul's Grasp", + "Dragon": "Dragon", "Dragon's": "Dragon's", "Dragonscale": "Dragonscale", "Drake's": "Drake's", "Dread": "Dread", + "DreadUM": "Dread", "Drehya": "Anya", "DriedCorpse": "Dried Corpse", "Drognan": "Drognan", @@ -989,6 +1006,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Fighter": "Fighter", "Fighter Mage": "Fighter Mage", "Fine": "Fine", + "Fire": "Flame", "Fire Eye": "Fire Eye", "FireBoar": "Fire Boar", "FireGolem": "Fire Golem", @@ -1009,6 +1027,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "FleshHunter": "Flesh Hunter", "FleshLancer": "Flesh Lancer", "FleshSpawner": "Flesh Spawner", + "FleshUM": "Flesh", "Fleshrender": "Fleshrender", "Fleshripper": "Fleshripper", "Fletcher's": "Fletcher's", @@ -1019,6 +1038,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Forked": "Forked", "Fortified": "Fortified", "Fortuitous": "Fortuitous", + "Foul": "Foul", "FoulCrow": "Foul Crow", "FoulCrowNest": "Foul Crow Nest", "Freezing": "Freezing", @@ -1071,6 +1091,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Gleamscythe": "Gleamscythe", "Glimmering": "Glimmering", "Gloam": "Gloam", + "GloomUM": "Gloom", "Gloombat": "Gloombat", "Gloomstrap": "Gloom's Trap", "Glorious": "Glorious", @@ -1084,6 +1105,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Goldwrap": "Goldwrap", "Golemlord's": "Golemlord's", "Gorbelly": "Gorebelly", + "Gore": "Gore", "Gore Ripper": "Gore Ripper", "GoreBearer": "Gore Bearer", "Gorefoot": "Gorefoot", @@ -1094,12 +1116,14 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Gravenspine": "Gravenspine", "Gravepalm": "Gravepalm", "Graverobber's": "Graverobber's", + "Gray": "Gray", "Great Marsh": "Great Marsh", "Great Wyrm's": "Great Wyrm's", "GreaterHellSpawn": "Greater Hell Spawn", "GreaterIceSpawn": "Greater Ice Spawn", "Greiz": "Greiz", "Greyform": "Greyform", + "GriefUM": "Grief", "Griffon's Eye": "Griffon's Eye", "Grim": "Grim", "Grim's Burning Dead": "Grim's Burning Dead", @@ -1114,6 +1138,8 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Groper": "Doom Caster", "Grotesque": "Grotesque", "GrotesqueWyrm": "Grotesque Wyrm", + "GrowlerUM": "Growler", + "GrumbleUM": "Grumble", "Guard": "Guard", "Guardian": "Guardian", "Guardian Angel": "Guardian Angel", @@ -1121,6 +1147,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Guardian's": "Guardian's", "Guillaume's Face": "Guillaume's Face", "Gull": "Gull", + "Gut": "Gut", "Gutsiphon": "Gut Siphon", "Gymnast's": "Gymnastic", "HP": "Life", @@ -1144,6 +1171,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Hawk Branded": "Hawk Branded", "Hawkeye": "Hawkeye", "Hawkmail": "Hawkmail", + "Haze": "Haze", "Headhunter's Glory": "Head Hunter's Glory", "Headstriker": "Headstriker", "Heart Carver": "Heart Carver", @@ -1310,6 +1338,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Kenshi's": "Kenshi's", "Kerke's Sanctuary": "Gerke's Sanctuary", "KhalimFlail": "Khalim's Flail", + "KillerUM": "Killer", "Kinemils Awl": "Kinemil's Awl", "King's": "King's", "Kira's Guardian": "Kira's Guardian", @@ -1480,6 +1509,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Milabrega's Regalia": "Milabrega's Regalia", "Milabrega's Robe": "Milabrega's Robe", "Milabrega's Rod": "Milabrega's Rod", + "Mind": "Mind", "Mindrend": "Skull Splitter", "MinionIce/fireBoarSpawner": "Demon Portal", "MinionSlayerSpawner": "Demon Portal", @@ -1710,9 +1740,11 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "ModitemskonLevel": "%d%% Chance to cast level %d %s when you Level-Up", "Moditemskondeath": "%d%% Chance to cast level %d %s when you Die", "Modstr2v": "Enhanced Defense", + "Mold": "Mold", "Monastery Gate": "Monastery Gate", "Monk's": "Monk's", "Moo Moo Farm": "The Secret Cow Level", + "Moon": "Moon", "MoonClan": "Moon Clan", "Moonfall": "Moonfall", "Mosers Blessed Circle": "Moser's Blessed Circle", @@ -1744,6 +1776,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Necromancer's": "Necromancer's", "Nethercrow": "Nethercrow", "Nickel": "Nickel", + "Night": "Night", "NightClan": "Night Clan", "NightLord": "Night Lord", "NightMarauder": "Night Marauder", @@ -1766,6 +1799,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Oligocene": "Honorable", "Ondal's Almighty": "Ondal's Almighty", "Ondal's Wisdom": "Ondal's Wisdom", + "Ooze": "Ooze", "Order": "Order", "Ormus' Robes": "Ormus' Robes", "Orphan's Call": "Orphan's Call", @@ -1800,11 +1834,13 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Piercerib": "Rogue's Bow", "Pierre Tombale Couant": "Pierre Tombale Couant", "Pindleskin": "Pindleskin", + "Pit": "Pit", "Pit Level 1": "Pit Level 1", "Pit Level 2": "Pit Level 2", "PitLord": "Pit Lord", "PitViper": "Pit Viper", "Pitspawn Fouldog": "Pitspawn Fouldog", + "Plague": "Plague", "Plague Bearer": "Plague Bearer", "PlagueBearer": "Plague Bearer", "PlagueBugs": "Plague Bugs", @@ -1822,7 +1858,9 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Prison Door": "Prison Door", "ProwlingDead": "Prowling Dead", "Psychic": "Psychic", + "Puke": "Puke", "Pullspite": "Stormstrike", + "Pulse": "Pulse", "Pure": "Pure", "Pus Spiter": "Pus Spitter", "Putrid Defiler1": "Putrid Defiler", @@ -1850,6 +1888,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "RazorBeast": "Razor Beast", "RazorPitDemon": "Cliff Lurker", "RazorSpine": "Razor Spine", + "RazorUM": "Razor", "Razoredge": "Razor's Edge", "Razorswitch": "Razorswitch", "Razortail": "Razortail", @@ -1888,6 +1927,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Rogue Encampment": "Rogue Encampment", "RogueScout": "Rogue Scout", "Rose Branded": "Rose Branded", + "Rot": "Rot", "RotWalker": "Rot Walker", "Ruby": "Ruby", "Rugged": "Rugged", @@ -2067,6 +2107,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Runeword98": "Peace", "Runeword99": "Voice of Reason", "Russet": "Russet", + "Rust": "Rust", "Rusthandle": "Rusthandle", "Rusty": "Rusty", "SaberCat": "Saber Cat", @@ -2097,6 +2138,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Scintillating": "Scintillating", "Scorching": "Condensing", "Screaming": "Screaming", + "Seethe": "Seethe", "Sell": "Sell value: ", "Sensei's": "Sensei's", "Septic": "Septic", @@ -2112,6 +2154,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Shadow": "Shadow", "Shadow Master": "Shadow Master", "Shadow Warrior": "Shadow Warrior", + "ShadowUM": "Shadow", "Shadowdancer": "Shadow Dancer", "Shadowfang": "Shadowfang", "Shadowkiller": "Shadow Killer", @@ -2119,6 +2162,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Shaman's": "Shaman's", "Sharp": "Sharp", "Sharp Tooth Sayer": "Sharptooth Slayer", + "SharpUM": "Sharp", "Shimmering": "Shimmering", "Shivering": "Shivering", "Shocking": "Shocking", @@ -2136,6 +2180,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Sigurd's Staunch": "Siggard's Stealth", "Silkweave": "Silkweave", "Silver": "Silver", + "Sin": "Sin", "Skeleton": "Skeleton", "SkeletonArcher": "Skeleton Archer", "Skillname222": "Raven", @@ -2207,12 +2252,15 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Slayer's": "Slayer's", "Slayerexp": "Slayer", "Slime Prince": "Slime Prince", + "SlimeUM": "Slime", "Slinger": "Slinger", "Smoking": "Smoking", "Smoldering": "Smoldering", "Snake's": "Snake's", "Snakecord": "Snakecord", "Snapchip Shatter": "Snapchip Shatter", + "SnarlUM": "Snarl", + "Snot": "Snot", "SnowYeti1": "Snow Drifter", "SnowYeti2": "Abominable", "SnowYeti3": "Chilled Froth", @@ -2227,6 +2275,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Soul Harvest": "Soul Harvest", "SoulKiller": "Soul Killer", "SoulKillerShaman": "Soul Killer Shaman", + "SoulUM": "Soul", "Souldrain": "Soul Drainer", "Soulfeast Tine": "Soulfeast Tine", "Soulflay": "Soulflay", @@ -2246,21 +2295,25 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "SpikeFiend": "Spike Fiend", "SpikeGiant": "Spike Giant", "Spikefist": "Spikefist", + "SpineUM": "Spine", "Spineripper": "Spineripper", "Spire of Honor": "Spire of Honor", "Spirit": "Spirit", "Spirit Ward": "Spirit Ward", "Spirit of Barbs": "Spirit of Barbs", + "SpiritUM": "Spirit", "Spiritforge": "Spirit Forge", "Spiritkeeper": "Spirit Keeper", "Spiritual": "Spiritual", "Spiritual Custodian": "Dark Adherent", "Staff of Kings": "Staff of Kings", "Stalwart": "Stalwart", + "Star": "Star", "Static": "Static", "Stealskull": "Stealskull", "Steel": "Steel", "Steel Carapice": "Steel Carapace", + "SteelUM": "Steel", "SteelWeevil": "Steel Scarab", "Steelclash": "Steelclash", "Steeldriver": "Steeldriver", @@ -2270,12 +2323,14 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Steelshade": "Steel Shade", "Stone": "Stone", "Stone Crusher": "Stone Crusher", + "StoneUM": "Stone", "Stoneraven": "Stoneraven", "Stony Field": "Stony Field", "Stony Tomb Level 1": "Stony Tomb Level 1", "Stony Tomb Level 2": "Stony Tomb Level 2", "Storm": "Storm", "StormCaster": "Storm Caster", + "StormUM": "Storm", "Stormchaser": "Stormchaser", "Stormeye": "Stormeye", "Stormguild": "Stormguild", @@ -2796,6 +2851,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Vidala's Rig": "Vidala's Rig", "Vidala's Snare": "Vidala's Snare", "Vigorous": "Vigorous", + "Vile": "Vile", "VileArcher": "Vile Archer", "VileHunter": "Vile Hunter", "VileLancer": "Vile Lancer", @@ -2804,6 +2860,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Vine Creature": "Vine Creature", "Vinvear Molech": "Vinvear Molech", "Viper": "Viper", + "ViperUM": "Viper", "Viperfork": "Viperfork", "Viridian": "Viridian", "Visceratuant": "Visceratuant", @@ -2857,6 +2914,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Widowmaker": "Widowmaker", "Wihtstan's Guard": "Whitstan's Guard", "Wilhelm's Pride": "Wilhelm's Pride", + "Wind": "Wind", "Windforce": "Windforce", "Windhammer": "Windhammer", "Winged Death": "Darkwing", @@ -2881,6 +2939,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "Wraith": "Wraith", "Wraithflight": "Wraith Flight", "Wraithra": "Wraith", + "Wrath": "Wrath", "Wyand Voidfinger": "Wyand Voidbringer", "Wyrm's": "Wyrm's", "Yelling": "Yelling", @@ -2941,24 +3000,29 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "bax": "Broad Axe", "bbb": "Lam Esen's Tome", "beads": "Beads", + "bender": "Bender", "bet": "Burning Essence of Terror", "bey": "Baal's Eye", "bhm": "Bone Helm", "bite": "Bite", + "biteUM": "Bite", "bkd": "Key to the Cairn Stones", "bkf": "Balanced Knife", "bks": "Scroll of Inifuss", "blazer": "Blazer", "bld": "Blade", + "blister": "Blister", "blow": "Blow", "bludgeon": "Bludgeon", "bolt": "Bolt", "box": "Horadric Cube", "branch": "Branch", "brand": "Brand", + "break": "Break", "breaker": "Breaker", "brn": "Brandistock", "brow": "Brow", + "browUM": "Brow", "brs": "Breast Plate", "brz": "Brain", "bsd": "Broad Sword", @@ -2969,8 +3033,10 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "btx": "Battle Axe", "buc": "Buckler", "buckle": "Buckle", + "burn": "Burn", "bwn": "Bone Wand", "call": "Call", + "callUM": "Call", "cap": "Cap", "carapace": "Carapace", "casque": "Casque", @@ -2992,11 +3058,13 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "circlet": "Circlet", "clasp": "Clasp", "claw": "Claw", + "clawUM": "Claw", "clb": "Club", "cleaver": "Cleaver", "clm": "Claymore", "cloak": "Cloak", "close": "close", + "cloud": "Cloud", "clutches": "Clutches", "clw": "Claws", "cm1": "Small Charm", @@ -3012,15 +3080,18 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "cowl": "Cowl", "cqv": "Bolts", "crack": "Crack", + "crawler": "Crawler", "crest": "Crest", "crn": "Crown", "crook": "Crook", + "crow": "Crow", "crs": "Crystal Sword", "crusher": "Crusher", "cry": "Cry", "cst": "Gnarled Staff", "cudgel": "Cudgel", "d33": "Decoy Gidbinn", + "dancer": "Dancer", "dart": "Dart", "dash": "-", "dgr": "Dagger", @@ -3044,20 +3115,29 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "drd": "Earth Spirit", "dre": "Sky Spirit", "drf": "Dream Spirit", + "drinker": "Drinker", + "drool": "Drool", "dummy": "an evil force", "ear": "Ear", + "eater": "Eater", "eats and spits corpses": "Eats and Spits Corpses", "edge": "Edge", "elx": "Elixir", "emblem": "Emblem", "eye": "Eye", + "eyeUM": "Eye", "eyz": "Eye", "fang": "Fang", + "fangUM": "Fang", + "feast": "Feast", "fed": "Festering Essence of Destruction", + "fester": "Fester", "fhl": "Full Helm", "finger": "Finger", "fist": "Fist", + "fistUM": "Fist", "fla": "Flail", + "flame": "Flame", "flange": "Flange", "flb": "Flamberge", "flc": "Falchion", @@ -3067,6 +3147,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "flight": "Flight", "fng": "Fang", "fringe": "Fringe", + "froth": "Froth", "ful": "Full Plate Mail", "g33": "The Gidbinn", "g34": "The Golden Bird", @@ -3110,6 +3191,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "gpy": "Perfect Topaz", "grasp": "Grasp", "greave": "Greaves", + "grin": "Grin", "grinder": "Grinder", "grip": "Grip", "gsb": "Sapphire", @@ -3127,18 +3209,22 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "gwn": "Grim Wand", "gyre": "Gyre", "gzv": "Flawless Amethyst", + "hack": "Hack", "hal": "Halberd", "halbu": "Halbu", "hand": "Hand", "harness": "Harness", "harp": "Harp", + "hawk": "Hawk", "hax": "Hand Axe", "hbl": "Plated Belt", "hbt": "Greaves", "hbw": "Hunter's Bow", "hdm": "Horadric Malus", + "head": "Head", "heals Zealots and Cantors": "Heals Others", "heart": "Heart", + "heartUM": "Heart", "hew": "Hew", "hfh": "Hell Forge Hammer", "hgl": "Gauntlets", @@ -3149,6 +3235,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "homing missiles": "Homing Missiles Drain Mana", "hood": "Hood", "horn": "Horn", + "hornUM": "Horn", "hp1": "Minor Healing Potion", "hp2": "Light Healing Potion", "hp3": "Healing Potion", @@ -3168,10 +3255,12 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "izual": "Izual", "j34": "A Jade Figurine", "jack": "Jack", + "jade": "Jade", "jav": "Javelin", "jaw": "Jawbone", "jew": "Jewel", "key": "Key", + "kill": "Kill", "kit": "Kite Shield", "knell": "Knell", "knot": "Knot", @@ -3197,10 +3286,13 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "lsd": "Long Sword", "lst": "Long Staff", "ltp": "Light Plate", + "lust": "Lust", "luv": "The Black Tower Key", "lwb": "Long War Bow", "lxb": "Light Crossbow", "mac": "Mace", + "maggot": "Maggot", + "maim": "Maim", "mallet": "Mallet", "mangler": "Mangler", "mantle": "Mantle", @@ -3209,6 +3301,8 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "mask": "Mask", "master": "Master", "mau": "Maul", + "maul": "Maul", + "maw": "Maw", "mbl": "Belt", "mbr": "Mephisto's Brain", "mbt": "Chain Boots", @@ -3682,7 +3776,10 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "pk2": "Key of Hate", "pk3": "Key of Destruction", "plt": "Plate Mail", + "poison": "Poison", + "pox": "Pox", "prod": "Prod", + "pus": "Pus", "qbr": "Khalim's Brain", "qey": "Khalim's Eye", "qf1": "Khalim's Flail", @@ -3739,7 +3836,9 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "razor": "Razor", "reaver": "Reaver", "rend": "Rend", + "rendUM": "Rend", "rin": "Ring", + "rip": "Ripper", "rng": "Ring Mail", "rock": "Rock", "rvl": "Full Rejuvenation Potion", @@ -3761,8 +3860,12 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "scz": "Scalp", "sever": "Sever", "shank": "Shank", + "shankUM": "Shank", + "shard": "Shard", "shell": "Shell", "shield": "Shield", + "shieldUM": "Shield", + "shifter": "Shifter", "shocking hit": "shocking hit", "shroud": "Shroud", "skc": "Chipped Skull", @@ -3928,13 +4031,16 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "skillname98": "Might", "skillname99": "Prayer", "skillsname61": "Fire Mastery", + "skinUM": "Skin", "skl": "Flawless Skull", "skp": "Skull Cap", "skr": "Scissors Katar", "sku": "Skull", + "skullUM": "Skull", "skz": "Perfect Skull", "slayer": "Slayer", "slippers": "Slippers", + "sludge": "Sludge", "smasher": "Smasher", "sml": "Small Shield", "sol": "Soul", @@ -3944,6 +4050,7 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "spc": "Spiked Club", "spe": "Spleen", "spell": "Spell", + "spellUM": "Spell", "spike": "Spike", "spiral": "Spiral", "spire": "Spire", @@ -4041,11 +4148,40 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "tch": "Torch", "tes": "Twisted Essence of Suffering", "tgl": "Light Gauntlets", + "the Axe": "the Axe", + "the Cold": "the Cold", + "the Dark": "the Dark", + "the Dead": "the Dead", + "the Destroyer": "the Destroyer", + "the Flayer": "the Flayer", + "the Grim": "the Grim", + "the Hammer": "the Hammer", + "the Howler": "the Howler", + "the Hungry": "the Hungry", + "the Hunter": "the Hunter", + "the Impaler": "the Impaler", + "the Jagged": "the Jagged", + "the Mad": "the Mad", + "the Mauler": "the Mauler", + "the Shade": "the Shade", + "the Sharp": "the Sharp", + "the Slasher": "the Slasher", + "the Slayer": "the Slayer", + "the Tainted": "the Tainted", + "the Unclean": "the Unclean", + "the Unholy": "the Unholy", + "the Witch": "the Witch", + "the Wraith": "the Wraith", + "theQuick": "the Quick", "thirst": "Thirst", + "thirstUM": "Thirst", + "thorn": "Thorn", "tkf": "Throwing Knife", "toa": "Right-click to reset Stat/Skill Points\nToken of Absolution", + "tongue": "Tongue", "torc": "Torc", "touch": "Touch", + "touchUM": "Touch", "tow": "Tower Shield", "tower": "Tower", "tr1": "Horadric Scroll", @@ -4113,6 +4249,8 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "vbl": "Light Belt", "vbt": "Heavy Boots", "veil": "Veil", + "venom": "Venom", + "vex": "Vex", "vgl": "Heavy Gloves", "vip": "Top of the Horadric Staff", "visage": "Visage", @@ -4123,13 +4261,20 @@ export const CANONICAL_ENG_TBL_DICT: Readonly> = { "ward": "Ward", "wax": "War Axe", "weaver": "Weaver", + "weaverUM": "Weaver", + "web": "Web", "whm": "War Hammer", "whorl": "Whorl", + "widow": "Widow", + "wight": "Wight", "winding": "Winding", "wing": "Wing", + "wingUM": "Wing", "wms": "Thawing Potion", "wnd": "Wand", + "wolf": "Wolf", "wood": "Wood", + "wound": "Wound", "wrack": "Wrack", "wrap": "Wrap", "wrb": "Wrist Blade", @@ -4442,6 +4587,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Arm of King Leoric": "李奥瑞克王的骨杖", "Armor": "防具", "Arreat's Face": "亚瑞特的面容", + "AshUM": "灰之", "AssOnly": "(限刺客使用)", "Assailant": "攻擊者", "Assassin": "刺客", @@ -4471,6 +4617,8 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Balrog": "巴羅格", "Bane": "剋星", "Bane Ash": "禍根之灰", + "BaneUM": "禍根之", + "BangUM": "重擊的", "Banished": "受放逐者", "BarOnly": "(限野蠻人使用)", "Baranar's Star": "巴拉那之星", @@ -4490,9 +4638,11 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Berserker's Hauberk": "狂戰士的鎖子甲", "Berserker's Headgear": "狂戰士的頭戴", "Beryl": "海藍之", + "Bile": "乖戾之", "Bing Sz Wang": "冰之王", "Bishibosh": "畢須博須", "Bitter": "受難之", + "Black": "黑色之", "Black Hades": "黑色哈迪斯", "Black Marsh": "黑色荒地", "BlackArcher": "黑色弓箭手", @@ -4508,6 +4658,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Blackleach Blade": "黑水之刃", "Blackoak Shield": "黑橡树盾", "Blacktongue": "黑色之舌", + "Blade": "刀鋒之", "Blade Creeper": "刀刃守卫", "Blade of Ali Baba": "阿里巴巴之刃", "Bladebone": "肩胛骨", @@ -4516,6 +4667,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Blastbark": "爆裂的吠叫", "Blaze Ripper": "烈焰撕裂者", "Blessed": "祝福之", + "Blight": "枯萎之", "Blinkbats Form": "眨眼蝙蝠的外觀", "Blood": "血之", "Blood Crescent": "血紅新月", @@ -4537,6 +4689,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "BloodHook": "血鉤", "BloodHookNest": "血鉤之巢", "BloodLord": "鮮血之王", + "BloodUM": "鮮血之", "BloodWing": "血翼", "BloodWingNest": "血翼之巢", "Bloodfist": "血拳", @@ -4553,6 +4706,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Bone": "白骨之", "BoneArcher": "白骨弓箭手", "BoneMage": "白骨法師", + "BoneUM": "白骨之", "BoneWarrior": "白骨戰士", "Boneash": "骨灰", "Bonebreak": "碎骨者", @@ -4622,6 +4776,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Chance Guards": "運氣守護", "Chaos": "混沌之", "Chaos Sanctum": "混沌庇難所", + "ChaosUM": "混沌之", "Charged Bolt Sentry": "充能弹卫哨", "Charsi": "恰西", "Chipped Amethyst": "碎裂的紫宝石", @@ -4652,6 +4807,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Cloudcrack": "裂云", "Cobalt": "鈷之", "Coif of Glory": "光榮布帽", + "Cold": "冰之", "Cold Plains": "冰冷之原", "Coldcrow": "冰冷鳥鴉", "Coldkill": "冷酷杀手", @@ -4726,6 +4882,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Death's Hand": "死亡之手", "Death's Touch": "死亡之觸", "DeathClan": "死亡一族", + "DeathUM": "死亡之", "Deathbit": "死亡碎片", "Deathcleaver": "死亡之刀", "Deaths's Web": "死亡之网", @@ -4741,6 +4898,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Demonlimb": "恶魔肢体", "Den of Evil": "邪惡洞窟", "DesertWing": "沙漠之翼", + "Devil": "惡魔之", "Devilkin": "小惡魔", "DevilkinShaman": "小惡魔巫師", "Devious": "迂迴之", @@ -4752,6 +4910,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Diamond": "钻石", "Dimoaks Hew": "迪馬克之劈砍", "Dire": "悲慘之", + "DireUM": "悲慘之", "Disfigured": "醜陋怪", "Disused Fane": "廢棄的寺院", "Disused Reliquary": "廢棄的聖物", @@ -4760,14 +4919,17 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Doom": "毀滅之", "DoomApe": "毀滅猴怪", "DoomKnight": "厄運騎士", + "DoomUM": "毀滅之", "Doombringer": "末日毁灭者", "Doomspittle": "毀滅投石者", "Dopplezon": "誘弭", "Dracul's Grasp": "卓古拉之握", + "Dragon": "龍之", "Dragon's": "龍之", "Dragonscale": "龙鳞", "Drake's": "德爾克之", "Dread": "恐懼之", + "DreadUM": "恐懼之", "Drehya": "安雅", "DriedCorpse": "乾屍", "Drognan": "卓格南", @@ -5071,6 +5233,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Fighter": "戰士", "Fighter Mage": "戰士法師", "Fine": "精良之", + "Fire": "火焰之", "Fire Eye": "火之眼", "FireBoar": " ", "FireGolem": "火石魔", @@ -5103,6 +5266,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "FleshHunter": "血肉獵人", "FleshLancer": "血肉長槍手", "FleshSpawner": "血肉復甦者", + "FleshUM": "血肉之", "Fleshrender": "撕肉者", "Fleshripper": "裂肉者", "Fletcher's": "製箭者之", @@ -5113,6 +5277,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Forked": "分叉之", "Fortified": "防禦強化之", "Fortuitous": "偶然之", + "Foul": "腐爛之", "FoulCrow": "惡臭烏鴉", "FoulCrowNest": "邪惡烏鴉之巢", "FrenziedHellSpawn": " ", @@ -5158,6 +5323,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Gleamscythe": "閃耀的鐮刀", "Glimmering": "微光之", "Gloam": "薄暮之魂", + "GloomUM": "黑暗之", "Gloombat": "幽暗蝙蝠", "Gloomstrap": "阴暗陷阱", "Glorious": "光榮之", @@ -5169,6 +5335,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Goldskin": "黃金之皮", "Goldwrap": "金色包裏", "Gorbelly": "血腹獸", + "Gore": "血塊之", "Gore Ripper": "血肉撕裂者", "GoreBearer": " ", "Gorefoot": "血腳", @@ -5177,11 +5344,13 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Grand Vizier of Chaos": "雄偉的混沌大臣", "Gravenspine": "墓穴之脊", "Gravepalm": "墓穴的抚弄", + "Gray": "灰色之", "Great Marsh": "龐大濕地", "GreaterHellSpawn": " ", "GreaterIceSpawn": " ", "Greiz": "格雷茲", "Greyform": "薄暮外觀", + "GriefUM": "悲嘆之", "Griffon's Eye": "格利风之眼", "Grim": "冷酷之", "Grim's Burning Dead": "格里姆的燃烧死者", @@ -5195,12 +5364,15 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Groper": "厄運施術者", "Grotesque": "怪誕魔", "GrotesqueWyrm": "怪異懷姆獸", + "GrowlerUM": "咆哮者的", + "GrumbleUM": "哭訴的", "Guard": "守護", "Guardian": "守護者", "Guardian Angel": "守护天使", "Guardian Naga": "守护那迦", "Guillaume's Face": "吉永之脸", "Gull": "海鷗", + "Gut": "內臟之", "Gutsiphon": "吸魂器", "HP": "生命", "Haemosu's Adament": "解慕漱的坚决", @@ -5215,6 +5387,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Harlequin Crest": "谐角之冠", "Havoc": "浩劫之", "Hawkmail": "鷹甲", + "Haze": "薄霧之", "Headhunter's Glory": "猎头人的荣耀", "Headstriker": "击头者", "Heart Carver": "刻心者", @@ -5370,6 +5543,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Kelpie Snare": "水魔陷阱", "Kerke's Sanctuary": "杰尔克的圣堂", "KhalimFlail": "克林姆的連枷", + "KillerUM": "殺手的", "Kinemils Awl": "金麥爾的錐子", "King's": "國王之", "Kira's Guardian": "奇拉的守护", @@ -5528,6 +5702,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Milabrega's Regalia": "米拉伯佳的雪茄", "Milabrega's Robe": "米拉伯佳之袍", "Milabrega's Rod": "米拉伯佳之杖", + "Mind": "意志之", "Mindrend": "切開骷髏", "MinionIce/fireBoarSpawner": " ", "MinionSlayerSpawner": " ", @@ -5756,9 +5931,11 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "ModitemskonLevel": "當你升級 %d%% 機會施展等級 %d %s", "Moditemskondeath": "當你死去 %d%% 機會施展等級 %d %s", "Modstr2v": "防禦強化", + "Mold": "發黴之", "Monastery Gate": "僧院大門", "Monk's": "僧人之", "Moo Moo Farm": "秘密母牛關卡", + "Moon": "月之", "MoonClan": "月亮一族", "Moonfall": "落月", "Mosers Blessed Circle": "摩西尔的祝福之环", @@ -5788,6 +5965,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Necromancer": "死靈法師", "Necromancer's": "死靈法師之", "Nethercrow": "冥界渡鸦", + "Night": "夜之", "NightClan": "黑夜一族", "NightLord": "黑夜之王", "NightMarauder": "夜間屠殺魔", @@ -5806,6 +5984,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Odium": "憎恨", "Ondal's Almighty": "奥达尔的全能", "Ondal's Wisdom": "奥达尔的智慧", + "Ooze": "滲流之", "Order": "秩序之", "Ormus' Robes": "奥玛斯的长袍", "Orphan's Call": "孤儿的呼唤", @@ -5843,11 +6022,13 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Piercerib": "蘿格之弓", "Pierre Tombale Couant": "皮尔·通巴尔·库앙", "Pindleskin": "暴躁外皮", + "Pit": "地洞之", "Pit Level 1": "地穴第一層", "Pit Level 2": "地穴第二層", "PitLord": "洞穴之王", "PitViper": "洞窟腹蛇", "Pitspawn Fouldog": "洞穴重生的邪惡之犬", + "Plague": "瘟疫之", "Plague Bearer": "瘟疫使者", "PlagueBearer": "瘟疫散佈者", "PlagueBugs": "瘟疫之蟲", @@ -5862,7 +6043,9 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Prismatic": "稜鏡之", "Prison Door": "牢门", "ProwlingDead": " ", + "Puke": "嘔吐之", "Pullspite": "暴風之擊", + "Pulse": "波動之", "Pus Spiter": "毒液喷射者", "Putrid Defiler1": "腐败玷污者", "Putrid Defiler2": "卑劣玷污者", @@ -5887,6 +6070,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "RazorBeast": "剃刀野獸", "RazorPitDemon": "峭壁潛伏者", "RazorSpine": "剃刀山脊怪", + "RazorUM": "剃刀的", "Razoredge": "剃刀之刃", "Razorswitch": "剃刀之枝", "Razortail": "穿刺腰带", @@ -5919,6 +6103,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Rocky Waste": "碎石荒地", "Rogue Encampment": "蘿格營地", "RogueScout": "蘿格斥候", + "Rot": "腐敗之", "RotWalker": " ", "Ruby": "紅寶石之", "Rugged": "凹凸之", @@ -6014,6 +6199,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Runeword98": "和平", "Runeword99": "理性之声", "Russet": "赤楬之", + "Rust": "銹之", "Rusthandle": "生銹的把手", "SaberCat": "劍齒貓", "Saintly": "崇高之", @@ -6038,6 +6224,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Sazabi's Mental Sheath": "沙撒璧的精神之鞘", "Scarab": "聖甲蟲", "Schaefer's Hammer": "史恰佛之锤", + "Seethe": "沸騰之", "Sell": "出售價格: ", "Seraph's Hymn": "炽天使之韵", "Serpent Lord": "海蛇之王", @@ -6050,6 +6237,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Shadow": "陰影之", "Shadow Master": "影子大师", "Shadow Warrior": "影子战士", + "ShadowUM": "影子之", "Shadowdancer": "影舞者", "Shadowfang": "影之牙", "Shadowkiller": "影杀者", @@ -6057,6 +6245,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Shaftstop": "谢夫特斯坦布 (物免甲)", "Sharp": "尖銳之", "Sharp Tooth Sayer": "利齿杀手", + "SharpUM": "銳利之", "Siege Beast": "攻城兽", "Siege Boss": "督军山克", "Sigon's Complete Steel": "西剛的全套刀劍", @@ -6069,6 +6258,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Sigurd's Staunch": "西格德的潜行", "Silkweave": "丝绸之织", "Silver": "銀之", + "Sin": "罪之", "Skeleton": "骷髏", "SkeletonArcher": "骷髏弓箭手", "Skillname222": "乌鸦", @@ -6140,10 +6330,13 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Slayer's": "屠殺者之", "Slayerexp": " ", "Slime Prince": "黏蟲王子", + "SlimeUM": "黏液的", "Slinger": "投石怪", "Snake's": "蛇之", "Snakecord": "蛇索", "Snapchip Shatter": "碎冰冲击者", + "SnarlUM": "吠叫的", + "Snot": "鼻涕之", "SnowYeti1": "雪地流浪者", "SnowYeti2": "雪怪", "SnowYeti3": "寒冰怪", @@ -6157,6 +6350,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Soul Harvest": "靈魂採集者", "SoulKiller": "靈魂殺手", "SoulKillerShaman": "靈魂殺手巫師", + "SoulUM": "靈魂之", "Souldrain": "吸魂者", "Soulfeast Tine": "噬魂叉", "Soulflay": "剝皮靈魂", @@ -6172,18 +6366,22 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "SpikeFiend": "長釘惡魔", "SpikeGiant": "尖釘巨魔", "Spikefist": "釘拳魔", + "SpineUM": "山脊之", "Spineripper": "裂脊者", "Spire of Honor": "荣耀尖塔", "Spirit": "精神之", "Spirit Ward": "灵魂庇护", "Spirit of Barbs": "荆棘之灵", + "SpiritUM": "精神之", "Spiritforge": "灵魂熔炉", "Spiritkeeper": "灵魂看守者", "Spiritual Custodian": "幽暗信徒", "Staff of Kings": "國王之杖", + "Star": "星之", "Stealskull": "偷取骷髅", "Steel": "鋼之", "Steel Carapice": "钢铁甲壳", + "SteelUM": "鐵之", "SteelWeevil": "鋼鐵聖甲蟲", "Steelclash": "作響的金屬", "Steeldriver": "鐵製大鎚", @@ -6193,12 +6391,14 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Steelshade": "钢铁鬼影", "Stone": "石塊之", "Stone Crusher": "碎石者", + "StoneUM": "石之", "Stoneraven": "石乌鸦", "Stony Field": "石塊曠野", "Stony Tomb Level 1": "石製古墓第一層", "Stony Tomb Level 2": "石製古墓第二層", "Storm": "暴風之", "StormCaster": "暴風施術者", + "StormUM": "風暴之", "Stormchaser": "风暴追逐者", "Stormeye": "暴風眼", "Stormguild": "暴風公會", @@ -6665,6 +6865,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Vidala's Fetlock": "維達拉的距毛", "Vidala's Rig": "維達拉的配備", "Vidala's Snare": "維達拉的陷阱", + "Vile": "邪惡之", "VileArcher": "邪惡弓箭手", "VileHunter": "邪惡獵人", "VileLancer": "邪惡長槍手", @@ -6673,6 +6874,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Vine Creature": "食尸藤", "Vinvear Molech": "温韦尔·摩洛克", "Viper": "蝮蛇之", + "ViperUM": "腹蛇之", "Viperfork": "毒蛇叉", "Viridian": "銘綠之", "Visceratuant": "维斯尔图特", @@ -6721,6 +6923,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Widowmaker": "寡妇制造者", "Wihtstan's Guard": "维斯坦的武装", "Wilhelm's Pride": "威廉的自尊", + "Wind": "風之", "Windforce": "风之力", "Windhammer": "风之锤", "Winged Death": "死亡之翼", @@ -6744,6 +6947,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "Wraith": "忿怒", "Wraithflight": "幽灵火焰", "Wraithra": "忿怒", + "Wrath": "忿怒之", "Wyand Voidfinger": "空虛之指-維恩", "Wyrm's": "維特之", "Yeti": "雅提", @@ -6802,24 +7006,29 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "bax": "闊斧", "bbb": "藍.依森之書", "beads": "串珠", + "bender": "鬧飲者的", "bet": "燃烧的恐惧精华", "bey": "巴爾之眼", "bhm": "白骨頭盔", "bite": "咬", + "biteUM": "咬的", "bkd": "石塚之鑰", "bkf": "平衡小刀", "bks": "艾尼弗斯卷軸", "blazer": "發光體", "bld": "短刀", + "blister": "起泡的", "blow": "吹", "bludgeon": "棍棒", "bolt": "彈", "box": "赫拉迪克方塊", "branch": "枝", "brand": "品牌", + "break": "打斷的", "breaker": "破壞者", "brn": "叉", "brow": "額頭", + "browUM": "面容的", "brs": "胸甲", "brz": "一股邪恶力量", "bsd": "闊劍", @@ -6830,8 +7039,10 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "btx": "戰鬥斧", "buc": "圓盾", "buckle": "釦帶", + "burn": "燃燒的", "bwn": "骨杖", "call": "呼減", + "callUM": "呼喊的", "cap": "帽子", "carapace": "甲殼", "casque": "盔", @@ -6853,11 +7064,13 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "circlet": "飾環", "clasp": "扣鉤", "claw": "爪", + "clawUM": "爪的", "clb": "木棒", "cleaver": "切肉刀", "clm": "雙刃大刀", "cloak": "斗蓬", "close": "關閉", + "cloud": "雲的", "clutches": "手爪", "clw": "爪", "cm1": "小护身符", @@ -6873,15 +7086,18 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "cowl": "蒙頭斗蓬", "cqv": "十字弓彈", "crack": "破裂", + "crawler": "爬行者的", "crest": "頂飾", "crn": "皇冠", "crook": "鉤", + "crow": "烏鴉的", "crs": "水晶劍", "crusher": "壓碎者", "cry": "哭喊", "cst": "多節棍", "cudgel": "木棒", "d33": "吉得賓誘餌", + "dancer": "舞者的", "dart": "鏢", "dash": "-", "dgr": "匕首", @@ -6905,20 +7121,29 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "drd": "大地之灵", "dre": "天空之灵", "drf": "梦境之灵", + "drinker": "酒鬼的", + "drool": "流口水的", "dummy": "一股邪惡力量", "ear": "耳", + "eater": "吞吃者的", "eats and spits corpses": "吃下並吐出死屍", "edge": "刃", "elx": "靈葯", "emblem": "象徵", "eye": "眼", + "eyeUM": "眼的", "eyz": "一股邪恶力量", "fang": "尖牙", + "fangUM": "牙的", + "feast": "筵席的", "fed": "溃烂的毁灭精华", + "fester": "潰爛的", "fhl": "高級頭盔", "finger": "手指", "fist": "拳", + "fistUM": "拳的", "fla": "連枷", + "flame": "火焰的", "flange": "凸緣", "flb": "雙手飾劍", "flc": "彎形大刀", @@ -6928,6 +7153,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "flight": "翔", "fng": "一股邪恶力量", "fringe": "邊飾", + "froth": "泡沫的", "ful": "高級戰甲", "g33": "吉得賓", "g34": "黃金鳥", @@ -6971,6 +7197,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "gpy": "完美的黃寶石", "grasp": "抓取", "greave": "油渣", + "grin": "露牙的", "grinder": "研磨者", "grip": "緊握", "gsb": "藍寶石", @@ -6988,18 +7215,22 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "gwn": "殘酷之杖", "gyre": "旋迴", "gzv": "無暇疵的紫寶石", + "hack": "亂切的", "hal": "長戟", "halbu": "海爾布", "hand": "手", "harness": "挽具", "harp": "豎琴", + "hawk": "鷹的", "hax": "手斧", "hbl": "金屬扣帶", "hbt": "護脛", "hbw": "獵弓", "hdm": "赫拉迪克.馬勒斯", + "head": "頭的", "heals Zealots and Cantors": "治癒狂戰士和領唱者", "heart": "心", + "heartUM": "心的", "hew": "砍", "hfh": "地獄熔爐鐵鎚", "hgl": "鐵手套", @@ -7010,6 +7241,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "homing missiles": "追蹤飛箭耗損法力", "hood": "兜帽", "horn": "角", + "hornUM": "角的", "hp1": "輕微治療葯劑", "hp2": "輕型治療葯劑", "hp3": "治療葯劑", @@ -7029,10 +7261,12 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "izual": "衣卒爾", "j34": "玉製小人", "jack": "水兵", + "jade": "玉的", "jav": "標槍", "jaw": "一股邪恶力量", "jew": "珠宝", "key": "鑰匙", + "kill": "殺的", "kit": "輕盾", "knell": "喪鐘", "knot": "結", @@ -7058,10 +7292,13 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "lsd": "長劍", "lst": "長棍", "ltp": "輕型裝甲", + "lust": "渴望的", "luv": "黑塔之鑰", "lwb": "長巨戰弓", "lxb": "輕十字弓", "mac": "釘頭錘", + "maggot": "蛆蟲的", + "maim": "殘廢的", "mallet": "大頭錘", "mangler": "亂砍者", "mantle": "披風", @@ -7070,6 +7307,8 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "mask": "面具", "master": "專家", "mau": "大木棍", + "maul": "大木槌的", + "maw": "魔口的", "mbl": "扣帶", "mbr": "曼菲斯托之腦", "mbt": "鎖鍊靴", @@ -7346,7 +7585,10 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "pk2": "憎恨之鑰", "pk3": "毀滅之鑰", "plt": "鎧甲", + "poison": "毒的", + "pox": "天花的", "prod": "刺", + "pus": "膿的", "qbr": "克林姆的大腦", "qey": "克林姆的眼球", "qf1": "克林姆的連枷", @@ -7403,7 +7645,9 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "razor": "剃刀", "reaver": "強奪者", "rend": "分裂", + "rendUM": "扯破的", "rin": "戒指", + "rip": "開膛手的", "rng": "鎖環甲", "rock": "石塊", "rvl": "全面回復活力葯劑", @@ -7425,8 +7669,12 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "scz": "一股邪恶力量", "sever": "斷裂者", "shank": "小腿", + "shankUM": "脛骨的", + "shard": "碎片的", "shell": "殼", "shield": "盾護", + "shieldUM": "盾的", + "shifter": "變形者的", "shocking hit": "震驚攻擊", "shroud": "壽衣", "skc": "碎裂的骷髏", @@ -7592,13 +7840,16 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "skillname98": "力量", "skillname99": "祈禱", "skillsname61": "支配之焰", + "skinUM": "皮的", "skl": "無暇疵的骷髏", "skp": "骷髏帽", "skr": "剪咬刀", "sku": "骷髏", + "skullUM": "骷髏的", "skz": "完美的骷髏", "slayer": "屠殺者", "slippers": "便鞋", + "sludge": "污泥的", "smasher": "擊碎者", "sml": "小盾牌", "sol": "灵魂之", @@ -7608,6 +7859,7 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "spc": "狼牙棒", "spe": "一股邪恶力量", "spell": "法術", + "spellUM": "法術的", "spike": "釘", "spiral": "螺旋", "spire": "螺旋", @@ -7705,11 +7957,40 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "tch": "火焰", "tes": "扭曲的苦痛精华", "tgl": "輕型鐵手套", + "the Axe": "斧頭者", + "the Cold": "冰冷者", + "the Dark": "黑暗者", + "the Dead": "死亡者", + "the Destroyer": "毀滅者", + "the Flayer": "剝皮者", + "the Grim": "殘酷者", + "the Hammer": "鐵鎚者", + "the Howler": "號哭者", + "the Hungry": "飢渴者", + "the Hunter": "獵人", + "the Impaler": "刺穿者", + "the Jagged": "鋸齒者", + "the Mad": "瘋狂者", + "the Mauler": "傷害手", + "the Shade": "陰影", + "the Sharp": "銳利者", + "the Slasher": "切割者", + "the Slayer": "屠殺者", + "the Tainted": "污染者", + "the Unclean": "不潔者", + "the Unholy": "不神聖者", + "the Witch": "女巫", + "the Wraith": "忿怒者", + "theQuick": "快速者", "thirst": "渴望", + "thirstUM": "饑渴的", + "thorn": "刺的", "tkf": "飛刀", "toa": "右键点击以重设属性/技能点数。\n赦免徽章", + "tongue": "舌的", "torc": "金絲項圈", "touch": "觸碰", + "touchUM": "接觸的", "tow": "塔盾", "tower": "塔", "tr1": "赫拉迪克卷軸", @@ -7777,6 +8058,8 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "vbl": "輕扣帶", "vbt": "重靴", "veil": "面紗", + "venom": "毒液的", + "vex": "苦惱的", "vgl": "重手套", "vip": "赫拉迪克之杖頂端", "visage": "臉", @@ -7787,13 +8070,20 @@ export const CANONICAL_CHI_TBL_DICT: Readonly> = { "ward": "保護", "wax": "巨戰斧", "weaver": "編織者", + "weaverUM": "揮舞者的", + "web": "網的", "whm": "巨戰鐵鎚", "whorl": "螺層", + "widow": "寡婦的", + "wight": "人的", "winding": "捲燒", "wing": "翼", + "wingUM": "翼的", "wms": "融解葯劑", "wnd": "法杖", + "wolf": "狼的", "wood": "木", + "wound": "傷痕的", "wrack": "破滅", "wrap": "包裹", "wrb": "腕刀", diff --git a/src/common/units/monsters-data.ts b/src/common/units/monsters-data.ts index f0bcb41..064b235 100644 --- a/src/common/units/monsters-data.ts +++ b/src/common/units/monsters-data.ts @@ -2135,6 +2135,232 @@ export function getMonsterKindByClassId(classId: number): MonsterKind | undefine return getCanonicalMonsterKinds().get(id) } +const cachedMonsterKindsByHcIdx = new Map() +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 = 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]!, + ] +} + /** * Compute D2 1.13c monster movement speed in sub-tiles per second (`D2Common.dll` * `UNITS_GetBaseVelocity` 0x6FDB1170, `UNITS_GetVelocity` 0x6FDB1B60, and `PathMisc.cpp` diff --git a/src/netproto/d2gs/s2c/monstats2-comp-bits.ts b/src/netproto/d2gs/s2c/monstats2-comp-bits.ts new file mode 100644 index 0000000..5aabbb2 --- /dev/null +++ b/src/netproto/d2gs/s2c/monstats2-comp-bits.ts @@ -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> = { + 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 +} diff --git a/src/netproto/d2gs/s2c/units.ts b/src/netproto/d2gs/s2c/units.ts index ef24e83..ab420f9 100644 --- a/src/netproto/d2gs/s2c/units.ts +++ b/src/netproto/d2gs/s2c/units.ts @@ -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 } : {}), }, } } diff --git a/src/netproto/domain/ids.ts b/src/netproto/domain/ids.ts index 00fb9e5..7a89c0c 100644 --- a/src/netproto/domain/ids.ts +++ b/src/netproto/domain/ids.ts @@ -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 diff --git a/src/netproto/index.ts b/src/netproto/index.ts index cecc195..095c0aa 100644 --- a/src/netproto/index.ts +++ b/src/netproto/index.ts @@ -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' diff --git a/tests/monster-target-bar.test.ts b/tests/monster-target-bar.test.ts index c5f861a..7f5329a 100644 --- a/tests/monster-target-bar.test.ts +++ b/tests/monster-target-bar.test.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('沉淪魔') + }) }) + diff --git a/tests/netproto/d2gs-s2c.test.ts b/tests/netproto/d2gs-s2c.test.ts index c45dea2..5fd1ddb 100644 --- a/tests/netproto/d2gs-s2c.test.ts +++ b/tests/netproto/d2gs-s2c.test.ts @@ -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)', () => {