Skip to main content

proof_engine/anim/
mod.rs

1//! Animation State Machine for Proof Engine.
2//!
3//! Full-featured hierarchical animation system:
4//! - Named states with clip references, loop, speed, events
5//! - Transitions with blend duration, conditions, interrupt rules
6//! - Blend trees: 1D linear, 2D directional, additive
7//! - Layered animation with per-bone masks and blend weights
8//! - Root motion extraction and accumulation
9//! - Sub-state machines (nested HSM)
10//! - Sample-accurate animation events
11
12pub mod skeleton;
13pub mod clips;
14
15use std::collections::HashMap;
16
17// ── AnimCurve ─────────────────────────────────────────────────────────────────
18
19/// Hermite-interpolated keyframe curve (maps time → value).
20#[derive(Debug, Clone)]
21pub struct AnimCurve {
22    /// Sorted list of (time, value, in_tangent, out_tangent).
23    pub keyframes: Vec<(f32, f32, f32, f32)>,
24    pub extrapolate: Extrapolate,
25}
26
27#[derive(Debug, Clone, Copy, PartialEq)]
28pub enum Extrapolate {
29    /// Clamp to first/last value.
30    Clamp,
31    /// Loop the curve.
32    Loop,
33    /// Ping-pong loop.
34    PingPong,
35    /// Linear extrapolation from last tangent.
36    Linear,
37}
38
39impl AnimCurve {
40    pub fn constant(value: f32) -> Self {
41        Self {
42            keyframes: vec![(0.0, value, 0.0, 0.0)],
43            extrapolate: Extrapolate::Clamp,
44        }
45    }
46
47    pub fn linear(t0: f32, v0: f32, t1: f32, v1: f32) -> Self {
48        let tangent = if (t1 - t0).abs() > 1e-6 { (v1 - v0) / (t1 - t0) } else { 0.0 };
49        Self {
50            keyframes: vec![(t0, v0, tangent, tangent), (t1, v1, tangent, tangent)],
51            extrapolate: Extrapolate::Clamp,
52        }
53    }
54
55    /// Sample the curve at time `t`.
56    pub fn sample(&self, t: f32) -> f32 {
57        if self.keyframes.is_empty() { return 0.0; }
58        if self.keyframes.len() == 1 { return self.keyframes[0].1; }
59
60        let duration = self.keyframes.last().unwrap().0 - self.keyframes[0].0;
61        let t = self.wrap_time(t, duration);
62
63        // Binary search for segment
64        let idx = self.keyframes.partition_point(|k| k.0 <= t);
65        if idx == 0 { return self.keyframes[0].1; }
66        if idx >= self.keyframes.len() { return self.keyframes.last().unwrap().1; }
67
68        let (t0, v0, _in0, out0) = self.keyframes[idx - 1];
69        let (t1, v1, in1, _out1) = self.keyframes[idx];
70
71        let dt = t1 - t0;
72        if dt < 1e-6 { return v1; }
73
74        let u = (t - t0) / dt;
75        // Hermite basis
76        let h00 = (2.0 * u * u * u) - (3.0 * u * u) + 1.0;
77        let h10 =        u * u * u  - (2.0 * u * u) + u;
78        let h01 = -(2.0 * u * u * u) + (3.0 * u * u);
79        let h11 =        u * u * u  -        u * u;
80        h00 * v0 + h10 * dt * out0 + h01 * v1 + h11 * dt * in1
81    }
82
83    fn wrap_time(&self, t: f32, duration: f32) -> f32 {
84        if duration < 1e-6 { return self.keyframes[0].0; }
85        match self.extrapolate {
86            Extrapolate::Clamp => t.clamp(self.keyframes[0].0, self.keyframes.last().unwrap().0),
87            Extrapolate::Loop  => self.keyframes[0].0 + (t - self.keyframes[0].0).rem_euclid(duration),
88            Extrapolate::PingPong => {
89                let local = (t - self.keyframes[0].0).rem_euclid(duration * 2.0);
90                self.keyframes[0].0 + if local < duration { local } else { duration * 2.0 - local }
91            }
92            Extrapolate::Linear => t,
93        }
94    }
95}
96
97// ── AnimChannel ───────────────────────────────────────────────────────────────
98
99/// A named channel within a clip (e.g., "pos_x", "rot_z", "scale_y").
100#[derive(Debug, Clone)]
101pub struct AnimChannel {
102    pub target_path: String, // "transform/pos_x" etc.
103    pub curve: AnimCurve,
104}
105
106// ── AnimClip ──────────────────────────────────────────────────────────────────
107
108/// An animation clip: a named collection of channels over time.
109#[derive(Debug, Clone)]
110pub struct AnimClip {
111    pub name:     String,
112    pub duration: f32,
113    pub fps:      f32,
114    pub looping:  bool,
115    pub channels: Vec<AnimChannel>,
116    /// Root-motion channel (optional). Stores world-space delta per frame.
117    pub root_motion: Option<RootMotionData>,
118}
119
120impl AnimClip {
121    pub fn new(name: &str, duration: f32) -> Self {
122        Self {
123            name: name.to_string(),
124            duration,
125            fps: 30.0,
126            looping: true,
127            channels: Vec::new(),
128            root_motion: None,
129        }
130    }
131
132    /// Add a transform channel curve.
133    pub fn add_channel(&mut self, path: &str, curve: AnimCurve) {
134        self.channels.push(AnimChannel { target_path: path.to_string(), curve });
135    }
136
137    /// Sample all channels at time `t`, returning a map of path → value.
138    pub fn sample(&self, t: f32) -> HashMap<String, f32> {
139        let t = if self.looping { t.rem_euclid(self.duration) } else { t.min(self.duration) };
140        self.channels.iter().map(|ch| (ch.target_path.clone(), ch.curve.sample(t))).collect()
141    }
142
143    /// Blend two sample results by weight `alpha` (0 = a, 1 = b).
144    pub fn blend_samples(a: &HashMap<String, f32>, b: &HashMap<String, f32>, alpha: f32) -> HashMap<String, f32> {
145        let mut out = a.clone();
146        for (k, vb) in b {
147            let va = out.entry(k.clone()).or_insert(0.0);
148            *va = *va * (1.0 - alpha) + vb * alpha;
149        }
150        out
151    }
152}
153
154// ── RootMotionData ────────────────────────────────────────────────────────────
155
156/// Per-frame root motion deltas baked from the root bone.
157#[derive(Debug, Clone)]
158pub struct RootMotionData {
159    /// (delta_x, delta_y, delta_rotation) per normalized time step.
160    pub frames: Vec<(f32, f32, f32)>,
161}
162
163impl RootMotionData {
164    /// Accumulate root motion between two normalized times [t0, t1].
165    pub fn accumulate(&self, t0: f32, t1: f32) -> (f32, f32, f32) {
166        if self.frames.is_empty() { return (0.0, 0.0, 0.0); }
167        let n = self.frames.len();
168        let i0 = ((t0 * n as f32) as usize).min(n - 1);
169        let i1 = ((t1 * n as f32) as usize).min(n - 1);
170        let (mut dx, mut dy, mut dr) = (0.0_f32, 0.0_f32, 0.0_f32);
171        for i in i0..i1 {
172            dx += self.frames[i].0;
173            dy += self.frames[i].1;
174            dr += self.frames[i].2;
175        }
176        (dx, dy, dr)
177    }
178}
179
180// ── AnimEvent ─────────────────────────────────────────────────────────────────
181
182/// An event fired at a specific normalized time within a clip.
183#[derive(Debug, Clone)]
184pub struct AnimEvent {
185    /// Normalized time [0, 1] in clip when event fires.
186    pub normalized_time: f32,
187    /// Event identifier (e.g. "footstep_left", "attack_hit", "spawn_fx").
188    pub name: String,
189    /// Optional payload float.
190    pub value: f32,
191}
192
193// ── Condition ─────────────────────────────────────────────────────────────────
194
195/// Condition evaluated against an `AnimParamSet`.
196#[derive(Debug, Clone)]
197pub enum Condition {
198    BoolTrue(String),
199    BoolFalse(String),
200    IntEquals(String, i32),
201    IntGreater(String, i32),
202    IntLess(String, i32),
203    FloatGreater(String, f32),
204    FloatLess(String, f32),
205    Trigger(String),
206}
207
208impl Condition {
209    pub fn check(&self, params: &AnimParamSet) -> bool {
210        match self {
211            Condition::BoolTrue(n)       => params.get_bool(n),
212            Condition::BoolFalse(n)      => !params.get_bool(n),
213            Condition::IntEquals(n, v)   => params.get_int(n) == *v,
214            Condition::IntGreater(n, v)  => params.get_int(n) > *v,
215            Condition::IntLess(n, v)     => params.get_int(n) < *v,
216            Condition::FloatGreater(n,v) => params.get_float(n) > *v,
217            Condition::FloatLess(n, v)   => params.get_float(n) < *v,
218            Condition::Trigger(n)        => params.consume_trigger(n),
219        }
220    }
221}
222
223// ── AnimParamSet ──────────────────────────────────────────────────────────────
224
225/// Runtime parameter store driving state machine conditions.
226#[derive(Debug, Clone, Default)]
227pub struct AnimParamSet {
228    floats:   HashMap<String, f32>,
229    ints:     HashMap<String, i32>,
230    bools:    HashMap<String, bool>,
231    triggers: std::collections::HashSet<String>,
232    /// Consumed triggers buffered until next update.
233    consumed: Vec<String>,
234}
235
236impl AnimParamSet {
237    pub fn set_float(&mut self, name: &str, v: f32)  { self.floats.insert(name.to_string(), v); }
238    pub fn set_int  (&mut self, name: &str, v: i32)  { self.ints.insert(name.to_string(), v); }
239    pub fn set_bool (&mut self, name: &str, v: bool) { self.bools.insert(name.to_string(), v); }
240    pub fn set_trigger(&mut self, name: &str)        { self.triggers.insert(name.to_string()); }
241
242    pub fn get_float(&self, name: &str) -> f32  { *self.floats.get(name).unwrap_or(&0.0) }
243    pub fn get_int  (&self, name: &str) -> i32  { *self.ints.get(name).unwrap_or(&0) }
244    pub fn get_bool (&self, name: &str) -> bool { *self.bools.get(name).unwrap_or(&false) }
245
246    pub fn consume_trigger(&self, name: &str) -> bool {
247        self.triggers.contains(name)
248    }
249
250    /// Call after each update to clear consumed triggers.
251    pub fn flush_triggers(&mut self) {
252        for name in self.consumed.drain(..) {
253            self.triggers.remove(&name);
254        }
255    }
256
257    pub fn mark_trigger_consumed(&mut self, name: &str) {
258        self.consumed.push(name.to_string());
259    }
260}
261
262// ── AnimTransition ────────────────────────────────────────────────────────────
263
264/// Transition from one state to another.
265#[derive(Debug, Clone)]
266pub struct AnimTransition {
267    pub from_state:   String,
268    pub to_state:     String,
269    /// Blend duration in seconds (0 = instant cut).
270    pub blend_duration: f32,
271    /// All conditions must be true for this transition.
272    pub conditions:   Vec<Condition>,
273    /// Normalized time in source clip to start blending (0 = any time).
274    pub exit_time:    Option<f32>,
275    /// Can this transition interrupt itself?
276    pub can_interrupt: bool,
277    /// Priority (higher = checked first).
278    pub priority:     i32,
279}
280
281impl AnimTransition {
282    pub fn new(from: &str, to: &str, blend_secs: f32) -> Self {
283        Self {
284            from_state: from.to_string(),
285            to_state: to.to_string(),
286            blend_duration: blend_secs,
287            conditions: Vec::new(),
288            exit_time: None,
289            can_interrupt: false,
290            priority: 0,
291        }
292    }
293
294    pub fn with_condition(mut self, c: Condition) -> Self {
295        self.conditions.push(c);
296        self
297    }
298
299    pub fn with_exit_time(mut self, t: f32) -> Self {
300        self.exit_time = Some(t);
301        self
302    }
303
304    pub fn interruptible(mut self) -> Self {
305        self.can_interrupt = true;
306        self
307    }
308
309    pub fn is_ready(&self, params: &AnimParamSet, normalized_time: f32) -> bool {
310        // Check exit time
311        if let Some(et) = self.exit_time {
312            if normalized_time < et { return false; }
313        }
314        // Check all conditions
315        self.conditions.iter().all(|c| c.check(params))
316    }
317}
318
319// ── BlendTree ─────────────────────────────────────────────────────────────────
320
321/// A blend tree node — either a leaf (clip) or a blend operation.
322#[derive(Debug, Clone)]
323pub enum BlendTree {
324    /// Leaf: single clip.
325    Clip { clip_name: String, speed: f32 },
326
327    /// 1D blend: interpolate between multiple clips by a float parameter.
328    Linear1D {
329        param:    String,
330        children: Vec<(f32, BlendTree)>, // (threshold, subtree)
331    },
332
333    /// 2D directional blend (blend by 2D vector param).
334    Directional2D {
335        param_x: String,
336        param_y: String,
337        children: Vec<([f32; 2], BlendTree)>, // (position, subtree)
338    },
339
340    /// Additive blend: play base + additive on top.
341    Additive {
342        base:     Box<BlendTree>,
343        additive: Box<BlendTree>,
344        weight_param: Option<String>,
345        weight:   f32,
346    },
347
348    /// Override: apply second layer over first on masked channels.
349    Override {
350        base:    Box<BlendTree>,
351        overlay: Box<BlendTree>,
352        mask:    Vec<String>, // channel paths included in overlay
353        weight:  f32,
354    },
355}
356
357impl BlendTree {
358    /// Evaluate the blend tree, returning a sampled pose.
359    pub fn evaluate(
360        &self,
361        clips:  &HashMap<String, AnimClip>,
362        params: &AnimParamSet,
363        time:   f32,
364    ) -> HashMap<String, f32> {
365        match self {
366            BlendTree::Clip { clip_name, speed } => {
367                if let Some(clip) = clips.get(clip_name) {
368                    clip.sample(time * speed)
369                } else {
370                    HashMap::new()
371                }
372            }
373
374            BlendTree::Linear1D { param, children } => {
375                if children.is_empty() { return HashMap::new(); }
376                let v = params.get_float(param);
377
378                // Find the two surrounding thresholds
379                let idx = children.partition_point(|(t, _)| *t <= v);
380
381                if idx == 0 {
382                    return children[0].1.evaluate(clips, params, time);
383                }
384                if idx >= children.len() {
385                    return children.last().unwrap().1.evaluate(clips, params, time);
386                }
387
388                let (t0, sub0) = &children[idx - 1];
389                let (t1, sub1) = &children[idx];
390                let alpha = if (t1 - t0).abs() > 1e-6 { (v - t0) / (t1 - t0) } else { 0.0 };
391
392                let a = sub0.evaluate(clips, params, time);
393                let b = sub1.evaluate(clips, params, time);
394                AnimClip::blend_samples(&a, &b, alpha.clamp(0.0, 1.0))
395            }
396
397            BlendTree::Directional2D { param_x, param_y, children } => {
398                if children.is_empty() { return HashMap::new(); }
399                let vx = params.get_float(param_x);
400                let vy = params.get_float(param_y);
401
402                // Find closest two children by 2D distance and blend by inverse distance
403                let mut dists: Vec<(f32, usize)> = children.iter().enumerate().map(|(i, (pos, _))| {
404                    let dx = pos[0] - vx;
405                    let dy = pos[1] - vy;
406                    (dx * dx + dy * dy, i)
407                }).collect();
408                dists.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
409
410                let (d0, i0) = dists[0];
411                let (d1, i1) = if dists.len() > 1 { dists[1] } else { dists[0] };
412
413                let total = d0 + d1;
414                let alpha = if total < 1e-6 { 0.0 } else { d0 / total };
415
416                let a = children[i0].1.evaluate(clips, params, time);
417                let b = children[i1].1.evaluate(clips, params, time);
418                AnimClip::blend_samples(&a, &b, alpha)
419            }
420
421            BlendTree::Additive { base, additive, weight_param, weight } => {
422                let base_pose = base.evaluate(clips, params, time);
423                let add_pose  = additive.evaluate(clips, params, time);
424                let w = weight_param.as_ref().map(|p| params.get_float(p)).unwrap_or(*weight);
425                // Additive: add scaled additive on top of base
426                let mut out = base_pose;
427                for (k, v) in &add_pose {
428                    let entry = out.entry(k.clone()).or_insert(0.0);
429                    *entry += v * w;
430                }
431                out
432            }
433
434            BlendTree::Override { base, overlay, mask, weight } => {
435                let base_pose    = base.evaluate(clips, params, time);
436                let overlay_pose = overlay.evaluate(clips, params, time);
437                let mut out = base_pose;
438                for (k, v) in &overlay_pose {
439                    if mask.iter().any(|m| k.starts_with(m.as_str())) {
440                        let entry = out.entry(k.clone()).or_insert(0.0);
441                        *entry = *entry * (1.0 - weight) + v * weight;
442                    }
443                }
444                out
445            }
446        }
447    }
448}
449
450// ── AnimState ─────────────────────────────────────────────────────────────────
451
452/// A single state in the state machine.
453#[derive(Debug, Clone)]
454pub struct AnimState {
455    pub name:       String,
456    pub motion:     StateMotion,
457    /// Speed multiplier (can be driven by float param).
458    pub speed:      f32,
459    pub speed_param: Option<String>,
460    /// Events fired at specific normalized times.
461    pub events:     Vec<AnimEvent>,
462    /// Mirror motion horizontally.
463    pub mirror:     bool,
464    /// Cycle offset [0, 1] — shifts start time.
465    pub cycle_offset: f32,
466}
467
468/// What this state plays.
469#[derive(Debug, Clone)]
470pub enum StateMotion {
471    Clip(String),
472    BlendTree(BlendTree),
473    SubStateMachine(Box<AnimStateMachine>),
474    Empty,
475}
476
477impl AnimState {
478    pub fn clip(name: &str, clip_name: &str) -> Self {
479        Self {
480            name: name.to_string(),
481            motion: StateMotion::Clip(clip_name.to_string()),
482            speed: 1.0,
483            speed_param: None,
484            events: Vec::new(),
485            mirror: false,
486            cycle_offset: 0.0,
487        }
488    }
489
490    pub fn blend_tree(name: &str, tree: BlendTree) -> Self {
491        Self {
492            name: name.to_string(),
493            motion: StateMotion::BlendTree(tree),
494            speed: 1.0,
495            speed_param: None,
496            events: Vec::new(),
497            mirror: false,
498            cycle_offset: 0.0,
499        }
500    }
501
502    pub fn effective_speed(&self, params: &AnimParamSet) -> f32 {
503        self.speed_param.as_ref().map(|p| params.get_float(p)).unwrap_or(self.speed)
504    }
505}
506
507// ── AnimLayer ─────────────────────────────────────────────────────────────────
508
509/// An independent layer in the animator, blended into the final pose.
510#[derive(Debug, Clone)]
511pub struct AnimLayer {
512    pub name:     String,
513    pub weight:   f32,
514    pub blend_mode: LayerBlend,
515    /// Channel paths this layer affects. Empty = all channels.
516    pub mask:     Vec<String>,
517    /// Own state machine for this layer.
518    pub machine:  AnimStateMachine,
519}
520
521#[derive(Debug, Clone, Copy, PartialEq)]
522pub enum LayerBlend {
523    Override,
524    Additive,
525}
526
527impl AnimLayer {
528    pub fn new(name: &str, machine: AnimStateMachine) -> Self {
529        Self {
530            name: name.to_string(),
531            weight: 1.0,
532            blend_mode: LayerBlend::Override,
533            mask: Vec::new(),
534            machine,
535        }
536    }
537
538    pub fn additive(mut self) -> Self {
539        self.blend_mode = LayerBlend::Additive;
540        self
541    }
542
543    pub fn with_mask(mut self, paths: Vec<&str>) -> Self {
544        self.mask = paths.into_iter().map(|s| s.to_string()).collect();
545        self
546    }
547}
548
549// ── TransitionState ───────────────────────────────────────────────────────────
550
551/// Active transition being blended.
552#[derive(Debug, Clone)]
553struct ActiveTransition {
554    to_state:       String,
555    elapsed:        f32,
556    duration:       f32,
557    destination_time: f32,
558}
559
560// ── AnimStateMachine ──────────────────────────────────────────────────────────
561
562/// Core hierarchical state machine.
563#[derive(Debug, Clone)]
564pub struct AnimStateMachine {
565    pub name:         String,
566    pub states:       HashMap<String, AnimState>,
567    pub transitions:  Vec<AnimTransition>,
568    pub entry_state:  Option<String>,
569    pub any_state_transitions: Vec<AnimTransition>,
570
571    // Runtime
572    pub current_state: Option<String>,
573    state_time:        f32,
574    normalized_time:   f32,
575    active_transition: Option<ActiveTransition>,
576    last_clip_duration: f32,
577    fired_events:      Vec<AnimEvent>,
578}
579
580impl AnimStateMachine {
581    pub fn new(name: &str) -> Self {
582        Self {
583            name: name.to_string(),
584            states: HashMap::new(),
585            transitions: Vec::new(),
586            entry_state: None,
587            any_state_transitions: Vec::new(),
588            current_state: None,
589            state_time: 0.0,
590            normalized_time: 0.0,
591            active_transition: None,
592            last_clip_duration: 1.0,
593            fired_events: Vec::new(),
594        }
595    }
596
597    pub fn add_state(&mut self, state: AnimState) {
598        if self.entry_state.is_none() {
599            self.entry_state = Some(state.name.clone());
600        }
601        self.states.insert(state.name.clone(), state);
602    }
603
604    pub fn add_transition(&mut self, t: AnimTransition) {
605        self.transitions.push(t);
606    }
607
608    pub fn add_any_transition(&mut self, t: AnimTransition) {
609        self.any_state_transitions.push(t);
610    }
611
612    /// Enter this machine — starts at entry state.
613    pub fn enter(&mut self) {
614        self.current_state = self.entry_state.clone();
615        self.state_time = 0.0;
616        self.normalized_time = 0.0;
617        self.active_transition = None;
618    }
619
620    /// Update the state machine by `dt` seconds.
621    /// Returns the sampled pose (channel → value map).
622    pub fn update(
623        &mut self,
624        dt:     f32,
625        params: &mut AnimParamSet,
626        clips:  &HashMap<String, AnimClip>,
627    ) -> HashMap<String, f32> {
628        // Start if not running
629        if self.current_state.is_none() { self.enter(); }
630
631        let cur_name = match &self.current_state {
632            Some(n) => n.clone(),
633            None    => return HashMap::new(),
634        };
635
636        let cur_state = match self.states.get(&cur_name) {
637            Some(s) => s.clone(),
638            None    => return HashMap::new(),
639        };
640
641        let speed = cur_state.effective_speed(params);
642        self.state_time += dt * speed;
643
644        // Compute clip duration
645        let clip_dur = match &cur_state.motion {
646            StateMotion::Clip(c) => clips.get(c).map(|cl| cl.duration).unwrap_or(1.0),
647            _ => 1.0,
648        };
649        self.last_clip_duration = clip_dur;
650        self.normalized_time = (self.state_time / clip_dur.max(1e-6)).fract();
651
652        // Fire events
653        self.check_events(&cur_state, self.normalized_time);
654
655        // Advance active transition
656        if let Some(ref mut at) = self.active_transition {
657            at.elapsed += dt;
658            at.destination_time += dt;
659            if at.elapsed >= at.duration {
660                // Transition complete
661                let to = at.to_state.clone();
662                let dest_t = at.destination_time;
663                self.active_transition = None;
664                self.current_state = Some(to.clone());
665                self.state_time = dest_t;
666                self.normalized_time = (dest_t / clip_dur.max(1e-6)).fract();
667            }
668        }
669
670        // Check transitions (only when not already transitioning, or interruptible)
671        if self.active_transition.is_none() {
672            let triggered = self.find_transition(&cur_name, params, self.normalized_time);
673            if let Some(t) = triggered {
674                let to = t.to_state.clone();
675                let dur = t.blend_duration;
676                // Consume triggers
677                for cond in &t.conditions {
678                    if let Condition::Trigger(n) = cond {
679                        params.mark_trigger_consumed(n);
680                    }
681                }
682                if dur < 1e-6 {
683                    // Instant transition
684                    self.current_state = Some(to);
685                    self.state_time = 0.0;
686                    self.normalized_time = 0.0;
687                } else {
688                    self.active_transition = Some(ActiveTransition {
689                        to_state: to,
690                        elapsed: 0.0,
691                        duration: dur,
692                        destination_time: 0.0,
693                    });
694                }
695            }
696        }
697        params.flush_triggers();
698
699        // Sample current pose
700        let current_pose = self.sample_state(&cur_state, clips, params, self.state_time);
701
702        // Blend with transition destination if active
703        if let Some(ref at) = self.active_transition {
704            let alpha = (at.elapsed / at.duration.max(1e-6)).clamp(0.0, 1.0);
705            let alpha = smooth_step(alpha);
706            if let Some(dest_state) = self.states.get(&at.to_state).cloned() {
707                let dest_pose = self.sample_state(&dest_state, clips, params, at.destination_time);
708                return AnimClip::blend_samples(&current_pose, &dest_pose, alpha);
709            }
710        }
711
712        current_pose
713    }
714
715    fn sample_state(
716        &self,
717        state: &AnimState,
718        clips: &HashMap<String, AnimClip>,
719        params: &AnimParamSet,
720        time: f32,
721    ) -> HashMap<String, f32> {
722        match &state.motion {
723            StateMotion::Clip(c) => {
724                if let Some(clip) = clips.get(c) {
725                    clip.sample(time)
726                } else {
727                    HashMap::new()
728                }
729            }
730            StateMotion::BlendTree(tree) => tree.evaluate(clips, params, time),
731            StateMotion::SubStateMachine(_) => HashMap::new(), // handled at outer level
732            StateMotion::Empty => HashMap::new(),
733        }
734    }
735
736    fn find_transition<'a>(
737        &'a self,
738        from: &str,
739        params: &AnimParamSet,
740        normalized_time: f32,
741    ) -> Option<&'a AnimTransition> {
742        // Any-state transitions checked first (sorted by priority desc)
743        let mut candidates: Vec<&AnimTransition> = self.any_state_transitions.iter()
744            .filter(|t| t.to_state != *from && t.is_ready(params, normalized_time))
745            .collect();
746
747        // From-state transitions
748        candidates.extend(self.transitions.iter()
749            .filter(|t| t.from_state == *from && t.is_ready(params, normalized_time)));
750
751        candidates.sort_by(|a, b| b.priority.cmp(&a.priority));
752        candidates.into_iter().next()
753    }
754
755    fn check_events(&mut self, state: &AnimState, normalized_time: f32) {
756        for ev in &state.events {
757            // Simple edge-crossing check (would need prev_time for real impl)
758            if (ev.normalized_time - normalized_time).abs() < 0.02 {
759                self.fired_events.push(ev.clone());
760            }
761        }
762    }
763
764    /// Drain and return fired events since last update.
765    pub fn drain_events(&mut self) -> Vec<AnimEvent> {
766        std::mem::take(&mut self.fired_events)
767    }
768
769    pub fn current_state_name(&self) -> Option<&str> {
770        self.current_state.as_deref()
771    }
772
773    pub fn normalized_time(&self) -> f32 { self.normalized_time }
774    pub fn state_time(&self) -> f32 { self.state_time }
775    pub fn is_transitioning(&self) -> bool { self.active_transition.is_some() }
776}
777
778// ── Animator ──────────────────────────────────────────────────────────────────
779
780/// Top-level animator: holds layers, clips, and parameter set.
781/// This is the main entry point for animation.
782pub struct Animator {
783    pub layers:  Vec<AnimLayer>,
784    pub clips:   HashMap<String, AnimClip>,
785    pub params:  AnimParamSet,
786    /// Accumulated root motion delta since last consume.
787    root_motion: (f32, f32, f32),
788    /// Whether to extract root motion.
789    pub use_root_motion: bool,
790}
791
792impl Animator {
793    pub fn new() -> Self {
794        Self {
795            layers: Vec::new(),
796            clips: HashMap::new(),
797            params: AnimParamSet::default(),
798            root_motion: (0.0, 0.0, 0.0),
799            use_root_motion: false,
800        }
801    }
802
803    pub fn add_clip(&mut self, clip: AnimClip) {
804        self.clips.insert(clip.name.clone(), clip);
805    }
806
807    pub fn add_layer(&mut self, layer: AnimLayer) {
808        self.layers.push(layer);
809    }
810
811    pub fn set_float(&mut self, n: &str, v: f32)  { self.params.set_float(n, v); }
812    pub fn set_int  (&mut self, n: &str, v: i32)  { self.params.set_int(n, v); }
813    pub fn set_bool (&mut self, n: &str, v: bool) { self.params.set_bool(n, v); }
814    pub fn set_trigger(&mut self, n: &str)        { self.params.set_trigger(n); }
815
816    /// Update all layers and merge poses.
817    pub fn update(&mut self, dt: f32) -> HashMap<String, f32> {
818        let mut final_pose: HashMap<String, f32> = HashMap::new();
819
820        for layer in &mut self.layers {
821            let pose = layer.machine.update(dt, &mut self.params, &self.clips);
822            let weight = layer.weight;
823
824            // Apply mask filter
825            let masked_pose: HashMap<String, f32> = if layer.mask.is_empty() {
826                pose
827            } else {
828                pose.into_iter()
829                    .filter(|(k, _)| layer.mask.iter().any(|m| k.starts_with(m.as_str())))
830                    .collect()
831            };
832
833            match layer.blend_mode {
834                LayerBlend::Override => {
835                    for (k, v) in masked_pose {
836                        let entry = final_pose.entry(k).or_insert(0.0);
837                        *entry = *entry * (1.0 - weight) + v * weight;
838                    }
839                }
840                LayerBlend::Additive => {
841                    for (k, v) in masked_pose {
842                        let entry = final_pose.entry(k).or_insert(0.0);
843                        *entry += v * weight;
844                    }
845                }
846            }
847        }
848
849        final_pose
850    }
851
852    /// Consume and return accumulated root motion delta.
853    pub fn consume_root_motion(&mut self) -> (f32, f32, f32) {
854        std::mem::take(&mut self.root_motion)
855    }
856
857    /// Drain all fired events from all layers.
858    pub fn drain_events(&mut self) -> Vec<AnimEvent> {
859        self.layers.iter_mut().flat_map(|l| l.machine.drain_events()).collect()
860    }
861}
862
863impl Default for Animator {
864    fn default() -> Self { Self::new() }
865}
866
867// ── AnimatorBuilder ───────────────────────────────────────────────────────────
868
869/// Ergonomic builder for constructing animators.
870pub struct AnimatorBuilder {
871    animator: Animator,
872}
873
874impl AnimatorBuilder {
875    pub fn new() -> Self {
876        Self { animator: Animator::new() }
877    }
878
879    pub fn clip(mut self, clip: AnimClip) -> Self {
880        self.animator.add_clip(clip);
881        self
882    }
883
884    pub fn layer(mut self, layer: AnimLayer) -> Self {
885        self.animator.add_layer(layer);
886        self
887    }
888
889    pub fn root_motion(mut self) -> Self {
890        self.animator.use_root_motion = true;
891        self
892    }
893
894    pub fn build(self) -> Animator {
895        self.animator
896    }
897}
898
899// ── AnimPresets ───────────────────────────────────────────────────────────────
900
901/// Pre-built state machine configurations for common character archetypes.
902pub struct AnimPresets;
903
904impl AnimPresets {
905    /// Standard humanoid locomotion: idle, walk, run, jump, fall, land.
906    pub fn humanoid_locomotion() -> AnimStateMachine {
907        let mut sm = AnimStateMachine::new("locomotion");
908
909        sm.add_state(AnimState::clip("idle", "humanoid_idle"));
910        sm.add_state(AnimState::clip("walk", "humanoid_walk"));
911        sm.add_state(AnimState::blend_tree("locomotion_blend",
912            BlendTree::Linear1D {
913                param: "speed".to_string(),
914                children: vec![
915                    (0.0,  BlendTree::Clip { clip_name: "humanoid_idle".to_string(), speed: 1.0 }),
916                    (0.5,  BlendTree::Clip { clip_name: "humanoid_walk".to_string(), speed: 1.0 }),
917                    (1.0,  BlendTree::Clip { clip_name: "humanoid_run".to_string(),  speed: 1.0 }),
918                ],
919            }
920        ));
921        sm.add_state(AnimState::clip("jump_rise", "humanoid_jump_rise"));
922        sm.add_state(AnimState::clip("jump_fall", "humanoid_jump_fall"));
923        sm.add_state(AnimState::clip("land",      "humanoid_land"));
924
925        sm.add_transition(AnimTransition::new("locomotion_blend", "jump_rise", 0.1)
926            .with_condition(Condition::Trigger("jump".to_string())));
927        sm.add_transition(AnimTransition::new("jump_rise", "jump_fall", 0.15)
928            .with_condition(Condition::FloatLess("velocity_y".to_string(), 0.0)));
929        sm.add_transition(AnimTransition::new("jump_fall", "land", 0.05)
930            .with_condition(Condition::BoolTrue("grounded".to_string())));
931        sm.add_transition(AnimTransition::new("land", "locomotion_blend", 0.2)
932            .with_exit_time(0.7));
933
934        sm.entry_state = Some("locomotion_blend".to_string());
935        sm
936    }
937
938    /// Combat state machine: idle_combat, attack_light, attack_heavy, dodge, block, hurt, death.
939    pub fn combat_humanoid() -> AnimStateMachine {
940        let mut sm = AnimStateMachine::new("combat");
941
942        sm.add_state(AnimState::clip("idle_combat",    "combat_idle"));
943        sm.add_state(AnimState::clip("attack_light",   "combat_attack_light"));
944        sm.add_state(AnimState::clip("attack_heavy",   "combat_attack_heavy"));
945        sm.add_state(AnimState::clip("attack_combo2",  "combat_attack_combo2"));
946        sm.add_state(AnimState::clip("dodge",          "combat_dodge"));
947        sm.add_state(AnimState::clip("block",          "combat_block"));
948        sm.add_state(AnimState::clip("hurt",           "combat_hurt"));
949        sm.add_state(AnimState::clip("death",          "combat_death"));
950
951        // Light attack chain
952        sm.add_transition(AnimTransition::new("idle_combat", "attack_light", 0.1)
953            .with_condition(Condition::Trigger("attack_light".to_string())));
954        sm.add_transition(AnimTransition::new("attack_light", "attack_combo2", 0.1)
955            .with_condition(Condition::Trigger("attack_light".to_string()))
956            .with_exit_time(0.4));
957        sm.add_transition(AnimTransition::new("attack_light", "idle_combat", 0.2)
958            .with_exit_time(0.9));
959        sm.add_transition(AnimTransition::new("attack_combo2", "idle_combat", 0.2)
960            .with_exit_time(0.9));
961
962        // Heavy attack
963        sm.add_transition(AnimTransition::new("idle_combat", "attack_heavy", 0.1)
964            .with_condition(Condition::Trigger("attack_heavy".to_string())));
965        sm.add_transition(AnimTransition::new("attack_heavy", "idle_combat", 0.2)
966            .with_exit_time(0.9));
967
968        // Dodge
969        sm.add_transition(AnimTransition::new("idle_combat", "dodge", 0.05)
970            .with_condition(Condition::Trigger("dodge".to_string())));
971        sm.add_transition(AnimTransition::new("dodge", "idle_combat", 0.1)
972            .with_exit_time(0.85));
973
974        // Block (hold)
975        sm.add_transition(AnimTransition::new("idle_combat", "block", 0.1)
976            .with_condition(Condition::BoolTrue("blocking".to_string())));
977        sm.add_transition(AnimTransition::new("block", "idle_combat", 0.15)
978            .with_condition(Condition::BoolFalse("blocking".to_string())));
979
980        // Hurt (any state)
981        sm.add_any_transition(AnimTransition::new("", "hurt", 0.05)
982            .with_condition(Condition::Trigger("hurt".to_string())));
983        sm.add_transition(AnimTransition::new("hurt", "idle_combat", 0.15)
984            .with_exit_time(0.8));
985
986        // Death (any state, priority)
987        let mut death_t = AnimTransition::new("", "death", 0.05);
988        death_t.conditions.push(Condition::Trigger("death".to_string()));
989        death_t.priority = 100;
990        sm.add_any_transition(death_t);
991
992        sm.entry_state = Some("idle_combat".to_string());
993        sm
994    }
995
996    /// Flying creature: glide, flap, dive, land, hover.
997    pub fn flying_creature() -> AnimStateMachine {
998        let mut sm = AnimStateMachine::new("flying");
999
1000        sm.add_state(AnimState::clip("hover", "fly_hover"));
1001        sm.add_state(AnimState::clip("flap",  "fly_flap"));
1002        sm.add_state(AnimState::clip("glide", "fly_glide"));
1003        sm.add_state(AnimState::clip("dive",  "fly_dive"));
1004        sm.add_state(AnimState::clip("land",  "fly_land"));
1005
1006        sm.add_transition(AnimTransition::new("hover", "flap", 0.2)
1007            .with_condition(Condition::FloatGreater("speed".to_string(), 0.3)));
1008        sm.add_transition(AnimTransition::new("flap", "glide", 0.3)
1009            .with_condition(Condition::FloatGreater("speed".to_string(), 0.8)));
1010        sm.add_transition(AnimTransition::new("glide", "flap", 0.2)
1011            .with_condition(Condition::FloatLess("speed".to_string(), 0.6)));
1012        sm.add_transition(AnimTransition::new("glide", "dive", 0.15)
1013            .with_condition(Condition::FloatLess("velocity_y".to_string(), -0.5)));
1014        sm.add_transition(AnimTransition::new("dive", "glide", 0.3)
1015            .with_condition(Condition::FloatGreater("velocity_y".to_string(), 0.0)));
1016        sm.add_any_transition(AnimTransition::new("", "land", 0.2)
1017            .with_condition(Condition::Trigger("land".to_string())));
1018        sm.add_transition(AnimTransition::new("land", "hover", 0.3)
1019            .with_exit_time(0.9));
1020
1021        sm.entry_state = Some("hover".to_string());
1022        sm
1023    }
1024}
1025
1026// ── Utility ───────────────────────────────────────────────────────────────────
1027
1028fn smooth_step(t: f32) -> f32 {
1029    let t = t.clamp(0.0, 1.0);
1030    t * t * (3.0 - 2.0 * t)
1031}
1032
1033// ── Tests ─────────────────────────────────────────────────────────────────────
1034
1035#[cfg(test)]
1036mod tests {
1037    use super::*;
1038
1039    fn make_clip(name: &str, duration: f32) -> AnimClip {
1040        let mut clip = AnimClip::new(name, duration);
1041        clip.add_channel("pos_x", AnimCurve::linear(0.0, 0.0, duration, 1.0));
1042        clip
1043    }
1044
1045    #[test]
1046    fn test_anim_curve_sample() {
1047        let curve = AnimCurve::linear(0.0, 0.0, 1.0, 1.0);
1048        assert!((curve.sample(0.5) - 0.5).abs() < 0.01);
1049        assert!((curve.sample(0.0) - 0.0).abs() < 0.01);
1050        assert!((curve.sample(1.0) - 1.0).abs() < 0.01);
1051    }
1052
1053    #[test]
1054    fn test_anim_curve_clamp() {
1055        let curve = AnimCurve::linear(0.0, 5.0, 1.0, 10.0);
1056        assert!((curve.sample(-1.0) - 5.0).abs() < 0.01);
1057        assert!((curve.sample(2.0) - 10.0).abs() < 0.01);
1058    }
1059
1060    #[test]
1061    fn test_anim_clip_sample() {
1062        let clip = make_clip("test", 2.0);
1063        let pose = clip.sample(1.0);
1064        assert!(pose.contains_key("pos_x"));
1065        let v = pose["pos_x"];
1066        assert!(v > 0.4 && v < 0.6, "pos_x at t=1 of 2s clip should be ~0.5, got {}", v);
1067    }
1068
1069    #[test]
1070    fn test_blend_samples() {
1071        let mut a = HashMap::new(); a.insert("x".to_string(), 0.0_f32);
1072        let mut b = HashMap::new(); b.insert("x".to_string(), 1.0_f32);
1073        let blended = AnimClip::blend_samples(&a, &b, 0.5);
1074        assert!((blended["x"] - 0.5).abs() < 0.001);
1075    }
1076
1077    #[test]
1078    fn test_state_machine_transitions() {
1079        let mut sm = AnimStateMachine::new("test");
1080        sm.add_state(AnimState::clip("idle", "idle_clip"));
1081        sm.add_state(AnimState::clip("run",  "run_clip"));
1082        sm.add_transition(AnimTransition::new("idle", "run", 0.1)
1083            .with_condition(Condition::Trigger("run".to_string())));
1084
1085        let mut clips = HashMap::new();
1086        clips.insert("idle_clip".to_string(), make_clip("idle_clip", 1.0));
1087        clips.insert("run_clip".to_string(),  make_clip("run_clip",  1.0));
1088
1089        let mut params = AnimParamSet::default();
1090        sm.enter();
1091        sm.update(0.016, &mut params, &clips);
1092        assert_eq!(sm.current_state_name(), Some("idle"));
1093
1094        params.set_trigger("run");
1095        sm.update(0.016, &mut params, &clips);
1096        // Transition starts; after blend duration it completes
1097        sm.update(0.15,  &mut params, &clips);
1098        assert_eq!(sm.current_state_name(), Some("run"));
1099    }
1100
1101    #[test]
1102    fn test_blend_tree_linear() {
1103        let mut clips = HashMap::new();
1104        clips.insert("idle".to_string(), make_clip("idle", 1.0));
1105        clips.insert("walk".to_string(), make_clip("walk", 1.0));
1106        clips.insert("run".to_string(),  make_clip("run",  1.0));
1107
1108        let tree = BlendTree::Linear1D {
1109            param: "speed".to_string(),
1110            children: vec![
1111                (0.0, BlendTree::Clip { clip_name: "idle".to_string(), speed: 1.0 }),
1112                (1.0, BlendTree::Clip { clip_name: "run".to_string(),  speed: 1.0 }),
1113            ],
1114        };
1115
1116        let mut params = AnimParamSet::default();
1117        params.set_float("speed", 0.5);
1118        let pose = tree.evaluate(&clips, &params, 0.5);
1119        // Should blend 50% between idle and run at t=0.5s of a 1s clip
1120        let v = pose.get("pos_x").copied().unwrap_or(0.0);
1121        assert!(v > 0.0, "blend tree should produce non-zero values");
1122    }
1123
1124    #[test]
1125    fn test_animator_layers() {
1126        let mut animator = Animator::new();
1127        animator.add_clip(make_clip("idle_clip", 1.0));
1128
1129        let mut sm = AnimStateMachine::new("base");
1130        sm.add_state(AnimState::clip("idle", "idle_clip"));
1131
1132        animator.add_layer(AnimLayer::new("base", sm));
1133        let pose = animator.update(0.016);
1134        assert!(!pose.is_empty() || pose.is_empty(), "should not panic");
1135    }
1136
1137    #[test]
1138    fn test_anim_presets_locomotion() {
1139        let sm = AnimPresets::humanoid_locomotion();
1140        assert!(sm.states.contains_key("locomotion_blend"));
1141        assert!(sm.states.contains_key("jump_rise"));
1142        assert!(sm.transitions.len() >= 4);
1143    }
1144
1145    #[test]
1146    fn test_anim_presets_combat() {
1147        let sm = AnimPresets::combat_humanoid();
1148        assert!(sm.states.contains_key("attack_light"));
1149        assert!(sm.states.contains_key("death"));
1150        assert!(!sm.any_state_transitions.is_empty());
1151    }
1152
1153    #[test]
1154    fn test_smooth_step() {
1155        assert!((smooth_step(0.0) - 0.0).abs() < 1e-6);
1156        assert!((smooth_step(1.0) - 1.0).abs() < 1e-6);
1157        assert!((smooth_step(0.5) - 0.5).abs() < 1e-6);
1158    }
1159}