fg_notation/
numpad.rs

1use core::fmt;
2use std::str::FromStr;
3
4use crate::{abbreviated, CreationError};
5
6/// A move represented using [numpad notation](https://glossary.infil.net/?t=Numpad%20Notation)
7#[derive(Debug, Clone, PartialEq, Eq)]
8pub struct Move {
9    modifier: Modifier,
10    motion: Motion,
11    button: Button,
12}
13
14/// A numpad notation motion
15#[derive(Debug, Clone, Eq)]
16pub struct Motion(String);
17
18/// A numpad notation button
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct Button(String);
21
22/// A numpad notation modifier
23#[non_exhaustive]
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum Modifier {
26    Jump,
27    SuperJump,
28    JumpCancel,
29    Close,
30    Far,
31    TigerKnee,
32    None,
33}
34
35impl Move {
36    /// Create a [`Move`] from `input` that can be represented
37    /// as a string
38    ///
39    /// Returns a [`CreationError`] if any component of the input
40    /// is invalid
41    pub fn new<S>(input: S) -> Result<Self, CreationError>
42    where
43        S: ToString,
44    {
45        let mut input = input.to_string().trim().to_string();
46        let modifier = Self::get_modifier(&mut input)?;
47        let motion = Motion::new(
48            input
49                .chars()
50                .take_while(|c| !c.is_ascii_alphabetic())
51                .collect::<String>(),
52        )?;
53        let button = Button::new(
54            input
55                .chars()
56                .skip_while(|c| !c.is_ascii_alphabetic())
57                .collect::<String>(),
58        )?;
59
60        Ok(Self {
61            modifier,
62            motion,
63            button,
64        })
65    }
66
67    pub fn button(&self) -> Button {
68        self.button.clone()
69    }
70
71    pub fn motion(&self) -> Motion {
72        self.motion.clone()
73    }
74
75    pub fn modifier(&self) -> Modifier {
76        self.modifier
77    }
78
79    fn get_modifier(input: &mut String) -> Result<Modifier, CreationError> {
80        if input.contains('.') {
81            let prefix = input.chars().take_while(|c| *c != '.').collect::<String>();
82            for _ in 0..prefix.len() {
83                (*input).remove(0); // Remove characters
84            }
85            (*input).remove(0); // Remove '.'
86            Ok(Modifier::new(prefix)?)
87        } else {
88            Ok(Modifier::None)
89        }
90    }
91}
92
93impl Modifier {
94    /// Create a [`Modifier`] from something that can be represented
95    /// as a string
96    ///
97    /// Returns a [`CreationError`] if the provided prefix cannot
98    /// be matched to a valid modifier
99    pub fn new<S>(m: S) -> Result<Self, CreationError>
100    where
101        S: ToString,
102    {
103        let m = m.to_string();
104
105        match m.to_lowercase().as_str() {
106            "j." | "j" => Ok(Self::Jump),
107            "sj." | "sj" => Ok(Self::SuperJump),
108            "jc." | "jc" => Ok(Self::JumpCancel),
109            "c." | "c" => Ok(Self::Close),
110            "f." | "f" => Ok(Self::Far),
111            "tk." | "tk" => Ok(Self::TigerKnee),
112            _ => Err(CreationError::InvalidModifier),
113        }
114    }
115}
116
117impl Button {
118    /// Create a [`Button`] from something that can be represented as a
119    /// string
120    pub fn new<S>(b: S) -> Result<Self, CreationError>
121    where
122        S: ToString,
123    {
124        let b = b.to_string();
125        if !b.chars().all(|c| c.is_ascii_alphabetic()) {
126            Err(CreationError::InvalidButton)
127        } else {
128            Ok(Self(b))
129        }
130    }
131}
132
133impl Motion {
134    #![allow(clippy::len_without_is_empty)] // Not possible for `Motion` to be empty
135
136    /// Create a [`Motion`] from something that can be represented
137    /// as a string
138    ///
139    /// Will return a [`CreationError`] if the input contains
140    /// any characters other than ASCII digits or square brackets
141    pub fn new<S>(m: S) -> Result<Self, CreationError>
142    where
143        S: ToString,
144    {
145        let m = m.to_string();
146
147        if !m
148            .chars()
149            .all(|c| c.is_ascii_digit() || (c == '[' || c == ']'))
150        {
151            Err(CreationError::InvalidMotion)
152        } else {
153            Ok(Self(m))
154        }
155    }
156
157    pub fn len(&self) -> usize {
158        self.0.len()
159    }
160
161    #[must_use]
162    pub fn is_neutral(&self) -> bool {
163        self.0 == "5"
164    }
165}
166
167impl From<abbreviated::Move> for Move {
168    fn from(m: abbreviated::Move) -> Self {
169        let button = Button::from(m.button());
170        let a_mod = m.modifier();
171        let motion = if a_mod == abbreviated::Modifier::Standing {
172            Motion::new("5").unwrap()
173        } else if a_mod == abbreviated::Modifier::Crouching {
174            Motion::new("2").unwrap()
175        } else {
176            Motion::from(m.motion())
177        };
178        let modifier = Modifier::from(a_mod);
179
180        Self {
181            button,
182            motion,
183            modifier,
184        }
185    }
186}
187
188impl fmt::Display for Move {
189    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190        write!(f, "{}{}{}", self.modifier, self.motion.0, self.button.0)
191    }
192}
193
194impl FromStr for Move {
195    type Err = CreationError;
196
197    fn from_str(s: &str) -> Result<Self, Self::Err> {
198        Self::new(s)
199    }
200}
201
202impl From<abbreviated::Button> for Button {
203    fn from(b: abbreviated::Button) -> Self {
204        Button(b.to_string())
205    }
206}
207
208impl FromStr for Button {
209    type Err = CreationError;
210
211    fn from_str(s: &str) -> Result<Self, Self::Err> {
212        Self::new(s)
213    }
214}
215
216impl fmt::Display for Button {
217    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(f, "{}", self.0)
219    }
220}
221
222impl From<abbreviated::Motion> for Motion {
223    fn from(m: abbreviated::Motion) -> Self {
224        match m {
225            abbreviated::Motion::N => Self("5".to_string()),
226            abbreviated::Motion::U => Self("8".to_string()),
227            abbreviated::Motion::D => Self("2".to_string()),
228            abbreviated::Motion::B => Self("4".to_string()),
229            abbreviated::Motion::F => Self("6".to_string()),
230            abbreviated::Motion::DB => Self("1".to_string()),
231            abbreviated::Motion::DF => Self("3".to_string()),
232            abbreviated::Motion::UB => Self("7".to_string()),
233            abbreviated::Motion::UF => Self("9".to_string()),
234            abbreviated::Motion::QCF => Self("236".to_string()),
235            abbreviated::Motion::QCB => Self("214".to_string()),
236            abbreviated::Motion::HCF => Self("41236".to_string()),
237            abbreviated::Motion::HCB => Self("63214".to_string()),
238            abbreviated::Motion::DP => Self("623".to_string()),
239            abbreviated::Motion::RDP => Self("421".to_string()),
240            abbreviated::Motion::FullCircle => Self("41236987".to_string()),
241            abbreviated::Motion::Double360 => Self("4123698741236987".to_string()),
242            abbreviated::Motion::Other(o) => Self::new(o).unwrap(),
243        }
244    }
245}
246
247impl FromStr for Motion {
248    type Err = CreationError;
249
250    fn from_str(s: &str) -> Result<Self, Self::Err> {
251        Self::new(s)
252    }
253}
254
255impl fmt::Display for Motion {
256    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
257        write!(f, "{}", self.0)
258    }
259}
260
261impl PartialEq for Motion {
262    fn eq(&self, other: &Self) -> bool {
263        (self.0.is_empty() && other.0 == "5")
264            || (self.0 == "5" && other.0.is_empty())
265            || self.0 == other.0
266    }
267}
268
269impl From<abbreviated::Modifier> for Modifier {
270    fn from(m: abbreviated::Modifier) -> Self {
271        match m {
272            abbreviated::Modifier::Close => Self::Close,
273            abbreviated::Modifier::Far => Self::Far,
274            abbreviated::Modifier::Standing => Self::None,
275            abbreviated::Modifier::Crouching => Self::None,
276            abbreviated::Modifier::Jump => Self::Jump,
277            abbreviated::Modifier::SuperJump => Self::SuperJump,
278            abbreviated::Modifier::JumpCancel => Self::JumpCancel,
279            abbreviated::Modifier::TigerKnee => Self::TigerKnee,
280            abbreviated::Modifier::None => Self::None,
281        }
282    }
283}
284
285impl fmt::Display for Modifier {
286    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
287        let prefix = match self {
288            Modifier::Jump => "j.",
289            Modifier::SuperJump => "sj.",
290            Modifier::JumpCancel => "jc.",
291            Modifier::Close => "c.",
292            Modifier::Far => "f.",
293            Modifier::TigerKnee => "tk.",
294            Modifier::None => "",
295        };
296        write!(f, "{prefix}")
297    }
298}
299
300impl FromStr for Modifier {
301    type Err = CreationError;
302
303    fn from_str(s: &str) -> Result<Self, Self::Err> {
304        Modifier::new(s)
305    }
306}
307
308#[cfg(test)]
309mod tests {
310    use super::*;
311    use pretty_assertions::assert_eq;
312
313    #[test]
314    fn j236h() {
315        let attack = "j.236H";
316        let created = Move::new(attack).unwrap();
317
318        assert_eq!(
319            created,
320            Move {
321                modifier: Modifier::Jump,
322                motion: Motion("236".to_string()),
323                button: Button("H".to_string())
324            }
325        )
326    }
327
328    #[test]
329    fn heavy_dp() {
330        let attack = "623Hp";
331        let created = Move::new(attack).unwrap();
332
333        assert_eq!(
334            created,
335            Move {
336                modifier: Modifier::None,
337                motion: Motion("623".to_string()),
338                button: Button("Hp".to_string())
339            }
340        )
341    }
342
343    #[test]
344    fn jl() {
345        let attack = "j.L";
346        let created = Move::new(attack).unwrap();
347
348        assert_eq!(
349            created,
350            Move {
351                modifier: Modifier::Jump,
352                motion: Motion("5".to_string()),
353                button: Button("L".to_string())
354            }
355        )
356    }
357
358    #[test]
359    fn charge_move() {
360        let attack = "[4]6A";
361        let created = Move::new(attack).unwrap();
362
363        assert_eq!(
364            created,
365            Move {
366                modifier: Modifier::None,
367                motion: Motion("[4]6".to_string()),
368                button: Button("A".to_string())
369            }
370        )
371    }
372
373    #[test]
374    fn cs() {
375        let attack = "c.S";
376        let created = Move::new(attack).unwrap();
377
378        assert_eq!(
379            created,
380            Move {
381                modifier: Modifier::Close,
382                motion: Motion("5".to_string()),
383                button: Button("S".to_string())
384            }
385        )
386    }
387
388    #[test]
389    fn superjump() {
390        let attack = "sj.236S";
391        let created = Move::new(attack).unwrap();
392
393        assert_eq!(
394            created,
395            Move {
396                modifier: Modifier::SuperJump,
397                motion: Motion("236".to_string()),
398                button: Button("S".to_string())
399            }
400        )
401    }
402
403    #[test]
404    fn move_tostring() {
405        let m = Move::new("214L").unwrap();
406        assert_eq!(m.to_string(), "214L".to_string());
407    }
408
409    #[test]
410    fn button_creation() {
411        let button = "HS";
412        let created = Button::new(button).unwrap();
413
414        assert_eq!(created, Button("HS".to_string()));
415    }
416
417    #[test]
418    #[should_panic]
419    fn invalid_button_fails() {
420        let invalid = "69lol";
421
422        Button::new(invalid).unwrap();
423    }
424
425    #[test]
426    fn motion_creation() {
427        let motion = "236";
428        let created = Motion::new(motion).unwrap();
429
430        assert_eq!(created, Motion("236".to_string()));
431    }
432
433    #[test]
434    #[should_panic]
435    fn invalid_motion_fails() {
436        let invalid = "balls22";
437
438        Motion::new(invalid).unwrap();
439    }
440
441    #[test]
442    fn no_motion() {
443        let m = Motion::new("").unwrap();
444
445        println!("{m}")
446    }
447}