Skip to main content

proof_engine/animation/
mod.rs

1//! Animation State Machine and Blend Trees.
2//!
3//! Provides a complete animation runtime for Proof Engine entities:
4//!
5//! - `AnimationClip`     — named sequence of keyframe channels over time
6//! - `AnimationCurve`    — per-property float curve (driven by MathFunction or raw keyframes)
7//! - `BlendTree`         — 1D/2D weighted blend of multiple clips
8//! - `AnimationLayer`    — masked layer (e.g., upper-body, full-body)
9//! - `AnimationState`    — named state that plays a clip or blend tree
10//! - `Transition`        — condition-triggered crossfade between states
11//! - `AnimatorController`— top-level controller driving all layers
12//!
13//! ## Quick Start
14//! ```rust,no_run
15//! use proof_engine::animation::*;
16//! let mut ctrl = AnimatorController::new();
17//! ctrl.add_state("idle",   AnimationState::clip("idle_clip",   1.0, true));
18//! ctrl.add_state("run",    AnimationState::clip("run_clip",    0.8, true));
19//! ctrl.add_state("attack", AnimationState::clip("attack_clip", 0.4, false));
20//! ctrl.add_transition("idle",   "run",    Condition::float_gt("speed", 0.1));
21//! ctrl.add_transition("run",    "idle",   Condition::float_lt("speed", 0.05));
22//! ctrl.add_transition("idle",   "attack", Condition::trigger("attack"));
23//! ctrl.start("idle");
24//! ```
25
26pub mod ik;
27pub mod sprite_anim;
28
29use std::collections::HashMap;
30use crate::math::MathFunction;
31
32// ── AnimationCurve ─────────────────────────────────────────────────────────────
33
34/// A single float channel over normalized time [0, 1].
35#[derive(Debug, Clone)]
36pub enum AnimationCurve {
37    /// Constant value.
38    Constant(f32),
39    /// Linear keyframes: list of (time, value) pairs sorted by time.
40    Keyframes(Vec<(f32, f32)>),
41    /// Driven entirely by a MathFunction evaluated at time t.
42    MathDriven(MathFunction),
43    /// Cubic bezier keyframes: (time, value, in_tangent, out_tangent).
44    BezierKeyframes(Vec<BezierKey>),
45}
46
47#[derive(Debug, Clone)]
48pub struct BezierKey {
49    pub time:        f32,
50    pub value:       f32,
51    pub in_tangent:  f32,
52    pub out_tangent: f32,
53}
54
55impl AnimationCurve {
56    /// Evaluate the curve at normalized time `t` in [0, 1].
57    pub fn evaluate(&self, t: f32) -> f32 {
58        let t = t.clamp(0.0, 1.0);
59        match self {
60            AnimationCurve::Constant(v) => *v,
61            AnimationCurve::MathDriven(f) => f.evaluate(t, t),
62            AnimationCurve::Keyframes(keys) => {
63                if keys.is_empty() { return 0.0; }
64                if keys.len() == 1 { return keys[0].1; }
65                // Find surrounding keys
66                let idx = keys.partition_point(|(kt, _)| *kt <= t);
67                if idx == 0 { return keys[0].1; }
68                if idx >= keys.len() { return keys[keys.len()-1].1; }
69                let (t0, v0) = keys[idx-1];
70                let (t1, v1) = keys[idx];
71                let span = (t1 - t0).max(1e-7);
72                let alpha = (t - t0) / span;
73                v0 + (v1 - v0) * alpha
74            }
75            AnimationCurve::BezierKeyframes(keys) => {
76                if keys.is_empty() { return 0.0; }
77                if keys.len() == 1 { return keys[0].value; }
78                let idx = keys.partition_point(|k| k.time <= t);
79                if idx == 0 { return keys[0].value; }
80                if idx >= keys.len() { return keys[keys.len()-1].value; }
81                let k0 = &keys[idx-1];
82                let k1 = &keys[idx];
83                let span = (k1.time - k0.time).max(1e-7);
84                let u = (t - k0.time) / span;
85                // Cubic Hermite
86                let h00 = 2.0*u*u*u - 3.0*u*u + 1.0;
87                let h10 = u*u*u - 2.0*u*u + u;
88                let h01 = -2.0*u*u*u + 3.0*u*u;
89                let h11 = u*u*u - u*u;
90                h00*k0.value + h10*span*k0.out_tangent + h01*k1.value + h11*span*k1.in_tangent
91            }
92        }
93    }
94
95    /// Build a constant curve.
96    pub fn constant(v: f32) -> Self { Self::Constant(v) }
97
98    /// Build a linear ramp from `a` at t=0 to `b` at t=1.
99    pub fn linear(a: f32, b: f32) -> Self {
100        Self::Keyframes(vec![(0.0, a), (1.0, b)])
101    }
102
103    /// Build a curve from raw (time, value) pairs.
104    pub fn from_keys(mut keys: Vec<(f32, f32)>) -> Self {
105        keys.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
106        Self::Keyframes(keys)
107    }
108}
109
110// ── AnimationClip ──────────────────────────────────────────────────────────────
111
112/// Named set of channels that animate properties over time.
113#[derive(Debug, Clone)]
114pub struct AnimationClip {
115    pub name:     String,
116    pub duration: f32,
117    pub looping:  bool,
118    /// Property path -> curve. E.g. "position.x", "scale.x", "color.r".
119    pub channels: HashMap<String, AnimationCurve>,
120    /// Events fired at specific normalized times.
121    pub events:   Vec<AnimationEvent>,
122}
123
124#[derive(Debug, Clone)]
125pub struct AnimationEvent {
126    /// Normalized time [0, 1] when this fires.
127    pub time:    f32,
128    /// Arbitrary tag passed to event handlers.
129    pub tag:     String,
130    pub payload: f32,
131}
132
133impl AnimationClip {
134    pub fn new(name: impl Into<String>, duration: f32, looping: bool) -> Self {
135        Self {
136            name: name.into(),
137            duration,
138            looping,
139            channels: HashMap::new(),
140            events: Vec::new(),
141        }
142    }
143
144    /// Add a channel curve.
145    pub fn with_channel(mut self, path: impl Into<String>, curve: AnimationCurve) -> Self {
146        self.channels.insert(path.into(), curve);
147        self
148    }
149
150    /// Add an event that fires at normalized time `t`.
151    pub fn with_event(mut self, t: f32, tag: impl Into<String>, payload: f32) -> Self {
152        self.events.push(AnimationEvent { time: t, tag: tag.into(), payload });
153        self
154    }
155
156    /// Sample all channels at normalized time `t`, returning (path, value) pairs.
157    pub fn sample(&self, t: f32) -> Vec<(String, f32)> {
158        self.channels.iter()
159            .map(|(path, curve)| (path.clone(), curve.evaluate(t)))
160            .collect()
161    }
162}
163
164// ── BlendTree ─────────────────────────────────────────────────────────────────
165
166/// Blend mode for a BlendTree.
167#[derive(Debug, Clone)]
168pub enum BlendTreeKind {
169    /// Blend between clips based on a single float parameter.
170    Linear1D { param: String, thresholds: Vec<f32> },
171    /// Blend between clips in a 2D parameter space.
172    Cartesian2D { param_x: String, param_y: String, positions: Vec<(f32, f32)> },
173    /// Additive blend — base clip plus additive clips.
174    Additive { base_index: usize },
175    /// Override blend — highest-weight non-zero clip wins.
176    Override,
177}
178
179#[derive(Debug, Clone)]
180pub struct BlendTree {
181    pub kind:   BlendTreeKind,
182    pub clips:  Vec<AnimationClip>,
183    pub weights: Vec<f32>,
184}
185
186impl BlendTree {
187    /// Compute blend weights given current parameter values.
188    pub fn compute_weights(&mut self, params: &HashMap<String, ParamValue>) {
189        let n = self.clips.len();
190        if n == 0 { return; }
191        self.weights.resize(n, 0.0);
192
193        match &self.kind {
194            BlendTreeKind::Linear1D { param, thresholds } => {
195                let v = params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0);
196                let thresholds = thresholds.clone();
197                if thresholds.len() != n { return; }
198                // Find surrounding pair
199                let idx = thresholds.partition_point(|&t| t <= v);
200                for w in &mut self.weights { *w = 0.0; }
201                if idx == 0 {
202                    self.weights[0] = 1.0;
203                } else if idx >= n {
204                    self.weights[n-1] = 1.0;
205                } else {
206                    let t0 = thresholds[idx-1];
207                    let t1 = thresholds[idx];
208                    let span = (t1 - t0).max(1e-7);
209                    let alpha = (v - t0) / span;
210                    self.weights[idx-1] = 1.0 - alpha;
211                    self.weights[idx]   = alpha;
212                }
213            }
214            BlendTreeKind::Cartesian2D { param_x, param_y, positions } => {
215                let px = params.get(param_x).and_then(|p| p.as_float()).unwrap_or(0.0);
216                let py = params.get(param_y).and_then(|p| p.as_float()).unwrap_or(0.0);
217                let positions = positions.clone();
218                // Inverse Distance Weighting
219                let dists: Vec<f32> = positions.iter()
220                    .map(|(x, y)| ((px - x).powi(2) + (py - y).powi(2)).sqrt().max(1e-6))
221                    .collect();
222                let sum: f32 = dists.iter().map(|d| 1.0 / d).sum();
223                for (i, d) in dists.iter().enumerate() {
224                    self.weights[i] = (1.0 / d) / sum.max(1e-7);
225                }
226            }
227            BlendTreeKind::Additive { .. } | BlendTreeKind::Override => {
228                // Weights set externally
229            }
230        }
231    }
232
233    /// Sample blended output at normalized time `t`.
234    pub fn sample(&self, t: f32) -> Vec<(String, f32)> {
235        if self.clips.is_empty() { return Vec::new(); }
236        let mut accum: HashMap<String, f32> = HashMap::new();
237        let mut total_weight = 0.0_f32;
238
239        for (clip, &w) in self.clips.iter().zip(self.weights.iter()) {
240            if w < 1e-6 { continue; }
241            total_weight += w;
242            for (path, val) in clip.sample(t) {
243                *accum.entry(path).or_insert(0.0) += val * w;
244            }
245        }
246
247        if total_weight > 1e-6 {
248            for v in accum.values_mut() { *v /= total_weight; }
249        }
250        accum.into_iter().collect()
251    }
252}
253
254// ── Condition ─────────────────────────────────────────────────────────────────
255
256/// Condition that must be satisfied for a state transition to fire.
257#[derive(Debug, Clone)]
258pub enum Condition {
259    FloatGt { param: String, threshold: f32 },
260    FloatLt { param: String, threshold: f32 },
261    FloatGe { param: String, threshold: f32 },
262    FloatLe { param: String, threshold: f32 },
263    FloatEq { param: String, value: f32, tolerance: f32 },
264    BoolTrue  { param: String },
265    BoolFalse { param: String },
266    /// One-shot: fires once then resets to false.
267    Trigger   { param: String },
268    /// Always true — transition fires immediately when source state exits.
269    Always,
270    /// Multiple conditions all true.
271    All(Vec<Condition>),
272    /// At least one condition true.
273    Any(Vec<Condition>),
274}
275
276impl Condition {
277    pub fn float_gt(param: impl Into<String>, v: f32) -> Self {
278        Self::FloatGt { param: param.into(), threshold: v }
279    }
280    pub fn float_lt(param: impl Into<String>, v: f32) -> Self {
281        Self::FloatLt { param: param.into(), threshold: v }
282    }
283    pub fn float_ge(param: impl Into<String>, v: f32) -> Self {
284        Self::FloatGe { param: param.into(), threshold: v }
285    }
286    pub fn float_le(param: impl Into<String>, v: f32) -> Self {
287        Self::FloatLe { param: param.into(), threshold: v }
288    }
289    pub fn bool_true(param: impl Into<String>) -> Self {
290        Self::BoolTrue { param: param.into() }
291    }
292    pub fn trigger(param: impl Into<String>) -> Self {
293        Self::Trigger { param: param.into() }
294    }
295
296    /// Evaluate against current params. Returns (satisfied, consumed_triggers).
297    pub fn evaluate(&self, params: &HashMap<String, ParamValue>) -> (bool, Vec<String>) {
298        match self {
299            Self::FloatGt { param, threshold } =>
300                (params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0) > *threshold, vec![]),
301            Self::FloatLt { param, threshold } =>
302                (params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0) < *threshold, vec![]),
303            Self::FloatGe { param, threshold } =>
304                (params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0) >= *threshold, vec![]),
305            Self::FloatLe { param, threshold } =>
306                (params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0) <= *threshold, vec![]),
307            Self::FloatEq { param, value, tolerance } => {
308                let v = params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0);
309                ((v - value).abs() <= *tolerance, vec![])
310            }
311            Self::BoolTrue  { param } =>
312                (params.get(param).and_then(|p| p.as_bool()).unwrap_or(false), vec![]),
313            Self::BoolFalse { param } =>
314                (!params.get(param).and_then(|p| p.as_bool()).unwrap_or(false), vec![]),
315            Self::Trigger { param } => {
316                let v = params.get(param).and_then(|p| p.as_bool()).unwrap_or(false);
317                if v { (true, vec![param.clone()]) } else { (false, vec![]) }
318            }
319            Self::Always => (true, vec![]),
320            Self::All(conds) => {
321                let mut consumed = Vec::new();
322                for c in conds {
323                    let (ok, mut trig) = c.evaluate(params);
324                    if !ok { return (false, vec![]); }
325                    consumed.append(&mut trig);
326                }
327                (true, consumed)
328            }
329            Self::Any(conds) => {
330                for c in conds {
331                    let (ok, trig) = c.evaluate(params);
332                    if ok { return (true, trig); }
333                }
334                (false, vec![])
335            }
336        }
337    }
338}
339
340// ── ParamValue ────────────────────────────────────────────────────────────────
341
342#[derive(Debug, Clone)]
343pub enum ParamValue {
344    Float(f32),
345    Bool(bool),
346    Int(i32),
347}
348
349impl ParamValue {
350    pub fn as_float(&self) -> Option<f32> {
351        match self {
352            Self::Float(v) => Some(*v),
353            Self::Int(v) => Some(*v as f32),
354            _ => None,
355        }
356    }
357    pub fn as_bool(&self) -> Option<bool> {
358        if let Self::Bool(v) = self { Some(*v) } else { None }
359    }
360    pub fn as_int(&self) -> Option<i32> {
361        if let Self::Int(v) = self { Some(*v) } else { None }
362    }
363}
364
365// ── Transition ────────────────────────────────────────────────────────────────
366
367#[derive(Debug, Clone)]
368pub struct Transition {
369    pub from:            String,
370    pub to:              String,
371    pub condition:       Condition,
372    /// Crossfade duration in seconds.
373    pub duration:        f32,
374    /// If true, can interrupt an existing transition.
375    pub can_interrupt:   bool,
376    /// Minimum time in source state before transition is eligible (seconds).
377    pub exit_time:       Option<f32>,
378    /// Normalized exit time: transition fires when state reaches this fraction.
379    pub normalized_exit: Option<f32>,
380}
381
382impl Transition {
383    pub fn new(from: impl Into<String>, to: impl Into<String>, cond: Condition) -> Self {
384        Self {
385            from: from.into(),
386            to: to.into(),
387            condition: cond,
388            duration: 0.15,
389            can_interrupt: false,
390            exit_time: None,
391            normalized_exit: None,
392        }
393    }
394
395    pub fn with_duration(mut self, d: f32) -> Self { self.duration = d; self }
396    pub fn interruptible(mut self) -> Self { self.can_interrupt = true; self }
397    pub fn exit_at(mut self, t: f32) -> Self { self.exit_time = Some(t); self }
398    pub fn exit_normalized(mut self, t: f32) -> Self { self.normalized_exit = Some(t); self }
399}
400
401// ── AnimationState ────────────────────────────────────────────────────────────
402
403#[derive(Debug, Clone)]
404pub enum StateContent {
405    Clip(AnimationClip),
406    Tree(BlendTree),
407}
408
409#[derive(Debug, Clone)]
410pub struct AnimationState {
411    pub name:       String,
412    pub content:    StateContent,
413    pub speed:      f32,
414    pub mirror:     bool,
415    pub cyclic_offset: f32,
416    /// MathFunction modulating playback speed over normalized time.
417    pub speed_curve: Option<MathFunction>,
418}
419
420impl AnimationState {
421    pub fn clip(clip: AnimationClip) -> Self {
422        let name = clip.name.clone();
423        Self {
424            name,
425            content: StateContent::Clip(clip),
426            speed: 1.0,
427            mirror: false,
428            cyclic_offset: 0.0,
429            speed_curve: None,
430        }
431    }
432
433    pub fn tree(name: impl Into<String>, tree: BlendTree) -> Self {
434        Self {
435            name: name.into(),
436            content: StateContent::Tree(tree),
437            speed: 1.0,
438            mirror: false,
439            cyclic_offset: 0.0,
440            speed_curve: None,
441        }
442    }
443
444    pub fn with_speed(mut self, s: f32) -> Self { self.speed = s; self }
445    pub fn mirrored(mut self) -> Self { self.mirror = true; self }
446
447    pub fn duration(&self) -> f32 {
448        match &self.content {
449            StateContent::Clip(c) => c.duration,
450            StateContent::Tree(t) => t.clips.iter().map(|c| c.duration).fold(0.0, f32::max),
451        }
452    }
453}
454
455// ── AnimationLayer ────────────────────────────────────────────────────────────
456
457/// A layer runs its own state machine and blends on top of lower layers.
458#[derive(Debug, Clone)]
459pub struct AnimationLayer {
460    pub name:    String,
461    pub weight:  f32,
462    /// Property paths this layer affects. Empty = all properties.
463    pub mask:    Vec<String>,
464    pub additive: bool,
465    // Runtime state
466    pub current_state: Option<String>,
467    pub current_time:  f32,
468    pub transition:    Option<ActiveTransition>,
469}
470
471#[derive(Debug, Clone)]
472pub struct ActiveTransition {
473    pub target_state: String,
474    pub progress:     f32,   // 0..1
475    pub duration:     f32,
476    pub prev_time:    f32,
477    pub prev_state:   String,
478}
479
480impl AnimationLayer {
481    pub fn new(name: impl Into<String>) -> Self {
482        Self {
483            name: name.into(),
484            weight: 1.0,
485            mask: Vec::new(),
486            additive: false,
487            current_state: None,
488            current_time: 0.0,
489            transition: None,
490        }
491    }
492
493    pub fn with_weight(mut self, w: f32) -> Self { self.weight = w; self }
494    pub fn with_mask(mut self, mask: Vec<String>) -> Self { self.mask = mask; self }
495    pub fn as_additive(mut self) -> Self { self.additive = true; self }
496}
497
498// ── AnimatorController ────────────────────────────────────────────────────────
499
500/// Drives one or more AnimationLayers with shared parameter space.
501pub struct AnimatorController {
502    pub states:      HashMap<String, AnimationState>,
503    pub transitions: Vec<Transition>,
504    pub params:      HashMap<String, ParamValue>,
505    pub layers:      Vec<AnimationLayer>,
506    /// Fired events since last call to drain_events().
507    events:          Vec<FiredEvent>,
508    /// Clip library for fetching clips by name.
509    pub clips:       HashMap<String, AnimationClip>,
510}
511
512#[derive(Debug, Clone)]
513pub struct FiredEvent {
514    pub layer:   String,
515    pub state:   String,
516    pub tag:     String,
517    pub payload: f32,
518}
519
520/// Sampled property output from a full controller tick.
521#[derive(Debug, Default, Clone)]
522pub struct AnimationOutput {
523    pub channels: HashMap<String, f32>,
524}
525
526impl AnimatorController {
527    pub fn new() -> Self {
528        let mut layers = vec![AnimationLayer::new("Base Layer")];
529        layers[0].weight = 1.0;
530        Self {
531            states: HashMap::new(),
532            transitions: Vec::new(),
533            params: HashMap::new(),
534            layers,
535            events: Vec::new(),
536            clips: HashMap::new(),
537        }
538    }
539
540    // ── Builder ──────────────────────────────────────────────────────────────
541
542    pub fn add_state(&mut self, state: AnimationState) {
543        self.states.insert(state.name.clone(), state);
544    }
545
546    pub fn add_clip(&mut self, clip: AnimationClip) {
547        self.clips.insert(clip.name.clone(), clip);
548    }
549
550    pub fn add_transition(&mut self, t: Transition) {
551        self.transitions.push(t);
552    }
553
554    pub fn add_layer(&mut self, layer: AnimationLayer) {
555        self.layers.push(layer);
556    }
557
558    // ── Parameters ───────────────────────────────────────────────────────────
559
560    pub fn set_float(&mut self, name: &str, v: f32) {
561        self.params.insert(name.to_owned(), ParamValue::Float(v));
562    }
563
564    pub fn set_bool(&mut self, name: &str, v: bool) {
565        self.params.insert(name.to_owned(), ParamValue::Bool(v));
566    }
567
568    pub fn set_int(&mut self, name: &str, v: i32) {
569        self.params.insert(name.to_owned(), ParamValue::Int(v));
570    }
571
572    /// Set a trigger (one-shot bool that resets after being consumed).
573    pub fn set_trigger(&mut self, name: &str) {
574        self.params.insert(name.to_owned(), ParamValue::Bool(true));
575    }
576
577    pub fn get_float(&self, name: &str) -> f32 {
578        self.params.get(name).and_then(|p| p.as_float()).unwrap_or(0.0)
579    }
580
581    pub fn get_bool(&self, name: &str) -> bool {
582        self.params.get(name).and_then(|p| p.as_bool()).unwrap_or(false)
583    }
584
585    // ── Entry point ──────────────────────────────────────────────────────────
586
587    pub fn start(&mut self, state_name: &str) {
588        for layer in &mut self.layers {
589            layer.current_state = Some(state_name.to_owned());
590            layer.current_time  = 0.0;
591            layer.transition    = None;
592        }
593    }
594
595    pub fn start_layer(&mut self, layer_name: &str, state_name: &str) {
596        if let Some(layer) = self.layers.iter_mut().find(|l| l.name == layer_name) {
597            layer.current_state = Some(state_name.to_owned());
598            layer.current_time  = 0.0;
599            layer.transition    = None;
600        }
601    }
602
603    // ── Tick ─────────────────────────────────────────────────────────────────
604
605    /// Advance all layers by `dt` seconds and evaluate transitions.
606    /// Returns blended output across all layers.
607    pub fn tick(&mut self, dt: f32) -> AnimationOutput {
608        let mut output = AnimationOutput::default();
609
610        // Collect triggered params to reset
611        let mut consumed_triggers: Vec<String> = Vec::new();
612
613        for layer in &mut self.layers {
614            if layer.current_state.is_none() { continue; }
615            let cur_name = layer.current_state.clone().unwrap();
616
617            // Advance transition if active
618            if let Some(ref mut tr) = layer.transition {
619                tr.progress += dt / tr.duration.max(1e-4);
620                tr.prev_time += dt;
621                if tr.progress >= 1.0 {
622                    // Transition complete
623                    let new_state = tr.target_state.clone();
624                    layer.current_time = 0.0;
625                    layer.current_state = Some(new_state);
626                    layer.transition = None;
627                }
628            } else {
629                // Check transitions from current state
630                let applicable: Vec<Transition> = self.transitions.iter()
631                    .filter(|t| t.from == cur_name || t.from == "*")
632                    .cloned()
633                    .collect();
634
635                let state_dur = self.states.get(&cur_name).map(|s| s.duration()).unwrap_or(1.0);
636
637                for trans in applicable {
638                    // Check exit time constraints
639                    if let Some(min_exit) = trans.exit_time {
640                        if layer.current_time < min_exit { continue; }
641                    }
642                    if let Some(norm_exit) = trans.normalized_exit {
643                        let norm = if state_dur > 1e-6 { layer.current_time / state_dur } else { 1.0 };
644                        if norm < norm_exit { continue; }
645                    }
646
647                    let (ok, mut trig) = trans.condition.evaluate(&self.params);
648                    if ok {
649                        consumed_triggers.append(&mut trig);
650                        let prev = cur_name.clone();
651                        layer.transition = Some(ActiveTransition {
652                            target_state: trans.to.clone(),
653                            progress: 0.0,
654                            duration: trans.duration,
655                            prev_time: layer.current_time,
656                            prev_state: prev,
657                        });
658                        break;
659                    }
660                }
661
662                // Advance current state time
663                if let Some(state) = self.states.get(&cur_name) {
664                    let speed_mod = if let Some(ref sf) = state.speed_curve {
665                        let norm = if state.duration() > 1e-6 { layer.current_time / state.duration() } else { 0.0 };
666                        sf.evaluate(norm, norm)
667                    } else {
668                        1.0
669                    };
670                    layer.current_time += dt * state.speed * speed_mod;
671                    if state.duration() > 1e-6 {
672                        if let StateContent::Clip(ref clip) = state.content {
673                            if clip.looping {
674                                layer.current_time %= clip.duration.max(1e-4);
675                            } else {
676                                layer.current_time = layer.current_time.min(clip.duration);
677                            }
678                        }
679                    }
680                }
681            }
682
683            // Sample current state
684            let sample_t = {
685                let dur = self.states.get(layer.current_state.as_deref().unwrap_or(""))
686                    .map(|s| s.duration()).unwrap_or(1.0).max(1e-4);
687                (layer.current_time / dur).clamp(0.0, 1.0)
688            };
689
690            if let Some(state) = self.states.get(layer.current_state.as_deref().unwrap_or("")) {
691                let samples = match &state.content {
692                    StateContent::Clip(c) => c.sample(sample_t),
693                    StateContent::Tree(t) => t.sample(sample_t),
694                };
695                for (path, val) in samples {
696                    let entry = output.channels.entry(path).or_insert(0.0);
697                    if layer.additive {
698                        *entry += val * layer.weight;
699                    } else {
700                        *entry = *entry * (1.0 - layer.weight) + val * layer.weight;
701                    }
702                }
703            }
704        }
705
706        // Reset consumed triggers
707        for key in consumed_triggers {
708            self.params.insert(key, ParamValue::Bool(false));
709        }
710
711        output
712    }
713
714    /// Drain all fired animation events since last call.
715    pub fn drain_events(&mut self) -> Vec<FiredEvent> {
716        std::mem::take(&mut self.events)
717    }
718
719    /// Force the base layer into a specific state immediately.
720    pub fn play(&mut self, state_name: &str) {
721        if let Some(layer) = self.layers.first_mut() {
722            layer.current_state = Some(state_name.to_owned());
723            layer.current_time  = 0.0;
724            layer.transition    = None;
725        }
726    }
727
728    /// Cross-fade to a state over `duration` seconds.
729    pub fn cross_fade(&mut self, state_name: &str, duration: f32) {
730        if let Some(layer) = self.layers.first_mut() {
731            let prev = layer.current_state.clone().unwrap_or_default();
732            layer.transition = Some(ActiveTransition {
733                target_state: state_name.to_owned(),
734                progress: 0.0,
735                duration,
736                prev_time: layer.current_time,
737                prev_state: prev,
738            });
739        }
740    }
741
742    /// Current normalized time of the base layer's active state.
743    pub fn normalized_time(&self) -> f32 {
744        if let Some(layer) = self.layers.first() {
745            if let Some(name) = layer.current_state.as_deref() {
746                if let Some(state) = self.states.get(name) {
747                    let dur = state.duration().max(1e-4);
748                    return (layer.current_time / dur).clamp(0.0, 1.0);
749                }
750            }
751        }
752        0.0
753    }
754
755    /// Name of the currently active state on the base layer.
756    pub fn current_state(&self) -> Option<&str> {
757        self.layers.first()?.current_state.as_deref()
758    }
759
760    /// Whether the base layer is transitioning.
761    pub fn is_transitioning(&self) -> bool {
762        self.layers.first().map(|l| l.transition.is_some()).unwrap_or(false)
763    }
764}
765
766impl Default for AnimatorController {
767    fn default() -> Self { Self::new() }
768}
769
770// ── RootMotion ────────────────────────────────────────────────────────────────
771
772/// Root motion extracted from animation, applied to entity transform.
773#[derive(Debug, Clone, Default)]
774pub struct RootMotion {
775    pub delta_position: glam::Vec3,
776    pub delta_rotation: f32,
777    pub delta_scale:    glam::Vec3,
778}
779
780impl RootMotion {
781    pub fn from_output(output: &AnimationOutput) -> Self {
782        let get = |key: &str| output.channels.get(key).copied().unwrap_or(0.0);
783        Self {
784            delta_position: glam::Vec3::new(get("root.dx"), get("root.dy"), get("root.dz")),
785            delta_rotation: get("root.dr"),
786            delta_scale:    glam::Vec3::ONE,
787        }
788    }
789
790    pub fn is_zero(&self) -> bool {
791        self.delta_position.length_squared() < 1e-10 && self.delta_rotation.abs() < 1e-6
792    }
793}
794
795// ── AnimationMirror ───────────────────────────────────────────────────────────
796
797/// Mirrors an AnimationOutput left/right (negate X-axis channels).
798pub fn mirror_output(output: &mut AnimationOutput) {
799    for (key, val) in &mut output.channels {
800        if key.ends_with(".x") || key.ends_with("_x") || key.contains("left") {
801            *val = -*val;
802        }
803    }
804}
805
806// ── AnimationBlend helpers ────────────────────────────────────────────────────
807
808/// Linearly blend two AnimationOutputs by alpha (0 = a, 1 = b).
809pub fn blend_outputs(a: &AnimationOutput, b: &AnimationOutput, alpha: f32) -> AnimationOutput {
810    let mut out = a.clone();
811    for (key, bv) in &b.channels {
812        let av = a.channels.get(key).copied().unwrap_or(0.0);
813        out.channels.insert(key.clone(), av + (bv - av) * alpha);
814    }
815    out
816}
817
818/// Additively layer `additive` on top of `base` with `weight`.
819pub fn add_output(base: &AnimationOutput, additive: &AnimationOutput, weight: f32) -> AnimationOutput {
820    let mut out = base.clone();
821    for (key, av) in &additive.channels {
822        *out.channels.entry(key.clone()).or_insert(0.0) += av * weight;
823    }
824    out
825}