diablo2-web/tests/skills/dru/adv-dru-assets-audit.test.ts

300 lines
12 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,
MOLTENBOULDER_META,
MOLTENBOULDEREMERGE_META,
FIRESTORMMAKER_META,
ARCTICBLAST1_META,
ARCTICBLAST2_META,
TWISTER_META,
TORNADO_META,
VOLCANOBASE_META,
VOLCANOFIRE_META,
VOLCANOROCK_META,
SHOCKWAVE_META,
ARMAGEDDONROCK_META,
ARMAGEDDONTAIL_META,
HURRICANESWOOSH_META,
HURRICANEROCK_META,
HURRICANETREE_META,
} from '../../../src/render/missiles-meta.ts'
import {
OVERLAY_METAS,
DRUID_FIRE_CAST_1_OVERLAY_META,
DRUID_FIRE_CAST_2_OVERLAY_META,
DRUIDMORPH_OVERLAY_META,
CYCLONEARMOR1FRONT_OVERLAY_META,
CYCLONEARMOR1BACK_OVERLAY_META,
CYCLONEARMOR2FRONT_OVERLAY_META,
CYCLONEARMOR2BACK_OVERLAY_META,
CYCLONEARMOR3FRONT_OVERLAY_META,
CYCLONEARMOR3BACK_OVERLAY_META,
RABIESPLAGUE_OVERLAY_META,
MAUL1FRONT_OVERLAY_META,
MAUL2FRONT_OVERLAY_META,
MAUL3FRONT_OVERLAY_META,
MAUL4FRONT_OVERLAY_META,
MAUL5FRONT_OVERLAY_META,
MAUL1BACK_OVERLAY_META,
MAUL2BACK_OVERLAY_META,
MAUL3BACK_OVERLAY_META,
MAUL4BACK_OVERLAY_META,
MAUL5BACK_OVERLAY_META,
FERALRAGE1FRONT_OVERLAY_META,
FERALRAGE2FRONT_OVERLAY_META,
FERALRAGE3FRONT_OVERLAY_META,
FERALRAGE4FRONT_OVERLAY_META,
FERALRAGE5FRONT_OVERLAY_META,
FERALRAGE1BACK_OVERLAY_META,
FERALRAGE2BACK_OVERLAY_META,
FERALRAGE3BACK_OVERLAY_META,
FERALRAGE4BACK_OVERLAY_META,
FERALRAGE5BACK_OVERLAY_META,
DIREWOLFCHARGED_OVERLAY_META,
} from '../../../src/render/overlays-meta.ts'
import { BATCH1_SKILLS } from '../../../src/game/skills.ts'
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('Adversarial Stress Test: Druid Asset Pipeline & Invariants (Milestone M21)', () => {
const publicMissilesDir = path.resolve(__dirname, '../../../public/missiles')
const publicOverlaysDir = path.resolve(__dirname, '../../../public/overlays')
const DRUID_MISSILES = [
{ name: 'moltenboulder', celFile: 'expansion/MoltenBoulderRoll', meta: MOLTENBOULDER_META },
{ name: 'moltenboulderemerge', celFile: 'expansion/MoltenBoulderEmerge', meta: MOLTENBOULDEREMERGE_META },
{ name: 'firestormmaker', celFile: 'expansion/FirestormEmitter', meta: FIRESTORMMAKER_META },
{ name: 'arcticblast1', celFile: 'expansion/ArcticBlastIce', meta: ARCTICBLAST1_META },
{ name: 'arcticblast2', celFile: 'expansion/ArcticBlastIce_2', meta: ARCTICBLAST2_META },
{ name: 'twister', celFile: 'expansion/twister', meta: TWISTER_META },
{ name: 'tornado', celFile: 'expansion/tornado', meta: TORNADO_META },
{ name: 'volcanobase', celFile: 'Expansion/volcano_base', meta: VOLCANOBASE_META },
{ name: 'volcanofire', celFile: 'Expansion/volcano_Fire', meta: VOLCANOFIRE_META },
{ name: 'volcanorock', celFile: 'expansion/volcano_rock', meta: VOLCANOROCK_META },
{ name: 'shockwave', celFile: 'expansion/shockwave', meta: SHOCKWAVE_META },
{ name: 'armageddonrock', celFile: 'expansion/armageddon_rock', meta: ARMAGEDDONROCK_META },
{ name: 'armageddontail', celFile: 'expansion/armageddon_fire', meta: ARMAGEDDONTAIL_META },
{ name: 'hurricaneswoosh', celFile: 'expansion/hurricane_swoosh', meta: HURRICANESWOOSH_META },
{ name: 'hurricanerock', celFile: 'expansion/hurricane_rocks', meta: HURRICANEROCK_META },
{ name: 'hurricanetree', celFile: 'expansion/hurricane_tree', meta: HURRICANETREE_META },
] as const
const DRUID_OVERLAYS = [
{ name: 'druid_fire_cast_1', celFile: 'Expansion/Fire_Overlay_A', meta: DRUID_FIRE_CAST_1_OVERLAY_META },
{ name: 'druid_fire_cast_2', celFile: 'Expansion/Fire_Overlay_B', meta: DRUID_FIRE_CAST_2_OVERLAY_META },
{ name: 'druidmorph', celFile: 'expansion/druidmorph', meta: DRUIDMORPH_OVERLAY_META },
{ name: 'cyclonearmor1front', celFile: 'Expansion/CycloneArmorAFront', meta: CYCLONEARMOR1FRONT_OVERLAY_META },
{ name: 'cyclonearmor1back', celFile: 'Expansion/CycloneArmorABack', meta: CYCLONEARMOR1BACK_OVERLAY_META },
{ name: 'cyclonearmor2front', celFile: 'Expansion/CycloneArmorBFront', meta: CYCLONEARMOR2FRONT_OVERLAY_META },
{ name: 'cyclonearmor2back', celFile: 'Expansion/CycloneArmorBBack', meta: CYCLONEARMOR2BACK_OVERLAY_META },
{ name: 'cyclonearmor3front', celFile: 'Expansion/CycloneArmorCFront', meta: CYCLONEARMOR3FRONT_OVERLAY_META },
{ name: 'cyclonearmor3back', celFile: 'Expansion/CycloneArmorCBack', meta: CYCLONEARMOR3BACK_OVERLAY_META },
{ name: 'rabiesplague', celFile: 'Expansion/rabies', meta: RABIESPLAGUE_OVERLAY_META },
{ name: 'maul1front', celFile: 'expansion/Maul_A_Front', meta: MAUL1FRONT_OVERLAY_META },
{ name: 'maul2front', celFile: 'expansion/Maul_B_Front', meta: MAUL2FRONT_OVERLAY_META },
{ name: 'maul3front', celFile: 'expansion/Maul_C_Front', meta: MAUL3FRONT_OVERLAY_META },
{ name: 'maul4front', celFile: 'expansion/Maul_D_Front', meta: MAUL4FRONT_OVERLAY_META },
{ name: 'maul5front', celFile: 'expansion/Maul_E_Front', meta: MAUL5FRONT_OVERLAY_META },
{ name: 'maul1back', celFile: 'expansion/Maul_A_Back', meta: MAUL1BACK_OVERLAY_META },
{ name: 'maul2back', celFile: 'expansion/Maul_B_Back', meta: MAUL2BACK_OVERLAY_META },
{ name: 'maul3back', celFile: 'expansion/Maul_C_Back', meta: MAUL3BACK_OVERLAY_META },
{ name: 'maul4back', celFile: 'expansion/Maul_D_Back', meta: MAUL4BACK_OVERLAY_META },
{ name: 'maul5back', celFile: 'expansion/Maul_E_Back', meta: MAUL5BACK_OVERLAY_META },
{ name: 'feralrage1front', celFile: 'expansion/feral_rage_A_Front', meta: FERALRAGE1FRONT_OVERLAY_META },
{ name: 'feralrage2front', celFile: 'expansion/feral_rage_B_Front', meta: FERALRAGE2FRONT_OVERLAY_META },
{ name: 'feralrage3front', celFile: 'expansion/feral_rage_C_Front', meta: FERALRAGE3FRONT_OVERLAY_META },
{ name: 'feralrage4front', celFile: 'expansion/feral_rage_D_Front', meta: FERALRAGE4FRONT_OVERLAY_META },
{ name: 'feralrage5front', celFile: 'expansion/feral_rage_E_Front', meta: FERALRAGE5FRONT_OVERLAY_META },
{ name: 'feralrage1back', celFile: 'expansion/feral_rage_A_Back', meta: FERALRAGE1BACK_OVERLAY_META },
{ name: 'feralrage2back', celFile: 'expansion/feral_rage_B_Back', meta: FERALRAGE2BACK_OVERLAY_META },
{ name: 'feralrage3back', celFile: 'expansion/feral_rage_C_Back', meta: FERALRAGE3BACK_OVERLAY_META },
{ name: 'feralrage4back', celFile: 'expansion/feral_rage_D_Back', meta: FERALRAGE4BACK_OVERLAY_META },
{ name: 'feralrage5back', celFile: 'expansion/feral_rage_E_Back', meta: FERALRAGE5BACK_OVERLAY_META },
{ name: 'direwolfcharged', celFile: 'expansion/DireWolfCharged', meta: DIREWOLFCHARGED_OVERLAY_META },
] as const
it('A1: Stress audits frame coordinates within atlas boundaries for all 16 Druid missiles', () => {
for (const m of DRUID_MISSILES) {
const meta = m.meta
expect(meta.width).toBeGreaterThan(0)
expect(meta.height).toBeGreaterThan(0)
expect(meta.directions).toBeGreaterThan(0)
expect(meta.groups.length).toBe(meta.directions)
for (let d = 0; d < meta.directions; d++) {
const frames = meta.groups[d]!
expect(frames.length).toBe(meta.framesPerDirection)
for (let f = 0; f < frames.length; f++) {
const [x, y, w, h, ax, ay] = frames[f]!
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(Number.isFinite(ax)).toBe(true)
expect(Number.isFinite(ay)).toBe(true)
}
}
}
})
it('A2: Stress audits overlay frame bounding boxes and positive dimensional envelopes', () => {
for (const o of DRUID_OVERLAYS) {
const meta = o.meta
expect(meta.width).toBeGreaterThan(0)
expect(meta.height).toBeGreaterThan(0)
expect(meta.frames).toBeGreaterThan(0)
expect(meta.box).toBeDefined()
expect(meta.box.width).toBeGreaterThan(0)
expect(meta.box.height).toBeGreaterThan(0)
for (const dirGroup of meta.groups) {
for (const frame of dirGroup) {
const [x, y, w, h] = 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)
}
}
}
})
it('A3: Adversarial pixel audit - No black edge artifacts or rogue alpha channels on dark backgrounds for additive missiles & overlays', () => {
const ADDITIVE_MISSILES = [
'firestormmaker',
'arcticblast1',
'arcticblast2',
'twister',
'tornado',
'volcanofire',
'volcanorock',
'shockwave',
'armageddontail',
'hurricaneswoosh',
]
for (const name of ADDITIVE_MISSILES) {
const filePath = path.join(publicMissilesDir, `${name}.png`)
const buf = fs.readFileSync(filePath)
const { pixels } = decodeRgbaPng(buf)
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]!
// For additive missiles, semi-black/fringe (max(r,g,b) <= 4) MUST be transparent (a === 0)
if (Math.max(r, g, b) <= 4 && a > 0) {
blackOpaqueCount++
}
}
expect(blackOpaqueCount, `Missile ${name} has dark border fringe`).toBe(0)
}
for (const o of DRUID_OVERLAYS) {
const filePath = path.join(publicOverlaysDir, `${o.name}.png`)
const buf = fs.readFileSync(filePath)
const { pixels } = decodeRgbaPng(buf)
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]!
if (Math.max(r, g, b) <= 4 && a > 0) {
blackOpaqueCount++
}
}
expect(blackOpaqueCount, `Overlay ${o.name} has dark border fringe`).toBe(0)
}
})
it('A4: Parity Invariant - BATCH1_SKILLS length remains strictly 38', () => {
expect(Object.keys(BATCH1_SKILLS).length).toBe(38)
})
it('A5: Zero Runtime MPQ Invariant - No runtime MPQ / DLL dependencies needed for Druid rendering', () => {
for (const m of DRUID_MISSILES) {
expect(MISSILE_METAS[m.name]).toBeDefined()
expect(MISSILE_METAS[m.name]!.groups.length).toBeGreaterThan(0)
}
for (const o of DRUID_OVERLAYS) {
expect(OVERLAY_METAS[o.name]).toBeDefined()
expect(OVERLAY_METAS[o.name]!.groups.length).toBeGreaterThan(0)
}
})
})