diff --git a/tests/skills/nec/adv_necro_assets_audit.test.ts b/tests/skills/nec/adv_necro_assets_audit.test.ts new file mode 100644 index 0000000..63148bb --- /dev/null +++ b/tests/skills/nec/adv_necro_assets_audit.test.ts @@ -0,0 +1,368 @@ +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, + TEETH_META, + TEETHEXPLODE_META, + BONESPEAR_META, + BONESPIRIT_META, + BONESPIRITEXPLODE_META, + POISONNOVA_META, + type MissileMeta, +} from '../../../src/render/missiles-meta.ts' +import { + OVERLAY_METAS, + ALL_OVERLAY_NAMES, + 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('Adv Coverage Audit — Necromancer Assets & Metadata Integrity', () => { + 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 NECRO_MISSILE_REQS = [ + { 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 }, + { name: 'revivesmall', celFile: 'Revive', meta: REVIVESMALL_META }, + { name: 'revivemedium', celFile: 'Revive', meta: REVIVEMEDIUM_META }, + { name: 'revivelarge', celFile: 'Revive', meta: REVIVELARGE_META }, + { name: 'bonespearexplode', celFile: 'teethexplode', meta: BONESPEAREXPLODE_META }, + { name: 'teeth', celFile: 'teethMissile', meta: TEETH_META }, + { name: 'teethexplode', celFile: 'teethexplode', meta: TEETHEXPLODE_META }, + { name: 'bonespear', celFile: 'BoneSpear', meta: BONESPEAR_META }, + { name: 'bonespirit', celFile: 'BoneSpirit', meta: BONESPIRIT_META }, + { name: 'bonespiritexplode', celFile: 'BoneSpiritExplode', meta: BONESPIRITEXPLODE_META }, + { name: 'poisonnova', celFile: 'poisonNova', meta: POISONNOVA_META }, + ] as const + + const NECRO_OVERLAY_REQS = [ + // 10 Curses overhead state icons + { 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 }, + // 2 Hit overlays + { 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 }, + // 3 Bone Armor overlays + { 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('Dimension 1: Feature Matrix & Skill Requirements Coverage', () => { + it('1.1 Verifies all 30 Necromancer skills required missiles are registered in MISSILE_METAS', () => { + for (const m of NECRO_MISSILE_REQS) { + expect(MISSILE_METAS[m.name], `Missile ${m.name} must be in MISSILE_METAS`).toBeDefined() + expect(MISSILE_METAS[m.name]).toBe(m.meta) + expect(m.meta.name).toBe(m.name) + expect(m.meta.celFile).toBe(m.celFile) + } + }) + + it('1.2 Verifies all 10 Curses overhead overlays, 2 hit overlays, and 3 Bone Armor overlays are in OVERLAY_METAS', () => { + for (const o of NECRO_OVERLAY_REQS) { + expect(OVERLAY_METAS[o.name], `Overlay ${o.name} must be in OVERLAY_METAS`).toBeDefined() + expect(OVERLAY_METAS[o.name]).toBe(o.meta) + expect(o.meta.name).toBe(o.name) + expect(o.meta.celFile).toBe(o.celFile) + expect(o.meta.preDraw).toBe(o.preDraw) + } + }) + + it('1.3 Verifies ALL_OVERLAY_NAMES contains all 15 Necromancer overlays', () => { + for (const o of NECRO_OVERLAY_REQS) { + expect(ALL_OVERLAY_NAMES).toContain(o.name) + } + }) + + it('1.4 Verifies bone armor back is preDraw:true while front and cast are preDraw:false', () => { + expect(BONE_ARMOR_BACK_OVERLAY_META.preDraw).toBe(true) + expect(BONE_ARMOR_FRONT_OVERLAY_META.preDraw).toBe(false) + expect(BONE_ARMOR_CAST_OVERLAY_META.preDraw).toBe(false) + }) + }) + + describe('Dimension 2: Zero Null / Missing / NaN Frame Arrays', () => { + it('2.1 Checks for zero null, undefined, or NaN values in missile frame geometry', () => { + for (const m of NECRO_MISSILE_REQS) { + const meta = m.meta + expect(meta.width).toBeGreaterThan(0) + expect(meta.height).toBeGreaterThan(0) + expect(meta.directions).toBeGreaterThan(0) + expect(meta.framesPerDirection).toBeGreaterThan(0) + expect(meta.animSpeed).toBeGreaterThan(0) + expect(meta.groups).toBeDefined() + expect(meta.groups.length).toBe(meta.directions) + + for (let d = 0; d < meta.groups.length; d++) { + const group = meta.groups[d]! + expect(group.length).toBe(meta.framesPerDirection) + for (let f = 0; f < group.length; f++) { + const frame = group[f]! + expect(frame.length).toBe(6) + for (let i = 0; i < 6; i++) { + const val = frame[i] + expect(val, `Null/undefined/NaN in ${m.name} dir ${d} frame ${f} idx ${i}`).not.toBeNull() + expect(val).not.toBeUndefined() + expect(typeof val).toBe('number') + expect(Number.isNaN(val)).toBe(false) + expect(Number.isFinite(val)).toBe(true) + } + 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(x + w).toBeLessThanOrEqual(meta.width) + expect(y + h).toBeLessThanOrEqual(meta.height) + expect(ax).toBeGreaterThanOrEqual(-1000) + expect(ax).toBeLessThanOrEqual(1000) + expect(ay).toBeGreaterThanOrEqual(-1000) + expect(ay).toBeLessThanOrEqual(1000) + } + } + } + }) + + it('2.2 Checks for zero null, undefined, or NaN values in overlay frame geometry', () => { + for (const o of NECRO_OVERLAY_REQS) { + const meta = o.meta + expect(meta.width).toBeGreaterThan(0) + expect(meta.height).toBeGreaterThan(0) + expect(meta.directions).toBe(1) + expect(meta.frames).toBeGreaterThan(0) + expect(meta.framesPerDirection).toBe(meta.frames) + expect(meta.animRate).toBeGreaterThan(0) + expect(meta.trans).toBe(3) + expect(meta.groups).toBeDefined() + expect(meta.groups.length).toBe(1) + + const group = meta.groups[0]! + expect(group.length).toBe(meta.frames) + for (let f = 0; f < group.length; f++) { + const frame = group[f]! + expect(frame.length).toBe(6) + for (let i = 0; i < 6; i++) { + const val = frame[i] + expect(val, `Null/undefined/NaN in ${o.name} frame ${f} idx ${i}`).not.toBeNull() + expect(val).not.toBeUndefined() + expect(typeof val).toBe('number') + expect(Number.isNaN(val)).toBe(false) + expect(Number.isFinite(val)).toBe(true) + } + 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(x + w).toBeLessThanOrEqual(meta.width) + expect(y + h).toBeLessThanOrEqual(meta.height) + expect(ax).toBeGreaterThanOrEqual(-1000) + expect(ax).toBeLessThanOrEqual(1000) + expect(ay).toBeGreaterThanOrEqual(-1000) + expect(ay).toBeLessThanOrEqual(1000) + } + } + }) + }) + + describe('Dimension 3: Physical Asset Disk Mirroring & File Health', () => { + it('3.1 Every Necromancer missile exists in public/missiles and samples/d2-packs/missiles', () => { + for (const m of NECRO_MISSILE_REQS) { + for (const dir of [publicMissilesDir, packMissilesDir]) { + const pngPath = path.join(dir, `${m.name}.png`) + const jsonPath = path.join(dir, `${m.name}.json`) + + expect(fs.existsSync(pngPath), `Missing ${pngPath}`).toBe(true) + expect(fs.existsSync(jsonPath), `Missing ${jsonPath}`).toBe(true) + + const stat = fs.statSync(pngPath) + expect(stat.size).toBeGreaterThan(500) + + const buf = fs.readFileSync(pngPath) + expect(buf.readUInt32BE(0)).toBe(0x89504e47) // \x89PNG + } + } + }) + + it('3.2 Every Necromancer overlay exists in public/overlays and samples/d2-packs/overlays', () => { + for (const o of NECRO_OVERLAY_REQS) { + for (const dir of [publicOverlaysDir, packOverlaysDir]) { + const pngPath = path.join(dir, `${o.name}.png`) + const jsonPath = path.join(dir, `${o.name}.json`) + + expect(fs.existsSync(pngPath), `Missing ${pngPath}`).toBe(true) + expect(fs.existsSync(jsonPath), `Missing ${jsonPath}`).toBe(true) + + const stat = fs.statSync(pngPath) + expect(stat.size).toBeGreaterThan(500) + + const buf = fs.readFileSync(pngPath) + expect(buf.readUInt32BE(0)).toBe(0x89504e47) // \x89PNG + } + } + }) + }) + + describe('Dimension 4: 32-bit RGBA & Transparent Border Thresholding Invariant', () => { + it('4.1 Asserts max(r,g,b) <= 4 implies alpha === 0 across all Necromancer missiles', () => { + for (const m of NECRO_MISSILE_REQS) { + const pngPath = path.join(publicMissilesDir, `${m.name}.png`) + const buf = fs.readFileSync(pngPath) + const { width, height, pixels } = decodeRgbaPng(buf) + + expect(width).toBe(m.meta.width) + expect(height).toBe(m.meta.height) + + let blackOpaqueCount = 0 + 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 mVal = Math.max(r, g, b) + + if (mVal <= 4 && a > 0) { + blackOpaqueCount++ + } + } + expect(blackOpaqueCount, `Dark halo pixels in ${m.name}.png`).toBe(0) + } + }) + + it('4.2 Asserts max(r,g,b) <= 4 implies alpha === 0 across all Necromancer overlays', () => { + for (const o of NECRO_OVERLAY_REQS) { + const pngPath = path.join(publicOverlaysDir, `${o.name}.png`) + const buf = fs.readFileSync(pngPath) + const { width, height, pixels } = decodeRgbaPng(buf) + + expect(width).toBe(o.meta.width) + expect(height).toBe(o.meta.height) + + let blackOpaqueCount = 0 + 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 mVal = Math.max(r, g, b) + + if (mVal <= 4 && a > 0) { + blackOpaqueCount++ + } + } + expect(blackOpaqueCount, `Dark halo pixels in ${o.name}.png`).toBe(0) + } + }) + }) +}) diff --git a/tests/skills/nec/necro-assets-adversarial-stress.test.ts b/tests/skills/nec/necro-assets-adversarial-stress.test.ts new file mode 100644 index 0000000..bb809d1 --- /dev/null +++ b/tests/skills/nec/necro-assets-adversarial-stress.test.ts @@ -0,0 +1,534 @@ +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) + } + }) + }) +})