535 lines
21 KiB
TypeScript
535 lines
21 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import fs from 'node:fs'
|
|
import path from 'node:path'
|
|
import zlib from 'node:zlib'
|
|
import {
|
|
MISSILE_METAS,
|
|
CORPSEEXPLOSION_META,
|
|
POISONCORPSEEXPLOSION_META,
|
|
POISONEXPLOSIONCLOUD_META,
|
|
POISONPUFF_META,
|
|
CURSECAST_META,
|
|
BONECAST_META,
|
|
BONESPEARTRAIL_META,
|
|
REVIVE_META,
|
|
REVIVESMALL_META,
|
|
REVIVEMEDIUM_META,
|
|
REVIVELARGE_META,
|
|
BONESPEAREXPLODE_META,
|
|
type MissileMeta,
|
|
} from '../../../src/render/missiles-meta.ts'
|
|
import {
|
|
OVERLAY_METAS,
|
|
CURSE_AMPLIFY_DAMAGE_OVERLAY_META,
|
|
CURSE_DIM_VISION_OVERLAY_META,
|
|
CURSE_WEAKEN_OVERLAY_META,
|
|
CURSE_IRON_MAIDEN_OVERLAY_META,
|
|
CURSE_TERROR_OVERLAY_META,
|
|
CURSE_CONFUSE_OVERLAY_META,
|
|
CURSE_LIFE_TAP_OVERLAY_META,
|
|
CURSE_ATTRACT_OVERLAY_META,
|
|
CURSE_DECREPIFY_OVERLAY_META,
|
|
CURSE_LOWER_RESIST_OVERLAY_META,
|
|
HIT_IRON_MAIDEN_OVERLAY_META,
|
|
HIT_LIFE_TAP_OVERLAY_META,
|
|
BONE_ARMOR_CAST_OVERLAY_META,
|
|
BONE_ARMOR_FRONT_OVERLAY_META,
|
|
BONE_ARMOR_BACK_OVERLAY_META,
|
|
type OverlayMeta,
|
|
} from '../../../src/render/overlays-meta.ts'
|
|
|
|
// Strict PNG RGBA Decoder
|
|
function decodeRgbaPng(buf: Buffer) {
|
|
const width = buf.readUInt32BE(16)
|
|
const height = buf.readUInt32BE(20)
|
|
const bitDepth = buf[24]!
|
|
const colorType = buf[25]!
|
|
|
|
const idatParts: Buffer[] = []
|
|
let offset = 8
|
|
while (offset < buf.length) {
|
|
const len = buf.readUInt32BE(offset)
|
|
const type = buf.subarray(offset + 4, offset + 8).toString('ascii')
|
|
if (type === 'IDAT') {
|
|
idatParts.push(buf.subarray(offset + 8, offset + 8 + len))
|
|
}
|
|
offset += 12 + len
|
|
}
|
|
const idat = Buffer.concat(idatParts)
|
|
const raw = zlib.inflateSync(idat)
|
|
|
|
const bpp = 4
|
|
const rowBytes = width * bpp
|
|
const pixels = new Uint8Array(width * height * 4)
|
|
let srcAt = 0
|
|
let dstAt = 0
|
|
|
|
for (let y = 0; y < height; y++) {
|
|
const filter = raw[srcAt++]
|
|
const row = raw.subarray(srcAt, srcAt + rowBytes)
|
|
srcAt += rowBytes
|
|
|
|
const prevRow = y > 0 ? pixels.subarray((y - 1) * rowBytes, y * rowBytes) : null
|
|
const curRow = pixels.subarray(dstAt, dstAt + rowBytes)
|
|
|
|
for (let x = 0; x < rowBytes; x++) {
|
|
const left = x >= bpp ? curRow[x - bpp]! : 0
|
|
const up = prevRow ? prevRow[x]! : 0
|
|
const upLeft = prevRow && x >= bpp ? prevRow[x - bpp]! : 0
|
|
|
|
let val = row[x]!
|
|
if (filter === 0) {
|
|
// None
|
|
} else if (filter === 1) {
|
|
val = (val + left) & 0xff
|
|
} else if (filter === 2) {
|
|
val = (val + up) & 0xff
|
|
} else if (filter === 3) {
|
|
val = (val + Math.floor((left + up) / 2)) & 0xff
|
|
} else if (filter === 4) {
|
|
const p = left + up - upLeft
|
|
const pa = Math.abs(p - left)
|
|
const pb = Math.abs(p - up)
|
|
const pc = Math.abs(p - upLeft)
|
|
let pr = upLeft
|
|
if (pa <= pb && pa <= pc) pr = left
|
|
else if (pb <= pc) pr = up
|
|
val = (val + pr) & 0xff
|
|
}
|
|
curRow[x] = val
|
|
}
|
|
dstAt += rowBytes
|
|
}
|
|
|
|
return { width, height, bitDepth, colorType, pixels }
|
|
}
|
|
|
|
describe('Challenger Adversarial Stress Suite — Necromancer Assets', () => {
|
|
const publicMissilesDir = path.resolve(__dirname, '../../../public/missiles')
|
|
const packMissilesDir = path.resolve(__dirname, '../../../samples/d2-packs/missiles')
|
|
const publicOverlaysDir = path.resolve(__dirname, '../../../public/overlays')
|
|
const packOverlaysDir = path.resolve(__dirname, '../../../samples/d2-packs/overlays')
|
|
|
|
const TARGET_MISSILES = [
|
|
{ name: 'corpseexplosion', celFile: 'CorpseExplodeGuts', meta: CORPSEEXPLOSION_META },
|
|
{ name: 'poisoncorpseexplosion', celFile: 'CorpseExplodeGutsPoison', meta: POISONCORPSEEXPLOSION_META },
|
|
{ name: 'poisonexplosioncloud', celFile: 'PoisonSparks', meta: POISONEXPLOSIONCLOUD_META },
|
|
{ name: 'poisonpuff', celFile: 'PoisonSmokePuff', meta: POISONPUFF_META },
|
|
{ name: 'cursecast', celFile: 'CurseCast', meta: CURSECAST_META },
|
|
{ name: 'bonecast', celFile: 'BoneCast', meta: BONECAST_META },
|
|
{ name: 'bonespeartrail', celFile: 'BoneSpearTrail', meta: BONESPEARTRAIL_META },
|
|
{ name: 'revive', celFile: 'Revive', meta: REVIVE_META },
|
|
] as const
|
|
|
|
const TARGET_OVERLAYS = [
|
|
{ name: 'curseamplifydamage', celFile: 'CurseAmplifyDamageEffect', meta: CURSE_AMPLIFY_DAMAGE_OVERLAY_META, preDraw: false },
|
|
{ name: 'cursedimvision', celFile: 'CurseDimVisionEffect', meta: CURSE_DIM_VISION_OVERLAY_META, preDraw: false },
|
|
{ name: 'curseweaken', celFile: 'CurseWeakenEffect', meta: CURSE_WEAKEN_OVERLAY_META, preDraw: false },
|
|
{ name: 'curseironmaiden', celFile: 'CurseIronMaidenEffect', meta: CURSE_IRON_MAIDEN_OVERLAY_META, preDraw: false },
|
|
{ name: 'curseterror', celFile: 'CurseTerrorEffect', meta: CURSE_TERROR_OVERLAY_META, preDraw: false },
|
|
{ name: 'curseconfuse', celFile: 'CurseConfuseEffect', meta: CURSE_CONFUSE_OVERLAY_META, preDraw: false },
|
|
{ name: 'cursereversevampire', celFile: 'CurseReverseVampireEffect', meta: CURSE_LIFE_TAP_OVERLAY_META, preDraw: false },
|
|
{ name: 'curseattract', celFile: 'CurseAttractHarmEffect', meta: CURSE_ATTRACT_OVERLAY_META, preDraw: false },
|
|
{ name: 'cursedecrepify', celFile: 'CurseDecrepifyEffect', meta: CURSE_DECREPIFY_OVERLAY_META, preDraw: false },
|
|
{ name: 'curselowerresist', celFile: 'CurseLowerResistEffect', meta: CURSE_LOWER_RESIST_OVERLAY_META, preDraw: false },
|
|
{ name: 'hit_ironmaiden', celFile: 'AuraThornsHit', meta: HIT_IRON_MAIDEN_OVERLAY_META, preDraw: false },
|
|
{ name: 'hit_reversevampire', celFile: 'CurseRevVampireHit', meta: HIT_LIFE_TAP_OVERLAY_META, preDraw: false },
|
|
{ name: 'bonearmor_cast', celFile: 'BoneShieldEmerge', meta: BONE_ARMOR_CAST_OVERLAY_META, preDraw: false },
|
|
{ name: 'bonearmor_front', celFile: 'BoneShieldFront', meta: BONE_ARMOR_FRONT_OVERLAY_META, preDraw: false },
|
|
{ name: 'bonearmor_back', celFile: 'BoneShieldBack', meta: BONE_ARMOR_BACK_OVERLAY_META, preDraw: true },
|
|
] as const
|
|
|
|
describe('Adversarial Test 1: Transparent Border Thresholding & Halo Elimination on Pixel Buffers', () => {
|
|
it('1.1 Asserts NO pixel in any Necromancer missile PNG has max(r,g,b) <= 4 with alpha > 0', () => {
|
|
for (const m of TARGET_MISSILES) {
|
|
const pngPath = path.join(publicMissilesDir, `${m.name}.png`)
|
|
const buf = fs.readFileSync(pngPath)
|
|
const { width, height, pixels } = decodeRgbaPng(buf)
|
|
|
|
let haloCount = 0
|
|
let haloSamples: { x: number; y: number; r: number; g: number; b: number; a: number }[] = []
|
|
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const idx = (y * width + x) * 4
|
|
const r = pixels[idx]!
|
|
const g = pixels[idx + 1]!
|
|
const b = pixels[idx + 2]!
|
|
const a = pixels[idx + 3]!
|
|
const mVal = Math.max(r, g, b)
|
|
|
|
if (mVal <= 4 && a > 0) {
|
|
haloCount++
|
|
if (haloSamples.length < 5) haloSamples.push({ x, y, r, g, b, a })
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(
|
|
haloCount,
|
|
`Missile ${m.name}.png has ${haloCount} dark halo pixels (max(RGB)<=4 & alpha>0): ${JSON.stringify(haloSamples)}`
|
|
).toBe(0)
|
|
}
|
|
})
|
|
|
|
it('1.2 Asserts NO pixel in any Necromancer overlay PNG has max(r,g,b) <= 4 with alpha > 0', () => {
|
|
for (const o of TARGET_OVERLAYS) {
|
|
const pngPath = path.join(publicOverlaysDir, `${o.name}.png`)
|
|
const buf = fs.readFileSync(pngPath)
|
|
const { width, height, pixels } = decodeRgbaPng(buf)
|
|
|
|
let haloCount = 0
|
|
let haloSamples: { x: number; y: number; r: number; g: number; b: number; a: number }[] = []
|
|
|
|
for (let y = 0; y < height; y++) {
|
|
for (let x = 0; x < width; x++) {
|
|
const idx = (y * width + x) * 4
|
|
const r = pixels[idx]!
|
|
const g = pixels[idx + 1]!
|
|
const b = pixels[idx + 2]!
|
|
const a = pixels[idx + 3]!
|
|
const mVal = Math.max(r, g, b)
|
|
|
|
if (mVal <= 4 && a > 0) {
|
|
haloCount++
|
|
if (haloSamples.length < 5) haloSamples.push({ x, y, r, g, b, a })
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(
|
|
haloCount,
|
|
`Overlay ${o.name}.png has ${haloCount} dark halo pixels (max(RGB)<=4 & alpha>0): ${JSON.stringify(haloSamples)}`
|
|
).toBe(0)
|
|
}
|
|
})
|
|
|
|
it('1.3 Adversarial perimeter inspection: checks 1-pixel frame border and perimeter pixels around non-empty frames', () => {
|
|
const allTargets = [
|
|
...TARGET_MISSILES.map(m => ({ name: m.name, meta: m.meta, dir: publicMissilesDir })),
|
|
...TARGET_OVERLAYS.map(o => ({ name: o.name, meta: o.meta, dir: publicOverlaysDir })),
|
|
]
|
|
|
|
for (const t of allTargets) {
|
|
const pngPath = path.join(t.dir, `${t.name}.png`)
|
|
const buf = fs.readFileSync(pngPath)
|
|
const { width, height, pixels } = decodeRgbaPng(buf)
|
|
|
|
for (const group of t.meta.groups) {
|
|
for (let fIdx = 0; fIdx < group.length; fIdx++) {
|
|
const [fx, fy, fw, fh] = group[fIdx]!
|
|
|
|
// Determine if this frame is non-empty
|
|
let frameVisiblePixels = 0
|
|
for (let y = fy; y < fy + fh; y++) {
|
|
for (let x = fx; x < fx + fw; x++) {
|
|
const idx = (y * width + x) * 4
|
|
if (pixels[idx + 3]! > 0) frameVisiblePixels++
|
|
}
|
|
}
|
|
|
|
if (frameVisiblePixels === 0) {
|
|
// Empty frame: all pixels in box must be strictly alpha === 0
|
|
continue
|
|
}
|
|
|
|
// For non-empty frame: inspect edge pixels (pixels with alpha > 0 adjacent to alpha === 0)
|
|
let perimeterPixelsChecked = 0
|
|
for (let y = fy; y < fy + fh; y++) {
|
|
for (let x = fx; x < fx + fw; x++) {
|
|
const idx = (y * width + x) * 4
|
|
const a = pixels[idx + 3]!
|
|
if (a === 0) continue
|
|
|
|
// Check 4-neighbors for any transparent pixel
|
|
let hasTransparentNeighbor = false
|
|
const neighbors = [
|
|
[x - 1, y],
|
|
[x + 1, y],
|
|
[x, y - 1],
|
|
[x, y + 1],
|
|
]
|
|
for (const [nx, ny] of neighbors) {
|
|
if (nx < 0 || nx >= width || ny < 0 || ny >= height) {
|
|
hasTransparentNeighbor = true
|
|
break
|
|
}
|
|
const nIdx = (ny * width + nx) * 4
|
|
if (pixels[nIdx + 3] === 0) {
|
|
hasTransparentNeighbor = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if (hasTransparentNeighbor) {
|
|
perimeterPixelsChecked++
|
|
const maxRgb = Math.max(pixels[idx]!, pixels[idx + 1]!, pixels[idx + 2]!)
|
|
// Edge pixel bordering transparency MUST NOT have maxRgb <= 4
|
|
expect(
|
|
maxRgb,
|
|
`Perimeter pixel at (${x},${y}) in ${t.name} (frame ${fIdx}) has maxRgb=${maxRgb} <= 4 with alpha=${a}`
|
|
).toBeGreaterThan(4)
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(
|
|
perimeterPixelsChecked,
|
|
`Non-empty frame at (${fx},${fy}) (fIdx: ${fIdx}) in ${t.name} must have non-empty perimeter`
|
|
).toBeGreaterThan(0)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
it('1.4 Mathematical verification of threshold ramp function across all non-zero alpha pixels', () => {
|
|
const allTargets = [
|
|
...TARGET_MISSILES.map(m => ({ name: m.name, dir: publicMissilesDir })),
|
|
...TARGET_OVERLAYS.map(o => ({ name: o.name, dir: publicOverlaysDir })),
|
|
]
|
|
|
|
for (const t of allTargets) {
|
|
const pngPath = path.join(t.dir, `${t.name}.png`)
|
|
const buf = fs.readFileSync(pngPath)
|
|
const { pixels } = decodeRgbaPng(buf)
|
|
|
|
for (let i = 0; i < pixels.length; i += 4) {
|
|
const r = pixels[i]!
|
|
const g = pixels[i + 1]!
|
|
const b = pixels[i + 2]!
|
|
const a = pixels[i + 3]!
|
|
const m = Math.max(r, g, b)
|
|
|
|
if (m <= 4) {
|
|
expect(a).toBe(0)
|
|
} else if (m >= 60) {
|
|
expect(a).toBe(255)
|
|
} else {
|
|
// 4 < m < 60
|
|
const expectedAlpha = Math.min(255, Math.round(((m - 4) / (60 - 4)) * 255))
|
|
expect(Math.abs(a - expectedAlpha)).toBeLessThanOrEqual(1)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Adversarial Test 2: Frame Geometry, Bounding Box Disjointness & Pivot Offsets', () => {
|
|
it('2.1 Atlas Frame Disjointness: No two frames within an atlas sheet overlap each other', () => {
|
|
const allMetas: { name: string; meta: MissileMeta | OverlayMeta }[] = [
|
|
...TARGET_MISSILES.map(m => ({ name: m.name, meta: m.meta })),
|
|
...TARGET_OVERLAYS.map(o => ({ name: o.name, meta: o.meta })),
|
|
]
|
|
|
|
for (const { name, meta } of allMetas) {
|
|
const frames: { x: number; y: number; w: number; h: number; fIdx: number }[] = []
|
|
let fIdx = 0
|
|
for (const group of meta.groups) {
|
|
for (const [x, y, w, h] of group) {
|
|
frames.push({ x, y, w, h, fIdx: fIdx++ })
|
|
}
|
|
}
|
|
|
|
// Quadratic collision check among all frames in sheet
|
|
for (let i = 0; i < frames.length; i++) {
|
|
const a = frames[i]!
|
|
for (let j = i + 1; j < frames.length; j++) {
|
|
const b = frames[j]!
|
|
const overlaps = !(
|
|
a.x + a.w <= b.x ||
|
|
b.x + b.w <= a.x ||
|
|
a.y + a.h <= b.y ||
|
|
b.y + b.h <= a.y
|
|
)
|
|
expect(
|
|
overlaps,
|
|
`Frame ${a.fIdx} [${a.x},${a.y},${a.w},${a.h}] overlaps Frame ${b.fIdx} [${b.x},${b.y},${b.w},${b.h}] in sheet ${name}`
|
|
).toBe(false)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
it('2.2 Bounding Box Rect Invariants: x>=0, y>=0, w>0, h>0 and strictly within sheet dimensions', () => {
|
|
const allMetas = [
|
|
...TARGET_MISSILES.map(m => ({ name: m.name, meta: m.meta })),
|
|
...TARGET_OVERLAYS.map(o => ({ name: o.name, meta: o.meta })),
|
|
]
|
|
|
|
for (const { name, meta } of allMetas) {
|
|
expect(meta.width).toBeGreaterThan(0)
|
|
expect(meta.height).toBeGreaterThan(0)
|
|
expect(meta.width).toBeLessThanOrEqual(4096)
|
|
expect(meta.height).toBeLessThanOrEqual(4096)
|
|
|
|
for (const group of meta.groups) {
|
|
for (const [x, y, w, h] of group) {
|
|
expect(Number.isInteger(x)).toBe(true)
|
|
expect(Number.isInteger(y)).toBe(true)
|
|
expect(Number.isInteger(w)).toBe(true)
|
|
expect(Number.isInteger(h)).toBe(true)
|
|
expect(x).toBeGreaterThanOrEqual(0)
|
|
expect(y).toBeGreaterThanOrEqual(0)
|
|
expect(w).toBeGreaterThan(0)
|
|
expect(h).toBeGreaterThan(0)
|
|
expect(x + w).toBeLessThanOrEqual(meta.width)
|
|
expect(y + h).toBeLessThanOrEqual(meta.height)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
it('2.3 Coordinate Pivot Offsets (ax, ay) Bounds & Sanity: finite, integer, and within Diablo II coordinates', () => {
|
|
const allMetas = [
|
|
...TARGET_MISSILES.map(m => ({ name: m.name, meta: m.meta })),
|
|
...TARGET_OVERLAYS.map(o => ({ name: o.name, meta: o.meta })),
|
|
]
|
|
|
|
for (const { name, meta } of allMetas) {
|
|
for (const group of meta.groups) {
|
|
for (const [, , , , ax, ay] of group) {
|
|
expect(typeof ax).toBe('number')
|
|
expect(typeof ay).toBe('number')
|
|
expect(Number.isNaN(ax)).toBe(false)
|
|
expect(Number.isNaN(ay)).toBe(false)
|
|
expect(Number.isFinite(ax)).toBe(true)
|
|
expect(Number.isFinite(ay)).toBe(true)
|
|
expect(Number.isInteger(ax)).toBe(true)
|
|
expect(Number.isInteger(ay)).toBe(true)
|
|
// Diablo II frame anchor offsets range from -1000 to +1000 pixels
|
|
expect(ax).toBeGreaterThanOrEqual(-1000)
|
|
expect(ax).toBeLessThanOrEqual(1000)
|
|
expect(ay).toBeGreaterThanOrEqual(-1000)
|
|
expect(ay).toBeLessThanOrEqual(1000)
|
|
}
|
|
}
|
|
}
|
|
})
|
|
|
|
it('2.4 Sheet Activity: every missile and overlay atlas sheet contains non-empty frames and active render data', () => {
|
|
const allTargets = [
|
|
...TARGET_MISSILES.map(m => ({ name: m.name, meta: m.meta, dir: publicMissilesDir })),
|
|
...TARGET_OVERLAYS.map(o => ({ name: o.name, meta: o.meta, dir: publicOverlaysDir })),
|
|
]
|
|
|
|
for (const t of allTargets) {
|
|
const pngPath = path.join(t.dir, `${t.name}.png`)
|
|
const buf = fs.readFileSync(pngPath)
|
|
const { width, pixels } = decodeRgbaPng(buf)
|
|
|
|
let totalVisiblePixels = 0
|
|
let nonZeroFramesCount = 0
|
|
|
|
for (const group of t.meta.groups) {
|
|
for (let fIdx = 0; fIdx < group.length; fIdx++) {
|
|
const [x, y, w, h] = group[fIdx]!
|
|
let frameVisible = 0
|
|
for (let row = y; row < y + h; row++) {
|
|
for (let col = x; col < x + w; col++) {
|
|
const idx = (row * width + col) * 4
|
|
if (pixels[idx + 3]! > 0) frameVisible++
|
|
}
|
|
}
|
|
if (frameVisible > 0) {
|
|
nonZeroFramesCount++
|
|
totalVisiblePixels += frameVisible
|
|
}
|
|
}
|
|
}
|
|
|
|
expect(totalVisiblePixels, `${t.name} must have positive visible pixel count`).toBeGreaterThan(100)
|
|
expect(nonZeroFramesCount, `${t.name} must have multiple active frames`).toBeGreaterThanOrEqual(1)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('Adversarial Test 3: Directional Groups, Frame Counts & Metadata Parity', () => {
|
|
it('3.1 Verifies directional group structure across all new missiles', () => {
|
|
for (const m of TARGET_MISSILES) {
|
|
expect(m.meta.directions).toBeGreaterThanOrEqual(1)
|
|
expect(m.meta.groups.length).toBe(m.meta.directions)
|
|
expect(m.meta.framesPerDirection).toBeGreaterThan(0)
|
|
|
|
// All directional groups must have exactly framesPerDirection frames
|
|
for (let d = 0; d < m.meta.directions; d++) {
|
|
const group = m.meta.groups[d]!
|
|
expect(group.length).toBe(m.meta.framesPerDirection)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('3.2 Verifies overlay single-direction and preDraw configuration', () => {
|
|
for (const o of TARGET_OVERLAYS) {
|
|
expect(o.meta.directions).toBe(1)
|
|
expect(o.meta.frames).toBeGreaterThan(0)
|
|
expect(o.meta.framesPerDirection).toBe(o.meta.frames)
|
|
expect(o.meta.groups.length).toBe(1)
|
|
expect(o.meta.groups[0]!.length).toBe(o.meta.frames)
|
|
expect(o.meta.preDraw).toBe(o.preDraw)
|
|
|
|
// Specific preDraw check: Bone Armor back must be true, front must be false
|
|
if (o.name === 'bonearmor_back') {
|
|
expect(o.meta.preDraw).toBe(true)
|
|
} else {
|
|
expect(o.meta.preDraw).toBe(false)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('3.3 JSON Disk vs TypeScript In-Memory Metadata Exact Parity', () => {
|
|
for (const m of TARGET_MISSILES) {
|
|
const jsonPath = path.join(publicMissilesDir, `${m.name}.json`)
|
|
const json = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'))
|
|
|
|
expect(json.name).toBe(m.meta.name)
|
|
expect(json.celFile).toBe(m.meta.celFile)
|
|
expect(json.width).toBe(m.meta.width)
|
|
expect(json.height).toBe(m.meta.height)
|
|
expect(json.directions).toBe(m.meta.directions)
|
|
expect(json.framesPerDirection).toBe(m.meta.framesPerDirection)
|
|
expect(json.animSpeed).toBe(m.meta.animSpeed)
|
|
expect(json.groups).toEqual(m.meta.groups)
|
|
}
|
|
|
|
for (const o of TARGET_OVERLAYS) {
|
|
const jsonPath = path.join(publicOverlaysDir, `${o.name}.json`)
|
|
const json = JSON.parse(fs.readFileSync(jsonPath, 'utf-8'))
|
|
|
|
expect(json.name).toBe(o.meta.name)
|
|
expect(json.celFile).toBe(o.meta.celFile)
|
|
expect(json.width).toBe(o.meta.width)
|
|
expect(json.height).toBe(o.meta.height)
|
|
expect(json.directions).toBe(o.meta.directions)
|
|
expect(json.frames).toBe(o.meta.frames)
|
|
expect(json.framesPerDirection).toBe(o.meta.framesPerDirection)
|
|
expect(json.animRate).toBe(o.meta.animRate)
|
|
expect(json.trans).toBe(o.meta.trans)
|
|
expect(json.preDraw).toBe(o.meta.preDraw)
|
|
expect(json.groups).toEqual(o.meta.groups)
|
|
}
|
|
})
|
|
|
|
it('3.4 Dual Storage Byte & Content Parity between public/ and samples/d2-packs/', () => {
|
|
for (const m of TARGET_MISSILES) {
|
|
const pubPng = fs.readFileSync(path.join(publicMissilesDir, `${m.name}.png`))
|
|
const packPng = fs.readFileSync(path.join(packMissilesDir, `${m.name}.png`))
|
|
expect(Buffer.compare(pubPng, packPng), `Missile ${m.name}.png differs between public and samples/d2-packs`).toBe(0)
|
|
|
|
const pubJson = fs.readFileSync(path.join(publicMissilesDir, `${m.name}.json`))
|
|
const packJson = fs.readFileSync(path.join(packMissilesDir, `${m.name}.json`))
|
|
expect(Buffer.compare(pubJson, packJson), `Missile ${m.name}.json differs between public and samples/d2-packs`).toBe(0)
|
|
}
|
|
|
|
for (const o of TARGET_OVERLAYS) {
|
|
const pubPng = fs.readFileSync(path.join(publicOverlaysDir, `${o.name}.png`))
|
|
const packPng = fs.readFileSync(path.join(packOverlaysDir, `${o.name}.png`))
|
|
expect(Buffer.compare(pubPng, packPng), `Overlay ${o.name}.png differs between public and samples/d2-packs`).toBe(0)
|
|
|
|
const pubJson = fs.readFileSync(path.join(publicOverlaysDir, `${o.name}.json`))
|
|
const packJson = fs.readFileSync(path.join(packOverlaysDir, `${o.name}.json`))
|
|
expect(Buffer.compare(pubJson, packJson), `Overlay ${o.name}.json differs between public and samples/d2-packs`).toBe(0)
|
|
}
|
|
})
|
|
})
|
|
})
|