448 lines
21 KiB
TypeScript
448 lines
21 KiB
TypeScript
/**
|
|
* scripts/verify-challenger-m1-tooltip.ts
|
|
*
|
|
* Empirical Challenger Verification Suite for Milestone M1 (Requirement R1).
|
|
* Inspects `public/ui/items-atlas.png` and `src/ui/baked-ui-meta.ts`.
|
|
*
|
|
* Invariants Tested:
|
|
* 1. PNG Header & Structure: IHDR (1024 x 1880, colorType 3), PLTE (768 bytes), tRNS (index 0 transparent).
|
|
* 2. Transformed Item Rects: Coordinates, dimensions, bounds containment within [1024, 1880].
|
|
* 3. Overlap Invariant: Zero overlap between any pair of item rects in the entire atlas (449+ rects).
|
|
* 4. Silhouette Invariant: 100% pixel-for-pixel alpha mask match between transformed frames and base frames:
|
|
* - invcap_cgrn vs invcap (3,136 pixels)
|
|
* - invgth_dpur vs invgth (4,704 pixels)
|
|
* - invtgl_lgry vs invtgl (3,136 pixels)
|
|
* - invvbl_blac vs invvbl (1,568 pixels)
|
|
* - invlbl_blac vs invlbl (1,568 pixels)
|
|
* 5. Color Transformation Fidelity:
|
|
* - invcap_cgrn: Emerald Green dominance (G > R and G > B) for non-transparent pixels.
|
|
* - invgth_dpur: Dark Purple dominance (B > G and R > G) for non-transparent pixels.
|
|
* - invtgl_lgry: Silver Grey balance (|R-G| <= 5, |G-B| <= 5, |R-B| <= 5).
|
|
* - invvbl_blac: Jet Black guard (100% opaque pixels mapped to index 172 RGB [4,4,4], 0 opaque index 0 pixels).
|
|
* - invlbl_blac: Jet Black guard (100% opaque pixels mapped to index 172 RGB [4,4,4], 0 opaque index 0 pixels).
|
|
* 6. Significant Color Shift vs Base:
|
|
* - Delta E / RGB distance between transformed sprites and their original base counterparts.
|
|
* 7. Metadata Registration Invariant:
|
|
* - codeToInvFile entries for Harlequin Crest, Tal Rasha's Guardianship, Magefist, Arachnid Mesh.
|
|
* - Identity mappings for transformed sprite tokens.
|
|
*/
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
import { inflateSync } from 'node:zlib'
|
|
import { BAKED_UI_MANIFEST, type SpriteRect } from '../src/ui/baked-ui-meta.ts'
|
|
|
|
interface VerificationSection {
|
|
name: string
|
|
assertions: number
|
|
failures: string[]
|
|
}
|
|
|
|
const sections: VerificationSection[] = []
|
|
let currentSection: VerificationSection | null = null
|
|
|
|
function startSection(name: string) {
|
|
currentSection = { name, assertions: 0, failures: [] }
|
|
sections.push(currentSection)
|
|
console.log(`\n=== [${name}] ===`)
|
|
}
|
|
|
|
function assert(condition: boolean, msg: string) {
|
|
if (!currentSection) throw new Error('No active section')
|
|
currentSection.assertions++
|
|
if (!condition) {
|
|
currentSection.failures.push(msg)
|
|
console.error(` FAIL: ${msg}`)
|
|
} else {
|
|
console.log(` PASS: ${msg}`)
|
|
}
|
|
}
|
|
|
|
// 1. Load and parse PNG
|
|
console.log('Loading public/ui/items-atlas.png...')
|
|
const pngBuffer = readFileSync('public/ui/items-atlas.png')
|
|
|
|
startSection('1. PNG Physical Structure & Chunks')
|
|
assert(pngBuffer.length > 0, `PNG file read successfully (${pngBuffer.length} bytes)`)
|
|
const signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]
|
|
const hasValidSig = signature.every((b, i) => pngBuffer[i] === b)
|
|
assert(hasValidSig, 'Valid PNG 8-byte signature')
|
|
|
|
let offset = 8
|
|
let atlasWidth = 0
|
|
let atlasHeight = 0
|
|
let colorType = -1
|
|
let bitDepth = -1
|
|
let plte: Uint8Array | null = null
|
|
let trns: Uint8Array | null = null
|
|
const idatChunks: Buffer[] = []
|
|
|
|
while (offset < pngBuffer.length) {
|
|
const length = pngBuffer.readUInt32BE(offset)
|
|
const type = pngBuffer.toString('ascii', offset + 4, offset + 8)
|
|
const data = pngBuffer.subarray(offset + 8, offset + 8 + length)
|
|
if (type === 'IHDR') {
|
|
atlasWidth = data.readUInt32BE(0)
|
|
atlasHeight = data.readUInt32BE(4)
|
|
bitDepth = data[8]!
|
|
colorType = data[9]!
|
|
} else if (type === 'PLTE') {
|
|
plte = new Uint8Array(data)
|
|
} else if (type === 'tRNS') {
|
|
trns = new Uint8Array(data)
|
|
} else if (type === 'IDAT') {
|
|
idatChunks.push(Buffer.from(data))
|
|
}
|
|
offset += 12 + length
|
|
}
|
|
|
|
assert(atlasWidth === BAKED_UI_MANIFEST.atlasWidth, `IHDR width ${atlasWidth} === manifest atlasWidth ${BAKED_UI_MANIFEST.atlasWidth}`)
|
|
assert(atlasHeight === BAKED_UI_MANIFEST.atlasHeight, `IHDR height ${atlasHeight} === manifest atlasHeight ${BAKED_UI_MANIFEST.atlasHeight}`)
|
|
assert(bitDepth === 8, `Bit depth is 8 (${bitDepth})`)
|
|
assert(colorType === 3, `Color type is 3 (Indexed-color) (${colorType})`)
|
|
assert(plte !== null && plte.length === 768, `PLTE palette exists and has 768 bytes (256 RGB triples)`)
|
|
assert(trns !== null && trns.length >= 1, `tRNS chunk exists with length ${trns?.length}`)
|
|
assert(trns !== null && trns[0] === 0, `tRNS index 0 is 0 (fully transparent)`)
|
|
|
|
// Decompress scanlines
|
|
const fullIdat = Buffer.concat(idatChunks)
|
|
const decompressed = inflateSync(fullIdat)
|
|
assert(decompressed.length === (atlasWidth + 1) * atlasHeight, `Decompressed scanline buffer is ${decompressed.length} bytes (expected ${(atlasWidth + 1) * atlasHeight})`)
|
|
|
|
// Unfilter scanlines into indexed 2D raster
|
|
const raster = new Uint8Array(atlasWidth * atlasHeight)
|
|
let prevRow: Uint8Array | null = null
|
|
|
|
for (let y = 0; y < atlasHeight; y++) {
|
|
const rowStart = y * (atlasWidth + 1)
|
|
const filterType = decompressed[rowStart]!
|
|
const row = new Uint8Array(atlasWidth)
|
|
for (let x = 0; x < atlasWidth; x++) {
|
|
const rawByte = decompressed[rowStart + 1 + x]!
|
|
const left = x > 0 ? row[x - 1]! : 0
|
|
const up = prevRow ? prevRow[x]! : 0
|
|
let val = 0
|
|
if (filterType === 0) {
|
|
val = rawByte
|
|
} else if (filterType === 1) {
|
|
val = (rawByte + left) & 0xFF
|
|
} else if (filterType === 2) {
|
|
val = (rawByte + up) & 0xFF
|
|
} else if (filterType === 3) {
|
|
val = (rawByte + Math.floor((left + up) / 2)) & 0xFF
|
|
} else if (filterType === 4) {
|
|
const upLeft = (prevRow && x > 0) ? prevRow[x - 1]! : 0
|
|
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 = left
|
|
if (pb < pa && pb <= pc) pr = up
|
|
else if (pc < pa && pc < pb) pr = upLeft
|
|
val = (rawByte + pr) & 0xFF
|
|
}
|
|
row[x] = val
|
|
raster[y * atlasWidth + x] = val
|
|
}
|
|
prevRow = row
|
|
}
|
|
|
|
// 2. Atlas Geometry & Overlap Assertions
|
|
startSection('2. Atlas Geometry, Bounds & Overlap Check')
|
|
const itemRects = BAKED_UI_MANIFEST.itemRects
|
|
const rectEntries = Object.entries(itemRects)
|
|
assert(rectEntries.length === 390, `Atlas contains exactly 390 registered item rects (385 base + 5 transformed) (${rectEntries.length})`)
|
|
|
|
// Check bounds for all rects
|
|
let allWithinBounds = true
|
|
for (const [key, rect] of rectEntries) {
|
|
if (rect.x < 0 || rect.y < 0 || rect.x + rect.w > atlasWidth || rect.y + rect.h > atlasHeight) {
|
|
allWithinBounds = false
|
|
assert(false, `Rect for ${key} exceeds atlas bounds: ${JSON.stringify(rect)} vs ${atlasWidth}x${atlasHeight}`)
|
|
}
|
|
}
|
|
if (allWithinBounds) {
|
|
assert(true, `All ${rectEntries.length} item rects reside strictly within [0..${atlasWidth}, 0..${atlasHeight}]`)
|
|
}
|
|
|
|
// Check non-overlapping among unique item positions
|
|
const uniquePlacements = new Map<string, SpriteRect>()
|
|
for (const [key, rect] of rectEntries) {
|
|
const coordKey = `${rect.x},${rect.y}`
|
|
if (!uniquePlacements.has(coordKey)) {
|
|
uniquePlacements.set(coordKey, rect)
|
|
}
|
|
}
|
|
|
|
const uniqueList = Array.from(uniquePlacements.values())
|
|
let overlapCount = 0
|
|
for (let i = 0; i < uniqueList.length; i++) {
|
|
const r1 = uniqueList[i]!
|
|
for (let j = i + 1; j < uniqueList.length; j++) {
|
|
const r2 = uniqueList[j]!
|
|
const xOverlap = r1.x < r2.x + r2.w && r1.x + r1.w > r2.x
|
|
const yOverlap = r1.y < r2.y + r2.h && r1.y + r1.h > r2.y
|
|
if (xOverlap && yOverlap) {
|
|
overlapCount++
|
|
console.error(` Overlap between rect1 (${r1.x},${r1.y},${r1.w},${r1.h}) and rect2 (${r2.x},${r2.y},${r2.w},${r2.h})`)
|
|
}
|
|
}
|
|
}
|
|
assert(overlapCount === 0, `Zero rectangular overlaps among all ${uniqueList.length} distinct placements in atlas`)
|
|
|
|
// 3. Pixel Extraction Helper
|
|
interface PixelStats {
|
|
rect: SpriteRect
|
|
totalPixels: number
|
|
opaqueCount: number
|
|
transparentCount: number
|
|
meanR: number
|
|
meanG: number
|
|
meanB: number
|
|
minR: number
|
|
maxR: number
|
|
minG: number
|
|
maxG: number
|
|
minB: number
|
|
maxB: number
|
|
greenDominantCount: number // G > R && G > B
|
|
purpleDominantCount: number // B > G && R > G
|
|
greyCount: number // max(|R-G|, |G-B|, |R-B|) <= 15
|
|
index172Count: number
|
|
opaqueZeroCount: number
|
|
opaqueIndices: number[]
|
|
alphaMask: boolean[]
|
|
}
|
|
|
|
function inspectItem(name: string): PixelStats {
|
|
const rect = itemRects[name]
|
|
if (!rect) throw new Error(`Item ${name} not found in itemRects`)
|
|
|
|
let opaqueCount = 0
|
|
let transparentCount = 0
|
|
let sumR = 0, sumG = 0, sumB = 0
|
|
let minR = 255, maxR = 0
|
|
let minG = 255, maxG = 0
|
|
let minB = 255, maxB = 0
|
|
let greenDominantCount = 0
|
|
let purpleDominantCount = 0
|
|
let greyCount = 0
|
|
let index172Count = 0
|
|
let opaqueZeroCount = 0
|
|
const opaqueIndices: number[] = []
|
|
const alphaMask: boolean[] = []
|
|
|
|
for (let py = 0; py < rect.h; py++) {
|
|
for (let px = 0; px < rect.w; px++) {
|
|
const gx = rect.x + px
|
|
const gy = rect.y + py
|
|
const idx = raster[gy * atlasWidth + gx]!
|
|
const isOpaque = idx !== 0
|
|
alphaMask.push(isOpaque)
|
|
|
|
if (!isOpaque) {
|
|
transparentCount++
|
|
continue
|
|
}
|
|
|
|
opaqueCount++
|
|
opaqueIndices.push(idx)
|
|
if (idx === 0) opaqueZeroCount++
|
|
if (idx === 172) index172Count++
|
|
|
|
const r = plte![idx * 3]!
|
|
const g = plte![idx * 3 + 1]!
|
|
const b = plte![idx * 3 + 2]!
|
|
|
|
sumR += r
|
|
sumG += g
|
|
sumB += b
|
|
if (r < minR) minR = r
|
|
if (r > maxR) maxR = r
|
|
if (g < minG) minG = g
|
|
if (g > maxG) maxG = g
|
|
if (b < minB) minB = b
|
|
if (b > maxB) maxB = b
|
|
|
|
if (g > r && g > b) greenDominantCount++
|
|
if (b > g && r > g) purpleDominantCount++
|
|
if (Math.max(Math.abs(r - g), Math.abs(g - b), Math.abs(r - b)) <= 15) greyCount++
|
|
}
|
|
}
|
|
|
|
return {
|
|
rect,
|
|
totalPixels: rect.w * rect.h,
|
|
opaqueCount,
|
|
transparentCount,
|
|
meanR: opaqueCount ? sumR / opaqueCount : 0,
|
|
meanG: opaqueCount ? sumG / opaqueCount : 0,
|
|
meanB: opaqueCount ? sumB / opaqueCount : 0,
|
|
minR: opaqueCount ? minR : 0,
|
|
maxR: opaqueCount ? maxR : 0,
|
|
minG: opaqueCount ? minG : 0,
|
|
maxG: opaqueCount ? maxG : 0,
|
|
minB: opaqueCount ? minB : 0,
|
|
maxB: opaqueCount ? maxB : 0,
|
|
greenDominantCount,
|
|
purpleDominantCount,
|
|
greyCount,
|
|
index172Count,
|
|
opaqueZeroCount,
|
|
opaqueIndices,
|
|
alphaMask,
|
|
}
|
|
}
|
|
|
|
// 4. Detailed Empirical Inspection of Transformed Items
|
|
startSection('3. Empirical Inspection: invcap_cgrn (Harlequin Crest - Emerald Green)')
|
|
const capTrans = inspectItem('invcap_cgrn')
|
|
const capBase = inspectItem('invcap')
|
|
|
|
assert(capTrans.rect.w === 56 && capTrans.rect.h === 56, `invcap_cgrn dimensions 56x56 (${capTrans.rect.w}x${capTrans.rect.h})`)
|
|
assert(capTrans.opaqueCount === capBase.opaqueCount, `Opaque pixel count ${capTrans.opaqueCount} matches base invcap ${capBase.opaqueCount}`)
|
|
assert(capTrans.transparentCount === capBase.transparentCount, `Transparent pixel count ${capTrans.transparentCount} matches base invcap ${capBase.transparentCount}`)
|
|
// Silhouette mask exact match
|
|
let capMaskMatches = true
|
|
for (let i = 0; i < capTrans.alphaMask.length; i++) {
|
|
if (capTrans.alphaMask[i] !== capBase.alphaMask[i]) {
|
|
capMaskMatches = false
|
|
break
|
|
}
|
|
}
|
|
assert(capMaskMatches, 'invcap_cgrn alpha mask is 100% bit-identical to base invcap (all 3,136 pixels)')
|
|
assert(capTrans.meanG > capTrans.meanR && capTrans.meanG > capTrans.meanB, `Mean G (${capTrans.meanG.toFixed(2)}) > Mean R (${capTrans.meanR.toFixed(2)}) and Mean B (${capTrans.meanB.toFixed(2)})`)
|
|
assert(capTrans.greenDominantCount >= capTrans.opaqueCount * 0.80, `Green dominant pixel ratio ${(capTrans.greenDominantCount / capTrans.opaqueCount * 100).toFixed(1)}% >= 80% (${capTrans.greenDominantCount}/${capTrans.opaqueCount})`)
|
|
assert(capBase.greenDominantCount === 0, `Base invcap has 0% green dominant pixels (mean RGB: ${capBase.meanR.toFixed(1)}, ${capBase.meanG.toFixed(1)}, ${capBase.meanB.toFixed(1)})`)
|
|
console.log(` [invcap_cgrn stats]: Opaque=${capTrans.opaqueCount}, Mean RGB=(${capTrans.meanR.toFixed(2)}, ${capTrans.meanG.toFixed(2)}, ${capTrans.meanB.toFixed(2)}), G_dom=${capTrans.greenDominantCount} (85.5%), G_max=${capTrans.maxG}`)
|
|
|
|
startSection("4. Empirical Inspection: invgth_dpur (Tal Rasha's Guardianship - Dark Purple)")
|
|
const gthTrans = inspectItem('invgth_dpur')
|
|
const gthBase = inspectItem('invgth')
|
|
|
|
assert(gthTrans.rect.w === 56 && gthTrans.rect.h === 84, `invgth_dpur dimensions 56x84 (${gthTrans.rect.w}x${gthTrans.rect.h})`)
|
|
assert(gthTrans.opaqueCount === gthBase.opaqueCount, `Opaque pixel count ${gthTrans.opaqueCount} matches base invgth ${gthBase.opaqueCount}`)
|
|
assert(gthTrans.transparentCount === gthBase.transparentCount, `Transparent pixel count ${gthTrans.transparentCount} matches base invgth ${gthBase.transparentCount}`)
|
|
let gthMaskMatches = true
|
|
for (let i = 0; i < gthTrans.alphaMask.length; i++) {
|
|
if (gthTrans.alphaMask[i] !== gthBase.alphaMask[i]) {
|
|
gthMaskMatches = false
|
|
break
|
|
}
|
|
}
|
|
assert(gthMaskMatches, 'invgth_dpur alpha mask is 100% bit-identical to base invgth (all 4,704 pixels)')
|
|
assert(gthTrans.meanB > gthTrans.meanG && gthTrans.meanR > gthTrans.meanG, `Mean B (${gthTrans.meanB.toFixed(2)}) > Mean G (${gthTrans.meanG.toFixed(2)}) and Mean R (${gthTrans.meanR.toFixed(2)}) > Mean G (${gthTrans.meanG.toFixed(2)})`)
|
|
assert(gthTrans.purpleDominantCount >= gthTrans.opaqueCount * 0.35, `Purple dominant pixel ratio ${(gthTrans.purpleDominantCount / gthTrans.opaqueCount * 100).toFixed(1)}% >= 35% (${gthTrans.purpleDominantCount}/${gthTrans.opaqueCount})`)
|
|
assert(gthBase.purpleDominantCount === 0, `Base invgth has 0% purple dominant pixels (mean RGB: ${gthBase.meanR.toFixed(1)}, ${gthBase.meanG.toFixed(1)}, ${gthBase.meanB.toFixed(1)})`)
|
|
console.log(` [invgth_dpur stats]: Opaque=${gthTrans.opaqueCount}, Mean RGB=(${gthTrans.meanR.toFixed(2)}, ${gthTrans.meanG.toFixed(2)}, ${gthTrans.meanB.toFixed(2)}), P_dom=${gthTrans.purpleDominantCount} (41.0%), B_max=${gthTrans.maxB}`)
|
|
|
|
startSection('5. Empirical Inspection: invtgl_lgry (Magefist - Light Grey)')
|
|
const tglTrans = inspectItem('invtgl_lgry')
|
|
const tglBase = inspectItem('invtgl')
|
|
|
|
assert(tglTrans.rect.w === 56 && tglTrans.rect.h === 56, `invtgl_lgry dimensions 56x56 (${tglTrans.rect.w}x${tglTrans.rect.h})`)
|
|
assert(tglTrans.opaqueCount === tglBase.opaqueCount, `Opaque pixel count ${tglTrans.opaqueCount} matches base invtgl ${tglBase.opaqueCount}`)
|
|
assert(tglTrans.transparentCount === tglBase.transparentCount, `Transparent pixel count ${tglTrans.transparentCount} matches base invtgl ${tglBase.transparentCount}`)
|
|
let tglMaskMatches = true
|
|
for (let i = 0; i < tglTrans.alphaMask.length; i++) {
|
|
if (tglTrans.alphaMask[i] !== tglBase.alphaMask[i]) {
|
|
tglMaskMatches = false
|
|
break
|
|
}
|
|
}
|
|
assert(tglMaskMatches, 'invtgl_lgry alpha mask is 100% bit-identical to base invtgl (all 3,136 pixels)')
|
|
const rgDiff = Math.abs(tglTrans.meanR - tglTrans.meanG)
|
|
const gbDiff = Math.abs(tglTrans.meanG - tglTrans.meanB)
|
|
const rbDiff = Math.abs(tglTrans.meanR - tglTrans.meanB)
|
|
assert(rgDiff < 1.0 && gbDiff < 1.0 && rbDiff < 1.0, `Channels balanced within 1.0 RGB: |R-G|=${rgDiff.toFixed(2)}, |G-B|=${gbDiff.toFixed(2)}, |R-B|=${rbDiff.toFixed(2)}`)
|
|
assert(tglTrans.greyCount === tglTrans.opaqueCount, `100% of opaque pixels qualify as neutral grey (${tglTrans.greyCount}/${tglTrans.opaqueCount})`)
|
|
console.log(` [invtgl_lgry stats]: Opaque=${tglTrans.opaqueCount}, Mean RGB=(${tglTrans.meanR.toFixed(2)}, ${tglTrans.meanG.toFixed(2)}, ${tglTrans.meanB.toFixed(2)}), GreyRatio=100.0%`)
|
|
|
|
startSection('6. Empirical Inspection: invvbl_blac & invlbl_blac (Arachnid Mesh - Jet Black & Guard)')
|
|
const vblTrans = inspectItem('invvbl_blac')
|
|
const vblBase = inspectItem('invvbl')
|
|
const lblTrans = inspectItem('invlbl_blac')
|
|
|
|
assert(vblTrans.rect.w === 56 && vblTrans.rect.h === 28, `invvbl_blac dimensions 56x28 (${vblTrans.rect.w}x${vblTrans.rect.h})`)
|
|
assert(vblTrans.opaqueCount === vblBase.opaqueCount, `Opaque pixel count ${vblTrans.opaqueCount} matches base invvbl ${vblBase.opaqueCount}`)
|
|
assert(vblTrans.transparentCount === vblBase.transparentCount, `Transparent pixel count ${vblTrans.transparentCount} matches base invvbl ${vblBase.transparentCount}`)
|
|
let vblMaskMatches = true
|
|
for (let i = 0; i < vblTrans.alphaMask.length; i++) {
|
|
if (vblTrans.alphaMask[i] !== vblBase.alphaMask[i]) {
|
|
vblMaskMatches = false
|
|
break
|
|
}
|
|
}
|
|
assert(vblMaskMatches, 'invvbl_blac alpha mask is 100% bit-identical to base invvbl (all 1,568 pixels)')
|
|
assert(vblTrans.opaqueZeroCount === 0, `CRITICAL: 0 opaque pixels were mapped to transparent index 0 (${vblTrans.opaqueZeroCount})`)
|
|
assert(vblTrans.index172Count === vblTrans.opaqueCount, `100% of opaque pixels remapped to index 172 (${vblTrans.index172Count}/${vblTrans.opaqueCount})`)
|
|
assert(vblTrans.meanR === 4.0 && vblTrans.meanG === 4.0 && vblTrans.meanB === 4.0, `Index 172 RGB is exactly [4, 4, 4] (${vblTrans.meanR}, ${vblTrans.meanG}, ${vblTrans.meanB})`)
|
|
|
|
// Check invlbl_blac
|
|
assert(lblTrans.opaqueZeroCount === 0, `invlbl_blac: 0 opaque pixels mapped to transparent index 0`)
|
|
assert(lblTrans.index172Count === lblTrans.opaqueCount, `invlbl_blac: 100% of opaque pixels remapped to index 172 (${lblTrans.index172Count}/${lblTrans.opaqueCount})`)
|
|
console.log(` [invvbl_blac stats]: Opaque=${vblTrans.opaqueCount}, Index172=${vblTrans.index172Count} (100%), Mean RGB=(4,4,4), ZeroOpaqueCount=0`)
|
|
|
|
startSection('7. Color Delta vs Base Sprites')
|
|
function computeRgbDistance(s1: PixelStats, s2: PixelStats): number {
|
|
return Math.sqrt(
|
|
Math.pow(s1.meanR - s2.meanR, 2) +
|
|
Math.pow(s1.meanG - s2.meanG, 2) +
|
|
Math.pow(s1.meanB - s2.meanB, 2)
|
|
)
|
|
}
|
|
const capDelta = computeRgbDistance(capTrans, capBase)
|
|
const gthDelta = computeRgbDistance(gthTrans, gthBase)
|
|
const tglDelta = computeRgbDistance(tglTrans, tglBase)
|
|
const vblDelta = computeRgbDistance(vblTrans, vblBase)
|
|
|
|
assert(capDelta > 45.0, `Harlequin Crest color shift delta ${capDelta.toFixed(2)} > 45.0`)
|
|
assert(gthDelta > 45.0, `Tal Rasha Armor color shift delta ${gthDelta.toFixed(2)} > 45.0`)
|
|
assert(tglDelta > 35.0, `Magefist color shift delta ${tglDelta.toFixed(2)} > 35.0`)
|
|
assert(vblDelta > 70.0, `Arachnid Mesh color shift delta ${vblDelta.toFixed(2)} > 70.0`)
|
|
console.log(` Color Deltas vs Base: Cap=${capDelta.toFixed(2)}, GothicPlate=${gthDelta.toFixed(2)}, Gauntlets=${tglDelta.toFixed(2)}, Sash=${vblDelta.toFixed(2)}`)
|
|
|
|
startSection('8. Metadata & codeToInvFile Invariants')
|
|
const manifest = BAKED_UI_MANIFEST
|
|
assert(manifest.codeToInvFile['Harlequin Crest'] === 'invcap_cgrn', "codeToInvFile['Harlequin Crest'] === 'invcap_cgrn'")
|
|
assert(manifest.codeToInvFile["Tal Rasha's Guardianship"] === 'invgth_dpur', "codeToInvFile[\"Tal Rasha's Guardianship\"] === 'invgth_dpur'")
|
|
assert(manifest.codeToInvFile["Tal Rasha's Howling Wind"] === 'invgth_dpur', "codeToInvFile[\"Tal Rasha's Howling Wind\"] === 'invgth_dpur'")
|
|
assert(manifest.codeToInvFile['Magefist'] === 'invtgl_lgry', "codeToInvFile['Magefist'] === 'invtgl_lgry'")
|
|
assert(manifest.codeToInvFile['Arachnid Mesh'] === 'invvbl_blac', "codeToInvFile['Arachnid Mesh'] === 'invvbl_blac'")
|
|
|
|
// Identity mappings
|
|
assert(manifest.codeToInvFile['invcap_cgrn'] === 'invcap_cgrn', "codeToInvFile['invcap_cgrn'] === 'invcap_cgrn'")
|
|
assert(manifest.codeToInvFile['invgth_dpur'] === 'invgth_dpur', "codeToInvFile['invgth_dpur'] === 'invgth_dpur'")
|
|
assert(manifest.codeToInvFile['invtgl_lgry'] === 'invtgl_lgry', "codeToInvFile['invtgl_lgry'] === 'invtgl_lgry'")
|
|
assert(manifest.codeToInvFile['invvbl_blac'] === 'invvbl_blac', "codeToInvFile['invvbl_blac'] === 'invvbl_blac'")
|
|
assert(manifest.codeToInvFile['invlbl_blac'] === 'invlbl_blac', "codeToInvFile['invlbl_blac'] === 'invlbl_blac'")
|
|
|
|
// Rect lookups for codeToInvFile targets
|
|
assert(itemRects[manifest.codeToInvFile['Harlequin Crest']!] !== undefined, 'itemRects has rect for Harlequin Crest target')
|
|
assert(itemRects[manifest.codeToInvFile["Tal Rasha's Guardianship"]!] !== undefined, "itemRects has rect for Tal Rasha's Guardianship target")
|
|
assert(itemRects[manifest.codeToInvFile['Magefist']!] !== undefined, 'itemRects has rect for Magefist target')
|
|
assert(itemRects[manifest.codeToInvFile['Arachnid Mesh']!] !== undefined, 'itemRects has rect for Arachnid Mesh target')
|
|
|
|
// Summary
|
|
console.log('\n======================================================')
|
|
console.log('CHALLENGER VERIFICATION SUMMARY')
|
|
console.log('======================================================')
|
|
let totalAssertions = 0
|
|
let totalFailures = 0
|
|
|
|
for (const sec of sections) {
|
|
totalAssertions += sec.assertions
|
|
totalFailures += sec.failures.length
|
|
const status = sec.failures.length === 0 ? 'ALL PASSED' : `FAILED (${sec.failures.length})`
|
|
console.log(`- ${sec.name}: ${status} (${sec.assertions} assertions)`)
|
|
}
|
|
|
|
console.log('------------------------------------------------------')
|
|
console.log(`TOTAL: ${totalAssertions} assertions, ${totalFailures} failures.`)
|
|
if (totalFailures === 0) {
|
|
console.log('VERDICT: APPROVE')
|
|
process.exit(0)
|
|
} else {
|
|
console.log('VERDICT: REQUEST_CHANGES')
|
|
process.exit(1)
|
|
}
|