diablo2-web/tests/challenger-m3-entity-concur...

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'))
}
})
})