From e5c40fa2441bd59527a0ee0a8a14e0c725f4cc25 Mon Sep 17 00:00:00 2001 From: troytt <47798984@qq.com> Date: Wed, 23 Sep 2026 07:25:25 +0000 Subject: [PATCH] fix(inventory): correct Swirling Crystal inventory size to 1x3 and fix Oculus sprite parity - Correct Swirling Crystal (oba) invWidth from 2 to 1 (1x3) per canonical 1.13c weapons.txt for both The Oculus (STARTER_EQUIPPED_GEAR.weapon1) and Tal Rasha's Lidless Eye (CLASS_EXCLUSIVE_SETS.sor.weapon1) - Fix The Oculus invFile from invbst (1x4 Battle Staff) to canonical invob5 (1x3 Swirling Crystal sprite) - Audit all starter item dimensions across all 7 classes against canonical 1.13c weapons.txt, armor.txt, and misc.txt - Add tests/starter-items-inv-size-parity.test.ts to prevent inventory size regressions --- src/game/class-starter-profiles.ts | 2 +- src/ui/inventory.ts | 4 +- tests/starter-items-inv-size-parity.test.ts | 121 ++++++++++++++++++++ 3 files changed, 124 insertions(+), 3 deletions(-) create mode 100644 tests/starter-items-inv-size-parity.test.ts diff --git a/src/game/class-starter-profiles.ts b/src/game/class-starter-profiles.ts index 7e5ee4c..f2e8634 100644 --- a/src/game/class-starter-profiles.ts +++ b/src/game/class-starter-profiles.ts @@ -350,7 +350,7 @@ export const CLASS_EXCLUSIVE_SETS: Record { + const out: Array<{ source: string; item: UiInventoryItem }> = [] + for (const [slot, item] of Object.entries(STARTER_EQUIPPED_GEAR)) { + if (item) out.push({ source: `STARTER_EQUIPPED_GEAR.${slot}`, item }) + } + STARTER_BAG_ITEMS.forEach((p, i) => out.push({ source: `STARTER_BAG_ITEMS[${i}]`, item: p.item })) + STARTER_STASH_ITEMS.forEach((p, i) => out.push({ source: `STARTER_STASH_ITEMS[${i}]`, item: p.item })) + for (const cls of CHARACTER_CLASS_CODES) { + for (const [slot, item] of Object.entries(CLASS_EXCLUSIVE_SETS[cls])) { + if (item) out.push({ source: `CLASS_EXCLUSIVE_SETS.${cls}.${slot}`, item }) + } + createStarterBagForClass(cls).forEach((p, i) => out.push({ source: `bag(${cls})[${i}]`, item: p.item })) + const profile = createStarterProfileForClass(cls) + for (const [slot, item] of Object.entries(profile.equipped)) { + if (item) out.push({ source: `profile(${cls}).equipped.${slot}`, item }) + } + profile.bagItems.forEach((p, i) => out.push({ source: `profile(${cls}).bag[${i}]`, item: p.item })) + profile.stashItems.forEach((p, i) => out.push({ source: `profile(${cls}).stash[${i}]`, item: p.item })) + } + return out +} + +describe('Starter item inventory sizes match canonical 1.13c InvWidth/InvHeight', () => { + const all = collectAllStarterItems() + + it('covers a meaningful number of hand-authored items', () => { + expect(all.length).toBeGreaterThan(50) + }) + + it('every starter item code exists in weapons/armor/misc.txt', () => { + const unknown = all.filter(({ item }) => !canonicalSize(item.code)).map(({ source, item }) => `${source}:${item.code}`) + expect(unknown, `Unknown item codes: ${unknown.join(', ')}`).toEqual([]) + }) + + it('every starter item invWidth/invHeight equals the canonical table value', () => { + const mismatches: string[] = [] + for (const { source, item } of all) { + const canon = canonicalSize(item.code) + if (!canon) continue + if (item.invWidth !== canon.w || item.invHeight !== canon.h) { + mismatches.push( + `${source} code=${item.code} (${canon.name}) declared ${item.invWidth}x${item.invHeight}, canonical ${canon.w}x${canon.h}`, + ) + } + } + expect(mismatches, mismatches.join('\n')).toEqual([]) + }) + + it('Swirling Crystal (oba) is 1x3 per weapons.txt (The Oculus / Tal Rasha\'s Lidless Eye)', () => { + const canon = canonicalSize('oba') + expect(canon).toEqual({ w: 1, h: 3, name: 'Swirling Crystal' }) + const oculus = STARTER_EQUIPPED_GEAR.weapon1! + expect(oculus.code).toBe('oba') + expect([oculus.invWidth, oculus.invHeight]).toEqual([1, 3]) + const talOrb = CLASS_EXCLUSIVE_SETS.sor.weapon1! + expect(talOrb.code).toBe('oba') + expect([talOrb.invWidth, talOrb.invHeight]).toEqual([1, 3]) + }) + + it('starter item invFile sprites have pixel dimensions consistent with the declared grid size', () => { + // Each inventory cell is 28x28 px in the 1.13c inventory panel. + const rects = BAKED_UI_MANIFEST.itemRects + const bad: string[] = [] + for (const { source, item } of all) { + const rect = rects[item.invFile] + if (!rect) continue // Some icons are resolved via fallback tiers; only validate direct hits. + const expectW = item.invWidth * 28 + const expectH = item.invHeight * 28 + if (rect.w !== expectW || rect.h !== expectH) { + bad.push(`${source} code=${item.code} invFile=${item.invFile} sprite ${rect.w}x${rect.h} vs grid ${expectW}x${expectH}`) + } + } + expect(bad, bad.join('\n')).toEqual([]) + }) +})