import { describe, expect, it } from 'vitest' import fs from 'node:fs' import path from 'node:path' import { execSync } from 'node:child_process' import { MISSILE_METAS } from '../src/render/missiles-meta.ts' import { OVERLAY_METAS, LIGHTNING_OVERLAY_META, FROZEN_ARMOR_OVERLAY_META, FROZEN_ARMOR_HIT_OVERLAY_META, ENERGY_SHIELD_OVERLAY_META, ENERGY_SHIELD_HIT_OVERLAY_META, THUNDERSTORM_CAST_OVERLAY_META, THUNDERSTORM_BACK_OVERLAY_META, ENCHANT_OVERLAY_META, ICE_EXPLODE_OVERLAY_META, } from '../src/render/overlays-meta.ts' describe('Milestone M0 — 9 Sorceress Skill & Hit Overlays Parity', () => { const publicDir = path.resolve(__dirname, '../public/overlays') const packDir = path.resolve(__dirname, '../samples/d2-packs/overlays') const TARGETS = [ { name: 'lightning', meta: LIGHTNING_OVERLAY_META, celFile: 'ShockHitSm', frames: 20, trans: 3, preDraw: false }, { name: 'frozenarmor', meta: FROZEN_ARMOR_OVERLAY_META, celFile: 'FrozenArmor', frames: 24, trans: 3, preDraw: false }, { name: 'frozenarmor_hit', meta: FROZEN_ARMOR_HIT_OVERLAY_META, celFile: 'AuraResistColdCast', frames: 11, trans: 3, preDraw: true }, { name: 'energyshield', meta: ENERGY_SHIELD_OVERLAY_META, celFile: 'ManaShieldBall', frames: 8, trans: 5, preDraw: false }, { name: 'energyshieldhit0', meta: ENERGY_SHIELD_HIT_OVERLAY_META, celFile: 'ManaShieldDir00', frames: 9, trans: 3, preDraw: false }, { name: 'thunderstormcast', meta: THUNDERSTORM_CAST_OVERLAY_META, celFile: 'ThunderstormCast', frames: 10, trans: 3, preDraw: false }, { name: 'thunderstormback', meta: THUNDERSTORM_BACK_OVERLAY_META, celFile: 'ThunderstormCast_operate', frames: 19, trans: 3, preDraw: true }, { name: 'enchant', meta: ENCHANT_OVERLAY_META, celFile: 'FireEnchant', frames: 17, trans: 3, preDraw: false }, { name: 'ice_explode', meta: ICE_EXPLODE_OVERLAY_META, celFile: 'IceExplode', frames: 16, trans: 3, preDraw: false }, ] it('1. Verifies physical PNG and JSON files exist in public/overlays/ and samples/d2-packs/overlays/', () => { for (const t of TARGETS) { for (const dir of [publicDir, packDir]) { const pngPath = path.join(dir, `${t.name}.png`) const jsonPath = path.join(dir, `${t.name}.json`) expect(fs.existsSync(pngPath), `Missing ${pngPath}`).toBe(true) expect(fs.existsSync(jsonPath), `Missing ${jsonPath}`).toBe(true) expect(fs.statSync(pngPath).size).toBeGreaterThan(1000) const raw = JSON.parse(fs.readFileSync(jsonPath, 'utf-8')) expect(raw.name).toBe(t.name) expect(raw.celFile).toBe(t.celFile) expect(raw.frames).toBe(t.frames) expect(raw.trans).toBe(t.trans) expect(raw.preDraw).toBe(t.preDraw) } } }) it('2. Verifies typed metadata constants and properties match 1.13c ground truth', () => { for (const t of TARGETS) { expect(t.meta.name).toBe(t.name) expect(t.meta.celFile).toBe(t.celFile) expect(t.meta.frames).toBe(t.frames) expect(t.meta.framesPerDirection).toBe(t.frames) expect(t.meta.directions).toBe(1) expect(t.meta.trans).toBe(t.trans) expect(t.meta.preDraw).toBe(t.preDraw) expect(t.meta.groups[0]?.length).toBe(t.frames) expect(t.meta.box.width).toBeGreaterThan(0) expect(t.meta.box.height).toBeGreaterThan(0) } }) it('3. Verifies OVERLAY_METAS maps primary keys, celFiles, and aliases cleanly', () => { expect(OVERLAY_METAS['lightning']).toBe(LIGHTNING_OVERLAY_META) expect(OVERLAY_METAS['ShockHitSm']).toBe(LIGHTNING_OVERLAY_META) expect(OVERLAY_METAS['holyshockhit']).toBe(LIGHTNING_OVERLAY_META) expect(OVERLAY_METAS['frozenarmor']).toBe(FROZEN_ARMOR_OVERLAY_META) expect(OVERLAY_METAS['shiverarmor']).toBe(FROZEN_ARMOR_OVERLAY_META) expect(OVERLAY_METAS['chillarmor']).toBe(FROZEN_ARMOR_OVERLAY_META) expect(OVERLAY_METAS['frozenarmor_hit']).toBe(FROZEN_ARMOR_HIT_OVERLAY_META) expect(OVERLAY_METAS['cast_shivers']).toBe(FROZEN_ARMOR_HIT_OVERLAY_META) expect(OVERLAY_METAS['shiverarmor_hit']).toBe(FROZEN_ARMOR_HIT_OVERLAY_META) expect(OVERLAY_METAS['energyshield']).toBe(ENERGY_SHIELD_OVERLAY_META) expect(OVERLAY_METAS['energyshieldhit0']).toBe(ENERGY_SHIELD_HIT_OVERLAY_META) expect(OVERLAY_METAS['thunderstormcast']).toBe(THUNDERSTORM_CAST_OVERLAY_META) expect(OVERLAY_METAS['thunderstormback']).toBe(THUNDERSTORM_BACK_OVERLAY_META) expect(OVERLAY_METAS['enchant']).toBe(ENCHANT_OVERLAY_META) expect(OVERLAY_METAS['ice_explode']).toBe(ICE_EXPLODE_OVERLAY_META) }) it('4. Verifies package.json contains "pack:overlays" script and execution succeeds', () => { const pkgPath = path.resolve(__dirname, '../package.json') const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8')) expect(pkg.scripts, 'package.json missing scripts section').toBeDefined() expect(pkg.scripts['pack:overlays'], 'package.json missing pack:overlays').toBe('tsx scripts/pack-overlays.ts') expect(pkg.scripts['pack:missiles'], 'package.json missing pack:missiles').toBe('tsx scripts/pack-missiles.ts') // Execute npm run pack:overlays and verify zero exit code & console logging const output = execSync('npm run pack:overlays', { cwd: path.resolve(__dirname, '..'), encoding: 'utf-8', timeout: 30000, }) expect(output).toContain('[Overlay Packer] Baked') expect(output).toContain('overlays-meta.ts') }) it('5. Verifies CelFile casing lookups in OVERLAY_METAS', () => { const CEL_FILE_CASING_TESTS = [ { key: 'ShockHitSm', expectedMeta: LIGHTNING_OVERLAY_META }, { key: 'FrozenArmor', expectedMeta: FROZEN_ARMOR_OVERLAY_META }, { key: 'AuraResistColdCast', expectedMeta: FROZEN_ARMOR_HIT_OVERLAY_META }, { key: 'ManaShieldBall', expectedMeta: ENERGY_SHIELD_OVERLAY_META }, { key: 'ManaShieldDir00', expectedMeta: ENERGY_SHIELD_HIT_OVERLAY_META }, { key: 'ThunderstormCast', expectedMeta: THUNDERSTORM_CAST_OVERLAY_META }, { key: 'ThunderstormCast_operate', expectedMeta: THUNDERSTORM_BACK_OVERLAY_META }, { key: 'FireEnchant', expectedMeta: ENCHANT_OVERLAY_META }, { key: 'IceExplode', expectedMeta: ICE_EXPLODE_OVERLAY_META }, ] for (const { key, expectedMeta } of CEL_FILE_CASING_TESTS) { expect(OVERLAY_METAS[key], `OVERLAY_METAS['${key}'] should resolve directly`).toBe(expectedMeta) expect(OVERLAY_METAS[key.toLowerCase()], `OVERLAY_METAS['${key.toLowerCase()}'] should resolve`).toBe(expectedMeta) } }) it('6. Verifies CelFile casing lookups in MISSILE_METAS', () => { const MISSILE_CASING_KEYS = [ 'ElectricNova', 'LightningStrike', 'LightningboltBig', 'Gleam', 'FreezeExplodeEjecta', 'groundFireBig', 'Flamethrower', 'Flamethrower2', 'FrostNova', 'Firebolt', 'Fireball', 'ChargedBolt', 'Icebolt', 'IceBlast', 'GlacialSpike', 'Blizzard', ] for (const key of MISSILE_CASING_KEYS) { expect(MISSILE_METAS[key], `MISSILE_METAS['${key}'] should resolve directly`).toBeDefined() expect(MISSILE_METAS[key.toLowerCase()], `MISSILE_METAS['${key.toLowerCase()}'] should resolve`).toBeDefined() } }) })