wow_m2/animation/
manager.rs

1//! Animation state machine for M2 models
2//!
3//! The AnimationManager tracks animation playback state and provides
4//! interpolated values for bones, textures, colors, and other animated properties.
5
6use super::interpolation::interpolate_with_blend;
7use super::state::{AnimationState, LcgRng};
8use super::types::{Lerp, Quat, ResolvedTrack, Vec3};
9
10/// Animation sequence data (resolved from M2Sequence)
11#[derive(Debug, Clone)]
12pub struct AnimSequence {
13    /// Animation ID (e.g., 0=Stand, 4=Walk, 5=Run)
14    pub id: u16,
15    /// Sub-animation ID for variations
16    pub sub_id: u16,
17    /// Duration in milliseconds
18    pub duration: u32,
19    /// Movement speed
20    pub movement_speed: f32,
21    /// Flags
22    pub flags: u32,
23    /// Probability weight for variation selection
24    pub frequency: u16,
25    /// Minimum repeat count
26    pub replay_min: u32,
27    /// Maximum repeat count
28    pub replay_max: u32,
29    /// Blend time for transitions (milliseconds)
30    pub blend_time: u32,
31    /// Index of next variation (-1 if none)
32    pub variation_next: i16,
33    /// Index of aliased animation
34    pub alias_next: u16,
35}
36
37impl AnimSequence {
38    /// Calculate number of repeats for this animation
39    pub fn calculate_repeats(&self, rng: &mut LcgRng) -> i32 {
40        if self.replay_max <= self.replay_min {
41            return self.replay_min as i32;
42        }
43        let range = (self.replay_max - self.replay_min) as f32;
44        self.replay_min as i32 + (range * rng.next_f32()) as i32
45    }
46
47    /// Check if this is an alias (references another animation)
48    pub fn is_alias(&self) -> bool {
49        (self.flags & 0x40) != 0 && (self.flags & 0x20) == 0
50    }
51}
52
53/// Resolved bone animation data
54#[derive(Debug, Clone)]
55pub struct ResolvedBone {
56    /// Bone ID
57    pub bone_id: i32,
58    /// Bone flags
59    pub flags: u32,
60    /// Parent bone index (-1 if root)
61    pub parent_bone: i16,
62    /// Translation animation track
63    pub translation: ResolvedTrack<Vec3>,
64    /// Rotation animation track (quaternion)
65    pub rotation: ResolvedTrack<Quat>,
66    /// Scale animation track
67    pub scale: ResolvedTrack<Vec3>,
68    /// Pivot point
69    pub pivot: Vec3,
70}
71
72/// M2 Animation Manager
73///
74/// Manages animation state and provides interpolated values for animated properties.
75/// Modeled after noclip.website's WowM2AnimationManager.
76#[derive(Debug, Clone)]
77pub struct AnimationManager {
78    /// Global sequence durations (milliseconds)
79    global_sequence_durations: Vec<u32>,
80    /// Current time for each global sequence
81    global_sequence_times: Vec<f64>,
82    /// Animation sequences
83    sequences: Vec<AnimSequence>,
84    /// Resolved bone data with animation tracks
85    bones: Vec<ResolvedBone>,
86    /// Current animation state
87    current_animation: AnimationState,
88    /// Next animation state (for blending)
89    next_animation: AnimationState,
90    /// Blend factor between current and next (1.0 = current only)
91    blend_factor: f32,
92    /// Random number generator for variation selection
93    rng: LcgRng,
94}
95
96impl AnimationManager {
97    /// Create a new AnimationManager
98    ///
99    /// # Arguments
100    /// * `global_sequence_durations` - Durations of global sequences
101    /// * `sequences` - Animation sequence definitions
102    /// * `bones` - Resolved bone animation data
103    pub fn new(
104        global_sequence_durations: Vec<u32>,
105        sequences: Vec<AnimSequence>,
106        bones: Vec<ResolvedBone>,
107    ) -> Self {
108        let global_sequence_times = vec![0.0; global_sequence_durations.len()];
109
110        // Find "Stand" animation (ID 0) as default
111        let stand_index = sequences.iter().position(|s| s.id == 0);
112
113        let mut rng = LcgRng::default();
114        let mut current_animation = AnimationState::new(stand_index);
115
116        // Set initial repeat count
117        if let Some(idx) = stand_index {
118            current_animation.repeat_times = sequences[idx].calculate_repeats(&mut rng);
119        }
120
121        Self {
122            global_sequence_durations,
123            global_sequence_times,
124            sequences,
125            bones,
126            current_animation,
127            next_animation: AnimationState::none(),
128            blend_factor: 1.0,
129            rng,
130        }
131    }
132
133    /// Create an empty AnimationManager (no animations)
134    pub fn empty() -> Self {
135        Self {
136            global_sequence_durations: Vec::new(),
137            global_sequence_times: Vec::new(),
138            sequences: Vec::new(),
139            bones: Vec::new(),
140            current_animation: AnimationState::none(),
141            next_animation: AnimationState::none(),
142            blend_factor: 1.0,
143            rng: LcgRng::default(),
144        }
145    }
146
147    /// Update animation state by the given delta time (milliseconds)
148    pub fn update(&mut self, delta_time_ms: f64) {
149        // Update current animation time
150        self.current_animation.animation_time += delta_time_ms;
151
152        // Update global sequence times
153        for (i, time) in self.global_sequence_times.iter_mut().enumerate() {
154            *time += delta_time_ms;
155            if self.global_sequence_durations[i] > 0 {
156                *time %= self.global_sequence_durations[i] as f64;
157            }
158        }
159
160        // Handle animation transitions
161        self.update_animation_transitions();
162    }
163
164    /// Handle animation looping and transitions
165    fn update_animation_transitions(&mut self) {
166        let Some(current_idx) = self.current_animation.animation_index else {
167            return;
168        };
169
170        if current_idx >= self.sequences.len() {
171            return;
172        }
173
174        let main_variation = &self.sequences[self.current_animation.main_variation_index];
175
176        // Select next animation if needed
177        if self.next_animation.animation_index.is_none()
178            && main_variation.variation_next > -1
179            && self.current_animation.repeat_times <= 0
180        {
181            self.select_next_variation();
182        } else if self.current_animation.repeat_times > 0 {
183            // Setup repeat of current animation
184            self.next_animation = self.current_animation.clone();
185            self.next_animation.repeat_times -= 1;
186        }
187
188        // Calculate blend factor
189        let current_seq = &self.sequences[current_idx];
190        let time_left = current_seq.duration as f64 - self.current_animation.animation_time;
191
192        if let Some(next_idx) = self.next_animation.animation_index {
193            let next_seq = &self.sequences[next_idx];
194            let blend_time = next_seq.blend_time as f64;
195
196            if blend_time > 0.0 && time_left < blend_time {
197                self.next_animation.animation_time =
198                    (blend_time - time_left) % next_seq.duration as f64;
199                self.blend_factor = (time_left / blend_time) as f32;
200            } else {
201                self.blend_factor = 1.0;
202            }
203        }
204
205        // Handle animation completion
206        if self.current_animation.animation_time >= current_seq.duration as f64 {
207            self.current_animation.repeat_times -= 1;
208
209            if let Some(next_idx) = self.next_animation.animation_index {
210                // Resolve aliases
211                let resolved_idx = self.resolve_alias(next_idx);
212                self.next_animation.animation_index = Some(resolved_idx);
213
214                // Swap to next animation
215                self.current_animation = self.next_animation.clone();
216                self.next_animation = AnimationState::none();
217                self.blend_factor = 1.0;
218            } else if current_seq.duration > 0 {
219                // Loop current animation
220                self.current_animation.animation_time %= current_seq.duration as f64;
221            }
222        }
223    }
224
225    /// Select next variation based on frequency weights
226    fn select_next_variation(&mut self) {
227        let main_idx = self.current_animation.main_variation_index;
228        let probability = (self.rng.next_f32() * 0x7fff as f32) as u16;
229
230        let mut calc_prob = 0u16;
231        let mut next_index = main_idx;
232
233        loop {
234            let seq = &self.sequences[next_index];
235            calc_prob = calc_prob.saturating_add(seq.frequency);
236
237            if calc_prob >= probability || seq.variation_next < 0 {
238                break;
239            }
240
241            let potential_next = seq.variation_next as usize;
242            if potential_next >= self.sequences.len() {
243                break;
244            }
245
246            // Skip current animation in probability calculation
247            if Some(potential_next) != self.current_animation.animation_index {
248                next_index = potential_next;
249            } else if seq.variation_next >= 0 {
250                next_index = seq.variation_next as usize;
251            }
252        }
253
254        self.next_animation.animation_index = Some(next_index);
255        self.next_animation.animation_time = 0.0;
256        self.next_animation.main_variation_index = main_idx;
257        self.next_animation.repeat_times =
258            self.sequences[next_index].calculate_repeats(&mut self.rng);
259    }
260
261    /// Resolve animation alias chain
262    fn resolve_alias(&self, index: usize) -> usize {
263        let mut current = index;
264        let mut iterations = 0;
265
266        while iterations < 100 {
267            // Safety limit
268            if current >= self.sequences.len() {
269                return index;
270            }
271
272            let seq = &self.sequences[current];
273            if !seq.is_alias() {
274                return current;
275            }
276
277            current = seq.alias_next as usize;
278            iterations += 1;
279        }
280
281        index
282    }
283
284    /// Set the current animation by ID
285    pub fn set_animation_id(&mut self, id: u16) {
286        let index = self.sequences.iter().position(|s| s.id == id);
287
288        if let Some(idx) = index {
289            self.current_animation = AnimationState::new(Some(idx));
290            self.current_animation.repeat_times =
291                self.sequences[idx].calculate_repeats(&mut self.rng);
292            self.next_animation = AnimationState::none();
293            self.blend_factor = 1.0;
294        }
295    }
296
297    /// Set the current animation by index
298    pub fn set_animation_index(&mut self, index: usize) {
299        if index < self.sequences.len() {
300            self.current_animation = AnimationState::new(Some(index));
301            self.current_animation.repeat_times =
302                self.sequences[index].calculate_repeats(&mut self.rng);
303            self.next_animation = AnimationState::none();
304            self.blend_factor = 1.0;
305        }
306    }
307
308    /// Get available animation IDs
309    pub fn get_animation_ids(&self) -> Vec<u16> {
310        self.sequences.iter().map(|s| s.id).collect()
311    }
312
313    /// Get current animation time (milliseconds)
314    pub fn current_time(&self) -> f64 {
315        self.current_animation.animation_time
316    }
317
318    /// Get current animation index
319    pub fn current_animation_index(&self) -> Option<usize> {
320        self.current_animation.animation_index
321    }
322
323    /// Get number of bones
324    pub fn bone_count(&self) -> usize {
325        self.bones.len()
326    }
327
328    /// Get number of sequences
329    pub fn sequence_count(&self) -> usize {
330        self.sequences.len()
331    }
332
333    /// Get interpolated value from a track with current animation blending
334    pub fn get_current_value<T: Lerp + Clone + Default>(&self, track: &ResolvedTrack<T>) -> T {
335        self.get_current_value_with_default(track, T::default())
336    }
337
338    /// Get interpolated value with a custom default
339    pub fn get_current_value_with_default<T: Lerp + Clone>(
340        &self,
341        track: &ResolvedTrack<T>,
342        default: T,
343    ) -> T {
344        let Some(current_idx) = self.current_animation.animation_index else {
345            return default;
346        };
347
348        interpolate_with_blend(
349            track,
350            current_idx,
351            self.current_animation.animation_time,
352            self.next_animation.animation_index,
353            self.next_animation.animation_time,
354            self.blend_factor,
355            &self.global_sequence_times,
356            default,
357        )
358    }
359
360    /// Get bone translation for the given bone index
361    pub fn get_bone_translation(&self, bone_index: usize) -> Vec3 {
362        if bone_index >= self.bones.len() {
363            return Vec3::ZERO;
364        }
365        self.get_current_value_with_default(&self.bones[bone_index].translation, Vec3::ZERO)
366    }
367
368    /// Get bone rotation for the given bone index
369    pub fn get_bone_rotation(&self, bone_index: usize) -> Quat {
370        if bone_index >= self.bones.len() {
371            return Quat::IDENTITY;
372        }
373        self.get_current_value_with_default(&self.bones[bone_index].rotation, Quat::IDENTITY)
374    }
375
376    /// Get bone scale for the given bone index
377    pub fn get_bone_scale(&self, bone_index: usize) -> Vec3 {
378        if bone_index >= self.bones.len() {
379            return Vec3::ONE;
380        }
381        self.get_current_value_with_default(&self.bones[bone_index].scale, Vec3::ONE)
382    }
383
384    /// Get bone data (for computing transforms)
385    pub fn bones(&self) -> &[ResolvedBone] {
386        &self.bones
387    }
388
389    /// Get sequences
390    pub fn sequences(&self) -> &[AnimSequence] {
391        &self.sequences
392    }
393
394    /// Get global sequence times
395    pub fn global_times(&self) -> &[f64] {
396        &self.global_sequence_times
397    }
398
399    /// Get blend factor
400    pub fn blend_factor(&self) -> f32 {
401        self.blend_factor
402    }
403}
404
405/// Builder for creating AnimationManager from M2Model data
406pub struct AnimationManagerBuilder;
407
408impl AnimationManagerBuilder {
409    /// Create an AnimationManager from M2Model data
410    ///
411    /// This resolves all bone animation tracks from the raw M2 data and creates
412    /// a fully functional animation manager ready for playback.
413    ///
414    /// # Arguments
415    /// * `model` - The parsed M2 model
416    /// * `data` - The raw M2 file bytes (needed to resolve animation data offsets)
417    ///
418    /// # Returns
419    /// An AnimationManager ready for animation playback, or an error if resolution fails
420    ///
421    /// # Example
422    /// ```rust,ignore
423    /// use wow_m2::{M2Model, animation::AnimationManagerBuilder};
424    ///
425    /// let data = std::fs::read("model.m2")?;
426    /// let model = M2Model::parse(&mut std::io::Cursor::new(&data))?;
427    /// let manager = AnimationManagerBuilder::from_model(&model, &data)?;
428    ///
429    /// // Update animation each frame
430    /// manager.update(delta_time_ms);
431    /// let translation = manager.get_bone_translation(0);
432    /// ```
433    pub fn from_model(model: &crate::M2Model, data: &[u8]) -> crate::Result<AnimationManager> {
434        use std::io::Cursor;
435
436        // Extract global sequence durations
437        let global_sequence_durations: Vec<u32> = model.global_sequences.to_vec();
438
439        // Convert M2Animation to AnimSequence
440        let sequences: Vec<AnimSequence> = model
441            .animations
442            .iter()
443            .map(|seq| {
444                // Duration calculation differs by version:
445                // - Classic: end_timestamp - start_timestamp
446                // - BC+: start_timestamp IS the duration
447                let duration = seq
448                    .end_timestamp
449                    .map(|end| end.saturating_sub(seq.start_timestamp))
450                    .unwrap_or(seq.start_timestamp);
451
452                // Replay range (Classic only, defaults for BC+)
453                let (replay_min, replay_max) = seq
454                    .replay
455                    .map(|r| (r.minimum as u32, r.maximum as u32))
456                    .unwrap_or((0, 0));
457
458                AnimSequence {
459                    id: seq.animation_id,
460                    sub_id: seq.sub_animation_id,
461                    duration,
462                    movement_speed: seq.movement_speed,
463                    flags: seq.flags,
464                    frequency: seq.frequency as u16,
465                    replay_min,
466                    replay_max,
467                    blend_time: 150, // Default blend time (milliseconds)
468                    variation_next: seq.next_animation.unwrap_or(-1),
469                    alias_next: seq.aliasing.unwrap_or(0),
470                }
471            })
472            .collect();
473
474        let num_sequences = sequences.len();
475
476        // Resolve bone animation data
477        let mut cursor = Cursor::new(data);
478        let mut bones = Vec::with_capacity(model.bones.len());
479
480        for bone in &model.bones {
481            // Resolve translation track
482            let translation =
483                Self::resolve_vec3_track(&bone.translation, &mut cursor, num_sequences)?;
484
485            // Resolve rotation track (quaternion)
486            let rotation = Self::resolve_quat_track(&bone.rotation, &mut cursor, num_sequences)?;
487
488            // Resolve scale track
489            let scale = Self::resolve_vec3_track(&bone.scale, &mut cursor, num_sequences)?;
490
491            bones.push(ResolvedBone {
492                bone_id: bone.bone_id,
493                flags: bone.flags.bits(),
494                parent_bone: bone.parent_bone,
495                translation,
496                rotation,
497                scale,
498                pivot: Vec3::new(bone.pivot.x, bone.pivot.y, bone.pivot.z),
499            });
500        }
501
502        Ok(AnimationManager::new(
503            global_sequence_durations,
504            sequences,
505            bones,
506        ))
507    }
508
509    /// Resolve a Vec3 animation track from M2 data
510    fn resolve_vec3_track<R: std::io::Read + std::io::Seek>(
511        track: &crate::chunks::m2_track::M2TrackVec3,
512        reader: &mut R,
513        num_sequences: usize,
514    ) -> crate::Result<ResolvedTrack<Vec3>> {
515        use crate::chunks::m2_track_resolver::M2TrackVec3Ext;
516
517        if !track.has_data() {
518            return Ok(ResolvedTrack::empty());
519        }
520
521        let (timestamps_flat, values_flat, ranges) = track.resolve_data(reader)?;
522
523        // Convert C3Vector to Vec3
524        let values_vec3: Vec<Vec3> = values_flat
525            .into_iter()
526            .map(|v| Vec3::new(v.x, v.y, v.z))
527            .collect();
528
529        // Convert global_sequence: 0xFFFF means no global sequence, map to -1
530        let global_sequence = if track.base.global_sequence == 0xFFFF {
531            -1i16
532        } else {
533            track.base.global_sequence as i16
534        };
535
536        // If using global sequence, put all data in one sequence slot
537        if global_sequence >= 0 {
538            return Ok(ResolvedTrack {
539                interpolation_type: track.base.interpolation_type as u16,
540                global_sequence,
541                timestamps: vec![timestamps_flat],
542                values: vec![values_vec3],
543            });
544        }
545
546        // Split by animation sequence using ranges
547        let (timestamps, values) = Self::split_by_ranges(
548            timestamps_flat,
549            values_vec3,
550            ranges.as_deref(),
551            num_sequences,
552        );
553
554        Ok(ResolvedTrack {
555            interpolation_type: track.base.interpolation_type as u16,
556            global_sequence,
557            timestamps,
558            values,
559        })
560    }
561
562    /// Resolve a Quat animation track from M2 data
563    fn resolve_quat_track<R: std::io::Read + std::io::Seek>(
564        track: &crate::chunks::m2_track::M2TrackQuat,
565        reader: &mut R,
566        num_sequences: usize,
567    ) -> crate::Result<ResolvedTrack<Quat>> {
568        use crate::chunks::m2_track_resolver::M2TrackQuatExt;
569
570        if !track.has_data() {
571            return Ok(ResolvedTrack::empty());
572        }
573
574        let (timestamps_flat, values_flat, ranges) = track.resolve_data(reader)?;
575
576        // Convert M2CompQuat to Quat
577        let values_quat: Vec<Quat> = values_flat
578            .into_iter()
579            .map(|q| {
580                let (x, y, z, w) = q.to_float_quaternion();
581                Quat::new(x, y, z, w).normalize()
582            })
583            .collect();
584
585        // Convert global_sequence
586        let global_sequence = if track.base.global_sequence == 0xFFFF {
587            -1i16
588        } else {
589            track.base.global_sequence as i16
590        };
591
592        // If using global sequence, put all data in one sequence slot
593        if global_sequence >= 0 {
594            return Ok(ResolvedTrack {
595                interpolation_type: track.base.interpolation_type as u16,
596                global_sequence,
597                timestamps: vec![timestamps_flat],
598                values: vec![values_quat],
599            });
600        }
601
602        // Split by animation sequence using ranges
603        let (timestamps, values) = Self::split_by_ranges(
604            timestamps_flat,
605            values_quat,
606            ranges.as_deref(),
607            num_sequences,
608        );
609
610        Ok(ResolvedTrack {
611            interpolation_type: track.base.interpolation_type as u16,
612            global_sequence,
613            timestamps,
614            values,
615        })
616    }
617
618    /// Split flat timestamp/value arrays by animation sequence ranges
619    ///
620    /// Pre-WotLK M2s store ranges as pairs of (start, end) indices.
621    /// WotLK+ M2s don't have ranges and store one timestamp/value per sequence.
622    fn split_by_ranges<T: Clone>(
623        timestamps_flat: Vec<u32>,
624        values_flat: Vec<T>,
625        ranges: Option<&[u32]>,
626        num_sequences: usize,
627    ) -> (Vec<Vec<u32>>, Vec<Vec<T>>) {
628        if let Some(ranges) = ranges {
629            // Pre-WotLK: use ranges to split data
630            let mut timestamps = Vec::with_capacity(num_sequences);
631            let mut values = Vec::with_capacity(num_sequences);
632
633            for i in 0..num_sequences {
634                let range_idx = i * 2;
635                if range_idx + 1 < ranges.len() {
636                    let start = ranges[range_idx] as usize;
637                    let end = ranges[range_idx + 1] as usize;
638
639                    if start <= end && end <= timestamps_flat.len() && end <= values_flat.len() {
640                        timestamps.push(timestamps_flat[start..end].to_vec());
641                        values.push(values_flat[start..end].to_vec());
642                    } else {
643                        timestamps.push(Vec::new());
644                        values.push(Vec::new());
645                    }
646                } else {
647                    timestamps.push(Vec::new());
648                    values.push(Vec::new());
649                }
650            }
651
652            (timestamps, values)
653        } else {
654            // WotLK+: timestamps/values are stored per-sequence in order
655            // This is a simplified view - in practice WotLK+ uses external .anim files
656            // For embedded data, we assume it's for all sequences
657            let mut timestamps = Vec::with_capacity(num_sequences);
658            let mut values = Vec::with_capacity(num_sequences);
659
660            // Put all data in first sequence, empty for rest
661            if !timestamps_flat.is_empty() {
662                timestamps.push(timestamps_flat);
663                values.push(values_flat);
664            }
665
666            // Pad with empty vectors for remaining sequences
667            while timestamps.len() < num_sequences {
668                timestamps.push(Vec::new());
669                values.push(Vec::new());
670            }
671
672            (timestamps, values)
673        }
674    }
675}
676
677#[cfg(test)]
678mod tests {
679    use super::*;
680
681    fn create_test_sequence(id: u16, duration: u32) -> AnimSequence {
682        AnimSequence {
683            id,
684            sub_id: 0,
685            duration,
686            movement_speed: 0.0,
687            flags: 0,
688            frequency: 0x7fff,
689            replay_min: 0,
690            replay_max: 0,
691            blend_time: 0,
692            variation_next: -1,
693            alias_next: 0,
694        }
695    }
696
697    #[test]
698    fn test_animation_manager_empty() {
699        let manager = AnimationManager::empty();
700        assert_eq!(manager.bone_count(), 0);
701        assert_eq!(manager.sequence_count(), 0);
702        assert_eq!(manager.current_animation_index(), None);
703    }
704
705    #[test]
706    fn test_animation_manager_basic() {
707        let sequences = vec![
708            create_test_sequence(0, 1000), // Stand
709            create_test_sequence(4, 500),  // Walk
710        ];
711
712        let manager = AnimationManager::new(vec![], sequences, vec![]);
713
714        // Should start with Stand animation
715        assert_eq!(manager.current_animation_index(), Some(0));
716        assert_eq!(manager.sequence_count(), 2);
717    }
718
719    #[test]
720    fn test_animation_update() {
721        let sequences = vec![create_test_sequence(0, 1000)];
722        let mut manager = AnimationManager::new(vec![], sequences, vec![]);
723
724        // Advance time
725        manager.update(500.0);
726        assert!((manager.current_time() - 500.0).abs() < 0.001);
727
728        // Advance past duration (should loop)
729        manager.update(600.0);
730        assert!(manager.current_time() < 1000.0);
731    }
732
733    #[test]
734    fn test_set_animation() {
735        let sequences = vec![create_test_sequence(0, 1000), create_test_sequence(4, 500)];
736        let mut manager = AnimationManager::new(vec![], sequences, vec![]);
737
738        manager.set_animation_id(4);
739        assert_eq!(manager.current_animation_index(), Some(1));
740        assert!((manager.current_time() - 0.0).abs() < 0.001);
741    }
742
743    #[test]
744    fn test_global_sequences() {
745        let sequences = vec![create_test_sequence(0, 1000)];
746        let global_durations = vec![500, 1000];
747
748        let mut manager = AnimationManager::new(global_durations, sequences, vec![]);
749
750        // Update global times
751        manager.update(250.0);
752
753        let times = manager.global_times();
754        assert!((times[0] - 250.0).abs() < 0.001);
755        assert!((times[1] - 250.0).abs() < 0.001);
756
757        // Check wrapping
758        manager.update(300.0); // Total 550ms
759        let times = manager.global_times();
760        assert!((times[0] - 50.0).abs() < 0.001); // 550 % 500 = 50
761        assert!((times[1] - 550.0).abs() < 0.001); // No wrap yet
762    }
763
764    #[test]
765    fn test_bone_interpolation() {
766        let bone = ResolvedBone {
767            bone_id: 0,
768            flags: 0,
769            parent_bone: -1,
770            translation: ResolvedTrack {
771                interpolation_type: 1, // Linear
772                global_sequence: -1,
773                timestamps: vec![vec![0, 100]],
774                values: vec![vec![Vec3::ZERO, Vec3::new(10.0, 0.0, 0.0)]],
775            },
776            rotation: ResolvedTrack::empty(),
777            scale: ResolvedTrack::empty(),
778            pivot: Vec3::ZERO,
779        };
780
781        let sequences = vec![create_test_sequence(0, 1000)];
782        let mut manager = AnimationManager::new(vec![], sequences, vec![bone]);
783
784        // At time 0
785        let trans = manager.get_bone_translation(0);
786        assert!((trans.x - 0.0).abs() < 0.001);
787
788        // At time 50
789        manager.update(50.0);
790        let trans = manager.get_bone_translation(0);
791        assert!((trans.x - 5.0).abs() < 0.001);
792
793        // At time 100
794        manager.update(50.0);
795        let trans = manager.get_bone_translation(0);
796        assert!((trans.x - 10.0).abs() < 0.001);
797    }
798}