diablo2-web/tests/overlay-assets-m0.test.ts

184 lines
8.4 KiB
TypeScript

import { beforeAll, describe, expect, it } from 'vitest'
import fs from 'node:fs'
import os from 'node:os'
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')
beforeAll(() => {
const hasPacks = fs.existsSync(packDir)
if (!hasPacks) {
throw new Error(
'[Preflight] Required asset directory missing in samples/d2-packs/overlays. ' +
'When running in a git worktree, symlink samples/d2-packs from the main repository:\n' +
' ln -s <main-repo>/samples/d2-packs samples/d2-packs'
)
}
})
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')
const hasD2 = fs.existsSync(path.resolve(__dirname, '../samples/d2/d2data.mpq')) ||
fs.existsSync('/usr/local/google/home/taodao/d2-data/d2data.mpq')
if (!hasD2) {
throw new Error(
'[Preflight] Cannot run pack:overlays: samples/d2/d2data.mpq is missing. ' +
'Please symlink samples/d2 to your Diablo II 1.13c installation directory.'
)
}
const tmpOut = fs.mkdtempSync(path.join(os.tmpdir(), 'd2-overlay-pack-test-'))
try {
// 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,
env: { ...process.env, D2_PACK_OUT_DIR: tmpOut },
})
expect(output).toContain('[Overlay Packer]')
expect(output).toContain('overlays-meta.ts')
expect(fs.existsSync(path.join(tmpOut, 'public/overlays/teleport.png'))).toBe(true)
expect(fs.existsSync(path.join(tmpOut, 'samples/d2-packs/overlays/teleport.png'))).toBe(true)
expect(fs.existsSync(path.join(tmpOut, 'src/render/overlays-meta.ts'))).toBe(true)
} finally {
fs.rmSync(tmpOut, { recursive: true, force: true })
}
})
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()
}
})
})