1use super::types::{BuddyState, CreatureTraits, Mood};
2
3pub(super) struct SpritePack {
4 pub(super) base: Vec<String>,
5 pub(super) frames: Vec<Vec<String>>,
6 pub(super) anim_ms: Option<u32>,
7}
8
9pub(super) fn sprite_tier(level: u32) -> u8 {
10 if level >= 75 {
11 4
12 } else if level >= 50 {
13 3
14 } else if level >= 25 {
15 2
16 } else {
17 u8::from(level >= 10)
18 }
19}
20
21fn tier_anim_ms(tier: u8) -> Option<u32> {
22 match tier {
23 0 => None,
24 1 => Some(950),
25 2 => Some(700),
26 3 => Some(520),
27 _ => Some(380),
28 }
29}
30
31pub(super) fn render_sprite_pack(traits: &CreatureTraits, mood: &Mood, level: u32) -> SpritePack {
32 let base = render_sprite(traits, mood);
33 let tier = sprite_tier(level);
34 if tier == 0 {
35 return SpritePack {
36 base,
37 frames: Vec::new(),
38 anim_ms: None,
39 };
40 }
41
42 let mut frames = Vec::new();
43 frames.push(base.clone());
44
45 let blink = match mood {
46 Mood::Sleeping => ("u", "u"),
47 _ => (".", "."),
48 };
49 frames.push(render_sprite_with_eyes(traits, mood, blink.0, blink.1));
50
51 if tier >= 2 {
52 let mut s = base.clone();
53 if let Some(l0) = s.get_mut(0) {
54 *l0 = sparkle_edges(l0, '*', '+');
55 }
56 frames.push(s);
57 }
58 if tier >= 3 {
59 let mut s = base.clone();
60 for line in &mut s {
61 *line = shift(line, 1);
62 }
63 frames.push(s);
64 }
65 if tier >= 4 {
66 let mut s = base.clone();
67 for (i, line) in s.iter_mut().enumerate() {
68 let (l, r) = if i % 2 == 0 { ('+', '+') } else { ('*', '*') };
69 *line = edge_aura(line, l, r);
70 }
71 frames.push(s);
72 }
73
74 SpritePack {
75 base,
76 frames,
77 anim_ms: tier_anim_ms(tier),
78 }
79}
80
81fn render_sprite_with_eyes(
82 traits: &CreatureTraits,
83 _mood: &Mood,
84 el: &str,
85 er: &str,
86) -> Vec<String> {
87 let ears = ear_part(traits.ears);
88 let head_top = head_top_part(traits.head);
89 let face = face_line(traits.head, traits.eyes, el, er);
90 let mouth = mouth_line(traits.head, traits.mouth);
91 let neck = neck_part(traits.head);
92 let body = body_part(traits.body, traits.markings);
93 let feet = leg_part(traits.legs, traits.tail);
94
95 vec![
96 pad(&ears),
97 pad(&head_top),
98 pad(&face),
99 pad(&mouth),
100 pad(&neck),
101 pad(&body),
102 pad(&feet),
103 ]
104}
105
106pub(super) fn sparkle_edges(line: &str, left: char, right: char) -> String {
107 let s = pad(line);
108 let mut chars: Vec<char> = s.chars().collect();
109 if chars.len() >= 2 {
110 chars[0] = left;
111 let last = chars.len() - 1;
112 chars[last] = right;
113 }
114 chars.into_iter().collect()
115}
116
117pub(super) fn edge_aura(line: &str, left: char, right: char) -> String {
118 let s = pad(line);
119 let mut chars: Vec<char> = s.chars().collect();
120 if chars.len() >= 2 {
121 chars[0] = left;
122 let last = chars.len() - 1;
123 chars[last] = right;
124 }
125 chars.into_iter().collect()
126}
127
128pub(super) fn shift(line: &str, offset: i32) -> String {
129 if offset == 0 {
130 return pad(line);
131 }
132 let s = pad(line);
133 let mut chars: Vec<char> = s.chars().collect();
134 if chars.is_empty() {
135 return s;
136 }
137 if offset > 0 {
138 for _ in 0..offset {
139 chars.insert(0, ' ');
140 chars.pop();
141 }
142 } else {
143 for _ in 0..(-offset) {
144 chars.remove(0);
145 chars.push(' ');
146 }
147 }
148 chars.into_iter().collect()
149}
150
151pub(super) fn sprite_lines_for_tick(state: &BuddyState, tick: Option<u64>) -> &[String] {
152 if let Some(t) = tick {
153 if !state.ascii_frames.is_empty() {
154 let idx = (t as usize) % state.ascii_frames.len();
155 return &state.ascii_frames[idx];
156 }
157 }
158 &state.ascii_art
159}
160
161const W: usize = 20;
162
163fn pad(s: &str) -> String {
164 let len = s.chars().count();
165 if len >= W {
166 s.chars().take(W).collect()
167 } else {
168 let left = (W - len) / 2;
169 let right = W - len - left;
170 format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
171 }
172}
173
174pub fn render_sprite(traits: &CreatureTraits, mood: &Mood) -> Vec<String> {
175 let (el, er) = mood_eyes(mood);
176 let ears = ear_part(traits.ears);
177 let head_top = head_top_part(traits.head);
178 let face = face_line(traits.head, traits.eyes, el, er);
179 let mouth = mouth_line(traits.head, traits.mouth);
180 let neck = neck_part(traits.head);
181 let body = body_part(traits.body, traits.markings);
182 let feet = leg_part(traits.legs, traits.tail);
183
184 vec![
185 pad(&ears),
186 pad(&head_top),
187 pad(&face),
188 pad(&mouth),
189 pad(&neck),
190 pad(&body),
191 pad(&feet),
192 ]
193}
194
195pub(super) fn mood_eyes(mood: &Mood) -> (&'static str, &'static str) {
196 match mood {
197 Mood::Ecstatic => ("*", "*"),
198 Mood::Happy => ("o", "o"),
199 Mood::Content => ("-", "-"),
200 Mood::Worried => (">", "<"),
201 Mood::Sleeping => ("u", "u"),
202 }
203}
204
205fn ear_part(idx: u8) -> String {
206 match idx % 12 {
207 0 => r" /\ /\".into(),
208 1 => r" / \ / \".into(),
209 2 => r" () ()".into(),
210 3 => r" || ||".into(),
211 4 => r" ~' '~".into(),
212 5 => r" >> <<".into(),
213 6 => r" ** **".into(),
214 7 => r" .' '.".into(),
215 8 => r" ~~ ~~".into(),
216 9 => r" ^^ ^^".into(),
217 10 => r" {} {}".into(),
218 _ => r" <> <>".into(),
219 }
220}
221
222fn head_top_part(idx: u8) -> String {
223 match idx % 12 {
224 0 => " .--------. ".into(),
225 1 => " +--------+ ".into(),
226 2 => " /--------\\ ".into(),
227 3 => " .========. ".into(),
228 4 => " (--------) ".into(),
229 5 => " .~~~~~~~~. ".into(),
230 6 => " /~~~~~~~~\\ ".into(),
231 7 => " {--------} ".into(),
232 8 => " <--------> ".into(),
233 9 => " .'^----^'. ".into(),
234 10 => " /********\\ ".into(),
235 _ => " (________) ".into(),
236 }
237}
238
239fn head_bracket(head: u8) -> (char, char) {
240 match head % 12 {
241 0 | 1 | 3 | 5 => ('|', '|'),
242 2 | 6 | 10 => ('/', '\\'),
243 7 => ('{', '}'),
244 8 => ('<', '>'),
245 _ => ('(', ')'),
246 }
247}
248
249fn face_line(head: u8, eye_idx: u8, el: &str, er: &str) -> String {
250 let (bl, br) = head_bracket(head);
251 let deco = match eye_idx % 10 {
252 1 => ("'", "'"),
253 2 => (".", "."),
254 3 => ("~", "~"),
255 4 => ("*", "*"),
256 5 => ("`", "`"),
257 6 => ("^", "^"),
258 7 => (",", ","),
259 8 => (":", ":"),
260 _ => (" ", " "),
261 };
262 format!(" {bl} {}{el} {er}{} {br} ", deco.0, deco.1)
263}
264
265fn mouth_line(head: u8, mouth: u8) -> String {
266 let (bl, br) = head_bracket(head);
267 let m = match mouth % 10 {
268 0 => " \\_/ ",
269 1 => " w ",
270 2 => " ^ ",
271 3 => " ~ ",
272 4 => " === ",
273 5 => " o ",
274 6 => " 3 ",
275 7 => " v ",
276 8 => " --- ",
277 _ => " U ",
278 };
279 format!(" {bl} {m} {br} ")
280}
281
282fn neck_part(head: u8) -> String {
283 match head % 12 {
284 0 => " '--------' ".into(),
285 1 => " +--------+ ".into(),
286 2 => " \\--------/ ".into(),
287 3 => " '========' ".into(),
288 4 => " (--------) ".into(),
289 5 => " '~~~~~~~~' ".into(),
290 6 => " \\~~~~~~~~/ ".into(),
291 7 => " {--------} ".into(),
292 8 => " <--------> ".into(),
293 9 => " '.^----^.' ".into(),
294 10 => " \\********/ ".into(),
295 _ => " (__________) ".into(),
296 }
297}
298
299fn body_part(body: u8, markings: u8) -> String {
300 let fill = match markings % 6 {
301 0 => " ",
302 1 => " |||| ",
303 2 => " .... ",
304 3 => " >><< ",
305 4 => " ~~~~ ",
306 _ => " :::: ",
307 };
308 match body % 10 {
309 0 | 8 => format!(" /{fill}\\ "),
310 1 | 7 => format!(" |{fill}| "),
311 2 => format!(" ({fill}) "),
312 3 => format!(" [{fill}] "),
313 4 => format!(" ~{fill}~ "),
314 5 => format!(" <{fill}> "),
315 6 => format!(" {{{fill}}} "),
316 _ => format!(" _{fill}_ "),
317 }
318}
319
320pub(super) fn leg_part(legs: u8, tail: u8) -> String {
321 let t = match tail % 8 {
322 0 => ' ',
323 1 => '~',
324 2 => '>',
325 3 => ')',
326 4 => '^',
327 5 => '*',
328 6 => '=',
329 _ => '/',
330 };
331 let base = match legs % 10 {
332 0 => " /| |\\",
333 1 => " ~~ ~~",
334 2 => "_/| |\\_",
335 3 => " || ||",
336 4 => " /\\ /\\",
337 5 => " <> <>",
338 6 => " () ()",
339 7 => " }{ }{",
340 8 => " // \\\\",
341 _ => " \\/ \\/",
342 };
343 if t == ' ' {
344 pad(base)
345 } else {
346 pad(&format!("{base} {t}"))
347 }
348}