data.rsannotateddata.rssource205 lines · 8.4 KB · raw
1//! The art as one text file the app reads (`android/app/src/main/resources/art.txt`, written by
2//! `bin/gen-art`, a build output): every motion as its frames, and every character in every mood (and every
3//! shape of the mouth) as the rectangles and groups it is, with the parts that repeat (the shadow, the weather)
4//! written once. The app has one interpreter for it (`art/ArtData.kt`) and no drawing or formula of its own, so a
5//! character added here is in the app at the next build with no Kotlin written.
6//!
7//! Format, one item to a line, numbers as Rust prints them (they read back exactly):
8//!
9//! ```text
10//! characters ID ID ...                    every character, in the order the chooser steps through them
11//! moods NAME ...                          the core's moods, and levels NAME ...: the four shapes of a mouth
12//! grid view=X,Y,W,H center=X,Y half=W gaze=RX,RY,STEP box=X,Y,SIDE   the drawing area, the middle touch is measured from, how far the pupils go, and the square a body stands in
13//! vowels all=aeiouy wide=a round=ou open_until=0.7   the vowels, which opens the mouth how, and how long it stays open in a gap
14//! track NAME PERIOD STEPS(0|1) FRAMES      then FRAMES lines: PERCENT TX TY ROTATE SX SY OPACITY
15//! def N                                    nodes, then `end`: a part a scene uses
16//! scene CHARACTER MOOD SHAPE               nodes, then `end`: SHAPE is `-`, `flap` or a level
17//! nodes:  r X Y W H RRGGBB [ALPHA]  |  g TRACK OX OY  ...  e  |  eyes follow|fixed GX GY  ...  e
18//!         slot  ...  e  |  u N
19//! ```
20
21use std::collections::{BTreeMap, HashMap};
22use std::fmt::Write;
23
24use whiskers_pet::Mood;
25
26use crate::character::Character;
27use crate::motion::Motion;
28use crate::node::{Color, Group, Look, Node, Role};
29use crate::scene::{MOODS, Mouth};
30
31fn f(v: f32) -> String {
32    format!("{v:?}")
33}
34
35fn rgb(c: Color) -> String {
36    format!("{:02x}{:02x}{:02x}", c.0, c.1, c.2)
37}
38
39fn node(n: &Node, out: &mut String, motions: &mut Vec<Motion>) {
40    match n {
41        Node::Rect(r) => {
42            let alpha = if r.alpha == 1.0 { String::new() } else { format!(" {}", f(r.alpha)) };
43            let _ = writeln!(out, "r {} {} {} {} {}{alpha}", f(r.x), f(r.y), f(r.w), f(r.h), rgb(r.color));
44        }
45        Node::Group(g) => group(g, out, motions),
46        Node::Slot(over) => {
47            out.push_str("slot\n");
48            for child in over {
49                node(child, out, motions);
50            }
51            out.push_str("e\n");
52        }
53    }
54}
55
56fn group(g: &Group, out: &mut String, motions: &mut Vec<Motion>) {
57    match g.role {
58        Role::Eyes(Look::Follow) => out.push_str("eyes follow\n"),
59        Role::Eyes(Look::Fixed(x, y)) => {
60            let _ = writeln!(out, "eyes fixed {} {}", f(x), f(y));
61        }
62        Role::Plain => {
63            if !motions.contains(&g.motion) {
64                motions.push(g.motion);
65            }
66            let _ = writeln!(out, "g {} {} {}", g.motion.name(), f(g.origin.0), f(g.origin.1));
67        }
68    }
69    for child in &g.children {
70        node(child, out, motions);
71    }
72    out.push_str("e\n");
73}
74
75/// The shapes a mood is written in: the mouth's four levels and its beat, for speaking; one for the rest.
76fn variants(mood: Mood) -> Vec<(&'static str, Option<Mouth>)> {
77    if mood == Mood::Speaking {
78        let mut v = vec![("flap", None)];
79        v.extend(Mouth::ALL.map(|m| (level_name(m), Some(m))));
80        v
81    } else {
82        vec![("-", None)]
83    }
84}
85
86/// The name a level has in the file and in the app's `MouthShape`.
87pub fn level_name(m: Mouth) -> &'static str {
88    match m {
89        Mouth::Closed => "Closed",
90        Mouth::Small => "Small",
91        Mouth::Wide => "Wide",
92        Mouth::Round => "Round",
93    }
94}
95
96/// The whole file.
97pub fn generate() -> String {
98    let mut motions: Vec<Motion> = Vec::new();
99    // Each scene's top-level parts as text; a part that two scenes share is written once as a `def`.
100    let mut scenes: Vec<(String, Vec<String>)> = Vec::new();
101    for character in Character::all() {
102        for mood in MOODS {
103            for (shape, mouth) in variants(mood) {
104                let parts: Vec<String> = character
105                    .scene(mood, mouth)
106                    .iter()
107                    .map(|n| {
108                        let mut s = String::new();
109                        node(n, &mut s, &mut motions);
110                        s
111                    })
112                    .collect();
113                scenes.push((format!("scene {} {mood:?} {shape}", character.id()), parts));
114            }
115        }
116    }
117    let mut uses: HashMap<&str, usize> = HashMap::new();
118    for (_, parts) in &scenes {
119        for p in parts {
120            *uses.entry(p.as_str()).or_default() += 1;
121        }
122    }
123    let mut ids: BTreeMap<&str, usize> = BTreeMap::new();
124    let mut defs = String::new();
125    for (_, parts) in &scenes {
126        for p in parts {
127            if uses[p.as_str()] > 1 && !ids.contains_key(p.as_str()) {
128                let id = ids.len();
129                ids.insert(p.as_str(), id);
130                let _ = write!(defs, "def {id}\n{p}end\n");
131            }
132        }
133    }
134    let mut out = String::new();
135    out.push_str("# GENERATED by `cargo run -p whiskers-art --bin gen-art` from crates/whiskers-art. Do not edit.\n");
136    let names: Vec<&str> = Character::all().iter().map(|c| c.id()).collect();
137    let _ = writeln!(out, "characters {}", names.join(" "));
138    let moods: Vec<String> = MOODS.iter().map(|m| format!("{m:?}")).collect();
139    let _ = writeln!(out, "moods {}", moods.join(" "));
140    let levels: Vec<&str> = Mouth::ALL.iter().map(|m| level_name(*m)).collect();
141    let _ = writeln!(out, "levels {}", levels.join(" "));
142    let (vx, vy, vw, vh) = crate::scene::VIEW;
143    let _ = writeln!(
144        out,
145        "grid view={},{},{},{} center={},{} half={} gaze={},{},{} box={},{},{}",
146        f(vx), f(vy), f(vw), f(vh),
147        f(crate::scene::CENTER.0), f(crate::scene::CENTER.1),
148        f(crate::scene::HALF_WIDTH),
149        f(crate::scene::GAZE_REACH.0), f(crate::scene::GAZE_REACH.1), f(crate::scene::GAZE_STEP),
150        f(crate::body::BOX.0), f(crate::body::BOX.1), f(crate::body::BOX.2),
151    );
152    let _ = writeln!(out, "vowels all={} wide={} round={} open_until={}", pixel_mouth::VOWELS, pixel_mouth::WIDE_VOWELS, pixel_mouth::ROUND_VOWELS, f(pixel_mouth::OPEN_UNTIL));
153    motions.sort_by_key(|m| m.name());
154    for m in &motions {
155        let t = m.played();
156        let _ = writeln!(out, "track {} {} {} {}", m.name(), f(t.period), u8::from(t.steps), t.frames.len());
157        for (percent, xf) in &t.frames {
158            let _ = writeln!(out, "{} {} {} {} {} {} {}", f(*percent), f(xf.tx), f(xf.ty), f(xf.rotate), f(xf.sx), f(xf.sy), f(xf.opacity));
159        }
160    }
161    out.push_str(&defs);
162    for (head, parts) in &scenes {
163        let _ = writeln!(out, "{head}");
164        for p in parts {
165            match ids.get(p.as_str()) {
166                Some(id) => {
167                    let _ = writeln!(out, "u {id}");
168                }
169                None => out.push_str(p),
170            }
171        }
172        out.push_str("end\n");
173    }
174    out
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn every_character_mood_and_shape_is_written_and_every_group_is_closed_and_has_its_track() {
183        let text = generate();
184        let heads = text.lines().filter(|l| l.starts_with("scene ")).count();
185        assert_eq!(heads, 13 * (10 + 5));
186        let opens = text.lines().filter(|l| l.starts_with("g ") || l.starts_with("eyes ") || *l == "slot").count();
187        let closes = text.lines().filter(|l| *l == "e").count();
188        assert_eq!(opens, closes);
189        let tracks: Vec<&str> = text.lines().filter_map(|l| l.strip_prefix("track ")).filter_map(|l| l.split(' ').next()).collect();
190        for line in text.lines().filter_map(|l| l.strip_prefix("g ")) {
191            assert!(tracks.contains(&line.split(' ').next().unwrap()), "{line}");
192        }
193        assert!(!text.contains("NaN") && !text.contains("inf"));
194    }
195
196    #[test]
197    fn a_part_two_scenes_share_is_written_once() {
198        let text = generate();
199        assert!(text.lines().filter(|l| l.starts_with("def ")).count() > 10);
200        // The 13 characters' idle scenes all start with the same shadow, as a use of one def.
201        let shadow = text.lines().find(|l| l.starts_with("r 3.0 15.0 9.0 1.0 000000")).map(|_| ()).is_some();
202        assert!(shadow, "the shadow is written once, inside a def");
203        assert!(text.len() < 400_000, "{} bytes", text.len());
204    }
205}