161 lines
6.3 KiB
TypeScript
161 lines
6.3 KiB
TypeScript
/**
|
|
* Challenger M3_2 Empirical Verification & Adversarial Stress Suite
|
|
*
|
|
* Tests:
|
|
* 1. Schema 2 alias parity and bugfix validation (walk/stand offset vs frame count).
|
|
* 2. Complete 11-mode player animation metadata integrity.
|
|
* 3. 68-monster R8 atlas validity, 6-tuple bounding boxes, and sprite-space anchor offsets.
|
|
* 4. Act 1..5 PNG atlas valid PNG signatures and dimensions.
|
|
* 5. Deterministic index ordering and global metadata consistency.
|
|
* 6. Concurrency error resilience and atomic write cleanliness (zero orphaned .tmp files).
|
|
*/
|
|
|
|
import { describe, it, expect } from 'vitest'
|
|
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
|
|
describe('Challenger M3_2: Entity Asset Packing Concurrency & Schema Parity', () => {
|
|
const packsDir = 'samples/d2-packs'
|
|
const entitiesDir = join(packsDir, 'entities')
|
|
const charSoJsonPath = join(entitiesDir, 'char-so.json')
|
|
const charSoR8Path = join(entitiesDir, 'char-so.r8')
|
|
const indexJsonPath = join(entitiesDir, 'index.json')
|
|
|
|
it('verifies char-so.json Schema 2 alias fix: walk and stand match group offsets, not frame counts', () => {
|
|
expect(existsSync(charSoJsonPath)).toBe(true)
|
|
const meta = JSON.parse(readFileSync(charSoJsonPath, 'utf8'))
|
|
|
|
expect(meta.schema).toBe(2)
|
|
expect(meta.clips).toBeDefined()
|
|
expect(meta.clips.wl).toBeDefined()
|
|
expect(meta.clips.nu).toBeDefined()
|
|
|
|
// The bug was assigning frame count (8) instead of group offset (0 and 16)
|
|
// walk must match clips.wl.group (0)
|
|
expect(meta.walk).toBe(meta.clips.wl.group)
|
|
expect(meta.walk).toBe(0)
|
|
|
|
// stand must match clips.nu.group (16)
|
|
expect(meta.stand).toBe(meta.clips.nu.group)
|
|
expect(meta.stand).toBe(16)
|
|
|
|
// walkOffset and standOffset
|
|
expect(meta.walkOffset).toBe(meta.clips.wl.group)
|
|
expect(meta.standOffset).toBe(meta.clips.nu.group)
|
|
|
|
// Frame counts inside clips must be 8
|
|
expect(meta.clips.wl.frames).toBe(8)
|
|
expect(meta.clips.nu.frames).toBe(8)
|
|
})
|
|
|
|
it('verifies char-so 11-mode player animations and R8 atlas binary consistency', () => {
|
|
const meta = JSON.parse(readFileSync(charSoJsonPath, 'utf8'))
|
|
const requiredModes = ['wl', 'nu', 'rn', 'a1', 'a2', 'sc', 'gh', 'dt', 'dd', 'tn', 'tw']
|
|
|
|
for (const mode of requiredModes) {
|
|
expect(meta.clips[mode], `Missing mode ${mode}`).toBeDefined()
|
|
expect(meta.clips[mode].frames).toBeGreaterThan(0)
|
|
expect(meta.clips[mode].group).toBeGreaterThanOrEqual(0)
|
|
expect(typeof meta.clips[mode].speed).toBe('number')
|
|
}
|
|
|
|
// Verify groups structure
|
|
expect(Array.isArray(meta.groups)).toBe(true)
|
|
expect(meta.groups.length).toBe(176) // 11 modes * 16 directions
|
|
|
|
// Check 6-tuple format [x, y, w, h, ax, ay]
|
|
for (const group of meta.groups) {
|
|
expect(Array.isArray(group)).toBe(true)
|
|
for (const frame of group) {
|
|
expect(frame.length).toBe(6)
|
|
const [x, y, w, h, ax, ay] = frame
|
|
expect(x).toBeGreaterThanOrEqual(0)
|
|
expect(y).toBeGreaterThanOrEqual(0)
|
|
expect(w).toBeGreaterThan(0)
|
|
expect(h).toBeGreaterThan(0)
|
|
expect(typeof ax).toBe('number')
|
|
expect(typeof ay).toBe('number')
|
|
}
|
|
}
|
|
|
|
// Verify R8 atlas file
|
|
expect(existsSync(charSoR8Path)).toBe(true)
|
|
const stat = statSync(charSoR8Path)
|
|
expect(stat.size).toBe(meta.width * meta.height)
|
|
expect(meta.width).toBe(4096)
|
|
expect(meta.height).toBe(1929)
|
|
})
|
|
|
|
it('verifies all 70 monster R8 atlases and metadata schemas', () => {
|
|
expect(existsSync(indexJsonPath)).toBe(true)
|
|
const index = JSON.parse(readFileSync(indexJsonPath, 'utf8'))
|
|
|
|
expect(index.schema).toBe(2)
|
|
expect(index.character).toBe('char-so')
|
|
expect(Array.isArray(index.entities)).toBe(true)
|
|
// 69 + EC (Hell Bovine, Moo Moo Farm)
|
|
expect(index.entities.length).toBe(70)
|
|
|
|
for (const entity of index.entities) {
|
|
expect(entity.key).toBeDefined()
|
|
expect(entity.token).toBeDefined()
|
|
expect(entity.weapon).toBeDefined()
|
|
expect(entity.file).toBe(`monster-${entity.key}.r8`)
|
|
expect(entity.format).toBe('r8')
|
|
expect(entity.width).toBeGreaterThan(0)
|
|
expect(entity.height).toBeGreaterThan(0)
|
|
|
|
const r8Path = join(entitiesDir, entity.file)
|
|
expect(existsSync(r8Path), `Missing ${r8Path}`).toBe(true)
|
|
const stat = statSync(r8Path)
|
|
expect(stat.size).toBe(entity.width * entity.height)
|
|
|
|
const jsonPath = join(entitiesDir, `monster-${entity.key}.json`)
|
|
expect(existsSync(jsonPath), `Missing ${jsonPath}`).toBe(true)
|
|
const monsterMeta = JSON.parse(readFileSync(jsonPath, 'utf8'))
|
|
expect(monsterMeta.schema).toBe(2)
|
|
expect(monsterMeta.clips.nu).toBeDefined()
|
|
expect(monsterMeta.standOffset).toBe(monsterMeta.clips.nu.group)
|
|
expect(monsterMeta.standFrames).toBe(monsterMeta.clips.nu.frames)
|
|
}
|
|
})
|
|
|
|
it('verifies Act 1..5 per-act PNG atlases have valid RFC 2083 signatures', () => {
|
|
const pngSignature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10])
|
|
|
|
for (let act = 1; act <= 5; act += 1) {
|
|
const actEntitiesDir = join(packsDir, `act${act}`, 'entities')
|
|
expect(existsSync(actEntitiesDir), `Missing act${act} entities dir`).toBe(true)
|
|
|
|
const charPngPath = join(actEntitiesDir, 'char-so.png')
|
|
expect(existsSync(charPngPath)).toBe(true)
|
|
const charBuf = readFileSync(charPngPath)
|
|
expect(charBuf.subarray(0, 8)).toEqual(pngSignature)
|
|
|
|
const files = readdirSync(actEntitiesDir)
|
|
const pngFiles = files.filter(f => f.endsWith('.png') && f.startsWith('monster-'))
|
|
expect(pngFiles.length).toBeGreaterThan(15)
|
|
|
|
for (const pngFile of pngFiles) {
|
|
const fullPath = join(actEntitiesDir, pngFile)
|
|
const buf = readFileSync(fullPath)
|
|
expect(buf.subarray(0, 8)).toEqual(pngSignature)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('verifies atomic write cleanliness: zero orphaned .tmp files in entity output directories', () => {
|
|
const checkDir = (dir: string) => {
|
|
if (!existsSync(dir)) return
|
|
const files = readdirSync(dir)
|
|
const tmpFiles = files.filter(f => f.includes('.tmp.'))
|
|
expect(tmpFiles, `Found orphaned tmp files in ${dir}: ${tmpFiles.join(', ')}`).toEqual([])
|
|
}
|
|
|
|
checkDir(entitiesDir)
|
|
for (let act = 1; act <= 5; act += 1) {
|
|
checkDir(join(packsDir, `act${act}`, 'entities'))
|
|
}
|
|
})
|
|
})
|