170 lines
6.4 KiB
TypeScript
170 lines
6.4 KiB
TypeScript
import { describe, it, expect, beforeAll } from 'vitest'
|
|
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import {
|
|
updateAnimatableTiles,
|
|
type AnimatableTile,
|
|
DEFAULT_ANIMATED_TILE_FRAME_DURATION_MS,
|
|
} from '../src/common/world/animated-tiles.ts'
|
|
|
|
describe('Dungeon & Wilderness Illumination & Animated Tile Sequences (Issue #398)', () => {
|
|
|
|
describe('DT1 animated tile serialization and unpacking parity', () => {
|
|
it('correctly unpacks extended animated tile rows [frameIndex, x, y, cx, cy, ...animFrames]', () => {
|
|
// Simulate scene frames
|
|
const dummyFrames = [
|
|
{ x: 0, y: 0, width: 160, height: 80 },
|
|
{ x: 160, y: 0, width: 160, height: 80 },
|
|
{ x: 320, y: 0, width: 160, height: 80 },
|
|
{ x: 480, y: 0, width: 160, height: 80 },
|
|
]
|
|
const framePlacement = [
|
|
[0, 0, 0, 160, 80],
|
|
[0, 160, 0, 160, 80],
|
|
[0, 320, 0, 160, 80],
|
|
[0, 480, 0, 160, 80],
|
|
]
|
|
|
|
// Row with 4 animated frames: indices 0, 1, 2, 3
|
|
const row = [0, 100, 200, 5, 10, 0, 1, 2, 3]
|
|
const frameIndex = row[0] ?? 0
|
|
const animFrames = row.length > 5 ? row.slice(5) : undefined
|
|
const anim = animFrames && animFrames.length > 1
|
|
? animFrames.map(fIdx => ({
|
|
frame: dummyFrames[fIdx]!,
|
|
page: framePlacement[fIdx]?.[0] ?? 0,
|
|
}))
|
|
: undefined
|
|
|
|
const tile: AnimatableTile = {
|
|
frame: dummyFrames[frameIndex]!,
|
|
page: framePlacement[frameIndex]?.[0] ?? 0,
|
|
animatedFrames: anim,
|
|
currentFrameIndex: 0,
|
|
}
|
|
|
|
expect(tile.animatedFrames).toBeDefined()
|
|
expect(tile.animatedFrames!.length).toBe(4)
|
|
|
|
// Verify updateAnimatableTiles advances frames
|
|
const tiles = [tile]
|
|
// At t=0ms
|
|
updateAnimatableTiles(tiles, 0, 'time', DEFAULT_ANIMATED_TILE_FRAME_DURATION_MS)
|
|
expect(tile.currentFrameIndex).toBe(0)
|
|
|
|
// At t=150ms (> 128ms)
|
|
updateAnimatableTiles(tiles, 150, 'time', DEFAULT_ANIMATED_TILE_FRAME_DURATION_MS)
|
|
expect(tile.currentFrameIndex).toBe(1)
|
|
expect(tile.frame).toEqual(dummyFrames[1])
|
|
|
|
// At t=550ms (> 4 * 128 = 512ms, looped to frame 0)
|
|
updateAnimatableTiles(tiles, 550, 'time', DEFAULT_ANIMATED_TILE_FRAME_DURATION_MS)
|
|
expect(tile.currentFrameIndex).toBe(0)
|
|
expect(tile.frame).toEqual(dummyFrames[0])
|
|
})
|
|
})
|
|
|
|
describe('Baked Map Scene Inspection (Wilderness & Dungeons)', () => {
|
|
const packsDir = join(process.cwd(), 'samples/d2-packs')
|
|
const hasPacks = existsSync(join(packsDir, 'index.json')) && existsSync(join(packsDir, 'act1'))
|
|
|
|
beforeAll(() => {
|
|
if (!hasPacks) {
|
|
throw new Error(
|
|
'[Preflight] Missing pre-baked map pack assets at samples/d2-packs/. ' +
|
|
'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\n' +
|
|
'Or generate them with: npm run pack:data'
|
|
)
|
|
}
|
|
})
|
|
|
|
it('verifies town scene (1-act-1-town-var1) preserves all animated objects', () => {
|
|
const townPath = join(packsDir, 'act1/1-act-1-town-var1/scene.json')
|
|
expect(existsSync(townPath)).toBe(true)
|
|
|
|
const scene = JSON.parse(readFileSync(townPath, 'utf8'))
|
|
const objects = scene.objects as any[]
|
|
|
|
// Campfire (RB)
|
|
const rbs = objects.filter(o => o.token === 'RB')
|
|
expect(rbs.length).toBeGreaterThan(0)
|
|
for (const rb of rbs) {
|
|
expect(rb.animatedFrames?.length).toBe(20)
|
|
expect(rb.cycleAnim).toBe(true)
|
|
}
|
|
|
|
// Torches (TO)
|
|
const tos = objects.filter(o => o.token === 'TO')
|
|
expect(tos.length).toBeGreaterThan(0)
|
|
for (const to of tos) {
|
|
expect(to.animatedFrames?.length).toBe(20)
|
|
expect(to.cycleAnim).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('verifies Catacombs (Level 34) packs have animated candles (A1, A2) and fires (BF, BR)', () => {
|
|
const act1Dir = join(packsDir, 'act1')
|
|
expect(existsSync(act1Dir)).toBe(true)
|
|
|
|
const catacombsDirs = readdirSync(act1Dir).filter(name => name.startsWith('34-act-1-catacombs'))
|
|
expect(catacombsDirs.length).toBeGreaterThan(0)
|
|
|
|
let totalCandlesOrFires = 0
|
|
for (const d of catacombsDirs) {
|
|
const scenePath = join(act1Dir, d, 'scene.json')
|
|
expect(existsSync(scenePath)).toBe(true)
|
|
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
|
const items = (scene.objects as any[]).filter(o =>
|
|
o.token && ['A1', 'A2', 'BF', 'BR', 'TO'].includes(o.token.trim().toUpperCase())
|
|
)
|
|
for (const item of items) {
|
|
totalCandlesOrFires += 1
|
|
if (item.animatedFrames) {
|
|
expect(item.animatedFrames.length).toBeGreaterThan(1)
|
|
expect(item.cycleAnim).toBe(true)
|
|
expect(item.frameDurationMs).toBeGreaterThan(0)
|
|
}
|
|
}
|
|
}
|
|
expect(totalCandlesOrFires).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('verifies DT1 animated tile sequences in scenes that contain animated tiles', () => {
|
|
const act4Dir = join(packsDir, 'act4')
|
|
expect(existsSync(act4Dir)).toBe(true)
|
|
|
|
// River of flame (Level 107) and Chaos Sanctuary (Level 108) have animated lava tiles
|
|
const chaosDirs = readdirSync(act4Dir).filter(name => name.startsWith('108-act-4-diablo') || name.startsWith('107-act-4-lava'))
|
|
expect(chaosDirs.length).toBeGreaterThan(0)
|
|
let totalAnimatedTiles = 0
|
|
for (const d of chaosDirs) {
|
|
const scenePath = join(act4Dir, d, 'scene.json')
|
|
expect(existsSync(scenePath)).toBe(true)
|
|
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
|
const draws = [
|
|
...(scene.floors ?? []),
|
|
...(scene.walls ?? []),
|
|
...(scene.shadows ?? []),
|
|
...(scene.roofs ?? []),
|
|
]
|
|
const animatedTileDraws = draws.filter((row: number[]) => row.length > 5)
|
|
totalAnimatedTiles += animatedTileDraws.length
|
|
if (animatedTileDraws.length > 0) {
|
|
for (const row of animatedTileDraws) {
|
|
const animFrames = row.slice(5)
|
|
expect(animFrames.length).toBeGreaterThan(1)
|
|
// Each frame index should be valid in scene.frames
|
|
for (const fIdx of animFrames) {
|
|
expect(fIdx).toBeGreaterThanOrEqual(0)
|
|
expect(fIdx).toBeLessThan(scene.frames.length)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
expect(totalAnimatedTiles).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
})
|
|
|