import { describe, it, expect } from 'vitest' import { AnimationState } from '../src/anim-state.js' import { makeClipLib } from './fixtures.js' describe('AnimationState 多轨道状态机', () => { it('多轨道后写覆盖互不串扰(验收 3)', () => { const s = new AnimationState(makeClipLib()) s.setAnimation('lower_body', 'leg.walk', { mix: 0 }) s.setAnimation('head', 'head.talk_shake', { mix: 0 }) s.setAnimation('arm_r', 'arm_r.point_right', { mix: 0 }) s.update(150) // 推进 const off = s.sampleBones() // 腿骨带到增量 expect(off.thigh_l).toBeDefined() expect(off.thigh_r).toBeDefined() // 头骨带到增量 expect(off.head).toBeDefined() // 右臂骨带到 point_right 增量 expect(off.upper_arm_r.drot).toBeCloseTo(90, 6) // 互不串扰:head track 不写腿骨;lower_body 不写头骨——各自只在自己集合里 // upper_arm_r 的增量来自 arm_r,不被 head/leg 覆盖 expect(off.upper_arm_r).toBeDefined() }) it('交叉淡入中点混合 ≈0.5(验收 4)', () => { const s = new AnimationState(makeClipLib()) // 从 point_right(drot=90) 切到 down(drot=0),mix=160 s.setAnimation('arm_r', 'arm_r.point_right', { mix: 0 }) s.update(10) s.setAnimation('arm_r', 'arm_r.down', { mix: 160 }) s.update(80) // t=80ms,权重≈0.5 const off = s.sampleBones() // 新 clip(down=0) 与旧 clip(point_right=90) 混合,权重 0.5 → ≈45 expect(off.upper_arm_r.drot).toBeGreaterThan(30) expect(off.upper_arm_r.drot).toBeLessThan(60) }) it('mix 走完只剩新 clip(验收 4)', () => { const s = new AnimationState(makeClipLib()) s.setAnimation('arm_r', 'arm_r.point_right', { mix: 0 }) s.update(10) s.setAnimation('arm_r', 'arm_r.down', { mix: 160 }) s.update(200) // 过 mix const off = s.sampleBones() expect(off.upper_arm_r.drot).toBeCloseTo(0, 6) expect(s.tracks.arm_r.prevClipId).toBeNull() }) it('oneshot 超时回落 fallback(验收 5)', () => { const s = new AnimationState(makeClipLib()) s.setAnimation('head', 'head.nod', { mix: 0, fallback: 'head.idle' }) expect(s.tracks.head.clipId).toBe('head.nod') s.update(500) // head.nod duration=400,过时 expect(s.tracks.head.clipId).toBe('head.idle') }) it('reset 回各轨道默认 clip', () => { const s = new AnimationState(makeClipLib()) s.setAnimation('lower_body', 'leg.walk', { mix: 0 }) s.reset() expect(s.tracks.lower_body.clipId).toBe('leg.stand_together') expect(s.tracks.head.clipId).toBe('head.idle') }) it('未知 clip / 已在播不打断', () => { const s = new AnimationState(makeClipLib()) s.setAnimation('head', 'no.such.clip') expect(s.tracks.head.clipId).toBe('head.idle') // 未变 const before = s.tracks.head.elapsed s.update(100) s.setAnimation('head', 'head.idle') // 已在播且无过渡 → 不打断(不归零 elapsed) expect(s.tracks.head.elapsed).toBeGreaterThanOrEqual(before) }) })