fix(inventory): align authentic 1.13c charm dimensions and graphic variants (Fixes #149)

This commit is contained in:
troytt 2026-09-22 04:48:38 +00:00
commit a46629cc11
5 changed files with 103 additions and 60 deletions

View File

@ -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",

View File

@ -463,10 +463,12 @@ async function main(): Promise<void> {
// 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')) {

View File

@ -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",

View File

@ -111,7 +111,7 @@ export interface ResolvableItem {
export function resolveItemSpriteRect(
item: ResolvableItem,
itemRects: Record<string, SpriteRect>,
itemRects: Record<string, SpriteRect> = 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)',

View File

@ -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)
}
}
})