1use std::{collections::VecDeque, fmt::Display, str::FromStr};
2
3use nom::{
4 branch::alt,
5 bytes::complete::{is_not, tag, tag_no_case},
6 character::complete::{
7 char, digit1, i32, line_ending, multispace0, one_of, space0, space1, u32,
8 },
9 combinator::{eof, map, map_res, opt, success, value},
10 multi::{many0, many1, separated_list1},
11 sequence::{delimited, preceded, terminated, tuple},
12 Finish, IResult,
13};
14use nom_locate::LocatedSpan;
15
16type Span<'a> = LocatedSpan<&'a str>;
17type ParseResult<'a, T> = IResult<Span<'a>, T>;
18
19#[derive(Debug)]
20pub struct Action {
21 pub name: String,
22 pub elements: Vec<Frame>,
23 pub loop_start: usize,
24}
25
26#[derive(Debug)]
27enum Element {
28 Frame(Frame),
29 LoopStart,
30 HurtBoxes {
31 default: bool,
32 boxes: Vec<CollisionBox>,
33 },
34 HitBoxes {
35 default: bool,
36 boxes: Vec<CollisionBox>,
37 },
38}
39
40#[derive(Debug)]
41pub struct Frame {
42 pub group: u32,
43 pub image: i32,
44 pub x: i32,
45 pub y: i32,
46 pub ticks: i32,
47 pub flip: Flip,
48 pub blend: Option<Blend>,
49 pub hit_boxes: Vec<CollisionBox>,
50 pub hurt_boxes: Vec<CollisionBox>,
51}
52
53#[derive(Debug, PartialEq, Eq, Clone, Copy)]
54pub enum Flip {
55 NoFlip,
56 Horizontal,
57 Vertical,
58 Both,
59}
60
61#[derive(Debug, PartialEq, Eq, Clone, Copy)]
62pub enum Blend {
63 Add { src: u32, dst: u32 },
64 Sub,
65}
66
67#[derive(Debug, PartialEq, Eq, Clone, Copy)]
68pub struct CollisionBox(i32, i32, i32, i32);
69
70impl CollisionBox {
71 pub fn x(&self) -> i32 {
72 self.0
73 }
74
75 pub fn y(&self) -> i32 {
76 self.1
77 }
78
79 pub fn width(&self) -> i32 {
80 self.2 - self.0
81 }
82
83 pub fn height(&self) -> i32 {
84 self.3 - self.1
85 }
86}
87
88#[derive(Debug)]
89pub struct ParseError {
90 pub line: u32,
91}
92
93impl FromStr for Action {
94 type Err = ParseError;
95
96 fn from_str(s: &str) -> Result<Self, Self::Err> {
97 match single_action(s.into()).finish() {
98 Ok((_, action)) => Ok(action),
99 Err(err) => Err(ParseError {
100 line: err.input.location_line(),
101 }),
102 }
103 }
104}
105
106pub fn parse_air(air: &str) -> Result<Vec<Action>, ParseError> {
107 match actions(air.into()).finish() {
108 Ok((_, actions)) => Ok(actions),
109 Err(err) => Err(ParseError {
110 line: err.input.location_line(),
111 }),
112 }
113}
114
115fn actions(i: Span) -> ParseResult<Vec<Action>> {
116 terminated(many0(action), ending)(i)
117}
118
119fn single_action(i: Span) -> ParseResult<Action> {
120 terminated(action, ending)(i)
121}
122
123fn ending(i: Span) -> ParseResult<Span> {
124 alt((
125 eof,
126 value("".into(), tuple((multispace0, eof))),
127 value(
128 "".into(),
129 tuple((multispace0, many0(comment_or_eol), multispace0, eof)),
130 ),
131 ))(i)
132}
133
134fn action(i: Span) -> ParseResult<Action> {
135 let (i, name) = delimited(
136 tuple((multispace0, many0(comment_or_eol), multispace0)),
137 begin_action,
138 tuple((space0, comment_or_eol)),
139 )(i)?;
140
141 let (i, elements) = many1(preceded(
142 tuple((multispace0, many0(comment_or_eol), multispace0)),
143 animation_element,
144 ))(i)?;
145
146 let mut elements: VecDeque<_> = elements.into();
147 let mut frames = Vec::with_capacity(elements.len());
148 let mut loop_start = 0;
149 let mut time = 0;
150 let mut hit_boxes: (bool, Option<Vec<CollisionBox>>) = Default::default();
151 let mut hurt_boxes: (bool, Option<Vec<CollisionBox>>) = Default::default();
152
153 while let Some(elem) = elements.pop_front() {
154 match elem {
155 Element::Frame(frame) => {
156 let frame = Frame {
157 hit_boxes: match hit_boxes {
158 (true, ref boxes) => boxes.clone().unwrap_or_default(),
159 (false, ref mut boxes) => boxes.take().unwrap_or_default(),
160 },
161 hurt_boxes: match hurt_boxes {
162 (true, ref boxes) => boxes.clone().unwrap_or_default(),
163 (false, ref mut boxes) => boxes.take().unwrap_or_default(),
164 },
165 ..frame
166 };
167 if frame.ticks >= 0 {
168 time += frame.ticks;
169 frames.push(frame);
170 } else {
171 frames.push(frame);
172 break;
173 }
174 }
175 Element::LoopStart => loop_start = time,
176 Element::HurtBoxes { default, boxes } => {
177 hurt_boxes = (default, Some(boxes));
178 }
179 Element::HitBoxes { default, boxes } => {
180 hit_boxes = (default, Some(boxes));
181 }
182 }
183 }
184
185 Ok((
186 i,
187 Action {
188 name: name.to_string(),
189 elements: frames,
190 loop_start: loop_start as usize,
191 },
192 ))
193}
194
195fn comment(i: Span) -> ParseResult<LocatedSpan<&str>> {
196 preceded(tag(";"), is_not("\n"))(i)
197}
198
199fn comment_or_eol(i: Span) -> ParseResult<LocatedSpan<&str>> {
200 alt((comment, line_ending, eof))(i)
201}
202
203fn begin_action(i: Span) -> ParseResult<Span> {
204 delimited(char('['), delimited(space0, action_name, space0), char(']'))(i)
205}
206
207fn action_name(i: Span) -> ParseResult<Span> {
208 let (i, _) = tag_no_case("begin")(i)?;
209 let (i, _) = space1(i)?;
210 let (i, _) = tag_no_case("action")(i)?;
211 let (i, _) = space1(i)?;
212 digit1(i)
213}
214
215fn animation_element(i: Span) -> ParseResult<Element> {
216 let frame = map(animation_frame, Element::Frame);
217 let loop_start = map(tag_no_case("loopstart"), |_| Element::LoopStart);
218 alt((
219 terminated(frame, tuple((space0, comment_or_eol))),
220 terminated(loop_start, tuple((space0, comment_or_eol))),
221 clsn_boxes,
222 ))(i)
223}
224
225fn animation_frame(i: Span) -> ParseResult<Frame> {
226 let (i, (group, image, x, y, ticks, flip, blend)) = tuple((
227 terminated(delimited(space0, u32, space0), char(',')),
228 terminated(delimited(space0, i32, space0), char(',')),
229 terminated(delimited(space0, i32, space0), char(',')),
230 terminated(delimited(space0, i32, space0), char(',')),
231 delimited(space0, i32, space0),
232 alt((
233 delimited(
234 delimited(space0, char(','), space0),
235 opt(element_flip),
236 space0,
237 ),
238 success(None),
239 )),
240 alt((
241 delimited(
242 delimited(space0, char(','), space0),
243 opt(element_blend),
244 space0,
245 ),
246 success(None),
247 )),
248 ))(i)?;
249
250 let elem = Frame {
251 group,
252 image,
253 x,
254 y,
255 ticks,
256 flip: flip.unwrap_or(Flip::NoFlip),
257 blend,
258 hit_boxes: Vec::new(),
259 hurt_boxes: Vec::new(),
260 };
261
262 Ok((i, elem))
263}
264
265fn element_flip(i: Span) -> ParseResult<Flip> {
266 let flip = alt((
267 tag_no_case("VH"),
268 tag_no_case("HV"),
269 tag_no_case("V"),
270 tag_no_case("H"),
271 ));
272
273 map_res(flip, |flip: Span| {
274 match flip.to_ascii_uppercase().as_str() {
275 "VH" | "HV" => Ok(Flip::Both),
276 "H" => Ok(Flip::Horizontal),
277 "V" => Ok(Flip::Vertical),
278 _ => Err("Invalid flip"),
279 }
280 })(i)
281}
282
283fn element_blend(i: Span) -> ParseResult<Blend> {
284 fn add_blend(i: Span) -> ParseResult<Blend> {
285 alt((full_add_blend, short_add_blend))(i)
286 }
287
288 fn short_add_blend(i: Span) -> ParseResult<Blend> {
289 let (i, _) = tag_no_case("A")(i)?;
290 let (i, dst) = opt(u32)(i)?;
291 let dst = match dst {
292 Some(n) => 256 / 2_u32.pow(n),
293 _ => 256,
294 };
295
296 Ok((i, Blend::Add { src: 256, dst }))
297 }
298
299 fn full_add_blend(i: Span) -> ParseResult<Blend> {
300 let (i, _) = tag_no_case("AS")(i)?;
301 let (i, src) = u32(i)?;
302 let (i, _) = tag_no_case("D")(i)?;
303 let (i, dst) = u32(i)?;
304
305 Ok((i, Blend::Add { src, dst }))
306 }
307
308 fn sub_blend(i: Span) -> ParseResult<Blend> {
309 let (i, _) = tag_no_case("S")(i)?;
310 Ok((i, Blend::Sub))
311 }
312
313 alt((add_blend, sub_blend))(i)
314}
315
316fn clsn_boxes(i: Span) -> ParseResult<Element> {
317 #[derive(Debug, PartialEq, Eq, Clone, Copy)]
318 enum ClsnBoxKind {
319 Hit,
320 Hurt,
321 }
322
323 impl Display for ClsnBoxKind {
324 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
325 match self {
326 ClsnBoxKind::Hit => write!(f, "1"),
327 ClsnBoxKind::Hurt => write!(f, "2"),
328 }
329 }
330 }
331
332 fn clsn_box_kind(i: Span) -> ParseResult<ClsnBoxKind> {
333 map_res(one_of("12"), |k| match k {
334 '1' => Ok(ClsnBoxKind::Hit),
335 '2' => Ok(ClsnBoxKind::Hurt),
336 k => Err(format!("Invalid collision box kind: {k}")),
337 })(i)
338 }
339
340 fn clsn_box(expected_kind: ClsnBoxKind) -> impl Fn(Span) -> ParseResult<CollisionBox> {
341 move |i| {
342 let (i, _) = tag_no_case("Clsn")(i)?;
343 let (i, _) = map_res(clsn_box_kind, |found_kind| {
344 if expected_kind == found_kind {
345 Ok(found_kind)
346 } else {
347 Err(format!(
348 "Expecting collision box kind {expected_kind} found: {found_kind}"
349 ))
350 }
351 })(i)?;
352 let (i, _n) = delimited(tag("["), delimited(space0, u32, space0), tag("]"))(i)?;
353 let (i, _) = preceded(space0, tag("="))(i)?;
354 let (i, coords) = map_res(
355 separated_list1(tag(","), delimited(space0, i32, space0)),
356 |coords| {
357 if coords.len() == 4 {
358 Ok(coords)
359 } else {
360 Err("Invalid collision box")
361 }
362 },
363 )(i)?;
364
365 Ok((i, CollisionBox(coords[0], coords[1], coords[2], coords[3])))
366 }
367 }
368
369 let (i, _) = tag_no_case("Clsn")(i)?;
370 let (i, kind) = clsn_box_kind(i)?;
371 let (i, def) = opt(tag_no_case("default"))(i)?;
372 let (i, _) = preceded(space0, tag(":"))(i)?;
373
374 let (i, count) = preceded(space0, u32)(i)?;
375
376 let (i, boxes) = map_res(
377 many1(preceded(
378 tuple((multispace0, many0(comment_or_eol), multispace0)),
379 clsn_box(kind),
380 )),
381 |boxes| {
382 if count as usize == boxes.len() {
383 Ok(boxes)
384 } else {
385 Err(format!(
386 "Expecting {count} collision boxes, found: {}",
387 boxes.len()
388 ))
389 }
390 },
391 )(i)?;
392
393 let element = match kind {
394 ClsnBoxKind::Hit => Element::HitBoxes {
395 default: def.is_some(),
396 boxes,
397 },
398 ClsnBoxKind::Hurt => Element::HurtBoxes {
399 default: def.is_some(),
400 boxes,
401 },
402 };
403 Ok((i, element))
404}
405
406#[cfg(test)]
407mod tests {
408 use indoc::indoc;
409
410 use super::*;
411
412 #[test]
413 fn it_shows_location_of_the_error() {
414 let text = "[begin action invalid]";
415 let error = text.parse::<Action>().err().unwrap();
416 assert_eq!(error.line, 1);
417
418 let text = indoc! {"
419 [begin action 001]
420 200, 20, 30, 40, 1
421 200, 20, 30, 40, invalid
422 "};
423 let error = text.parse::<Action>().err().unwrap();
424 assert_eq!(error.line, 3);
425
426 let text = indoc! {"
427 [begin action 001]
428 200, 20, 30, 40, 1
429 200, 20, 30, 40, 2
430
431 [begin action 002]
432 200, 20, 30, 40, 1
433 200, 20, 30, 40, 2
434
435 [begin action 003]
436 200, 20, 30, 40, 1
437 200, 20, 30, 40, 2
438 "};
439 let error = text.parse::<Action>().err().unwrap();
440 assert_eq!(error.line, 5);
441
442 let text = indoc! {"
443 [begin action 001]
444 200, 20, 30, 40, 1
445 200, 20, 30, 40, 2
446
447 [begin action 002]
448 200, 20, 30, 40, 1
449 200, 20, 30, 40, 2
450
451 [begin action 003]
452 200, 20, 30, 40, 1
453 200, 20, 30, 40, invalid
454
455 [begin action 004]
456 200, 20, 30, 40, 1
457 "};
458 let error = parse_air(text).err().unwrap();
459 assert_eq!(error.line, 11);
460 }
461
462 #[test]
463 fn it_parses_air_file() {
464 let text = indoc! {"
465 [begin action 001]
466 200, 20, 30, 40, 50
467
468 [begin action 002]
469 200, 20, 30, 40, 50
470
471 ; Action 3
472 [begin action 003]
473 200, 20, 30, 40, 50
474
475 "};
476 let actions = parse_air(text).unwrap();
477 assert_eq!(actions.len(), 3);
478 assert_eq!(actions[0].name, "001");
479 assert_eq!(actions[1].name, "002");
480 assert_eq!(actions[2].name, "003");
481 }
482
483 #[test]
484 fn it_parses_action_name() {
485 let text = indoc! {"
486 [begin action 001]
487 200, 20, 30, 40, 50
488 "};
489 let action = text.parse::<Action>().unwrap();
490 assert_eq!(action.name, "001");
491
492 let text = indoc! {"
493 [ begin action 001 ]
494 200, 20, 30, 40, 50
495 "};
496 let action = text.parse::<Action>().unwrap();
497 assert_eq!(action.name, "001");
498
499 let text = indoc! {"
500 [begin action 001]
501 200, 20, 30, 40, 50
502 "};
503 let action = text.parse::<Action>().unwrap();
504 assert_eq!(action.name, "001");
505
506 let text = indoc! {"
507
508
509 ; Comment
510
511 [begin action 001]
512 200, 20, 30, 40, 50
513 "};
514 let action = text.parse::<Action>().unwrap();
515 assert_eq!(action.name, "001");
516 }
517
518 #[test]
519 fn it_parses_animation_elements() {
520 let text = indoc! {"
521 [begin action 001]
522 200, 20, 30, 40, 50
523 "};
524 let action = text.parse::<Action>().unwrap();
525 assert_eq!(action.elements.len(), 1);
526 let element = &action.elements[0];
527 assert_eq!(element.group, 200);
528 assert_eq!(element.image, 20);
529 assert_eq!(element.x, 30);
530 assert_eq!(element.y, 40);
531 assert_eq!(element.ticks, 50);
532 }
533
534 #[test]
535 fn it_parses_animation_element_flip() {
536 let text = indoc! {"
537 [begin action 001]
538 200, 20, 30, 40, 50
539 200, 20, 30, 40, 50,
540 200, 20, 30, 40, 50, V
541 200, 20, 30, 40, 50, H
542 200, 20, 30, 40, 50, VH
543 200, 20, 30, 40, 50, HV
544 "};
545 let action = text.parse::<Action>().unwrap();
546 assert_eq!(action.elements[0].flip, Flip::NoFlip);
547 assert_eq!(action.elements[1].flip, Flip::NoFlip);
548 assert_eq!(action.elements[2].flip, Flip::Vertical);
549 assert_eq!(action.elements[3].flip, Flip::Horizontal);
550 assert_eq!(action.elements[4].flip, Flip::Both);
551 assert_eq!(action.elements[5].flip, Flip::Both);
552
553 let text = indoc! {"
554 [begin action 001]
555 200, 20, 30, 40, 50, O
556 "};
557 assert!(text.parse::<Action>().is_err());
558 }
559
560 #[test]
561 fn it_parses_animation_element_blend() {
562 let text = indoc! {"
563 [begin action 001]
564 200, 20, 30, 40, 50
565 200, 20, 30, 40, 50, , A
566 200, 20, 30, 40, 50, , S
567 200, 20, 30, 40, 50, , A1
568 200, 20, 30, 40, 50, , A2
569 200, 20, 30, 40, 50, , A3
570 200, 20, 30, 40, 50, , AS33D44
571 "};
572 let action = text.parse::<Action>().unwrap();
573 let elems = &action.elements;
574 assert_eq!(elems[0].blend, None);
575 assert_eq!(elems[1].blend, Some(Blend::Add { src: 256, dst: 256 }));
576 assert_eq!(elems[2].blend, Some(Blend::Sub));
577 assert_eq!(elems[3].blend, Some(Blend::Add { src: 256, dst: 128 }));
578 assert_eq!(elems[4].blend, Some(Blend::Add { src: 256, dst: 64 }));
579 assert_eq!(elems[5].blend, Some(Blend::Add { src: 256, dst: 32 }));
580 assert_eq!(elems[6].blend, Some(Blend::Add { src: 33, dst: 44 }));
581
582 let text = indoc! {"
583 [begin action 001]
584 200, 20, 30, 40, 50, , invalid
585 "};
586 assert!(text.parse::<Action>().is_err());
587 }
588
589 #[test]
590 fn it_parses_loop_start() {
591 let text = indoc! {"
592 [begin action 001]
593 200, 10, 30, 40, 50
594 200, 20, 30, 40, 50
595 loopstart
596 200, 30, 30, 40, 50
597 200, 40, 30, 40, 50
598 "};
599 let action = text.parse::<Action>().unwrap();
600 assert_eq!(action.loop_start, 100);
601 }
602
603 #[test]
604 fn it_parses_collision_boxes() {
605 let text = indoc! {"
606 [begin action 001]
607 Clsn2: 2
608 Clsn2[0] = 15, -1,-24,-72
609 Clsn2[1] = -12,-84, 11,-61
610 200, 20, 30, 40, 50
611 200, 30, 30, 40, 50
612 "};
613 let action = text.parse::<Action>().unwrap();
614 let frames = &action.elements;
615 assert!(frames[0].hit_boxes.is_empty());
616 assert_eq!(frames[0].hurt_boxes.len(), 2);
617 assert_eq!(frames[0].hurt_boxes[0], CollisionBox(15, -1, -24, -72));
618 assert_eq!(frames[0].hurt_boxes[1], CollisionBox(-12, -84, 11, -61));
619 assert!(frames[1].hit_boxes.is_empty());
620 assert!(frames[1].hurt_boxes.is_empty());
621
622 let text = indoc! {"
623 [begin action 001]
624 Clsn1: 2
625 Clsn1[0] = 15, -1,-24,-72
626 Clsn1[1] = -12,-84, 11,-61
627 200, 20, 30, 40, 50
628 200, 30, 30, 40, 50
629 "};
630 let action = text.parse::<Action>().unwrap();
631 let frames = &action.elements;
632 assert!(frames[0].hurt_boxes.is_empty());
633 assert_eq!(frames[0].hit_boxes.len(), 2);
634 assert_eq!(frames[0].hit_boxes[0], CollisionBox(15, -1, -24, -72));
635 assert_eq!(frames[0].hit_boxes[1], CollisionBox(-12, -84, 11, -61));
636 assert!(frames[1].hurt_boxes.is_empty());
637 assert!(frames[1].hit_boxes.is_empty());
638 }
639
640 #[test]
641 fn it_parses_collision_boxes_defaults() {
642 let text = indoc! {"
643 [begin action 001]
644 clsn2default: 1
645 clsn2[0] = 15, -1,-24,-72
646 200, 20, 30, 40, 50
647 200, 40, 30, 40, 50
648 "};
649 let action = text.parse::<Action>().unwrap();
650 let frames = &action.elements;
651 assert!(frames[0].hit_boxes.is_empty());
652 assert_eq!(frames[0].hurt_boxes.len(), 1);
653 assert_eq!(frames[0].hurt_boxes[0], CollisionBox(15, -1, -24, -72));
654 assert!(frames[1].hit_boxes.is_empty());
655 assert_eq!(frames[1].hurt_boxes[0], CollisionBox(15, -1, -24, -72));
656
657 let text = indoc! {"
658 [begin action 001]
659 Clsn1default: 1
660 Clsn1[0] = 15, -1,-24,-72
661 200, 20, 30, 40, 50
662 200, 40, 30, 40, 50
663 "};
664 let action = text.parse::<Action>().unwrap();
665 let frames = &action.elements;
666 assert!(frames[0].hurt_boxes.is_empty());
667 assert_eq!(frames[0].hit_boxes.len(), 1);
668 assert_eq!(frames[0].hit_boxes[0], CollisionBox(15, -1, -24, -72));
669 assert!(frames[1].hurt_boxes.is_empty());
670 assert_eq!(frames[1].hit_boxes[0], CollisionBox(15, -1, -24, -72));
671 }
672
673 #[test]
674 fn it_rejects_collision_boxes_with_unmatched_kind() {
675 let text = indoc! {"
676 [begin action 001]
677 Clsn2: 1
678 clsn1[0] = 15, -1,-24,-72
679 200, 20, 30, 40, 50
680 "};
681 assert!(text.parse::<Action>().is_err());
682
683 let text = indoc! {"
684 [begin action 001]
685 Clsn1: 1
686 clsn2[0] = 15, -1,-24,-72
687 200, 20, 30, 40, 50
688 "};
689 assert!(text.parse::<Action>().is_err());
690 }
691
692 #[test]
693 fn it_rejects_collision_boxes_with_unmatched_count() {
694 let text = indoc! {"
695 [begin action 001]
696 Clsn2: 2
697 clsn2[0] = 15, -1,-24,-72
698 200, 20, 30, 40, 50
699 "};
700 assert!(text.parse::<Action>().is_err());
701
702 let text = indoc! {"
703 [begin action 001]
704 Clsn1: 2
705 clsn1[0] = 15, -1,-24,-72
706 200, 20, 30, 40, 50
707 "};
708 assert!(text.parse::<Action>().is_err());
709 }
710 #[test]
711 fn it_ignores_comments() {
712 let text = indoc! {"
713 [begin action 001] ; Comment
714 200, 20, 30, 40, 50 ; Comment
715 200, 20, 30, 40, 50; Comment
716 "};
717 assert!(text.parse::<Action>().is_ok());
718 let action = text.parse::<Action>().unwrap();
719 assert_eq!(action.elements.len(), 2);
720
721 let text = indoc! {"
722 [begin action 001]
723 200, 20, 30, 40, 50 ; Comment
724 "};
725 assert!(text.parse::<Action>().is_ok());
726 let action = text.parse::<Action>().unwrap();
727 assert_eq!(action.elements.len(), 1);
728
729 let text = indoc! {"
730 [begin action 001]
731 ; Comment
732 ; Comment
733
734 ; Comment
735 200, 20, 30, 40, 50
736 "};
737 assert!(text.parse::<Action>().is_ok());
738 let action = text.parse::<Action>().unwrap();
739 assert_eq!(action.elements.len(), 1);
740
741 let text = indoc! {"
742 [begin action 001]
743 ; Comment
744 ; Comment
745 ; Comment
746 Clsn1default: 1; Comment
747 ; Comment
748 ; Comment
749
750 ; Comment
751 Clsn1[0] = 15, -1,-24,-72 ; Comment
752 ; Comment
753 ; Comment
754 200, 20, 30, 40, 50
755 Clsn1: 2 ; Comment
756 Clsn1[0] = 16, -1,-24,-72 ; Comment
757 ; Comment
758
759 ; Comment
760
761 Clsn1[1] = -12,-84, 11,-61 ; Comment
762 ; Comment
763 200, 40, 30, 40, 50
764 "};
765 assert!(text.parse::<Action>().is_ok());
766 let action = text.parse::<Action>().unwrap();
767 assert_eq!(
768 action.elements[0].hit_boxes[0],
769 CollisionBox(15, -1, -24, -72)
770 );
771 assert_eq!(
772 action.elements[1].hit_boxes[0],
773 CollisionBox(16, -1, -24, -72)
774 );
775 assert_eq!(
776 action.elements[1].hit_boxes[1],
777 CollisionBox(-12, -84, 11, -61)
778 );
779 }
780}