import { describe, expect, it } from 'vitest' import { ActorAnimator, type AnimClipMeta } from '../src/game/actor-animator.ts' describe('ActorAnimator: 8.8 fixed-point timing & frame events', () => { it('advances exactly 1 frame per tick at speed 256 (1.0x)', () => { const animator = new ActorAnimator() const clip: AnimClipMeta = { group: 0, frames: 8, speed: 256, loop: true, events: [], } animator.play('wl', clip) expect(animator.frameIndex).toBe(0) expect(animator.finished).toBe(false) for (let tick = 1; tick <= 7; tick += 1) { animator.tick() expect(animator.frameIndex).toBe(tick) } // 8th tick wraps back to frame 0 animator.tick() expect(animator.frameIndex).toBe(0) }) it('advances 1 frame every 2 ticks at speed 128 (0.5x, e.g. SONUHTH)', () => { const animator = new ActorAnimator() const clip: AnimClipMeta = { group: 16, frames: 8, speed: 128, loop: true, events: [], } animator.play('nu', clip) expect(animator.frameIndex).toBe(0) // Tick 1: acc = 128 -> frame 0 animator.tick() expect(animator.frameIndex).toBe(0) // Tick 2: acc = 256 -> frame 1 animator.tick() expect(animator.frameIndex).toBe(1) // Run remaining ticks to complete full 8-frame cycle (16 ticks total) for (let t = 3; t <= 16; t += 1) { animator.tick() } expect(animator.frameIndex).toBe(0) }) it('accurately reproduces fractional pacing (BAWL1HS: 8 frames at speed 168 -> 12.19 ticks)', () => { const animator = new ActorAnimator() const clip: AnimClipMeta = { group: 0, frames: 8, speed: 168, loop: true, events: [], } animator.play('wl', clip) let cycleCompletedAtTick = -1 for (let t = 1; t <= 25; t += 1) { const prevFrame = animator.frameIndex animator.tick() if (prevFrame === 7 && animator.frameIndex === 0) { cycleCompletedAtTick = t break } } // 8 * 256 = 2048 accumulator units needed for a cycle. // At speed 168 per tick: // 12 ticks * 168 = 2016 (< 2048, still frame 7) // 13 ticks * 168 = 2184 (>= 2048, wraps to frame 0 on tick 13) expect(cycleCompletedAtTick).toBe(13) }) it('dispatches frame events in chronological order on the tick they are crossed', () => { const animator = new ActorAnimator() const clip: AnimClipMeta = { group: 48, frames: 16, speed: 256, loop: false, events: [{ f: 9, c: 1 }], // SOA1HTH: attack event code 1 at frame 9 } animator.play('a1', clip) const collectedEvents: { tick: number; frame: number; code: number }[] = [] for (let t = 1; t <= 16; t += 1) { const events = animator.tick() for (const e of events) { collectedEvents.push({ tick: t, frame: e.frame, code: e.code }) } } expect(collectedEvents).toEqual([{ tick: 9, frame: 9, code: 1 }]) expect(animator.finished).toBe(true) expect(animator.frameIndex).toBe(15) // clamps to last frame (15) }) it('dispatches multiple events in chronological order on fast ticks (speed > 256)', () => { const animator = new ActorAnimator() const clip: AnimClipMeta = { group: 0, frames: 10, speed: 512, // 2 frames per tick loop: false, events: [ { f: 2, c: 3 }, // sound { f: 3, c: 1 }, // attack ], } animator.play('a1', clip) // Tick 1: advances frames 0 -> 1 -> 2 (triggers event at 2) const events1 = animator.tick() expect(events1).toEqual([{ frame: 2, code: 3 }]) expect(animator.frameIndex).toBe(2) // Tick 2: advances frames 2 -> 3 -> 4 (triggers event at 3) const events2 = animator.tick() expect(events2).toEqual([{ frame: 3, code: 1 }]) expect(animator.frameIndex).toBe(4) }) it('stops at final frame and marks finished = true for one-shot clips', () => { const animator = new ActorAnimator() const clip: AnimClipMeta = { group: 80, frames: 5, speed: 256, loop: false, events: [], } animator.play('dt', clip) for (let t = 1; t <= 4; t += 1) { animator.tick() expect(animator.finished).toBe(false) } // On 5th tick, reaches frame 4 and marks finished animator.tick() expect(animator.frameIndex).toBe(4) expect(animator.finished).toBe(true) // Further ticks remain at frame 4 and finished = true animator.tick() expect(animator.frameIndex).toBe(4) expect(animator.finished).toBe(true) }) it('marks single-frame one-shot clip (e.g. DD corpse) as finished immediately', () => { const animator = new ActorAnimator() const clip: AnimClipMeta = { group: 96, frames: 1, speed: 256, loop: false, events: [], } animator.play('dd', clip) expect(animator.frameIndex).toBe(0) expect(animator.finished).toBe(true) const events = animator.tick() expect(events).toEqual([]) expect(animator.frameIndex).toBe(0) expect(animator.finished).toBe(true) }) it('preserves playback state when re-requesting the same mode without restart flag', () => { const animator = new ActorAnimator() const clip: AnimClipMeta = { group: 0, frames: 8, speed: 256, loop: true, events: [], } animator.play('wl', clip) animator.tick() animator.tick() animator.tick() expect(animator.frameIndex).toBe(3) // Re-play same clip without restart -> no jitter / reset animator.play('wl', clip) expect(animator.frameIndex).toBe(3) // Re-play with restart -> resets to 0 animator.play('wl', clip, { restart: true }) expect(animator.frameIndex).toBe(0) }) })