1199 lines
37 KiB
TypeScript
1199 lines
37 KiB
TypeScript
/**
|
|
* 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<string, TreasureClassNode>
|
|
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<string, MonsterKind>
|
|
readonly superUniques: Map<string, SuperUnique>
|
|
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<DropTables> {
|
|
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<string, TreasureClassNode>()
|
|
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<string, SuperUnique>()
|
|
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<string>()
|
|
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<string, number> = {}
|
|
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<DropPipelineOptions, 'dropTables'>,
|
|
): 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
|
|
}
|