/** * 1.13c Diablo II Item Drop Pipeline. * * Complete implementation of the real TreasureClassEx drop tree, replacing * synthetic rollDrop and deleting demo item bases and affixes. * * Gold standard entry flow (D2Game!6FC32D60): * - nLevel = monster.level * - tcName = monStats.getTreasureClass(difficulty, monsterType) * - Top-level TC group upgrade: resolveTreasureClassGroup(tcTable, tcName, nLevel) * - Expand TC tree using expandTreasureClass (from rollTreasureClass) * - For each rolled leaf item: * * Gold: calculate scaled gold amount and emit goldItem(amount). * * Base item: * - Lookup base in weapons/armor/misc via dropTables.getBase(code). * - Base upgrade: upgradeItemBase(base, qf4, qf5, rng, dropTables.getBase). * - Init item seed: dwInitSeed = (rng.rand(0xFFFFFFFF) >>> 0), itemRng = new D2Rng(dwInitSeed). * - ItemRatio quality roll using rollItemQuality. * - Quality branches: * * Unique: lookup eligible enabled uniques (lvl <= nLevel). Fallback if empty: Rare + 3x durability. * * Set: lookup eligible set items (lvl <= nLevel). Fallback if empty: Magic + 2x durability. * * Rare: rollRareAffixes. * * Magic: rollMagicAffixes. * * Superior / Normal / Low: standard base. */ import type { MountedArchives } from '../mpq/mount.ts' import { parseTable } from './acts.ts' import type { D2Table } from './acts.ts' import { D2Rng } from './d2-rng.ts' import { Rng } from './rng.ts' import { type TreasureClassTable, type TreasureClassNode, loadTreasureClasses, resolveTreasureClassGroup, } from './treasure-class.ts' import { expandTreasureClass, type DroppedItemEntry, } from './treasure-engine.ts' import { type WeaponTable, loadWeapons, } from './weapons.ts' import { type ArmorTable, loadArmor, } from './armor.ts' import { type MiscTable, loadMisc, } from './misc-items.ts' import { type ItemRatioTable, loadItemRatio, rollItemQuality, type ItemQualityTier, QUALITY_TIER_TO_ITEM_QUALITY, } from './item-ratio.ts' import { type DifficultyLevelTable, loadDifficultyLevels, DEFAULT_DIFFICULTY_LEVEL_ODDS, upgradeItemBase, } from './item-upgrade.ts' import { type UniqueItemsTable, loadUniqueItems, type UniqueItem, } from './unique-items.ts' import { type SetTable, type SetItemTable, loadSetData, type SetItem, } from './set-items.ts' import { type AffixTable, loadMagicAffixes, } from './affixes.ts' import { type RareNameTable, loadRareNames, } from './rare-names.ts' import { type ItemTypeTable, loadItemTypes, isA, } from './item-types.ts' import { generateAutoTreasureClasses, autoTcToTreasureClassNode, } from './auto-tc.ts' import { type Item, type ItemBase, ItemQuality, goldItem, ItemFlag, decodeItemFlags, formatItemCode, type RolledItemProp, } from './items.ts' import { type MonsterKind, readMonsterKinds, getMonsterTreasureClass, type Difficulty, readSuperUniques, type SuperUnique, } from './monsters.ts' import { rollMagicAffixes, rollRareAffixes, getItemTypeCode, NON_RANDOM_RANGE_CODES, type GeneratedAffixes, type GeneratedRareAffixes, } from './affix-generator.ts' import { type AutoMagicTable, loadAutoMagic, CANONICAL_AUTOMAGIC_TABLE, rollAutoMagic, type RolledAutoMagic, type RolledAutoMagicMod, resolveAutoPrefixGroup, ITEM_TYPE_AUTO_PREFIX_MAP, rollStaffMods, resolveStaffModsClass, ITEM_TYPE_STAFF_MODS_MAP, STAFF_MOD_CLASS_SKILL_START, } from './automagic.ts' export { rollAutoMagic, type RolledAutoMagic, type RolledAutoMagicMod, resolveAutoPrefixGroup, ITEM_TYPE_AUTO_PREFIX_MAP, rollStaffMods, resolveStaffModsClass, ITEM_TYPE_STAFF_MODS_MAP, STAFF_MOD_CLASS_SKILL_START, } /** * Complete collection of parsed game data tables required for the drop pipeline. */ export interface DropTables { readonly tcTable: TreasureClassTable readonly autoTcTable: Map readonly weapons: WeaponTable readonly armor: ArmorTable readonly misc: MiscTable readonly itemRatio: ItemRatioTable readonly difficultyLevels: DifficultyLevelTable readonly uniques: UniqueItemsTable readonly sets: { readonly items: SetItemTable; readonly sets: SetTable } readonly magicAffixes: { readonly prefixes: AffixTable; readonly suffixes: AffixTable } readonly rareNames: { readonly prefixes: RareNameTable; readonly suffixes: RareNameTable } readonly autoMagic?: AutoMagicTable | undefined readonly itemTypes: ItemTypeTable readonly monsterKinds: Map readonly superUniques: Map readonly isA: (type: string, target: string) => boolean readonly getBase: (code: string) => ItemBase | undefined } /** * Loads all data tables required by the drop pipeline from mounted MPQ archives. * Fails loudly with an Error if required files or tables are missing. * * @param archives - Mounted MPQ archives. * @returns Promise resolving to DropTables. */ export async function loadDropTables(archives: MountedArchives): Promise { const [ tcTable, weapons, armor, misc, itemRatio, difficultyLevels, uniques, sets, magicAffixes, rareNames, itemTypes, monstatsBytes, ] = await Promise.all([ loadTreasureClasses(archives), loadWeapons(archives), loadArmor(archives), loadMisc(archives), loadItemRatio(archives), loadDifficultyLevels(archives), loadUniqueItems(archives), loadSetData(archives), loadMagicAffixes(archives), loadRareNames(archives), loadItemTypes(archives), archives.read('data\\global\\excel\\monstats.txt'), ]) let autoMagic: AutoMagicTable = CANONICAL_AUTOMAGIC_TABLE try { autoMagic = await loadAutoMagic(archives) } catch { autoMagic = CANONICAL_AUTOMAGIC_TABLE } const rawAutoTcTable = generateAutoTreasureClasses(itemTypes, armor, weapons) const autoTcTable = new Map() for (const [key, node] of rawAutoTcTable) { autoTcTable.set(key, autoTcToTreasureClassNode(node)) } const isAPredicate = (type: string, target: string): boolean => isA(type, target, itemTypes) const getBase = (code: string): ItemBase | undefined => { return weapons.get(code) ?? armor.get(code) ?? misc.get(code) } const monstatsTable: D2Table = parseTable(monstatsBytes) const monsterKinds = readMonsterKinds(monstatsTable) const superUniques = new Map() try { const suBytes = await archives.read('data\\global\\excel\\SuperUniques.txt') const suList = readSuperUniques(parseTable(suBytes)) for (const su of suList) { superUniques.set(su.id, su) } } catch { // If SuperUniques.txt is absent, keep map empty } return { tcTable, autoTcTable, weapons, armor, misc, itemRatio, difficultyLevels, uniques, sets, magicAffixes, rareNames, autoMagic, itemTypes, monsterKinds, superUniques, isA: isAPredicate, getBase, } } /** * Options for executing the TreasureClass drop pipeline. */ export interface DropPipelineOptions { /** Top-level TreasureClass name (e.g. 'Act 1 H2H A', 'Act 1 Champ A'). */ readonly tcName: string /** Monster level (or area level) used for group upgrade and ilvl. */ readonly nLevel: number /** Monster type: 1 = Normal, 2 = Champion, 3 = Unique, 4 = Boss / Quest. */ readonly monsterType?: number | undefined /** Monster drop RNG instance or seed. */ readonly monsterRng?: D2Rng | Rng | number | undefined /** Player Magic Find percentage (e.g. 50 = +50% MF). */ readonly playerMf?: number | undefined /** Game difficulty. */ readonly difficulty?: Difficulty | undefined /** Total players in game (for NoDrop scaling). */ readonly gamePlayers?: number | undefined /** Nearby party members (for NoDrop scaling). */ readonly partyPlayers?: number | undefined /** Whether Expansion items and runes are enabled (default: true). */ readonly isExpansion?: boolean | undefined /** Parsed drop tables. */ readonly dropTables: DropTables } /** * Checks whether an item base can spawn as Rare in Diablo II 1.13c. * * Ground truth rules (ItemTypes.txt & D2Common.dll): * - Charms (cm1, cm2, cm3 / 'char') NEVER spawn as Rare. * - Items with `rare === false` in ItemTypes.txt cannot be Rare. * - Items with `normal === true` in ItemTypes.txt (potions, scrolls, runes, gems, keys) cannot be Rare. * - Weapons, Armor, Rings, Amulets, Jewels CAN spawn as Rare. */ export function canItemBeRare( base: ItemBase, itemTypes?: ItemTypeTable, isAPredicate?: Function, ): boolean { if (base.id === 'cm1' || base.id === 'cm2' || base.id === 'cm3') { return false } const typeCode = getItemTypeCode(base).toLowerCase() if (typeCode === 'char' || typeCode === 'cm1' || typeCode === 'cm2' || typeCode === 'cm3' || typeCode === 'scha' || typeCode === 'mcha' || typeCode === 'lcha') { return false } if (isAPredicate && (isAPredicate(typeCode, 'char') || isAPredicate(typeCode, 'cm1') || isAPredicate(typeCode, 'cm2') || isAPredicate(typeCode, 'cm3'))) { return false } if (itemTypes) { const def = itemTypes.byCode.get(typeCode) if (def) { if (def.charm || !def.rare || def.normal) { return false } } } return true } /** * Resolved ItemTypes.txt quality and class flags for an item base (D2 1.13c Ground Truth). */ export interface ItemTypeFlags { readonly isNormalOnly: boolean readonly isAlwaysMagic: boolean readonly canBeRare: boolean readonly isClassSpecific: boolean } const ALWAYS_MAGIC_BASE_IDS = new Set(['rin', 'amu', 'jew', 'cm1', 'cm2', 'cm3']) const ALWAYS_MAGIC_TYPE_CODES = new Set(['ring', 'amul', 'jewl', 'char', 'scha', 'mcha', 'lcha', 'mboq', 'mxbq']) const CLASS_SPECIFIC_TYPE_CODES = new Set([ 'clas', 'amaz', 'barb', 'necr', 'pala', 'sorc', 'assn', 'drui', 'abow', 'aspe', 'ajav', 'orb', 'head', 'voodoo', 'ashd', 'phlm', 'pelt', 'cloa', 'h2h', 'h2h2', ]) /** * Resolves quality availability and class-specific flags from `ItemTypes.txt` for a given item base. * * Ground truth rules (ItemTypes.txt & D2Game!6FC2FC40): * - `isNormalOnly`: `def.normal === true` (and not ring/amu/jew/cm1..3/equippable weapon/armor). * - `isAlwaysMagic`: `def.magic === true` or `base.id` in `rin`/`amu`/`jew`/`cm1`/`cm2`/`cm3`. * Minimum quality for these items is always Magic (never Superior/Normal/Low). * - `canBeRare`: delegates to `canItemBeRare(base, itemTypes, isAPredicate)`. * - `isClassSpecific`: `def.class !== ''` or any `equiv1`/`equiv2` ancestor has `class !== ''`. */ export function resolveItemTypeFlags( base: ItemBase, itemTypes?: ItemTypeTable, isAPredicate?: (type: string, target: string) => boolean, ): ItemTypeFlags { const baseId = (base.id ?? '').trim().toLowerCase() const typeCode = getItemTypeCode(base).trim().toLowerCase() const def = itemTypes?.byCode.get(typeCode) const isAlwaysMagic = Boolean( def?.magic || def?.charm || ALWAYS_MAGIC_BASE_IDS.has(baseId) || ALWAYS_MAGIC_TYPE_CODES.has(typeCode), ) const isEquippableWeaponOrArmor = (base.kind === 'weapon' || base.kind === 'armor') && typeCode !== 'tpot' && typeCode !== 'bowq' && typeCode !== 'xboq' const isNormalOnly = !isAlwaysMagic && !isEquippableWeaponOrArmor && Boolean(def ? def.normal : true) const canBeRare = canItemBeRare(base, itemTypes, isAPredicate) let isClassSpecific = CLASS_SPECIFIC_TYPE_CODES.has(typeCode) if (!isClassSpecific && itemTypes) { const visited = new Set() const queue: string[] = [typeCode] while (queue.length > 0) { const curr = queue.shift()!.trim().toLowerCase() if (!curr || visited.has(curr)) continue visited.add(curr) if (CLASS_SPECIFIC_TYPE_CODES.has(curr)) { isClassSpecific = true break } const currDef = itemTypes.byCode.get(curr) if (!currDef) continue if (currDef.class && currDef.class.trim() !== '') { isClassSpecific = true break } if (currDef.equiv1) queue.push(currDef.equiv1) if (currDef.equiv2) queue.push(currDef.equiv2) } } if (!isClassSpecific && isAPredicate) { if (isAPredicate(typeCode, 'clas')) { isClassSpecific = true } } return { isNormalOnly, isAlwaysMagic, canBeRare, isClassSpecific, } } /** * Canonical 1.13c Low Quality prefixes (4 subtypes: 0..3). */ export const LOW_QUALITY_PREFIXES = ['Low Quality', 'Cracked', 'Crude', 'Damaged'] as const export interface EtherealAndSocketsRoll { readonly ethereal: boolean readonly sockets: number } /** * Rolls Ethereal status (1/20 chance) and Socket count (1/3 chance for normal/superior/low) * according to Diablo II 1.13c rules (D2Game!6FC30090). */ export function rollItemEtherealAndSockets( base: ItemBase, qualityTier: ItemQualityTier, ilvl: number, itemRng: D2Rng, options?: { readonly uniqueItem?: UniqueItem | undefined readonly itemTypes?: ItemTypeTable | undefined readonly rolledProps?: readonly RolledItemProp[] | undefined }, ): EtherealAndSocketsRoll { let ethereal = false let sockets = 0 const isWeaponOrArmor = base.kind === 'weapon' || base.kind === 'armor' const rawDur = Number((base as any).durability) || 0 const noDur = Boolean((base as any).nodurability) || Boolean((base as any).indestructible) const hasFixedIndestruct = Boolean( options?.uniqueItem?.props.some(p => p.code.trim().toLowerCase() === 'indestruct') || options?.rolledProps?.some(p => p.code.trim().toLowerCase() === 'indestruct'), ) const hasFixedEthereal = Boolean( options?.uniqueItem?.props.some(p => p.code.trim().toLowerCase() === 'ethereal') || options?.rolledProps?.some(p => p.code.trim().toLowerCase() === 'ethereal'), ) if (hasFixedEthereal) { ethereal = true } else if (isWeaponOrArmor && rawDur > 0 && !noDur && qualityTier !== 'set' && !hasFixedIndestruct) { if (itemRng.rand(20) === 0) { ethereal = true } } if ( isWeaponOrArmor && (qualityTier === 'normal' || qualityTier === 'superior' || qualityTier === 'low') ) { const baseMaxSockets = Number((base as any).gemsockets) || 0 if (baseMaxSockets > 0) { const typeCode = getItemTypeCode(base).trim().toLowerCase() const def = options?.itemTypes?.byCode.get(typeCode) const typeMaxSockets = def ? ilvl >= 40 ? def.maxSock40 : ilvl >= 25 ? def.maxSock25 : def.maxSock1 : baseMaxSockets const maxAllowedSockets = Math.min(baseMaxSockets, typeMaxSockets) if (maxAllowedSockets > 0 && itemRng.rand(3) === 0) { sockets = itemRng.randRange(1, maxAllowedSockets) } } } return { ethereal, sockets } } /** * Creates a concrete Item instance for a dropped leaf item. */ export function createDroppedItem( base: ItemBase, qualityTier: ItemQualityTier, options: { readonly uniqueItem?: UniqueItem | undefined readonly setItem?: SetItem | undefined readonly magicAffixes?: GeneratedAffixes | undefined readonly rareAffixes?: GeneratedRareAffixes | undefined readonly autoMagicAffix?: RolledAutoMagic | null | undefined readonly staffMods?: readonly RolledItemProp[] | undefined readonly rolledProps?: readonly RolledItemProp[] | undefined readonly ethereal?: boolean | undefined readonly sockets?: number | undefined readonly itemTypes?: ItemTypeTable | undefined readonly ilvl: number readonly dwInitSeed: number readonly durabilityMultiplier?: number | undefined readonly itemRng?: D2Rng | undefined }, ): Item { const quality = QUALITY_TIER_TO_ITEM_QUALITY[qualityTier] ?? ItemQuality.NORMAL let rawFlags = ItemFlag.IDENTIFIED let name = base.name let prefix = null let suffix = null let qualitySubtype: number | undefined = undefined const stats: Record = {} const rolledProps: RolledItemProp[] = [] if (base.damage > 0) stats.damage = base.damage if (base.defense > 0) stats.defense = base.defense const propRng = options.itemRng ?? new D2Rng(options.dwInitSeed) const resolveItemPropValue = (prop: { code: string; par?: string | number; min: number; max: number }): number => { const codeLower = prop.code.toLowerCase() if (NON_RANDOM_RANGE_CODES.has(codeLower)) { return prop.min } if (prop.min === prop.max) { return prop.min } return propRng.randRange(prop.min, prop.max) } if (options.rolledProps && options.rolledProps.length > 0) { for (const p of options.rolledProps) { rolledProps.push(p) stats[p.code] = (stats[p.code] ?? 0) + p.value } } if (qualityTier === 'unique' && options.uniqueItem) { name = options.uniqueItem.index if (!options.rolledProps || options.rolledProps.length === 0) { for (const prop of options.uniqueItem.props) { if (prop.code) { const val = resolveItemPropValue(prop) rolledProps.push({ code: prop.code, param: prop.par, min: prop.min, max: prop.max, value: val, }) stats[prop.code] = (stats[prop.code] ?? 0) + val } } } } else if (qualityTier === 'set' && options.setItem) { name = options.setItem.index if (!options.rolledProps || options.rolledProps.length === 0) { for (const prop of options.setItem.props) { if (prop.code) { const val = resolveItemPropValue(prop) rolledProps.push({ code: prop.code, param: prop.par, min: prop.min, max: prop.max, value: val, }) stats[prop.code] = (stats[prop.code] ?? 0) + val } } } } else if (qualityTier === 'rare' && options.rareAffixes) { name = options.rareAffixes.name for (const affix of options.rareAffixes.affixes) { for (const mod of affix.mods) { rolledProps.push({ code: mod.code, param: mod.param, min: mod.min, max: mod.max, value: mod.value, }) stats[mod.code] = (stats[mod.code] ?? 0) + mod.value } } } else if (qualityTier === 'magic' && options.magicAffixes) { name = options.magicAffixes.name if (options.magicAffixes.prefix) { const p = options.magicAffixes.prefix prefix = { id: p.affix.name, name: p.name, kind: 'prefix' as const, level: p.level, itemTypes: p.affix.itypes, modifiers: p.mods.map(m => ({ stat: m.code, min: m.min, max: m.max })), } for (const mod of p.mods) { rolledProps.push({ code: mod.code, param: mod.param, min: mod.min, max: mod.max, value: mod.value, }) stats[mod.code] = (stats[mod.code] ?? 0) + mod.value } } if (options.magicAffixes.suffix) { const s = options.magicAffixes.suffix suffix = { id: s.affix.name, name: s.name, kind: 'suffix' as const, level: s.level, itemTypes: s.affix.itypes, modifiers: s.mods.map(m => ({ stat: m.code, min: m.min, max: m.max })), } for (const mod of s.mods) { rolledProps.push({ code: mod.code, param: mod.param, min: mod.min, max: mod.max, value: mod.value, }) stats[mod.code] = (stats[mod.code] ?? 0) + mod.value } } } else if (qualityTier === 'superior') { name = `Superior ${base.name}` if (!options.rolledProps || options.rolledProps.length === 0) { if (base.kind === 'armor') { const acPct = propRng.randRange(5, 15) rolledProps.push({ code: 'ac%', min: 5, max: 15, value: acPct, }) stats['ac%'] = (stats['ac%'] ?? 0) + acPct if (propRng.rand(2) === 0) { const durPct = propRng.randRange(10, 15) rolledProps.push({ code: 'dur%', min: 10, max: 15, value: durPct, }) stats['dur%'] = (stats['dur%'] ?? 0) + durPct } } else if (base.kind === 'weapon') { const dmgPct = propRng.randRange(5, 15) rolledProps.push({ code: 'dmg%', min: 5, max: 15, value: dmgPct, }) stats['dmg%'] = (stats['dmg%'] ?? 0) + dmgPct if (propRng.rand(2) === 0) { const attVal = propRng.randRange(1, 3) rolledProps.push({ code: 'att', min: 1, max: 3, value: attVal, }) stats['att'] = (stats['att'] ?? 0) + attVal } } } } else if (qualityTier === 'low') { qualitySubtype = propRng.rand(4) const lowPrefix = LOW_QUALITY_PREFIXES[qualitySubtype] ?? 'Cracked' name = `${lowPrefix} ${base.name}` } let effectiveAutoMagic = options.autoMagicAffix if (effectiveAutoMagic === undefined && qualityTier !== 'unique' && qualityTier !== 'set') { const autoPrefixGroup = resolveAutoPrefixGroup(base) if (autoPrefixGroup !== undefined && autoPrefixGroup > 0) { effectiveAutoMagic = rollAutoMagic( base, options.ilvl, CANONICAL_AUTOMAGIC_TABLE, (a, b) => a === b, propRng, { group: autoPrefixGroup, isRare: qualityTier === 'rare' }, ) } } if (qualityTier !== 'unique' && qualityTier !== 'set') { if (effectiveAutoMagic) { for (const mod of effectiveAutoMagic.mods) { rolledProps.push({ code: mod.code, param: mod.param, min: mod.min, max: mod.max, value: mod.value, }) stats[mod.code] = (stats[mod.code] ?? 0) + mod.value } } if (options.staffMods && options.staffMods.length > 0) { for (const sm of options.staffMods) { rolledProps.push({ code: sm.code, param: sm.param, min: sm.min, max: sm.max, value: sm.value, }) stats[sm.code] = (stats[sm.code] ?? 0) + sm.value } } } const isEthereal = Boolean( options.ethereal || rolledProps.some(p => p.code.trim().toLowerCase() === 'ethereal'), ) const socketCount = options.sockets !== undefined && options.sockets > 0 ? options.sockets : 0 if (isEthereal) { rawFlags |= ItemFlag.ETHEREAL stats.ethereal = 1 } if (socketCount > 0) { rawFlags |= ItemFlag.SOCKETED } const rawDurability = (base as any).durability ?? 0 const mult = options.durabilityMultiplier ?? 1 let durability: number | undefined = undefined let maxDurability: number | undefined = undefined if (typeof rawDurability === 'number' && rawDurability > 0) { if (qualityTier === 'low') { maxDurability = Math.max(1, Math.floor(rawDurability / 3)) } else { maxDurability = Math.round(rawDurability * mult) } durability = maxDurability } return { base, prefix, suffix, level: options.ilvl, ilvl: options.ilvl, name, stats, invWidth: base.invWidth, invHeight: base.invHeight, stack: 1, value: base.value, rawFlags, flags: decodeItemFlags(rawFlags), version: 0x65, code: formatItemCode(base.id), quality, rarity: qualityTier, uniqueId: options.dwInitSeed, durability, maxDurability, ...(qualitySubtype !== undefined ? { qualitySubtype } : {}), ...(isEthereal ? { ethereal: true } : {}), ...(socketCount > 0 ? { sockets: socketCount, totalSockets: socketCount } : {}), ...(options.uniqueItem ? { uniqueId: options.dwInitSeed, uniqueItemDef: options.uniqueItem } : {}), ...(options.setItem ? { setId: options.setItem.id, setItemDef: options.setItem } : {}), ...(options.rareAffixes ? { rolledRareAffixes: options.rareAffixes } : {}), ...(options.magicAffixes ? { rolledMagicAffixes: options.magicAffixes } : {}), ...(effectiveAutoMagic ? { rolledAutoMagic: effectiveAutoMagic } : {}), ...(options.staffMods && options.staffMods.length > 0 ? { staffMods: options.staffMods } : {}), ...(rolledProps.length > 0 ? { rolledProps } : {}), } as Item } /** * Executes the full TreasureClass drop pipeline for a slain monster or container. * * @param options - Drop pipeline parameters. * @returns Array of dropped Item instances (including gold items). */ export function executeDropPipeline( tablesOrOptions: DropTables | DropPipelineOptions, maybeOptions?: Omit, ): Item[] { const options: DropPipelineOptions = 'tcTable' in tablesOrOptions ? { ...maybeOptions!, dropTables: tablesOrOptions } : tablesOrOptions const { tcName, nLevel, monsterType = 1, dropTables, difficulty = 'normal', playerMf = 0, gamePlayers = 1, partyPlayers = 1, isExpansion = true, } = options if (!tcName || tcName.trim() === '') { return [] } // 1. Top-level TC group upgrade (D2Game!6FC32D60) const resolvedNode = resolveTreasureClassGroup(dropTables.tcTable, tcName, nLevel) const effectiveTcName = resolvedNode?.name ?? tcName // 2. Setup RNG let rng: D2Rng if (options.monsterRng instanceof D2Rng) { rng = options.monsterRng } else if (options.monsterRng instanceof Rng) { rng = new D2Rng(options.monsterRng.int(0, 0x7FFFFFFF)) } else if (typeof options.monsterRng === 'number') { rng = new D2Rng(options.monsterRng >>> 0) } else { rng = new D2Rng((Math.random() * 0xFFFFFFFF) >>> 0, (Math.random() * 0xFFFFFFFF) >>> 0) } // 3. Expand TC tree const dropLeaves: DroppedItemEntry[] = expandTreasureClass(effectiveTcName, { rng, tcTable: dropTables.tcTable, autoTcTable: dropTables.autoTcTable, gamePlayers, partyPlayers, isExpansion, }) if ( dropLeaves.length === 0 && !dropTables.tcTable.get(effectiveTcName) && !dropTables.autoTcTable.get(effectiveTcName) && dropTables.getBase(effectiveTcName) ) { dropLeaves.push({ code: effectiveTcName, item: effectiveTcName, isGold: false, qualityFactors: [0, 0, 0, 0], flags: 0, forcedUnique: false, forcedSet: false, }) } // 4. Resolve difficulty odds for base upgrades const diffKey = difficulty === 'hell' ? 'Hell' : difficulty === 'nightmare' ? 'Nightmare' : 'Normal' const odds = dropTables.difficultyLevels.get(diffKey) ?? DEFAULT_DIFFICULTY_LEVEL_ODDS[diffKey]! const isGood = monsterType > 1 const qf4 = isGood ? odds.uberCodeOddsGood : odds.uberCodeOddsNormal const qf5 = isGood ? odds.ultraCodeOddsGood : odds.ultraCodeOddsNormal const drops: Item[] = [] // 5. Process each dropped leaf for (const leaf of dropLeaves) { if (leaf.isGold) { const multiplier = leaf.goldMultiplier ?? 1 const baseGold = rng.randRange(Math.max(1, Math.trunc(nLevel / 2)), Math.max(5, nLevel * 5)) const goldAmount = Math.max(1, baseGold * multiplier) drops.push(goldItem(goldAmount)) continue } const base = dropTables.getBase(leaf.code) if (!base) { continue } // Base upgrade const upgradeResult = upgradeItemBase(base, qf4, qf5, rng, dropTables.getBase) const effectiveBase = upgradeResult.base const isUber = upgradeResult.isUber // Initialize item PRNG const dwInitSeed = (rng.rand(0xFFFFFFFF) >>> 0) const itemRng = new D2Rng(dwInitSeed) // Resolve ItemTypes.txt quality & class-specific flags const typeFlags = resolveItemTypeFlags(effectiveBase, dropTables.itemTypes, dropTables.isA) // Check if the item can roll magical qualities const canHaveQuality = !typeFlags.isNormalOnly && (effectiveBase.kind === 'weapon' || effectiveBase.kind === 'armor' || typeFlags.isAlwaysMagic) // Roll quality let quality: ItemQualityTier if (!canHaveQuality) { quality = 'normal' } else if (leaf.forcedUnique) { quality = 'unique' } else if (leaf.forcedSet) { quality = 'set' } else { quality = rollItemQuality({ ilvl: nLevel, qlvl: effectiveBase.level ?? 1, magicFind: playerMf, tcFactors: { unique: leaf.qualityFactors[0], set: leaf.qualityFactors[1], rare: leaf.qualityFactors[2], magic: leaf.qualityFactors[3], }, table: dropTables.itemRatio, rng: itemRng, uber: isUber ? 1 : 0, classSpecific: typeFlags.isClassSpecific ? 1 : 0, allowRare: typeFlags.canBeRare, allowSuperior: !typeFlags.isAlwaysMagic, }) if ( typeFlags.isAlwaysMagic && (quality === 'normal' || quality === 'low' || quality === 'superior') ) { quality = 'magic' } } let durabilityMultiplier = 1 // Quality branches & fallbacks (D2 1.13c Ground Truth) if (quality === 'unique') { const uniqueCandidates = dropTables.uniques.getEnabledByCode(effectiveBase.id).filter(u => u.lvl <= nLevel) if (uniqueCandidates.length > 0) { let chosenUnique = uniqueCandidates[0]! if (uniqueCandidates.length > 1) { const totalRarity = uniqueCandidates.reduce((sum, u) => sum + Math.max(1, u.rarity), 0) let roll = itemRng.rand(totalRarity) for (const u of uniqueCandidates) { const weight = Math.max(1, u.rarity) if (roll < weight) { chosenUnique = u break } roll -= weight } } const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'unique', nLevel, itemRng, { uniqueItem: chosenUnique, itemTypes: dropTables.itemTypes, }) drops.push(createDroppedItem(effectiveBase, 'unique', { uniqueItem: chosenUnique, ethereal: ethAndSock.ethereal, sockets: ethAndSock.sockets, itemTypes: dropTables.itemTypes, ilvl: nLevel, dwInitSeed, itemRng, })) continue } // Unique check failed (no eligible unique exists or ilvl < unique lvl) // D2 1.13c Fallback: // If the item can be Rare: Degrade to Rare + 3x durability (Triple Durability) // If the item CANNOT be Rare (e.g. Charms): Degrade to Magic + 2x durability (Double Durability) if (canItemBeRare(effectiveBase, dropTables.itemTypes, dropTables.isA)) { quality = 'rare' durabilityMultiplier = 3 } else { quality = 'magic' durabilityMultiplier = 2 } } if (quality === 'set') { const setCandidates = dropTables.sets.items.entries.filter( s => s.item.toLowerCase() === effectiveBase.id.toLowerCase() && s.lvl <= nLevel, ) if (setCandidates.length > 0) { let chosenSet = setCandidates[0]! if (setCandidates.length > 1) { const totalRarity = setCandidates.reduce((sum, s) => sum + Math.max(1, s.rarity), 0) let roll = itemRng.rand(totalRarity) for (const s of setCandidates) { const weight = Math.max(1, s.rarity) if (roll < weight) { chosenSet = s break } roll -= weight } } // 1.13c Ground Truth: Set items NEVER spawn Ethereal drops.push(createDroppedItem(effectiveBase, 'set', { setItem: chosenSet, ethereal: false, itemTypes: dropTables.itemTypes, ilvl: nLevel, dwInitSeed, itemRng, })) continue } // Set check failed (no eligible set exists or ilvl < set lvl, e.g. Charms) // D2 1.13c Fallback: Degrade to Magic + 2x durability (Double Durability) quality = 'magic' durabilityMultiplier = 2 } if (quality === 'rare') { // Check if item can actually be rare (e.g. charms can NEVER be rare) if (!canItemBeRare(effectiveBase, dropTables.itemTypes, dropTables.isA)) { quality = 'magic' durabilityMultiplier = Math.max(durabilityMultiplier, 2) } else { const rareAffixes = rollRareAffixes( effectiveBase, nLevel, dropTables.magicAffixes, dropTables.rareNames, dropTables.isA, itemRng, ) if (rareAffixes && rareAffixes.affixes && rareAffixes.affixes.length > 0) { const autoPrefixGroup = resolveAutoPrefixGroup(effectiveBase, dropTables.itemTypes, dropTables.isA) const autoMagicAffix = autoPrefixGroup !== undefined ? rollAutoMagic( effectiveBase, nLevel, dropTables.autoMagic ?? CANONICAL_AUTOMAGIC_TABLE, dropTables.isA, itemRng, { group: autoPrefixGroup, isRare: true }, ) : null const staffMods = resolveStaffModsClass(effectiveBase, dropTables.itemTypes) !== undefined ? rollStaffMods(effectiveBase, nLevel, dropTables.itemTypes, itemRng) : undefined const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'rare', nLevel, itemRng, { itemTypes: dropTables.itemTypes, }) drops.push(createDroppedItem(effectiveBase, 'rare', { rareAffixes, autoMagicAffix, staffMods, ethereal: ethAndSock.ethereal, sockets: ethAndSock.sockets, itemTypes: dropTables.itemTypes, ilvl: nLevel, dwInitSeed, durabilityMultiplier, itemRng, })) continue } // Rare affix roll failed: Degrade to Magic + 2x durability quality = 'magic' durabilityMultiplier = Math.max(durabilityMultiplier, 2) } } if (quality === 'magic') { const magicAffixes = rollMagicAffixes( effectiveBase, nLevel, dropTables.magicAffixes, dropTables.isA, itemRng, ) const autoPrefixGroup = resolveAutoPrefixGroup(effectiveBase, dropTables.itemTypes, dropTables.isA) const autoMagicAffix = autoPrefixGroup !== undefined ? rollAutoMagic( effectiveBase, nLevel, dropTables.autoMagic ?? CANONICAL_AUTOMAGIC_TABLE, dropTables.isA, itemRng, { group: autoPrefixGroup, isRare: false }, ) : null const staffMods = resolveStaffModsClass(effectiveBase, dropTables.itemTypes) !== undefined ? rollStaffMods(effectiveBase, nLevel, dropTables.itemTypes, itemRng) : undefined const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'magic', nLevel, itemRng, { itemTypes: dropTables.itemTypes, }) drops.push(createDroppedItem(effectiveBase, 'magic', { magicAffixes, autoMagicAffix, staffMods, ethereal: ethAndSock.ethereal, sockets: ethAndSock.sockets, itemTypes: dropTables.itemTypes, ilvl: nLevel, dwInitSeed, durabilityMultiplier, itemRng, })) continue } const autoPrefixGroup = resolveAutoPrefixGroup(effectiveBase, dropTables.itemTypes, dropTables.isA) const autoMagicAffix = autoPrefixGroup !== undefined ? rollAutoMagic( effectiveBase, nLevel, dropTables.autoMagic ?? CANONICAL_AUTOMAGIC_TABLE, dropTables.isA, itemRng, { group: autoPrefixGroup, isRare: false }, ) : null const staffMods = resolveStaffModsClass(effectiveBase, dropTables.itemTypes) !== undefined ? rollStaffMods(effectiveBase, nLevel, dropTables.itemTypes, itemRng) : undefined if (quality === 'superior') { const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'superior', nLevel, itemRng, { itemTypes: dropTables.itemTypes, }) drops.push(createDroppedItem(effectiveBase, 'superior', { autoMagicAffix, staffMods, ethereal: ethAndSock.ethereal, sockets: ethAndSock.sockets, itemTypes: dropTables.itemTypes, ilvl: nLevel, dwInitSeed, itemRng, })) continue } if (quality === 'low') { const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'low', nLevel, itemRng, { itemTypes: dropTables.itemTypes, }) drops.push(createDroppedItem(effectiveBase, 'low', { autoMagicAffix, staffMods, ethereal: ethAndSock.ethereal, sockets: ethAndSock.sockets, itemTypes: dropTables.itemTypes, ilvl: nLevel, dwInitSeed, itemRng, })) continue } // Default: normal quality const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'normal', nLevel, itemRng, { itemTypes: dropTables.itemTypes, }) drops.push(createDroppedItem(effectiveBase, 'normal', { autoMagicAffix, staffMods, ethereal: ethAndSock.ethereal, sockets: ethAndSock.sockets, itemTypes: dropTables.itemTypes, ilvl: nLevel, dwInitSeed, itemRng, })) } return drops }