test(inventory): track challenger M4 inventory audit suite

This commit is contained in:
troytt 2026-09-22 05:02:56 +00:00
parent a46629cc11
commit e7d9070bb8
1 changed files with 276 additions and 0 deletions

View File

@ -0,0 +1,276 @@
/**
* tests/challenger-m4-inventory-audit.test.ts
*
* EMPIRICAL CHALLENGER ADVERSARIAL TEST HARNESS
* Milestone M4 (Issue #149): Complete base item atlas resolution, bounding limits,
* transformed item dyes, authentic charm aspect ratios, and fallback guarantees.
*/
import { describe, expect, it } from 'vitest'
import { join } from 'node:path'
import { existsSync } from 'node:fs'
import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts'
import {
resolveItemSpriteRect,
STARTER_BAG_ITEMS,
STARTER_EQUIPPED_GEAR,
} from '../src/ui/inventory.ts'
import { MountedArchives } from '../src/mpq/mount.ts'
import { MpqArchive } from '../src/mpq/archive.ts'
import { fileSource } from '../src/mpq/file-source.ts'
import { parseTable } from '../src/game/tables.ts'
describe('Challenger M4 Inventory & Item Atlas Audit (Issue #149)', () => {
const itemRects = BAKED_UI_MANIFEST.itemRects
const atlasWidth = BAKED_UI_MANIFEST.atlasWidth
const atlasHeight = BAKED_UI_MANIFEST.atlasHeight
describe('Objective 1: Atlas Bounding Limits & Integrity', () => {
it('contains at least 399 sprite rects in BAKED_UI_MANIFEST.itemRects', () => {
const keys = Object.keys(itemRects)
expect(keys.length).toBeGreaterThanOrEqual(399)
})
it('enforces that every sprite rect in itemRects is strictly within atlas bounds', () => {
expect(atlasWidth).toBe(1024)
expect(atlasHeight).toBeGreaterThanOrEqual(1024)
for (const [key, rect] of Object.entries(itemRects)) {
expect(rect, `Sprite ${key} must be defined`).toBeDefined()
expect(rect.w, `Sprite ${key} width must be > 0`).toBeGreaterThan(0)
expect(rect.h, `Sprite ${key} height must be > 0`).toBeGreaterThan(0)
expect(rect.x, `Sprite ${key} x must be >= 0`).toBeGreaterThanOrEqual(0)
expect(rect.y, `Sprite ${key} y must be >= 0`).toBeGreaterThanOrEqual(0)
// Strict boundary containment
const right = rect.x + rect.w
const bottom = rect.y + rect.h
expect(
right,
`Sprite ${key} exceeds atlas width: right edge ${right} > atlasWidth ${atlasWidth}`,
).toBeLessThanOrEqual(atlasWidth)
expect(
bottom,
`Sprite ${key} exceeds atlas height: bottom edge ${bottom} > atlasHeight ${atlasHeight}`,
).toBeLessThanOrEqual(atlasHeight)
}
})
it('enforces that questRects are strictly within questsAtlas bounds', () => {
const questRects = BAKED_UI_MANIFEST.questRects
const qW = BAKED_UI_MANIFEST.questsAtlasWidth
const qH = BAKED_UI_MANIFEST.questsAtlasHeight
expect(Object.keys(questRects).length).toBeGreaterThan(0)
for (const [key, rect] of Object.entries(questRects)) {
expect(rect.w).toBeGreaterThan(0)
expect(rect.h).toBeGreaterThan(0)
expect(rect.x).toBeGreaterThanOrEqual(0)
expect(rect.y).toBeGreaterThanOrEqual(0)
expect(rect.x + rect.w).toBeLessThanOrEqual(qW)
expect(rect.y + rect.h).toBeLessThanOrEqual(qH)
}
})
})
describe('Objective 2: Transformed Items (Dyes) Existence & Bounding', () => {
const transformedKeys = [
'invcap_cgrn', // Emerald Harlequin Crest Shako
'invgth_dpur', // Dark Purple Gothic Plate
'invtgl_lgry', // Light Grey Heavy Gloves
'invvbl_blac', // Black Vampirefang Belt
'invlbl_blac', // Black Light Belt
]
it.each(transformedKeys)('transformed item sprite "%s" exists with non-zero dimensions inside atlas', (key) => {
const rect = itemRects[key]
expect(rect, `Transformed sprite "${key}" must exist in itemRects`).toBeDefined()
expect(rect!.w, `Transformed sprite "${key}" width must be > 0`).toBeGreaterThan(0)
expect(rect!.h, `Transformed sprite "${key}" height must be > 0`).toBeGreaterThan(0)
expect(rect!.x + rect!.w).toBeLessThanOrEqual(atlasWidth)
expect(rect!.y + rect!.h).toBeLessThanOrEqual(atlasHeight)
})
it('resolves transformed dyed items via resolveItemSpriteRect with case-insensitivity', () => {
const shako = resolveItemSpriteRect({ invFile: 'invcap', invtransform: 'cgrn' }, itemRects)
expect(shako).toEqual(itemRects['invcap_cgrn'])
const shakoUpper = resolveItemSpriteRect({ invFile: 'INVCAP', invtransform: 'CGRN' }, itemRects)
expect(shakoUpper).toEqual(itemRects['invcap_cgrn'])
const gothic = resolveItemSpriteRect({ invFile: 'invgth', invtransform: 'dpur' }, itemRects)
expect(gothic).toEqual(itemRects['invgth_dpur'])
const gloves = resolveItemSpriteRect({ invFile: 'invtgl', invtransform: 'lgry' }, itemRects)
expect(gloves).toEqual(itemRects['invtgl_lgry'])
const beltV = resolveItemSpriteRect({ invFile: 'invvbl', invtransform: 'blac' }, itemRects)
expect(beltV).toEqual(itemRects['invvbl_blac'])
const beltL = resolveItemSpriteRect({ invFile: 'invlbl', invtransform: 'blac' }, itemRects)
expect(beltL).toEqual(itemRects['invlbl_blac'])
})
})
describe('Objective 3: 100+ Base Items from weapons.txt, armor.txt, and misc.txt', () => {
it('empirically audits 100+ base items from 1.13c MPQ data tables', async () => {
const d2Path = join(process.cwd(), 'samples', 'd2')
if (!existsSync(join(d2Path, 'Patch_D2.mpq'))) {
throw new Error('Authoritative MPQ directory samples/d2 is missing or unmounted')
}
const archives = new MountedArchives()
for (const m of ['d2char.mpq', 'd2data.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
archives.add(m, await MpqArchive.open(await fileSource(join(d2Path, m))))
}
let totalBaseItemsAudited = 0
const auditedCodes = new Set<string>()
for (const tblFile of ['weapons.txt', 'armor.txt', 'misc.txt']) {
const data = await archives.read(`data/global/excel/${tblFile}`)
const tbl = parseTable(new TextDecoder().decode(data))
for (const row of tbl.rows) {
const code = row['code']?.trim()
if (!code || auditedCodes.has(code)) continue
auditedCodes.add(code)
const invfile = row['invfile']?.trim()
const name = row['name']?.trim()
// 1. Resolve with full item info
const rectFull = resolveItemSpriteRect({ code, invFile: invfile, name }, itemRects)
expect(
rectFull,
`Base item code="${code}" (invFile="${invfile}", name="${name}") from ${tblFile} must resolve`,
).not.toBeNull()
expect(rectFull!.w).toBeGreaterThan(0)
expect(rectFull!.h).toBeGreaterThan(0)
expect(rectFull!.x + rectFull!.w).toBeLessThanOrEqual(atlasWidth)
expect(rectFull!.y + rectFull!.h).toBeLessThanOrEqual(atlasHeight)
// 2. Resolve by code alone (Tier 3 lookup in codeToInvFile)
const rectByCode = resolveItemSpriteRect({ code }, itemRects)
expect(
rectByCode,
`Base item code="${code}" must resolve without invFile`,
).not.toBeNull()
// 3. Strict charm aspect ratio and ground truth assertions
if (code === 'cm1') {
expect(rectFull!.w).toBe(28)
expect(rectFull!.h).toBe(28)
expect(rectByCode!.h).toBe(28)
} else if (code === 'cm2') {
expect(rectFull!.w).toBe(28)
expect(rectFull!.h).toBe(56)
expect(rectByCode!.h).toBe(56)
expect(rectFull).not.toEqual(itemRects['invwnd'])
} else if (code === 'cm3') {
expect(rectFull!.w).toBe(28)
expect(rectFull!.h).toBe(84)
expect(rectByCode!.h).toBe(84)
expect(rectFull).not.toEqual(itemRects['invsst'])
}
totalBaseItemsAudited++
}
}
console.log(`Empirically audited ${totalBaseItemsAudited} unique base items from MPQ data tables.`)
expect(totalBaseItemsAudited).toBeGreaterThanOrEqual(100)
})
})
describe('Objective 4: Dimension Fallbacks Stress Testing', () => {
it('w=1, h=2 resolves to 56px height', () => {
const item1x2 = resolveItemSpriteRect({ invWidth: 1, invHeight: 2 }, itemRects)
expect(item1x2).not.toBeNull()
expect(item1x2!.h).toBe(56)
expect(item1x2!.w).toBe(28)
})
it('w=1, h=3 resolves to 84px height', () => {
// Non-charm generic 1x3
const generic1x3 = resolveItemSpriteRect({ invWidth: 1, invHeight: 3 }, itemRects)
expect(generic1x3).not.toBeNull()
expect(generic1x3!.h).toBe(84)
expect(generic1x3!.w).toBe(28)
// Charm-designated 1x3
const charm1x3 = resolveItemSpriteRect({ name: 'Mystic Grand Charm', invWidth: 1, invHeight: 3 }, itemRects)
expect(charm1x3).not.toBeNull()
expect(charm1x3!.h).toBe(84)
expect(charm1x3!.w).toBe(28)
expect(charm1x3).toEqual(itemRects['invch3'])
})
it('exhaustively verifies all standard grid dimension fallbacks', () => {
const testCases = [
{ w: 1, h: 1, expectedH: 28 },
{ w: 1, h: 2, expectedH: 56 },
{ w: 1, h: 3, expectedH: 84 },
{ w: 1, h: 4, expectedH: 112 },
{ w: 2, h: 1, expectedH: 28 },
{ w: 2, h: 2, expectedH: 56 },
{ w: 2, h: 3, expectedH: 84 },
{ w: 2, h: 4, expectedH: 112 },
]
for (const { w, h, expectedH } of testCases) {
const res = resolveItemSpriteRect({ invWidth: w, invHeight: h }, itemRects)
expect(res, `Fallback for w=${w}, h=${h} must resolve`).not.toBeNull()
expect(res!.h, `Fallback for w=${w}, h=${h} must match height ${expectedH}`).toBe(expectedH)
}
})
})
describe('Adversarial & Boundary Edge Cases', () => {
it('handles empty item object safely without crashing', () => {
const empty = resolveItemSpriteRect({}, itemRects)
expect(empty).not.toBeNull()
expect(empty!.w).toBeGreaterThan(0)
expect(empty!.h).toBeGreaterThan(0)
})
it('handles unmapped item with unknown code but valid dimensions', () => {
const unknownItem = resolveItemSpriteRect(
{ code: 'nonexistent_code_9999', invWidth: 1, invHeight: 2 },
itemRects,
)
expect(unknownItem).not.toBeNull()
expect(unknownItem!.h).toBe(56)
})
it('handles adversarial keys without prototype pollution or exceptions', () => {
const badKeys = ['__proto__', 'constructor', 'toString', 'valueOf', '<script>', "'; DROP TABLE;"]
for (const bad of badKeys) {
expect(() => resolveItemSpriteRect({ code: bad }, itemRects)).not.toThrow()
expect(() => resolveItemSpriteRect({ invFile: bad }, itemRects)).not.toThrow()
expect(() => resolveItemSpriteRect({ invtransform: bad }, itemRects)).not.toThrow()
}
})
it('confirms starter inventory and equipped gear resolution integrity', () => {
for (const [slot, item] of Object.entries(STARTER_EQUIPPED_GEAR)) {
if (!item) continue
const rect = resolveItemSpriteRect(item, itemRects)
expect(rect, `Equipped item ${slot} (${item.name}) must resolve`).not.toBeNull()
expect(rect!.w).toBeGreaterThan(0)
expect(rect!.h).toBeGreaterThan(0)
expect(rect!.x + rect!.w).toBeLessThanOrEqual(atlasWidth)
expect(rect!.y + rect!.h).toBeLessThanOrEqual(atlasHeight)
}
for (const { item, col, row } of STARTER_BAG_ITEMS) {
const rect = resolveItemSpriteRect(item, itemRects)
expect(rect, `Bag item at (${col}, ${row}) (${item.name}) must resolve`).not.toBeNull()
expect(rect!.w).toBeGreaterThan(0)
expect(rect!.h).toBeGreaterThan(0)
expect(rect!.x + rect!.w).toBeLessThanOrEqual(atlasWidth)
expect(rect!.y + rect!.h).toBeLessThanOrEqual(atlasHeight)
}
})
})
})