122 lines
5.2 KiB
TypeScript
122 lines
5.2 KiB
TypeScript
/**
|
|
* 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([])
|
|
})
|
|
})
|