From 041cd65ce0aa02dcf43592c666713fa713848b4a Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Sat, 19 Sep 2026 10:32:47 +0000 Subject: [PATCH] =?UTF-8?q?fix(tooltip):=20=E5=AF=B9=E9=BD=901.13c=20dgrp?= =?UTF-8?q?=E5=85=A8=E7=AD=89=E5=90=88=E5=B9=B6=E8=A7=84=E5=88=99=EF=BC=8C?= =?UTF-8?q?=E5=9B=9B=E6=8A=97=E4=B8=8E=E5=9B=9B=E7=BB=B4=E5=B1=9E=E6=80=A7?= =?UTF-8?q?=E4=BB=85=E5=9C=A8=E5=85=A8=E9=83=A8=E7=9B=B8=E7=AD=89=E6=97=B6?= =?UTF-8?q?=E6=8A=98=E5=8F=A0=E6=98=BE=E7=A4=BA=20(fixes=20#130)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/game/item-tooltip.ts | 49 +++++++--------- tests/item-tooltip-123-125.test.ts | 40 ++++++++++--- tests/parity-1.13c.test.ts | 92 +++++++++++++++++++++++++++++- 3 files changed, 143 insertions(+), 38 deletions(-) diff --git a/src/game/item-tooltip.ts b/src/game/item-tooltip.ts index fe7c548..51cabd0 100644 --- a/src/game/item-tooltip.ts +++ b/src/game/item-tooltip.ts @@ -1181,6 +1181,7 @@ const ALIAS_CODE_MAP: Record = { coldresist: 'res-cold', lightresist: 'res-ltng', poisonresist: 'res-pois', + 'all-res': 'res-all', dmag: 'red-dmg%', 'dmag-ac': 'red-dmg', 'mag-ac': 'red-mag', @@ -1194,11 +1195,11 @@ const ALIAS_CODE_MAP: Record = { } /** - * Aggregates and sorts item properties per Diablo II 1.13c tooltip rules (Issue #125): + * Aggregates and sorts item properties per Diablo II 1.13c tooltip rules (Issue #125 & Issue #130): * 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 + * 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` */ @@ -1347,7 +1348,7 @@ export function aggregateAndSortProperties(rawProps: readonly RolledItemProp[]): } // 7. Resistances bucket (dgrp=2) - if (code === 'res-all') { + if (code === 'res-all' || code === 'all-res') { fireRes += val coldRes += val ltngRes += val @@ -1481,33 +1482,25 @@ export function aggregateAndSortProperties(rawProps: readonly RolledItemProp[]): }) } - // 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 + // Emit merged Attributes (dgrp=1): fold ONLY when all 4 attributes are positive and strictly equal + if (strVal > 0 && strVal === dexVal && dexVal === vitVal && vitVal === enrVal) { + out.push({ code: 'all-stats', min: strVal, max: strVal, value: strVal }) + } else { + 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 }) } - 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 + // Emit merged Resistances (dgrp=2): fold ONLY when all 4 resistances are positive and strictly equal + if (fireRes > 0 && fireRes === coldRes && coldRes === ltngRes && ltngRes === poisRes) { + out.push({ code: 'res-all', min: fireRes, max: fireRes, value: fireRes }) + } else { + 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 }) } - 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 diff --git a/tests/item-tooltip-123-125.test.ts b/tests/item-tooltip-123-125.test.ts index fd046a0..ba3bef8 100644 --- a/tests/item-tooltip-123-125.test.ts +++ b/tests/item-tooltip-123-125.test.ts @@ -179,7 +179,7 @@ describe('Issue #123 & Issue #125: Enhanced vs Base Stats, Property Merging & Pr 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', () => { + it('Issue #125 & #130: merges 4-element resistances (dgrp=2) into All Resistances and 4 attributes (dgrp=1) into All Attributes ONLY when all 4 are strictly equal', () => { const base: ItemBase = { id: 'amu', name: 'Amulet', @@ -194,9 +194,10 @@ describe('Issue #123 & Issue #125: Enhanced vs Base Stats, Property Merging & Pr level: 1, } - const item = createDroppedItem(base, 'normal', { ilvl: 80, dwInitSeed: 2 }) - ;(item as any).rolledProps = [ - { code: 'res-fire', min: 35, max: 35, value: 35 }, + // Case 1: All 4 resistances equal (+15) and all 4 attributes equal (+10) -> fold into All Attributes / All Resistances + const equalItem = createDroppedItem(base, 'normal', { ilvl: 80, dwInitSeed: 2 }) + ;(equalItem as any).rolledProps = [ + { code: 'res-fire', min: 15, max: 15, value: 15 }, { 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 }, @@ -206,12 +207,33 @@ describe('Issue #123 & Issue #125: Enhanced vs Base Stats, Property Merging & Pr { code: 'enr', min: 10, max: 10, value: 10 }, ] - const tooltip = formatItemTooltip(item) - const texts = tooltip.lines.map(l => l.text) + const equalTooltip = formatItemTooltip(equalItem) + expect(equalTooltip.lines.map(l => l.text)).toEqual([ + '+10 to All Attributes', + 'All Resistances +15', + ]) - // 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%']) + // Case 2: Unequal resistances (res-all: 15 + res-fire: 15 => fire=30, cold=15, ltng=15, pois=15) -> 4 independent lines, no partial All Resistances + const unequalItem = createDroppedItem(base, 'normal', { ilvl: 80, dwInitSeed: 22 }) + ;(unequalItem as any).rolledProps = [ + { code: 'res-all', min: 15, max: 15, value: 15 }, + { code: 'res-fire', 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 unequalTooltip = formatItemTooltip(unequalItem) + const unequalTexts = unequalTooltip.lines.map(l => l.text) + expect(unequalTexts).toEqual([ + '+10 to All Attributes', + 'Cold Resist +15%', + 'Lightning Resist +15%', + 'Fire Resist +30%', + 'Poison Resist +15%', + ]) + expect(unequalTexts).not.toContain('All Resistances +15') }) it('Issue #125: sorts properties in descending order of ItemStatCost.txt descpriority', () => { diff --git a/tests/parity-1.13c.test.ts b/tests/parity-1.13c.test.ts index 2565b2d..c4f1ba5 100644 --- a/tests/parity-1.13c.test.ts +++ b/tests/parity-1.13c.test.ts @@ -16,7 +16,7 @@ import { rollMagicAffixes, rollRareAffixes, } from '../src/game/affix-generator.ts' -import { formatItemTooltip } from '../src/game/item-tooltip.ts' +import { formatItemTooltip, aggregateAndSortProperties } from '../src/game/item-tooltip.ts' import { D2Rng } from '../src/game/d2-rng.ts' import type { MagicAffix, AffixTable } from '../src/game/affixes.ts' import type { RareNameTable } from '../src/game/rare-names.ts' @@ -736,3 +736,93 @@ describe('AutoMagic.txt & 1.13c StaffMods Drop Pipeline & Tooltip Integration (I }) }) +describe('dgrp Exact-Equality Aggregation Rule (Issue #130 / C2)', () => { + const amuletBase: ItemBase = { + id: 'amu', + name: 'Amulet', + kind: 'armor', + tags: ['amul'], + damage: 0, + defense: 0, + value: 100, + invWidth: 1, + invHeight: 1, + maxStack: 1, + level: 1, + } + + it('1. folds dgrp=2 into All Resistances +X ONLY when all 4 resistances are positive and strictly equal', () => { + // 4 equal individual resistances (+15 each) + const itemFromFour = createDroppedItem(amuletBase, 'normal', { ilvl: 80, dwInitSeed: 100 }) + ;(itemFromFour as any).rolledProps = [ + { code: 'res-fire', min: 15, max: 15, value: 15 }, + { 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 }, + ] + expect(formatItemTooltip(itemFromFour).lines.map(l => l.text)).toEqual(['All Resistances +15']) + + // Pure res-all (+15) + const itemFromResAll = createDroppedItem(amuletBase, 'normal', { ilvl: 80, dwInitSeed: 101 }) + ;(itemFromResAll as any).rolledProps = [{ code: 'res-all', min: 15, max: 15, value: 15 }] + expect(formatItemTooltip(itemFromResAll).lines.map(l => l.text)).toEqual(['All Resistances +15']) + }) + + it('2. unfolds dgrp=2 into 4 separate resistance lines when res-all stacks with a single elemental resistance (+15 res-all + +20 res-fire)', () => { + const item = createDroppedItem(amuletBase, 'normal', { ilvl: 80, dwInitSeed: 102 }) + ;(item as any).rolledProps = [ + { code: 'res-all', min: 15, max: 15, value: 15 }, + { code: 'res-fire', min: 20, max: 20, value: 20 }, + ] + + const texts = formatItemTooltip(item).lines.map(l => l.text) + expect(texts).toEqual([ + 'Cold Resist +15%', + 'Lightning Resist +15%', + 'Fire Resist +35%', + 'Poison Resist +15%', + ]) + expect(texts.some(t => t.startsWith('All Resistances'))).toBe(false) + }) + + it('3. unfolds dgrp=1 into 4 separate attribute lines when all-stats stacks with a single attribute (+10 all-stats + +15 str), and folds ONLY when all 4 are strictly equal', () => { + // Strictly equal (+10 to all 4) + const equalAttrItem = createDroppedItem(amuletBase, 'normal', { ilvl: 80, dwInitSeed: 103 }) + ;(equalAttrItem as any).rolledProps = [{ code: 'all-stats', min: 10, max: 10, value: 10 }] + expect(formatItemTooltip(equalAttrItem).lines.map(l => l.text)).toEqual(['+10 to All Attributes']) + + // Stacked (+10 all-stats + +15 str -> str=25, dex=10, vit=10, enr=10) + const unequalAttrItem = createDroppedItem(amuletBase, 'normal', { ilvl: 80, dwInitSeed: 104 }) + ;(unequalAttrItem as any).rolledProps = [ + { code: 'all-stats', min: 10, max: 10, value: 10 }, + { code: 'str', min: 15, max: 15, value: 15 }, + ] + const texts = formatItemTooltip(unequalAttrItem).lines.map(l => l.text) + expect(texts).toEqual([ + '+25 to Strength', + '+10 to Dexterity', + '+10 to Vitality', + '+10 to Energy', + ]) + expect(texts.some(t => t.includes('All Attributes'))).toBe(false) + + // Verify aggregateAndSortProperties never puts 'all-res' or 'all-stats' into otherMerged when unequal + const rawMerged = aggregateAndSortProperties([ + { code: 'all-res', min: 12, max: 12, value: 12 }, + { code: 'res-ltng', min: 18, max: 18, value: 18 }, + { code: 'all-stats', min: 5, max: 5, value: 5 }, + { code: 'dex', min: 7, max: 7, value: 7 }, + ]) + expect(rawMerged.map(p => `${p.code}:${p.value}`)).toEqual([ + 'str:5', + 'dex:12', + 'vit:5', + 'enr:5', + 'res-cold:12', + 'res-ltng:30', + 'res-fire:12', + 'res-pois:12', + ]) + }) +}) +