From f68b1de4ecc29b687049d2e5744bbc8bfd72bf7d Mon Sep 17 00:00:00 2001
From: troytt <47798984@qq.com>
Date: Sat, 19 Sep 2026 08:05:07 +0000
Subject: [PATCH] =?UTF-8?q?feat(tooltip/stats):=20=E5=AE=9E=E7=8E=B0?=
=?UTF-8?q?=E6=AD=A6=E5=99=A8=E9=98=B2=E5=85=B7=E5=9F=BA=E7=A1=80=E5=B1=9E?=
=?UTF-8?q?=E6=80=A7=E5=A2=9E=E5=BC=BA=E4=B8=8E=E5=BA=95=E6=A8=A1=E5=AF=B9?=
=?UTF-8?q?=E7=85=A7=E3=80=81=E8=AF=8D=E6=9D=A1=E5=90=88=E5=B9=B6=E4=B8=8E?=
=?UTF-8?q?ItemStatCost=E4=BC=98=E5=85=88=E7=BA=A7=E6=8E=92=E5=BA=8F=20(fi?=
=?UTF-8?q?xes=20#123,=20#125)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
- Issue #123: 计算武器1H/2H增强伤害 (dmg%, dmg-min, dmg-max)、防具增强防御 (ac%, ac)、力量/敏捷/等级需求 (ease, levelreq) 及耐久度上限 (dur, dur%, 3x/2x降级),并附带原始底模数值对照(如 Two-Hand Damage: 36 to 48 (18 to 24), Defense: 260 (130), Required Strength: 124 (156))
- Issue #125: 实现 aggregateAndSortProperties:
* 自动合并 fire-min + fire-max -> Adds X-Y Fire Damage(冰、电、毒、物理同理)
* 自动合并 dgrp=2 四抗为 All Resistances +X,dgrp=1 四维为 +X to All Attributes
* 累加前后缀同类标量词缀,并按 ItemStatCost.txt descpriority (180..0) 严格降序排列
- 全程保持 1.13c 官方英文规范,新增 tests/item-tooltip-123-125.test.ts 专项单测通过
TAG=agy
CONV=aade657e-0c8e-42ff-a5f2-87b2afe80e02
---
boss.html | 8 +
src/boss.ts | 59 ++-
src/game/item-tooltip.ts | 820 +++++++++++++++++++++++++++--
tests/item-tooltip-123-125.test.ts | 271 ++++++++++
4 files changed, 1103 insertions(+), 55 deletions(-)
create mode 100644 tests/item-tooltip-123-125.test.ts
diff --git a/boss.html b/boss.html
index 91044ad..5b1b2e9 100644
--- a/boss.html
+++ b/boss.html
@@ -605,6 +605,14 @@
color: #fff;
font-weight: 700;
}
+ .base-stat-item .val-blue {
+ color: #6888ff;
+ font-weight: 700;
+ }
+ .base-stat-item .val-base {
+ color: #9e9382;
+ font-weight: 500;
+ }
.base-stat-item.req {
color: #b08d74;
}
diff --git a/src/boss.ts b/src/boss.ts
index 0212f39..4bed057 100644
--- a/src/boss.ts
+++ b/src/boss.ts
@@ -511,13 +511,58 @@ function renderSingleItemCard(t: FormattedItemTooltip, index: number): string {
}).join('')
const baseStatsHtml: string[] = []
- if (t.defense) baseStatsHtml.push(`
Defense: ${t.defense}
`)
- if (t.oneHandDamage) baseStatsHtml.push(`One-Hand Damage: ${t.oneHandDamage.min} to ${t.oneHandDamage.max}
`)
- if (t.twoHandDamage) baseStatsHtml.push(`Two-Hand Damage: ${t.twoHandDamage.min} to ${t.twoHandDamage.max}
`)
- if (t.durability) baseStatsHtml.push(`Durability: ${t.durability.current} of ${t.durability.max}
`)
- if (t.reqLevel) baseStatsHtml.push(`Required Level: ${t.reqLevel}
`)
- if (t.reqStr) baseStatsHtml.push(`Required Strength: ${t.reqStr}
`)
- if (t.reqDex) baseStatsHtml.push(`Required Dexterity: ${t.reqDex}
`)
+ if (t.defense !== undefined) {
+ const defText =
+ t.baseDefense !== undefined && t.baseDefense !== t.defense
+ ? `${t.defense} (${t.baseDefense})`
+ : `${t.defense}`
+ baseStatsHtml.push(`Defense: ${defText}
`)
+ }
+ if (t.oneHandDamage) {
+ const d = t.oneHandDamage
+ const dmgText =
+ d.isEnhanced && d.baseMin !== undefined && d.baseMax !== undefined
+ ? `${d.min} to ${d.max} (${d.baseMin} to ${d.baseMax})`
+ : `${d.min} to ${d.max}`
+ baseStatsHtml.push(`One-Hand Damage: ${dmgText}
`)
+ }
+ if (t.twoHandDamage) {
+ const d = t.twoHandDamage
+ const dmgText =
+ d.isEnhanced && d.baseMin !== undefined && d.baseMax !== undefined
+ ? `${d.min} to ${d.max} (${d.baseMin} to ${d.baseMax})`
+ : `${d.min} to ${d.max}`
+ baseStatsHtml.push(`Two-Hand Damage: ${dmgText}
`)
+ }
+ if (t.durability) {
+ const dur = t.durability
+ const durText =
+ dur.isEnhanced && dur.baseMax !== undefined
+ ? `${dur.current} of ${dur.max} (${dur.baseMax})`
+ : `${dur.current} of ${dur.max}`
+ baseStatsHtml.push(`Durability: ${durText}
`)
+ }
+ if (t.reqLevel !== undefined) {
+ const lvlText =
+ t.baseReqLevel !== undefined && t.baseReqLevel !== t.reqLevel
+ ? `${t.reqLevel} (${t.baseReqLevel})`
+ : `${t.reqLevel}`
+ baseStatsHtml.push(`Required Level: ${lvlText}
`)
+ }
+ if (t.reqStr !== undefined) {
+ const strText =
+ t.baseReqStr !== undefined && t.baseReqStr !== t.reqStr
+ ? `${t.reqStr} (${t.baseReqStr})`
+ : `${t.reqStr}`
+ baseStatsHtml.push(`Required Strength: ${strText}
`)
+ }
+ if (t.reqDex !== undefined) {
+ const dexText =
+ t.baseReqDex !== undefined && t.baseReqDex !== t.reqDex
+ ? `${t.reqDex} (${t.baseReqDex})`
+ : `${t.reqDex}`
+ baseStatsHtml.push(`Required Dexterity: ${dexText}
`)
+ }
return `
diff --git a/src/game/item-tooltip.ts b/src/game/item-tooltip.ts
index c449cd2..3cbc4b9 100644
--- a/src/game/item-tooltip.ts
+++ b/src/game/item-tooltip.ts
@@ -28,12 +28,33 @@ export interface FormattedItemTooltip {
baseType: string
ilvl: number
reqLevel?: number | undefined
+ baseReqLevel?: number | undefined
reqStr?: number | undefined
+ baseReqStr?: number | undefined
reqDex?: number | undefined
+ baseReqDex?: number | undefined
defense?: number | undefined
- oneHandDamage?: { min: number; max: number } | undefined
- twoHandDamage?: { min: number; max: number } | undefined
- durability?: { current: number; max: number } | undefined
+ baseDefense?: number | undefined
+ oneHandDamage?: {
+ min: number
+ max: number
+ baseMin?: number | undefined
+ baseMax?: number | undefined
+ isEnhanced?: boolean | undefined
+ } | undefined
+ twoHandDamage?: {
+ min: number
+ max: number
+ baseMin?: number | undefined
+ baseMax?: number | undefined
+ isEnhanced?: boolean | undefined
+ } | undefined
+ durability?: {
+ current: number
+ max: number
+ baseMax?: number | undefined
+ isEnhanced?: boolean | undefined
+ } | undefined
sockets?: number | undefined
lines: FormattedStatLine[]
setPieces?: string[] | undefined
@@ -916,6 +937,579 @@ export function formatPropertyCode(
}
}
+/**
+ * Canonical 1.13c ItemStatCost.txt descpriority lookup table (0..180).
+ * Higher priority stats appear higher on the item tooltip.
+ */
+const PROP_DESC_PRIORITY: Record = {
+ 'ac/time': 180,
+ 'att-skill': 160,
+ 'hit-skill': 160,
+ 'gethit-skill': 160,
+ 'kill-skill': 160,
+ 'death-skill': 160,
+ 'levelup-skill': 160,
+ indestruct: 160,
+ aura: 159,
+ allskills: 158,
+ fireskill: 157,
+ skilltab: 151,
+ ama: 150,
+ pal: 150,
+ nec: 150,
+ sor: 150,
+ bar: 150,
+ dru: 150,
+ ass: 150,
+ randclassskill: 150,
+ classskills: 150,
+ frw: 148,
+ move1: 148,
+ move2: 148,
+ move3: 148,
+ ias: 145,
+ swing1: 145,
+ swing2: 145,
+ swing3: 145,
+ fcr: 142,
+ cast1: 142,
+ cast2: 142,
+ cast3: 142,
+ fhr: 139,
+ balance1: 139,
+ balance2: 139,
+ balance3: 139,
+ fbr: 136,
+ block1: 136,
+ block2: 136,
+ block3: 136,
+ block: 134,
+ explosivearrow: 133,
+ pierce: 132,
+ magicarrow: 131,
+ 'dmg%': 129,
+ 'dmg%/lvl': 128,
+ 'dmg-norm': 127,
+ 'dmg-min': 126,
+ 'dmg-max': 125,
+ 'dmg/lvl': 125,
+ dmg: 122,
+ 'kick/lvl': 120,
+ 'ignore-ac': 119,
+ 'reduce-ac': 118,
+ 'att%': 117,
+ 'att%/lvl': 116,
+ att: 115,
+ 'att/lvl': 114,
+ 'dmg-demon': 112,
+ 'dmg-dem/lvl': 111,
+ 'att-demon': 110,
+ 'att-dem/lvl': 109,
+ 'dmg-undead': 108,
+ 'dmg-und/lvl': 107,
+ 'att-undead': 106,
+ 'att-und/lvl': 105,
+ 'dmg-mag': 104,
+ 'dmg-fire': 102,
+ 'fire-min': 102,
+ 'dmg-elem': 102,
+ 'fire-max': 101,
+ 'dmg-ltng': 99,
+ 'ltng-min': 99,
+ 'ltng-max': 98,
+ 'dmg-cold': 96,
+ 'cold-min': 96,
+ 'cold-max': 95,
+ 'dmg-pois': 92,
+ 'pois-min': 92,
+ 'pois-max': 91,
+ manasteal: 89,
+ lifesteal: 88,
+ leech: 88,
+ 'pierce-fire': 88,
+ 'pierce-ltng': 88,
+ 'pierce-cold': 88,
+ 'pierce-pois': 88,
+ 'extra-fire': 88,
+ 'extra-ltng': 88,
+ 'extra-cold': 88,
+ 'extra-pois': 88,
+ crush: 87,
+ 'crush/lvl': 86,
+ deadly: 85,
+ 'deadly/lvl': 84,
+ openwounds: 83,
+ 'wounds/lvl': 82,
+ noheal: 81,
+ 'no-heal': 81,
+ skill: 81,
+ rip: 81,
+ 'skill-rand': 81,
+ oskill: 81,
+ stupidity: 80,
+ blind: 80,
+ howl: 79,
+ freeze: 78,
+ slow: 77,
+ knock: 76,
+ 'dmg-ac': 75,
+ 'ac%': 74,
+ 'ac%/lvl': 73,
+ 'ac/lvl': 72,
+ ac: 71,
+ 'ac-hth': 70,
+ 'ac-miss': 69,
+ 'all-stats': 68,
+ str: 67,
+ 'str/lvl': 66,
+ dex: 65,
+ 'dex/lvl': 64,
+ vit: 63,
+ 'vit/lvl': 62,
+ enr: 61,
+ 'enr/lvl': 60,
+ hp: 59,
+ 'hp%': 58,
+ 'hp/lvl': 57,
+ regen: 56,
+ 'rep-life': 56,
+ mana: 55,
+ 'mana%': 54,
+ 'mana/lvl': 53,
+ 'regen-mana': 52,
+ manaregen: 52,
+ stam: 51,
+ 'stam/lvl': 50,
+ stamdrain: 49,
+ 'regen-stam': 48,
+ 'regen-stam/lvl': 47,
+ 'res-pois-max': 45,
+ 'res-cold-max': 44,
+ 'res-ltng-max': 43,
+ 'res-fire-max': 42,
+ 'res-all-max': 42,
+ 'res-all': 41,
+ 'res-cold': 40,
+ coldresist: 40,
+ 'res-cold/lvl': 39,
+ 'res-ltng': 38,
+ lightresist: 38,
+ 'res-ltng/lvl': 37,
+ 'res-fire': 36,
+ fireresist: 36,
+ 'res-fire/lvl': 35,
+ 'res-pois': 34,
+ poisonresist: 34,
+ 'abs-mag': 33,
+ 'res-pois/lvl': 33,
+ 'abs-cold/lvl': 32,
+ 'abs-cold': 31,
+ 'abs-ltng/lvl': 30,
+ 'abs-ltng': 29,
+ 'abs-fire/lvl': 28,
+ 'abs-fire': 27,
+ 'abs-cold%': 25,
+ 'abs-ltng%': 24,
+ 'abs-fire%': 23,
+ 'red-dmg': 22,
+ 'dmag-ac': 22,
+ 'red-dmg%': 22,
+ dmag: 22,
+ 'red-mag': 21,
+ 'mag-ac': 21,
+ nofreeze: 20,
+ 'no-freeze': 20,
+ 'half-freeze': 19,
+ 'res-pois-len': 18,
+ reanimate: 17,
+ 'mana-kill': 16,
+ 'heal-kill': 16,
+ 'hp-kill': 16,
+ 'demon-heal': 15,
+ 'light-thorns': 14,
+ thorns: 13,
+ 'thorns/lvl': 12,
+ 'dmg-to-mana': 11,
+ addxp: 11,
+ 'gold%': 10,
+ 'gold%/lvl': 9,
+ 'mag%': 8,
+ cheap: 8,
+ 'mag%/lvl': 7,
+ light: 6,
+ stack: 4,
+ 'dur%': 3,
+ dur: 3,
+ 'rep-quant': 2,
+ 'rep-dur': 1,
+ charged: 1,
+ ease: 0,
+ sock: 0,
+ ethereal: 0,
+ bloody: 0,
+ fade: 0,
+ state: 0,
+}
+
+export function getPropertyPriority(code: string): number {
+ return PROP_DESC_PRIORITY[code.trim().toLowerCase()] ?? 10
+}
+
+const ALIAS_CODE_MAP: Record = {
+ swing1: 'ias',
+ swing2: 'ias',
+ swing3: 'ias',
+ cast1: 'fcr',
+ cast2: 'fcr',
+ cast3: 'fcr',
+ balance1: 'fhr',
+ balance2: 'fhr',
+ balance3: 'fhr',
+ move1: 'frw',
+ move2: 'frw',
+ move3: 'frw',
+ block1: 'fbr',
+ block2: 'fbr',
+ block3: 'fbr',
+ fireresist: 'res-fire',
+ coldresist: 'res-cold',
+ lightresist: 'res-ltng',
+ poisonresist: 'res-pois',
+ 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',
+}
+
+/**
+ * Aggregates and sorts item properties per Diablo II 1.13c tooltip rules (Issue #125):
+ * 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` when all 4 are positive
+ * 4. Merges 4 core attributes (`dgrp=1`) into `+X to All Attributes` when all 4 are positive
+ * 5. Accumulates identical scalar stats across prefixes, suffixes, and base modifiers
+ * 6. Sorts all resulting properties descending by `ItemStatCost.txt` `descpriority`
+ */
+export function aggregateAndSortProperties(rawProps: readonly RolledItemProp[]): RolledItemProp[] {
+ let fireMin = 0
+ let fireMax = 0
+ let ltngMin = 0
+ let ltngMax = 0
+ let coldMin = 0
+ let coldMax = 0
+ let coldLen = 0
+ let poisMin = 0
+ let poisMax = 0
+ let poisLen = 0
+ let physMin = 0
+ let physMax = 0
+
+ let fireRes = 0
+ let coldRes = 0
+ let ltngRes = 0
+ let poisRes = 0
+
+ let strVal = 0
+ let dexVal = 0
+ let vitVal = 0
+ let enrVal = 0
+
+ const otherMerged = new Map()
+ const unmergedProcs: RolledItemProp[] = []
+
+ const NON_ACCUMULATABLE = new Set([
+ 'hit-skill',
+ 'att-skill',
+ 'gethit-skill',
+ 'kill-skill',
+ 'death-skill',
+ 'levelup-skill',
+ 'charged',
+ 'skill-rand',
+ 'dmg-mag',
+ ])
+
+ for (const p of rawProps) {
+ const rawCode = (p.code || '').trim().toLowerCase()
+ if (!rawCode) continue
+ const code = ALIAS_CODE_MAP[rawCode] ?? rawCode
+ const val = p.value
+
+ // 1. Fire damage bucket
+ if (code === 'dmg-fire') {
+ fireMin += p.min > 0 ? p.min : val
+ fireMax += p.max > 0 ? p.max : val
+ continue
+ }
+ if (code === 'fire-min') {
+ fireMin += val
+ continue
+ }
+ if (code === 'fire-max') {
+ fireMax += val
+ continue
+ }
+
+ // 2. Lightning damage bucket
+ if (code === 'dmg-ltng') {
+ ltngMin += p.min > 0 ? p.min : val
+ ltngMax += p.max > 0 ? p.max : val
+ continue
+ }
+ if (code === 'ltng-min') {
+ ltngMin += val
+ continue
+ }
+ if (code === 'ltng-max') {
+ ltngMax += val
+ continue
+ }
+
+ // 3. Cold damage bucket
+ if (code === 'dmg-cold') {
+ coldMin += p.min > 0 ? p.min : val
+ coldMax += p.max > 0 ? p.max : val
+ coldLen += Number(p.param) || 0
+ continue
+ }
+ if (code === 'cold-min') {
+ coldMin += val
+ continue
+ }
+ if (code === 'cold-max') {
+ coldMax += val
+ continue
+ }
+ if (code === 'cold-len') {
+ coldLen += val
+ continue
+ }
+
+ // 4. Tri-element damage
+ if (code === 'dmg-elem') {
+ const eMin = p.min > 0 ? p.min : val
+ const eMax = p.max > 0 ? p.max : val
+ fireMin += eMin
+ fireMax += eMax
+ ltngMin += eMin
+ ltngMax += eMax
+ coldMin += eMin
+ coldMax += eMax
+ coldLen += Number(p.param) || 0
+ continue
+ }
+
+ // 5. Poison damage bucket
+ if (code === 'dmg-pois') {
+ poisMin += p.min > 0 ? p.min : val
+ poisMax += p.max > 0 ? p.max : val
+ poisLen += Number(p.param) || 75
+ continue
+ }
+ if (code === 'pois-min') {
+ poisMin += val
+ continue
+ }
+ if (code === 'pois-max') {
+ poisMax += val
+ continue
+ }
+ if (code === 'pois-len') {
+ poisLen += val
+ continue
+ }
+
+ // 6. Physical min/max damage bucket
+ if (code === 'dmg-norm') {
+ physMin += p.min > 0 ? p.min : val
+ physMax += p.max > 0 ? p.max : val
+ continue
+ }
+ if (code === 'dmg-min') {
+ physMin += val
+ continue
+ }
+ if (code === 'dmg-max') {
+ physMax += val
+ continue
+ }
+
+ // 7. Resistances bucket (dgrp=2)
+ if (code === 'res-all') {
+ fireRes += val
+ coldRes += val
+ ltngRes += val
+ poisRes += val
+ continue
+ }
+ if (code === 'res-fire') {
+ fireRes += val
+ continue
+ }
+ if (code === 'res-cold') {
+ coldRes += val
+ continue
+ }
+ if (code === 'res-ltng') {
+ ltngRes += val
+ continue
+ }
+ if (code === 'res-pois') {
+ poisRes += val
+ continue
+ }
+
+ // 8. Attributes bucket (dgrp=1)
+ if (code === 'all-stats') {
+ strVal += val
+ dexVal += val
+ vitVal += val
+ enrVal += val
+ continue
+ }
+ if (code === 'str') {
+ strVal += val
+ continue
+ }
+ if (code === 'dex') {
+ dexVal += val
+ continue
+ }
+ if (code === 'vit') {
+ vitVal += val
+ continue
+ }
+ if (code === 'enr') {
+ enrVal += val
+ continue
+ }
+
+ // 9. Procs / Charges / Ranges
+ if (NON_ACCUMULATABLE.has(code)) {
+ unmergedProcs.push({ ...p, code })
+ continue
+ }
+
+ // 10. General accumulatable scalar stats
+ const mergeKey = `${code}::${String(p.param ?? '')}`
+ const existing = otherMerged.get(mergeKey)
+ if (existing) {
+ otherMerged.set(mergeKey, {
+ code,
+ param: existing.param,
+ min: existing.min + p.min,
+ max: existing.max + p.max,
+ value: existing.value + val,
+ })
+ } else {
+ otherMerged.set(mergeKey, {
+ code,
+ param: p.param,
+ min: p.min,
+ max: p.max,
+ value: val,
+ })
+ }
+ }
+
+ const out: RolledItemProp[] = [...unmergedProcs, ...otherMerged.values()]
+
+ // Emit merged Physical Damage
+ if (physMin > 0 && physMax > 0) {
+ out.push({ code: 'dmg-norm', min: physMin, max: Math.max(physMin, physMax), value: physMin })
+ } else if (physMin > 0) {
+ out.push({ code: 'dmg-min', min: physMin, max: physMin, value: physMin })
+ } else if (physMax > 0) {
+ out.push({ code: 'dmg-max', min: physMax, max: physMax, value: physMax })
+ }
+
+ // Emit merged Fire Damage
+ if (fireMin > 0 && fireMax > 0) {
+ out.push({ code: 'dmg-fire', min: fireMin, max: Math.max(fireMin, fireMax), value: fireMin })
+ } else if (fireMin > 0) {
+ out.push({ code: 'fire-min', min: fireMin, max: fireMin, value: fireMin })
+ } else if (fireMax > 0) {
+ out.push({ code: 'fire-max', min: fireMax, max: fireMax, value: fireMax })
+ }
+
+ // Emit merged Lightning Damage
+ if (ltngMin > 0 && ltngMax > 0) {
+ out.push({ code: 'dmg-ltng', min: ltngMin, max: Math.max(ltngMin, ltngMax), value: ltngMin })
+ } else if (ltngMin > 0) {
+ out.push({ code: 'ltng-min', min: ltngMin, max: ltngMin, value: ltngMin })
+ } else if (ltngMax > 0) {
+ out.push({ code: 'ltng-max', min: ltngMax, max: ltngMax, value: ltngMax })
+ }
+
+ // Emit merged Cold Damage
+ if (coldMin > 0 && coldMax > 0) {
+ out.push({
+ code: 'dmg-cold',
+ param: coldLen || 50,
+ min: coldMin,
+ max: Math.max(coldMin, coldMax),
+ value: coldMin,
+ })
+ } else if (coldMin > 0) {
+ out.push({ code: 'cold-min', min: coldMin, max: coldMin, value: coldMin })
+ } else if (coldMax > 0) {
+ out.push({ code: 'cold-max', min: coldMax, max: coldMax, value: coldMax })
+ }
+
+ // Emit merged Poison Damage
+ if (poisMin > 0 || poisMax > 0) {
+ const pVal = poisMin > 0 ? poisMin : poisMax
+ const pLen = poisLen || 75
+ out.push({
+ code: 'dmg-pois',
+ param: pLen,
+ min: pVal,
+ max: pLen,
+ value: pVal,
+ })
+ }
+
+ // Emit merged Attributes (dgrp=1)
+ const commonAttr = Math.min(strVal, dexVal, vitVal, enrVal)
+ if (commonAttr > 0) {
+ out.push({ code: 'all-stats', min: commonAttr, max: commonAttr, value: commonAttr })
+ strVal -= commonAttr
+ dexVal -= commonAttr
+ vitVal -= commonAttr
+ enrVal -= commonAttr
+ }
+ if (strVal !== 0) out.push({ code: 'str', min: strVal, max: strVal, value: strVal })
+ if (dexVal !== 0) out.push({ code: 'dex', min: dexVal, max: dexVal, value: dexVal })
+ if (vitVal !== 0) out.push({ code: 'vit', min: vitVal, max: vitVal, value: vitVal })
+ if (enrVal !== 0) out.push({ code: 'enr', min: enrVal, max: enrVal, value: enrVal })
+
+ // Emit merged Resistances (dgrp=2)
+ const commonRes = Math.min(fireRes, coldRes, ltngRes, poisRes)
+ if (commonRes > 0) {
+ out.push({ code: 'res-all', min: commonRes, max: commonRes, value: commonRes })
+ fireRes -= commonRes
+ coldRes -= commonRes
+ ltngRes -= commonRes
+ poisRes -= commonRes
+ }
+ if (coldRes !== 0) out.push({ code: 'res-cold', min: coldRes, max: coldRes, value: coldRes })
+ if (ltngRes !== 0) out.push({ code: 'res-ltng', min: ltngRes, max: ltngRes, value: ltngRes })
+ if (fireRes !== 0) out.push({ code: 'res-fire', min: fireRes, max: fireRes, value: fireRes })
+ if (poisRes !== 0) out.push({ code: 'res-pois', min: poisRes, max: poisRes, value: poisRes })
+
+ // Sort descending by ItemStatCost.txt descpriority
+ 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(x => x.item)
+}
+
/**
* Generates formatted tooltip structure for any dropped Diablo II Item.
*/
@@ -968,27 +1562,28 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
}
}
- // Determine Quality Presentation & Modifiers
+ // Collect raw properties from the item across all quality tiers
+ const rawProps: RolledItemProp[] = []
+ let affixReqLevel = 0
+
if (quality === 'unique') {
qualityColor = '#c8a15a'
const uniqDef = item.uniqueItemDef as UniqueItem | undefined
if (uniqDef) {
title = uniqDef.index
subTitle = base.name
- // Add Unique Properties with concrete rolled values
+ if (uniqDef.levelreq > 0) affixReqLevel = Math.max(affixReqLevel, uniqDef.levelreq)
if (item.rolledProps && item.rolledProps.length > 0) {
- for (const prop of item.rolledProps) {
- lines.push({
- text: formatPropertyCode(prop.code, prop.value, prop.min, prop.max, prop.param),
- color: 'blue',
- })
- }
+ rawProps.push(...item.rolledProps)
} else {
for (const prop of uniqDef.props) {
if (prop.code) {
- lines.push({
- text: formatPropertyCode(prop.code, prop.min, prop.min, prop.max, prop.par),
- color: 'blue',
+ rawProps.push({
+ code: prop.code,
+ param: prop.par,
+ min: prop.min,
+ max: prop.max,
+ value: prop.min,
})
}
}
@@ -1002,19 +1597,18 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
if (setDef) {
title = setDef.index
subTitle = `${base.name} · [${setDef.set}]`
+ if (setDef.levelreq > 0) affixReqLevel = Math.max(affixReqLevel, setDef.levelreq)
if (item.rolledProps && item.rolledProps.length > 0) {
- for (const prop of item.rolledProps) {
- lines.push({
- text: formatPropertyCode(prop.code, prop.value, prop.min, prop.max, prop.param),
- color: 'blue',
- })
- }
+ rawProps.push(...item.rolledProps)
} else {
for (const prop of setDef.props) {
if (prop.code) {
- lines.push({
- text: formatPropertyCode(prop.code, prop.min, prop.min, prop.max, prop.par),
- color: 'blue',
+ rawProps.push({
+ code: prop.code,
+ param: prop.par,
+ min: prop.min,
+ max: prop.max,
+ value: prop.min,
})
}
}
@@ -1029,13 +1623,19 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
if (rareDef && rareDef.affixes) {
title = rareDef.name
for (const aff of rareDef.affixes) {
+ if (aff.levelreq > 0) affixReqLevel = Math.max(affixReqLevel, aff.levelreq)
for (const mod of aff.mods) {
- lines.push({
- text: formatPropertyCode(mod.code, mod.value, mod.min, mod.max, mod.param),
- color: 'blue',
+ rawProps.push({
+ code: mod.code,
+ param: mod.param,
+ min: mod.min,
+ max: mod.max,
+ value: mod.value,
})
}
}
+ } else if (item.rolledProps && item.rolledProps.length > 0) {
+ rawProps.push(...item.rolledProps)
}
} else if (quality === 'magic') {
qualityColor = '#4880ff'
@@ -1044,51 +1644,170 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
title = magicDef.name
subTitle = base.name
if (magicDef.prefix) {
+ if (magicDef.prefix.levelreq > 0) affixReqLevel = Math.max(affixReqLevel, magicDef.prefix.levelreq)
for (const m of magicDef.prefix.mods) {
- lines.push({
- text: formatPropertyCode(m.code, m.value, m.min, m.max, m.param),
- color: 'blue',
+ rawProps.push({
+ code: m.code,
+ param: m.param,
+ min: m.min,
+ max: m.max,
+ value: m.value,
})
}
}
if (magicDef.suffix) {
+ if (magicDef.suffix.levelreq > 0) affixReqLevel = Math.max(affixReqLevel, magicDef.suffix.levelreq)
for (const m of magicDef.suffix.mods) {
- lines.push({
- text: formatPropertyCode(m.code, m.value, m.min, m.max, m.param),
- color: 'blue',
+ rawProps.push({
+ code: m.code,
+ param: m.param,
+ min: m.min,
+ max: m.max,
+ value: m.value,
})
}
}
+ } else if (item.rolledProps && item.rolledProps.length > 0) {
+ subTitle = base.name
+ rawProps.push(...item.rolledProps)
} else {
subTitle = base.name
}
} else if (quality === 'superior') {
qualityColor = '#ffffff'
title = `Superior ${base.name}`
- lines.push({ text: '+15% Enhanced Defense', color: 'blue' })
+ if (base.kind === 'weapon') {
+ rawProps.push({ code: 'dmg%', min: 15, max: 15, value: 15 })
+ } else {
+ rawProps.push({ code: 'ac%', min: 15, max: 15, value: 15 })
+ }
} else if (quality === 'low') {
qualityColor = '#a0a0a0'
title = `Cracked ${base.name}`
} else {
qualityColor = '#e0d5c1'
title = base.name
+ if (item.rolledProps && item.rolledProps.length > 0) {
+ rawProps.push(...item.rolledProps)
+ }
}
- // Equipment Base Stats
- const reqLevel = (base as any).levelreq > 0 ? (base as any).levelreq : undefined
- const reqStr = (base as any).reqstr > 0 ? (base as any).reqstr : undefined
- const reqDex = (base as any).reqdex > 0 ? (base as any).reqdex : undefined
- const defense = base.defense > 0 ? (item.stats.defense ?? base.defense) : undefined
- const oneHandDamage = (base as any).mindam && (base as any).maxdam
- ? { min: (base as any).mindam, max: (base as any).maxdam }
- : undefined
- const twoHandDamage = (base as any).twoHandedMindam && (base as any).twoHandedMaxdam
- ? { min: (base as any).twoHandedMindam, max: (base as any).twoHandedMaxdam }
- : undefined
- const durability = item.durability && item.maxDurability
- ? { current: item.durability, max: item.maxDurability }
- : undefined
- const sockets = (base as any).gemsockets > 0 ? (base as any).gemsockets : undefined
+ // Merge and sort properties per Issue #125 (min/max damage pairs, dgrp resists/attributes, descpriority)
+ const mergedProps = aggregateAndSortProperties(rawProps)
+ for (const prop of mergedProps) {
+ lines.push({
+ text: formatPropertyCode(prop.code, prop.value, prop.min, prop.max, prop.param),
+ color: 'blue',
+ })
+ }
+
+ // Compute Enhanced Base Stats vs Original Base Stats per Issue #123
+ let totalEdPct = 0
+ let flatMinDmg = 0
+ let flatMaxDmg = 0
+ let totalAcPct = 0
+ let flatAc = 0
+ let totalEase = 0
+ let durBonus = 0
+ let durPct = 0
+ let rolledSockets = 0
+
+ for (const p of rawProps) {
+ const c = (p.code || '').trim().toLowerCase()
+ if (c === 'dmg%') totalEdPct += p.value
+ else if (c === 'dmg-min') flatMinDmg += p.value
+ else if (c === 'dmg-max') flatMaxDmg += p.value
+ else if (c === 'dmg-norm') {
+ flatMinDmg += p.min > 0 ? p.min : p.value
+ flatMaxDmg += p.max > 0 ? p.max : p.value
+ } else if (c === 'dmg') {
+ flatMinDmg += p.value
+ flatMaxDmg += p.value
+ } else if (c === 'ac%') totalAcPct += p.value
+ else if (c === 'ac') flatAc += p.value
+ else if (c === 'ease') totalEase += p.value
+ else if (c === 'dur') durBonus += p.value
+ else if (c === 'dur%') durPct += p.value
+ else if (c === 'sock') rolledSockets += p.value
+ }
+
+ // 1. Defense (Enhanced vs Base)
+ const rawBaseDef = base.defense > 0 ? base.defense : 0
+ let defense: number | undefined = undefined
+ let baseDefense: number | undefined = undefined
+ if (rawBaseDef > 0) {
+ const finalDef = Math.floor(rawBaseDef * (100 + totalAcPct) / 100) + flatAc
+ defense = finalDef
+ if (finalDef !== rawBaseDef) {
+ baseDefense = rawBaseDef
+ }
+ }
+
+ // 2. One-Hand & Two-Hand Damage (Enhanced vs Base)
+ const computeWeaponDamage = (baseMin: number, baseMax: number) => {
+ let finalMin = Math.floor(baseMin * (100 + totalEdPct) / 100) + flatMinDmg
+ let finalMax = Math.floor(baseMax * (100 + totalEdPct) / 100) + flatMaxDmg
+ if ((totalEdPct !== 0 || flatMinDmg !== 0 || flatMaxDmg !== 0) && finalMin >= finalMax) {
+ finalMax = finalMin + 1
+ }
+ const isEnhanced = finalMin !== baseMin || finalMax !== baseMax
+ return isEnhanced
+ ? { min: finalMin, max: finalMax, baseMin, baseMax, isEnhanced: true }
+ : { min: baseMin, max: baseMax }
+ }
+
+ const oneHandDamage =
+ (base as any).mindam && (base as any).maxdam
+ ? computeWeaponDamage((base as any).mindam, (base as any).maxdam)
+ : undefined
+
+ const twoHandDamage =
+ (base as any).twoHandedMindam && (base as any).twoHandedMaxdam
+ ? computeWeaponDamage((base as any).twoHandedMindam, (base as any).twoHandedMaxdam)
+ : undefined
+
+ // 3. Requirements (Required Strength, Required Dexterity, Required Level)
+ const easeSigned = totalEase > 0 ? -totalEase : totalEase
+ const rawBaseReqStr = (base as any).reqstr > 0 ? (base as any).reqstr : 0
+ let reqStr: number | undefined = undefined
+ let baseReqStr: number | undefined = undefined
+ if (rawBaseReqStr > 0) {
+ const finalReqStr = Math.max(0, Math.floor(rawBaseReqStr * (100 + easeSigned) / 100))
+ reqStr = finalReqStr
+ if (finalReqStr !== rawBaseReqStr) {
+ baseReqStr = rawBaseReqStr
+ }
+ }
+
+ const rawBaseReqDex = (base as any).reqdex > 0 ? (base as any).reqdex : 0
+ let reqDex: number | undefined = undefined
+ let baseReqDex: number | undefined = undefined
+ if (rawBaseReqDex > 0) {
+ const finalReqDex = Math.max(0, Math.floor(rawBaseReqDex * (100 + easeSigned) / 100))
+ reqDex = finalReqDex
+ if (finalReqDex !== rawBaseReqDex) {
+ baseReqDex = rawBaseReqDex
+ }
+ }
+
+ const rawBaseReqLevel = (base as any).levelreq > 0 ? (base as any).levelreq : 0
+ const finalReqLevel = Math.max(rawBaseReqLevel, affixReqLevel)
+ const reqLevel = finalReqLevel > 0 ? finalReqLevel : undefined
+ const baseReqLevel = rawBaseReqLevel > 0 && finalReqLevel !== rawBaseReqLevel ? rawBaseReqLevel : undefined
+
+ // 4. Durability
+ const rawBaseDur = (base as any).durability > 0 ? (base as any).durability : undefined
+ let durability: FormattedItemTooltip['durability'] = undefined
+ if (item.durability && item.maxDurability) {
+ const finalMaxDur = Math.floor(item.maxDurability * (100 + durPct) / 100) + durBonus
+ const finalCurDur = Math.floor(item.durability * (100 + durPct) / 100) + durBonus
+ const isEnhanced = rawBaseDur !== undefined && finalMaxDur !== rawBaseDur
+ durability = isEnhanced
+ ? { current: finalCurDur, max: finalMaxDur, baseMax: rawBaseDur, isEnhanced: true }
+ : { current: finalCurDur, max: finalMaxDur }
+ }
+
+ const sockets = rolledSockets > 0 ? rolledSockets : (base as any).gemsockets > 0 ? (base as any).gemsockets : undefined
const displayQuality =
quality === 'unique' ? 'Unique' :
@@ -1107,9 +1826,13 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
baseType: base.kind === 'weapon' ? 'Weapon' : base.kind === 'armor' ? 'Armor' : 'Misc',
ilvl,
reqLevel,
+ ...(baseReqLevel !== undefined ? { baseReqLevel } : {}),
reqStr,
+ ...(baseReqStr !== undefined ? { baseReqStr } : {}),
reqDex,
+ ...(baseReqDex !== undefined ? { baseReqDex } : {}),
defense,
+ ...(baseDefense !== undefined ? { baseDefense } : {}),
oneHandDamage,
twoHandDamage,
durability,
@@ -1117,3 +1840,4 @@ export function formatItemTooltip(item: Item): FormattedItemTooltip {
lines,
}
}
+
diff --git a/tests/item-tooltip-123-125.test.ts b/tests/item-tooltip-123-125.test.ts
new file mode 100644
index 0000000..fd046a0
--- /dev/null
+++ b/tests/item-tooltip-123-125.test.ts
@@ -0,0 +1,271 @@
+import { describe, expect, it } from 'vitest'
+import type { ItemBase } from '../src/game/items.ts'
+import { createDroppedItem } from '../src/game/drop-pipeline.ts'
+import { formatItemTooltip, aggregateAndSortProperties } from '../src/game/item-tooltip.ts'
+
+describe('Issue #123 & Issue #125: Enhanced vs Base Stats, Property Merging & Priority Sorting', () => {
+ it('Issue #123: calculates enhanced 1H/2H weapon damage, defense, requirements, and durability alongside original base stats', () => {
+ const weaponBase: ItemBase & Record = {
+ id: 'btx',
+ name: 'Battle Axe',
+ kind: 'weapon',
+ tags: ['axe', 'weap'],
+ damage: 24,
+ defense: 0,
+ value: 500,
+ invWidth: 2,
+ invHeight: 3,
+ maxStack: 1,
+ level: 25,
+ mindam: 6,
+ maxdam: 12,
+ twoHandedMindam: 18,
+ twoHandedMaxdam: 24,
+ reqstr: 100,
+ reqdex: 50,
+ levelreq: 20,
+ durability: 40,
+ }
+
+ // 100% ED + Requirements -20%
+ const droppedWeapon = createDroppedItem(weaponBase, 'unique', {
+ ilvl: 85,
+ dwInitSeed: 42,
+ uniqueItem: {
+ id: 1,
+ index: 'The Chieftain',
+ version: 100,
+ enabled: true,
+ ladder: false,
+ rarity: 1,
+ nolimit: false,
+ lvl: 29,
+ levelreq: 35,
+ code: 'btx',
+ carry1: false,
+ costMult: 5,
+ costAdd: 5000,
+ props: [
+ { code: 'dmg%', min: 100, max: 100 },
+ { code: 'ease', min: -20, max: -20 },
+ ],
+ },
+ })
+
+ const wTooltip = formatItemTooltip(droppedWeapon)
+
+ // Two-Hand Damage: 36 to 48 (18 to 24)
+ expect(wTooltip.twoHandDamage).toEqual({
+ min: 36,
+ max: 48,
+ baseMin: 18,
+ baseMax: 24,
+ isEnhanced: true,
+ })
+
+ // One-Hand Damage: 12 to 24 (6 to 12)
+ expect(wTooltip.oneHandDamage).toEqual({
+ min: 12,
+ max: 24,
+ baseMin: 6,
+ baseMax: 12,
+ isEnhanced: true,
+ })
+
+ // Requirements -20%: Str 100 -> 80 (100), Dex 50 -> 40 (50), Level 35 (20)
+ expect(wTooltip.reqStr).toBe(80)
+ expect(wTooltip.baseReqStr).toBe(100)
+ expect(wTooltip.reqDex).toBe(40)
+ expect(wTooltip.baseReqDex).toBe(50)
+ expect(wTooltip.reqLevel).toBe(35)
+ expect(wTooltip.baseReqLevel).toBe(20)
+
+ // Armor with +100% Enhanced Defense (Defense: 260 (130)) and 3x Rare Fallback Durability (258 of 258 (86))
+ const armorBase: ItemBase & Record = {
+ id: 'uit',
+ name: 'Monarch',
+ kind: 'armor',
+ tags: ['shie', 'armo'],
+ damage: 0,
+ defense: 130,
+ value: 1000,
+ invWidth: 2,
+ invHeight: 3,
+ maxStack: 1,
+ level: 72,
+ reqstr: 156,
+ levelreq: 54,
+ durability: 86,
+ }
+
+ const droppedArmor = createDroppedItem(armorBase, 'unique', {
+ ilvl: 85,
+ dwInitSeed: 99,
+ durabilityMultiplier: 3,
+ uniqueItem: {
+ id: 2,
+ index: 'Stormshield',
+ version: 100,
+ enabled: true,
+ ladder: false,
+ rarity: 1,
+ nolimit: false,
+ lvl: 77,
+ levelreq: 73,
+ code: 'uit',
+ carry1: false,
+ costMult: 5,
+ costAdd: 5000,
+ props: [
+ { code: 'ac%', min: 100, max: 100 },
+ { code: 'ease', min: -20, max: -20 },
+ ],
+ },
+ })
+
+ const aTooltip = formatItemTooltip(droppedArmor)
+ expect(aTooltip.defense).toBe(260)
+ expect(aTooltip.baseDefense).toBe(130)
+ expect(aTooltip.reqStr).toBe(124)
+ expect(aTooltip.baseReqStr).toBe(156)
+ expect(aTooltip.durability).toEqual({
+ current: 258,
+ max: 258,
+ baseMax: 86,
+ isEnhanced: true,
+ })
+ })
+
+ it('Issue #125: merges elemental and physical min/max damage pairs (e.g., fire-min +2 and fire-max +8 -> Adds 2-8 Fire Damage)', () => {
+ const merged = aggregateAndSortProperties([
+ { code: 'fire-min', min: 2, max: 2, value: 2 },
+ { code: 'fire-max', min: 8, max: 8, value: 8 },
+ { code: 'ltng-min', min: 1, max: 1, value: 1 },
+ { code: 'ltng-max', min: 20, max: 20, value: 20 },
+ { code: 'cold-min', min: 6, max: 6, value: 6 },
+ { code: 'cold-max', min: 14, max: 14, value: 14 },
+ { code: 'dmg-min', min: 5, max: 5, value: 5 },
+ { code: 'dmg-max', min: 10, max: 10, value: 10 },
+ ])
+
+ const base: ItemBase = {
+ id: 'rin',
+ name: 'Ring',
+ kind: 'armor',
+ tags: ['ring'],
+ damage: 0,
+ defense: 0,
+ value: 100,
+ invWidth: 1,
+ invHeight: 1,
+ maxStack: 1,
+ level: 1,
+ }
+
+ const item = createDroppedItem(base, 'normal', {
+ ilvl: 50,
+ dwInitSeed: 1,
+ })
+ ;(item as any).rolledProps = merged
+
+ const tooltip = formatItemTooltip(item)
+ const texts = tooltip.lines.map(l => l.text)
+
+ expect(texts).toContain('Adds 5-10 Damage')
+ expect(texts).toContain('Adds 2-8 Fire Damage')
+ expect(texts).toContain('Adds 1-20 Lightning Damage')
+ expect(texts).toContain('Adds 6-14 Cold Damage')
+ expect(texts).not.toContain('+2 to Minimum Fire Damage')
+ expect(texts).not.toContain('+8 to Maximum Fire Damage')
+ })
+
+ it('Issue #125: merges 4-element resistances (dgrp=2) into All Resistances and 4 attributes (dgrp=1) into All Attributes', () => {
+ const base: ItemBase = {
+ id: 'amu',
+ name: 'Amulet',
+ kind: 'armor',
+ tags: ['amul'],
+ damage: 0,
+ defense: 0,
+ value: 100,
+ invWidth: 1,
+ invHeight: 1,
+ maxStack: 1,
+ level: 1,
+ }
+
+ const item = createDroppedItem(base, 'normal', { ilvl: 80, dwInitSeed: 2 })
+ ;(item as any).rolledProps = [
+ { code: 'res-fire', min: 35, max: 35, value: 35 },
+ { code: 'res-cold', min: 15, max: 15, value: 15 },
+ { code: 'res-ltng', min: 15, max: 15, value: 15 },
+ { code: 'res-pois', min: 15, max: 15, value: 15 },
+ { code: 'str', min: 10, max: 10, value: 10 },
+ { code: 'dex', min: 10, max: 10, value: 10 },
+ { code: 'vit', min: 10, max: 10, value: 10 },
+ { code: 'enr', min: 10, max: 10, value: 10 },
+ ]
+
+ const tooltip = formatItemTooltip(item)
+ const texts = tooltip.lines.map(l => l.text)
+
+ // 4 attributes of +10 merge into "+10 to All Attributes"
+ // 4 resists of 15 + extra 20 fire merge into "All Resistances +15" and "Fire Resist +20%"
+ expect(texts).toEqual(['+10 to All Attributes', 'All Resistances +15', 'Fire Resist +20%'])
+ })
+
+ it('Issue #125: sorts properties in descending order of ItemStatCost.txt descpriority', () => {
+ const base: ItemBase = {
+ id: 'uap',
+ name: 'Shako',
+ kind: 'armor',
+ tags: ['helm', 'armo'],
+ damage: 0,
+ defense: 141,
+ value: 1000,
+ invWidth: 2,
+ invHeight: 2,
+ maxStack: 1,
+ level: 58,
+ }
+
+ const item = createDroppedItem(base, 'normal', { ilvl: 85, dwInitSeed: 3 })
+ // Deliberately provide properties in reverse/scrambled priority order
+ ;(item as any).rolledProps = [
+ { code: 'sock', min: 2, max: 2, value: 2 }, // prio 0
+ { code: 'rep-dur', param: 5, min: 0, max: 0, value: 0 }, // prio 1
+ { code: 'mag%', min: 50, max: 50, value: 50 }, // prio 8
+ { code: 'res-fire', min: 30, max: 30, value: 30 }, // prio 36
+ { code: 'mana', min: 40, max: 40, value: 40 }, // prio 55
+ { code: 'hp', min: 60, max: 60, value: 60 }, // prio 59
+ { code: 'str', min: 20, max: 20, value: 20 }, // prio 67
+ { code: 'ac%', min: 150, max: 150, value: 150 }, // prio 74
+ { code: 'lifesteal', min: 8, max: 8, value: 8 }, // prio 88
+ { code: 'dmg-fire', min: 10, max: 20, value: 10 }, // prio 102
+ { code: 'dmg%', min: 200, max: 200, value: 200 }, // prio 129
+ { code: 'swing2', min: 20, max: 20, value: 20 }, // prio 145
+ { code: 'allskills', min: 2, max: 2, value: 2 }, // prio 158
+ { code: 'hit-skill', param: 197, min: 5, max: 10, value: 5 }, // prio 160
+ ]
+
+ const tooltip = formatItemTooltip(item)
+ const texts = tooltip.lines.map(l => l.text)
+
+ expect(texts).toEqual([
+ '5% Chance to cast level 10 Firestorm on striking',
+ '+2 to All Skills',
+ '+20% Increased Attack Speed',
+ '+200% Enhanced Damage',
+ 'Adds 10-20 Fire Damage',
+ '8% Life stolen per hit',
+ '+150% Enhanced Defense',
+ '+20 to Strength',
+ '+60 to Life',
+ '+40 to Mana',
+ 'Fire Resist +30%',
+ '50% Better Chance of Getting Magic Items',
+ 'Repairs 1 durability in 20 seconds',
+ 'Socketed (2)',
+ ])
+ })
+})