diablo2-web/tests/animated-tiles-speed.test.ts

156 lines
5.9 KiB
TypeScript

import { describe, expect, test } from 'vitest'
import {
DEFAULT_ANIMATED_TILE_FRAME_DURATION_MS,
frameDurationMsFromAnimSpeed,
updateAnimatableTile,
updateAnimatableTiles,
} from '../src/game/animated-tiles.ts'
import type { AnimatableTile } from '../src/game/animated-tiles.ts'
import { MAZE_LEVEL_TYPE_PROFILES } from '../src/game/maze.ts'
describe('Animated Tile Speed from LvlPrest.txt Animate Column (Issue #85)', () => {
describe('frameDurationMsFromAnimSpeed formula', () => {
test('defaults to 128ms for 0, negative, undefined, or missing speed', () => {
// In 1.13c: if (nAnimationSpeed == 0) nAnimationSpeed = 80;
// At 25Hz tick rate and 80/256 frames per tick:
// (256 / 80) * (1000 / 25) = 3.2 * 40 = 128 ms
expect(frameDurationMsFromAnimSpeed(undefined)).toBe(128)
expect(frameDurationMsFromAnimSpeed(0)).toBe(128)
expect(frameDurationMsFromAnimSpeed(-1)).toBe(128)
expect(frameDurationMsFromAnimSpeed(-80)).toBe(128)
expect(frameDurationMsFromAnimSpeed(Number.NaN)).toBe(128)
})
test('calculates 128ms for standard speed 80', () => {
expect(frameDurationMsFromAnimSpeed(80)).toBe(128)
expect(frameDurationMsFromAnimSpeed(80)).toBe(DEFAULT_ANIMATED_TILE_FRAME_DURATION_MS)
})
test('calculates 10240ms for Act 4 lava speed 1', () => {
// Speed 1: (256 / 1) * (1000 / 25) = 256 * 40 = 10240 ms per frame (~0.09765 FPS)
expect(frameDurationMsFromAnimSpeed(1)).toBe(10240)
})
test('calculates correct durations for various speeds', () => {
// Speed 160: twice as fast as default -> 64ms per frame
expect(frameDurationMsFromAnimSpeed(160)).toBe(64)
// Speed 256: 1 frame per tick (25 FPS) -> 40ms per frame
expect(frameDurationMsFromAnimSpeed(256)).toBe(40)
// Speed 40: half speed of default -> 256ms per frame
expect(frameDurationMsFromAnimSpeed(40)).toBe(256)
// Speed 20: quarter speed -> 512ms per frame
expect(frameDurationMsFromAnimSpeed(20)).toBe(512)
})
})
describe('updateAnimatableTiles with per-level frame duration', () => {
const createTile = (frameCount: number): AnimatableTile => {
const frames = Array.from({ length: frameCount }, (_, i) => ({
frame: { x: i * 160, y: 0, width: 160, height: 80 },
page: 0,
}))
return {
frame: frames[0]!.frame,
page: frames[0]!.page,
animatedFrames: frames,
currentFrameIndex: 0,
}
}
test('advances at 10240ms per frame when frameDurationMs is 10240 (Act 4 Lava)', () => {
const tile = createTile(4)
const durationMs = frameDurationMsFromAnimSpeed(1) // 10240ms
expect(durationMs).toBe(10240)
// At 0ms -> frame 0
updateAnimatableTile(tile, 0, 'time', durationMs)
expect(tile.currentFrameIndex).toBe(0)
// At 5000ms -> still frame 0 (not reached 10240ms yet)
updateAnimatableTile(tile, 5000, 'time', durationMs)
expect(tile.currentFrameIndex).toBe(0)
// At 10239ms -> still frame 0
updateAnimatableTile(tile, 10239, 'time', durationMs)
expect(tile.currentFrameIndex).toBe(0)
// At 10240ms -> advances to frame 1
updateAnimatableTile(tile, 10240, 'time', durationMs)
expect(tile.currentFrameIndex).toBe(1)
// At 20480ms -> advances to frame 2
updateAnimatableTile(tile, 20480, 'time', durationMs)
expect(tile.currentFrameIndex).toBe(2)
// At 40960ms -> wraps back to frame 0
updateAnimatableTile(tile, 40960, 'time', durationMs)
expect(tile.currentFrameIndex).toBe(0)
})
test('advances at 64ms per frame when frameDurationMs is 64 (speed 160)', () => {
const tile = createTile(4)
const durationMs = frameDurationMsFromAnimSpeed(160) // 64ms
expect(durationMs).toBe(64)
updateAnimatableTile(tile, 0, 'time', durationMs)
expect(tile.currentFrameIndex).toBe(0)
updateAnimatableTile(tile, 63, 'time', durationMs)
expect(tile.currentFrameIndex).toBe(0)
updateAnimatableTile(tile, 64, 'time', durationMs)
expect(tile.currentFrameIndex).toBe(1)
updateAnimatableTile(tile, 128, 'time', durationMs)
expect(tile.currentFrameIndex).toBe(2)
})
test('updateAnimatableTiles propagates custom frameDurationMs to all tiles', () => {
const tiles = [createTile(3), createTile(4), createTile(5)]
const lavaDuration = frameDurationMsFromAnimSpeed(1) // 10240ms
// At 10240ms, all should be on frame 1
updateAnimatableTiles(tiles, 10240, 'time', lavaDuration)
expect(tiles[0]!.currentFrameIndex).toBe(1)
expect(tiles[1]!.currentFrameIndex).toBe(1)
expect(tiles[2]!.currentFrameIndex).toBe(1)
// At 30720ms:
// tile 0 (3 frames): 30720 / 10240 = 3 -> 3 % 3 = frame 0
// tile 1 (4 frames): 3 % 4 = frame 3
// tile 2 (5 frames): 3 % 5 = frame 3
updateAnimatableTiles(tiles, 30720, 'time', lavaDuration)
expect(tiles[0]!.currentFrameIndex).toBe(0)
expect(tiles[1]!.currentFrameIndex).toBe(3)
expect(tiles[2]!.currentFrameIndex).toBe(3)
})
test('updateAnimatableTiles falls back to DEFAULT_ANIMATED_TILE_FRAME_DURATION_MS (128ms) when omitted', () => {
const tile = createTile(4)
updateAnimatableTiles([tile], 0, 'time')
expect(tile.currentFrameIndex).toBe(0)
updateAnimatableTiles([tile], 127, 'time')
expect(tile.currentFrameIndex).toBe(0)
updateAnimatableTiles([tile], 128, 'time')
expect(tile.currentFrameIndex).toBe(1)
updateAnimatableTiles([tile], 256, 'time')
expect(tile.currentFrameIndex).toBe(2)
})
})
describe('Act 4 Lava Profile configuration', () => {
test('Act 4 - Lava profile specifies animSpeed: 1', () => {
const lavaProfile = MAZE_LEVEL_TYPE_PROFILES.find(p => p.levelTypeName === 'Act 4 - Lava')
expect(lavaProfile).toBeDefined()
expect(lavaProfile?.animSpeed).toBe(1)
})
})
})