diablo2-web/tests/skills/pal/pal-assets.test.ts

490 lines
22 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,
HOLYBOLT_META,
BLESSEDHAMMER_META,
FISTOFTHEHEAVENSBOLT_META,
FOA_META,
BLESSEDHAMMEREX_META,
BLOOD1_META,
type MissileMeta,
} from '../../../src/render/missiles-meta.ts'
import {
OVERLAY_METAS,
ALL_OVERLAY_NAMES,
// 10 Offensive Auras
MIGHT_FRONT_AURA_META,
MIGHT_BACK_AURA_META,
HOLY_FIRE_FRONT_AURA_META,
HOLY_FIRE_BACK_AURA_META,
THORNS_FRONT_AURA_META,
THORNS_BACK_AURA_META,
BLESSED_AIM_FRONT_AURA_META,
BLESSED_AIM_BACK_AURA_META,
CONCENTRATION_FRONT_AURA_META,
CONCENTRATION_BACK_AURA_META,
HOLY_FREEZE_AURA_META,
HOLY_SHOCK_FRONT_AURA_META,
HOLY_SHOCK_BACK_AURA_META,
SANCTUARY_FRONT_AURA_META,
SANCTUARY_BACK_AURA_META,
FANATICISM_FRONT_AURA_META,
FANATICISM_BACK_AURA_META,
CONVICTION_AURA_META,
// 10 Defensive Auras
PRAYER_FRONT_AURA_META,
PRAYER_BACK_AURA_META,
RESIST_FIRE_AURA_META,
DEFIANCE_FRONT_AURA_META,
DEFIANCE_BACK_AURA_META,
RESIST_COLD_AURA_META,
CLEANSING_FRONT_AURA_META,
CLEANSING_BACK_AURA_META,
RESIST_LIGHTNING_AURA_META,
VIGOR_FRONT_AURA_META,
VIGOR_BACK_AURA_META,
MEDITATION_AURA_META,
REDEMPTION_AURA_META,
SALVATION_FRONT_AURA_META,
SALVATION_BACK_AURA_META,
// 16 Combat, Cast, Hit & State Overlays
CHARGE_OVERLAY_META,
CONVERSION_OVERLAY_META,
CONVERSION_CAST_OVERLAY_META,
HOLY_BOLT_CAST_OVERLAY_META,
FIST_OF_THE_HEAVENS_CAST_OVERLAY_META,
HAND_OF_GOD_OVERLAY_META,
SANCTUARY_KNOCKBACK_OVERLAY_META,
HOLY_FIRE_HIT_OVERLAY_META,
HOLY_FIRE_INDICATE_OVERLAY_META,
THORNS_HIT_OVERLAY_META,
THORNS_INDICATE_OVERLAY_META,
CAST_RESIST_FIRE_OVERLAY_META,
CAST_RESIST_COLD_OVERLAY_META,
CAST_RESIST_LIGHTNING_OVERLAY_META,
CAST_RESIST_ALL_OVERLAY_META,
STUN_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('Milestone M6 — Paladin 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 PALADIN_MISSILES = [
{ name: 'holybolt', celFile: 'HolyBoltMissile', meta: HOLYBOLT_META },
{ name: 'blessedhammer', celFile: 'blessedhammer', meta: BLESSEDHAMMER_META },
{ name: 'fistoftheheavensbolt', celFile: 'HolyBoltMissile', meta: FISTOFTHEHEAVENSBOLT_META },
{ name: 'foa', celFile: 'FOAorbiter', meta: FOA_META },
{ name: 'blessedhammerex', celFile: 'blessedhammer', meta: BLESSEDHAMMEREX_META },
{ name: 'blood1', celFile: 'BloodSmall01', meta: BLOOD1_META },
] as const
const PALADIN_AURAS = [
// 10 Offensive Auras
{ name: 'might_front', celFile: 'AuraMightFront', meta: MIGHT_FRONT_AURA_META, preDraw: false },
{ name: 'might_back', celFile: 'AuraMightBack', meta: MIGHT_BACK_AURA_META, preDraw: true },
{ name: 'holy_fire_front', celFile: 'AuraHolyFireFront', meta: HOLY_FIRE_FRONT_AURA_META, preDraw: false },
{ name: 'holy_fire_back', celFile: 'AuraHolyFireBack', meta: HOLY_FIRE_BACK_AURA_META, preDraw: true },
{ name: 'thorns_front', celFile: 'AuraThornsFront', meta: THORNS_FRONT_AURA_META, preDraw: false },
{ name: 'thorns_back', celFile: 'AuraThornsBack', meta: THORNS_BACK_AURA_META, preDraw: true },
{ name: 'blessed_aim_front', celFile: 'BlessedAim_front', meta: BLESSED_AIM_FRONT_AURA_META, preDraw: false },
{ name: 'blessed_aim_back', celFile: 'BlessedAim_back', meta: BLESSED_AIM_BACK_AURA_META, preDraw: true },
{ name: 'concentration_front', celFile: 'Concentration_front', meta: CONCENTRATION_FRONT_AURA_META, preDraw: false },
{ name: 'concentration_back', celFile: 'Concentration_back', meta: CONCENTRATION_BACK_AURA_META, preDraw: true },
{ name: 'holy_freeze', celFile: 'HolyFreeze', meta: HOLY_FREEZE_AURA_META, preDraw: true },
{ name: 'holy_shock_front', celFile: 'HolyShock_front', meta: HOLY_SHOCK_FRONT_AURA_META, preDraw: false },
{ name: 'holy_shock_back', celFile: 'HolyShock_back', meta: HOLY_SHOCK_BACK_AURA_META, preDraw: true },
{ name: 'sanctuary_front', celFile: 'SanctuaryAura_Front', meta: SANCTUARY_FRONT_AURA_META, preDraw: true },
{ name: 'sanctuary_back', celFile: 'SanctuaryAura_Back', meta: SANCTUARY_BACK_AURA_META, preDraw: true },
{ name: 'fanaticism_front', celFile: 'Fanatacism_Front', meta: FANATICISM_FRONT_AURA_META, preDraw: false },
{ name: 'fanaticism_back', celFile: 'Fanatacism_Back', meta: FANATICISM_BACK_AURA_META, preDraw: true },
{ name: 'conviction', celFile: 'Conviction', meta: CONVICTION_AURA_META, preDraw: true },
// 10 Defensive Auras
{ name: 'prayer_front', celFile: 'AuraPrayerFront', meta: PRAYER_FRONT_AURA_META, preDraw: false },
{ name: 'prayer_back', celFile: 'AuraPrayerBack', meta: PRAYER_BACK_AURA_META, preDraw: true },
{ name: 'resist_fire', celFile: 'AuraResistFireFront', meta: RESIST_FIRE_AURA_META, preDraw: true },
{ name: 'defiance_front', celFile: 'AuraDefianceFront', meta: DEFIANCE_FRONT_AURA_META, preDraw: false },
{ name: 'defiance_back', celFile: 'AuraDefianceBack', meta: DEFIANCE_BACK_AURA_META, preDraw: true },
{ name: 'resist_cold', celFile: 'AuraResistColdBack', meta: RESIST_COLD_AURA_META, preDraw: true },
{ name: 'cleansing_front', celFile: 'Cleansing_front', meta: CLEANSING_FRONT_AURA_META, preDraw: false },
{ name: 'cleansing_back', celFile: 'Cleansing_back', meta: CLEANSING_BACK_AURA_META, preDraw: true },
{ name: 'resist_lightning', celFile: 'AuraResistLightningBack', meta: RESIST_LIGHTNING_AURA_META, preDraw: true },
{ name: 'vigor_front', celFile: 'Stamina_front', meta: VIGOR_FRONT_AURA_META, preDraw: false },
{ name: 'vigor_back', celFile: 'Stamina_back', meta: VIGOR_BACK_AURA_META, preDraw: true },
{ name: 'meditation', celFile: 'Meditation', meta: MEDITATION_AURA_META, preDraw: false },
{ name: 'redemption', celFile: 'Redemption', meta: REDEMPTION_AURA_META, preDraw: true },
{ name: 'salvation_front', celFile: 'AuraResistAllFront', meta: SALVATION_FRONT_AURA_META, preDraw: false },
{ name: 'salvation_back', celFile: 'AuraResistAllBack', meta: SALVATION_BACK_AURA_META, preDraw: true },
] as const
const PALADIN_COMBAT_OVERLAYS = [
{ name: 'charge', celFile: 'Charge', meta: CHARGE_OVERLAY_META, preDraw: false },
{ name: 'conversion', celFile: 'Conversion', meta: CONVERSION_OVERLAY_META, preDraw: false },
{ name: 'conversioncast', celFile: 'AuraResistColdBackCast', meta: CONVERSION_CAST_OVERLAY_META, preDraw: false },
{ name: 'holy_bolt_cast', celFile: 'HolyBoltCloud', meta: HOLY_BOLT_CAST_OVERLAY_META, preDraw: false },
{ name: 'fist_of_the_heavens_cast', celFile: 'FOACast', meta: FIST_OF_THE_HEAVENS_CAST_OVERLAY_META, preDraw: false },
{ name: 'handofgod', celFile: 'HolyShockHit', meta: HAND_OF_GOD_OVERLAY_META, preDraw: false },
{ name: 'sanctuary_knockback', celFile: 'SanctuaryKnockback', meta: SANCTUARY_KNOCKBACK_OVERLAY_META, preDraw: false },
{ name: 'holy_fire_hit', celFile: 'AuraHolyFireHit', meta: HOLY_FIRE_HIT_OVERLAY_META, preDraw: false },
{ name: 'holy_fire_indicate', celFile: 'AuraHfireIndicate', meta: HOLY_FIRE_INDICATE_OVERLAY_META, preDraw: false },
{ name: 'thorns_hit', celFile: 'AuraThornsHit', meta: THORNS_HIT_OVERLAY_META, preDraw: false },
{ name: 'thorns_indicate', celFile: 'AuraHfireIndicate', meta: THORNS_INDICATE_OVERLAY_META, preDraw: false },
{ name: 'cast_resistfire', celFile: 'AuraResistFireFrontCast', meta: CAST_RESIST_FIRE_OVERLAY_META, preDraw: true },
{ name: 'cast_resistcold', celFile: 'AuraResistColdBackCast', meta: CAST_RESIST_COLD_OVERLAY_META, preDraw: false },
{ name: 'cast_resistlight', celFile: 'AuraResistLightningBackCast', meta: CAST_RESIST_LIGHTNING_OVERLAY_META, preDraw: false },
{ name: 'cast_resistall', celFile: 'AuraResistAllCast', meta: CAST_RESIST_ALL_OVERLAY_META, preDraw: true },
{ name: 'stun', celFile: 'Stun', meta: STUN_OVERLAY_META, preDraw: false },
] as const
const ALL_PALADIN_OVERLAYS = [...PALADIN_AURAS, ...PALADIN_COMBAT_OVERLAYS]
describe('Suite 1: Physical Asset Existence & File Mirroring', () => {
it('1.1 Verifies all 6 Paladin missile PNG and JSON files exist in public/missiles and samples/d2-packs/missiles', () => {
for (const m of PALADIN_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} file size must be > 1000 bytes`).toBeGreaterThan(1000)
const buf = fs.readFileSync(pngPath)
expect(buf[0]).toBe(0x89)
expect(buf[1]).toBe(0x50) // 'P'
expect(buf[2]).toBe(0x4e) // 'N'
expect(buf[3]).toBe(0x47) // 'G'
}
}
})
it('1.2 Verifies all Paladin overlay PNG and JSON files exist in public/overlays and samples/d2-packs/overlays', () => {
for (const o of ALL_PALADIN_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} file size must be > 1000 bytes`).toBeGreaterThan(1000)
const buf = fs.readFileSync(pngPath)
expect(buf[0]).toBe(0x89)
expect(buf[1]).toBe(0x50)
expect(buf[2]).toBe(0x4e)
expect(buf[3]).toBe(0x47)
}
}
})
})
describe('Suite 2: Metadata Registry & Frame Geometry Invariants', () => {
it('2.1 Verifies Paladin missiles metadata schema and geometry bounds', () => {
for (const m of PALADIN_MISSILES) {
const meta = MISSILE_METAS[m.name]
expect(meta, `MISSILE_METAS missing ${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).toBeGreaterThan(0)
expect(meta!.framesPerDirection).toBeGreaterThan(0)
expect(meta!.animSpeed).toBe(16)
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(x).toBeGreaterThanOrEqual(0)
expect(y).toBeGreaterThanOrEqual(0)
expect(x + w).toBeLessThanOrEqual(meta!.width)
expect(y + h).toBeLessThanOrEqual(meta!.height)
expect(w).toBeGreaterThan(0)
expect(h).toBeGreaterThan(0)
expect(typeof ax).toBe('number')
expect(typeof ay).toBe('number')
}
}
}
})
it('2.2 Verifies Paladin overlays metadata schema, frames, and preDraw flags', () => {
for (const o of ALL_PALADIN_OVERLAYS) {
const meta = OVERLAY_METAS[o.name]
expect(meta, `OVERLAY_METAS missing ${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).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!.preDraw).toBe(o.preDraw)
expect(meta!.groups.length).toBe(1)
expect(meta!.groups[0]?.length).toBe(meta!.frames)
for (const [x, y, w, h, ax, ay] of meta!.groups[0]!) {
expect(x).toBeGreaterThanOrEqual(0)
expect(y).toBeGreaterThanOrEqual(0)
expect(x + w).toBeLessThanOrEqual(meta!.width)
expect(y + h).toBeLessThanOrEqual(meta!.height)
expect(w).toBeGreaterThan(0)
expect(h).toBeGreaterThan(0)
expect(typeof ax).toBe('number')
expect(typeof ay).toBe('number')
}
}
})
it('2.3 Verifies CelFile and alias lookups in MISSILE_METAS and OVERLAY_METAS', () => {
// Missile CelFiles & aliases
expect(MISSILE_METAS['HolyBoltMissile']).toBe(HOLYBOLT_META)
expect(MISSILE_METAS['blessedhammer']).toBe(BLESSEDHAMMER_META)
expect(MISSILE_METAS['FOAorbiter']).toBe(FOA_META)
expect(MISSILE_METAS['BloodSmall01']).toBe(BLOOD1_META)
// Overlay CelFiles & aliases
expect(OVERLAY_METAS['Charge']).toBe(CHARGE_OVERLAY_META)
expect(OVERLAY_METAS['Conversion']).toBe(CONVERSION_OVERLAY_META)
expect(OVERLAY_METAS['conversionaura']).toBe(CONVERSION_OVERLAY_META)
expect(OVERLAY_METAS['HolyBoltCloud']).toBe(HOLY_BOLT_CAST_OVERLAY_META)
expect(OVERLAY_METAS['cast_undead']).toBe(HOLY_BOLT_CAST_OVERLAY_META)
expect(OVERLAY_METAS['FOACast']).toBe(FIST_OF_THE_HEAVENS_CAST_OVERLAY_META)
expect(OVERLAY_METAS['cast_fistofares']).toBe(FIST_OF_THE_HEAVENS_CAST_OVERLAY_META)
expect(OVERLAY_METAS['handofgod']).toBe(HAND_OF_GOD_OVERLAY_META)
expect(OVERLAY_METAS['fist_of_the_heavens_hit']).toBe(HAND_OF_GOD_OVERLAY_META)
expect(OVERLAY_METAS['SanctuaryKnockback']).toBe(SANCTUARY_KNOCKBACK_OVERLAY_META)
expect(OVERLAY_METAS['sanctuaryknockback']).toBe(SANCTUARY_KNOCKBACK_OVERLAY_META)
expect(OVERLAY_METAS['Stun']).toBe(STUN_OVERLAY_META)
expect(OVERLAY_METAS['stun']).toBe(STUN_OVERLAY_META)
})
})
describe('Suite 3: PreDraw & Aura Stacking Rules', () => {
it('3.1 Validates dual-layer preDraw invariant: back auras/rings are preDraw:true, front auras/hits are preDraw:false', () => {
// All back auras must be preDraw: true
const backAuras = [
MIGHT_BACK_AURA_META,
PRAYER_BACK_AURA_META,
HOLY_FIRE_BACK_AURA_META,
THORNS_BACK_AURA_META,
DEFIANCE_BACK_AURA_META,
BLESSED_AIM_BACK_AURA_META,
CLEANSING_BACK_AURA_META,
CONCENTRATION_BACK_AURA_META,
VIGOR_BACK_AURA_META,
HOLY_SHOCK_BACK_AURA_META,
SANCTUARY_BACK_AURA_META,
FANATICISM_BACK_AURA_META,
SALVATION_BACK_AURA_META,
]
for (const meta of backAuras) {
expect(meta.preDraw, `${meta.name} must have preDraw === true`).toBe(true)
}
// Ground rings must be preDraw: true
const groundRings = [
RESIST_FIRE_AURA_META,
RESIST_COLD_AURA_META,
RESIST_LIGHTNING_AURA_META,
HOLY_FREEZE_AURA_META,
CONVICTION_AURA_META,
REDEMPTION_AURA_META,
SANCTUARY_FRONT_AURA_META,
]
for (const meta of groundRings) {
expect(meta.preDraw, `${meta.name} must have preDraw === true`).toBe(true)
}
// Front auras must be preDraw: false
const frontAuras = [
MIGHT_FRONT_AURA_META,
PRAYER_FRONT_AURA_META,
HOLY_FIRE_FRONT_AURA_META,
THORNS_FRONT_AURA_META,
DEFIANCE_FRONT_AURA_META,
BLESSED_AIM_FRONT_AURA_META,
CLEANSING_FRONT_AURA_META,
CONCENTRATION_FRONT_AURA_META,
VIGOR_FRONT_AURA_META,
HOLY_SHOCK_FRONT_AURA_META,
FANATICISM_FRONT_AURA_META,
SALVATION_FRONT_AURA_META,
MEDITATION_AURA_META,
]
for (const meta of frontAuras) {
expect(meta.preDraw, `${meta.name} must have preDraw === false`).toBe(false)
}
// Combat hits, casts, and indicators
expect(CHARGE_OVERLAY_META.preDraw).toBe(false)
expect(CONVERSION_OVERLAY_META.preDraw).toBe(false)
expect(CONVERSION_CAST_OVERLAY_META.preDraw).toBe(false)
expect(HOLY_BOLT_CAST_OVERLAY_META.preDraw).toBe(false)
expect(FIST_OF_THE_HEAVENS_CAST_OVERLAY_META.preDraw).toBe(false)
expect(HAND_OF_GOD_OVERLAY_META.preDraw).toBe(false)
expect(SANCTUARY_KNOCKBACK_OVERLAY_META.preDraw).toBe(false)
expect(HOLY_FIRE_HIT_OVERLAY_META.preDraw).toBe(false)
expect(HOLY_FIRE_INDICATE_OVERLAY_META.preDraw).toBe(false)
expect(THORNS_HIT_OVERLAY_META.preDraw).toBe(false)
expect(THORNS_INDICATE_OVERLAY_META.preDraw).toBe(false)
expect(CAST_RESIST_FIRE_OVERLAY_META.preDraw).toBe(true)
expect(CAST_RESIST_COLD_OVERLAY_META.preDraw).toBe(false)
expect(CAST_RESIST_LIGHTNING_OVERLAY_META.preDraw).toBe(false)
expect(CAST_RESIST_ALL_OVERLAY_META.preDraw).toBe(true)
expect(STUN_OVERLAY_META.preDraw).toBe(false)
})
})
describe('Suite 4: 32-bit RGBA & Dark Halo Suppression (Zero Black Borders)', () => {
it('4.1 Verifies all baked Paladin missiles have 32-bit RGBA and zero dark boundary artifact pixels', () => {
for (const m of PALADIN_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
let gradientCount = 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 >= 30 && a > 0) {
visibleCount++
}
if (maxVal > 4 && maxVal < 60 && a > 0 && a < 255) {
gradientCount++
}
}
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)
expect(gradientCount, `${m.name}.png must contain anti-aliased edge gradient pixels`).toBeGreaterThan(0)
}
})
it('4.2 Verifies all baked Paladin overlays have 32-bit RGBA and zero dark boundary artifact pixels', () => {
for (const o of ALL_PALADIN_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 >= 30 && 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)
}
})
})
})