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) }) }) })