Skip to main content

lean_ctx/core/buddy/
sprite.rs

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}