test(render): tighten assertions and add anti-drift/edge-case tests for Issue #398
This commit is contained in:
parent
e5a34af708
commit
806bc9c8bc
|
|
@ -104,6 +104,63 @@ describe('Dungeon & Wilderness Illumination & Animated Tile Sequences (Issue #39
|
||||||
expect(p1).toBeGreaterThanOrEqual(0)
|
expect(p1).toBeGreaterThanOrEqual(0)
|
||||||
expect(p1).toBeLessThan(duration)
|
expect(p1).toBeLessThan(duration)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('prevents sprite coordinate drift across frame transitions and loops', () => {
|
||||||
|
const frames: AnimatedObjectFrame[] = [
|
||||||
|
{ frame: dummyAtlasFrame(0), page: 0, offsetX: -10, offsetY: -20 },
|
||||||
|
{ frame: dummyAtlasFrame(1), page: 0, offsetX: -15, offsetY: -25 },
|
||||||
|
{ frame: dummyAtlasFrame(2), page: 0, offsetX: -8, offsetY: -18 },
|
||||||
|
]
|
||||||
|
const obj: ObjectDrawable = {
|
||||||
|
token: 'TO',
|
||||||
|
frame: frames[0]!.frame,
|
||||||
|
x: 100 + frames[0]!.offsetX,
|
||||||
|
y: 200 + frames[0]!.offsetY,
|
||||||
|
depth: 60,
|
||||||
|
page: 0,
|
||||||
|
baseX: 100,
|
||||||
|
baseY: 200,
|
||||||
|
animatedFrames: frames,
|
||||||
|
frameDurationMs: 100,
|
||||||
|
cycleAnim: true,
|
||||||
|
currentFrameIndex: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let clock = 0; clock <= 3000; clock += 50) {
|
||||||
|
updateAnimatedObject(obj, clock)
|
||||||
|
const expectedFrameIdx = Math.floor(clock / 100) % 3
|
||||||
|
expect(obj.currentFrameIndex).toBe(expectedFrameIdx)
|
||||||
|
const expectedFrame = frames[expectedFrameIdx]!
|
||||||
|
expect(obj.x).toBe(100 + expectedFrame.offsetX)
|
||||||
|
expect(obj.y).toBe(200 + expectedFrame.offsetY)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('safely handles degenerate frame inputs (empty, single frame, zero/negative duration)', () => {
|
||||||
|
const singleFrameObj: ObjectDrawable = {
|
||||||
|
token: 'TO',
|
||||||
|
frame: dummyAtlasFrame(0),
|
||||||
|
x: 100,
|
||||||
|
y: 200,
|
||||||
|
depth: 60,
|
||||||
|
page: 0,
|
||||||
|
baseX: 100,
|
||||||
|
baseY: 200,
|
||||||
|
animatedFrames: [{ frame: dummyAtlasFrame(0), page: 0, offsetX: 0, offsetY: 0 }],
|
||||||
|
frameDurationMs: 80,
|
||||||
|
currentFrameIndex: 0,
|
||||||
|
}
|
||||||
|
expect(updateAnimatedObject(singleFrameObj, 1000)).toBe(false)
|
||||||
|
expect(singleFrameObj.currentFrameIndex).toBe(0)
|
||||||
|
|
||||||
|
const zeroDurationObj = createTestIlluminationDrawable('TO', 5, 0, true)
|
||||||
|
expect(updateAnimatedObject(zeroDurationObj, 120)).toBe(true)
|
||||||
|
expect(zeroDurationObj.currentFrameIndex).toBe(3)
|
||||||
|
|
||||||
|
const negClockObj = createTestIlluminationDrawable('TO', 5, 40, true)
|
||||||
|
expect(updateAnimatedObject(negClockObj, -100)).toBe(false)
|
||||||
|
expect(negClockObj.currentFrameIndex).toBe(0)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('DT1 animated tile serialization and unpacking parity', () => {
|
describe('DT1 animated tile serialization and unpacking parity', () => {
|
||||||
|
|
@ -166,7 +223,7 @@ describe('Dungeon & Wilderness Illumination & Animated Tile Sequences (Issue #39
|
||||||
|
|
||||||
it('verifies town scene (towne1) preserves all animated objects', () => {
|
it('verifies town scene (towne1) preserves all animated objects', () => {
|
||||||
const townPath = join(packsDir, 'act1/1-act-1-town-towne1/scene.json')
|
const townPath = join(packsDir, 'act1/1-act-1-town-towne1/scene.json')
|
||||||
if (!existsSync(townPath)) return
|
expect(existsSync(townPath)).toBe(true)
|
||||||
|
|
||||||
const scene = JSON.parse(readFileSync(townPath, 'utf8'))
|
const scene = JSON.parse(readFileSync(townPath, 'utf8'))
|
||||||
const objects = scene.objects as any[]
|
const objects = scene.objects as any[]
|
||||||
|
|
@ -190,13 +247,13 @@ describe('Dungeon & Wilderness Illumination & Animated Tile Sequences (Issue #39
|
||||||
|
|
||||||
it('verifies Cave (Level 10) packs have animated illumination objects', () => {
|
it('verifies Cave (Level 10) packs have animated illumination objects', () => {
|
||||||
const act1Dir = join(packsDir, 'act1')
|
const act1Dir = join(packsDir, 'act1')
|
||||||
if (!existsSync(act1Dir)) return
|
expect(existsSync(act1Dir)).toBe(true)
|
||||||
|
|
||||||
const caveDirs = readdirSync(act1Dir).filter(name => name.startsWith('10-act-1-cave'))
|
const caveDirs = readdirSync(act1Dir).filter(name => name.startsWith('10-act-1-cave'))
|
||||||
expect(caveDirs.length).toBeGreaterThan(0)
|
expect(caveDirs.length).toBeGreaterThan(0)
|
||||||
for (const d of caveDirs) {
|
for (const d of caveDirs) {
|
||||||
const scenePath = join(act1Dir, d, 'scene.json')
|
const scenePath = join(act1Dir, d, 'scene.json')
|
||||||
if (!existsSync(scenePath)) continue
|
expect(existsSync(scenePath)).toBe(true)
|
||||||
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
||||||
const illumination = (scene.objects as any[]).filter(o =>
|
const illumination = (scene.objects as any[]).filter(o =>
|
||||||
o.token && PERSISTENT_ILLUMINATION_TOKENS.has(o.token.trim().toUpperCase())
|
o.token && PERSISTENT_ILLUMINATION_TOKENS.has(o.token.trim().toUpperCase())
|
||||||
|
|
@ -214,7 +271,7 @@ describe('Dungeon & Wilderness Illumination & Animated Tile Sequences (Issue #39
|
||||||
|
|
||||||
it('verifies Catacombs (Level 34) packs have animated candles (A1, A2) and fires (BF, BR)', () => {
|
it('verifies Catacombs (Level 34) packs have animated candles (A1, A2) and fires (BF, BR)', () => {
|
||||||
const act1Dir = join(packsDir, 'act1')
|
const act1Dir = join(packsDir, 'act1')
|
||||||
if (!existsSync(act1Dir)) return
|
expect(existsSync(act1Dir)).toBe(true)
|
||||||
|
|
||||||
const catacombsDirs = readdirSync(act1Dir).filter(name => name.startsWith('34-act-1-catacombs'))
|
const catacombsDirs = readdirSync(act1Dir).filter(name => name.startsWith('34-act-1-catacombs'))
|
||||||
expect(catacombsDirs.length).toBeGreaterThan(0)
|
expect(catacombsDirs.length).toBeGreaterThan(0)
|
||||||
|
|
@ -222,7 +279,7 @@ describe('Dungeon & Wilderness Illumination & Animated Tile Sequences (Issue #39
|
||||||
let totalCandlesOrFires = 0
|
let totalCandlesOrFires = 0
|
||||||
for (const d of catacombsDirs) {
|
for (const d of catacombsDirs) {
|
||||||
const scenePath = join(act1Dir, d, 'scene.json')
|
const scenePath = join(act1Dir, d, 'scene.json')
|
||||||
if (!existsSync(scenePath)) continue
|
expect(existsSync(scenePath)).toBe(true)
|
||||||
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
||||||
const items = (scene.objects as any[]).filter(o =>
|
const items = (scene.objects as any[]).filter(o =>
|
||||||
o.token && ['A1', 'A2', 'BF', 'BR', 'TO'].includes(o.token.trim().toUpperCase())
|
o.token && ['A1', 'A2', 'BF', 'BR', 'TO'].includes(o.token.trim().toUpperCase())
|
||||||
|
|
@ -241,13 +298,13 @@ describe('Dungeon & Wilderness Illumination & Animated Tile Sequences (Issue #39
|
||||||
|
|
||||||
it('verifies Wilderness and Crypt maps contain cycleAnim=true on illumination objects', () => {
|
it('verifies Wilderness and Crypt maps contain cycleAnim=true on illumination objects', () => {
|
||||||
const act1Dir = join(packsDir, 'act1')
|
const act1Dir = join(packsDir, 'act1')
|
||||||
if (!existsSync(act1Dir)) return
|
expect(existsSync(act1Dir)).toBe(true)
|
||||||
|
|
||||||
const cryptDirs = readdirSync(act1Dir).filter(name => name.startsWith('18-act-1-crypt') || name.startsWith('19-act-1-crypt'))
|
const cryptDirs = readdirSync(act1Dir).filter(name => name.startsWith('18-act-1-crypt') || name.startsWith('19-act-1-crypt'))
|
||||||
expect(cryptDirs.length).toBeGreaterThan(0)
|
expect(cryptDirs.length).toBeGreaterThan(0)
|
||||||
for (const d of cryptDirs) {
|
for (const d of cryptDirs) {
|
||||||
const scenePath = join(act1Dir, d, 'scene.json')
|
const scenePath = join(act1Dir, d, 'scene.json')
|
||||||
if (!existsSync(scenePath)) continue
|
expect(existsSync(scenePath)).toBe(true)
|
||||||
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
||||||
const items = (scene.objects as any[]).filter(o =>
|
const items = (scene.objects as any[]).filter(o =>
|
||||||
o.token && PERSISTENT_ILLUMINATION_TOKENS.has(o.token.trim().toUpperCase())
|
o.token && PERSISTENT_ILLUMINATION_TOKENS.has(o.token.trim().toUpperCase())
|
||||||
|
|
@ -262,7 +319,7 @@ describe('Dungeon & Wilderness Illumination & Animated Tile Sequences (Issue #39
|
||||||
|
|
||||||
it('verifies DT1 animated tile sequences in scenes that contain animated tiles', () => {
|
it('verifies DT1 animated tile sequences in scenes that contain animated tiles', () => {
|
||||||
const act4Dir = join(packsDir, 'act4')
|
const act4Dir = join(packsDir, 'act4')
|
||||||
if (!existsSync(act4Dir)) return
|
expect(existsSync(act4Dir)).toBe(true)
|
||||||
|
|
||||||
// River of flame (Level 107) and Chaos Sanctuary (Level 108) have animated lava tiles
|
// 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'))
|
const chaosDirs = readdirSync(act4Dir).filter(name => name.startsWith('108-act-4-diablo') || name.startsWith('107-act-4-lava'))
|
||||||
|
|
@ -270,7 +327,7 @@ describe('Dungeon & Wilderness Illumination & Animated Tile Sequences (Issue #39
|
||||||
let totalAnimatedTiles = 0
|
let totalAnimatedTiles = 0
|
||||||
for (const d of chaosDirs) {
|
for (const d of chaosDirs) {
|
||||||
const scenePath = join(act4Dir, d, 'scene.json')
|
const scenePath = join(act4Dir, d, 'scene.json')
|
||||||
if (!existsSync(scenePath)) continue
|
expect(existsSync(scenePath)).toBe(true)
|
||||||
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
const scene = JSON.parse(readFileSync(scenePath, 'utf8'))
|
||||||
const draws = [
|
const draws = [
|
||||||
...(scene.floors ?? []),
|
...(scene.floors ?? []),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue