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
This commit is contained in:
parent
b1479b31ac
commit
e5c40fa244
|
|
@ -350,7 +350,7 @@ export const CLASS_EXCLUSIVE_SETS: Record<CharacterClassCode, Readonly<Partial<R
|
|||
nameZh: "塔·拉夏的无睑之眼 (Tal Rasha's Lidless Eye)",
|
||||
baseNameZh: '涡流水晶 (Swirling Crystal)',
|
||||
quality: 'set',
|
||||
invWidth: 2,
|
||||
invWidth: 1,
|
||||
invHeight: 3,
|
||||
allowedSlots: ['weapon1'],
|
||||
damage: '18 - 42',
|
||||
|
|
|
|||
|
|
@ -493,12 +493,12 @@ export const STARTER_EQUIPPED_GEAR: Readonly<Partial<Record<EquipSlotId, UiInven
|
|||
weapon1: {
|
||||
id: 'eq-oculus',
|
||||
code: 'oba',
|
||||
invFile: 'invbst',
|
||||
invFile: 'invob5',
|
||||
name: 'The Oculus',
|
||||
nameZh: '眼球 (The Oculus)',
|
||||
baseNameZh: '涡流水晶 (Swirling Crystal)',
|
||||
quality: 'unique',
|
||||
invWidth: 2,
|
||||
invWidth: 1,
|
||||
invHeight: 3,
|
||||
allowedSlots: ['weapon1'],
|
||||
damage: '18 - 42',
|
||||
|
|
|
|||
|
|
@ -0,0 +1,121 @@
|
|||
/**
|
||||
* tests/starter-items-inv-size-parity.test.ts
|
||||
*
|
||||
* Regression guard: every hand-authored starter item (equipped gear, bag, stash, for all 7
|
||||
* classes, plus the legacy `STARTER_EQUIPPED_GEAR` / `STARTER_BAG_ITEMS` UI defaults) must
|
||||
* declare exactly the `InvWidth` x `InvHeight` recorded in the canonical 1.13c
|
||||
* weapons.txt / armor.txt / misc.txt tables (embedded in `canonical-drop-data.ts`).
|
||||
*
|
||||
* Origin: Swirling Crystal (`oba`, The Oculus / Tal Rasha's Lidless Eye) was hardcoded as 2x3
|
||||
* while weapons.txt says 1x3, and The Oculus pointed at `invbst` (a 1x4 Battle Staff icon).
|
||||
*/
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { getEmbeddedDropTables } from '../src/game/embedded-drop-tables.ts'
|
||||
import { CHARACTER_CLASS_CODES } from '../src/game/classes.ts'
|
||||
import {
|
||||
CLASS_EXCLUSIVE_SETS,
|
||||
STARTER_STASH_ITEMS,
|
||||
createStarterBagForClass,
|
||||
createStarterProfileForClass,
|
||||
} from '../src/game/class-starter-profiles.ts'
|
||||
import {
|
||||
STARTER_BAG_ITEMS,
|
||||
STARTER_EQUIPPED_GEAR,
|
||||
type UiInventoryItem,
|
||||
} from '../src/ui/inventory.ts'
|
||||
import { BAKED_UI_MANIFEST } from '../src/ui/baked-ui-meta.ts'
|
||||
|
||||
interface CanonSize {
|
||||
readonly w: number
|
||||
readonly h: number
|
||||
readonly name: string
|
||||
}
|
||||
|
||||
function canonicalSize(code: string): CanonSize | undefined {
|
||||
const tables = getEmbeddedDropTables()
|
||||
const key = code.trim().toLowerCase()
|
||||
const w = tables.weapons.byCode.get(key)
|
||||
if (w) return { w: w.invWidth, h: w.invHeight, name: w.name }
|
||||
const a = tables.armor.byCode.get(key)
|
||||
if (a) return { w: a.invWidth, h: a.invHeight, name: a.name }
|
||||
const m = tables.misc.byCode.get(key)
|
||||
if (m) return { w: m.invWidth, h: m.invHeight, name: m.name }
|
||||
return undefined
|
||||
}
|
||||
|
||||
function collectAllStarterItems(): ReadonlyArray<{ readonly source: string; readonly item: UiInventoryItem }> {
|
||||
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([])
|
||||
})
|
||||
})
|
||||
Loading…
Reference in New Issue