feat(tooltip/drop): 接线properties.ts实现显示层数据驱动化并完成Phase4 1.13c细节对齐 (fixes #133)
This commit is contained in:
parent
0cd799cdcb
commit
fc6388779e
|
|
@ -457,6 +457,20 @@ export function rollRareAffixes(
|
|||
if (eligibleRareSuffixes.length > 0) {
|
||||
const idx = rng.rand(eligibleRareSuffixes.length)
|
||||
rareSuffix = eligibleRareSuffixes[idx]
|
||||
if (
|
||||
eligibleRareSuffixes.length > 1 &&
|
||||
rarePrefix &&
|
||||
rareSuffix &&
|
||||
rareSuffix.name.trim().toLowerCase() === rarePrefix.name.trim().toLowerCase()
|
||||
) {
|
||||
for (let offset = 1; offset < eligibleRareSuffixes.length; offset++) {
|
||||
const candidate = eligibleRareSuffixes[(idx + offset) % eligibleRareSuffixes.length]
|
||||
if (candidate && candidate.name.trim().toLowerCase() !== rarePrefix.name.trim().toLowerCase()) {
|
||||
rareSuffix = candidate
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rareNameParts = [rarePrefix?.name, rareSuffix?.name].filter(
|
||||
|
|
|
|||
|
|
@ -133,6 +133,12 @@ import {
|
|||
ITEM_TYPE_STAFF_MODS_MAP,
|
||||
STAFF_MOD_CLASS_SKILL_START,
|
||||
} from './automagic.ts'
|
||||
import {
|
||||
loadItemPropertiesAndStats,
|
||||
type PropertiesTable,
|
||||
type ItemStatCostTable,
|
||||
} from './properties.ts'
|
||||
import { setRuntimePropertyTables } from './item-tooltip.ts'
|
||||
|
||||
export {
|
||||
rollAutoMagic,
|
||||
|
|
@ -162,6 +168,8 @@ export interface DropTables {
|
|||
readonly magicAffixes: { readonly prefixes: AffixTable; readonly suffixes: AffixTable }
|
||||
readonly rareNames: { readonly prefixes: RareNameTable; readonly suffixes: RareNameTable }
|
||||
readonly autoMagic?: AutoMagicTable | undefined
|
||||
readonly properties?: PropertiesTable | undefined
|
||||
readonly itemStatCost?: ItemStatCostTable | undefined
|
||||
readonly itemTypes: ItemTypeTable
|
||||
readonly monsterKinds: Map<string, MonsterKind>
|
||||
readonly superUniques: Map<string, SuperUnique>
|
||||
|
|
@ -190,6 +198,7 @@ export async function loadDropTables(archives: MountedArchives): Promise<DropTab
|
|||
rareNames,
|
||||
itemTypes,
|
||||
monstatsBytes,
|
||||
propAndStats,
|
||||
] = await Promise.all([
|
||||
loadTreasureClasses(archives),
|
||||
loadWeapons(archives),
|
||||
|
|
@ -203,8 +212,12 @@ export async function loadDropTables(archives: MountedArchives): Promise<DropTab
|
|||
loadRareNames(archives),
|
||||
loadItemTypes(archives),
|
||||
archives.read('data\\global\\excel\\monstats.txt'),
|
||||
loadItemPropertiesAndStats(archives),
|
||||
])
|
||||
|
||||
const { properties, itemStatCost } = propAndStats
|
||||
setRuntimePropertyTables(properties, itemStatCost)
|
||||
|
||||
let autoMagic: AutoMagicTable = CANONICAL_AUTOMAGIC_TABLE
|
||||
try {
|
||||
autoMagic = await loadAutoMagic(archives)
|
||||
|
|
@ -251,6 +264,8 @@ export async function loadDropTables(archives: MountedArchives): Promise<DropTab
|
|||
magicAffixes,
|
||||
rareNames,
|
||||
autoMagic,
|
||||
properties,
|
||||
itemStatCost,
|
||||
itemTypes,
|
||||
monsterKinds,
|
||||
superUniques,
|
||||
|
|
@ -848,8 +863,15 @@ export function executeDropPipeline(
|
|||
return []
|
||||
}
|
||||
|
||||
if (dropTables.properties && dropTables.itemStatCost) {
|
||||
setRuntimePropertyTables(dropTables.properties, dropTables.itemStatCost)
|
||||
}
|
||||
|
||||
// A8: Clamp effective ilvl to [1, 99] per 1.13c D2Game!6FC32D60
|
||||
const effectiveIlvl = Math.max(1, Math.min(99, Math.trunc(nLevel)))
|
||||
|
||||
// 1. Top-level TC group upgrade (D2Game!6FC32D60)
|
||||
const resolvedNode = resolveTreasureClassGroup(dropTables.tcTable, tcName, nLevel)
|
||||
const resolvedNode = resolveTreasureClassGroup(dropTables.tcTable, tcName, effectiveIlvl)
|
||||
const effectiveTcName = resolvedNode?.name ?? tcName
|
||||
|
||||
// 2. Setup RNG
|
||||
|
|
@ -902,8 +924,10 @@ export function executeDropPipeline(
|
|||
// 5. Process each dropped leaf
|
||||
for (const leaf of dropLeaves) {
|
||||
if (leaf.isGold) {
|
||||
// A6: 1.13c D2Game!6FC319D0 gold drop range [floor(clampedLvl / 2), clampedLvl * 6] * multiplier
|
||||
const multiplier = leaf.goldMultiplier ?? 1
|
||||
const baseGold = rng.randRange(Math.max(1, Math.trunc(nLevel / 2)), Math.max(5, nLevel * 5))
|
||||
const clampedLvl = effectiveIlvl
|
||||
const baseGold = rng.randRange(Math.max(1, Math.trunc(clampedLvl / 2)), Math.max(6, clampedLvl * 6))
|
||||
const goldAmount = Math.max(1, baseGold * multiplier)
|
||||
drops.push(goldItem(goldAmount))
|
||||
continue
|
||||
|
|
@ -944,8 +968,8 @@ export function executeDropPipeline(
|
|||
quality = 'set'
|
||||
} else {
|
||||
quality = rollItemQuality({
|
||||
ilvl: nLevel,
|
||||
qlvl: effectiveBase.level ?? 1,
|
||||
ilvl: effectiveIlvl,
|
||||
qlvl: Math.min(effectiveIlvl, effectiveBase.level ?? 1),
|
||||
magicFind: playerMf,
|
||||
tcFactors: {
|
||||
unique: leaf.qualityFactors[0],
|
||||
|
|
@ -972,7 +996,7 @@ export function executeDropPipeline(
|
|||
|
||||
// Quality branches & fallbacks (D2 1.13c Ground Truth)
|
||||
if (quality === 'unique') {
|
||||
const uniqueCandidates = dropTables.uniques.getEnabledByCode(effectiveBase.id).filter(u => u.lvl <= nLevel)
|
||||
const uniqueCandidates = dropTables.uniques.getEnabledByCode(effectiveBase.id).filter(u => u.lvl <= effectiveIlvl)
|
||||
if (uniqueCandidates.length > 0) {
|
||||
let chosenUnique = uniqueCandidates[0]!
|
||||
if (uniqueCandidates.length > 1) {
|
||||
|
|
@ -987,7 +1011,7 @@ export function executeDropPipeline(
|
|||
roll -= weight
|
||||
}
|
||||
}
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'unique', nLevel, itemRng, {
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'unique', effectiveIlvl, itemRng, {
|
||||
uniqueItem: chosenUnique,
|
||||
itemTypes: dropTables.itemTypes,
|
||||
})
|
||||
|
|
@ -996,7 +1020,7 @@ export function executeDropPipeline(
|
|||
ethereal: ethAndSock.ethereal,
|
||||
sockets: ethAndSock.sockets,
|
||||
itemTypes: dropTables.itemTypes,
|
||||
ilvl: nLevel,
|
||||
ilvl: effectiveIlvl,
|
||||
dwInitSeed,
|
||||
itemRng,
|
||||
}))
|
||||
|
|
@ -1017,7 +1041,7 @@ export function executeDropPipeline(
|
|||
|
||||
if (quality === 'set') {
|
||||
const setCandidates = dropTables.sets.items.entries.filter(
|
||||
s => s.item.toLowerCase() === effectiveBase.id.toLowerCase() && s.lvl <= nLevel,
|
||||
s => s.item.toLowerCase() === effectiveBase.id.toLowerCase() && s.lvl <= effectiveIlvl,
|
||||
)
|
||||
if (setCandidates.length > 0) {
|
||||
let chosenSet = setCandidates[0]!
|
||||
|
|
@ -1038,7 +1062,7 @@ export function executeDropPipeline(
|
|||
setItem: chosenSet,
|
||||
ethereal: false,
|
||||
itemTypes: dropTables.itemTypes,
|
||||
ilvl: nLevel,
|
||||
ilvl: effectiveIlvl,
|
||||
dwInitSeed,
|
||||
itemRng,
|
||||
}))
|
||||
|
|
@ -1058,7 +1082,7 @@ export function executeDropPipeline(
|
|||
} else {
|
||||
const rareAffixes = rollRareAffixes(
|
||||
effectiveBase,
|
||||
nLevel,
|
||||
effectiveIlvl,
|
||||
dropTables.magicAffixes,
|
||||
dropTables.rareNames,
|
||||
dropTables.isA,
|
||||
|
|
@ -1071,7 +1095,7 @@ export function executeDropPipeline(
|
|||
autoPrefixGroup !== undefined
|
||||
? rollAutoMagic(
|
||||
effectiveBase,
|
||||
nLevel,
|
||||
effectiveIlvl,
|
||||
dropTables.autoMagic ?? CANONICAL_AUTOMAGIC_TABLE,
|
||||
dropTables.isA,
|
||||
itemRng,
|
||||
|
|
@ -1080,9 +1104,9 @@ export function executeDropPipeline(
|
|||
: null
|
||||
const staffMods =
|
||||
resolveStaffModsClass(effectiveBase, dropTables.itemTypes) !== undefined
|
||||
? rollStaffMods(effectiveBase, nLevel, dropTables.itemTypes, itemRng)
|
||||
? rollStaffMods(effectiveBase, effectiveIlvl, dropTables.itemTypes, itemRng)
|
||||
: undefined
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'rare', nLevel, itemRng, {
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'rare', effectiveIlvl, itemRng, {
|
||||
itemTypes: dropTables.itemTypes,
|
||||
})
|
||||
drops.push(createDroppedItem(effectiveBase, 'rare', {
|
||||
|
|
@ -1092,7 +1116,7 @@ export function executeDropPipeline(
|
|||
ethereal: ethAndSock.ethereal,
|
||||
sockets: ethAndSock.sockets,
|
||||
itemTypes: dropTables.itemTypes,
|
||||
ilvl: nLevel,
|
||||
ilvl: effectiveIlvl,
|
||||
dwInitSeed,
|
||||
durabilityMultiplier,
|
||||
itemRng,
|
||||
|
|
@ -1108,7 +1132,7 @@ export function executeDropPipeline(
|
|||
if (quality === 'magic') {
|
||||
const magicAffixes = rollMagicAffixes(
|
||||
effectiveBase,
|
||||
nLevel,
|
||||
effectiveIlvl,
|
||||
dropTables.magicAffixes,
|
||||
dropTables.isA,
|
||||
itemRng,
|
||||
|
|
@ -1119,7 +1143,7 @@ export function executeDropPipeline(
|
|||
autoPrefixGroup !== undefined
|
||||
? rollAutoMagic(
|
||||
effectiveBase,
|
||||
nLevel,
|
||||
effectiveIlvl,
|
||||
dropTables.autoMagic ?? CANONICAL_AUTOMAGIC_TABLE,
|
||||
dropTables.isA,
|
||||
itemRng,
|
||||
|
|
@ -1128,9 +1152,9 @@ export function executeDropPipeline(
|
|||
: null
|
||||
const staffMods =
|
||||
resolveStaffModsClass(effectiveBase, dropTables.itemTypes) !== undefined
|
||||
? rollStaffMods(effectiveBase, nLevel, dropTables.itemTypes, itemRng)
|
||||
? rollStaffMods(effectiveBase, effectiveIlvl, dropTables.itemTypes, itemRng)
|
||||
: undefined
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'magic', nLevel, itemRng, {
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'magic', effectiveIlvl, itemRng, {
|
||||
itemTypes: dropTables.itemTypes,
|
||||
})
|
||||
drops.push(createDroppedItem(effectiveBase, 'magic', {
|
||||
|
|
@ -1140,7 +1164,7 @@ export function executeDropPipeline(
|
|||
ethereal: ethAndSock.ethereal,
|
||||
sockets: ethAndSock.sockets,
|
||||
itemTypes: dropTables.itemTypes,
|
||||
ilvl: nLevel,
|
||||
ilvl: effectiveIlvl,
|
||||
dwInitSeed,
|
||||
durabilityMultiplier,
|
||||
itemRng,
|
||||
|
|
@ -1153,7 +1177,7 @@ export function executeDropPipeline(
|
|||
autoPrefixGroup !== undefined
|
||||
? rollAutoMagic(
|
||||
effectiveBase,
|
||||
nLevel,
|
||||
effectiveIlvl,
|
||||
dropTables.autoMagic ?? CANONICAL_AUTOMAGIC_TABLE,
|
||||
dropTables.isA,
|
||||
itemRng,
|
||||
|
|
@ -1162,11 +1186,11 @@ export function executeDropPipeline(
|
|||
: null
|
||||
const staffMods =
|
||||
resolveStaffModsClass(effectiveBase, dropTables.itemTypes) !== undefined
|
||||
? rollStaffMods(effectiveBase, nLevel, dropTables.itemTypes, itemRng)
|
||||
? rollStaffMods(effectiveBase, effectiveIlvl, dropTables.itemTypes, itemRng)
|
||||
: undefined
|
||||
|
||||
if (quality === 'superior') {
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'superior', nLevel, itemRng, {
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'superior', effectiveIlvl, itemRng, {
|
||||
itemTypes: dropTables.itemTypes,
|
||||
})
|
||||
drops.push(createDroppedItem(effectiveBase, 'superior', {
|
||||
|
|
@ -1175,7 +1199,7 @@ export function executeDropPipeline(
|
|||
ethereal: ethAndSock.ethereal,
|
||||
sockets: ethAndSock.sockets,
|
||||
itemTypes: dropTables.itemTypes,
|
||||
ilvl: nLevel,
|
||||
ilvl: effectiveIlvl,
|
||||
dwInitSeed,
|
||||
itemRng,
|
||||
}))
|
||||
|
|
@ -1183,7 +1207,7 @@ export function executeDropPipeline(
|
|||
}
|
||||
|
||||
if (quality === 'low') {
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'low', nLevel, itemRng, {
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'low', effectiveIlvl, itemRng, {
|
||||
itemTypes: dropTables.itemTypes,
|
||||
})
|
||||
drops.push(createDroppedItem(effectiveBase, 'low', {
|
||||
|
|
@ -1192,7 +1216,7 @@ export function executeDropPipeline(
|
|||
ethereal: ethAndSock.ethereal,
|
||||
sockets: ethAndSock.sockets,
|
||||
itemTypes: dropTables.itemTypes,
|
||||
ilvl: nLevel,
|
||||
ilvl: effectiveIlvl,
|
||||
dwInitSeed,
|
||||
itemRng,
|
||||
}))
|
||||
|
|
@ -1200,7 +1224,7 @@ export function executeDropPipeline(
|
|||
}
|
||||
|
||||
// Default: normal quality
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'normal', nLevel, itemRng, {
|
||||
const ethAndSock = rollItemEtherealAndSockets(effectiveBase, 'normal', effectiveIlvl, itemRng, {
|
||||
itemTypes: dropTables.itemTypes,
|
||||
})
|
||||
drops.push(createDroppedItem(effectiveBase, 'normal', {
|
||||
|
|
@ -1209,7 +1233,7 @@ export function executeDropPipeline(
|
|||
ethereal: ethAndSock.ethereal,
|
||||
sockets: ethAndSock.sockets,
|
||||
itemTypes: dropTables.itemTypes,
|
||||
ilvl: nLevel,
|
||||
ilvl: effectiveIlvl,
|
||||
dwInitSeed,
|
||||
itemRng,
|
||||
}))
|
||||
|
|
|
|||
|
|
@ -22,6 +22,13 @@ import {
|
|||
type RolledAffix,
|
||||
type RolledMod,
|
||||
} from './affix-generator.ts'
|
||||
import {
|
||||
resolveProperty,
|
||||
getDefaultItemPropertiesAndStats,
|
||||
type PropertiesTable,
|
||||
type ItemStatCostTable,
|
||||
type ItemStatCostRecord,
|
||||
} from './properties.ts'
|
||||
|
||||
export interface FormattedStatLine {
|
||||
text: string
|
||||
|
|
@ -1158,10 +1165,400 @@ const PROP_DESC_PRIORITY: Record<string, number> = {
|
|||
bloody: 0,
|
||||
fade: 0,
|
||||
state: 0,
|
||||
'res-mag': 41,
|
||||
'res-mag-max': 46,
|
||||
'abs-mag%': 26,
|
||||
kick: 121,
|
||||
throw: 5,
|
||||
'dmg-fire/lvl': 100,
|
||||
'dmg-ltng/lvl': 97,
|
||||
'dmg-cold/lvl': 94,
|
||||
'dmg-pois/lvl': 90,
|
||||
'att-mon%': 108,
|
||||
'dmg-mon%': 106,
|
||||
}
|
||||
|
||||
export function getPropertyPriority(code: string): number {
|
||||
return PROP_DESC_PRIORITY[code.trim().toLowerCase()] ?? 10
|
||||
/**
|
||||
* Canonical 1.13c ItemStatCost.txt numeric Stat ID lookup table (0..358, plus 359 for ethereal).
|
||||
* Used as the secondary tie-breaker when two distinct properties share the same descpriority.
|
||||
*/
|
||||
export const PROP_STAT_ID: Record<string, number> = {
|
||||
ac: 31,
|
||||
'ac-miss': 32,
|
||||
'ac-hth': 33,
|
||||
'red-dmg': 34,
|
||||
'dmag-ac': 34,
|
||||
'red-mag': 35,
|
||||
'mag-ac': 35,
|
||||
'red-dmg%': 36,
|
||||
dmag: 36,
|
||||
'ac%': 16,
|
||||
str: 0,
|
||||
enr: 1,
|
||||
dex: 2,
|
||||
vit: 3,
|
||||
hp: 7,
|
||||
mana: 9,
|
||||
stam: 11,
|
||||
'dmg%': 17,
|
||||
att: 19,
|
||||
block: 20,
|
||||
'dmg-min': 21,
|
||||
'dmg-norm': 21,
|
||||
'dmg-max': 22,
|
||||
'regen-mana': 27,
|
||||
manaregen: 27,
|
||||
'regen-stam': 28,
|
||||
'res-mag': 37,
|
||||
'res-mag-max': 38,
|
||||
'res-fire': 39,
|
||||
fireresist: 39,
|
||||
'res-all': 39,
|
||||
'all-res': 39,
|
||||
'res-fire-max': 40,
|
||||
'res-all-max': 40,
|
||||
'res-ltng': 41,
|
||||
lightresist: 41,
|
||||
'res-ltng-max': 42,
|
||||
'res-cold': 43,
|
||||
coldresist: 43,
|
||||
'res-cold-max': 44,
|
||||
'res-pois': 45,
|
||||
poisonresist: 45,
|
||||
'res-pois-max': 46,
|
||||
'fire-min': 48,
|
||||
'dmg-fire': 48,
|
||||
'dmg-elem': 48,
|
||||
'dmg-elem-min': 48,
|
||||
'fire-max': 49,
|
||||
'dmg-elem-max': 49,
|
||||
'ltng-min': 50,
|
||||
'dmg-ltng': 50,
|
||||
'ltng-max': 51,
|
||||
'dmg-mag': 52,
|
||||
'cold-min': 54,
|
||||
'dmg-cold': 54,
|
||||
'cold-max': 55,
|
||||
'cold-len': 56,
|
||||
'pois-min': 57,
|
||||
'dmg-pois': 57,
|
||||
'pois-max': 58,
|
||||
'pois-len': 59,
|
||||
lifesteal: 60,
|
||||
leech: 60,
|
||||
manasteal: 62,
|
||||
dur: 73,
|
||||
regen: 74,
|
||||
'rep-life': 74,
|
||||
'dur%': 75,
|
||||
'hp%': 76,
|
||||
'mana%': 77,
|
||||
thorns: 78,
|
||||
'gold%': 79,
|
||||
'mag%': 80,
|
||||
knock: 81,
|
||||
time: 82,
|
||||
ama: 83,
|
||||
pal: 83,
|
||||
nec: 83,
|
||||
sor: 83,
|
||||
bar: 83,
|
||||
dru: 83,
|
||||
ass: 83,
|
||||
randclassskill: 83,
|
||||
classskills: 83,
|
||||
addxp: 85,
|
||||
'heal-kill': 86,
|
||||
'hp-kill': 86,
|
||||
cheap: 87,
|
||||
herb: 88,
|
||||
light: 89,
|
||||
color: 90,
|
||||
ease: 91,
|
||||
levelreq: 92,
|
||||
ias: 93,
|
||||
swing1: 93,
|
||||
swing2: 93,
|
||||
swing3: 93,
|
||||
frw: 96,
|
||||
move1: 96,
|
||||
move2: 96,
|
||||
move3: 96,
|
||||
oskill: 97,
|
||||
state: 98,
|
||||
fhr: 99,
|
||||
balance1: 99,
|
||||
balance2: 99,
|
||||
balance3: 99,
|
||||
fbr: 102,
|
||||
block1: 102,
|
||||
block2: 102,
|
||||
block3: 102,
|
||||
fcr: 105,
|
||||
cast1: 105,
|
||||
cast2: 105,
|
||||
cast3: 105,
|
||||
skill: 107,
|
||||
'skill-rand': 107,
|
||||
rip: 108,
|
||||
'res-pois-len': 110,
|
||||
dmg: 111,
|
||||
howl: 112,
|
||||
stupidity: 113,
|
||||
blind: 113,
|
||||
'dmg-to-mana': 114,
|
||||
'ignore-ac': 115,
|
||||
'reduce-ac': 116,
|
||||
noheal: 117,
|
||||
'no-heal': 117,
|
||||
'half-freeze': 118,
|
||||
'att%': 119,
|
||||
'dmg-ac': 120,
|
||||
'dmg-demon': 121,
|
||||
'dmg-undead': 122,
|
||||
'att-demon': 123,
|
||||
'att-undead': 124,
|
||||
throw: 125,
|
||||
fireskill: 126,
|
||||
allskills: 127,
|
||||
'light-thorns': 128,
|
||||
freeze: 134,
|
||||
openwounds: 135,
|
||||
crush: 136,
|
||||
kick: 137,
|
||||
'mana-kill': 138,
|
||||
'demon-heal': 139,
|
||||
bloody: 140,
|
||||
deadly: 141,
|
||||
'abs-fire%': 142,
|
||||
'abs-fire': 143,
|
||||
'abs-ltng%': 144,
|
||||
'abs-ltng': 145,
|
||||
'abs-mag%': 146,
|
||||
'abs-mag': 147,
|
||||
'abs-cold%': 148,
|
||||
'abs-cold': 149,
|
||||
slow: 150,
|
||||
aura: 151,
|
||||
indestruct: 152,
|
||||
nofreeze: 153,
|
||||
'no-freeze': 153,
|
||||
stamdrain: 154,
|
||||
reanimate: 155,
|
||||
pierce: 156,
|
||||
magicarrow: 157,
|
||||
explosivearrow: 158,
|
||||
'dmg-throw': 159,
|
||||
'att-mon%': 179,
|
||||
'dmg-mon%': 180,
|
||||
fade: 181,
|
||||
skilltab: 188,
|
||||
sock: 194,
|
||||
'att-skill': 195,
|
||||
'kill-skill': 196,
|
||||
'death-skill': 197,
|
||||
'hit-skill': 198,
|
||||
'levelup-skill': 199,
|
||||
'gethit-skill': 201,
|
||||
charged: 204,
|
||||
'ac/lvl': 214,
|
||||
'ac%/lvl': 215,
|
||||
'hp/lvl': 216,
|
||||
'mana/lvl': 217,
|
||||
'dmg/lvl': 218,
|
||||
'dmg%/lvl': 219,
|
||||
'str/lvl': 220,
|
||||
'dex/lvl': 221,
|
||||
'enr/lvl': 222,
|
||||
'vit/lvl': 223,
|
||||
'att/lvl': 224,
|
||||
'att%/lvl': 225,
|
||||
'dmg-cold/lvl': 226,
|
||||
'dmg-fire/lvl': 227,
|
||||
'dmg-ltng/lvl': 228,
|
||||
'dmg-pois/lvl': 229,
|
||||
'res-cold/lvl': 230,
|
||||
'res-fire/lvl': 231,
|
||||
'res-ltng/lvl': 232,
|
||||
'res-pois/lvl': 233,
|
||||
'abs-cold/lvl': 234,
|
||||
'abs-fire/lvl': 235,
|
||||
'abs-ltng/lvl': 236,
|
||||
'abs-pois/lvl': 237,
|
||||
'thorns/lvl': 238,
|
||||
'gold%/lvl': 239,
|
||||
'mag%/lvl': 240,
|
||||
'regen-stam/lvl': 241,
|
||||
'stam/lvl': 242,
|
||||
'dmg-dem/lvl': 243,
|
||||
'dmg-und/lvl': 244,
|
||||
'att-dem/lvl': 245,
|
||||
'att-und/lvl': 246,
|
||||
'crush/lvl': 247,
|
||||
'wounds/lvl': 248,
|
||||
'kick/lvl': 249,
|
||||
'deadly/lvl': 250,
|
||||
'rep-dur': 252,
|
||||
'rep-quant': 253,
|
||||
stack: 254,
|
||||
'ac/time': 268,
|
||||
'extra-fire': 329,
|
||||
'extra-ltng': 330,
|
||||
'extra-cold': 331,
|
||||
'extra-pois': 332,
|
||||
'pierce-fire': 333,
|
||||
'pierce-ltng': 334,
|
||||
'pierce-cold': 335,
|
||||
'pierce-pois': 336,
|
||||
'all-stats': 0,
|
||||
ethereal: 359,
|
||||
}
|
||||
|
||||
const PROP_CODE_TO_MPQ_PROP: Record<string, string> = {
|
||||
ias: 'swing1',
|
||||
fcr: 'cast1',
|
||||
fhr: 'balance1',
|
||||
frw: 'move1',
|
||||
fbr: 'block1',
|
||||
classskills: 'ama',
|
||||
fireresist: 'res-fire',
|
||||
coldresist: 'res-cold',
|
||||
lightresist: 'res-ltng',
|
||||
poisonresist: 'res-pois',
|
||||
'all-res': 'res-all',
|
||||
dmag: 'red-dmg%',
|
||||
'dmag-ac': 'red-dmg',
|
||||
'mag-ac': 'red-mag',
|
||||
leech: 'lifesteal',
|
||||
'rep-life': 'regen',
|
||||
manaregen: 'regen-mana',
|
||||
'hp-kill': 'heal-kill',
|
||||
'no-freeze': 'nofreeze',
|
||||
'no-heal': 'noheal',
|
||||
blind: 'stupidity',
|
||||
}
|
||||
|
||||
const COMPOSITE_OR_SPECIAL_PRIORITY_CODES = new Set([
|
||||
'dmg-norm',
|
||||
'dmg-min',
|
||||
'dmg-max',
|
||||
'res-all',
|
||||
'all-res',
|
||||
'all-stats',
|
||||
'dur',
|
||||
'cold-len',
|
||||
'pois-len',
|
||||
'levelreq',
|
||||
])
|
||||
|
||||
let runtimePropertiesTable: PropertiesTable | undefined
|
||||
let runtimeItemStatCostTable: ItemStatCostTable | undefined
|
||||
|
||||
export interface RuntimePropertyTables {
|
||||
readonly properties?: PropertiesTable | undefined
|
||||
readonly itemStatCost?: ItemStatCostTable | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Injects runtime MPQ-parsed Properties.txt and ItemStatCost.txt tables into the tooltip engine.
|
||||
*/
|
||||
export function setRuntimePropertyTables(
|
||||
properties?: PropertiesTable,
|
||||
itemStatCost?: ItemStatCostTable,
|
||||
): void {
|
||||
runtimePropertiesTable = properties
|
||||
runtimeItemStatCostTable = itemStatCost
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves live metadata (descPriority, statId, descFunc, descVal, dgrp) for a property code
|
||||
* from the active MPQ Properties.txt -> ItemStatCost.txt tables.
|
||||
*/
|
||||
export function getRuntimePropertyMetadata(
|
||||
code: string,
|
||||
tables?: RuntimePropertyTables,
|
||||
): { descPriority: number; statId: number; descFunc?: number; descVal?: number; dgrp?: number } | undefined {
|
||||
const defaults = getDefaultItemPropertiesAndStats()
|
||||
const propsTable = tables?.properties ?? runtimePropertiesTable ?? defaults.properties
|
||||
const iscTable = tables?.itemStatCost ?? runtimeItemStatCostTable ?? defaults.stats
|
||||
|
||||
if (!propsTable || !iscTable) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const norm = (code || '').trim().toLowerCase()
|
||||
if (!norm) return undefined
|
||||
|
||||
const lookupCode = PROP_CODE_TO_MPQ_PROP[norm] ?? norm
|
||||
const propRec = propsTable.byCode.get(norm) ?? propsTable.byCode.get(lookupCode)
|
||||
|
||||
let statRec: ItemStatCostRecord | undefined
|
||||
if (propRec?.stat1) {
|
||||
statRec = iscTable.byStat.get(propRec.stat1)
|
||||
}
|
||||
if (!statRec && propRec) {
|
||||
try {
|
||||
const resolved = resolveProperty(propRec.code, undefined, 1, 1, {
|
||||
properties: propsTable,
|
||||
stats: iscTable,
|
||||
})
|
||||
if (resolved[0]?.stat) {
|
||||
statRec = iscTable.byStat.get(resolved[0].stat)
|
||||
}
|
||||
} catch {
|
||||
// Fallback below if resolveProperty fails
|
||||
}
|
||||
}
|
||||
if (!statRec) {
|
||||
statRec = iscTable.byStat.get(norm)
|
||||
}
|
||||
|
||||
if (!propRec && !statRec && !(norm in PROP_DESC_PRIORITY) && !(norm in PROP_STAT_ID)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const statId = statRec?.id ?? PROP_STAT_ID[norm] ?? PROP_STAT_ID[lookupCode] ?? -1
|
||||
let descPriority: number
|
||||
if (
|
||||
COMPOSITE_OR_SPECIAL_PRIORITY_CODES.has(norm) ||
|
||||
COMPOSITE_OR_SPECIAL_PRIORITY_CODES.has(lookupCode) ||
|
||||
!statRec
|
||||
) {
|
||||
descPriority = PROP_DESC_PRIORITY[norm] ?? PROP_DESC_PRIORITY[lookupCode] ?? statRec?.descPriority ?? 10
|
||||
} else {
|
||||
descPriority = statRec.descPriority
|
||||
}
|
||||
|
||||
return {
|
||||
descPriority,
|
||||
statId,
|
||||
...(statRec
|
||||
? {
|
||||
descFunc: statRec.descFunc,
|
||||
descVal: statRec.descVal,
|
||||
dgrp: statRec.dgrp,
|
||||
}
|
||||
: {}),
|
||||
}
|
||||
}
|
||||
|
||||
export function getPropertyPriority(code: string, tables?: RuntimePropertyTables): number {
|
||||
const norm = code.trim().toLowerCase()
|
||||
const meta = getRuntimePropertyMetadata(norm, tables)
|
||||
if (meta !== undefined) {
|
||||
return meta.descPriority
|
||||
}
|
||||
return PROP_DESC_PRIORITY[norm] ?? 10
|
||||
}
|
||||
|
||||
export function getPropertyStatId(code: string, tables?: RuntimePropertyTables): number {
|
||||
const norm = code.trim().toLowerCase()
|
||||
const meta = getRuntimePropertyMetadata(norm, tables)
|
||||
if (meta !== undefined && meta.statId >= 0) {
|
||||
return meta.statId
|
||||
}
|
||||
const lookup = PROP_CODE_TO_MPQ_PROP[norm] ?? norm
|
||||
return PROP_STAT_ID[norm] ?? PROP_STAT_ID[lookup] ?? -1
|
||||
}
|
||||
|
||||
const ALIAS_CODE_MAP: Record<string, string> = {
|
||||
|
|
@ -1198,15 +1595,18 @@ const ALIAS_CODE_MAP: Record<string, string> = {
|
|||
}
|
||||
|
||||
/**
|
||||
* Aggregates and sorts item properties per Diablo II 1.13c tooltip rules (Issue #125 & Issue #130):
|
||||
* Aggregates and sorts item properties per Diablo II 1.13c tooltip rules (Issue #125, #130, #133):
|
||||
* 1. Combines elemental min/max pairs (`fire-min` + `fire-max` -> `Adds X-Y Fire Damage`, etc.)
|
||||
* 2. Combines physical min/max pairs (`dmg-min` + `dmg-max` -> `Adds X-Y Damage`)
|
||||
* 3. Merges 4-element resistances (`dgrp=2`) into `All Resistances +X` ONLY when all 4 are positive and strictly equal
|
||||
* 4. Merges 4 core attributes (`dgrp=1`) into `+X to All Attributes` ONLY when all 4 are positive and strictly equal
|
||||
* 5. Accumulates identical scalar stats across prefixes, suffixes, and base modifiers
|
||||
* 6. Sorts all resulting properties descending by `ItemStatCost.txt` `descpriority`
|
||||
* 6. Sorts all resulting properties descending by `ItemStatCost.txt` `descpriority`, with secondary ascending `statId` tie-breaker
|
||||
*/
|
||||
export function aggregateAndSortProperties(rawProps: readonly RolledItemProp[]): RolledItemProp[] {
|
||||
export function aggregateAndSortProperties(
|
||||
rawProps: readonly RolledItemProp[],
|
||||
tables?: RuntimePropertyTables,
|
||||
): RolledItemProp[] {
|
||||
let fireMin = 0
|
||||
let fireMax = 0
|
||||
let ltngMin = 0
|
||||
|
|
@ -1505,10 +1905,28 @@ export function aggregateAndSortProperties(rawProps: readonly RolledItemProp[]):
|
|||
if (poisRes !== 0) out.push({ code: 'res-pois', min: poisRes, max: poisRes, value: poisRes })
|
||||
}
|
||||
|
||||
// Sort descending by ItemStatCost.txt descpriority
|
||||
// Sort descending by ItemStatCost.txt descpriority, then ascending by 1.13c statId for distinct properties
|
||||
return out
|
||||
.map((item, index) => ({ item, index, prio: getPropertyPriority(item.code) }))
|
||||
.sort((a, b) => (b.prio !== a.prio ? b.prio - a.prio : a.index - b.index))
|
||||
.map((item, index) => ({
|
||||
item,
|
||||
index,
|
||||
prio: getPropertyPriority(item.code, tables),
|
||||
statId: getPropertyStatId(item.code, tables),
|
||||
}))
|
||||
.sort((a, b) => {
|
||||
if (b.prio !== a.prio) {
|
||||
return b.prio - a.prio
|
||||
}
|
||||
if (
|
||||
a.statId >= 0 &&
|
||||
b.statId >= 0 &&
|
||||
a.statId !== b.statId &&
|
||||
a.item.code.trim().toLowerCase() !== b.item.code.trim().toLowerCase()
|
||||
) {
|
||||
return a.statId - b.statId
|
||||
}
|
||||
return a.index - b.index
|
||||
})
|
||||
.map(x => x.item)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -523,7 +523,7 @@ export function parsePropertiesTable(
|
|||
*/
|
||||
export async function loadItemPropertiesAndStats(
|
||||
archives: MountedArchives
|
||||
): Promise<{ stats: ItemStatCostTable; properties: PropertiesTable }> {
|
||||
): Promise<{ stats: ItemStatCostTable; itemStatCost: ItemStatCostTable; properties: PropertiesTable }> {
|
||||
const statsRaw = await archives.read(ITEM_STAT_COST_PATH)
|
||||
const stats = parseItemStatCostTable(parseTable(statsRaw))
|
||||
|
||||
|
|
@ -531,7 +531,7 @@ export async function loadItemPropertiesAndStats(
|
|||
const properties = parsePropertiesTable(parseTable(propertiesRaw), stats)
|
||||
|
||||
setDefaultItemPropertiesAndStats({ stats, properties })
|
||||
return { stats, properties }
|
||||
return { stats, itemStatCost: stats, properties }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -1,8 +1,13 @@
|
|||
import { describe, it, expect, beforeAll } from 'vitest'
|
||||
import { ACT_BOSSES, BOSS_LIST, type BossDefinition } from '../src/boss.ts'
|
||||
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts'
|
||||
import * as fs from 'fs'
|
||||
import { MpqArchive } from '../src/mpq/archive.ts'
|
||||
import { fileSource } from '../src/mpq/file-source.ts'
|
||||
import { MountedArchives } from '../src/mpq/mount.ts'
|
||||
import {
|
||||
executeDropPipeline,
|
||||
loadDropTables,
|
||||
resolveItemTypeFlags,
|
||||
createDroppedItem,
|
||||
rollAutoMagic,
|
||||
|
|
@ -18,7 +23,15 @@ import {
|
|||
rollMagicAffixes,
|
||||
rollRareAffixes,
|
||||
} from '../src/game/affix-generator.ts'
|
||||
import { formatItemTooltip, aggregateAndSortProperties } from '../src/game/item-tooltip.ts'
|
||||
import {
|
||||
formatItemTooltip,
|
||||
aggregateAndSortProperties,
|
||||
setRuntimePropertyTables,
|
||||
getRuntimePropertyMetadata,
|
||||
getPropertyPriority,
|
||||
getPropertyStatId,
|
||||
PROP_STAT_ID,
|
||||
} from '../src/game/item-tooltip.ts'
|
||||
import { D2Rng } from '../src/game/d2-rng.ts'
|
||||
import { isAffixEligible, type MagicAffix, type AffixTable } from '../src/game/affixes.ts'
|
||||
import type { RareNameTable } from '../src/game/rare-names.ts'
|
||||
|
|
@ -1126,4 +1139,204 @@ describe('Affix classspecific Restriction & classlevelreq Tooltip Level Requirem
|
|||
})
|
||||
})
|
||||
|
||||
describe('Data-Driven Tooltip Properties (C1/C3) & 1.13c Drop Pipeline Alignment (A6/A8/B7) (Issue #133)', () => {
|
||||
const hasD2Mpq = fs.existsSync('samples/d2/d2data.mpq')
|
||||
|
||||
it.skipIf(!hasD2Mpq)('1. C1 — loadDropTables(archives) loads Properties.txt (268) & ItemStatCost.txt (359), activates setRuntimePropertyTables, and matches 100% of core descPriority values', async () => {
|
||||
const archives = new MountedArchives()
|
||||
for (const name of ['d2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
|
||||
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
|
||||
}
|
||||
|
||||
const mpqDropTables = await loadDropTables(archives)
|
||||
expect(mpqDropTables.properties).toBeDefined()
|
||||
expect(mpqDropTables.itemStatCost).toBeDefined()
|
||||
expect(mpqDropTables.properties!.records).toHaveLength(268)
|
||||
expect(mpqDropTables.itemStatCost!.records).toHaveLength(359)
|
||||
|
||||
const corePropertyCodes = [
|
||||
'ac',
|
||||
'ac%',
|
||||
'str',
|
||||
'dex',
|
||||
'vit',
|
||||
'enr',
|
||||
'hp',
|
||||
'mana',
|
||||
'fcr',
|
||||
'ias',
|
||||
'frw',
|
||||
'fhr',
|
||||
'fbr',
|
||||
'allskills',
|
||||
'skilltab',
|
||||
'res-fire',
|
||||
'res-cold',
|
||||
'res-ltng',
|
||||
'res-pois',
|
||||
'res-all',
|
||||
'all-stats',
|
||||
'dmg%',
|
||||
'dmg-min',
|
||||
'dmg-max',
|
||||
'dmg-fire',
|
||||
'dmg-ltng',
|
||||
'dmg-cold',
|
||||
'dmg-pois',
|
||||
'lifesteal',
|
||||
'manasteal',
|
||||
'mag%',
|
||||
'gold%',
|
||||
'crush',
|
||||
'deadly',
|
||||
'openwounds',
|
||||
'indestruct',
|
||||
'charged',
|
||||
'rep-dur',
|
||||
'ease',
|
||||
'sock',
|
||||
]
|
||||
|
||||
// Snapshot baseline priorities without runtime tables
|
||||
setRuntimePropertyTables(undefined, undefined)
|
||||
const baselinePriorities = new Map<string, number>()
|
||||
for (const code of corePropertyCodes) {
|
||||
baselinePriorities.set(code, getPropertyPriority(code))
|
||||
}
|
||||
|
||||
// Re-activate runtime tables loaded from MPQ
|
||||
setRuntimePropertyTables(mpqDropTables.properties, mpqDropTables.itemStatCost)
|
||||
|
||||
for (const code of corePropertyCodes) {
|
||||
const meta = getRuntimePropertyMetadata(code)
|
||||
expect(meta, `getRuntimePropertyMetadata('${code}') must be defined`).toBeDefined()
|
||||
expect(meta!.statId, `statId for '${code}' must be >= 0`).toBeGreaterThanOrEqual(0)
|
||||
expect(meta!.descPriority, `descPriority for '${code}' must match baseline`).toBe(
|
||||
baselinePriorities.get(code),
|
||||
)
|
||||
expect(getPropertyPriority(code)).toBe(baselinePriorities.get(code))
|
||||
}
|
||||
})
|
||||
|
||||
it('2. C3 — aggregateAndSortProperties breaks descPriority ties using ascending 1.13c ItemStatCost statId while keeping identical properties stable', () => {
|
||||
// 1) prio = 88: extra-fire (passive_fire_mastery, statId=329) vs pierce-fire (passive_fire_pierce, statId=333)
|
||||
// 2) prio = 22: red-dmg (normal_damage_reduction, statId=34) vs red-dmg% (damageresist, statId=36)
|
||||
// 3) prio = 16: heal-kill (item_healafterkill, statId=86) vs mana-kill (item_manaafterkill, statId=138)
|
||||
// 4) prio = 3: dur (maxdurability, statId=73) vs dur% (item_maxdurability_percent, statId=75)
|
||||
expect(getPropertyStatId('extra-fire')).toBe(329)
|
||||
expect(getPropertyStatId('pierce-fire')).toBe(333)
|
||||
expect(getPropertyStatId('red-dmg')).toBe(34)
|
||||
expect(getPropertyStatId('red-dmg%')).toBe(36)
|
||||
expect(getPropertyStatId('heal-kill')).toBe(86)
|
||||
expect(getPropertyStatId('mana-kill')).toBe(138)
|
||||
expect(getPropertyStatId('dur')).toBe(73)
|
||||
expect(getPropertyStatId('dur%')).toBe(75)
|
||||
|
||||
// Pass reversed statId order within each tied descPriority group
|
||||
const sorted = aggregateAndSortProperties([
|
||||
{ code: 'dur%', min: 15, max: 15, value: 15 }, // prio 3, statId 75
|
||||
{ code: 'dur', min: 10, max: 10, value: 10 }, // prio 3, statId 73
|
||||
{ code: 'mana-kill', min: 3, max: 3, value: 3 }, // prio 16, statId 138
|
||||
{ code: 'heal-kill', min: 5, max: 5, value: 5 }, // prio 16, statId 86
|
||||
{ code: 'red-dmg%', min: 10, max: 10, value: 10 }, // prio 22, statId 36
|
||||
{ code: 'red-dmg', min: 7, max: 7, value: 7 }, // prio 22, statId 34
|
||||
{ code: 'pierce-fire', min: 5, max: 5, value: 5 }, // prio 88, statId 333
|
||||
{ code: 'extra-fire', min: 5, max: 5, value: 5 }, // prio 88, statId 329
|
||||
{ code: 'lifesteal', min: 8, max: 8, value: 8 }, // prio 88, statId 60
|
||||
{ code: 'hit-skill', param: 197, min: 5, max: 10, value: 5 }, // prio 160, statId 198 (1st)
|
||||
{ code: 'hit-skill', param: 53, min: 8, max: 12, value: 8 }, // prio 160, statId 198 (2nd)
|
||||
])
|
||||
|
||||
expect(sorted.map(p => `${p.code}${p.param ? `:${p.param}` : ''}`)).toEqual([
|
||||
'hit-skill:197',
|
||||
'hit-skill:53',
|
||||
'lifesteal',
|
||||
'extra-fire',
|
||||
'pierce-fire',
|
||||
'red-dmg',
|
||||
'red-dmg%',
|
||||
'heal-kill',
|
||||
'mana-kill',
|
||||
'dur',
|
||||
'dur%',
|
||||
])
|
||||
})
|
||||
|
||||
it('3. A6 & A8 — executeDropPipeline clamps effectiveIlvl to [1, 99], uses 1.13c [floor(lvl/2), lvl*6] gold range, and prevents qlvl > effectiveIlvl inflation', () => {
|
||||
const dropTables = getEmbeddedDropTables()
|
||||
|
||||
// Test A6 gold range & A8 nLevel clamping at nLevel = 120 (clamped to 99 -> gold base [49, 594])
|
||||
for (let i = 0; i < 40; i++) {
|
||||
const dropsHigh = executeDropPipeline({
|
||||
tcName: 'Act 1 H2H A',
|
||||
nLevel: 120,
|
||||
monsterType: 1,
|
||||
dropTables,
|
||||
difficulty: 'hell',
|
||||
playerMf: 0,
|
||||
monsterRng: (0x133000 + i * 37) >>> 0,
|
||||
})
|
||||
for (const item of dropsHigh) {
|
||||
if (item.code?.trim() === 'gld' || item.name === 'Gold') {
|
||||
const amt = item.stack ?? item.value ?? 0
|
||||
expect(amt).toBeGreaterThanOrEqual(49)
|
||||
} else {
|
||||
expect(item.ilvl).toBe(99)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Test A8 clamping at nLevel = 0 (clamped to 1) on high-qlvl base ('uit' Monarch, qlvl=72)
|
||||
// Ensures ilvl === 1 and qlvl is clamped to 1 so D = ilvl - qlvl = 0 (not -71)
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const dropsLow = executeDropPipeline({
|
||||
tcName: 'uit',
|
||||
nLevel: 0,
|
||||
monsterType: 1,
|
||||
dropTables,
|
||||
difficulty: 'normal',
|
||||
playerMf: 0,
|
||||
monsterRng: (0x233000 + i * 41) >>> 0,
|
||||
})
|
||||
expect(dropsLow).toHaveLength(1)
|
||||
expect(dropsLow[0]!.ilvl).toBe(1)
|
||||
}
|
||||
})
|
||||
|
||||
it('4. B7 — rollRareAffixes prevents duplicate rarePrefix and rareSuffix names when eligibleRareSuffixes.length > 1', () => {
|
||||
const dropTables = getEmbeddedDropTables()
|
||||
const swordBase = dropTables.getBase('crs')!
|
||||
|
||||
const collidingRareNames: { prefixes: RareNameTable; suffixes: RareNameTable } = {
|
||||
prefixes: {
|
||||
entries: [],
|
||||
getEligible: () => [
|
||||
{ index: 0, name: 'Shadow', version: 100, itypes: ['weap'], etypes: [], add: 0, multiply: 0, divide: 0 },
|
||||
],
|
||||
} as unknown as RareNameTable,
|
||||
suffixes: {
|
||||
entries: [],
|
||||
getEligible: () => [
|
||||
{ index: 0, name: 'Shadow', version: 100, itypes: ['weap'], etypes: [], add: 0, multiply: 0, divide: 0 },
|
||||
{ index: 1, name: 'Fang', version: 100, itypes: ['weap'], etypes: [], add: 0, multiply: 0, divide: 0 },
|
||||
],
|
||||
} as unknown as RareNameTable,
|
||||
}
|
||||
|
||||
for (let s = 1; s <= 30; s++) {
|
||||
const rolled = rollRareAffixes(
|
||||
swordBase,
|
||||
50,
|
||||
dropTables.magicAffixes,
|
||||
collidingRareNames,
|
||||
dropTables.isA,
|
||||
new D2Rng(s),
|
||||
)
|
||||
expect(rolled.rarePrefix?.name).toBe('Shadow')
|
||||
expect(rolled.rareSuffix?.name).toBe('Fang')
|
||||
expect(rolled.name).toBe('Shadow Fang')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue