374 lines
16 KiB
TypeScript
374 lines
16 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,
|
||
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/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/render/overlays-meta.ts'
|
||
import { BATCH1_SKILLS, ISO_GROUND_ASPECT_RATIO } from '../../../src/game/skills.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 — Amazon 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 entitiesDir = path.resolve(__dirname, '../../../samples/d2-packs/entities')
|
||
|
||
const AMAZON_SKILL_MATRIX = [
|
||
{ id: 6, name: 'Magic Arrow', anim: 'A1', weapon: 'bow', missile: 'magicarrow' },
|
||
{ id: 7, name: 'Fire Arrow', anim: 'A1', weapon: 'bow', missile: 'firearrow' },
|
||
{ id: 8, name: 'Inner Sight', anim: 'SC', overlay: 'cast_innersight' },
|
||
{ id: 9, name: 'Critical Strike', type: 'passive', overlay: 'doubledamage1' },
|
||
{ id: 10, name: 'Jab', anim: 'A1', weapon: '1ht' },
|
||
{ id: 11, name: 'Cold Arrow', anim: 'A1', weapon: 'bow', missile: 'coldarrow' },
|
||
{ id: 12, name: 'Multiple Shot', anim: 'A1', weapon: 'bow', missile: 'arrow' },
|
||
{ id: 13, name: 'Dodge', anim: 'S1', type: 'passive' },
|
||
{ id: 14, name: 'Power Strike', anim: 'A1', weapon: '1ht' },
|
||
{ id: 15, name: 'Poison Javelin', anim: 'TH', weapon: '1ht', missile: 'poisonjav' },
|
||
{ id: 16, name: 'Exploding Arrow', anim: 'A1', weapon: 'bow', missile: 'explodingarrow' },
|
||
{ id: 17, name: 'Slow Missiles', anim: 'SC', overlay: 'cast_slowmissiles' },
|
||
{ id: 18, name: 'Avoid', anim: 'S1', type: 'passive' },
|
||
{ id: 19, name: 'Impale', anim: 'A1', weapon: '1ht' },
|
||
{ id: 20, name: 'Lightning Bolt', anim: 'TH', weapon: '1ht', missile: 'lightningjavelin' },
|
||
{ id: 21, name: 'Ice Arrow', anim: 'A1', weapon: 'bow', missile: 'icearrow' },
|
||
{ id: 22, name: 'Guided Arrow', anim: 'A1', weapon: 'bow', missile: 'guidedarrow' },
|
||
{ id: 23, name: 'Penetrate', type: 'passive' },
|
||
{ id: 24, name: 'Charged Strike', anim: 'A1', weapon: '1ht', missile: 'chargedstrikebolt' },
|
||
{ id: 25, name: 'Plague Javelin', anim: 'TH', weapon: '1ht', missile: 'plaguejavelin' },
|
||
{ id: 26, name: 'Strafe', anim: 'A1', weapon: 'bow', missile: 'strafearrow' },
|
||
{ id: 27, name: 'Immolation Arrow', anim: 'A1', weapon: 'bow', missile: 'immolationarrow' },
|
||
{ id: 28, name: 'Decoy', anim: 'SC', overlay: 'dopllezon_appear' },
|
||
{ id: 29, name: 'Evade', anim: 'S1', type: 'passive' },
|
||
{ id: 30, name: 'Fend', anim: 'A1', weapon: '1ht' },
|
||
{ id: 31, name: 'Freezing Arrow', anim: 'A1', weapon: 'bow', missile: 'freezingarrow' },
|
||
{ id: 32, name: 'Valkyrie', anim: 'SC', overlay: 'valkyriestart', minion: 'valkyrie' },
|
||
{ id: 33, name: 'Pierce', type: 'passive' },
|
||
{ id: 34, name: 'Lightning Strike', anim: 'A1', weapon: '1ht', missile: 'lightningstrike' },
|
||
{ id: 35, name: 'Lightning Fury', anim: 'TH', weapon: '1ht', missile: 'lightningfury' },
|
||
] as const
|
||
|
||
const ALL_AMAZON_MISSILES: readonly { readonly name: string; readonly meta: MissileMeta; readonly trans: number }[] = [
|
||
{ name: 'magicarrow', meta: MAGICARROW_META, trans: 1 },
|
||
{ name: 'firearrow', meta: FIREARROW_META, trans: 1 },
|
||
{ name: 'icearrow', meta: ICEARROW_META, trans: 1 },
|
||
{ name: 'arrow', meta: ARROW_META, trans: 0 },
|
||
{ name: 'lightningjavelin', meta: LIGHTNINGJAVELIN_META, trans: 1 },
|
||
{ name: 'bolt', meta: BOLT_META, trans: 0 },
|
||
{ name: 'coldarrow', meta: COLDARROW_META, trans: 1 },
|
||
{ name: 'explodingarrow', meta: EXPLODINGARROW_META, trans: 1 },
|
||
{ name: 'explodingarrowexp', meta: EXPLODINGARROWEXP_META, trans: 1 },
|
||
{ name: 'icearrowexplode', meta: ICEARROWEXPLODE_META, trans: 1 },
|
||
{ name: 'freezingarrow', meta: FREEZINGARROW_META, trans: 1 },
|
||
{ name: 'guidedarrow', meta: GUIDEDARROW_META, trans: 1 },
|
||
{ name: 'strafearrow', meta: STRAFEARROW_META, trans: 0 },
|
||
{ name: 'strafebolt', meta: STRAFEBOLT_META, trans: 0 },
|
||
{ name: 'immolationarrow', meta: IMMOLATIONARROW_META, trans: 1 },
|
||
{ name: 'immolationarrowexp', meta: IMMOLATIONARROWEXP_META, trans: 1 },
|
||
{ name: 'immolationfire', meta: IMMOLATIONFIRE_META, trans: 1 },
|
||
{ name: 'poisonjav', meta: POISONJAV_META, trans: 0 },
|
||
{ name: 'poisonjavcloud', meta: POISONJAVCLOUD_META, trans: 1 },
|
||
{ name: 'chargedstrikebolt', meta: CHARGEDSTRIKEBOLT_META, trans: 1 },
|
||
{ name: 'plaguejavelin', meta: PLAGUEJAVELIN_META, trans: 0 },
|
||
{ name: 'plaguejavcloud', meta: PLAGUEJAVCLOUD_META, trans: 1 },
|
||
{ name: 'lightningfury', meta: LIGHTNINGFURY_META, trans: 1 },
|
||
{ name: 'furylightning', meta: FURYLIGHTNING_META, trans: 1 },
|
||
]
|
||
|
||
const ALL_AMAZON_OVERLAYS: readonly { readonly name: string; readonly meta: OverlayMeta }[] = [
|
||
{ name: 'cast_innersight', meta: CAST_INNERSIGHT_OVERLAY_META },
|
||
{ name: 'innersight', meta: INNERSIGHT_OVERLAY_META },
|
||
{ name: 'cast_slowmissiles', meta: CAST_SLOWMISSILES_OVERLAY_META },
|
||
{ name: 'handofathena', meta: HANDOFATHENA_OVERLAY_META },
|
||
{ name: 'valkyriestart', meta: VALKYRIESTART_OVERLAY_META },
|
||
{ name: 'valkyrie', meta: VALKYRIE_OVERLAY_META },
|
||
{ name: 'dopllezon_appear', meta: DOPLLEZON_APPEAR_OVERLAY_META },
|
||
{ name: 'doubledamage1', meta: DOUBLEDAMAGE1_OVERLAY_META },
|
||
{ name: 'doubledamage2', meta: DOUBLEDAMAGE2_OVERLAY_META },
|
||
{ name: 'doubledamage3', meta: DOUBLEDAMAGE3_OVERLAY_META },
|
||
]
|
||
|
||
// =========================================================================
|
||
// Dimension 1: Feature Matrix & 30-Skill Requirements Coverage
|
||
// =========================================================================
|
||
describe('Dimension 1: Feature Matrix & 30-Skill Requirements Coverage', () => {
|
||
it('1.1 Verifies all 30 Amazon skills (IDs 6–35) are mapped in requirement matrix', () => {
|
||
expect(AMAZON_SKILL_MATRIX.length).toBe(30)
|
||
for (let i = 0; i < 30; i++) {
|
||
expect(AMAZON_SKILL_MATRIX[i]!.id).toBe(6 + i)
|
||
}
|
||
})
|
||
|
||
it('1.2 Verifies every missile skill has a registered missile meta in MISSILE_METAS', () => {
|
||
for (const skill of AMAZON_SKILL_MATRIX) {
|
||
if ('missile' in skill && skill.missile) {
|
||
expect(MISSILE_METAS[skill.missile], `Missing missile meta for ${skill.name} (${skill.missile})`).toBeDefined()
|
||
}
|
||
}
|
||
})
|
||
|
||
it('1.3 Verifies every overlay skill has a registered overlay meta in OVERLAY_METAS', () => {
|
||
for (const skill of AMAZON_SKILL_MATRIX) {
|
||
if ('overlay' in skill && skill.overlay) {
|
||
expect(OVERLAY_METAS[skill.overlay], `Missing overlay meta for ${skill.name} (${skill.overlay})`).toBeDefined()
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
// =========================================================================
|
||
// Dimension 2: Zero Null / Missing / NaN Frame Arrays & Geometry
|
||
// =========================================================================
|
||
describe('Dimension 2: Zero Null / Missing / NaN Frame Arrays & Geometry', () => {
|
||
it('2.1 Asserts no NaN or Infinite coordinates in all Amazon missile frame bounding boxes', () => {
|
||
for (const m of ALL_AMAZON_MISSILES) {
|
||
expect(m.meta.groups.length).toBeGreaterThanOrEqual(1)
|
||
for (const grp of m.meta.groups) {
|
||
expect(grp.length).toBeGreaterThanOrEqual(1)
|
||
for (const [x, y, w, h, ax, ay] of grp) {
|
||
expect(Number.isFinite(x) && !Number.isNaN(x)).toBe(true)
|
||
expect(Number.isFinite(y) && !Number.isNaN(y)).toBe(true)
|
||
expect(Number.isFinite(w) && !Number.isNaN(w) && w > 0).toBe(true)
|
||
expect(Number.isFinite(h) && !Number.isNaN(h) && h > 0).toBe(true)
|
||
expect(Number.isFinite(ax) && !Number.isNaN(ax)).toBe(true)
|
||
expect(Number.isFinite(ay) && !Number.isNaN(ay)).toBe(true)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
it('2.2 Asserts no NaN or Infinite coordinates in all Amazon overlay frame bounding boxes', () => {
|
||
for (const o of ALL_AMAZON_OVERLAYS) {
|
||
expect(o.meta.groups.length).toBeGreaterThanOrEqual(1)
|
||
for (const grp of o.meta.groups) {
|
||
expect(grp.length).toBeGreaterThanOrEqual(1)
|
||
for (const [x, y, w, h, ax, ay] of grp) {
|
||
expect(Number.isFinite(x) && !Number.isNaN(x)).toBe(true)
|
||
expect(Number.isFinite(y) && !Number.isNaN(y)).toBe(true)
|
||
expect(Number.isFinite(w) && !Number.isNaN(w) && w > 0).toBe(true)
|
||
expect(Number.isFinite(h) && !Number.isNaN(h) && h > 0).toBe(true)
|
||
expect(Number.isFinite(ax) && !Number.isNaN(ax)).toBe(true)
|
||
expect(Number.isFinite(ay) && !Number.isNaN(ay)).toBe(true)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
// =========================================================================
|
||
// Dimension 3: Boundary & Coordinate Geometric Stress
|
||
// =========================================================================
|
||
describe('Dimension 3: Boundary & Coordinate Geometric Stress', () => {
|
||
it('3.1 Asserts anchor coordinates are bounded within valid sprite offset range [-1000, 1000]', () => {
|
||
for (const m of ALL_AMAZON_MISSILES) {
|
||
for (const grp of m.meta.groups) {
|
||
for (const [, , , , ax, ay] of grp) {
|
||
expect(ax).toBeGreaterThanOrEqual(-1000)
|
||
expect(ax).toBeLessThanOrEqual(1000)
|
||
expect(ay).toBeGreaterThanOrEqual(-1000)
|
||
expect(ay).toBeLessThanOrEqual(1000)
|
||
}
|
||
}
|
||
}
|
||
|
||
for (const o of ALL_AMAZON_OVERLAYS) {
|
||
for (const grp of o.meta.groups) {
|
||
for (const [, , , , ax, ay] of grp) {
|
||
expect(ax).toBeGreaterThanOrEqual(-1000)
|
||
expect(ax).toBeLessThanOrEqual(1000)
|
||
expect(ay).toBeGreaterThanOrEqual(-1000)
|
||
expect(ay).toBeLessThanOrEqual(1000)
|
||
}
|
||
}
|
||
}
|
||
})
|
||
|
||
it('3.2 Every Amazon missile exists in public/missiles and samples/d2-packs/missiles with valid PNG header', () => {
|
||
for (const m of ALL_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 ${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.3 Every Amazon overlay exists in public/overlays and samples/d2-packs/overlays with valid PNG header', () => {
|
||
for (const o of ALL_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 ${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
|
||
}
|
||
}
|
||
})
|
||
})
|
||
|
||
// =========================================================================
|
||
// Dimension 4: State Invariants & Trans/PreDraw Flags
|
||
// =========================================================================
|
||
describe('Dimension 4: State Invariants & Trans/PreDraw Flags', () => {
|
||
it('4.1 Valkyrie glow overlay has preDraw === true, all other Amazon overlays preDraw === false', () => {
|
||
expect(VALKYRIE_OVERLAY_META.preDraw).toBe(true)
|
||
expect(CAST_INNERSIGHT_OVERLAY_META.preDraw).toBe(false)
|
||
expect(INNERSIGHT_OVERLAY_META.preDraw).toBe(false)
|
||
expect(CAST_SLOWMISSILES_OVERLAY_META.preDraw).toBe(false)
|
||
expect(HANDOFATHENA_OVERLAY_META.preDraw).toBe(false)
|
||
expect(VALKYRIESTART_OVERLAY_META.preDraw).toBe(false)
|
||
expect(DOPLLEZON_APPEAR_OVERLAY_META.preDraw).toBe(false)
|
||
expect(DOUBLEDAMAGE1_OVERLAY_META.preDraw).toBe(false)
|
||
expect(DOUBLEDAMAGE2_OVERLAY_META.preDraw).toBe(false)
|
||
expect(DOUBLEDAMAGE3_OVERLAY_META.preDraw).toBe(false)
|
||
})
|
||
|
||
it('4.2 All Amazon overlays enforce trans === 3 (additive blending mode)', () => {
|
||
for (const o of ALL_AMAZON_OVERLAYS) {
|
||
expect(o.meta.trans).toBe(3)
|
||
}
|
||
})
|
||
})
|
||
|
||
// =========================================================================
|
||
// Dimension 5: System Regression, Cross-Class Isolation & Zero Runtime MPQ
|
||
// =========================================================================
|
||
describe('Dimension 5: System Regression, Cross-Class Isolation & Zero Runtime MPQ', () => {
|
||
it('5.1 Verifies metadata files contain zero runtime MPQ, DLL or fs imports', () => {
|
||
const missilesMetaPath = path.resolve(__dirname, '../../../src/render/missiles-meta.ts')
|
||
const overlaysMetaPath = path.resolve(__dirname, '../../../src/render/overlays-meta.ts')
|
||
|
||
for (const p of [missilesMetaPath, overlaysMetaPath]) {
|
||
const content = fs.readFileSync(p, 'utf-8')
|
||
expect(content).not.toContain('node:fs')
|
||
expect(content).not.toContain('src/mpq')
|
||
expect(content).not.toContain('.mpq')
|
||
expect(content).not.toContain('.dll')
|
||
}
|
||
})
|
||
|
||
it('5.2 Verifies BATCH1_SKILLS length invariant strictly maintained at 38', () => {
|
||
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
|
||
})
|
||
})
|
||
})
|