From 317b170a223f19d701fbe2e28cbe42be6f58c717 Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Tue, 22 Sep 2026 04:48:23 +0000 Subject: [PATCH] fix(inventory): align authentic 1.13c charm dimensions and graphic variants (Fixes #149) --- public/ui/manifest.json | 10 ++--- scripts/pack-ui.ts | 10 +++-- src/ui/baked-ui-meta.ts | 10 ++--- src/ui/inventory.ts | 50 ++++++++++++++++--------- tests/inventory.test.ts | 83 +++++++++++++++++++++++++++-------------- 5 files changed, 103 insertions(+), 60 deletions(-) diff --git a/public/ui/manifest.json b/public/ui/manifest.json index 32463db..d9b6239 100644 --- a/public/ui/manifest.json +++ b/public/ui/manifest.json @@ -611,8 +611,8 @@ "skz": "invskz", "hrb": "invhrb", "cm1": "invch1", - "cm2": "invch4", - "cm3": "invch7", + "cm2": "invch2", + "cm3": "invch3", "rps": "invrps", "rpl": "invrpl", "bps": "invbps", @@ -1199,11 +1199,11 @@ "invch7": "invch7", "invch8": "invch8", "invch9": "invch9", - "gheeds": "invch7", - "Gheed's Fortune": "invch7", + "gheeds": "invch3", + "Gheed's Fortune": "invch3", "anni": "invmss", - "trch": "invtrch", "torch": "invtrch", + "trch": "invtrch", "Harlequin Crest": "invcap_cgrn", "Tal Rasha's Guardianship": "invgth_dpur", "Tal Rasha's Howling Wind": "invgth_dpur", diff --git a/scripts/pack-ui.ts b/scripts/pack-ui.ts index 65d7d26..95a07d7 100644 --- a/scripts/pack-ui.ts +++ b/scripts/pack-ui.ts @@ -463,10 +463,12 @@ async function main(): Promise { // Authentic charm base item code mappings (overriding misc.txt placeholder invfiles invchm, invwnd, invsst) codeToInvFile['cm1'] = 'invch1' - codeToInvFile['cm2'] = 'invch4' - codeToInvFile['cm3'] = 'invch7' - codeToInvFile['gheeds'] = 'invch7' - codeToInvFile["Gheed's Fortune"] = 'invch7' + codeToInvFile['cm2'] = 'invch2' + codeToInvFile['cm3'] = 'invch3' + codeToInvFile['gheeds'] = 'invch3' + codeToInvFile["Gheed's Fortune"] = 'invch3' + codeToInvFile['anni'] = 'invmss' + codeToInvFile['torch'] = 'invtrch' // Explicitly guarantee Annihilus and Torch mappings if (archives.has('data/global/items/invmss.dc6')) { diff --git a/src/ui/baked-ui-meta.ts b/src/ui/baked-ui-meta.ts index 9e891e1..4f0f089 100644 --- a/src/ui/baked-ui-meta.ts +++ b/src/ui/baked-ui-meta.ts @@ -692,8 +692,8 @@ export const BAKED_UI_MANIFEST: BakedUiManifest = { "skz": "invskz", "hrb": "invhrb", "cm1": "invch1", - "cm2": "invch4", - "cm3": "invch7", + "cm2": "invch2", + "cm3": "invch3", "rps": "invrps", "rpl": "invrpl", "bps": "invbps", @@ -1280,11 +1280,11 @@ export const BAKED_UI_MANIFEST: BakedUiManifest = { "invch7": "invch7", "invch8": "invch8", "invch9": "invch9", - "gheeds": "invch7", - "Gheed's Fortune": "invch7", + "gheeds": "invch3", + "Gheed's Fortune": "invch3", "anni": "invmss", - "trch": "invtrch", "torch": "invtrch", + "trch": "invtrch", "Harlequin Crest": "invcap_cgrn", "Tal Rasha's Guardianship": "invgth_dpur", "Tal Rasha's Howling Wind": "invgth_dpur", diff --git a/src/ui/inventory.ts b/src/ui/inventory.ts index 4101f30..2e29d5a 100644 --- a/src/ui/inventory.ts +++ b/src/ui/inventory.ts @@ -111,7 +111,7 @@ export interface ResolvableItem { export function resolveItemSpriteRect( item: ResolvableItem, - itemRects: Record, + itemRects: Record = BAKED_UI_MANIFEST.itemRects, ): SpriteRect | null { // Tier 0: Transformed dyed item lookup: `${invFile}_${invtransform}` if (item.invFile && item.invtransform) { @@ -122,6 +122,32 @@ export function resolveItemSpriteRect( } } + // Tier 0.5: Authentic Diablo II 1.13c Charm Resolution (cm1, cm2, cm3, Annihilus, Torch, Gheed's) + if (item.code === 'cm1' || item.code === 'anni' || item.name?.includes('Annihilus')) { + const validSmall = ['invch1', 'invch4', 'invch7', 'invmss'] + const file = item.invFile?.toLowerCase() + const direct = file && validSmall.includes(file) ? (itemRects[item.invFile!] ?? itemRects[file]) : undefined + if (item.code === 'anni' || item.name?.includes('Annihilus')) { + return direct ?? itemRects['invmss'] ?? itemRects['invch1'] ?? null + } + return direct ?? itemRects['invch1'] ?? itemRects['invmss'] ?? null + } + if (item.code === 'cm2' || item.code === 'torch' || item.name?.includes('Torch')) { + const validLarge = ['invch2', 'invch5', 'invch8', 'invtrch'] + const file = item.invFile?.toLowerCase() + const direct = file && validLarge.includes(file) ? (itemRects[item.invFile!] ?? itemRects[file]) : undefined + if (item.code === 'torch' || item.name?.includes('Torch')) { + return direct ?? itemRects['invtrch'] ?? itemRects['invch2'] ?? null + } + return direct ?? itemRects['invch2'] ?? itemRects['invtrch'] ?? null + } + if (item.code === 'cm3' || item.code === 'gheeds' || item.name?.includes('Gheed')) { + const validGrand = ['invch3', 'invch6', 'invch9'] + const file = item.invFile?.toLowerCase() + const direct = file && validGrand.includes(file) ? (itemRects[item.invFile!] ?? itemRects[file]) : undefined + return direct ?? itemRects['invch3'] ?? itemRects['invch6'] ?? itemRects['invch9'] ?? null + } + // Tier 1: Direct invFile (ignore misc.txt placeholder invfiles for charms) const isCharmPlaceholder = (item.code === 'cm3' && item.invFile?.toLowerCase() === 'invsst') || @@ -150,18 +176,6 @@ export function resolveItemSpriteRect( if (item.code === 'tbk') return itemRects['invbbk'] ?? null if (item.code === 'ibk') return itemRects['invrbk'] ?? null if (item.code === 'box' || item.code === 'cube') return itemRects['invbox'] ?? null - if (item.code === 'cm1' || item.code === 'anni' || item.name?.includes('Annihilus')) { - const direct = item.invFile && item.invFile !== 'invchm' ? (itemRects[item.invFile] ?? itemRects[item.invFile.toLowerCase()]) : undefined - return direct ?? itemRects['invch1'] ?? itemRects['invmss'] ?? null - } - if (item.code === 'cm2' || item.code === 'torch' || item.name?.includes('Torch')) { - const direct = item.invFile && item.invFile !== 'invwnd' ? (itemRects[item.invFile] ?? itemRects[item.invFile.toLowerCase()]) : undefined - return direct ?? itemRects['invch4'] ?? itemRects['invtrch'] ?? null - } - if (item.code === 'cm3' || item.code === 'gheeds' || item.name?.includes('Gheed')) { - const direct = item.invFile && item.invFile !== 'invsst' ? (itemRects[item.invFile] ?? itemRects[item.invFile.toLowerCase()]) : undefined - return direct ?? itemRects['invch7'] ?? itemRects['invch8'] ?? itemRects['invch9'] ?? null - } if (item.code === 'crs' || item.code === 'cta' || item.name?.includes('Call to Arms')) { return itemRects['invcrs'] ?? null } @@ -169,14 +183,14 @@ export function resolveItemSpriteRect( // Tier 5: Dimension-based fallback guarantee (w x h in cells) const w = item.invWidth ?? (item as any).width ?? 1 const h = item.invHeight ?? (item as any).height ?? 1 - if (w === 1 && h === 1) return itemRects['invch1'] ?? itemRects['invchm'] ?? itemRects['invrin'] ?? itemRects['invcap'] ?? null - if (w === 1 && h === 2) return itemRects['invch4'] ?? itemRects['invtrch'] ?? itemRects['invbbk'] ?? itemRects['invglv'] ?? null + if (w === 1 && h === 1) return itemRects['invch1'] ?? itemRects['invmss'] ?? itemRects['invchm'] ?? itemRects['invrin'] ?? itemRects['invcap'] ?? null + if (w === 1 && h === 2) return itemRects['invch2'] ?? itemRects['invtrch'] ?? itemRects['invbbk'] ?? itemRects['invglv'] ?? null if (w === 1 && h === 3) { const isCharm = item.code?.startsWith('cm') || item.name?.toLowerCase().includes('charm') if (isCharm) { - return itemRects['invch7'] ?? itemRects['invch8'] ?? itemRects['invch9'] ?? null + return itemRects['invch3'] ?? itemRects['invch6'] ?? itemRects['invch9'] ?? null } - return itemRects['invch7'] ?? itemRects['invsst'] ?? itemRects['invclb'] ?? itemRects['invwnd'] ?? null + return itemRects['invsst'] ?? itemRects['invclb'] ?? itemRects['invwnd'] ?? itemRects['invch3'] ?? null } if (w === 1 && h === 4) return itemRects['invbst'] ?? itemRects['invstf'] ?? null if (w === 2 && h === 1) return itemRects['invvbl'] ?? itemRects['invlbl'] ?? null @@ -540,7 +554,7 @@ export const STARTER_BAG_ITEMS: readonly GridPlacement[] = [ item: { id: 'bag-gheeds', code: 'cm3', - invFile: 'invch7', + invFile: 'invch3', name: "Gheed's Fortune", nameZh: '基德的运气 (Gheed\'s Fortune)', baseNameZh: '超大型护身符 (Grand Charm)', diff --git a/tests/inventory.test.ts b/tests/inventory.test.ts index 9c77588..1cc5319 100644 --- a/tests/inventory.test.ts +++ b/tests/inventory.test.ts @@ -59,8 +59,8 @@ describe('Milestone M4 (Issue #149): Base item invfile audit & charm icons', () it('maps authentic charm item codes to charm DC6 icons in codeToInvFile', () => { // Base charm code mappings expect(codeToInv['cm1']).toBe('invch1') - expect(codeToInv['cm2']).toBe('invch4') - expect(codeToInv['cm3']).toBe('invch7') + expect(codeToInv['cm2']).toBe('invch2') + expect(codeToInv['cm3']).toBe('invch3') // Identity mappings for all 9 charm invfiles for (let i = 1; i <= 9; i++) { @@ -68,8 +68,8 @@ describe('Milestone M4 (Issue #149): Base item invfile audit & charm icons', () } // Unique charms - expect(codeToInv['gheeds']).toBe('invch7') - expect(codeToInv["Gheed's Fortune"]).toBe('invch7') + expect(codeToInv['gheeds']).toBe('invch3') + expect(codeToInv["Gheed's Fortune"]).toBe('invch3') expect(codeToInv['anni']).toBe('invmss') expect(codeToInv['Annihilus']).toBe('invmss') expect(codeToInv['torch']).toBe('invtrch') @@ -77,8 +77,8 @@ describe('Milestone M4 (Issue #149): Base item invfile audit & charm icons', () }) describe('resolveItemSpriteRect charm resolution', () => { - it('cm1 (Small Charm) resolves to 1x1 charm rect (invch1, invch2, invch3, or invmss)', () => { - const validSmallCharmKeys = ['invch1', 'invch2', 'invch3', 'invmss'] + it('cm1 (Small Charm) resolves to 1x1 charm rect (invch1, invch4, invch7, or invmss)', () => { + const validSmallCharmKeys = ['invch1', 'invch4', 'invch7', 'invmss'] const validSmallCharmRects = validSmallCharmKeys.map((k) => itemRects[k]).filter(Boolean) // Base item code cm1 @@ -86,10 +86,13 @@ describe('Milestone M4 (Issue #149): Base item invfile audit & charm icons', () expect(rectBase).not.toBeNull() expect(validSmallCharmRects).toContainEqual(rectBase) expect(rectBase).toEqual(itemRects['invch1']) + expect(rectBase!.w).toBe(28) + expect(rectBase!.h).toBe(28) // Item with name const rectName = resolveItemSpriteRect({ code: 'cm1', name: 'Small Charm' }, itemRects) expect(validSmallCharmRects).toContainEqual(rectName) + expect(rectName!.h).toBe(28) // Item with misc.txt placeholder invfile (invchm) const rectPlaceholder = resolveItemSpriteRect( @@ -98,28 +101,34 @@ describe('Milestone M4 (Issue #149): Base item invfile audit & charm icons', () ) expect(rectPlaceholder).not.toBeNull() expect(validSmallCharmRects).toContainEqual(rectPlaceholder) + expect(rectPlaceholder!.h).toBe(28) - // Specific variants invch2, invch3 - const rectVar2 = resolveItemSpriteRect({ code: 'cm1', invFile: 'invch2' }, itemRects) - expect(rectVar2).toEqual(itemRects['invch2']) + // Specific variants invch4, invch7 + const rectVar4 = resolveItemSpriteRect({ code: 'cm1', invFile: 'invch4' }, itemRects) + expect(rectVar4).toEqual(itemRects['invch4']) + expect(rectVar4!.h).toBe(28) - const rectVar3 = resolveItemSpriteRect({ code: 'cm1', invFile: 'invch3' }, itemRects) - expect(rectVar3).toEqual(itemRects['invch3']) + const rectVar7 = resolveItemSpriteRect({ code: 'cm1', invFile: 'invch7' }, itemRects) + expect(rectVar7).toEqual(itemRects['invch7']) + expect(rectVar7!.h).toBe(28) // Unique Annihilus const rectAnni = resolveItemSpriteRect({ code: 'anni', name: 'Annihilus' }, itemRects) expect(rectAnni).toEqual(itemRects['invmss']) + expect(rectAnni!.h).toBe(28) }) - it('cm2 (Large Charm) resolves to 1x2 charm rect (invch4, invch5, invch6, or invtrch)', () => { - const validLargeCharmKeys = ['invch4', 'invch5', 'invch6', 'invtrch'] + it('cm2 (Large Charm) resolves to 1x2 charm rect (invch2, invch5, invch8, or invtrch)', () => { + const validLargeCharmKeys = ['invch2', 'invch5', 'invch8', 'invtrch'] const validLargeCharmRects = validLargeCharmKeys.map((k) => itemRects[k]).filter(Boolean) // Base item code cm2 const rectBase = resolveItemSpriteRect({ code: 'cm2' }, itemRects) expect(rectBase).not.toBeNull() expect(validLargeCharmRects).toContainEqual(rectBase) - expect(rectBase).toEqual(itemRects['invch4']) + expect(rectBase).toEqual(itemRects['invch2']) + expect(rectBase!.w).toBe(28) + expect(rectBase!.h).toBe(56) // Item with misc.txt placeholder invfile (invwnd - Wand) const rectPlaceholder = resolveItemSpriteRect( @@ -129,29 +138,35 @@ describe('Milestone M4 (Issue #149): Base item invfile audit & charm icons', () expect(rectPlaceholder).not.toBeNull() expect(validLargeCharmRects).toContainEqual(rectPlaceholder) expect(rectPlaceholder).not.toEqual(itemRects['invwnd']) + expect(rectPlaceholder!.h).toBe(56) - // Specific variants invch5, invch6 + // Specific variants invch5, invch8 const rectVar5 = resolveItemSpriteRect({ code: 'cm2', invFile: 'invch5' }, itemRects) expect(rectVar5).toEqual(itemRects['invch5']) + expect(rectVar5!.h).toBe(56) - const rectVar6 = resolveItemSpriteRect({ code: 'cm2', invFile: 'invch6' }, itemRects) - expect(rectVar6).toEqual(itemRects['invch6']) + const rectVar8 = resolveItemSpriteRect({ code: 'cm2', invFile: 'invch8' }, itemRects) + expect(rectVar8).toEqual(itemRects['invch8']) + expect(rectVar8!.h).toBe(56) // Unique Hellfire Torch const rectTorch = resolveItemSpriteRect({ code: 'torch', name: 'Hellfire Torch' }, itemRects) expect(rectTorch).toEqual(itemRects['invtrch']) + expect(rectTorch!.h).toBe(56) }) - it('cm3 (Grand Charm / Gheed) resolves to 1x3 charm rect (invch7, invch8, invch9), and NOT invsst', () => { - const validGrandCharmKeys = ['invch7', 'invch8', 'invch9'] + it('cm3 (Grand Charm / Gheed) resolves to 1x3 charm rect (invch3, invch6, invch9), and NOT invsst', () => { + const validGrandCharmKeys = ['invch3', 'invch6', 'invch9'] const validGrandCharmRects = validGrandCharmKeys.map((k) => itemRects[k]).filter(Boolean) // Base item code cm3 const rectBase = resolveItemSpriteRect({ code: 'cm3' }, itemRects) expect(rectBase).not.toBeNull() expect(validGrandCharmRects).toContainEqual(rectBase) - expect(rectBase).toEqual(itemRects['invch7']) + expect(rectBase).toEqual(itemRects['invch3']) expect(rectBase).not.toEqual(itemRects['invsst']) + expect(rectBase!.w).toBe(28) + expect(rectBase!.h).toBe(84) // Item with misc.txt placeholder invfile (invsst - Short Staff) const rectPlaceholder = resolveItemSpriteRect( @@ -161,45 +176,54 @@ describe('Milestone M4 (Issue #149): Base item invfile audit & charm icons', () expect(rectPlaceholder).not.toBeNull() expect(validGrandCharmRects).toContainEqual(rectPlaceholder) expect(rectPlaceholder).not.toEqual(itemRects['invsst']) + expect(rectPlaceholder!.h).toBe(84) - // Specific variants invch8, invch9 - const rectVar8 = resolveItemSpriteRect({ code: 'cm3', invFile: 'invch8' }, itemRects) - expect(rectVar8).toEqual(itemRects['invch8']) + // Specific variants invch6, invch9 + const rectVar6 = resolveItemSpriteRect({ code: 'cm3', invFile: 'invch6' }, itemRects) + expect(rectVar6).toEqual(itemRects['invch6']) + expect(rectVar6!.h).toBe(84) const rectVar9 = resolveItemSpriteRect({ code: 'cm3', invFile: 'invch9' }, itemRects) expect(rectVar9).toEqual(itemRects['invch9']) + expect(rectVar9!.h).toBe(84) // Unique Gheed's Fortune const rectGheeds = resolveItemSpriteRect({ code: 'cm3', name: "Gheed's Fortune" }, itemRects) expect(rectGheeds).not.toBeNull() expect(validGrandCharmRects).toContainEqual(rectGheeds) - expect(rectGheeds).toEqual(itemRects['invch7']) + expect(rectGheeds).toEqual(itemRects['invch3']) expect(rectGheeds).not.toEqual(itemRects['invsst']) + expect(rectGheeds!.h).toBe(84) const rectGheedsAlias = resolveItemSpriteRect({ code: 'gheeds' }, itemRects) expect(rectGheedsAlias).not.toBeNull() expect(validGrandCharmRects).toContainEqual(rectGheedsAlias) + expect(rectGheedsAlias).toEqual(itemRects['invch3']) expect(rectGheedsAlias).not.toEqual(itemRects['invsst']) + expect(rectGheedsAlias!.h).toBe(84) }) it('enforces Tier 5 dimension fallback for 1x3 items to avoid rendering as wooden staff', () => { // 1x3 item with charm name or cm code const fallbackCharm = { name: 'Unknown Grand Charm', invWidth: 1, invHeight: 3 } const rectCharm = resolveItemSpriteRect(fallbackCharm, itemRects) - expect(rectCharm).toEqual(itemRects['invch7']) + expect(rectCharm).toEqual(itemRects['invch3']) expect(rectCharm).not.toEqual(itemRects['invsst']) + expect(rectCharm!.h).toBe(84) // 1x3 item with cm3 code and missing invfile const fallbackCm3 = { code: 'cm3', invWidth: 1, invHeight: 3 } const rectCm3 = resolveItemSpriteRect(fallbackCm3, itemRects) - expect(rectCm3).toEqual(itemRects['invch7']) + expect(rectCm3).toEqual(itemRects['invch3']) expect(rectCm3).not.toEqual(itemRects['invsst']) + expect(rectCm3!.h).toBe(84) // Generic 1x3 item fallback const generic1x3 = { invWidth: 1, invHeight: 3 } const rect1x3 = resolveItemSpriteRect(generic1x3, itemRects) expect(rect1x3).not.toBeNull() - expect(rect1x3).toEqual(itemRects['invch7']) + expect(rect1x3).toEqual(itemRects['invsst']) + expect(rect1x3!.h).toBe(84) }) }) @@ -233,12 +257,15 @@ describe('Milestone M4 (Issue #149): Base item invfile audit & charm icons', () // Specifically check charms in bag if (item.id === 'bag-gheeds') { - expect(rect).toEqual(itemRects['invch7']) + expect(rect).toEqual(itemRects['invch3']) expect(rect).not.toEqual(itemRects['invsst']) + expect(rect!.h).toBe(84) } else if (item.id === 'bag-torch') { expect(rect).toEqual(itemRects['invtrch']) + expect(rect!.h).toBe(56) } else if (item.id === 'bag-anni') { expect(rect).toEqual(itemRects['invmss']) + expect(rect!.h).toBe(28) } } })