1use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(untagged)]
25pub enum VarOr<T> {
26 Value(T),
28 Var(String),
30}
31
32impl<T: Default> Default for VarOr<T> {
33 fn default() -> Self {
34 VarOr::Value(T::default())
35 }
36}
37
38impl<T> VarOr<T> {
39 pub fn is_var(&self) -> bool {
41 matches!(self, VarOr::Var(_))
42 }
43
44 pub fn is_value(&self) -> bool {
46 matches!(self, VarOr::Value(_))
47 }
48
49 pub fn as_value(&self) -> Option<&T> {
51 match self {
52 VarOr::Value(v) => Some(v),
53 VarOr::Var(_) => None,
54 }
55 }
56
57 pub fn as_var(&self) -> Option<&str> {
59 match self {
60 VarOr::Value(_) => None,
61 VarOr::Var(s) => Some(s),
62 }
63 }
64}
65
66impl<T: Copy> VarOr<T> {
67 pub fn value(&self) -> Option<T> {
69 match self {
70 VarOr::Value(v) => Some(*v),
71 VarOr::Var(_) => None,
72 }
73 }
74}
75
76impl From<f64> for VarOr<f64> {
77 fn from(v: f64) -> Self {
78 VarOr::Value(v)
79 }
80}
81
82impl From<String> for VarOr<f64> {
83 fn from(s: String) -> Self {
84 VarOr::Var(s)
85 }
86}
87
88#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
103#[serde(untagged)]
104pub enum Duration {
105 Milliseconds(u32),
107 CssString(String),
109}
110
111impl Duration {
112 pub fn as_milliseconds(&self) -> Option<u32> {
116 match self {
117 Duration::Milliseconds(ms) => Some(*ms),
118 Duration::CssString(s) => parse_css_duration(s),
119 }
120 }
121}
122
123impl Default for Duration {
124 fn default() -> Self {
125 Duration::Milliseconds(100)
126 }
127}
128
129impl std::fmt::Display for Duration {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 match self {
132 Duration::Milliseconds(ms) => write!(f, "{}", ms),
133 Duration::CssString(s) => write!(f, "\"{}\"", s),
134 }
135 }
136}
137
138impl From<u32> for Duration {
139 fn from(ms: u32) -> Self {
140 Duration::Milliseconds(ms)
141 }
142}
143
144impl From<&str> for Duration {
145 fn from(s: &str) -> Self {
146 Duration::CssString(s.to_string())
147 }
148}
149
150fn parse_css_duration(s: &str) -> Option<u32> {
156 let s = s.trim().to_lowercase();
157
158 if let Some(ms_str) = s.strip_suffix("ms") {
159 ms_str.trim().parse::<f64>().ok().map(|v| v as u32)
160 } else if let Some(s_str) = s.strip_suffix('s') {
161 s_str.trim().parse::<f64>().ok().map(|v| (v * 1000.0) as u32)
162 } else {
163 s.parse::<f64>().ok().map(|v| v as u32)
165 }
166}
167
168#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
185pub struct CssKeyframe {
186 #[serde(skip_serializing_if = "Option::is_none", default)]
188 pub sprite: Option<String>,
189 #[serde(skip_serializing_if = "Option::is_none", default)]
191 pub transform: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none", default)]
194 pub opacity: Option<f64>,
195 #[serde(skip_serializing_if = "Option::is_none", default)]
197 pub offset: Option<[i32; 2]>,
198}
199
200#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
202pub struct Palette {
203 pub name: String,
204 pub colors: HashMap<String, String>,
205}
206
207#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
209#[serde(untagged)]
210pub enum PaletteRef {
211 Named(String),
212 Inline(HashMap<String, String>),
213}
214
215impl Default for PaletteRef {
216 fn default() -> Self {
217 PaletteRef::Named(String::new())
218 }
219}
220
221#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
226#[serde(untagged)]
227pub enum TransformSpec {
228 String(String),
230 Object {
232 op: String,
233 #[serde(flatten)]
234 params: HashMap<String, serde_json::Value>,
235 },
236}
237
238#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
244pub struct NineSlice {
245 pub left: u32,
247 pub right: u32,
249 pub top: u32,
251 pub bottom: u32,
253}
254
255#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
260pub struct Sprite {
261 pub name: String,
262 #[serde(skip_serializing_if = "Option::is_none", default)]
263 pub size: Option<[u32; 2]>,
264 pub palette: PaletteRef,
265 #[serde(default)]
267 pub grid: Vec<String>,
268 #[serde(skip_serializing_if = "Option::is_none", default)]
270 pub source: Option<String>,
271 #[serde(skip_serializing_if = "Option::is_none", default)]
273 pub transform: Option<Vec<TransformSpec>>,
274 #[serde(skip_serializing_if = "Option::is_none", default)]
276 pub metadata: Option<SpriteMetadata>,
277 #[serde(skip_serializing_if = "Option::is_none", default)]
279 pub nine_slice: Option<NineSlice>,
280}
281
282#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
288pub struct PaletteCycle {
289 pub tokens: Vec<String>,
291 #[serde(skip_serializing_if = "Option::is_none", default)]
293 pub duration: Option<u32>,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
298pub struct FrameTag {
299 pub start: u32,
301 pub end: u32,
303 #[serde(skip_serializing_if = "Option::is_none", default)]
305 pub r#loop: Option<bool>,
306 #[serde(skip_serializing_if = "Option::is_none", default)]
308 pub fps: Option<u32>,
309}
310
311#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
315pub struct CollisionBox {
316 pub x: i32,
318 pub y: i32,
320 pub w: u32,
322 pub h: u32,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
330pub struct SpriteMetadata {
331 #[serde(skip_serializing_if = "Option::is_none", default)]
333 pub origin: Option<[i32; 2]>,
334 #[serde(skip_serializing_if = "Option::is_none", default)]
336 pub boxes: Option<HashMap<String, CollisionBox>>,
337 #[serde(skip_serializing_if = "Option::is_none", default)]
339 pub attach_in: Option<[i32; 2]>,
340 #[serde(skip_serializing_if = "Option::is_none", default)]
342 pub attach_out: Option<[i32; 2]>,
343}
344
345#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
349pub struct FrameMetadata {
350 #[serde(skip_serializing_if = "Option::is_none", default)]
353 pub boxes: Option<HashMap<String, Option<CollisionBox>>>,
354}
355
356#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
358#[serde(rename_all = "lowercase")]
359pub enum FollowMode {
360 #[default]
362 Position,
363 Velocity,
365 Rotation,
367}
368
369#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
371pub struct AttachmentKeyframe {
372 pub offset: [i32; 2],
374}
375
376#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
381pub struct Attachment {
382 pub name: String,
384 pub anchor: [i32; 2],
386 pub chain: Vec<String>,
388 #[serde(skip_serializing_if = "Option::is_none", default)]
390 pub delay: Option<u32>,
391 #[serde(skip_serializing_if = "Option::is_none", default)]
393 pub follow: Option<FollowMode>,
394 #[serde(skip_serializing_if = "Option::is_none", default)]
396 pub damping: Option<f32>,
397 #[serde(skip_serializing_if = "Option::is_none", default)]
399 pub stiffness: Option<f32>,
400 #[serde(skip_serializing_if = "Option::is_none", default)]
402 pub z_index: Option<i32>,
403 #[serde(skip_serializing_if = "Option::is_none", default)]
405 pub keyframes: Option<HashMap<String, AttachmentKeyframe>>,
406}
407
408impl Attachment {
409 pub const DEFAULT_DELAY: u32 = 1;
411 pub const DEFAULT_DAMPING: f32 = 0.8;
413 pub const DEFAULT_STIFFNESS: f32 = 0.5;
415
416 pub fn delay(&self) -> u32 {
418 self.delay.unwrap_or(Self::DEFAULT_DELAY)
419 }
420
421 pub fn follow_mode(&self) -> FollowMode {
423 self.follow.clone().unwrap_or_default()
424 }
425
426 pub fn damping(&self) -> f32 {
428 self.damping.unwrap_or(Self::DEFAULT_DAMPING)
429 }
430
431 pub fn stiffness(&self) -> f32 {
433 self.stiffness.unwrap_or(Self::DEFAULT_STIFFNESS)
434 }
435
436 pub fn z_index(&self) -> i32 {
438 self.z_index.unwrap_or(0)
439 }
440
441 pub fn is_keyframed(&self) -> bool {
443 self.keyframes.is_some()
444 }
445}
446
447#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
455pub struct Animation {
456 pub name: String,
457 #[serde(default)]
459 pub frames: Vec<String>,
460 #[serde(skip_serializing_if = "Option::is_none", default)]
480 pub keyframes: Option<HashMap<String, CssKeyframe>>,
481 #[serde(skip_serializing_if = "Option::is_none", default)]
483 pub source: Option<String>,
484 #[serde(skip_serializing_if = "Option::is_none", default)]
486 pub transform: Option<Vec<TransformSpec>>,
487 #[serde(skip_serializing_if = "Option::is_none", default)]
490 pub duration: Option<Duration>,
491 #[serde(skip_serializing_if = "Option::is_none", default)]
494 pub timing_function: Option<String>,
495 #[serde(skip_serializing_if = "Option::is_none", default)]
496 pub r#loop: Option<bool>,
497 #[serde(skip_serializing_if = "Option::is_none", default)]
499 pub palette_cycle: Option<Vec<PaletteCycle>>,
500 #[serde(skip_serializing_if = "Option::is_none", default)]
502 pub tags: Option<HashMap<String, FrameTag>>,
503 #[serde(skip_serializing_if = "Option::is_none", default)]
505 pub frame_metadata: Option<Vec<FrameMetadata>>,
506 #[serde(skip_serializing_if = "Option::is_none", default)]
508 pub attachments: Option<Vec<Attachment>>,
509}
510
511#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
517pub struct Variant {
518 pub name: String,
519 pub base: String,
520 pub palette: HashMap<String, String>,
521 #[serde(skip_serializing_if = "Option::is_none", default)]
523 pub transform: Option<Vec<TransformSpec>>,
524}
525
526impl Animation {
527 pub const DEFAULT_DURATION_MS: u32 = 100;
529
530 pub fn duration_ms(&self) -> u32 {
535 self.duration
536 .as_ref()
537 .and_then(|d| d.as_milliseconds())
538 .unwrap_or(Self::DEFAULT_DURATION_MS)
539 }
540
541 pub fn loops(&self) -> bool {
543 self.r#loop.unwrap_or(true)
544 }
545
546 pub fn is_css_keyframes(&self) -> bool {
548 self.keyframes.is_some() && !self.keyframes.as_ref().unwrap().is_empty()
549 }
550
551 pub fn is_frame_based(&self) -> bool {
553 !self.frames.is_empty()
554 }
555
556 pub fn css_keyframes(&self) -> Option<&HashMap<String, CssKeyframe>> {
558 self.keyframes.as_ref()
559 }
560
561 pub fn has_palette_cycle(&self) -> bool {
563 self.palette_cycle.as_ref().map(|cycles| !cycles.is_empty()).unwrap_or(false)
564 }
565
566 pub fn palette_cycles(&self) -> &[PaletteCycle] {
568 self.palette_cycle.as_deref().unwrap_or(&[])
569 }
570
571 pub fn has_attachments(&self) -> bool {
573 self.attachments.as_ref().map(|a| !a.is_empty()).unwrap_or(false)
574 }
575
576 pub fn attachments(&self) -> &[Attachment] {
578 self.attachments.as_deref().unwrap_or(&[])
579 }
580
581 pub fn parse_keyframe_percent(key: &str) -> Option<f64> {
589 let key = key.trim().to_lowercase();
590
591 match key.as_str() {
592 "from" => Some(0.0),
593 "to" => Some(1.0),
594 _ => {
595 if let Some(pct_str) = key.strip_suffix('%') {
596 pct_str.trim().parse::<f64>().ok().map(|v| (v / 100.0).clamp(0.0, 1.0))
597 } else {
598 None
599 }
600 }
601 }
602 }
603
604 pub fn sorted_keyframes(&self) -> Vec<(f64, &CssKeyframe)> {
608 let Some(keyframes) = &self.keyframes else {
609 return vec![];
610 };
611
612 let mut entries: Vec<_> = keyframes
613 .iter()
614 .filter_map(|(key, kf)| Self::parse_keyframe_percent(key).map(|pct| (pct, kf)))
615 .collect();
616
617 entries.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap_or(std::cmp::Ordering::Equal));
618 entries
619 }
620}
621
622impl PaletteCycle {
623 pub fn duration_ms(&self, default: u32) -> u32 {
626 self.duration.unwrap_or(default)
627 }
628
629 pub fn cycle_length(&self) -> usize {
631 self.tokens.len()
632 }
633}
634
635#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
637pub struct CompositionLayer {
638 #[serde(skip_serializing_if = "Option::is_none", default)]
639 pub name: Option<String>,
640 #[serde(skip_serializing_if = "Option::is_none", default)]
641 pub fill: Option<String>,
642 #[serde(skip_serializing_if = "Option::is_none", default)]
643 pub map: Option<Vec<String>>,
644 #[serde(skip_serializing_if = "Option::is_none", default)]
646 pub transform: Option<Vec<TransformSpec>>,
647 #[serde(skip_serializing_if = "Option::is_none", default)]
650 pub blend: Option<String>,
651 #[serde(skip_serializing_if = "Option::is_none", default)]
655 pub opacity: Option<VarOr<f64>>,
656}
657
658#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
660pub struct Composition {
661 pub name: String,
662 #[serde(skip_serializing_if = "Option::is_none", default)]
663 pub base: Option<String>,
664 #[serde(skip_serializing_if = "Option::is_none", default)]
665 pub size: Option<[u32; 2]>,
666 #[serde(skip_serializing_if = "Option::is_none", default)]
667 pub cell_size: Option<[u32; 2]>,
668 pub sprites: HashMap<String, Option<String>>,
669 pub layers: Vec<CompositionLayer>,
670}
671
672impl Composition {
673 pub const DEFAULT_CELL_SIZE: [u32; 2] = [1, 1];
675
676 pub fn cell_size(&self) -> [u32; 2] {
678 self.cell_size.unwrap_or(Self::DEFAULT_CELL_SIZE)
679 }
680}
681
682#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
684pub struct VelocityRange {
685 pub x: [f64; 2],
687 pub y: [f64; 2],
689}
690
691impl Default for VelocityRange {
692 fn default() -> Self {
693 Self { x: [0.0, 0.0], y: [0.0, 0.0] }
694 }
695}
696
697#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
699pub struct ParticleEmitter {
700 #[serde(default = "default_rate")]
702 pub rate: f64,
703 #[serde(default = "default_lifetime")]
705 pub lifetime: [u32; 2],
706 #[serde(skip_serializing_if = "Option::is_none", default)]
708 pub velocity: Option<VelocityRange>,
709 #[serde(skip_serializing_if = "Option::is_none", default)]
711 pub gravity: Option<f64>,
712 #[serde(skip_serializing_if = "Option::is_none", default)]
714 pub fade: Option<bool>,
715 #[serde(skip_serializing_if = "Option::is_none", default)]
717 pub rotation: Option<[f64; 2]>,
718 #[serde(skip_serializing_if = "Option::is_none", default)]
720 pub seed: Option<u64>,
721}
722
723fn default_rate() -> f64 {
724 1.0
725}
726
727fn default_lifetime() -> [u32; 2] {
728 [10, 20]
729}
730
731impl Default for ParticleEmitter {
732 fn default() -> Self {
733 Self {
734 rate: default_rate(),
735 lifetime: default_lifetime(),
736 velocity: None,
737 gravity: None,
738 fade: None,
739 rotation: None,
740 seed: None,
741 }
742 }
743}
744
745#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
750pub struct Particle {
751 pub name: String,
753 pub sprite: String,
755 pub emitter: ParticleEmitter,
757}
758
759#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
763#[serde(rename_all = "kebab-case")]
764pub enum Easing {
765 #[default]
767 Linear,
768 EaseIn,
770 EaseOut,
772 EaseInOut,
774 Bounce,
776 Elastic,
778}
779
780impl Easing {
781 pub fn apply(&self, t: f64) -> f64 {
785 let t = t.clamp(0.0, 1.0);
786 match self {
787 Easing::Linear => t,
788 Easing::EaseIn => t * t,
789 Easing::EaseOut => 1.0 - (1.0 - t).powi(2),
790 Easing::EaseInOut => {
791 if t < 0.5 {
792 2.0 * t * t
793 } else {
794 1.0 - (-2.0 * t + 2.0).powi(2) / 2.0
795 }
796 }
797 Easing::Bounce => {
798 let n1 = 7.5625;
799 let d1 = 2.75;
800 let t = 1.0 - t;
801 let bounce = if t < 1.0 / d1 {
802 n1 * t * t
803 } else if t < 2.0 / d1 {
804 let t = t - 1.5 / d1;
805 n1 * t * t + 0.75
806 } else if t < 2.5 / d1 {
807 let t = t - 2.25 / d1;
808 n1 * t * t + 0.9375
809 } else {
810 let t = t - 2.625 / d1;
811 n1 * t * t + 0.984375
812 };
813 1.0 - bounce
814 }
815 Easing::Elastic => {
816 if t == 0.0 {
817 0.0
818 } else if t == 1.0 {
819 1.0
820 } else {
821 let c4 = (2.0 * std::f64::consts::PI) / 3.0;
822 2.0_f64.powf(-10.0 * t) * ((t * 10.0 - 0.75) * c4).sin() + 1.0
823 }
824 }
825 }
826 }
827
828 pub fn from_str(s: &str) -> Option<Easing> {
830 match s.to_lowercase().replace('_', "-").as_str() {
831 "linear" => Some(Easing::Linear),
832 "ease-in" | "easein" => Some(Easing::EaseIn),
833 "ease-out" | "easeout" => Some(Easing::EaseOut),
834 "ease-in-out" | "easeinout" => Some(Easing::EaseInOut),
835 "bounce" => Some(Easing::Bounce),
836 "elastic" => Some(Easing::Elastic),
837 _ => None,
838 }
839 }
840}
841
842#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
844pub struct Keyframe {
845 pub frame: u32,
847 #[serde(flatten)]
849 pub values: HashMap<String, f64>,
850}
851
852#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
854pub struct PropertyKeyframes {
855 #[serde(skip_serializing_if = "Option::is_none", default)]
859 pub expr: Option<String>,
860 #[serde(skip_serializing_if = "Option::is_none", default)]
862 pub keyframes: Option<Vec<[f64; 2]>>,
863 #[serde(skip_serializing_if = "Option::is_none", default)]
865 pub easing: Option<Easing>,
866}
867
868#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
870#[serde(untagged)]
871pub enum KeyframeSpec {
872 Array(Vec<Keyframe>),
874 Properties(HashMap<String, PropertyKeyframes>),
876}
877
878#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
921pub struct TransformDef {
922 pub name: String,
924 #[serde(skip_serializing_if = "Option::is_none", default)]
926 pub params: Option<Vec<String>>,
927 #[serde(skip_serializing_if = "Option::is_none", default)]
929 pub ops: Option<Vec<TransformSpec>>,
930 #[serde(skip_serializing_if = "Option::is_none", default)]
932 pub compose: Option<Vec<TransformSpec>>,
933 #[serde(skip_serializing_if = "Option::is_none", default)]
935 pub cycle: Option<Vec<Vec<TransformSpec>>>,
936 #[serde(skip_serializing_if = "Option::is_none", default)]
938 pub frames: Option<u32>,
939 #[serde(skip_serializing_if = "Option::is_none", default)]
941 pub keyframes: Option<KeyframeSpec>,
942 #[serde(skip_serializing_if = "Option::is_none", default)]
944 pub easing: Option<Easing>,
945}
946
947impl TransformDef {
948 pub fn is_simple(&self) -> bool {
950 self.ops.is_some()
951 && self.compose.is_none()
952 && self.cycle.is_none()
953 && self.keyframes.is_none()
954 }
955
956 pub fn generates_animation(&self) -> bool {
958 self.frames.is_some() && self.keyframes.is_some()
959 }
960
961 pub fn is_parameterized(&self) -> bool {
963 self.params.as_ref().map(|p| !p.is_empty()).unwrap_or(false)
964 }
965
966 pub fn is_cycling(&self) -> bool {
968 self.cycle.as_ref().map(|c| !c.is_empty()).unwrap_or(false)
969 }
970}
971
972#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
974#[serde(tag = "type", rename_all = "lowercase")]
975pub enum TtpObject {
976 Palette(Palette),
977 Sprite(Sprite),
978 Variant(Variant),
979 Composition(Composition),
980 Animation(Animation),
981 Particle(Particle),
982 Transform(TransformDef),
983}
984
985#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
987pub struct Warning {
988 pub message: String,
989 pub line: usize,
990}
991
992#[cfg(test)]
993mod tests {
994 use super::*;
995
996 #[test]
997 fn test_palette_roundtrip() {
998 let palette = Palette {
999 name: "mono".to_string(),
1000 colors: HashMap::from([
1001 ("{_}".to_string(), "#00000000".to_string()),
1002 ("{on}".to_string(), "#FFFFFF".to_string()),
1003 ]),
1004 };
1005 let json = serde_json::to_string(&palette).unwrap();
1006 let parsed: Palette = serde_json::from_str(&json).unwrap();
1007 assert_eq!(palette, parsed);
1008 }
1009
1010 #[test]
1011 fn test_sprite_with_inline_palette_roundtrip() {
1012 let sprite = Sprite {
1013 name: "dot".to_string(),
1014 size: None,
1015 palette: PaletteRef::Inline(HashMap::from([
1016 ("{_}".to_string(), "#00000000".to_string()),
1017 ("{x}".to_string(), "#FF0000".to_string()),
1018 ])),
1019 grid: vec!["{x}".to_string()],
1020 metadata: None,
1021 ..Default::default()
1022 };
1023 let json = serde_json::to_string(&sprite).unwrap();
1024 let parsed: Sprite = serde_json::from_str(&json).unwrap();
1025 assert_eq!(sprite, parsed);
1026 }
1027
1028 #[test]
1029 fn test_sprite_with_named_palette_roundtrip() {
1030 let sprite = Sprite {
1031 name: "checker".to_string(),
1032 size: Some([4, 4]),
1033 palette: PaletteRef::Named("mono".to_string()),
1034 grid: vec!["{on}{off}{on}{off}".to_string(), "{off}{on}{off}{on}".to_string()],
1035 metadata: None,
1036 ..Default::default()
1037 };
1038 let json = serde_json::to_string(&sprite).unwrap();
1039 let parsed: Sprite = serde_json::from_str(&json).unwrap();
1040 assert_eq!(sprite, parsed);
1041 }
1042
1043 #[test]
1044 fn test_ttp_object_palette_roundtrip() {
1045 let obj = TtpObject::Palette(Palette {
1046 name: "test".to_string(),
1047 colors: HashMap::from([("{a}".to_string(), "#FF0000".to_string())]),
1048 });
1049 let json = serde_json::to_string(&obj).unwrap();
1050 assert!(json.contains(r#""type":"palette""#));
1051 let parsed: TtpObject = serde_json::from_str(&json).unwrap();
1052 assert_eq!(obj, parsed);
1053 }
1054
1055 #[test]
1056 fn test_ttp_object_sprite_roundtrip() {
1057 let obj = TtpObject::Sprite(Sprite {
1058 name: "test".to_string(),
1059 size: None,
1060 palette: PaletteRef::Named("colors".to_string()),
1061 grid: vec!["{a}{b}".to_string()],
1062 metadata: None,
1063 ..Default::default()
1064 });
1065 let json = serde_json::to_string(&obj).unwrap();
1066 assert!(json.contains(r#""type":"sprite""#));
1067 let parsed: TtpObject = serde_json::from_str(&json).unwrap();
1068 assert_eq!(obj, parsed);
1069 }
1070
1071 #[test]
1072 fn test_warning_roundtrip() {
1073 let warning = Warning { message: "Row 1 has 3 tokens, expected 4".to_string(), line: 5 };
1074 let json = serde_json::to_string(&warning).unwrap();
1075 let parsed: Warning = serde_json::from_str(&json).unwrap();
1076 assert_eq!(warning, parsed);
1077 }
1078
1079 #[test]
1080 fn test_minimal_dot_fixture() {
1081 let json = r##"{"type": "sprite", "name": "dot", "palette": {"{_}": "#00000000", "{x}": "#FF0000"}, "grid": ["{x}"]}"##;
1083 let obj: TtpObject = serde_json::from_str(json).unwrap();
1084 match obj {
1085 TtpObject::Sprite(sprite) => {
1086 assert_eq!(sprite.name, "dot");
1087 assert!(sprite.size.is_none());
1088 assert_eq!(sprite.grid, vec!["{x}"]);
1089 match sprite.palette {
1090 PaletteRef::Inline(colors) => {
1091 assert_eq!(colors.get("{x}"), Some(&"#FF0000".to_string()));
1092 }
1093 _ => panic!("Expected inline palette"),
1094 }
1095 }
1096 _ => panic!("Expected sprite"),
1097 }
1098 }
1099
1100 #[test]
1101 fn test_named_palette_fixture() {
1102 let json = r##"{"type": "palette", "name": "mono", "colors": {"{_}": "#00000000", "{on}": "#FFFFFF", "{off}": "#000000"}}"##;
1104 let obj: TtpObject = serde_json::from_str(json).unwrap();
1105 match obj {
1106 TtpObject::Palette(palette) => {
1107 assert_eq!(palette.name, "mono");
1108 assert_eq!(palette.colors.len(), 3);
1109 assert_eq!(palette.colors.get("{on}"), Some(&"#FFFFFF".to_string()));
1110 }
1111 _ => panic!("Expected palette"),
1112 }
1113
1114 let json = r#"{"type": "sprite", "name": "checker", "palette": "mono", "grid": ["{on}{off}{on}{off}", "{off}{on}{off}{on}"]}"#;
1116 let obj: TtpObject = serde_json::from_str(json).unwrap();
1117 match obj {
1118 TtpObject::Sprite(sprite) => {
1119 assert_eq!(sprite.name, "checker");
1120 match sprite.palette {
1121 PaletteRef::Named(name) => assert_eq!(name, "mono"),
1122 _ => panic!("Expected named palette reference"),
1123 }
1124 }
1125 _ => panic!("Expected sprite"),
1126 }
1127 }
1128
1129 #[test]
1130 fn test_composition_basic_parse() {
1131 let json = r#"{"type": "composition", "name": "test_comp", "sprites": {".": null, "X": "sprite_x"}, "layers": [{"name": "layer1", "map": ["X."]}]}"#;
1132 let obj: TtpObject = serde_json::from_str(json).unwrap();
1133 match obj {
1134 TtpObject::Composition(comp) => {
1135 assert_eq!(comp.name, "test_comp");
1136 assert!(comp.base.is_none());
1137 assert!(comp.size.is_none());
1138 assert!(comp.cell_size.is_none());
1139 assert_eq!(comp.sprites.len(), 2);
1140 assert_eq!(comp.sprites.get("."), Some(&None));
1141 assert_eq!(comp.sprites.get("X"), Some(&Some("sprite_x".to_string())));
1142 assert_eq!(comp.layers.len(), 1);
1143 assert_eq!(comp.layers[0].name, Some("layer1".to_string()));
1144 assert_eq!(comp.layers[0].map, Some(vec!["X.".to_string()]));
1145 }
1146 _ => panic!("Expected composition"),
1147 }
1148 }
1149
1150 #[test]
1151 fn test_composition_with_all_fields() {
1152 let json = r#"{"type": "composition", "name": "full_comp", "base": "hero_base", "size": [64, 64], "cell_size": [8, 8], "sprites": {".": null, "H": "hat"}, "layers": [{"name": "gear", "fill": "H", "map": ["H.", ".H"]}]}"#;
1153 let obj: TtpObject = serde_json::from_str(json).unwrap();
1154 match obj {
1155 TtpObject::Composition(comp) => {
1156 assert_eq!(comp.name, "full_comp");
1157 assert_eq!(comp.base, Some("hero_base".to_string()));
1158 assert_eq!(comp.size, Some([64, 64]));
1159 assert_eq!(comp.cell_size, Some([8, 8]));
1160 assert_eq!(comp.layers[0].fill, Some("H".to_string()));
1161 }
1162 _ => panic!("Expected composition"),
1163 }
1164 }
1165
1166 #[test]
1167 fn test_composition_roundtrip() {
1168 let comp = Composition {
1169 name: "roundtrip_test".to_string(),
1170 base: Some("base_sprite".to_string()),
1171 size: Some([32, 32]),
1172 cell_size: Some([4, 4]),
1173 sprites: HashMap::from([
1174 (".".to_string(), None),
1175 ("A".to_string(), Some("sprite_a".to_string())),
1176 ]),
1177 layers: vec![CompositionLayer {
1178 name: Some("layer1".to_string()),
1179 fill: None,
1180 map: Some(vec!["A.".to_string(), ".A".to_string()]),
1181 ..Default::default()
1182 }],
1183 };
1184 let obj = TtpObject::Composition(comp.clone());
1185 let json = serde_json::to_string(&obj).unwrap();
1186 assert!(json.contains(r#""type":"composition""#));
1187 let parsed: TtpObject = serde_json::from_str(&json).unwrap();
1188 match parsed {
1189 TtpObject::Composition(parsed_comp) => {
1190 assert_eq!(comp, parsed_comp);
1191 }
1192 _ => panic!("Expected composition"),
1193 }
1194 }
1195
1196 #[test]
1197 fn test_composition_default_cell_size() {
1198 let json =
1200 r#"{"type": "composition", "name": "no_cell_size", "sprites": {}, "layers": []}"#;
1201 let obj: TtpObject = serde_json::from_str(json).unwrap();
1202 match obj {
1203 TtpObject::Composition(comp) => {
1204 assert!(comp.cell_size.is_none());
1205 assert_eq!(comp.cell_size(), [1, 1]);
1207 }
1208 _ => panic!("Expected composition"),
1209 }
1210 }
1211
1212 #[test]
1213 fn test_composition_cell_size_helper() {
1214 let comp = Composition {
1216 name: "test".to_string(),
1217 base: None,
1218 size: None,
1219 cell_size: Some([8, 8]),
1220 sprites: HashMap::new(),
1221 layers: vec![],
1222 };
1223 assert_eq!(comp.cell_size(), [8, 8]);
1224
1225 let comp_default = Composition {
1227 name: "test_default".to_string(),
1228 base: None,
1229 size: None,
1230 cell_size: None,
1231 sprites: HashMap::new(),
1232 layers: vec![],
1233 };
1234 assert_eq!(comp_default.cell_size(), Composition::DEFAULT_CELL_SIZE);
1235 assert_eq!(comp_default.cell_size(), [1, 1]);
1236 }
1237
1238 #[test]
1239 fn test_animation_parse_full() {
1240 let json = r#"{"type": "animation", "name": "blink_anim", "frames": ["on", "off"], "duration": 500, "loop": true}"#;
1242 let obj: TtpObject = serde_json::from_str(json).unwrap();
1243 match obj {
1244 TtpObject::Animation(anim) => {
1245 assert_eq!(anim.name, "blink_anim");
1246 assert_eq!(anim.frames, vec!["on", "off"]);
1247 assert_eq!(anim.duration, Some(Duration::Milliseconds(500)));
1248 assert_eq!(anim.r#loop, Some(true));
1249 assert_eq!(anim.duration_ms(), 500);
1251 assert!(anim.loops());
1252 }
1253 _ => panic!("Expected animation"),
1254 }
1255 }
1256
1257 #[test]
1258 fn test_animation_default_duration() {
1259 let json = r#"{"type": "animation", "name": "walk", "frames": ["frame1", "frame2"]}"#;
1261 let obj: TtpObject = serde_json::from_str(json).unwrap();
1262 match obj {
1263 TtpObject::Animation(anim) => {
1264 assert_eq!(anim.name, "walk");
1265 assert!(anim.duration.is_none());
1266 assert_eq!(anim.duration_ms(), 100); }
1268 _ => panic!("Expected animation"),
1269 }
1270 }
1271
1272 #[test]
1273 fn test_animation_default_loop() {
1274 let json = r#"{"type": "animation", "name": "idle", "frames": ["f1"]}"#;
1276 let obj: TtpObject = serde_json::from_str(json).unwrap();
1277 match obj {
1278 TtpObject::Animation(anim) => {
1279 assert!(anim.r#loop.is_none());
1280 assert!(anim.loops()); }
1282 _ => panic!("Expected animation"),
1283 }
1284 }
1285
1286 #[test]
1287 fn test_animation_loop_false() {
1288 let json =
1290 r#"{"type": "animation", "name": "death", "frames": ["f1", "f2"], "loop": false}"#;
1291 let obj: TtpObject = serde_json::from_str(json).unwrap();
1292 match obj {
1293 TtpObject::Animation(anim) => {
1294 assert_eq!(anim.r#loop, Some(false));
1295 assert!(!anim.loops());
1296 }
1297 _ => panic!("Expected animation"),
1298 }
1299 }
1300
1301 #[test]
1302 fn test_animation_roundtrip() {
1303 let anim = Animation {
1304 name: "test_anim".to_string(),
1305 frames: vec!["a".to_string(), "b".to_string(), "c".to_string()],
1306 duration: Some(Duration::Milliseconds(200)),
1307 r#loop: Some(false),
1308 palette_cycle: None,
1309 tags: None,
1310 frame_metadata: None,
1311 attachments: None,
1312 ..Default::default()
1313 };
1314 let obj = TtpObject::Animation(anim.clone());
1315 let json = serde_json::to_string(&obj).unwrap();
1316 assert!(json.contains(r#""type":"animation""#));
1317 let parsed: TtpObject = serde_json::from_str(&json).unwrap();
1318 match parsed {
1319 TtpObject::Animation(parsed_anim) => {
1320 assert_eq!(anim, parsed_anim);
1321 }
1322 _ => panic!("Expected animation"),
1323 }
1324 }
1325
1326 #[test]
1327 fn test_animation_with_palette_cycle() {
1328 let json = r#"{"type": "animation", "name": "water", "frames": ["water_tile"], "duration": 100, "palette_cycle": [{"tokens": ["{w1}", "{w2}", "{w3}"], "duration": 150}]}"#;
1330 let obj: TtpObject = serde_json::from_str(json).unwrap();
1331 match obj {
1332 TtpObject::Animation(anim) => {
1333 assert_eq!(anim.name, "water");
1334 assert!(anim.has_palette_cycle());
1335 let cycles = anim.palette_cycles();
1336 assert_eq!(cycles.len(), 1);
1337 assert_eq!(cycles[0].tokens, vec!["{w1}", "{w2}", "{w3}"]);
1338 assert_eq!(cycles[0].duration, Some(150));
1339 assert_eq!(cycles[0].duration_ms(100), 150);
1340 assert_eq!(cycles[0].cycle_length(), 3);
1341 }
1342 _ => panic!("Expected animation"),
1343 }
1344 }
1345
1346 #[test]
1347 fn test_animation_palette_cycle_default_duration() {
1348 let json = r#"{"type": "animation", "name": "fire", "frames": ["flame"], "duration": 80, "palette_cycle": [{"tokens": ["{f1}", "{f2}"]}]}"#;
1350 let obj: TtpObject = serde_json::from_str(json).unwrap();
1351 match obj {
1352 TtpObject::Animation(anim) => {
1353 let cycles = anim.palette_cycles();
1354 assert_eq!(cycles.len(), 1);
1355 assert!(cycles[0].duration.is_none());
1356 assert_eq!(cycles[0].duration_ms(anim.duration_ms()), 80);
1358 }
1359 _ => panic!("Expected animation"),
1360 }
1361 }
1362
1363 #[test]
1364 fn test_animation_multiple_palette_cycles() {
1365 let json = r#"{"type": "animation", "name": "scene", "frames": ["scene_frame"], "palette_cycle": [{"tokens": ["{water1}", "{water2}"], "duration": 200}, {"tokens": ["{fire1}", "{fire2}", "{fire3}"], "duration": 100}]}"#;
1367 let obj: TtpObject = serde_json::from_str(json).unwrap();
1368 match obj {
1369 TtpObject::Animation(anim) => {
1370 let cycles = anim.palette_cycles();
1371 assert_eq!(cycles.len(), 2);
1372 assert_eq!(cycles[0].tokens.len(), 2);
1374 assert_eq!(cycles[0].duration, Some(200));
1375 assert_eq!(cycles[1].tokens.len(), 3);
1377 assert_eq!(cycles[1].duration, Some(100));
1378 }
1379 _ => panic!("Expected animation"),
1380 }
1381 }
1382
1383 #[test]
1384 fn test_animation_palette_cycle_roundtrip() {
1385 let anim = Animation {
1386 name: "cycle_test".to_string(),
1387 frames: vec!["sprite".to_string()],
1388 duration: Some(Duration::Milliseconds(100)),
1389 r#loop: Some(true),
1390 palette_cycle: Some(vec![PaletteCycle {
1391 tokens: vec!["{a}".to_string(), "{b}".to_string()],
1392 duration: Some(150),
1393 }]),
1394 tags: None,
1395 frame_metadata: None,
1396 attachments: None,
1397 ..Default::default()
1398 };
1399 let obj = TtpObject::Animation(anim.clone());
1400 let json = serde_json::to_string(&obj).unwrap();
1401 assert!(json.contains("palette_cycle"));
1402 let parsed: TtpObject = serde_json::from_str(&json).unwrap();
1403 match parsed {
1404 TtpObject::Animation(parsed_anim) => {
1405 assert_eq!(anim, parsed_anim);
1406 }
1407 _ => panic!("Expected animation"),
1408 }
1409 }
1410
1411 #[test]
1412 fn test_animation_no_palette_cycle() {
1413 let anim = Animation {
1415 name: "normal".to_string(),
1416 frames: vec!["f1".to_string()],
1417 duration: None,
1418 r#loop: None,
1419 palette_cycle: None,
1420 tags: None,
1421 frame_metadata: None,
1422 attachments: None,
1423 ..Default::default()
1424 };
1425 assert!(!anim.has_palette_cycle());
1426 assert!(anim.palette_cycles().is_empty());
1427 }
1428
1429 #[test]
1430 fn test_variant_parse_basic() {
1431 let json = r##"{"type": "variant", "name": "hero_red", "base": "hero", "palette": {"{skin}": "#FF0000"}}"##;
1433 let obj: TtpObject = serde_json::from_str(json).unwrap();
1434 match obj {
1435 TtpObject::Variant(variant) => {
1436 assert_eq!(variant.name, "hero_red");
1437 assert_eq!(variant.base, "hero");
1438 assert_eq!(variant.palette.len(), 1);
1439 assert_eq!(variant.palette.get("{skin}"), Some(&"#FF0000".to_string()));
1440 }
1441 _ => panic!("Expected variant"),
1442 }
1443 }
1444
1445 #[test]
1446 fn test_variant_parse_multiple_overrides() {
1447 let json = r##"{"type": "variant", "name": "hero_alt", "base": "hero", "palette": {"{skin}": "#00FF00", "{hair}": "#0000FF", "{eyes}": "#FFFF00"}}"##;
1449 let obj: TtpObject = serde_json::from_str(json).unwrap();
1450 match obj {
1451 TtpObject::Variant(variant) => {
1452 assert_eq!(variant.name, "hero_alt");
1453 assert_eq!(variant.base, "hero");
1454 assert_eq!(variant.palette.len(), 3);
1455 assert_eq!(variant.palette.get("{skin}"), Some(&"#00FF00".to_string()));
1456 assert_eq!(variant.palette.get("{hair}"), Some(&"#0000FF".to_string()));
1457 assert_eq!(variant.palette.get("{eyes}"), Some(&"#FFFF00".to_string()));
1458 }
1459 _ => panic!("Expected variant"),
1460 }
1461 }
1462
1463 #[test]
1464 fn test_variant_roundtrip() {
1465 let variant = Variant {
1466 name: "test_variant".to_string(),
1467 base: "base_sprite".to_string(),
1468 palette: HashMap::from([
1469 ("{a}".to_string(), "#FF0000".to_string()),
1470 ("{b}".to_string(), "#00FF00".to_string()),
1471 ]),
1472 ..Default::default()
1473 };
1474 let obj = TtpObject::Variant(variant.clone());
1475 let json = serde_json::to_string(&obj).unwrap();
1476 assert!(json.contains(r##""type":"variant""##));
1477 let parsed: TtpObject = serde_json::from_str(&json).unwrap();
1478 match parsed {
1479 TtpObject::Variant(parsed_variant) => {
1480 assert_eq!(variant, parsed_variant);
1481 }
1482 _ => panic!("Expected variant"),
1483 }
1484 }
1485
1486 #[test]
1487 fn test_variant_empty_palette() {
1488 let json = r#"{"type": "variant", "name": "hero_copy", "base": "hero", "palette": {}}"#;
1490 let obj: TtpObject = serde_json::from_str(json).unwrap();
1491 match obj {
1492 TtpObject::Variant(variant) => {
1493 assert_eq!(variant.name, "hero_copy");
1494 assert_eq!(variant.base, "hero");
1495 assert!(variant.palette.is_empty());
1496 }
1497 _ => panic!("Expected variant"),
1498 }
1499 }
1500
1501 #[test]
1506 fn test_particle_parse_basic() {
1507 let json = r#"{
1508 "type": "particle",
1509 "name": "sparkle",
1510 "sprite": "spark",
1511 "emitter": {
1512 "rate": 5,
1513 "lifetime": [10, 20]
1514 }
1515 }"#;
1516 let obj: TtpObject = serde_json::from_str(json).unwrap();
1517 match obj {
1518 TtpObject::Particle(p) => {
1519 assert_eq!(p.name, "sparkle");
1520 assert_eq!(p.sprite, "spark");
1521 assert_eq!(p.emitter.rate, 5.0);
1522 assert_eq!(p.emitter.lifetime, [10, 20]);
1523 }
1524 _ => panic!("Expected particle"),
1525 }
1526 }
1527
1528 #[test]
1529 fn test_particle_parse_full() {
1530 let json = r#"{
1531 "type": "particle",
1532 "name": "rain",
1533 "sprite": "raindrop",
1534 "emitter": {
1535 "rate": 10,
1536 "lifetime": [30, 60],
1537 "velocity": {"x": [-1, 1], "y": [5, 8]},
1538 "gravity": 0.5,
1539 "fade": true,
1540 "rotation": [0, 360],
1541 "seed": 12345
1542 }
1543 }"#;
1544 let obj: TtpObject = serde_json::from_str(json).unwrap();
1545 match obj {
1546 TtpObject::Particle(p) => {
1547 assert_eq!(p.name, "rain");
1548 assert_eq!(p.sprite, "raindrop");
1549 assert_eq!(p.emitter.rate, 10.0);
1550 assert_eq!(p.emitter.lifetime, [30, 60]);
1551 let vel = p.emitter.velocity.unwrap();
1552 assert_eq!(vel.x, [-1.0, 1.0]);
1553 assert_eq!(vel.y, [5.0, 8.0]);
1554 assert_eq!(p.emitter.gravity, Some(0.5));
1555 assert_eq!(p.emitter.fade, Some(true));
1556 assert_eq!(p.emitter.rotation, Some([0.0, 360.0]));
1557 assert_eq!(p.emitter.seed, Some(12345));
1558 }
1559 _ => panic!("Expected particle"),
1560 }
1561 }
1562
1563 #[test]
1564 fn test_particle_roundtrip() {
1565 let particle = Particle {
1566 name: "dust".to_string(),
1567 sprite: "dust_mote".to_string(),
1568 emitter: ParticleEmitter {
1569 rate: 2.0,
1570 lifetime: [5, 15],
1571 velocity: Some(VelocityRange { x: [-2.0, 2.0], y: [-1.0, 0.0] }),
1572 gravity: Some(0.1),
1573 fade: Some(true),
1574 rotation: None,
1575 seed: Some(42),
1576 },
1577 };
1578 let obj = TtpObject::Particle(particle.clone());
1579 let json = serde_json::to_string(&obj).unwrap();
1580 assert!(json.contains(r#""type":"particle""#));
1581 let parsed: TtpObject = serde_json::from_str(&json).unwrap();
1582 match parsed {
1583 TtpObject::Particle(parsed_particle) => {
1584 assert_eq!(particle, parsed_particle);
1585 }
1586 _ => panic!("Expected particle"),
1587 }
1588 }
1589
1590 #[test]
1591 fn test_particle_emitter_defaults() {
1592 let json = r#"{
1594 "type": "particle",
1595 "name": "minimal",
1596 "sprite": "dot",
1597 "emitter": {}
1598 }"#;
1599 let obj: TtpObject = serde_json::from_str(json).unwrap();
1600 match obj {
1601 TtpObject::Particle(p) => {
1602 assert_eq!(p.emitter.rate, 1.0); assert_eq!(p.emitter.lifetime, [10, 20]); assert!(p.emitter.velocity.is_none());
1605 assert!(p.emitter.gravity.is_none());
1606 assert!(p.emitter.fade.is_none());
1607 assert!(p.emitter.rotation.is_none());
1608 assert!(p.emitter.seed.is_none());
1609 }
1610 _ => panic!("Expected particle"),
1611 }
1612 }
1613
1614 #[test]
1617 fn test_collision_box_roundtrip() {
1618 let box_data = CollisionBox { x: 4, y: 0, w: 24, h: 32 };
1619 let json = serde_json::to_string(&box_data).unwrap();
1620 let parsed: CollisionBox = serde_json::from_str(&json).unwrap();
1621 assert_eq!(box_data, parsed);
1622 }
1623
1624 #[test]
1625 fn test_sprite_metadata_roundtrip() {
1626 let metadata = SpriteMetadata {
1627 origin: Some([16, 32]),
1628 boxes: Some(HashMap::from([
1629 ("hurt".to_string(), CollisionBox { x: 4, y: 0, w: 24, h: 32 }),
1630 ("hit".to_string(), CollisionBox { x: 20, y: 8, w: 20, h: 16 }),
1631 ])),
1632 attach_in: None,
1633 attach_out: None,
1634 };
1635 let json = serde_json::to_string(&metadata).unwrap();
1636 let parsed: SpriteMetadata = serde_json::from_str(&json).unwrap();
1637 assert_eq!(metadata, parsed);
1638 }
1639
1640 #[test]
1641 fn test_sprite_with_metadata_parse() {
1642 let json = r#"{
1644 "type": "sprite",
1645 "name": "player_attack",
1646 "palette": "characters",
1647 "grid": ["{x}"],
1648 "metadata": {
1649 "origin": [16, 32],
1650 "boxes": {
1651 "hurt": {"x": 4, "y": 0, "w": 24, "h": 32},
1652 "hit": {"x": 20, "y": 8, "w": 20, "h": 16}
1653 }
1654 }
1655 }"#;
1656 let obj: TtpObject = serde_json::from_str(json).unwrap();
1657 match obj {
1658 TtpObject::Sprite(sprite) => {
1659 assert_eq!(sprite.name, "player_attack");
1660 assert!(sprite.metadata.is_some());
1661 let meta = sprite.metadata.unwrap();
1662 assert_eq!(meta.origin, Some([16, 32]));
1663 assert!(meta.boxes.is_some());
1664 let boxes = meta.boxes.unwrap();
1665 assert_eq!(boxes.len(), 2);
1666 assert!(boxes.contains_key("hurt"));
1667 assert!(boxes.contains_key("hit"));
1668 let hurt_box = &boxes["hurt"];
1669 assert_eq!(hurt_box.x, 4);
1670 assert_eq!(hurt_box.y, 0);
1671 assert_eq!(hurt_box.w, 24);
1672 assert_eq!(hurt_box.h, 32);
1673 }
1674 _ => panic!("Expected sprite"),
1675 }
1676 }
1677
1678 #[test]
1679 fn test_velocity_range_default() {
1680 let vel = VelocityRange::default();
1681 assert_eq!(vel.x, [0.0, 0.0]);
1682 assert_eq!(vel.y, [0.0, 0.0]);
1683 }
1684
1685 #[test]
1686 fn test_particle_emitter_default() {
1687 let emitter = ParticleEmitter::default();
1688 assert_eq!(emitter.rate, 1.0);
1689 assert_eq!(emitter.lifetime, [10, 20]);
1690 assert!(emitter.velocity.is_none());
1691 assert!(emitter.gravity.is_none());
1692 assert!(emitter.fade.is_none());
1693 assert!(emitter.rotation.is_none());
1694 assert!(emitter.seed.is_none());
1695 }
1696
1697 #[test]
1698 fn test_sprite_metadata_origin_only() {
1699 let json = r#"{
1701 "type": "sprite",
1702 "name": "centered_sprite",
1703 "palette": "default",
1704 "grid": ["{x}"],
1705 "metadata": {
1706 "origin": [8, 16]
1707 }
1708 }"#;
1709 let obj: TtpObject = serde_json::from_str(json).unwrap();
1710 match obj {
1711 TtpObject::Sprite(sprite) => {
1712 let meta = sprite.metadata.unwrap();
1713 assert_eq!(meta.origin, Some([8, 16]));
1714 assert!(meta.boxes.is_none());
1715 }
1716 _ => panic!("Expected sprite"),
1717 }
1718 }
1719
1720 #[test]
1721 fn test_sprite_metadata_boxes_only() {
1722 let json = r#"{
1724 "type": "sprite",
1725 "name": "collider",
1726 "palette": "default",
1727 "grid": ["{x}"],
1728 "metadata": {
1729 "boxes": {
1730 "collide": {"x": 0, "y": 0, "w": 16, "h": 16}
1731 }
1732 }
1733 }"#;
1734 let obj: TtpObject = serde_json::from_str(json).unwrap();
1735 match obj {
1736 TtpObject::Sprite(sprite) => {
1737 let meta = sprite.metadata.unwrap();
1738 assert!(meta.origin.is_none());
1739 assert!(meta.boxes.is_some());
1740 assert!(meta.boxes.unwrap().contains_key("collide"));
1741 }
1742 _ => panic!("Expected sprite"),
1743 }
1744 }
1745
1746 #[test]
1747 fn test_animation_frame_metadata_parse() {
1748 let json = r#"{
1750 "type": "animation",
1751 "name": "attack",
1752 "frames": ["f1", "f2", "f3"],
1753 "frame_metadata": [
1754 {"boxes": {"hit": null}},
1755 {"boxes": {"hit": {"x": 20, "y": 8, "w": 20, "h": 16}}},
1756 {"boxes": {"hit": {"x": 24, "y": 4, "w": 24, "h": 20}}}
1757 ]
1758 }"#;
1759 let obj: TtpObject = serde_json::from_str(json).unwrap();
1760 match obj {
1761 TtpObject::Animation(anim) => {
1762 assert_eq!(anim.name, "attack");
1763 assert_eq!(anim.frames.len(), 3);
1764 assert!(anim.frame_metadata.is_some());
1765 let frame_meta = anim.frame_metadata.unwrap();
1766 assert_eq!(frame_meta.len(), 3);
1767
1768 let f0_boxes = frame_meta[0].boxes.as_ref().unwrap();
1770 assert!(f0_boxes.get("hit").unwrap().is_none());
1771
1772 let f1_boxes = frame_meta[1].boxes.as_ref().unwrap();
1774 let f1_hit = f1_boxes.get("hit").unwrap().as_ref().unwrap();
1775 assert_eq!(f1_hit.x, 20);
1776 assert_eq!(f1_hit.y, 8);
1777
1778 let f2_boxes = frame_meta[2].boxes.as_ref().unwrap();
1780 let f2_hit = f2_boxes.get("hit").unwrap().as_ref().unwrap();
1781 assert_eq!(f2_hit.x, 24);
1782 assert_eq!(f2_hit.w, 24);
1783 }
1784 _ => panic!("Expected animation"),
1785 }
1786 }
1787
1788 #[test]
1789 fn test_sprite_without_metadata_roundtrip() {
1790 let sprite = Sprite {
1792 name: "simple".to_string(),
1793 size: None,
1794 palette: PaletteRef::Named("default".to_string()),
1795 grid: vec!["{x}".to_string()],
1796 metadata: None,
1797 ..Default::default()
1798 };
1799 let json = serde_json::to_string(&sprite).unwrap();
1800 assert!(!json.contains("metadata"));
1802 let parsed: Sprite = serde_json::from_str(&json).unwrap();
1803 assert_eq!(sprite, parsed);
1804 }
1805
1806 #[test]
1807 fn test_animation_without_frame_metadata_roundtrip() {
1808 let anim = Animation {
1810 name: "simple".to_string(),
1811 frames: vec!["f1".to_string()],
1812 duration: None,
1813 r#loop: None,
1814 palette_cycle: None,
1815 tags: None,
1816 frame_metadata: None,
1817 attachments: None,
1818 ..Default::default()
1819 };
1820 let json = serde_json::to_string(&anim).unwrap();
1821 assert!(!json.contains("frame_metadata"));
1823 let parsed: Animation = serde_json::from_str(&json).unwrap();
1824 assert_eq!(anim, parsed);
1825 }
1826
1827 #[test]
1828 fn test_collision_box_negative_coordinates() {
1829 let box_data = CollisionBox { x: -8, y: -16, w: 16, h: 32 };
1831 let json = serde_json::to_string(&box_data).unwrap();
1832 let parsed: CollisionBox = serde_json::from_str(&json).unwrap();
1833 assert_eq!(box_data, parsed);
1834 assert_eq!(parsed.x, -8);
1835 assert_eq!(parsed.y, -16);
1836 }
1837
1838 #[test]
1841 fn test_follow_mode_parse() {
1842 assert_eq!(
1844 serde_json::from_str::<FollowMode>(r#""position""#).unwrap(),
1845 FollowMode::Position
1846 );
1847 assert_eq!(
1848 serde_json::from_str::<FollowMode>(r#""velocity""#).unwrap(),
1849 FollowMode::Velocity
1850 );
1851 assert_eq!(
1852 serde_json::from_str::<FollowMode>(r#""rotation""#).unwrap(),
1853 FollowMode::Rotation
1854 );
1855 }
1856
1857 #[test]
1858 fn test_follow_mode_default() {
1859 assert_eq!(FollowMode::default(), FollowMode::Position);
1861 }
1862
1863 #[test]
1864 fn test_attachment_keyframe_roundtrip() {
1865 let keyframe = AttachmentKeyframe { offset: [5, -3] };
1866 let json = serde_json::to_string(&keyframe).unwrap();
1867 let parsed: AttachmentKeyframe = serde_json::from_str(&json).unwrap();
1868 assert_eq!(keyframe, parsed);
1869 }
1870
1871 #[test]
1872 fn test_attachment_basic_roundtrip() {
1873 let attachment = Attachment {
1874 name: "hair".to_string(),
1875 anchor: [12, 4],
1876 chain: vec!["hair_1".to_string(), "hair_2".to_string()],
1877 delay: None,
1878 follow: None,
1879 damping: None,
1880 stiffness: None,
1881 z_index: None,
1882 keyframes: None,
1883 };
1884 let json = serde_json::to_string(&attachment).unwrap();
1885 let parsed: Attachment = serde_json::from_str(&json).unwrap();
1886 assert_eq!(attachment, parsed);
1887 }
1888
1889 #[test]
1890 fn test_attachment_with_all_fields() {
1891 let attachment = Attachment {
1892 name: "cape".to_string(),
1893 anchor: [8, 8],
1894 chain: vec!["cape_top".to_string(), "cape_mid".to_string(), "cape_bottom".to_string()],
1895 delay: Some(2),
1896 follow: Some(FollowMode::Velocity),
1897 damping: Some(0.7),
1898 stiffness: Some(0.4),
1899 z_index: Some(-1),
1900 keyframes: None,
1901 };
1902 let json = serde_json::to_string(&attachment).unwrap();
1903 let parsed: Attachment = serde_json::from_str(&json).unwrap();
1904 assert_eq!(attachment, parsed);
1905 }
1906
1907 #[test]
1908 fn test_attachment_with_keyframes() {
1909 let mut keyframes = HashMap::new();
1910 keyframes.insert("0".to_string(), AttachmentKeyframe { offset: [0, 0] });
1911 keyframes.insert("1".to_string(), AttachmentKeyframe { offset: [2, 1] });
1912 keyframes.insert("2".to_string(), AttachmentKeyframe { offset: [3, 2] });
1913
1914 let attachment = Attachment {
1915 name: "hair".to_string(),
1916 anchor: [12, 4],
1917 chain: vec!["hair_1".to_string()],
1918 delay: None,
1919 follow: None,
1920 damping: None,
1921 stiffness: None,
1922 z_index: None,
1923 keyframes: Some(keyframes),
1924 };
1925
1926 let json = serde_json::to_string(&attachment).unwrap();
1927 let parsed: Attachment = serde_json::from_str(&json).unwrap();
1928 assert_eq!(attachment, parsed);
1929 assert!(parsed.is_keyframed());
1930 }
1931
1932 #[test]
1933 fn test_attachment_helper_methods() {
1934 let attachment = Attachment {
1936 name: "test".to_string(),
1937 anchor: [0, 0],
1938 chain: vec!["sprite".to_string()],
1939 delay: None,
1940 follow: None,
1941 damping: None,
1942 stiffness: None,
1943 z_index: None,
1944 keyframes: None,
1945 };
1946
1947 assert_eq!(attachment.delay(), 1); assert_eq!(attachment.follow_mode(), FollowMode::Position);
1949 assert!((attachment.damping() - 0.8).abs() < 0.001); assert!((attachment.stiffness() - 0.5).abs() < 0.001); assert_eq!(attachment.z_index(), 0);
1952 assert!(!attachment.is_keyframed());
1953
1954 let attachment_custom = Attachment {
1956 name: "custom".to_string(),
1957 anchor: [0, 0],
1958 chain: vec!["sprite".to_string()],
1959 delay: Some(3),
1960 follow: Some(FollowMode::Velocity),
1961 damping: Some(0.5),
1962 stiffness: Some(0.9),
1963 z_index: Some(-2),
1964 keyframes: None,
1965 };
1966
1967 assert_eq!(attachment_custom.delay(), 3);
1968 assert_eq!(attachment_custom.follow_mode(), FollowMode::Velocity);
1969 assert!((attachment_custom.damping() - 0.5).abs() < 0.001);
1970 assert!((attachment_custom.stiffness() - 0.9).abs() < 0.001);
1971 assert_eq!(attachment_custom.z_index(), -2);
1972 }
1973
1974 #[test]
1975 fn test_animation_with_attachments_parse() {
1976 let json = r#"{
1978 "type": "animation",
1979 "name": "hero_walk",
1980 "frames": ["walk_1", "walk_2", "walk_3", "walk_4"],
1981 "duration": 100,
1982 "attachments": [
1983 {
1984 "name": "hair",
1985 "anchor": [12, 4],
1986 "chain": ["hair_1", "hair_2", "hair_3"],
1987 "delay": 1,
1988 "follow": "position"
1989 },
1990 {
1991 "name": "cape",
1992 "anchor": [8, 8],
1993 "chain": ["cape_top", "cape_mid", "cape_bottom"],
1994 "delay": 2,
1995 "follow": "velocity",
1996 "z_index": -1
1997 }
1998 ]
1999 }"#;
2000 let obj: TtpObject = serde_json::from_str(json).unwrap();
2001 match obj {
2002 TtpObject::Animation(anim) => {
2003 assert_eq!(anim.name, "hero_walk");
2004 assert!(anim.attachments.is_some());
2005 let attachments = anim.attachments.unwrap();
2006 assert_eq!(attachments.len(), 2);
2007
2008 let hair = &attachments[0];
2010 assert_eq!(hair.name, "hair");
2011 assert_eq!(hair.anchor, [12, 4]);
2012 assert_eq!(hair.chain.len(), 3);
2013 assert_eq!(hair.delay(), 1);
2014 assert_eq!(hair.follow_mode(), FollowMode::Position);
2015
2016 let cape = &attachments[1];
2018 assert_eq!(cape.name, "cape");
2019 assert_eq!(cape.anchor, [8, 8]);
2020 assert_eq!(cape.chain.len(), 3);
2021 assert_eq!(cape.delay(), 2);
2022 assert_eq!(cape.follow_mode(), FollowMode::Velocity);
2023 assert_eq!(cape.z_index(), -1);
2024 }
2025 _ => panic!("Expected animation"),
2026 }
2027 }
2028
2029 #[test]
2030 fn test_animation_attachments_roundtrip() {
2031 let anim = Animation {
2032 name: "test_anim".to_string(),
2033 frames: vec!["f1".to_string(), "f2".to_string()],
2034 duration: Some(Duration::Milliseconds(100)),
2035 r#loop: Some(true),
2036 palette_cycle: None,
2037 tags: None,
2038 frame_metadata: None,
2039 attachments: Some(vec![Attachment {
2040 name: "tail".to_string(),
2041 anchor: [4, 8],
2042 chain: vec!["tail_1".to_string(), "tail_2".to_string()],
2043 delay: Some(1),
2044 follow: Some(FollowMode::Position),
2045 damping: Some(0.8),
2046 stiffness: Some(0.5),
2047 z_index: Some(1),
2048 keyframes: None,
2049 }]),
2050 ..Default::default()
2051 };
2052 let obj = TtpObject::Animation(anim.clone());
2053 let json = serde_json::to_string(&obj).unwrap();
2054 assert!(json.contains("attachments"));
2055 let parsed: TtpObject = serde_json::from_str(&json).unwrap();
2056 match parsed {
2057 TtpObject::Animation(parsed_anim) => {
2058 assert_eq!(anim, parsed_anim);
2059 }
2060 _ => panic!("Expected animation"),
2061 }
2062 }
2063
2064 #[test]
2065 fn test_animation_without_attachments_roundtrip() {
2066 let anim = Animation {
2068 name: "simple".to_string(),
2069 frames: vec!["f1".to_string()],
2070 duration: None,
2071 r#loop: None,
2072 palette_cycle: None,
2073 tags: None,
2074 frame_metadata: None,
2075 attachments: None,
2076 ..Default::default()
2077 };
2078 let json = serde_json::to_string(&anim).unwrap();
2079 assert!(!json.contains("attachments"));
2081 let parsed: Animation = serde_json::from_str(&json).unwrap();
2082 assert_eq!(anim, parsed);
2083 }
2084
2085 #[test]
2086 fn test_sprite_metadata_with_attach_points() {
2087 let json = r#"{
2089 "type": "sprite",
2090 "name": "hair_2",
2091 "palette": "character",
2092 "grid": ["{x}"],
2093 "metadata": {
2094 "attach_in": [4, 0],
2095 "attach_out": [4, 8]
2096 }
2097 }"#;
2098 let obj: TtpObject = serde_json::from_str(json).unwrap();
2099 match obj {
2100 TtpObject::Sprite(sprite) => {
2101 assert_eq!(sprite.name, "hair_2");
2102 assert!(sprite.metadata.is_some());
2103 let meta = sprite.metadata.unwrap();
2104 assert_eq!(meta.attach_in, Some([4, 0]));
2105 assert_eq!(meta.attach_out, Some([4, 8]));
2106 }
2107 _ => panic!("Expected sprite"),
2108 }
2109 }
2110
2111 #[test]
2112 fn test_sprite_metadata_attach_points_roundtrip() {
2113 let metadata = SpriteMetadata {
2114 origin: Some([8, 8]),
2115 boxes: None,
2116 attach_in: Some([4, 0]),
2117 attach_out: Some([4, 8]),
2118 };
2119 let json = serde_json::to_string(&metadata).unwrap();
2120 let parsed: SpriteMetadata = serde_json::from_str(&json).unwrap();
2121 assert_eq!(metadata, parsed);
2122 }
2123
2124 #[test]
2125 fn test_attachment_keyframed_parse() {
2126 let json = r#"{
2128 "name": "hair",
2129 "anchor": [12, 4],
2130 "chain": ["hair_1", "hair_2"],
2131 "keyframes": {
2132 "0": {"offset": [0, 0]},
2133 "1": {"offset": [2, 1]},
2134 "2": {"offset": [3, 2]},
2135 "3": {"offset": [1, 1]}
2136 }
2137 }"#;
2138 let attachment: Attachment = serde_json::from_str(json).unwrap();
2139 assert_eq!(attachment.name, "hair");
2140 assert!(attachment.is_keyframed());
2141 let keyframes = attachment.keyframes.unwrap();
2142 assert_eq!(keyframes.len(), 4);
2143 assert_eq!(keyframes.get("0").unwrap().offset, [0, 0]);
2144 assert_eq!(keyframes.get("2").unwrap().offset, [3, 2]);
2145 }
2146
2147 #[test]
2152 fn test_duration_milliseconds_parse() {
2153 let dur: Duration = serde_json::from_str("100").unwrap();
2154 assert_eq!(dur, Duration::Milliseconds(100));
2155 assert_eq!(dur.as_milliseconds(), Some(100));
2156 }
2157
2158 #[test]
2159 fn test_duration_css_string_ms() {
2160 let dur: Duration = serde_json::from_str(r#""500ms""#).unwrap();
2161 assert!(matches!(dur, Duration::CssString(_)));
2162 assert_eq!(dur.as_milliseconds(), Some(500));
2163 }
2164
2165 #[test]
2166 fn test_duration_css_string_seconds() {
2167 let dur: Duration = serde_json::from_str(r#""1.5s""#).unwrap();
2168 assert!(matches!(dur, Duration::CssString(_)));
2169 assert_eq!(dur.as_milliseconds(), Some(1500));
2170 }
2171
2172 #[test]
2173 fn test_duration_display() {
2174 assert_eq!(format!("{}", Duration::Milliseconds(100)), "100");
2175 assert_eq!(format!("{}", Duration::CssString("500ms".to_string())), "\"500ms\"");
2176 }
2177
2178 #[test]
2179 fn test_duration_default() {
2180 let dur = Duration::default();
2181 assert_eq!(dur, Duration::Milliseconds(100));
2182 }
2183
2184 #[test]
2185 fn test_duration_from_u32() {
2186 let dur: Duration = 250u32.into();
2187 assert_eq!(dur, Duration::Milliseconds(250));
2188 }
2189
2190 #[test]
2191 fn test_duration_from_str() {
2192 let dur: Duration = "1s".into();
2193 assert_eq!(dur, Duration::CssString("1s".to_string()));
2194 assert_eq!(dur.as_milliseconds(), Some(1000));
2195 }
2196
2197 #[test]
2202 fn test_css_keyframe_parse_basic() {
2203 let kf: CssKeyframe = serde_json::from_str(r#"{"sprite": "walk_1"}"#).unwrap();
2204 assert_eq!(kf.sprite, Some("walk_1".to_string()));
2205 assert!(kf.transform.is_none());
2206 assert!(kf.opacity.is_none());
2207 assert!(kf.offset.is_none());
2208 }
2209
2210 #[test]
2211 fn test_css_keyframe_parse_full() {
2212 let json = r#"{
2213 "sprite": "walk_1",
2214 "transform": "rotate(45deg) scale(2)",
2215 "opacity": 0.5,
2216 "offset": [10, -5]
2217 }"#;
2218 let kf: CssKeyframe = serde_json::from_str(json).unwrap();
2219 assert_eq!(kf.sprite, Some("walk_1".to_string()));
2220 assert_eq!(kf.transform, Some("rotate(45deg) scale(2)".to_string()));
2221 assert_eq!(kf.opacity, Some(0.5));
2222 assert_eq!(kf.offset, Some([10, -5]));
2223 }
2224
2225 #[test]
2226 fn test_css_keyframe_roundtrip() {
2227 let kf = CssKeyframe {
2228 sprite: Some("test".to_string()),
2229 transform: Some("scale(2)".to_string()),
2230 opacity: Some(0.8),
2231 offset: Some([5, 10]),
2232 };
2233 let json = serde_json::to_string(&kf).unwrap();
2234 let parsed: CssKeyframe = serde_json::from_str(&json).unwrap();
2235 assert_eq!(kf, parsed);
2236 }
2237
2238 #[test]
2243 fn test_animation_css_keyframes_parse() {
2244 let json = r#"{
2245 "type": "animation",
2246 "name": "fade_walk",
2247 "keyframes": {
2248 "0%": {"sprite": "walk_1", "opacity": 0.0},
2249 "50%": {"sprite": "walk_2", "opacity": 1.0},
2250 "100%": {"sprite": "walk_1", "opacity": 0.0}
2251 },
2252 "duration": "500ms",
2253 "timing_function": "ease-in-out"
2254 }"#;
2255 let obj: TtpObject = serde_json::from_str(json).unwrap();
2256 match obj {
2257 TtpObject::Animation(anim) => {
2258 assert_eq!(anim.name, "fade_walk");
2259 assert!(anim.is_css_keyframes());
2260 assert!(!anim.is_frame_based());
2261
2262 let keyframes = anim.css_keyframes().unwrap();
2263 assert_eq!(keyframes.len(), 3);
2264
2265 let kf_0 = keyframes.get("0%").unwrap();
2266 assert_eq!(kf_0.sprite, Some("walk_1".to_string()));
2267 assert_eq!(kf_0.opacity, Some(0.0));
2268
2269 let kf_50 = keyframes.get("50%").unwrap();
2270 assert_eq!(kf_50.sprite, Some("walk_2".to_string()));
2271 assert_eq!(kf_50.opacity, Some(1.0));
2272
2273 assert_eq!(anim.duration_ms(), 500);
2274 assert_eq!(anim.timing_function, Some("ease-in-out".to_string()));
2275 }
2276 _ => panic!("Expected animation"),
2277 }
2278 }
2279
2280 #[test]
2281 fn test_animation_css_keyframes_from_to_aliases() {
2282 let json = r#"{
2283 "type": "animation",
2284 "name": "fade",
2285 "keyframes": {
2286 "from": {"opacity": 0.0},
2287 "to": {"opacity": 1.0}
2288 },
2289 "duration": "1s"
2290 }"#;
2291 let obj: TtpObject = serde_json::from_str(json).unwrap();
2292 match obj {
2293 TtpObject::Animation(anim) => {
2294 assert!(anim.is_css_keyframes());
2295 let keyframes = anim.css_keyframes().unwrap();
2296 assert!(keyframes.contains_key("from"));
2297 assert!(keyframes.contains_key("to"));
2298 assert_eq!(anim.duration_ms(), 1000);
2299 }
2300 _ => panic!("Expected animation"),
2301 }
2302 }
2303
2304 #[test]
2305 fn test_animation_parse_keyframe_percent() {
2306 assert_eq!(Animation::parse_keyframe_percent("0%"), Some(0.0));
2308 assert_eq!(Animation::parse_keyframe_percent("50%"), Some(0.5));
2309 assert_eq!(Animation::parse_keyframe_percent("100%"), Some(1.0));
2310 assert_eq!(Animation::parse_keyframe_percent("25%"), Some(0.25));
2311
2312 assert_eq!(Animation::parse_keyframe_percent("from"), Some(0.0));
2314 assert_eq!(Animation::parse_keyframe_percent("to"), Some(1.0));
2315
2316 assert_eq!(Animation::parse_keyframe_percent("FROM"), Some(0.0));
2318 assert_eq!(Animation::parse_keyframe_percent("TO"), Some(1.0));
2319
2320 assert_eq!(Animation::parse_keyframe_percent("invalid"), None);
2322 assert_eq!(Animation::parse_keyframe_percent("50"), None);
2323 }
2324
2325 #[test]
2326 fn test_animation_sorted_keyframes() {
2327 let anim = Animation {
2328 name: "test".to_string(),
2329 keyframes: Some(HashMap::from([
2330 (
2331 "100%".to_string(),
2332 CssKeyframe { sprite: Some("c".to_string()), ..Default::default() },
2333 ),
2334 (
2335 "0%".to_string(),
2336 CssKeyframe { sprite: Some("a".to_string()), ..Default::default() },
2337 ),
2338 (
2339 "50%".to_string(),
2340 CssKeyframe { sprite: Some("b".to_string()), ..Default::default() },
2341 ),
2342 ])),
2343 ..Default::default()
2344 };
2345
2346 let sorted = anim.sorted_keyframes();
2347 assert_eq!(sorted.len(), 3);
2348 assert_eq!(sorted[0].0, 0.0);
2349 assert_eq!(sorted[0].1.sprite, Some("a".to_string()));
2350 assert_eq!(sorted[1].0, 0.5);
2351 assert_eq!(sorted[1].1.sprite, Some("b".to_string()));
2352 assert_eq!(sorted[2].0, 1.0);
2353 assert_eq!(sorted[2].1.sprite, Some("c".to_string()));
2354 }
2355
2356 #[test]
2357 fn test_animation_css_keyframes_with_transforms() {
2358 let json = r#"{
2359 "type": "animation",
2360 "name": "spin",
2361 "keyframes": {
2362 "0%": {"sprite": "star", "transform": "rotate(0deg)"},
2363 "100%": {"sprite": "star", "transform": "rotate(360deg)"}
2364 },
2365 "duration": 1000,
2366 "timing_function": "linear"
2367 }"#;
2368 let obj: TtpObject = serde_json::from_str(json).unwrap();
2369 match obj {
2370 TtpObject::Animation(anim) => {
2371 let keyframes = anim.css_keyframes().unwrap();
2372 assert_eq!(
2373 keyframes.get("0%").unwrap().transform,
2374 Some("rotate(0deg)".to_string())
2375 );
2376 assert_eq!(
2377 keyframes.get("100%").unwrap().transform,
2378 Some("rotate(360deg)".to_string())
2379 );
2380 assert_eq!(anim.timing_function, Some("linear".to_string()));
2381 }
2382 _ => panic!("Expected animation"),
2383 }
2384 }
2385
2386 #[test]
2387 fn test_animation_frame_vs_keyframe() {
2388 let frame_anim = Animation {
2390 name: "frames".to_string(),
2391 frames: vec!["f1".to_string(), "f2".to_string()],
2392 ..Default::default()
2393 };
2394 assert!(frame_anim.is_frame_based());
2395 assert!(!frame_anim.is_css_keyframes());
2396
2397 let keyframe_anim = Animation {
2399 name: "keyframes".to_string(),
2400 keyframes: Some(HashMap::from([
2401 ("0%".to_string(), CssKeyframe::default()),
2402 ("100%".to_string(), CssKeyframe::default()),
2403 ])),
2404 ..Default::default()
2405 };
2406 assert!(!keyframe_anim.is_frame_based());
2407 assert!(keyframe_anim.is_css_keyframes());
2408 }
2409
2410 #[test]
2411 fn test_animation_css_keyframes_roundtrip() {
2412 let anim = Animation {
2413 name: "test_kf".to_string(),
2414 keyframes: Some(HashMap::from([
2415 (
2416 "0%".to_string(),
2417 CssKeyframe {
2418 sprite: Some("start".to_string()),
2419 opacity: Some(0.0),
2420 ..Default::default()
2421 },
2422 ),
2423 (
2424 "100%".to_string(),
2425 CssKeyframe {
2426 sprite: Some("end".to_string()),
2427 opacity: Some(1.0),
2428 ..Default::default()
2429 },
2430 ),
2431 ])),
2432 duration: Some(Duration::CssString("500ms".to_string())),
2433 timing_function: Some("ease".to_string()),
2434 ..Default::default()
2435 };
2436
2437 let obj = TtpObject::Animation(anim.clone());
2438 let json = serde_json::to_string(&obj).unwrap();
2439 assert!(json.contains("keyframes"));
2440 assert!(json.contains("timing_function"));
2441
2442 let parsed: TtpObject = serde_json::from_str(&json).unwrap();
2443 match parsed {
2444 TtpObject::Animation(parsed_anim) => {
2445 assert_eq!(anim.name, parsed_anim.name);
2446 assert_eq!(anim.timing_function, parsed_anim.timing_function);
2447 assert!(parsed_anim.is_css_keyframes());
2448 }
2449 _ => panic!("Expected animation"),
2450 }
2451 }
2452}