Skip to main content

mittens_engine/engine/ecs/component/
animation.rs

1use super::{Component, ComponentRef};
2use crate::engine::ecs::ComponentId;
3
4#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum AnimationState {
6    Playing,
7    Looping,
8    Paused,
9}
10
11/// When the AnimationSystem resolves ActionComponent target sources
12/// (selectors / guids) into concrete ComponentIds.
13///
14/// - `OnAttach` (default): resolve once when the animation is first seen
15///   by the system. All targets must exist by then. Cheapest at play
16///   time — runtime tick uses the cached ids directly.
17/// - `OnPlay`: defer resolution until each Action actually fires. Lets
18///   actions reference components that don't exist until the animation
19///   is mid-play (procedurally spawned, lazily attached). Pays one
20///   resolution per Action on first fire.
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ResolveTargetsMode {
23    OnAttach,
24    OnPlay,
25}
26
27impl Default for ResolveTargetsMode {
28    fn default() -> Self {
29        Self::OnAttach
30    }
31}
32
33#[derive(Debug, Clone)]
34pub struct AnimationComponent {
35    pub state: AnimationState,
36    pub resolve_targets: ResolveTargetsMode,
37    /// Explicit loop length in beats. `None` falls back to the derived
38    /// default in `AnimationSystem` (`floor(max_keyframe_beat) + 1`).
39    /// Authored via `Animation.length(beats)`.
40    pub length_beats: Option<f64>,
41    pub scope_source: Option<ComponentRef>,
42    pub resolved_scope: Option<ComponentId>,
43
44    component: Option<ComponentId>,
45}
46
47impl AnimationComponent {
48    pub fn new() -> Self {
49        Self {
50            state: AnimationState::Looping,
51            resolve_targets: ResolveTargetsMode::default(),
52            length_beats: None,
53            scope_source: None,
54            resolved_scope: None,
55            component: None,
56        }
57    }
58
59    pub fn with_state(mut self, state: AnimationState) -> Self {
60        self.state = state;
61        self
62    }
63
64    pub fn with_resolve_targets(mut self, mode: ResolveTargetsMode) -> Self {
65        self.resolve_targets = mode;
66        self
67    }
68
69    pub fn with_length_beats(mut self, beats: f64) -> Self {
70        self.length_beats = Some(beats);
71        self
72    }
73
74    pub fn with_scope_source(mut self, source: ComponentRef) -> Self {
75        self.scope_source = Some(source);
76        self.resolved_scope = None;
77        self
78    }
79
80    /// Backward-compatible helper for older callers.
81    pub fn with_playing(self, playing: bool) -> Self {
82        self.with_state(if playing {
83            AnimationState::Looping
84        } else {
85            AnimationState::Paused
86        })
87    }
88
89    pub fn id(&self) -> Option<ComponentId> {
90        self.component
91    }
92}
93
94impl Default for AnimationComponent {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100impl Component for AnimationComponent {
101    fn set_id(&mut self, component: ComponentId) {
102        self.component = Some(component);
103    }
104
105    fn name(&self) -> &'static str {
106        "animation"
107    }
108
109    fn init(&mut self, emit: &mut dyn crate::engine::ecs::SignalEmitter, component: ComponentId) {
110        emit.push_intent_now(
111            component,
112            crate::engine::ecs::IntentValue::RegisterAnimation {
113                component_ids: vec![component],
114            },
115        );
116    }
117
118    fn as_any(&self) -> &dyn std::any::Any {
119        self
120    }
121
122    fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
123        self
124    }
125
126    fn to_mms_ast(
127        &self,
128        _world: &crate::engine::ecs::World,
129    ) -> crate::scripting::ast::ComponentExpression {
130        use crate::engine::ecs::component::ce_helpers::*;
131        use crate::scripting::ast::Expression;
132
133        fn target_expr(t: &ComponentRef) -> Expression {
134            match t {
135                ComponentRef::Guid(u) => Expression::String(format!("@uuid:{u}")),
136                ComponentRef::Query(s) => Expression::String(s.clone()),
137            }
138        }
139
140        let ctor = match self.state {
141            AnimationState::Playing => "playing",
142            AnimationState::Looping => "looping",
143            AnimationState::Paused => "paused",
144        };
145        let mut ce = ce_call("Animation", ctor, vec![]);
146        // Only emit non-default resolve_targets — OnAttach is the default
147        // and would just add noise to dumps of typical animations.
148        if self.resolve_targets != ResolveTargetsMode::default() {
149            let mode = match self.resolve_targets {
150                ResolveTargetsMode::OnAttach => "on_attach",
151                ResolveTargetsMode::OnPlay => "on_play",
152            };
153            ce = ce.with_call("resolve_targets", vec![s(mode)]);
154        }
155        if let Some(n) = self.length_beats {
156            ce = ce.with_call("length", vec![num(n)]);
157        }
158        if let Some(source) = &self.scope_source {
159            ce = ce.with_call("scope", vec![target_expr(source)]);
160        }
161        ce
162    }
163}