feat(inventory): support ring, amulet, jewel, and charm VarInvGfx graphic variants (Fixes #375)

This commit is contained in:
troytt 2026-09-22 09:36:29 +00:00
parent 609149fa5f
commit 43748eac53
6 changed files with 1520 additions and 1158 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 391 KiB

After

Width:  |  Height:  |  Size: 397 KiB

File diff suppressed because it is too large Load Diff

View File

@ -472,6 +472,30 @@ async function main(): Promise<void> {
} }
} }
// Register authentic ring DC6 icons (invrin1..invrin5 per ItemTypes.txt VarInvGfx = 5)
for (let i = 1; i <= 5; i++) {
const ringInv = `invrin${i}`
if (archives.has(`data/global/items/${ringInv}.dc6`)) {
uniqueInvFiles.add(ringInv)
}
}
// Register authentic amulet DC6 icons (invamu1..invamu3 per ItemTypes.txt VarInvGfx = 3)
for (let i = 1; i <= 3; i++) {
const amuInv = `invamu${i}`
if (archives.has(`data/global/items/${amuInv}.dc6`)) {
uniqueInvFiles.add(amuInv)
}
}
// Register authentic jewel DC6 icons (invjw1..invjw6 per ItemTypes.txt VarInvGfx = 6)
for (let i = 1; i <= 6; i++) {
const jwInv = `invjw${i}`
if (archives.has(`data/global/items/${jwInv}.dc6`)) {
uniqueInvFiles.add(jwInv)
}
}
// Identity mapping for every invfile: codeToInvFile[inv] = inv // Identity mapping for every invfile: codeToInvFile[inv] = inv
for (const inv of uniqueInvFiles) { for (const inv of uniqueInvFiles) {
codeToInvFile[inv] = inv codeToInvFile[inv] = inv

File diff suppressed because it is too large Load Diff

View File

@ -103,6 +103,8 @@ export interface ResolvableItem {
readonly invFile?: string | undefined readonly invFile?: string | undefined
readonly invtransform?: string | undefined readonly invtransform?: string | undefined
readonly chrtransform?: string | undefined readonly chrtransform?: string | undefined
readonly variant?: number | undefined
readonly id?: string | undefined
readonly invWidth?: number | undefined readonly invWidth?: number | undefined
readonly invHeight?: number | undefined readonly invHeight?: number | undefined
readonly width?: number | undefined readonly width?: number | undefined
@ -125,6 +127,10 @@ export function resolveItemSpriteRect(
// Tier 0.5: Authentic Diablo II 1.13c Charm Resolution (cm1, cm2, cm3, Annihilus, Torch, Gheed's) // 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')) { if (item.code === 'cm1' || item.code === 'anni' || item.name?.includes('Annihilus')) {
const validSmall = ['invch1', 'invch4', 'invch7', 'invmss'] const validSmall = ['invch1', 'invch4', 'invch7', 'invmss']
if (item.variant !== undefined && !item.name?.includes('Annihilus') && item.code !== 'anni') {
const v = ['invch1', 'invch4', 'invch7'][Math.abs(Math.floor(item.variant)) % 3]
if (itemRects[v]) return itemRects[v]
}
const file = item.invFile?.toLowerCase() const file = item.invFile?.toLowerCase()
const direct = file && validSmall.includes(file) ? (itemRects[item.invFile!] ?? itemRects[file]) : undefined const direct = file && validSmall.includes(file) ? (itemRects[item.invFile!] ?? itemRects[file]) : undefined
if (item.code === 'anni' || item.name?.includes('Annihilus')) { if (item.code === 'anni' || item.name?.includes('Annihilus')) {
@ -134,6 +140,10 @@ export function resolveItemSpriteRect(
} }
if (item.code === 'cm2' || item.code === 'torch' || item.name?.includes('Torch')) { if (item.code === 'cm2' || item.code === 'torch' || item.name?.includes('Torch')) {
const validLarge = ['invch2', 'invch5', 'invch8', 'invtrch'] const validLarge = ['invch2', 'invch5', 'invch8', 'invtrch']
if (item.variant !== undefined && !item.name?.includes('Torch') && item.code !== 'torch') {
const v = ['invch2', 'invch5', 'invch8'][Math.abs(Math.floor(item.variant)) % 3]
if (itemRects[v]) return itemRects[v]
}
const file = item.invFile?.toLowerCase() const file = item.invFile?.toLowerCase()
const direct = file && validLarge.includes(file) ? (itemRects[item.invFile!] ?? itemRects[file]) : undefined const direct = file && validLarge.includes(file) ? (itemRects[item.invFile!] ?? itemRects[file]) : undefined
if (item.code === 'torch' || item.name?.includes('Torch')) { if (item.code === 'torch' || item.name?.includes('Torch')) {
@ -143,11 +153,85 @@ export function resolveItemSpriteRect(
} }
if (item.code === 'cm3' || item.code === 'gheeds' || item.name?.includes('Gheed')) { if (item.code === 'cm3' || item.code === 'gheeds' || item.name?.includes('Gheed')) {
const validGrand = ['invch3', 'invch6', 'invch9'] const validGrand = ['invch3', 'invch6', 'invch9']
if (item.variant !== undefined) {
const v = validGrand[Math.abs(Math.floor(item.variant)) % validGrand.length]
if (itemRects[v]) return itemRects[v]
}
const file = item.invFile?.toLowerCase() const file = item.invFile?.toLowerCase()
const direct = file && validGrand.includes(file) ? (itemRects[item.invFile!] ?? itemRects[file]) : undefined const direct = file && validGrand.includes(file) ? (itemRects[item.invFile!] ?? itemRects[file]) : undefined
return direct ?? itemRects['invch3'] ?? itemRects['invch6'] ?? itemRects['invch9'] ?? null return direct ?? itemRects['invch3'] ?? itemRects['invch6'] ?? itemRects['invch9'] ?? null
} }
// Tier 0.6: Authentic Diablo II 1.13c ItemTypes.txt VarInvGfx (Ring: 5, Amulet: 3, Jewel: 6)
if (item.code === 'rin' || item.code === 'ring' || (item.invFile && /^invrin\d?$/i.test(item.invFile))) {
const ringVariants = ['invrin1', 'invrin2', 'invrin3', 'invrin4', 'invrin5']
if (item.variant !== undefined) {
const idx = Math.abs(Math.floor(item.variant)) % ringVariants.length
const key = ringVariants[idx]
if (itemRects[key]) return itemRects[key]
}
if (item.invFile && item.invFile.toLowerCase() !== 'invrin' && itemRects[item.invFile]) {
return itemRects[item.invFile]
}
if (item.id) {
let hash = 0
for (let i = 0; i < item.id.length; i++) {
hash = ((hash << 5) - hash + item.id.charCodeAt(i)) | 0
}
const idx = Math.abs(hash) % ringVariants.length
const key = ringVariants[idx]
if (itemRects[key]) return itemRects[key]
}
return itemRects['invrin1'] ?? itemRects['invrin'] ?? null
}
if (item.code === 'amu' || item.code === 'amul' || (item.invFile && /^invamu\d?$/i.test(item.invFile))) {
const amuVariants = ['invamu1', 'invamu2', 'invamu3']
if (item.variant !== undefined) {
const idx = Math.abs(Math.floor(item.variant)) % amuVariants.length
const key = amuVariants[idx]
if (itemRects[key]) return itemRects[key]
}
if (item.invFile && item.invFile.toLowerCase() !== 'invamu' && itemRects[item.invFile]) {
return itemRects[item.invFile]
}
if (item.name && BAKED_UI_MANIFEST.codeToInvFile[item.name] && itemRects[BAKED_UI_MANIFEST.codeToInvFile[item.name]]) {
return itemRects[BAKED_UI_MANIFEST.codeToInvFile[item.name]]
}
if (item.id) {
let hash = 0
for (let i = 0; i < item.id.length; i++) {
hash = ((hash << 5) - hash + item.id.charCodeAt(i)) | 0
}
const idx = Math.abs(hash) % amuVariants.length
const key = amuVariants[idx]
if (itemRects[key]) return itemRects[key]
}
return itemRects['invamu1'] ?? itemRects['invamu'] ?? null
}
if (item.code === 'jew' || item.code === 'jewl' || (item.invFile && /^(invjw|invjew)\d?$/i.test(item.invFile))) {
const jewelVariants = ['invjw1', 'invjw2', 'invjw3', 'invjw4', 'invjw5', 'invjw6']
if (item.variant !== undefined) {
const idx = Math.abs(Math.floor(item.variant)) % jewelVariants.length
const key = jewelVariants[idx]
if (itemRects[key]) return itemRects[key]
}
if (item.invFile && itemRects[item.invFile]) {
return itemRects[item.invFile]
}
if (item.id) {
let hash = 0
for (let i = 0; i < item.id.length; i++) {
hash = ((hash << 5) - hash + item.id.charCodeAt(i)) | 0
}
const idx = Math.abs(hash) % jewelVariants.length
const key = jewelVariants[idx]
if (itemRects[key]) return itemRects[key]
}
return itemRects['invjw1'] ?? null
}
// Tier 1: Direct invFile (ignore misc.txt placeholder invfiles for charms) // Tier 1: Direct invFile (ignore misc.txt placeholder invfiles for charms)
const isCharmPlaceholder = const isCharmPlaceholder =
(item.code === 'cm3' && item.invFile?.toLowerCase() === 'invsst') || (item.code === 'cm3' && item.invFile?.toLowerCase() === 'invsst') ||

View File

@ -302,5 +302,75 @@ describe('Milestone M4 (Issue #149): Base item invfile audit & charm icons', ()
} }
} }
}) })
describe('Issue #375: ItemTypes.txt VarInvGfx variants for rings, amulets, jewels, and charms', () => {
it('bakes all ring (5), amulet (3), and jewel (6) graphic variants into itemRects', () => {
// Rings: invrin1 to invrin5
for (let i = 1; i <= 5; i++) {
const key = `invrin${i}`
expect(itemRects[key], `Expected ${key} in itemRects`).toBeDefined()
expect(itemRects[key]!.w).toBe(28)
expect(itemRects[key]!.h).toBe(28)
}
// Amulets: invamu1 to invamu3
for (let i = 1; i <= 3; i++) {
const key = `invamu${i}`
expect(itemRects[key], `Expected ${key} in itemRects`).toBeDefined()
expect(itemRects[key]!.w).toBe(28)
expect(itemRects[key]!.h).toBe(28)
}
// Jewels: invjw1 to invjw6
for (let i = 1; i <= 6; i++) {
const key = `invjw${i}`
expect(itemRects[key], `Expected ${key} in itemRects`).toBeDefined()
expect(itemRects[key]!.w).toBe(28)
expect(itemRects[key]!.h).toBe(28)
}
})
it('resolves all ring variants 0..4 correctly', () => {
for (let v = 0; v < 5; v++) {
const rect = resolveItemSpriteRect({ code: 'rin', variant: v }, itemRects)
expect(rect).toEqual(itemRects[`invrin${v + 1}`])
}
// Direct invFile
expect(resolveItemSpriteRect({ code: 'rin', invFile: 'invrin3' }, itemRects)).toEqual(itemRects['invrin3'])
})
it('resolves all amulet variants 0..2 correctly', () => {
for (let v = 0; v < 3; v++) {
const rect = resolveItemSpriteRect({ code: 'amu', variant: v }, itemRects)
expect(rect).toEqual(itemRects[`invamu${v + 1}`])
}
// Direct invFile
expect(resolveItemSpriteRect({ code: 'amu', invFile: 'invamu2' }, itemRects)).toEqual(itemRects['invamu2'])
})
it('resolves all jewel variants 0..5 correctly', () => {
for (let v = 0; v < 6; v++) {
const rect = resolveItemSpriteRect({ code: 'jew', variant: v }, itemRects)
expect(rect).toEqual(itemRects[`invjw${v + 1}`])
}
// Direct invFile
expect(resolveItemSpriteRect({ code: 'jew', invFile: 'invjw5' }, itemRects)).toEqual(itemRects['invjw5'])
})
it('resolves charm variants when variant property is specified', () => {
expect(resolveItemSpriteRect({ code: 'cm1', variant: 0 }, itemRects)).toEqual(itemRects['invch1'])
expect(resolveItemSpriteRect({ code: 'cm1', variant: 1 }, itemRects)).toEqual(itemRects['invch4'])
expect(resolveItemSpriteRect({ code: 'cm1', variant: 2 }, itemRects)).toEqual(itemRects['invch7'])
expect(resolveItemSpriteRect({ code: 'cm2', variant: 0 }, itemRects)).toEqual(itemRects['invch2'])
expect(resolveItemSpriteRect({ code: 'cm2', variant: 1 }, itemRects)).toEqual(itemRects['invch5'])
expect(resolveItemSpriteRect({ code: 'cm2', variant: 2 }, itemRects)).toEqual(itemRects['invch8'])
expect(resolveItemSpriteRect({ code: 'cm3', variant: 0 }, itemRects)).toEqual(itemRects['invch3'])
expect(resolveItemSpriteRect({ code: 'cm3', variant: 1 }, itemRects)).toEqual(itemRects['invch6'])
expect(resolveItemSpriteRect({ code: 'cm3', variant: 2 }, itemRects)).toEqual(itemRects['invch9'])
})
})
}) })
}) })