fix(tooltip): 对齐1.13c dgrp全等合并规则,四抗与四维属性仅在全部相等时折叠显示 (fixes #130)
This commit is contained in:
parent
6a935cc91a
commit
041cd65ce0
|
|
@ -1181,6 +1181,7 @@ const ALIAS_CODE_MAP: Record<string, string> = {
|
||||||
coldresist: 'res-cold',
|
coldresist: 'res-cold',
|
||||||
lightresist: 'res-ltng',
|
lightresist: 'res-ltng',
|
||||||
poisonresist: 'res-pois',
|
poisonresist: 'res-pois',
|
||||||
|
'all-res': 'res-all',
|
||||||
dmag: 'red-dmg%',
|
dmag: 'red-dmg%',
|
||||||
'dmag-ac': 'red-dmg',
|
'dmag-ac': 'red-dmg',
|
||||||
'mag-ac': 'red-mag',
|
'mag-ac': 'red-mag',
|
||||||
|
|
@ -1194,11 +1195,11 @@ const ALIAS_CODE_MAP: Record<string, string> = {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.)
|
* 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`)
|
* 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
|
* 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` when all 4 are positive
|
* 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
|
* 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`
|
||||||
*/
|
*/
|
||||||
|
|
@ -1347,7 +1348,7 @@ export function aggregateAndSortProperties(rawProps: readonly RolledItemProp[]):
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7. Resistances bucket (dgrp=2)
|
// 7. Resistances bucket (dgrp=2)
|
||||||
if (code === 'res-all') {
|
if (code === 'res-all' || code === 'all-res') {
|
||||||
fireRes += val
|
fireRes += val
|
||||||
coldRes += val
|
coldRes += val
|
||||||
ltngRes += val
|
ltngRes += val
|
||||||
|
|
@ -1481,33 +1482,25 @@ export function aggregateAndSortProperties(rawProps: readonly RolledItemProp[]):
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emit merged Attributes (dgrp=1)
|
// Emit merged Attributes (dgrp=1): fold ONLY when all 4 attributes are positive and strictly equal
|
||||||
const commonAttr = Math.min(strVal, dexVal, vitVal, enrVal)
|
if (strVal > 0 && strVal === dexVal && dexVal === vitVal && vitVal === enrVal) {
|
||||||
if (commonAttr > 0) {
|
out.push({ code: 'all-stats', min: strVal, max: strVal, value: strVal })
|
||||||
out.push({ code: 'all-stats', min: commonAttr, max: commonAttr, value: commonAttr })
|
} else {
|
||||||
strVal -= commonAttr
|
if (strVal !== 0) out.push({ code: 'str', min: strVal, max: strVal, value: strVal })
|
||||||
dexVal -= commonAttr
|
if (dexVal !== 0) out.push({ code: 'dex', min: dexVal, max: dexVal, value: dexVal })
|
||||||
vitVal -= commonAttr
|
if (vitVal !== 0) out.push({ code: 'vit', min: vitVal, max: vitVal, value: vitVal })
|
||||||
enrVal -= commonAttr
|
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)
|
// Emit merged Resistances (dgrp=2): fold ONLY when all 4 resistances are positive and strictly equal
|
||||||
const commonRes = Math.min(fireRes, coldRes, ltngRes, poisRes)
|
if (fireRes > 0 && fireRes === coldRes && coldRes === ltngRes && ltngRes === poisRes) {
|
||||||
if (commonRes > 0) {
|
out.push({ code: 'res-all', min: fireRes, max: fireRes, value: fireRes })
|
||||||
out.push({ code: 'res-all', min: commonRes, max: commonRes, value: commonRes })
|
} else {
|
||||||
fireRes -= commonRes
|
if (coldRes !== 0) out.push({ code: 'res-cold', min: coldRes, max: coldRes, value: coldRes })
|
||||||
coldRes -= commonRes
|
if (ltngRes !== 0) out.push({ code: 'res-ltng', min: ltngRes, max: ltngRes, value: ltngRes })
|
||||||
ltngRes -= commonRes
|
if (fireRes !== 0) out.push({ code: 'res-fire', min: fireRes, max: fireRes, value: fireRes })
|
||||||
poisRes -= commonRes
|
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
|
// Sort descending by ItemStatCost.txt descpriority
|
||||||
return out
|
return out
|
||||||
|
|
|
||||||
|
|
@ -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')
|
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 = {
|
const base: ItemBase = {
|
||||||
id: 'amu',
|
id: 'amu',
|
||||||
name: 'Amulet',
|
name: 'Amulet',
|
||||||
|
|
@ -194,9 +194,10 @@ describe('Issue #123 & Issue #125: Enhanced vs Base Stats, Property Merging & Pr
|
||||||
level: 1,
|
level: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = createDroppedItem(base, 'normal', { ilvl: 80, dwInitSeed: 2 })
|
// Case 1: All 4 resistances equal (+15) and all 4 attributes equal (+10) -> fold into All Attributes / All Resistances
|
||||||
;(item as any).rolledProps = [
|
const equalItem = createDroppedItem(base, 'normal', { ilvl: 80, dwInitSeed: 2 })
|
||||||
{ code: 'res-fire', min: 35, max: 35, value: 35 },
|
;(equalItem as any).rolledProps = [
|
||||||
|
{ code: 'res-fire', min: 15, max: 15, value: 15 },
|
||||||
{ code: 'res-cold', 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-ltng', min: 15, max: 15, value: 15 },
|
||||||
{ code: 'res-pois', 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 },
|
{ code: 'enr', min: 10, max: 10, value: 10 },
|
||||||
]
|
]
|
||||||
|
|
||||||
const tooltip = formatItemTooltip(item)
|
const equalTooltip = formatItemTooltip(equalItem)
|
||||||
const texts = tooltip.lines.map(l => l.text)
|
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"
|
// 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
|
||||||
// 4 resists of 15 + extra 20 fire merge into "All Resistances +15" and "Fire Resist +20%"
|
const unequalItem = createDroppedItem(base, 'normal', { ilvl: 80, dwInitSeed: 22 })
|
||||||
expect(texts).toEqual(['+10 to All Attributes', 'All Resistances +15', 'Fire Resist +20%'])
|
;(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', () => {
|
it('Issue #125: sorts properties in descending order of ItemStatCost.txt descpriority', () => {
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@ import {
|
||||||
rollMagicAffixes,
|
rollMagicAffixes,
|
||||||
rollRareAffixes,
|
rollRareAffixes,
|
||||||
} from '../src/game/affix-generator.ts'
|
} 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 { D2Rng } from '../src/game/d2-rng.ts'
|
||||||
import type { MagicAffix, AffixTable } from '../src/game/affixes.ts'
|
import type { MagicAffix, AffixTable } from '../src/game/affixes.ts'
|
||||||
import type { RareNameTable } from '../src/game/rare-names.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',
|
||||||
|
])
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue