diablo2-web/tests/pack-pipelines-transparency...

146 lines
6.5 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import { existsSync } from 'node:fs'
import { ADDITIVE_OBJECT_TOKENS, loadObjectsTable, resolveDs1Object } from '../src/game/objects.ts'
import { ADDITIVE_MONSTER_TOKENS, compositeMonsterAnimation } from '../src/game/monster-art.ts'
import { packSheetToR8 } from '../scripts/pack-entity-assets.ts'
import type { SpriteSheet, SpriteGroup } from '../src/formats/sprite.ts'
import { decodeCof } from '../src/formats/cof.ts'
describe('Offline Asset Packing Pipelines Transparency (Milestone 3)', () => {
const hasD2 = existsSync('samples/d2/d2data.mpq') && existsSync('samples/d2/Patch_D2.mpq')
describe('Act Asset Packing (pack-act-assets.ts)', () => {
it.skipIf(!hasD2)('resolves objects with Trans=7 or in ADDITIVE_OBJECT_TOKENS with blendMode: additive', async () => {
const { MountedArchives } = await import('../src/mpq/mount.ts')
const { MpqArchive } = await import('../src/mpq/archive.ts')
const { fileSource } = await import('../src/mpq/file-source.ts')
const archives = new MountedArchives()
for (const name of ['d2data.mpq', 'd2char.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
}
const objectsTable = await loadObjectsTable(archives)
// Token 11 & 12 (Chaos Sanctuary cathedral window light beams in Act 4)
for (const token of ['11', '12']) {
expect(ADDITIVE_OBJECT_TOKENS.has(token)).toBe(true)
const row = objectsTable.rows.find(r => r.token.toUpperCase() === token)
const isAdditive = (row !== undefined && (row.trans === 7 || row.trans === 1)) || ADDITIVE_OBJECT_TOKENS.has(token)
expect(isAdditive).toBe(true)
}
// Tokens R1..R5, 1R..4R (Trans=7 directional light beams)
const beamTokens = ['R1', 'R2', 'R3', 'R4', 'R5', '1R', '2R', '3R', '4R']
for (const token of beamTokens) {
expect(ADDITIVE_OBJECT_TOKENS.has(token)).toBe(true)
const row = objectsTable.rows.find(r => r.token.toUpperCase() === token)
expect(row).toBeDefined()
expect(row?.trans).toBe(7)
}
// Token HA (Trans=1 arcane orifice)
const haRow = objectsTable.rows.find(r => r.token.toUpperCase() === 'HA')
expect(haRow).toBeDefined()
expect(haRow?.trans).toBe(1)
expect(ADDITIVE_OBJECT_TOKENS.has('HA')).toBe(true)
})
it.skipIf(!hasD2)('verifies COF layers for token 11 have drawEffect=3 and transparent=true', async () => {
const { MountedArchives } = await import('../src/mpq/mount.ts')
const { MpqArchive } = await import('../src/mpq/archive.ts')
const { fileSource } = await import('../src/mpq/file-source.ts')
const archives = new MountedArchives()
for (const name of ['d2data.mpq', 'd2char.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
}
const cofBytes = await archives.read('data\\global\\objects\\11\\cof\\11nuhth.cof')
const cof = decodeCof(cofBytes)
expect(cof.layers.length).toBeGreaterThan(0)
const layer0 = cof.layers[0]!
expect(layer0.drawEffect).toBe(3)
expect(layer0.transparent).toBe(true)
})
})
describe('Entity Asset Packing (pack-entity-assets.ts)', () => {
it.skipIf(!hasD2)('packs pure additive monster (WR) into R8 with blendMode: additive and no glowSheet', async () => {
const { MountedArchives } = await import('../src/mpq/mount.ts')
const { MpqArchive } = await import('../src/mpq/archive.ts')
const { fileSource } = await import('../src/mpq/file-source.ts')
const archives = new MountedArchives()
for (const name of ['d2data.mpq', 'd2char.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
}
const wr = await compositeMonsterAnimation(archives, 'WR', 'wl', 'hth')
expect(wr.blendMode).toBe('additive')
expect(wr.glowSheet).toBeUndefined()
const packed = packSheetToR8(wr.sheet, 172)
expect(packed.width).toBeGreaterThan(0)
expect(packed.height).toBeGreaterThan(0)
expect(packed.r8.byteLength).toBe(packed.width * packed.height)
expect(packed.groups.length).toBe(wr.sheet.groups.length)
})
it.skipIf(!hasD2)('packs composite monster (FR) combining base and glow groups into single R8 atlas', async () => {
const { MountedArchives } = await import('../src/mpq/mount.ts')
const { MpqArchive } = await import('../src/mpq/archive.ts')
const { fileSource } = await import('../src/mpq/file-source.ts')
const archives = new MountedArchives()
for (const name of ['d2data.mpq', 'd2char.mpq', 'd2exp.mpq', 'Patch_D2.mpq']) {
archives.add(name, await MpqArchive.open(await fileSource(`samples/d2/${name}`)))
}
const fr = await compositeMonsterAnimation(archives, 'FR', 'wl', 'hth')
expect(fr.blendMode).toBe('normal')
expect(fr.glowSheet).toBeDefined()
expect(fr.glowSheet?.groups.length).toBe(fr.sheet.groups.length)
const baseGroups = fr.sheet.groups
const glowGroups = fr.glowSheet!.groups
const baseCount = baseGroups.length
const joinedSheet: SpriteSheet = {
groups: [...baseGroups, ...glowGroups],
width: null,
}
const packed = packSheetToR8(joinedSheet, 172)
expect(packed.groups.length).toBe(baseCount * 2)
const packedBaseGroups = packed.groups.slice(0, baseCount)
const packedGlowGroups = packed.groups.slice(baseCount)
expect(packedBaseGroups.length).toBe(baseCount)
expect(packedGlowGroups.length).toBe(baseCount)
// Ensure every base and glow frame has valid coords in the packed R8 buffer
for (let g = 0; g < baseCount; g += 1) {
const bg = packedBaseGroups[g]!
const gg = packedGlowGroups[g]!
expect(bg.length).toBe(gg.length)
for (let f = 0; f < bg.length; f += 1) {
const [bx, by, bw, bh, bax, bay] = bg[f]!
const [gx, gy, gw, gh, gax, gay] = gg[f]!
// Valid dimensions within atlas
expect(bx + bw).toBeLessThanOrEqual(packed.width)
expect(by + bh).toBeLessThanOrEqual(packed.height)
expect(gx + gw).toBeLessThanOrEqual(packed.width)
expect(gy + gh).toBeLessThanOrEqual(packed.height)
// Anchors must match between base frame and glow frame
expect(bax).toBe(gax)
expect(bay).toBe(gay)
}
}
})
})
})