import { describe, expect, it } from 'vitest' import fs from 'node:fs' import path from 'node:path' import zlib from 'node:zlib' import { MISSILE_METAS, MAGICARROW_META, FIREARROW_META, ICEARROW_META, ARROW_META, LIGHTNINGJAVELIN_META, BOLT_META, COLDARROW_META, EXPLODINGARROW_META, EXPLODINGARROWEXP_META, ICEARROWEXPLODE_META, FREEZINGARROW_META, GUIDEDARROW_META, STRAFEARROW_META, STRAFEBOLT_META, IMMOLATIONARROW_META, IMMOLATIONARROWEXP_META, IMMOLATIONFIRE_META, POISONJAV_META, POISONJAVCLOUD_META, CHARGEDSTRIKEBOLT_META, PLAGUEJAVELIN_META, PLAGUEJAVCLOUD_META, LIGHTNINGFURY_META, FURYLIGHTNING_META, type MissileMeta, } from '../../../src/client/render/missiles-meta.ts' import { OVERLAY_METAS, ALL_OVERLAY_NAMES, CAST_INNERSIGHT_OVERLAY_META, INNERSIGHT_OVERLAY_META, CAST_SLOWMISSILES_OVERLAY_META, HANDOFATHENA_OVERLAY_META, VALKYRIESTART_OVERLAY_META, VALKYRIE_OVERLAY_META, DOPLLEZON_APPEAR_OVERLAY_META, DOUBLEDAMAGE1_OVERLAY_META, DOUBLEDAMAGE2_OVERLAY_META, DOUBLEDAMAGE3_OVERLAY_META, type OverlayMeta, } from '../../../src/client/render/overlays-meta.ts' import { BATCH1_SKILLS, ISO_GROUND_ASPECT_RATIO } from '../../../src/server/skills/skills.ts' // Strict PNG RGBA Decoder (validates colorType === 6, bitDepth === 8, and pixel transparency) 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('Milestone M11 — Amazon Asset Packaging & Registration', () => { 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 entitiesDir = path.resolve(__dirname, '../../../samples/d2-packs/entities') const AMAZON_MISSILES = [ { name: 'magicarrow', celFile: 'SafeArrow', meta: MAGICARROW_META, trans: 1 }, { name: 'firearrow', celFile: 'FireArrow', meta: FIREARROW_META, trans: 1 }, { name: 'icearrow', celFile: 'IceArrow', meta: ICEARROW_META, trans: 1 }, { name: 'arrow', celFile: 'Arrow', meta: ARROW_META, trans: 0 }, { name: 'lightningjavelin', celFile: 'LightningJavelin', meta: LIGHTNINGJAVELIN_META, trans: 1 }, { name: 'bolt', celFile: 'XBowBolt', meta: BOLT_META, trans: 0 }, { name: 'coldarrow', celFile: 'IceArrow', meta: COLDARROW_META, trans: 1 }, { name: 'explodingarrow', celFile: 'FireArrow', meta: EXPLODINGARROW_META, trans: 1 }, { name: 'explodingarrowexp', celFile: 'ExpArrowExplode', meta: EXPLODINGARROWEXP_META, trans: 1 }, { name: 'icearrowexplode', celFile: 'IceArrowExplode', meta: ICEARROWEXPLODE_META, trans: 1 }, { name: 'freezingarrow', celFile: 'IceArrow', meta: FREEZINGARROW_META, trans: 1 }, { name: 'guidedarrow', celFile: 'SafeArrow', meta: GUIDEDARROW_META, trans: 1 }, { name: 'strafearrow', celFile: 'Arrow', meta: STRAFEARROW_META, trans: 0 }, { name: 'strafebolt', celFile: 'XBowBolt', meta: STRAFEBOLT_META, trans: 0 }, { name: 'immolationarrow', celFile: 'FireArrow', meta: IMMOLATIONARROW_META, trans: 1 }, { name: 'immolationarrowexp', celFile: 'ExpArrowExplode', meta: IMMOLATIONARROWEXP_META, trans: 1 }, { name: 'immolationfire', celFile: 'groundFireBig', meta: IMMOLATIONFIRE_META, trans: 1 }, { name: 'poisonjav', celFile: 'Javelin', meta: POISONJAV_META, trans: 0 }, { name: 'poisonjavcloud', celFile: 'PoisonSparks', meta: POISONJAVCLOUD_META, trans: 1 }, { name: 'chargedstrikebolt', celFile: 'ChargedBolt', meta: CHARGEDSTRIKEBOLT_META, trans: 1 }, { name: 'plaguejavelin', celFile: 'Javelin', meta: PLAGUEJAVELIN_META, trans: 0 }, { name: 'plaguejavcloud', celFile: 'PoisonSparks', meta: PLAGUEJAVCLOUD_META, trans: 1 }, { name: 'lightningfury', celFile: 'LightningJavelin', meta: LIGHTNINGFURY_META, trans: 1 }, { name: 'furylightning', celFile: 'LightningStrike', meta: FURYLIGHTNING_META, trans: 1 }, ] as const const AMAZON_OVERLAYS = [ { name: 'cast_innersight', celFile: 'EOZCast', meta: CAST_INNERSIGHT_OVERLAY_META }, { name: 'innersight', celFile: 'HardenArmor', meta: INNERSIGHT_OVERLAY_META }, { name: 'cast_slowmissiles', celFile: 'HOAcast', meta: CAST_SLOWMISSILES_OVERLAY_META }, { name: 'handofathena', celFile: 'HOAmissileExplode', meta: HANDOFATHENA_OVERLAY_META }, { name: 'valkyriestart', celFile: 'ValkarieEmerge', meta: VALKYRIESTART_OVERLAY_META }, { name: 'valkyrie', celFile: 'ValkarieGlow', meta: VALKYRIE_OVERLAY_META }, { name: 'dopllezon_appear', celFile: 'DopplezonAppear', meta: DOPLLEZON_APPEAR_OVERLAY_META }, { name: 'doubledamage1', celFile: 'impacting01', meta: DOUBLEDAMAGE1_OVERLAY_META }, { name: 'doubledamage2', celFile: 'impacting02', meta: DOUBLEDAMAGE2_OVERLAY_META }, { name: 'doubledamage3', celFile: 'impacting03', meta: DOUBLEDAMAGE3_OVERLAY_META }, ] as const // ========================================================================= // Suite 1: Physical Asset Existence & File Mirroring // ========================================================================= describe('Suite 1: Physical Asset Existence & Dual Mirroring', () => { it('1.1 Verifies all 24 Amazon missiles exist in both public/ and samples/d2-packs/', () => { for (const m of AMAZON_MISSILES) { 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 missile PNG: ${pngPath}`).toBe(true) expect(fs.existsSync(jsonPath), `Missing missile JSON: ${jsonPath}`).toBe(true) const stat = fs.statSync(pngPath) expect(stat.size, `${pngPath} must be > 1000 bytes`).toBeGreaterThan(1000) const buf = fs.readFileSync(pngPath) expect(buf.readUInt32BE(0)).toBe(0x89504e47) // PNG signature } } }) it('1.2 Verifies all 10 Amazon overlays exist in both public/ and samples/d2-packs/', () => { for (const o of AMAZON_OVERLAYS) { 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 overlay PNG: ${pngPath}`).toBe(true) expect(fs.existsSync(jsonPath), `Missing overlay JSON: ${jsonPath}`).toBe(true) const stat = fs.statSync(pngPath) expect(stat.size, `${pngPath} must be > 1000 bytes`).toBeGreaterThan(1000) const buf = fs.readFileSync(pngPath) expect(buf.readUInt32BE(0)).toBe(0x89504e47) // PNG signature } } }) it('1.3 Verifies Valkyrie monster entity files exist in samples/d2-packs/entities/', () => { const r8Path = path.join(entitiesDir, 'monster-vk-hth.r8') const jsonPath = path.join(entitiesDir, 'monster-vk-hth.json') const indexPath = path.join(entitiesDir, 'index.json') expect(fs.existsSync(r8Path), `Missing ${r8Path}`).toBe(true) expect(fs.existsSync(jsonPath), `Missing ${jsonPath}`).toBe(true) expect(fs.existsSync(indexPath), `Missing ${indexPath}`).toBe(true) const r8Stat = fs.statSync(r8Path) expect(r8Stat.size).toBeGreaterThan(100000) const json = JSON.parse(fs.readFileSync(jsonPath, 'utf8')) expect(json.token).toBe('VK') expect(json.weapon).toBe('hth') expect(json.schema).toBe(2) const index = JSON.parse(fs.readFileSync(indexPath, 'utf8')) const vkEntry = index.entities.find((e: any) => e.key === 'vk-hth') expect(vkEntry, 'vk-hth must be indexed in entities/index.json').toBeDefined() expect(vkEntry.token).toBe('VK') expect(vkEntry.weapon).toBe('hth') }) }) // ========================================================================= // Suite 2: Metadata Registry & Frame Geometry Invariants // ========================================================================= describe('Suite 2: Metadata Registry & Frame Geometry Invariants', () => { it('2.1 Schema validation for all Amazon missiles in MISSILE_METAS', () => { for (const m of AMAZON_MISSILES) { const meta = MISSILE_METAS[m.name] expect(meta, `MISSILE_METAS must register ${m.name}`).toBeDefined() expect(meta.name).toBe(m.name) expect(meta.celFile).toBe(m.celFile) expect(meta.width).toBeGreaterThan(0) expect(meta.height).toBeGreaterThan(0) expect(meta.directions).toBeGreaterThanOrEqual(1) expect(meta.framesPerDirection).toBeGreaterThanOrEqual(1) expect(meta.animSpeed).toBeGreaterThan(0) expect(meta.groups.length).toBe(meta.directions) for (const group of meta.groups) { expect(group.length).toBe(meta.framesPerDirection) for (const [x, y, w, h, ax, ay] of group) { expect(Number.isFinite(x) && !Number.isNaN(x)).toBe(true) expect(Number.isFinite(y) && !Number.isNaN(y)).toBe(true) expect(w).toBeGreaterThan(0) expect(h).toBeGreaterThan(0) expect(x + w).toBeLessThanOrEqual(meta.width) expect(y + h).toBeLessThanOrEqual(meta.height) expect(Number.isFinite(ax) && !Number.isNaN(ax)).toBe(true) expect(Number.isFinite(ay) && !Number.isNaN(ay)).toBe(true) } } } }) it('2.2 Schema validation for all Amazon overlays in OVERLAY_METAS', () => { for (const o of AMAZON_OVERLAYS) { const meta = OVERLAY_METAS[o.name] expect(meta, `OVERLAY_METAS must register ${o.name}`).toBeDefined() expect(meta.name).toBe(o.name) expect(meta.celFile).toBe(o.celFile) expect(meta.width).toBeGreaterThan(0) expect(meta.height).toBeGreaterThan(0) expect(meta.directions).toBeGreaterThanOrEqual(1) expect(meta.frames).toBeGreaterThanOrEqual(1) expect(meta.animRate).toBeGreaterThan(0) expect(meta.trans).toBe(3) expect(meta.groups.length).toBe(meta.directions) // Valkyrie glow must be preDraw: true, all other Amazon overlays preDraw: false if (o.name === 'valkyrie') { expect(meta.preDraw, 'Valkyrie glow overlay must have preDraw === true').toBe(true) } else { expect(meta.preDraw, `${o.name} must have preDraw === false`).toBe(false) } for (const group of meta.groups) { expect(group.length).toBe(meta.framesPerDirection) for (const [x, y, w, h, ax, ay] of group) { expect(Number.isFinite(x) && !Number.isNaN(x)).toBe(true) expect(Number.isFinite(y) && !Number.isNaN(y)).toBe(true) expect(w).toBeGreaterThan(0) expect(h).toBeGreaterThan(0) expect(x + w).toBeLessThanOrEqual(meta.width) expect(y + h).toBeLessThanOrEqual(meta.height) expect(Number.isFinite(ax) && !Number.isNaN(ax)).toBe(true) expect(Number.isFinite(ay) && !Number.isNaN(ay)).toBe(true) } } } }) }) // ========================================================================= // Suite 3: Zero-Black-Border & Additive Blending Quality // ========================================================================= describe('Suite 3: Zero-Black-Border & Additive Blending Quality', () => { it('3.1 Verifies all baked Amazon missiles have 32-bit RGBA and zero dark boundary artifact pixels', () => { for (const m of AMAZON_MISSILES) { const pngPath = path.join(publicMissilesDir, `${m.name}.png`) const buf = fs.readFileSync(pngPath) const decoded = decodeRgbaPng(buf) expect(decoded.colorType, `${m.name}.png must be 32-bit RGBA (colorType 6)`).toBe(6) expect(decoded.bitDepth, `${m.name}.png bitDepth must be 8`).toBe(8) let blackOpaqueCount = 0 let visibleCount = 0 for (let i = 0; i < decoded.pixels.length; i += 4) { const r = decoded.pixels[i]! const g = decoded.pixels[i + 1]! const b = decoded.pixels[i + 2]! const a = decoded.pixels[i + 3]! const maxVal = Math.max(r, g, b) // Threshold invariant: For translucent missiles (trans: 1), max(r,g,b) <= 4 must have alpha === 0 if (m.trans === 1 && maxVal <= 4 && a > 0) { blackOpaqueCount++ } if (maxVal >= 20 && a > 0) { visibleCount++ } } if (m.trans === 1) { expect(blackOpaqueCount, `${m.name}.png contains ${blackOpaqueCount} dark boundary artifact pixels`).toBe(0) } expect(visibleCount, `${m.name}.png must contain visible spell pixels`).toBeGreaterThan(0) } }) it('3.2 Verifies all baked Amazon overlays have 32-bit RGBA and zero dark boundary artifact pixels', () => { for (const o of AMAZON_OVERLAYS) { const pngPath = path.join(publicOverlaysDir, `${o.name}.png`) const buf = fs.readFileSync(pngPath) const decoded = decodeRgbaPng(buf) expect(decoded.colorType, `${o.name}.png must be 32-bit RGBA (colorType 6)`).toBe(6) expect(decoded.bitDepth, `${o.name}.png bitDepth must be 8`).toBe(8) let blackOpaqueCount = 0 let visibleCount = 0 for (let i = 0; i < decoded.pixels.length; i += 4) { const r = decoded.pixels[i]! const g = decoded.pixels[i + 1]! const b = decoded.pixels[i + 2]! const a = decoded.pixels[i + 3]! const maxVal = Math.max(r, g, b) // Threshold invariant: max(r,g,b) <= 4 must have alpha === 0 if (maxVal <= 4 && a > 0) { blackOpaqueCount++ } if (maxVal >= 20 && a > 0) { visibleCount++ } } expect(blackOpaqueCount, `${o.name}.png contains ${blackOpaqueCount} dark boundary artifact pixels`).toBe(0) expect(visibleCount, `${o.name}.png must contain visible overlay pixels`).toBeGreaterThan(0) } }) }) // ========================================================================= // Suite 4: Animation & Mode Tokens Invariants // ========================================================================= describe('Suite 4: Animation & Mode Tokens Invariants', () => { it('4.1 Valkyrie monster pack has valid animation clips and modes', () => { const jsonPath = path.join(entitiesDir, 'monster-vk-hth.json') const json = JSON.parse(fs.readFileSync(jsonPath, 'utf8')) const expectedClips = ['nu', 'wl', 'rn', 'a1', 'gh', 'dd', 'dt'] for (const clip of expectedClips) { expect(json.clips[clip], `Valkyrie must contain clip '${clip}'`).toBeDefined() expect(json.clips[clip].directions).toBeGreaterThanOrEqual(1) expect(json.clips[clip].frames).toBeGreaterThanOrEqual(1) } expect(json.standFrames).toBeGreaterThanOrEqual(1) expect(json.walkFrames).toBeGreaterThanOrEqual(1) expect(json.width).toBe(4096) expect(json.height).toBeGreaterThan(0) }) }) // ========================================================================= // Suite 5: Anti-Regression Invariants // ========================================================================= describe('Suite 5: Anti-Regression Invariants', () => { it('5.1 Enforces BATCH1_SKILLS length strictly at 38 skills', () => { expect(Object.keys(BATCH1_SKILLS).length).toBe(38) }) it('5.2 Enforces ISO_GROUND_ASPECT_RATIO === 0.5', () => { expect(ISO_GROUND_ASPECT_RATIO).toBe(0.5) }) }) })