Skip to main content

game_toolkit_platformer/anim/
spec.rs

1//! [`GraphSpec`]: a state graph by names, compiled into a validated [`Graph`].
2//!
3//! Everything refers to parameters and states by name, so a spec can be written in code
4//! with the builder methods or produced from data. [`GraphSpec::compile`] resolves the
5//! names, checks that every condition uses a parameter of the right kind, and orders the
6//! transitions by priority.
7
8use std::fmt;
9
10use super::cond::Use;
11use super::{
12    Clip, Cond, Graph, Name, ParamId, ParamKind, Params, Rate, Source, State, StateId, Transition,
13};
14
15/// A state by names.
16#[derive(Clone, Debug, PartialEq)]
17pub struct StateSpec {
18    /// Its name, unique in the graph.
19    pub name: Name,
20    /// What it plays.
21    pub clip: Clip,
22    /// How fast, by parameter name.
23    pub rate: Rate<Name>,
24    /// Seconds only lock-breaking transitions may leave it.
25    pub lock: f32,
26    /// Where to go when the clip finishes.
27    pub next: Option<Name>,
28}
29
30impl StateSpec {
31    /// A state `name` playing `clip` at rate 1, unlocked, staying when the clip ends.
32    pub fn new(name: impl Into<Name>, clip: Clip) -> Self {
33        Self {
34            name: name.into(),
35            clip,
36            rate: Rate::Fixed(1.0),
37            lock: 0.0,
38            next: None,
39        }
40    }
41
42    /// Play at `|param| / per` speed, clamped to `min..=max`.
43    pub fn rate_by(mut self, param: impl Into<Name>, per: f32, min: f32, max: f32) -> Self {
44        self.rate = Rate::Param {
45            param: param.into(),
46            per,
47            min,
48            max,
49        };
50        self
51    }
52
53    /// Only lock-breaking transitions leave it for the first `secs`.
54    pub fn lock(mut self, secs: f32) -> Self {
55        self.lock = secs;
56        self
57    }
58
59    /// Go to `next` when the clip finishes.
60    pub fn then(mut self, next: impl Into<Name>) -> Self {
61        self.next = Some(next.into());
62        self
63    }
64}
65
66/// A transition by names.
67#[derive(Clone, Debug, PartialEq)]
68pub struct TransitionSpec {
69    /// Source states; empty means any state.
70    pub from: Vec<Name>,
71    /// With `from` empty: states it does not apply to.
72    pub except: Vec<Name>,
73    /// Target state.
74    pub to: Name,
75    /// When it fires.
76    pub when: Cond<Name>,
77    /// Higher goes first.
78    pub priority: i32,
79    /// Considered while the current state is locked.
80    pub breaks_lock: bool,
81    /// May restart the state it starts from.
82    pub restart: bool,
83}
84
85impl TransitionSpec {
86    /// From any of `from` to `to` when `when` holds.
87    pub fn new<S: Into<Name>>(
88        from: impl IntoIterator<Item = S>,
89        to: impl Into<Name>,
90        when: Cond<Name>,
91    ) -> Self {
92        Self {
93            from: from.into_iter().map(Into::into).collect(),
94            except: Vec::new(),
95            to: to.into(),
96            when,
97            priority: 0,
98            breaks_lock: false,
99            restart: false,
100        }
101    }
102
103    /// From any state to `to` when `when` holds.
104    pub fn any(to: impl Into<Name>, when: Cond<Name>) -> Self {
105        Self::new(std::iter::empty::<Name>(), to, when)
106    }
107
108    /// Not from these states (with [`TransitionSpec::any`]).
109    pub fn except<S: Into<Name>>(mut self, states: impl IntoIterator<Item = S>) -> Self {
110        self.except = states.into_iter().map(Into::into).collect();
111        self
112    }
113
114    /// Set the priority.
115    pub fn priority(mut self, p: i32) -> Self {
116        self.priority = p;
117        self
118    }
119
120    /// Fire even while the current state is locked.
121    pub fn breaks_lock(mut self) -> Self {
122        self.breaks_lock = true;
123        self
124    }
125
126    /// Allow restarting the current state.
127    pub fn restart(mut self) -> Self {
128        self.restart = true;
129        self
130    }
131}
132
133/// A whole graph by names.
134#[derive(Clone, Debug, Default, PartialEq)]
135pub struct GraphSpec {
136    /// Declared parameters.
137    pub params: Vec<(Name, ParamKind)>,
138    /// The states; the first is the entry unless [`GraphSpec::entry`] says otherwise.
139    pub states: Vec<StateSpec>,
140    /// The transitions.
141    pub transitions: Vec<TransitionSpec>,
142    /// The starting state's name; `None`: the first state.
143    pub entry: Option<Name>,
144}
145
146/// Why a spec doesn't compile.
147#[derive(Clone, Debug, PartialEq)]
148pub enum SpecError {
149    /// There are no states.
150    NoStates,
151    /// Two states share a name.
152    DuplicateState(Name),
153    /// Two parameters share a name.
154    DuplicateParam(Name),
155    /// A name that isn't a state.
156    UnknownState(Name),
157    /// A name that isn't a parameter.
158    UnknownParam(Name),
159    /// A parameter used as the wrong kind (a float tested as a trigger, ...).
160    WrongKind {
161        /// The parameter.
162        param: Name,
163        /// Its declared kind.
164        declared: ParamKind,
165    },
166}
167
168impl fmt::Display for SpecError {
169    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
170        match self {
171            SpecError::NoStates => write!(f, "the graph has no states"),
172            SpecError::DuplicateState(n) => write!(f, "state {n:?} is declared twice"),
173            SpecError::DuplicateParam(n) => write!(f, "parameter {n:?} is declared twice"),
174            SpecError::UnknownState(n) => write!(f, "no state called {n:?}"),
175            SpecError::UnknownParam(n) => write!(f, "no parameter called {n:?}"),
176            SpecError::WrongKind { param, declared } => {
177                write!(
178                    f,
179                    "parameter {param:?} is a {declared:?} and can't be used that way"
180                )
181            }
182        }
183    }
184}
185
186impl std::error::Error for SpecError {}
187
188impl GraphSpec {
189    /// An empty spec.
190    pub fn new() -> Self {
191        Self::default()
192    }
193
194    /// Declare a bool parameter.
195    pub fn bool(mut self, name: impl Into<Name>) -> Self {
196        self.params.push((name.into(), ParamKind::Bool));
197        self
198    }
199
200    /// Declare a float parameter.
201    pub fn float(mut self, name: impl Into<Name>) -> Self {
202        self.params.push((name.into(), ParamKind::Float));
203        self
204    }
205
206    /// Declare a trigger that waits `ttl` seconds to be consumed.
207    pub fn trigger(mut self, name: impl Into<Name>, ttl: f32) -> Self {
208        self.params.push((name.into(), ParamKind::Trigger { ttl }));
209        self
210    }
211
212    /// Add a state.
213    pub fn state(mut self, s: StateSpec) -> Self {
214        self.states.push(s);
215        self
216    }
217
218    /// Add a transition.
219    pub fn transition(mut self, t: TransitionSpec) -> Self {
220        self.transitions.push(t);
221        self
222    }
223
224    /// Start in the state `name`.
225    pub fn entry(mut self, name: impl Into<Name>) -> Self {
226        self.entry = Some(name.into());
227        self
228    }
229
230    /// Resolve names, check kinds, order transitions by priority.
231    pub fn compile(&self) -> Result<Graph, SpecError> {
232        let mut params = Params::default();
233        for (name, kind) in &self.params {
234            if params.id(name).is_some() {
235                return Err(SpecError::DuplicateParam(name.clone()));
236            }
237            params.declare(name.clone(), *kind);
238        }
239        if self.states.is_empty() {
240            return Err(SpecError::NoStates);
241        }
242        let names: Vec<&Name> = self.states.iter().map(|s| &s.name).collect();
243        for (i, n) in names.iter().enumerate() {
244            if names[..i].contains(n) {
245                return Err(SpecError::DuplicateState((*n).clone()));
246            }
247        }
248        let state = |n: &Name| {
249            names
250                .iter()
251                .position(|s| *s == n)
252                .map(|i| StateId(i as u16))
253                .ok_or_else(|| SpecError::UnknownState(n.clone()))
254        };
255        let mut param = |n: &Name, u: Use| -> Result<ParamId, SpecError> {
256            let id = params
257                .id(n)
258                .ok_or_else(|| SpecError::UnknownParam(n.clone()))?;
259            let declared = params.kind(id);
260            let ok = matches!(
261                (u, declared),
262                (Use::Bool, ParamKind::Bool)
263                    | (Use::Float, ParamKind::Float)
264                    | (Use::Trigger, ParamKind::Trigger { .. })
265            );
266            if ok {
267                Ok(id)
268            } else {
269                Err(SpecError::WrongKind {
270                    param: n.clone(),
271                    declared,
272                })
273            }
274        };
275
276        let mut states = Vec::with_capacity(self.states.len());
277        for s in &self.states {
278            let rate = match &s.rate {
279                Rate::Fixed(r) => Rate::Fixed(*r),
280                Rate::Param {
281                    param: p,
282                    per,
283                    min,
284                    max,
285                } => Rate::Param {
286                    param: param(p, Use::Float)?,
287                    per: *per,
288                    min: *min,
289                    max: *max,
290                },
291            };
292            states.push(State {
293                name: s.name.clone(),
294                clip: s.clip.clone(),
295                rate,
296                lock: s.lock,
297                next: s.next.as_ref().map(state).transpose()?,
298            });
299        }
300
301        let mut transitions = Vec::with_capacity(self.transitions.len());
302        for t in &self.transitions {
303            let ids = |v: &[Name]| v.iter().map(state).collect::<Result<Vec<_>, _>>();
304            let from = if t.from.is_empty() {
305                Source::Any {
306                    except: ids(&t.except)?,
307                }
308            } else {
309                Source::States(ids(&t.from)?)
310            };
311            transitions.push(Transition {
312                from,
313                to: state(&t.to)?,
314                when: t.when.resolve(&mut param)?,
315                priority: t.priority,
316                breaks_lock: t.breaks_lock,
317                restart: t.restart,
318            });
319        }
320        transitions.sort_by_key(|t| std::cmp::Reverse(t.priority));
321
322        let entry = match &self.entry {
323            Some(n) => state(n)?,
324            None => StateId(0),
325        };
326        Ok(Graph {
327            params,
328            states,
329            transitions,
330            entry,
331        })
332    }
333}
334
335#[cfg(test)]
336mod tests {
337    use super::*;
338    use crate::anim::Playback;
339
340    fn clip() -> Clip {
341        Clip::uniform(Playback::Loop, &[0], 0.1)
342    }
343
344    #[test]
345    fn unknown_names_and_wrong_kinds_are_errors() {
346        let base = GraphSpec::new()
347            .float("speed")
348            .state(StateSpec::new("idle", clip()))
349            .state(StateSpec::new("run", clip()));
350        let bad_state =
351            base.clone()
352                .transition(TransitionSpec::new(["idle"], "walk", Cond::Always));
353        assert_eq!(
354            bad_state.compile(),
355            Err(SpecError::UnknownState("walk".into()))
356        );
357        let bad_kind = base.clone().transition(TransitionSpec::new(
358            ["idle"],
359            "run",
360            Cond::Trigger("speed".into()),
361        ));
362        assert!(matches!(
363            bad_kind.compile(),
364            Err(SpecError::WrongKind { .. })
365        ));
366        let ok = base.transition(TransitionSpec::new(
367            ["idle"],
368            "run",
369            Cond::Above("speed".into(), 1.0),
370        ));
371        assert!(ok.compile().is_ok());
372    }
373
374    #[test]
375    fn transitions_are_ordered_by_priority_stably() {
376        let g = GraphSpec::new()
377            .state(StateSpec::new("a", clip()))
378            .state(StateSpec::new("b", clip()))
379            .state(StateSpec::new("c", clip()))
380            .transition(TransitionSpec::any("b", Cond::Always))
381            .transition(TransitionSpec::any("c", Cond::Always).priority(5))
382            .transition(TransitionSpec::any("a", Cond::Always))
383            .compile()
384            .unwrap();
385        let order: Vec<_> = g
386            .transitions
387            .iter()
388            .map(|t| g.state(t.to).name.as_ref())
389            .collect();
390        assert_eq!(order, ["c", "b", "a"]);
391    }
392}