A mood as a whole picture: the shadow, the cat breathing, and what is round it. The cat
itself is a theme's (grey.rs here); everything else is the same for every theme.
4use whiskers_pet::Mood;
The drawing area, in grid units: x, y, width and height. It runs from (-3, -4) so that bubbles, stars and sound waves fit round the body.
12pub const VIEW: (f32, f32, f32, f32) = (-3.0, -4.0, 21.0, 20.0);
The cat's middle and half-width in grid units, which touch is measured in.
How far the pupils travel for a gaze of 1 on each axis, and the step they snap to.
Every mood the core has, in the order the core lists them.
Where the pupils go for a gaze (each axis -1..1), snapped as the app snaps them.
The shape of the mouth while speaking, as the voice's clock gives it. Without one the mouth flaps on a beat.
A cat's body (the standing one for every mood but sleeping, the flat one for sleeping) as a whole picture: shadow, breathing, swaying or shivering, and what is round it.
59pub fn assemble(mood: Mood, body: Vec<Node>) -> Vec<Node> { 60 if mood == Mood::Sleeping { 61 let mut nodes = vec![ra(-1.0, 15.0, 17.0, 1.0, INK, 0.5), moving(Motion::BreatheAsleep, (7.5, 15.0), body)]; 62 nodes.extend(decor::zs()); 63 return nodes; 64 } 65 let mut body = moving(Motion::Breathe, (7.5, 13.0), body); 66 match mood { 67 Mood::Dizzy => body = moving(Motion::Sway, (0.0, 0.0), vec![moving(Motion::Tilt, (7.5, 13.0), vec![body])]), 68 Mood::Tickled => body = moving(Motion::Shiver, (0.0, 0.0), vec![body]), 69 _ => {} 70 } 71 let mut nodes = vec![ra(3.0, 15.0, 9.0, 1.0, INK, 0.5), body]; 72 nodes.extend(decor::around(mood)); 73 nodes 74}
The grey cat in mood. mouth matters only while speaking.
82#[cfg(test)] 83mod tests { 84 use super::*; 85 use crate::node::{Group, Node}; 86 87 fn walk(nodes: &[Node], f: &mut impl FnMut(&Node)) { 88 for node in nodes { 89 f(node); 90 if let Node::Group(Group { children, .. }) = node { 91 walk(children, f); 92 } 93 } 94 } 95 96 #[test] 97 fn every_mood_has_a_scene_that_stays_inside_the_drawing_area() { 98 let (x0, y0, w, h) = VIEW; 99 for mood in MOODS { 100 let nodes = scene(mood, None); 101 assert!(!nodes.is_empty()); 102 walk(&nodes, &mut |node| { 103 if let Node::Rect(r) = node { 104 // Orbiting stars, rising z's, balls and a wagging tail move; allow their travel. 105 let slack = 6.0; 106 assert!(r.x >= x0 - slack && r.x + r.w <= x0 + w + slack, "{mood:?} {r:?}"); 107 assert!(r.y >= y0 - slack && r.y + r.h <= y0 + h + slack, "{mood:?} {r:?}"); 108 } 109 }); 110 } 111 } 112 113 #[test] 114 fn the_moods_listed_are_the_cores_moods() { 115 // An exhaustive match: a mood added to the core stops this compiling until it is drawn. 116 for mood in MOODS { 117 match mood { 118 Mood::Idle | Mood::Listening | Mood::Thinking | Mood::Working | Mood::Speaking | Mood::Tangled | Mood::Busy | Mood::Offline | Mood::Dizzy | Mood::Tickled | Mood::Sleeping => {} 119 } 120 } 121 let mut seen = MOODS.to_vec(); 122 seen.dedup(); 123 assert_eq!(seen.len(), 11); 124 } 125 126 fn tops(mood: Mood) -> f32 { 127 let mut top = f32::MAX; 128 walk(&scene(mood, None), &mut |node| { 129 if let Node::Rect(r) = node { 130 if r.color != INK && r.alpha == 1.0 && r.w * r.h > 4.0 { 131 top = top.min(r.y); 132 } 133 } 134 }); 135 top 136 } 137 138 #[test] 139 fn a_sleeping_cat_lies_flat_and_the_others_stand() { 140 assert!(tops(Mood::Sleeping) > tops(Mood::Idle)); 141 } 142 143 fn uses(mood: Mood, motion: Motion) -> bool { 144 let mut found = false; 145 walk(&scene(mood, None), &mut |node| found |= matches!(node, Node::Group(g) if g.motion == motion)); 146 found 147 } 148 149 #[test] 150 fn only_the_dizzy_cat_sways_and_only_the_tickled_one_shivers() { 151 for mood in MOODS { 152 assert_eq!(uses(mood, Motion::Sway), mood == Mood::Dizzy, "{mood:?}"); 153 assert_eq!(uses(mood, Motion::Shiver), mood == Mood::Tickled, "{mood:?}"); 154 } 155 } 156 157 #[test] 158 fn the_gaze_snaps_to_half_units_as_the_app_does() { 159 assert_eq!(gaze_offset((0.0, 0.0)), (0.0, 0.0)); 160 assert_eq!(gaze_offset((1.0, 1.0)), (1.5, 1.0)); 161 assert_eq!(gaze_offset((-1.0, -1.0)), (-1.5, -1.0)); 162 // Thinking looks up and to the right by (1.0, -1.0) (0.8 * 1.5 snaps to 1.0). 163 assert_eq!(gaze_offset((0.8, -0.8)), (1.0, -1.0)); 164 } 165 166 #[test] 167 fn a_speaking_mouth_is_flapping_only_without_a_shape() { 168 assert!(uses(Mood::Speaking, Motion::MouthFlap)); 169 for mouth in Mouth::ALL { 170 let mut found = false; 171 walk(&scene(Mood::Speaking, Some(mouth)), &mut |node| found |= matches!(node, Node::Group(g) if g.motion == Motion::MouthFlap)); 172 assert!(!found, "{mouth:?}"); 173 } 174 } 175 176 #[test] 177 fn only_a_speaking_cat_cares_about_its_mouth() { 178 for mood in MOODS.into_iter().filter(|m| *m != Mood::Speaking) { 179 assert_eq!(scene(mood, None), scene(mood, Some(Mouth::Wide)), "{mood:?}"); 180 } 181 assert_ne!(scene(Mood::Speaking, Some(Mouth::Closed)), scene(Mood::Speaking, Some(Mouth::Wide))); 182 } 183}